Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Import literal "module"#export hoists import { export } from "module" #1652

Draft
wants to merge 3 commits into
base: main
Choose a base branch
from

Conversation

edemaine
Copy link
Collaborator

@edemaine edemaine commented Dec 20, 2024

Fixes #113 (auto imports) by introducing syntax "module"#export that turns into a ref resulting from import { export as ref } from "module".

But I'd like some feedback (here or in #113) on whether we have the right syntax here. (It should be easy to change.)

One thing I don't like about this notation is that the default export is rather verbose: "module"#default. We can't use "module"# because that refers to length.

@STRd6 STRd6 marked this pull request as ready for review December 21, 2024 02:34
@STRd6 STRd6 marked this pull request as draft December 21, 2024 02:35
@STRd6
Copy link
Contributor

STRd6 commented Dec 21, 2024

I think the original syntax of a snug prefix ^ could work pretty well. Similar to how symbols fit in despite object properties and labels.

I’m not as big a fan of overloading the length syntax since it requires the quotes which reduces some of the benefits.

^ ModuleSpecifier seems like it handles everything except spaces and in that case it could be explicitly wrapped with quotes.

The main open questions are:

  • how to suffix the module specifier to differentiate default vs named vs *.
  • Should the auto imports be hoisted or dynamic implying async within the function. I assume hoisted but maybe we could also have a prefix/suffix that modifies it to become dynamic.

Sorry I can’t experiment more myself but I’m away from PC for the rest of the week. (Mobile interface is also why I accidentally untoggled draft on this PR 😵‍💫)

@edemaine
Copy link
Collaborator Author

Thanks for the feedback; I agree. ^module does seem good, but it has the problem that (without a suffix) we don't know whether ^module.x is accessing property x of the import, or is just specifying an extension of .x in the module filename. (See the recent discussion in #113.)

So I think we need some kind of character after the module name. This is what led me down the # path: ^module#export and ^module# for the default export. I went aggressive and removed the ^ but in hindsight I think that was a mistake. Maybe there's a better character than #. (Or require quotes, as in ^"module", but then how do we specify the export? ^"module"export seems weird.) If we want to embrace ^, we could go for ^module^export. And ^module^ could be the default export. I'm not sure whether this is better... It feels a little weird that module and export are different things but they get the same character.

I had forgot about * import. Maybe ^module* or ^module^* could work well for this?

I always had in mind that ^ meant "hoist to top", so I was favoring static import. But I agree a dynamic version could be nice. Dynamic is a bit less necessary because you could just put a (dynamic) import declaration on the previous line of where you need it. But still could be a bit more concise via this syntax (with an extra symbol or something to denote dynamic).

@STRd6
Copy link
Contributor

STRd6 commented Dec 21, 2024

Maybe : for named export, ^. for property? And plain . is always part of the file name. We’ll need to play around to see what we prefer to be most common.

@edemaine
Copy link
Collaborator Author

Unfortunately, we can't use : for named property because we want to support ^node:fs as a module name.

I wish we could use the same symbols from an import declaration, as in ^module{export}, which would mesh well with glob notation. Unfortunately, glob notation would dictate returning an object {export}, not the export itself, which isn't what we want.

Similar to ^module^export, Vendethiel points out we could do "module"^export. I find the ^ in the middle (in both cases) a little weird looking though, I think because it's so different from pinning, and it's weird to say "hoist this" in the middle. Maybe I should think of ^ as weird quotes and ^module^ is then balanced. Or maybe there's another symbol for the middle... ^module//export?

@edemaine
Copy link
Collaborator Author

edemaine commented Dec 22, 2024

Here's a weird idea: if ^module is the default export and ^module* is the * export, then a named export would be equivalent to ^module*.export. So perhaps a natural shorthand would be ^module*export? (Similar to how ? is shorthand for ?., etc.) Certainly * is a rare character for a filename (because of globs). Though it is plausible that the default export is a number and you'd want to multiply it, you could use spaces in that case (^module * 2).

^module // import ref1 from 'module'
^module* // import * as ref2 from 'module'
^module*foo // import {foo as ref3} from 'module'
// or just use ref2.foo

Update: Vendethiel points out that ^module is annoying if you wanted to match against it in a pattern-matching switch, because ^foo already has a meaning... I guess we could do ^^module in that case.


Weirder (bad) idea: *module could be notation for the * export of module. Then *module.export is how we'd write the named export. Currently * isn't used as a prefix operator (except for generator methods), and it's a reasonable pneumonic that it's equivalent to import * from. I think the only downside is that we need a completely different notation for default export. Is it too much use of ASCII characters to reserve ^module for default export and *module for * export?

^module // import ref1 from 'module'
*module // import * as ref2 from 'module'
*module.foo // import {foo as ref3} from 'module'
// or just use ref2.foo

Update: Sorry, what am I thinking? *module.foo has the same extension ambiguity. 🤦

@STRd6
Copy link
Contributor

STRd6 commented Dec 22, 2024

I think // is pretty interesting for the named export.

^lodash/add
^lodash//add
import ref from "lodash/add"
import { add as ref1 } from "lodash"

The advantage is it allows single / to remain the regular path separator. We could also consider defaulting . to being property access and require it being escaped or the whole path to be quoted for it to appear in the path since it seems uncommon there.

*. as a suffix seems pretty good for property access after a star import. I think star import is generally rare except for common JS compatibility.

Favoring default export and paths may help with code splitting.

@edemaine
Copy link
Collaborator Author

Cool, // does look pretty compelling in that example. I assume people aren't going to use this syntax with whole URLs (though we could also allow those, by not allowing // to be preceded by :).

I forgot that // also means one-line comment. Are we OK with using it here, provided there is no space? We already have some exceptions to // being a comment, like ///...///. And the module name already has special unquoted string handling, so maybe it makes sense to have some magic. But I guess we would also have ^"module name"//export.

We could also consider defaulting . to being property access and require it being escaped or the whole path to be quoted for it to appear in the path since it seems uncommon there.

So including an extension in the filename would require quotes? Hmm, also an interesting idea. I guess we could still allow for leading ./ and ../../ which are also common. But with so many .s in module names, I think this might feel a little weird.

*. as a suffix seems pretty good for property access after a star import. I think star import is generally rare except for common JS compatibility.

My point was that import * as foo from 'module'; foo.bar is semantically equivalent to import { bar } from 'module'; bar (I think), so we could use ^foo*.bar or ^foo*bar as notation for either (probably preferring the latter compilation). That is, *.bar or *bar could act like //bar above. Though I guess if you do weird stuff like ^foo*.bar = 5 (changing the * import) then the future use of ^foo*.bar becomes murky...

@edemaine
Copy link
Collaborator Author

edemaine commented Dec 22, 2024

On Discord, @vendethiel reminds us that the original PR's string-based approach would work well with ^ (as a suffix):

"module"^ // import ref from "module"
"module"^export // import { export as ref } from "module"
"module"^* // import * as ref from "module"

Can dereference the default export via "module"^.foo. Could also add fun things like "module"^{export1,export2} to get multiple exports in an object.

We're overloading binary XOR (for named exports) but that seems fine for string literal LHS.

If we wanted an unquoted version, we could use ^module^ and ^module^export (where ^ are acting more like quotes). This could co-exist with the quoted version.

@edemaine
Copy link
Collaborator Author

I went ahead and implemented the following:

^module^ // import ref from "module"
^module^export // import { export as ref } from "module"
^module^* // import * as ref from "module"

"module"^ // import ref from "module"
"module"^export // import { export as ref } from "module"
"module"^* // import * as ref from "module"

Mostly I wanted to implement the * and default import machinery, which we'd need no matter what syntax we use.

We don't need to use this syntax, but I wanted to nudge a bit in this direction. I like that ^ is the sole character that represents import literals, and "^ as quotes" is starting to grow on me... I'm also not sure whether to keep both syntaxes or just one, or to use something else entirely.

@STRd6
Copy link
Contributor

STRd6 commented Dec 28, 2024

If the quoted strings exist as a fallback we could do a more concise prefix caret for common cases:

^underscore/modules/map // import ref from "underscore/modules/map"
^underscore.map // import { map as ref } from "underscore"
^underscore.* // import * as ref from "underscore"

"module"^ // import ref from "module"
"module"^export // import { export as ref } from "module"
"module"^* // import * as ref from "module"

I generally prefer the prefix caret since it limits backtracking but a postfix caret could also work.

@edemaine
Copy link
Collaborator Author

^underscore.map // import { map as ref } from "underscore"
^underscore.* // import * as ref from "underscore"

If we're going to require quotes for extensions and give . over to the language, I would have expected . to mean property access. In particular, I would have guessed ^underscore.map to correspond to import ref from "underscore"; ref.map. Maybe this would be a good place for //, as in ^underscore//map?

Combining with your suggestion of prefix caret (less backtracking is maybe also a good argument for human readers):

^underscore/modules/map // import ref from "underscore/modules/map"
^underscore.map // import ref from "underscore"; ref.map
^underscore//map // import { map as ref } from "underscore"
^underscore//* // import * as ref from "underscore"

^"./module.js" // import ref from "./module.js"
^"./module.js//export" // import { export as ref } from "./module.js"
^"./module.js"//* // import * as ref from "./module.js"

Or is conflict with comments too weird? (I didn't add syntax highlighting because it looks terrible.) At this point we could also consider # again: after a caret is a very special context.

^underscore/modules/map // import ref from "underscore/modules/map"
^underscore.map // import ref from "underscore"; ref.map
^underscore#map // import { map as ref } from "underscore"
^underscore#* // import * as ref from "underscore"

^"./module.js" // import ref from "./module.js"
^"./module.js"#export // import { export as ref } from "./module.js"
^"./module.js"#* // import * as ref from "./module.js"

Or we could follow the "^ means import" mantra and use it for module specifier and export specifier:

^underscore/modules/map // import ref from "underscore/modules/map"
^underscore.map // import ref from "underscore"; ref.map
^underscore^map // import { map as ref } from "underscore"
^underscore^* // import * as ref from "underscore"

^"./module.js" // import ref from "./module.js"
^"./module.js"^export // import { export as ref } from "./module.js"
^"./module.js"^* // import * as ref from "./module.js"

I'm still not sure I like any of these better than the currently implemented ^module^ syntax:

^module^ // import ref from "module"
^module^.map // import ref from "module"; ref.map
^module^export // import { export as ref } from "module"
^module^* // import * as ref from "module"

^./module.js^ // import ref from "./module.js"
^./module.js^.map // import ref from "./module.js"; ref.map
^./module.js^export // import { export as ref } from "./module.js"
^./module.js^* // import * as ref from "./module.js"

@STRd6
Copy link
Contributor

STRd6 commented Dec 29, 2024

We could also consider snug : and :: in this context as a scope resolution kind of thing.

Edit: forgot about : in paths but :: is probably ok.

@edemaine
Copy link
Collaborator Author

edemaine commented Dec 29, 2024

I don't think : is commonly used in an import path (full absolute Windows paths would be kinda bonkers IMO, and I hope MacOS 9 doesn't exist anymore), so it's probably fine.

:: has some interesting prototype vibes (ignoring that coffeePrototype isn't on by default), pretending that ^module.prototype is the module itself, so its properties are the imports. I guess that would mean ^module:: would be the * import, while ^module::export would be the named export. Then the default export would be ^module, so it can't have any periods in it without using quotes?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants