wgsl-tooling-wg / wesl-spec

A portable and modular superset of WGSL
BSD 3-Clause "New" or "Revised" License
18 stars 3 forks source link

Unified imports proposal #53

Open stefnotch opened 1 week ago

stefnotch commented 1 week ago

Background

For future features, such as re-exports, and inline modules, we want an import system that has a concept of a module! One where import foo::bar::baz does not necessarily translate to foo/bar/baz.wesl. There could have been a re-export after all.

The basic idea of an import is

  1. Resolve the import. Examples for what import a::b can mean, depending on the rest of the code
    • It points at a/b.wesl.
    • It points at a function b from a.wesl
    • Due to a re-export, it points at a/utils/b.wesl
  2. Bring it into scope. e.g. import a::b can bring a module named b into scope. Or a function named b

Caching import resolutions is explicitly an implementation detail. The algorithms described below will work without caching, but caching can speed them up considerably.

Resolving the source of an import is not always obvious. So here is a proposed algorithm.

Sparse Web Projects

Some web projects scatter .wesl files throughout their hierarchy. Usually the .wesl files are reasonably concentrated, but there occasionally are folders do not have a wesl file. e.g.

wesl.toml
src/
  components/
    HelloWorld.vue
    shaders/
      glitch.wesl
  utils/
    math.wesl
    rgb.wesl
    hsv.wesl

Basic Observation

Even without re-exports, or anything advanced, we need a resolution algorithm for import a::b! It can either import the module b, from the file a/b.wesl. Or it can import the function b, from the file a.wesl.

To solve that, we go over the import, step by step. At each step, we decide where to go next to find the correct module.

Step by step resolution

At the end of the resolution, we have a tree of modules. Each module has exactly one fully qualified path. (Future: Module re-exports will get resolved to the fully qualified path.)

First comes the root step.

Now we are at a valid module that we can parse.

Then comes the next step.

Finally, we repeat that step until we are finished with our import. At the end, we either found a valid import, or ran into an error.

k2d222 commented 1 week ago

Great, this proposal is roughly what I want too! Some comments:

If none of the above were true, we go to the next file.

what if the file does not exist? do we throw or do we try the next path component?

In my understanding, a project could get away with not writing a single export declaration if the file tree is well structured: all subfolders are adjacent to a file with the same name. This is close to what python does: a module is a folder that contains a file __init__py (but python is more cryptic!). I'm slightly concerned that it would encourage ppl to create empty files just for that purpose. like in your example, I would be tempted to create an empty src/utils.wesl so all files in src can import util::anything.

Proposal A

Proposal B

Additional notes

stefnotch commented 1 week ago

Great, this proposal is roughly what I want too! Some comments:

If none of the above were true, we go to the next file.

what if the file does not exist? do we throw or do we try the next path component?

That is a really good question. Initially I was assuming that it's an immediate error, but after thinking about it for a bit, I don't actually see an issue with "just continue and try the next path component".

Maybe that should be proposal C?

Proposal A

* it seems to me that `export` works like the `mod` statement in rust, except that it can skip subfolders and can rename. Is this correct?

Yes, pretty much

* does an export also act as an import for the current module, i.e. brings `some_name` into scope?

No, it doesn't bring some_name into scope. If the user wants some_name, they can now import it with the usual mechanisms.

Proposal B

* does it mean that a file can have only one wildcard `export` statement?

Yes, exactly

* could you mix and match proposal A exports and a proposal B export, the latter acting as a catch-all?

I'm thinking that we should only pick one of them. Currently I prefer proposal A over proposal B.

* does it mean that the import can only resolve to a file module, and not a declaration inside the file? e.g. `import ./src/utils/anything/blah` would resolve to `/src/utils/anything/blah.wesl`, but never to declaration `blah` in file `/src/utils/anything.wesl`

Yes. That's a major limitation of proposal B. I actually haven't thought about this limitation, I only thought about the limitation below vvv.

* similarly, does it mean that if a submodule declares an inline module, it could never be imported from the outside?.

Also yes. Also a limitation of proposal B.

Additional notes

* re-exports could simply be aliases, since we can import aliases. `export "./src/utils" as utils; alias math = utils::math;`

Oh yes, that would also work! We could also do export file("./src/utils") as utils;, juuuust in case the WGSL peeps ever want to add strings to WGSL so that we can have print("quoted string").

* similarly for proposal A, this could be an alternative equivalent syntax: `alias some_same = "./src/utils/shader"`. Essentiallly the quoted path is a regular ident. Then perhaps we could do things like accessing file declarations directly: `let x = "./src/utils/shader"::sqrt(2.0);`. Actually you can do that in `naga_oil`!

Similarly to above, we could let the syntax be let x = file("./src/utils/shader")::sqrt(2.0); We can, of course, bikeshed over whether the function should be called file or mod or import or ...

mighdoll commented 1 week ago

I think there are concerns here we might discuss separately, maybe as separate github issues. But I may be misunderstanding too. Anyway, here's my first take:

stefnotch commented 6 days ago

@k2d222 @mighdoll @ncthbrt I updated the proposal, now it really is just

  1. We have the segments of the import path
  2. Start at root
  3. Look at file
  4. Go to next file (and skip missing ones)
  5. Repeat
k2d222 commented 6 days ago

LGTM