diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e43b0f9 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.DS_Store diff --git a/GLOSSARY.md b/GLOSSARY.md index dee05d6..6fc14f4 100644 --- a/GLOSSARY.md +++ b/GLOSSARY.md @@ -12,9 +12,13 @@ * Type aliases * [Const declarations, override declarations](https://www.w3.org/TR/WGSL/#value-decls) * [Var declarations](https://www.w3.org/TR/WGSL/#var-decls) -* **Module**: A single WESL or WGSL file. +* **Module**: A unit of WESL or WGSL code with its own top-level scope, stored in a single module source. +* **Module Source**: The stored text of a module, typically a WESL or WGSL file. * **Root Module**: The WESL module from which translation starts. A single project can have many root modules. -* **Module Path**: Hierarchical address of a module file or partial path, akin to a filesystem path +* **Declaration Path**: A fully qualified `::`-separated path whose final segment names a declared item. +* **Module Path**: A `::`-separated path naming a module; equivalently, a declaration path minus its final segment. See [Imports](Imports.md#resolving-a-declaration-path). +* **Import Path**: The `::`-separated path written in an import statement. An import collection (`{}`) flattens into separate imports, each with its own import path. +* **Package Module**: The top-level module of a package, stored as the optional file `package.wesl` in the package root. A module path consisting only of `package` or a bare package name refers to the package module. * **Side effects**: WESL/WGSL shader code that is visible to host code (e.g. in Rust or JavaScript). Changes to that shader code have the side effect of changing the host interface to the shader. * Things that are specified when [creating a WGSL pipeline](https://developer.mozilla.org/en-US/docs/Web/API/GPUDevice/createRenderPipeline#fragment_object_structure) diff --git a/Imports.md b/Imports.md index 4988291..6743102 100644 --- a/Imports.md +++ b/Imports.md @@ -11,33 +11,37 @@ We also should account for **importing shader from libraries**. Ideally, users c Finally, we want **multiple tools** which can compile WGSL-with-imports down to raw WGSL. Using WGSL-with-imports both in Rust projects and in web projects should be possible. # Guide-level explanation -The `import` statement extension brings items or entire modules into scope. Import statements map to files with minimal searching. +The `import` statement extension brings item or module names into scope. -```wgsl +```wesl // Importing a single function using a relative path import super::lighting::pbr; // Importing multiple items import my::geom::sphere::{ draw, default_radius as foobar }; -// Imports a whole module. Use it with `bevy_ui::name` +// Imports a module name. Use it with `bevy_ui::name` import bevy_ui; + +// Import all items from another module +import bevy::prelude::*; +import wgsl_test::expect::*; ``` These can then be used anywhere in the source code. -```wgsl +```wesl fn main() { bevy_ui::quad(vec2f(0.0, 1.0), 1.0); let a = draw(3); } ``` -Both `bevy_ui` and `my` are packages in the current project. Language servers and related tools can look in a `wesl.toml` file to find the location of the packages. This lets libraries be published to package managers, and users can import them with a simple syntax. +Both `bevy_ui` and `my` are packages in the current project, typically installed by a package manager. Recursive import definitions are also supported, which leads to shorter import statements. -```wgsl +```wesl import bevy_pbr::{ forward_io::VertexOutput, pbr_types::{PbrInput, pbr_input_new}, @@ -45,17 +49,15 @@ import bevy_pbr::{ }; ``` -To find the relevant items, the following algorithm is used: - -Proceeding left to right through the path segments, consider the segments `prev` and `seg`. -1. if `prev.wesl` exists and includes WESL elements, check if `seg` is one of those elements, e.g. `fn seg` or `namespace seg`. -2. else if the directory `prev/` exists, check to see if the file `seg.wesl` or the directory `seg/` is in the `prev/` directory; -3. error if a `seg` is not found. +*Import paths* map directly to modules and their items. Each module is stored +in a single source, typically a WESL or WGSL file: so +`bevy_pbr::pbr_types::PbrInput` names the item `PbrInput` declared in the +source file `pbr_types.wesl` in the `bevy_pbr` package. # Reference-level explanation A WESL program is composed of a tree of WESL modules. -Imports must appear as the first items in a WESL file. They can import entire modules or individual "importable items" (see [GLOSSARY](GLOSSARY.md)). +Imports must appear at the beginning of a WESL file. They can bind the name of a module or of an individual "importable item" (see [GLOSSARY](GLOSSARY.md)). ### Grammar @@ -66,7 +68,7 @@ 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' '::')* @@ -74,6 +76,7 @@ import_relative: import_path_or_item: | ident '::' (import_collection | import_path_or_item) | ident ('as' ident)? +| '*' import_collection: | '{' (import_path_or_item) (',' (import_path_or_item))* ','? '}' @@ -87,13 +90,36 @@ not current keywords are allowed, but not recommended. Lint tools may optionally warn when reserved words are used. +Attributes may precede an import statement, notably `@if` for +[conditional translation](ConditionalTranslation.md) and `@diagnostic` for +[suppressible diagnostics](#suppressible-diagnostics). + An item import imports a single item. The item can be renamed with the `as` keyword. An import collection imports multiple items, and allows for nested imports. -### Import resolution algorithm +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 bindings](#import-bindings)). + +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 is otherwise reserved for future use. + +```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 bindings -To resolve the import, the recursive structure is flattened out. This means turning every `import_collection` into multiple separate imports, ending with the items. +An import statement's recursive structure is first flattened, turning every `import_collection` into multiple separate imports. For instance, `import a::{b, c::{d, e as f}};` would be turned into ```wesl @@ -102,61 +128,140 @@ import a::c::d; import a::c::e as f; ``` -Then, one iterates over each segment from left to right, and looks it up one by one. +A wildcard may appear as a member of an import collection: +`import foo::{a::b, *};` becomes -1. We start with the first segment. - * `super` refers to the parent module. Can be repeated to go up multiple parent modules. Exiting the root is an error. - * `package` refers to the top level module of the current package. - * `ident` must be a known package, usually found in the `wesl.toml` file. It refers to the top level module of that package. -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` - * 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.) - * (Inline modules do not have a path.) +```wesl +import foo::a::b; +import foo::*; +``` -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. +The wildcard imports only `foo`'s own top-level declarations: the sibling +branch reaching into submodule `foo::a` doesn't widen it. -Once the import has been resolved, the last segment, or its alias, is brought into scope. +Each flattened import statement binds one name in the importing module: the +last segment of its *import path*, or its `as` alias. The binding is a +shorthand for the import path. Each *reference*, a use of a name in code +(such as in a function call, type, attribute argument, or other expression), +determines a declaration path: a bound name expands to its import path, and +any `::` segments written after it extend the path +(see [Inline Usage](#inline-usage)). -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. +How a bound name is used determines whether it refers to a declaration or a +module: +```wesl +import bevy_pbr::forward_io; -### Example +fn main() { + var out: forward_io::VertexOutput; // forward_io used as a module: + // the file bevy_pbr/forward_io.wesl + let x = forward_io(); // forward_io used as a declaration: + // fn forward_io declared in bevy_pbr/package.wesl +} +``` -For example +A declaration and a module may share the same name: a bare `forward_io` +refers to the declaration, while `forward_io::VertexOutput` reaches into the +module. + +### Resolving a declaration path + +A *declaration path* is a fully qualified path whose final segment names a +declared item. The segments before the final segment are the *module path*, +naming the module containing the declaration. In +`bevy_pbr::forward_io::VertexOutput`, `VertexOutput` is declared in the +module named by `bevy_pbr::forward_io`. Each module path names exactly one +*module source*: the stored text of a module, typically a file. + +The first segment anchors the path: + +* `package` anchors the path at the current package. +* `super` refers to the parent of the current module, removing the last + segment of the current module's path. Each additional `super` removes + another segment; it is an error to remove beyond the package root. +* Any other first segment must name a known package, and anchors the path at + that package. Tools find the known packages in the + [`wesl.toml`](WeslToml.md) file or through the host package manager's + dependencies. + +A package amounts to a mapping from module paths to module sources; the +semantics of the module path segments beyond the first are specific to the +package's storage. Only the module path maps to storage; the final segment of +a declaration path names a declaration inside the module source, never a file. +For a package stored on a filesystem, the first segment refers to the +package's root directory; each following segment except the last names a +subdirectory, and the last segment of the module path names the source file: +`seg.wesl`, or `seg.wgsl` when no `.wesl` file exists +(see [Filesystem Resolution](#filesystem-resolution)). A +package served over the web can map each module path to a URL, and a bundled +library can store module sources in a dictionary keyed by module path. + +A module path may consist of just `package`, or of just a bare package name. +Such a path refers to the *package module*: on a filesystem, +the file `package.wesl` in the package's root directory. The package module +is optional. + +Referencing a declaration path that fails to resolve is an error. An import +statement whose bound name is never referenced is allowed, even if +referencing it would be an error; tools may warn about unused or unresolvable +imports. Import statements and references removed by +[conditional translation](ConditionalTranslation.md) are also allowed, even +if they would be errors under other conditions; tools may warn about these +too. + +Tools can enumerate the potential resolutions an import statement +enables by analyzing the source tree paths and the declarations in each +module source, for example to suggest editor auto-completions. + +For a wildcard import, the entire path before the `*` is a module path; the +wildcard brings the module's top-level declarations into scope. + +### Examples -```wgsl +```wesl import bevy_pbr::forward_io::VertexOutput; -``` -This first looks for `bevy_pbr.wesl`. -`bevy_pbr.wesl` is found, and doesn't contain an item named `forward_io`. -Thus, we go to `bevy_pbr/forward_io.wesl`. It contains a struct named `VertexOutput`. +@fragment +fn fragMain(v: VertexOutput) -> @location(0) vec4f { /* ... */ } +``` -Another example +The reference `VertexOutput` in `v: VertexOutput` determines the declaration +path `bevy_pbr::forward_io::VertexOutput`. The module path is +`bevy_pbr::forward_io`, and the final segment refers to a declaration named +`VertexOutput` in that module (a struct, not shown). `bevy_pbr` is a package, +so the module source is the file `forward_io.wesl` in the `bevy_pbr` +package's root directory, or `forward_io.wgsl` if there is no `.wesl` file. +No other file is consulted; if both files are missing, the reference is an +error. -```wgsl +```wesl +// lighting/pbr.wesl import super::shadowmapping; + +fn shade() { + let s = shadowmapping::pcf(); +} ``` -Assume that the current module lives at `shaders/lighting.wesl`. We first go to the super module at `shaders.wesl`. We then look for an item called `shadowmapping` in `shaders.wesl`. -After not finding it, we look for a module `shadowmapping` at `shaders/shadowmapping.wesl`. +The current module is stored at `lighting/pbr.wesl`, with module path +`package::lighting::pbr`. `super` removes the last segment, giving +`package::lighting`. The import binds `shadowmapping` as a shorthand for +`package::lighting::shadowmapping`. The reference `shadowmapping::pcf()` +determines the declaration path `package::lighting::shadowmapping::pcf`, +referring to a function `pcf` declared in `lighting/shadowmapping.wesl` (not +shown). ## `wesl.toml` -The [`wesl.toml`](WeslToml.md) file provides linker configuration options affecting the import resolution algorithm. It can customize: +The [`wesl.toml`](WeslToml.md) file provides linker configuration options affecting import resolution. It can customize: * The root directory, * Available package dependencies, * A file whitelist and/or blacklist. ## Filesystem Resolution -To resolve a module on a filesystem, one follows the algorithm above. +To resolve a module on a filesystem, one follows the mapping in +[Resolving a declaration path](#resolving-a-declaration-path). The root folder, or the root module, needs to be provided to the linker. This is currently a linker-specific API, and may change once we introduce a `wesl.toml`. Linkers should fall back to `.wgsl` files when a `.wesl` file cannot be found. @@ -209,17 +314,18 @@ type_specifier: | full_ident ... ``` -When resolving inline imports, we can also use modules that were imported. This means that the import resolution algorithm is extended to -1. We start with the first segment. - * ... - * `ident` refers to an identifier that is in scope. If it is a module, we start with that. If it is not a module, it is an error. If there is none, we fall back to the packages. +In an inline declaration path, the first segment may also be a name bound by +an import, which expands to its import path. Otherwise, the first segment +anchors the path as in +[Resolving a declaration path](#resolving-a-declaration-path): `package`, +`super`, or a known package name. -Examples +### Examples ```wesl import foo::bar; fn main() { - let a = bar::baz; // Uses bar from the import above + let a = bar::baz; // bar expands to foo::bar, from the import above let b = bevy::main(); // Uses the known bevy package } ``` @@ -231,13 +337,13 @@ However, the following is still illegal. ```wesl // foo.wesl -import bar::b; +import package::bar::b; const a = b + 1; ``` ```wesl // bar.wesl -import foo::a; +import package::foo::a; const b = a + 1; ``` @@ -245,6 +351,219 @@ 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 current 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 +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 module +import bevy::prelude::*; +import wgsl_test::expect::*; + +// wildcard import from within the current 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.** A future re-exports mechanism (TBD) could +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 should not 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 + +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 + +The table below summarizes import errors and warnings. Resolution failures +and genuine collisions cannot be suppressed; other diagnostics are +suppressible via `@diagnostic`. + +| Situation | Behavior | +| --- | --- | +| Reference fails to resolve (missing module source, or no such declaration) | Error | +| Local declaration conflicts with named import | Error | +| Named import conflicts with named import | Error | +| Wildcard imports provide different declarations for the same referenced name | 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 package::foo::*; // exports clashing_zap +import package::bar::*; // exports a different clashing_zap + +fn main() { + let x = clashing_zap(); // error: ambiguous between the two clashing_zap declarations +} +``` + +The fix is to disambiguate with a named import (`import package::foo::clashing_zap;`, +or `import package::foo::{clashing_zap, *};` to keep the wildcard) or an +[inline declaration path](#inline-usage) (`package::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 +highest-precedence one wins. The table in +[Import errors and warnings](#import-errors-and-warnings) lists the overlaps +that produce a warning or an error; other overlaps resolve silently. + +1. user declarations and named imports (non-wildcard) +2. wildcard-imported names +3. 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. + +### Module and package names form a separate namespace from declarations + +In a reference, a path segment followed by `::` names a module or a package, +and a bare name refers to a declaration. So a declaration may share its name +with a package without ambiguity: + +```wesl +import light::foo; + +const bar: light::foo = 0; // `light::` unambiguously names the package + +fn light() {} // no conflict with the package name +``` + +Wildcard imports preserve this separation: they bring in only the imported +module's top-level declarations, never module or package names. + +Within the namespace, an import binding shadows a package with the same name: +a first segment refers to a bound name when one is in scope, and otherwise to +a package (see [Inline Usage](#inline-usage)). Tools may warn about the +shadowing. + ## Directives Under discussion, see: @@ -267,28 +586,28 @@ 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() { } +```wesl +// main.wesl: +import package::foo::bar; +fn main() { bar(); } + +// foo.wesl: +import package::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 -```wgsl -import foo::bar; +```wesl +import package::foo::bar; -// Only the foo::bar::baz module would bring in its const assertions +// Only the package::foo::bar::baz module would bring in its const assertions const a: u32 = bar::baz::hello; ``` @@ -357,7 +676,7 @@ The Bevy team, with a large shader codebase, had a few wishes To fully copy Rust's importing syntax, one needs something akin to a `mod` statement. The rules have carefully been architected to imitate the Rust style, while not requiring an explicit `mod` statement. -In Rust, `use foo::bar;` could either map to "import an item called `bar` from `foo.rs`" or it could map to "import the module `foo/bar.rs`". Rust uses the explicit `mod` statement to disambiguate. We instead check for the presence of an item. +In Rust, `use foo::bar;` could either map to "import an item called `bar` from `foo.rs`" or it could map to "import the module `foo/bar.rs`". Rust uses the explicit `mod` statement to disambiguate. We instead decide at each reference: a bare `bar` is the item, and `bar::baz` reaches into the module. ## Putting exports in comments This would have the advantage of letting some existing WGSL tools ignore the new syntax. For example, a WGSL formatter would not need to know about imports, and could just format the code as usual. diff --git a/ImportsDesign.md b/ImportsDesign.md new file mode 100644 index 0000000..1a395db --- /dev/null +++ b/ImportsDesign.md @@ -0,0 +1,92 @@ +# Imports Design + +This document records design decisions behind WESL's import system. The +normative spec lives in [Imports.md](Imports.md). + +## Why the `@!` module attribute form? + +`@!wildcardable` is module-level metadata. WESL will likely want module-level +annotations for other module-scoped features, and libraries and users will want +a place to attach their own metadata to a whole module. A general-purpose syntax +for module metadata avoids inventing one ad-hoc form per feature. + +`@!wildcardable` mirrors the item-level attribute convention (`@group`, +`@binding`, `@if`, `@diagnostic`, ...), but the `!` marks the attribute as +scoped to the whole module. Unlike an item attribute, it doesn't attach to a +following element, so a trailing `;` terminates it. + +Module attributes sit below the imports so that they can use imported +names (for example a hypothetical `@!play_version(2);`). + +See [`@!wildcardable` annotation](Imports.md#wildcardable-annotation) for the +normative spec. + +## Aren't wildcards an anti-pattern? + +Many language communities discourage wildcard imports. TypeScript, Go, Zig, and +Carbon disallow wildcards entirely or restrict them to narrow cases. Java and +Rust permit them syntactically but discourage broad use by convention; Rust's +`prelude` modules are one curated pattern in that style. The general concerns +are practical: + +- **Traceability.** Direct imports make it obvious where a name comes from. + Wildcards push that work onto the reader, the language server, or the + compiler's name-resolution diagnostics. +- **API stability.** Adding a public item to a wildcard-imported module can + conflict with downstream declarations or with other wildcard imports. Stacked + wildcards across a dependency tree can create conflicts the end user neither + caused nor can easily fix. + +The WESL environment adds further concerns: + +- **Cross-ecosystem publishing.** WESL libraries can be published into multiple + host ecosystems (npm, crates, etc.), and the language's stability rules have + to work for all of them. npm in particular treats minor/patch breakage as an + upstream bug, so wildcard-driven conflicts on additive package updates would + be read there as buggy packages, not as users accepting a WESL-specific + tradeoff. The defaults can't be split per-ecosystem; even libraries that + aren't actively cross-published inherit the same rules. +- **Mixed-language ownership.** In host applications, dependency updates are + often routine maintenance handled by someone other than the shader author. A + wildcard conflict can land on a teammate who did not cause it and may not be + best positioned to fix shader-side breakage. +- **Shader test coverage.** Shader test coverage is often thinner than + application-code coverage, and some failures are visual or runtime-dependent. + Fewer tests and WGSL's comparatively small type system mean that wildcard + conflicts are less likely to be caught at the moment a dependency is updated. +- **Single namespace.** WGSL has a single namespace for types and values, and no + namespace construct or object-style surface to limit the scope of wildcarded + names after import. There are fewer places for names to coexist harmlessly. + +These concerns motivate guardrails for WESL wildcard defaults. + +## Wildcards in WESL: when to allow, when to gate + +WESL keeps wildcards available because some libraries are designed to feel +pervasive. Game engines, test frameworks, and math libraries expect a +domain-specific API where prefixing every call with `test::expect::` or similar +would obscure the shader rather than help it. Concise import syntax matters even +where an IDE can autocomplete: not every editor has a language server, and long +import blocks add noise regardless of how they were typed. + +But the concerns in +[Aren't wildcards an anti-pattern?](#arent-wildcards-an-anti-pattern) still +apply, especially across package boundaries. WESL's defaults try to keep the +benefits while limiting the risk: + +- **Not every public module suits wildcards.** Modules with a fast-growing API + or with generic names (`Buffer`, `Result`) are fine to import by name but + hazardous to wildcard. +- **Authors can signal which modules are curated for wildcards.** An explicit + `@!wildcardable` marker lets library authors tell consumers (and tools) which + modules they've designed for wildcard use. It also gives tooling a hook for + lints around generic names, builtin shadowing, churn-prone additions, etc. +- **Defaults shape the ecosystem.** Red/yellow squiggles and linter messages + teach safe wildcard practice to new and part-time shader authors more reliably + than community blogs or documentation. +- **Advanced users are not blocked.** Within a package, wildcard imports are + unrestricted; externally, wildcard-importing a non-`@!wildcardable` module is + possible via + [standard diagnostic controls](Imports.md#suppressible-diagnostics). The + default tunes the path of least resistance, but doesn't block users who + intentionally accept the risk.