Skip to content

Instantly share code, notes, and snippets.

@munificent
Last active October 24, 2023 18:46
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save munificent/b4373df431b7a07a6c7a74bff857cedd to your computer and use it in GitHub Desktop.
Save munificent/b4373df431b7a07a6c7a74bff857cedd to your computer and use it in GitHub Desktop.
Thoughts in a relative and logical import syntax for Wren

So we need some syntax to distinguish between a relative import and a logical import. I'm not sure which way to go, and I'd like some feedback (or possibly other alternate ideas I haven't considered).

My two favorites are:

// Use
use "relative/path"
import "logical/path"

// Node-style
import "./relative/path"
import "logical/path"

If you folks are OK with "use", that's my preference. But otherwise, the Node style will definitely work too. I'm open to other ideas as well, including a few below, but I'd like to not bikeshed this forever.

Background

There are four general approaches we can take:

Use a modifier ("modifier")

Both kinds of imports start with import, but then we use a second keyword afterwards to identify either a relative or logical import. We could use two keywords -- one for each kind -- but that's unnecessarily verbose. Instead, we use the presence or absence of the keyword to distinguish. In other words:

import foo "string"
import "string"

The specific questions we have to answer are:

  1. Which kind of import gets the keyword? Ideally, the most common kind of import would be the one that doesn't need an extra keyword.

  2. What keyword? This is surprisingly hard. Probably some kind of preposition.

Use different keywords ("keyword")

Instead of using import for both logical and relative imports, we could have two keywords, one for each kind. The specific questions to answer then are:

  1. Which kind of import gets import?
  2. What's the other keyword?

Use different syntax for the path ("syntax")

Instead of always using a string literal to identify what's being imported, we could use a different kind of token or tokens for the different kinds of import. For example, a string literal for one kind, and an identifier token for the other:

The specific questions are:

  1. Which kind of import uses a string literal?
  2. What's the syntax for the other kind?

Use a signifier in the import string itself to distinguish ("string")

An import is always import followed by a string literal. Then we use some specific markers inside the string literal itself to distinguish the two kinds. For example, Node says that an import string starting with "./" or "../" is relative and other import strings are logical.

The specific question to answer is what kind of signifier we'd use. I think Node's convention is the only real contender here, though.

One feature this style has that none of the others do is that it means the language syntax itself has no notion of logical and relative imports. This means there is no overhead or complexity for host applications where that distinction isn't meaningful.

Contenders

These are options I'm open to, in roughly descending order of preference:

Use (keyword)

The use keyword is for relative imports, import is for logical.

use "relative/path"
import "logical/path"

The use keyword comes from Pascal, but that's not very widely known. I kind of like this. It's short, and use feels "nearer" to me than "import" so it has the right connotation. (You can't "use" something unless you have it near to hand.)

It adds a little complexity to the language and VM. We have to support both keywords and pass that "use versus import" bit through the name resolution process. But that's pretty minor.

Node-style (string)

If the string starts with "./" or "../", it's relative.

import "./relative/path"
import "logical/path"

This is how Node works, so there's prior art. It keeps the language completely simple. It does feel sort of arbitrary and magical to me, but it's the simplest, most expedient solution.

Slashes (syntax)

If the path is a string literal, it's relative. Otherwise, it is a slash-separated series of unquoted identifiers.

import "relative/path"
import logical/path

This means you can't (easily) use reserved words as names of logical imports. This was my initial pitch. I still like how it looks, but I seem to be in the minority.

Relative (modifier)

The relative modifier is for relative imports.

import relative "relative/path"
import "logical/path"

It's explicit, which is good. It is unfortunately verbose. I think relative is too useful of a word to make into a reserved word, which means it would have to be a contextual keyword (i.e. treated like a reserved word after import but behaving like a regular identifier elsewhere). I'm not generally a fan of contextual keywords—they tend to make things like syntax highlighters more difficult to create—so I try to avoid them.

Rejected

I considered these ideas, but don't think they are good enough approaches for various reasons:

Package identifier (syntax)

If an unquoted identifier appears before the import string, then it's a logical import within that package. Otherwise, it's relative.

import "relative/path"
import logical "path"

This was one of my initial ideas. It has the same problem as other unquoted imports in that it makes it harder to have odd package names. It means the VM has to understand this syntax and figure out how to display package names in stack traces and stuff, so there is some extra complexity involved.

The form where you have both a package name and a relative path within that package is pretty unusual and likely unintuitive to users.

Dotted (syntax)

If the path is a string literal, it's relative. Otherwise, it is a dot-separated series of unquoted identifiers.

import "relative/path"
import logical.path

Similar to slashes, but using dots. This helps make logical imports look more visually distinct from relative ones. But it also makes them look more similar to getter calls, which they aren't related to at all.

Include (keyword)

The include keyword is for relative imports, import is for logical.

include "relative/path"
import "logical/path"

Ruby uses include for applying mixins. "Include" reads to me more like some kind of transclusion thing, so it feels a little weird.

Require (keyword)

The require keyword is for relative imports, import is for logical.

require "relative/path"
import "logical/path"

Node uses "require" and ES6 uses "import" so this is kind of confusing. Ruby uses require and require_relative, so using require for a relative import is kind of confusing. Lua also uses require, but for both relative and logical. Overall, this feels murky and unhelpful to me.

Angle-brackets (syntax)

As in C/C++, an import string can be in angle brackets or quotes. Angle brackets are for logical imports, quotes for relative.

import "relative/path"
import <logical/path>

Hard pass. It requires context-sensitive tokenization (!) in C and we definitely don't want to go there.

URI scheme (string)

An import string starting with "package:" and maybe "wren:" is treated as logical, like they are URIs with an explicit scheme. Others are relative.

import "relative/path"
import "package:logical/path"
import "wren:random"

This is (roughly) how Dart works. I'm not a fan. I think it's too verbose for logical imports.

Package (modifier)

A package modifier indicates a logical import. Others are relative.

import "relative/path"
import package "logical/path"

Pretty long, and I'm not too crazy about baking "package" into the language and VM.

From (modifier)

A from modifier indicates, uh, one kind of import.

import "some/path"
import from "other/path"

It looks nice, but it's totally unclear to me whether logical imports should get from or relative ones. Also kind of confusing in that Python and ES6 use from in their notation for importing explicit variables from a module (where Wren uses for).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment