Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
071d6db
Add glob imports
stefnotch Apr 19, 2026
0e146af
Update glob imports
stefnotch Apr 22, 2026
3232a88
revise around @wildcardable, more cleanups
mighdoll May 2, 2026
ae56930
move wildcardable motivation and advanced mode higher
mighdoll May 6, 2026
216457c
move builtin_shadow to client side warning
mighdoll May 6, 2026
8fcb4a7
rm forward compatibility section
mighdoll May 6, 2026
f4940e2
no waffling about library publish tool expansion
mighdoll May 6, 2026
a1ec6c8
change wgsl_test module to 'expect'
mighdoll May 6, 2026
b758d95
add supressible diagnostics section
mighdoll May 6, 2026
abfb0e7
fix outline level for scope precedence
mighdoll May 6, 2026
81e06f4
restore builtin_shadow publisher side check
mighdoll May 6, 2026
7d4bfd0
clarify wildcard clash case
mighdoll May 6, 2026
9e46adf
clarify precedence means wildcard updates are also safe
mighdoll May 6, 2026
2a21505
switch to `@wildcardable module;` syntax
mighdoll May 10, 2026
1a46cd8
add ImportsDesign explainer
mighdoll May 10, 2026
b58f43b
direct imports are more traceable
mighdoll May 12, 2026
983259d
clarify at most one 'module' per file
mighdoll May 12, 2026
eb455ad
rm client side builtin_shadow for separate PR
mighdoll May 12, 2026
fa9e837
clarify where wildcard_shadow appears
mighdoll May 13, 2026
946d64d
clarify error location in table
mighdoll May 13, 2026
ca3c45b
@! syntax for module attributes
mighdoll Jun 25, 2026
66df0b7
revise based on PR feedback
mighdoll Jul 2, 2026
78ff4d1
rm some zero width characters
mighdoll Jul 2, 2026
cb61e0c
note prohibiting import *;
mighdoll Jul 2, 2026
9feb566
revise based on latest k2d222 comments / agent review
mighdoll Jul 6, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
266 changes: 246 additions & 20 deletions Imports.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ import my::geom::sphere::{ draw, default_radius as foobar };

// Imports a whole module. Use it with `bevy_ui::name`
import bevy_ui;

// Import all items from another module
import bevy::prelude::*;
Comment thread
stefnotch marked this conversation as resolved.
import wgsl_test::expect::*;
```

These can then be used anywhere in the source code.
Expand Down Expand Up @@ -66,14 +70,15 @@ translation_unit:
| import_statement* global_directive* global_decl*

import_statement:
| 'import' import_relative? (import_collection | import_path_or_item) ';'
| attribute* 'import' import_relative? (import_collection | import_path_or_item) ';'

import_relative:
| 'package' '::' | 'super' '::' ('super' '::')*

import_path_or_item:
| ident '::' (import_collection | import_path_or_item)
| ident ('as' ident)?
| '*'

import_collection:
| '{' (import_path_or_item) (',' (import_path_or_item))* ','? '}'
Expand All @@ -91,6 +96,25 @@ An item import imports a single item. The item can be renamed with the `as` keyw

An import collection imports multiple items, and allows for nested imports.

A wildcard import imports all top-level declarations from a module. Submodule names and submodule contents are not imported. A wildcard must follow a module path; a bare `import *;` is an error. A wildcard may also appear as a member of an import collection, applying to the module path before the braces (see [Import resolution algorithm](#import-resolution-algorithm)).

WESL also extends WGSL's `global_directive` rule with a *module attribute*: a `@!`-prefixed attribute that carries module-level metadata. It is used by `@!wildcardable` (see [Wildcard imports](#wildcard-imports)) and reserved for future module-level metadata.

```ebnf
global_directive:
| ... // existing WGSL forms
| module_attribute_directive

module_attribute_directive:
| '@' '!' ident_pattern_token argument_expression_list? ';'
```

A module attribute is written like a WGSL `attribute` with a `!` immediately
after the `@`, and is terminated with `;`; `ident_pattern_token` and
`argument_expression_list` are the WGSL rules. Like other global directives,
module attributes appear after any imports and before any global declarations,
and apply to the module they appear in.

### Import resolution algorithm

To resolve the import, the recursive structure is flattened out. This means turning every `import_collection` into multiple separate imports, ending with the items.
Expand All @@ -102,6 +126,17 @@ import a::c::d;
import a::c::e as f;
```

A wildcard may appear as a member of an import collection:
`import foo::{a::b, *};` becomes

```wesl
import foo::a::b;
import foo::*;
```

The wildcard imports only `foo`'s own top-level declarations: the sibling
branch reaching into submodule `foo::a` doesn't widen it.

Then, one iterates over each segment from left to right, and looks it up one by one.

1. We start with the first segment.
Expand All @@ -111,8 +146,9 @@ Then, one iterates over each segment from left to right, and looks it up one by
2. We take that as the "current module".
3. We repeatedly look at the next segment.
1. Item in current module: Take that item. We must be at the last segment, otherwise it's an error.
2. (Else if re-exported or inline module in current module: We continue with that module.)
3. Else go to `current module path/ident.wesl`
2. Wildcard import: Take all items in the current module.
3. (Else if re-exported or inline module in current module: We continue with that module.)
4. Else go to `current module path/ident.wesl`
* File found: We take that file as the current module.
* File not found: We assume an empty module as the current module, and continue with that.
* (Re-exporting changes the path.)
Expand All @@ -121,10 +157,7 @@ Then, one iterates over each segment from left to right, and looks it up one by
To get an absolute path to a module, one follows the algorithm above. In step 1, one takes the known absolute path of the `super` module, or the package.
The absolute path of the `super` module is always known, since the first loaded WESL file must always be the root module, and children are only discovered from there.

Once the import has been resolved, the last segment, or its alias, is brought into scope.

The order of the scopes is "user declarations and imported items > package names > predeclared items".
This lets WGSL add more predeclared items without breaking existing WESL code. Package names can shadow predeclared items, but we recommend that authors avoid doing that.
Once the import has been resolved, the last segment, or its `as` alias, is brought into scope.


### Example
Expand Down Expand Up @@ -245,6 +278,199 @@ const b = a + 1;

Basic linker implementations do not need to check for this. Generating broken code and letting the underlying shader compiler throw an error is fine.

## Wildcard imports

Wildcard imports bring all items from another module into the importing module's
scope.

Users can wildcard import:
- from any other module in the local package.
- from any external library module where the library author has added a
`@!wildcardable` annotation.

Wildcard importing brings some stability risk when the imported module adds to
its API. The newly introduced names may conflict with other names in the
Comment thread
k2d222 marked this conversation as resolved.
importer's namespace (from local definitions and other imports), leading to
compiler warnings or errors. To help mitigate this risk, WESL provides a
`@!wildcardable` annotation that library authors can place on modules that are
designed for wildcard importing.

Advanced users who wish to wildcard import from external modules not marked as
`@!wildcardable` can do so by suppressing `unsupported_wildcard` (see
[Suppressible diagnostics](#suppressible-diagnostics)).

```wesl
// wildcard import from a @!wildcardable external
import bevy::prelude::*;
import wgsl_test::expect::*;

// wildcard import from within the local package
import package::utils::*;
import super::fun::*;
```

### `@!wildcardable` annotation

Library authors mark modules they intend for library consumers to wildcard
import with the `@!wildcardable;` module attribute (see [Grammar](#grammar)):

```wesl
// math.wesl (in a library)
@!wildcardable;

fn dot2(a: vec2f, b: vec2f) -> f32 { return a.x*b.x + a.y*b.y; }
fn cross2(a: vec2f, b: vec2f) -> f32 { return a.x*b.y - a.y*b.x; }
```

### Recommendations for `@!wildcardable` modules

Because every name in a `@!wildcardable` module is a potential collision in
importer code, library authors should curate these modules carefully.

**Add hesitantly.** Additions to a `@!wildcardable` module are semver minor
version bumps but can break users who have local declarations or import other
`@!wildcardable` modules.
- **Bundle** additions into a major release when one is upcoming.
- **Document** additions clearly in changelogs so downstream users debugging
unexpected name resolution can trace them.

**Compose with re-exports.** See [Re-exports](#re-exports) (TBD) to collect
items from other modules into a single `@!wildcardable` module for user
convenience.

**Avoid generic names.** Prefer domain-specific names. Common names like
`Buffer`, `Config`, `Result`, `Vec`, etc. are more likely to collide with user
applications.

**Don't shadow WGSL builtins.** Names like `vec3`, `clamp`, `inverseSqrt` have
expected semantics that oughtn't be implicitly overridden with wildcards.
Similarly, avoid experimental Naga/Dawn/Safari builtins.
- The `builtin_shadow` diagnostic flags this (see
[Suppressible diagnostics](#suppressible-diagnostics)).
- If a future WGSL update adds a conflicting builtin name, plan to update the
`@!wildcardable` module to rename the conflicting item.

### Library-to-library wildcard imports

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought about this a bit more and there's a simpler rule:

Adding items to a wildcardable module is a semver breaking change. Publish accordingly.

We just have to note that and then we can get rid of this section. We can add this special trick later if we want.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The problem is that user communities reject having every add to an API be a major semver release. It causes too much churn. Theoretically, we could be the first lang community to convince our users differently, seems righteous. But that's probably not where we'd want to spend our limited teaching-users-new-things tokens.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For what it's worth, Cargo says that wildcards are just minor changes but that they can be breaking changes. And then discourages wildcard imports

Glob imports of items from external crates should be avoided.
https://doc.rust-lang.org/cargo/reference/semver.html#item-new

I'm assuming that that only applies to libraries.

@stefnotch stefnotch Jun 25, 2026

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One problem is that I don't yet see a way for wesl-rs to actually implement the rewriting behaviour in a sane way

https://github.com/webgpu-tools/wesl-spec/pull/183/changes#r3473134717

Which is frustrating, because I'm the one who came up with this idea in the first place.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, let's go back to discouraging cross-package wildcard imports in library code, as a suppressible error.

While I'm sure some libraries do code generation in every host language ecosystem, it may be awkward. So let's not require that. And we can't get the golang/js/jvm communities to change their library expectations. So lowest common denominator for cross-host-language libraries: no ecosystem-required publish tool and no ecosytem-required occasional library breakage.

Which is frustrating, because I'm the one who came up with this idea in the first place.

Expansion can remain an optional packaging feature. We may eventually want a packaging step for other reasons besides wildcards: to fill in runtime visible library metadata (web runnable tests, viewable examples), incremental translation. But we should try to keep the packaging step optional.

I'll revise the section to lead with the diagnostic and move expansion to optional tooling.

For the diagnostic, tooling can usually infer library vs. application context from package metadata such as Cargo.toml or package.json. If that proves insufficient, we can later add an explicit wesl.toml flag.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you! That alleviates my implementation concerns

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

revised draft.

@stefnotch stefnotch Jul 3, 2026

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We'll have to figure out some wesl.toml way of saying "this is a library" for the diagnostic to be implementable. Happy to defer that though.

(I think the JS ecosystem has private: "true". The Cargo ecosystem doesn't have a direct equivalent from what I remember.)


Libraries that wildcard import from other libraries raise special concerns. If a
user's package manager chooses a newer version of the imported-from library,
the user may see a conflict in library code they don't expect to modify.

Cross-package wildcard imports in library code
trigger the `cross_package_wildcard` diagnostic, a suppressible error. Library
authors can suppress the diagnostic with
`@diagnostic(off, cross_package_wildcard)` on the import statement, e.g. when
wildcard importing from external packages they control.

**Optional: publish-time wildcard expansion.** Library publishing tools may
also offer to expand wildcards to named imports in the published version of a
module, snapshotting the names at publish time:

```wesl
// source
import bevy::prelude::*;
```

```wesl
// published artifact
import bevy::prelude::{Color, Mesh, Transform, /* snapshot at publish time */};
```

Expansion pins the imported names so that a newer version of the imported-from
library can't introduce conflicts into already-published code.

## Import errors and warnings

WESL emits errors for name collisions and for unsupported wildcard imports,
and warnings for shadowing that could surprise readers. Genuine
collisions cannot be suppressed; other diagnostics are suppressible via
`@diagnostic`.

| Situation | Behavior |
| --- | --- |
| Local declaration conflicts with named import | Error |
| Named import conflicts with named import | Error |
| Wildcard import conflicts with wildcard import (when name is referenced) | Error |
| Wildcard import from a non-`@!wildcardable` external module | Error (`unsupported_wildcard`) on the import; suppressible |
| Cross-package wildcard import in library code | Error (`cross_package_wildcard`) on the import; suppressible |
| Local declaration or named import shadows a wildcard-imported name | Warning (`wildcard_shadow`) on the shadowing declaration or import; suppressible |
| `@!wildcardable` module exports an item shadowing a WGSL builtin | Warning (`builtin_shadow`) on the shadowing declaration; suppressible |

When multiple wildcard imports are in scope, the same name may be exported by
more than one module. If every wildcard resolves the name to the same
declaration, references are unambiguous and no error occurs. A name that could
refer to two different declarations is a dormant conflict: an error occurs
only where the name is referenced:

```wesl
import foo::*; // exports clashing_zap
import bar::*; // exports a different clashing_zap

fn main() {
let x = clashing_zap(); // error: ambiguous between foo::clashing_zap and bar::clashing_zap
}
```

The fix is to disambiguate with a named import (`import foo::clashing_zap;`,
or `import foo::{clashing_zap, *};` to keep the wildcard) or an
[inline module path](#inline-usage) (`foo::clashing_zap()`).

### Suppressible diagnostics

Each diagnostic below can be suppressed at the site indicated with a
`@diagnostic` attribute, or module-wide with WGSL's
[global diagnostic directive](https://www.w3.org/TR/WGSL/#global-diagnostic-directive),
e.g. `diagnostic(off, wildcard_shadow);`.

- **`unsupported_wildcard`** fires on a wildcard import from an external module
that doesn't support wildcard import (not marked `@!wildcardable`). Suppress
with `@diagnostic(off, unsupported_wildcard)` on the import statement to
accept the upgrade risk that future versions of the imported module may add
conflicting names. The suppression has no effect on an import from a
`@!wildcardable` module, where the diagnostic never fires.

- **`wildcard_shadow`** is a warning that fires on a local declaration or named
import that shadows a name brought in by a wildcard import. The shadowing is
allowed: the local declaration or named import wins by precedence
(see [Scope precedence](#scope-precedence)). Suppressing the warning with
`@diagnostic(off, wildcard_shadow)` on the shadowing declaration or import
statement changes only the reporting, not the resolution.

- **`cross_package_wildcard`** is a suppressible error that fires on a
cross-package wildcard import in library code (see
[Library-to-library wildcard imports](#library-to-library-wildcard-imports)).
Suppress with `@diagnostic(off, cross_package_wildcard)` on the import
statement.

- **`builtin_shadow`** is a warning that fires in a `@!wildcardable` module,
on a top-level declaration whose name shadows a WGSL builtin such as `vec3`
or `clamp`, whether or not any module wildcard imports it. Suppress with
`@diagnostic(off, builtin_shadow)` on the offending declaration if the
override is intentional.

## Scope precedence

When a name could resolve to items at multiple precedence levels, the
Comment thread
stefnotch marked this conversation as resolved.
highest-precedence one wins. The table in
[Import errors and warnings](#import-errors-and-warnings) describes whether
such a resolution is silent, produces a warning, or produces an error.

1. user declarations and named imports (non-wildcard)
2. wildcard-imported names
3. package names
4. predeclared items (WGSL builtins)

User declarations and named imports share the top precedence level: a conflict
between them is an error, so at most one candidate can exist at that level.

Predeclared items rank lowest so that future WGSL spec revisions can add new
builtins without breaking existing shaders: any name already bound at a higher
level continues to resolve as before. Wildcard-imported names rank below user
declarations and named imports for the analogous reason: additions to a
`@!wildcardable` module won't silently change resolution at call sites that
already have a local or named binding for the same name. Wildcard imports bring
in only the imported module's top-level declarations, not package names.

## Directives
Under discussion, see: <https://github.com/wgsl-tooling-wg/wesl-spec/issues/71>

Expand All @@ -268,19 +494,19 @@ This only refers to the exact module that an element is in, and not any of the p
Example:

```wgsl
​​​​// main.wesl:
​​​​import foo::bar;
​​​​fn main() { bar(); }

​​​​// foo.wesl:
​​​​import zig::zag;
​​​​const_assert(1 > 0); // included in link because bar is used
​​​​fn bar() { }
​​​​fn miz() { zag() }

​​​​// zig.wesl:
​​​​const_assert(2 < 0); // not included in link
​​​​fn zag() { }
// main.wesl:
import foo::bar;
fn main() { bar(); }

// foo.wesl:
import zig::zag;
const_assert(1 > 0); // included in link because bar is used
fn bar() { }
fn miz() { zag() }

// zig.wesl:
const_assert(2 < 0); // not included in link
fn zag() { }
```

Example
Expand Down
Loading
Loading