diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7664fec --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +# macOS +.DS_Store + +# vi temp files +*.swp diff --git a/Imports.md b/Imports.md index 089e8c8..c95fbce 100644 --- a/Imports.md +++ b/Imports.md @@ -70,7 +70,7 @@ translation_unit: | import_statement* global_directive* global_decl* import_statement: -| attribute* 'import' import_relative? (import_collection | import_path_or_item) ';' +| attribute* 'public'? 'import' import_relative? (import_collection | import_path_or_item) ';' import_relative: | 'package' '::' | 'super' '::' ('super' '::')* @@ -85,8 +85,8 @@ import_collection: ``` Where `translation_unit` and `ident` are defined in the WGSL grammar. -`ident`s must not be current WGSL keywords. `ident`s also must not be -current WESL keywords: `as`, `import`, `module`, `package`, `super`, or `self`. +`ident`s must not be current WGSL keywords. `ident`s also must not be +current WESL keywords: `as`, `import`, `module`, `package`, `private`, `public`, `self`, or `super`. Reserved words that are not current keywords are allowed, but not recommended. @@ -98,6 +98,9 @@ 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. +The optional `public` prefix also re-exports the imported names under the importing module's path; see +[Visibility.md](Visibility.md). + WESL also extends WGSL's `global_directive` rule with a `module_declaration` form, used by `@wildcardable` (see [Wildcard imports](#wildcard-imports)) and reserved for future module-level metadata. `attribute` is the WGSL attribute rule. ```ebnf @@ -125,22 +128,18 @@ import a::c::e as f; Then, one iterates over each segment from left to right, and looks it up one by one. 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. + * `super` refers to the parent module, so `super::sibling` reaches a module alongside the current one. Can be repeated to climb further; climbing out of the current package is an error. * `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. + 1. Item in current module (declared or re-exported via `public import`): Take that item. We must be at the last segment, otherwise it's an error. 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` + 3. Else go to `current module path/ident.wesl` (or `.wgsl` if `.wesl` is not found) * 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.) -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 steps above resolve a path; whether the referencing module may then use the result is governed by [visibility](Visibility.md). Once the import has been resolved, the last segment, or its `as` alias, is brought into scope. @@ -174,10 +173,22 @@ The [`wesl.toml`](WeslToml.md) file provides linker configuration options affect * A file whitelist and/or blacklist. ## Filesystem Resolution -To resolve a module on a filesystem, one follows the algorithm above. -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. +To resolve a module on a filesystem, one follows the [import resolution +algorithm](#import-resolution-algorithm). It traces a path from a known starting +directory (a package's root directory, or for `super` an enclosing directory of +the current module) through any subdirectories to the module's `.wesl` or +`.wgsl` file. The starting directory is always known: each module's path is +fixed relative to its package's root directory, which the linker takes from a +`wesl.toml` file or its own configuration. + +### `package.wesl` + +`package.wesl` is the file backing a package's top-level module, placed at the +package's root directory. Items declared in or `public import`ed from +`package.wesl` are reachable as `::item`. + +### Reserved file names Due to filesystem limitations, it can happen that WESL idents are invalid file or folder names. Notable examples are `CON, PRN, AUX, NUL, COM1 - COM9, LPT1 - LPT9` on Windows, and Windows being case-insensitive. @@ -323,7 +334,7 @@ version bumps but can break users who have local declarations or import other - **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 +**Compose with re-exports.** See [Re-exports](Visibility.md#re-exports) to collect items from other modules into a single `@wildcardable` module for user convenience. @@ -487,11 +498,6 @@ Linkers may choose to do dead code elimination, but it is a non-observable imple `const_assert` statements inside of functions need special treatment, see relevant section. -## Visibility -Everything is public by default. - -Future proposals will introduce visibility (privacy) for items and/or modules. - # Drawbacks Are there reasons as to why we should not do this? diff --git a/Visibility.md b/Visibility.md index 8c3c961..5807b68 100644 --- a/Visibility.md +++ b/Visibility.md @@ -1,104 +1,257 @@ -# Visibility Control -This section will describe WESL enhancements to control which WGSL elements are visible to importers. - -* how to re-export elements so that they're visible with a different path or name? -* controlling host visible names like entry points and overrides? -* Should export allow `as` renaming? -* Why not export struct Foo? - * Many current WGSL parsers (including wgpu's naga) would - choke on the unknown attribute as is and feels like having - two export forms is a bit inconsistent. -* Consider changing to public by default for imports within the package only. - * Matches semantics when importing from WGSL code - * no annotation effort for tiny projects, everything public is fine. - * (Private by default gives a gentle push to programmers to consider their api every time - they add a public annotation.) - * (And the path of laziness leads to undersharing, - which is safer from a maintenance point of view.) - * (less consistent with package visibility.) - * (unexpected if programmers are accustomed to e.g. JavaScript imports.) - -## Export -One natural extension is to add explicit exports. -For one, this would allow library authors to hide certain functions or structs from the outside world. -It would also enable re-exports, where a library re-exports a function from another library. - -There are two variations of exports, which could be combined like in Typescript - -### Exporting a list of items -A standalone export statement simply specifies which globals are exported. -Then, imports can be checked against the list of exports. This is very easy to parse and implement. - -```wgsl -export { foo, bar }; +# Visibility + +Visibility controls which other modules can reference or re-export an item, and +which shader-interface declarations WebGPU pipeline creation APIs can use. + +| Visibility | Cross-module access | Re-exportable | Pipeline-visible (root module) | +| --- | --- | --- | --- | +| `@public` | Any package | Yes | Yes | +| *package* (default) | Same package only | No | Yes | +| `@private` | Declaring module only | No | No | + +## Module visibility + +Every WESL item has one of three visibility levels: + +* `@public`: visible from any package. +* *package*: visible from any module in the same package. +* `@private`: visible only within the declaring module. + +A module can use an item from another module (named through `import`, or through +an inline path such as `other_pkg::math::dot2`) only if that item is visible to +it. + +### Syntax + +Visibility is set with a `@public` or `@private` attribute on the declaration: + +```wesl +fn helper() { ... } // package (default) +@public fn dot2(...) -> f32 { ... } // public +@private const scratch_size: u32 = 64; // private +``` + +`@public` and `@private` are WGSL attributes, so the grammar accepts them in any +position among a declaration's other attributes. By convention, place the +visibility attribute last, immediately before the declaration keyword: + +```wesl +@group(0) @binding(0) @public var data: array; +``` + +There is no `@package` attribute: an item with neither `@public` nor `@private` +is *package*-visible. (*package* is written in italics here as a level name, not +an attribute name.) A declaration may carry at most one visibility attribute. + +### Grammar + +WESL adds two attributes to WGSL's attribute set: + +```ebnf +visibility_attribute: +| '@' 'public' +| '@' 'private' +``` + +`@public` and `@private` extend WGSL's `attribute` non-terminal and follow the +same syntax and any-order placement as other attributes. They are accepted on +the global declaration forms WGSL accepts attributes on (`global_variable_decl`, +`global_value_decl`, `function_decl`, `struct_decl`, `type_alias_decl`); a +`const_assert_statement` declares no name and accepts no visibility attribute. +At most one visibility attribute may appear on a single declaration. + +See the +[WGSL recursive descent grammar](https://www.w3.org/TR/WGSL/#grammar-recursive-descent) +for WGSL's full attribute production. + +### Referring to less-visible declarations + +Visibility governs where a declaration can be named, not which other +declarations it may mention. An `@public` declaration may name a less-visible +one: an `@public fn` can return or accept a *package* struct, and an `@public +struct` can have a field of a less-visible type. A consumer that cannot see the +type can still use a value of it (call the function, read its fields) but cannot +name the less-visible type to declare a variable, import it, or construct a new +value. + +WESL publishing tools should warn when an `@public` declaration mentions a +less-visible type in its signature or fields; the warning is suppressible with +`@diagnostic(off, leaked_type)` on the declaration. + +## Re-exports + +`@public import` re-exports the imported names under the current module's path, +in addition to bringing them into local scope. + +```wesl +// my_lib/prelude.wesl +@wildcardable module; // lets external importers use `prelude::*` + +@public import super::math::{dot2, cross2}; +@public import super::types::Mesh as M; +``` + +After this, importers can write: + +```wesl +import my_lib::prelude::dot2; +import my_lib::prelude::M; +import my_lib::prelude::*; // wildcard pulls all of the above ``` -And when one wants to export everything defined in the current file, they can use the `*` syntax. +(Bare `import` brings names into local scope without re-exporting.) -```wgsl -export *; +### Re-export identity + +Re-exports add reachable paths but do not create new declarations. An item's +identity is its absolute module path: the package name, the chain of module +names to the declaration site, and the declared name (for example +`my_lib::types::Mesh`). A re-export's reachable name (the last segment of the +re-export path, or its `as` alias) can differ from the declared name, but +identity follows the original declaration. Two paths that resolve to the same +identity collapse with no name collision and no duplicated emission. +Re-exporting one item under several aliases (`@public import super::types::Mesh +as MeshA;` alongside `... as MeshB;`) just adds reachable names; all of them +resolve to the single declaration. + +### Only public items can be re-exported + +A `@public import` re-exports a name at `@public` visibility. The original +declaration must already be `@public`; `@public import` of a *package* or +`@private` item is an error. + +```wesl +// my_lib/internal.wesl +fn helper() { ... } // package (default) + +// my_lib/prelude.wesl +@public import super::internal::helper; // error: helper is package +``` + +## Pipeline visibility + +WESL translation always starts from a single root module, which defines the +entire pipeline-visible API: the shader declarations available to WebGPU +pipeline creation APIs. Only three kinds of shader declarations can become +pipeline-visible in `createRenderPipeline` and `createComputePipeline` calls: +**entry points**, **resource variables**, and **pipeline-overridable +constants**. + +A pipeline-relevant declaration is in the pipeline-visible API when the root +module declares it with `@public` or *package* visibility, or when the root +module `@public import`s it from another module. A bare `import` in the root +module brings a declaration into local scope but does not add it to the +pipeline-visible API. + +A pipeline-relevant item +[statically accessed](https://www.w3.org/TR/WGSL/#statically-accessed) from the +pipeline-visible API's dependency graph but absent from the pipeline-visible API +is a link error (except for overrides with a default value; see below). The +check starts from pipeline-visible declarations and follows references +transitively. + +The `import` statement itself does not count as a static access; the imported +item counts as statically accessed when an identifier referencing it appears in +a body, type, initializer, or attribute. + +Items declared in the root file keep their root-namespace name in the linked +WGSL output. A non-renamed `@public import` exposes the item under its original +name; a renamed re-import (`@public import some_pkg::pbr_fragment as my_frag;`) +appears as `my_frag` in the pipeline-visible API. + +Libraries cannot directly add to the pipeline-visible API. Instead, libraries +publish `@public` declarations for the root module to `@public import`. + +### Entry points + +An entry point is a function with `@vertex`, `@fragment`, or `@compute`. An +entry point is in the pipeline-visible API only if the root module declares it +or `@public import`s it. Non-entry helper functions are never selectable by +WebGPU pipeline creation merely because they are `@public` or *package*-visible. + +```wesl +// pbr_lib/passes.wesl +@fragment @public +fn pbr_fragment() -> @location(0) vec4f { ... } + +// app/main.wesl (root) +@public import pbr_lib::passes::pbr_fragment; // host can select this entry point ``` -To re-export an item, one can use the same syntax. +### Resource variables + +A [*resource variable*](https://www.w3.org/TR/WGSL/#resource-interface) is a +`@group/@binding var<...>` declaration that lets host code provide values to the +shader (uniforms, storage buffers, textures, samplers). A resource variable is +in the pipeline-visible API only if the root module declares it or +`@public import`s it. Resource variables have no host-side fallback, so a +*resource variable* statically accessed but absent from the pipeline-visible API +is a link error. + +```wesl +// filter_wgsl/package.wesl +@group(3) @binding(0) @public var data: array; -```wgsl -import my/lighting/{ pbr }; +@compute @workgroup_size(64) @public +fn blur(@builtin(global_invocation_id) id: vec3u) { + data[id.x] = ...; +} -export { pbr }; +// app/main.wesl (root) +@public import filter_wgsl::blur; +// error: resource variable `filter_wgsl::data` is used but absent from the pipeline-visible API +// fix: add `@public import filter_wgsl::data;` to the root module ``` -### Exporting as an attribute -Exports can also be added as an attribute to the item itself. +The re-import does not create a new resource variable; the original +`@group(3) @binding(0)` annotations carry through unchanged. + +### Pipeline-overridable constants + +A [*pipeline-overridable constant*](https://www.w3.org/TR/WGSL/#override-decls) +is an `override` declaration the host may set at pipeline creation. An override +is in the pipeline-visible API only if the root module declares it or +`@public import`s it. + +An override with a default value that is absent from the pipeline-visible API +silently degrades to a constant: the linker bakes in the default and the host +cannot set it. An override without a default value has no such fallback, so an +override statically accessed but absent from the pipeline-visible API is a link +error. -```wgsl -@export -struct Foo { - x: f32; +```wesl +// pbr_lib/lighting.wesl +@public override sun_intensity: f32 = 1.0; // has default +@public override max_lights: u32; // no default +@public fn apply_lighting() -> vec4f { ... } // uses both overrides + +// app/main.wesl (root) +import pbr_lib::lighting::apply_lighting; +@public import pbr_lib::lighting::max_lights; // host must set + +@fragment +fn fragment_main() -> @location(0) vec4f { + return apply_lighting(); } -@export -fn foo() {} +// sun_intensity not in the pipeline-visible API: linker bakes in 1.0 ``` -This is more user friendly, but also more complex to parse. It requires a partial parsing of the WGSL file to find the exports and their names. -A future export specification would include the minimal WGSL syntax that is necessary to implement this. - -## Translating Source File Paths to Import Module Paths -Tools that look at source code will refer to a `package_root` in `wesl.toml` that defines -the common prefix of `.wesl` and `.wgsl` files. - -Source directories and files under the `package_root` and map directly to module paths -under the package name as expected. -e.g. - -* Source file `C:\Users\lee\myProj\wgpu\foo.wesl` contains `export fn bar() {}` -* `wgpu` is the `project_root` in `wesl.toml`. -* The project as published as package `fooz`. -* Other projects can write `import fooz/foo/bar;` to use `bar()`. - -### `lib.wgsl` -If there is a file named `lib.wgsl` in the `package_root` directory, -any public exports in `lib.wgsl` are visible at the root of the module. -e.g. if a package ‘pkg’ has a source file `pkg_root/lib.wgsl` -that contains `export fn fun()`, -a module file in another package can import that function with `import pkg/fun`. - -## Libraries and Internal Modules Need Privacy -We want library publishers to decide carefully what to expose as -part of their public api, so they can upgrade private parts of the library safely. -Smooth upgrades are valuable to the library ecosystem. - -Authors of significant internal modules will similarly want -to make a public vs private distinction to reduce maintenance effort as -the internal module evolves. - -### Private by Default -We propose that importable elements like functions and structs -be private by default, (i.e. inaccessible from import statements). -The programmer needs to add an annotation to make them public, (i.e. available to import). -The programmer can decide whether the element should be public within the package only -or also public to importers from other packages. Perhaps `@export` and `@export(public)`. - -Importable elements from (unenhanced) WGSL code may be imported from WESL functions -in the same package. Elements in WGSL code are not public from other packages -(WESL code may reexport WGSL element for package publishing). +### Aggregating entry points + +When an app's entry points live in multiple source files, a small root module +brings them together. The entry points must be declared `@public` so the root +can re-export them (see +[Only public items can be re-exported](#only-public-items-can-be-re-exported)): + +```wesl +// app/vertex.wesl +@vertex @public fn vertex_main(...) -> @builtin(position) vec4f { ... } + +// app/fragment.wesl +@fragment @public fn fragment_main(...) -> @location(0) vec4f { ... } + +// app/main.wesl (root) +@public import super::vertex::vertex_main; +@public import super::fragment::fragment_main; +``` diff --git a/VisibilityDesign.md b/VisibilityDesign.md new file mode 100644 index 0000000..5f5510e --- /dev/null +++ b/VisibilityDesign.md @@ -0,0 +1,263 @@ +# Visibility Design +This document records design decisions behind WESL's visibility system. The +normative spec lives in [Visibility.md](Visibility.md). + +## Contents + +* [Why three levels and not two](#why-three-levels-and-not-two) +* [Why three levels and not four or more](#why-three-levels-and-not-four-or-more) +* [Why package by default?](#why-package-by-default) +* [Why `@public` and `@private`?](#why-public-and-private) +* [Why re-exports cannot widen](#why-re-exports-cannot-widen) +* [Future possibilities](#future-possibilities) + +## Why three levels and not two + +A two-level system (public / private) is feasible: Zig gets by with just `pub` +and an unannotated default, and small projects rarely miss more. Three levels +pay off because the package boundary and the module boundary are genuinely +separate questions every author asks ("what's part of my external API" vs. +"what's shared within the package"), and with only two levels they collapse into +one. + +The result is cheaper internal refactors and a smaller external API. With a +middle level, a cross-file helper stays inside the package without inflating +what consumers depend on, and moving it between internal files is not a breaking +change to anyone outside. With only public/private, that helper is either +exposed (its path becomes part of the contract) or file-local (no cross-file +sharing at all). + +WGSL compatibility nudges in the same direction. WGSL has no visibility +keywords, so an unannotated `.wgsl` file participates with whatever the default +is, and a middle level is the only default that gives sensible behavior here +(see [Why package by default?](#why-package-by-default)). + +The keyword set has plenty of prior art: roughly Rust's `pub(crate)`, Java's +package-private (default), Scala's `private[pkg]`, Swift's `package` (5.9). +Kotlin, Slang, and Swift also offer `internal` at their compilation-unit +granularity (Kotlin module, Swift module, Slang module). Languages that stop at +two keyword levels often recover the third structurally: Go's `internal/` +directories restrict a subtree to its enclosing package, and TypeScript packages +routinely keep a curated public entry point (an `exports` map plus a barrel +`index.ts`) distinct from the modules they `export` for cross-file use but never +re-export. + +## Why three levels and not four or more + +WESL stops at three because: + +* **Shader packages are relatively small.** A typical WESL package is on the + order of tens of files, not the thousands a Rust crate or C# assembly might + contain. Finer-than-package visibility levels (Rust's `pub(super)` and + `pub(in path)`, Swift's `fileprivate`) pay off when refactoring large + codebases; at the WESL scale, the cost of the extra concept exceeds the + benefit. +* **The two boundaries that matter most are the package boundary and the module + boundary.** "What's part of my external API" and "what's an implementation + detail of this module" are decisions every author makes. Levels in between are + nice to have in large codebases but not essential. +* **Less for authors to learn.** Two attributes (`@public`, `@private`) plus an + unannotated default. An author can hold the whole visibility model in their + head without referring back to docs. + +## Why package by default? + +There are three plausible defaults (public, private, package), and prior art has +examples of each as the default for at least one mainstream language. + +### Why not public by default? + +First, public by default works against deliberate modularity, especially +important for libraries. The default would encourage authors not to bother with +a conscious choice about what's part of the contract. Accidentally exposing too +much silently leaks `clamp_to_unit` helpers, scratch constants, and internal +structs as part of the library's external API. Refactors that rename or remove +an unmarked helper become breaking changes for downstream callers. + +Second, ceremony lands on the wrong case. Most items in a library aren't part of +the external API, so the author would have to write `@private` in the common +case instead of `@public` on the rare case. + +### Why not private by default? + +Making private the default favors strict encapsulation: every cross-file use +requires an explicit visibility marker. That discipline is attractive for +libraries with stable APIs and large codebases. It's less of a fit for typical +WESL projects, which tend to be small and internal. Four arguments push WESL +toward package as the default: + +* **Encapsulation gains are smaller at WESL scale.** A typical WESL project is a + handful of files: a vertex/fragment pair, a compute shader, perhaps a few + helper modules. The author already knows the whole codebase, so the + disciplined boundaries that pay off in larger code don't add as much value. + +* **Don't force visibility decisions on every author.** Any non-trivial module + shares at least some declarations with other modules in the package (helpers, + types, constants). With private as the default, every author would have to + mark those items explicitly, even authors who don't otherwise care about + encapsulation. Package as the default makes encapsulation discipline available + to projects that want it (via `@private`), without forcing typical small + projects to do the extra work. + +* **WGSL files can be imported without modification.** WGSL has no visibility + keywords. With package as the default, a `.wgsl` file dropped into a WESL + project has all of its top-level declarations available to the rest of the + package, exactly as a user would expect. With private as the default, every + WGSL file would need to be edited or re-annotated before its declarations were + reachable, which would undermine the WESL goal of working smoothly with + existing WGSL code. + + WESL could distinguish WESL from WGSL by file extension and give each a + different default. WESL doesn't: it weakens the "WESL is a superset of WGSL" + principle, and shader source loaded as a string (without a file extension) + would still need some other way to mark the dialect. + +* **Root module entry points would need explicit markers.** A root module's + entry points, resource variables, and overrides reach the host only when + non-private (see [Visibility.md](Visibility.md)). With private as the default, + every one would need a visibility marker. A plain `.wgsl` file used as the + root wouldn't work, it would expose nothing to the host. WESL could give root + modules a different default to avoid this, but then the root behaves unlike + every other module. With package as the default, `@fragment fn frag_main(...)` + at root is pipeline-visible with no extra annotation, and only `@private` + changes anything at root. + +### Package by default + +Package as the default keeps the external API explicit while leaving internal +sharing free. The `@public` marker stays meaningful and rare, so internal +helpers don't accidentally leak as part of the external API. Authors aren't +forced to make visibility decisions on every cross-file use, and plain WGSL +files participate without modification. + +## Why `@public` and `@private`? + +Two choices shape the visibility syntax: the names (`public` and `private`) and +the form (an attribute, not a bare keyword). + +### The names: `public` and `private` + +`public` and `private` are plain English words used as visibility markers in +most languages with a visibility system (Java, C++, C#, Kotlin, Scala, Swift). +They form a symmetric pair and read clearly to newcomers regardless of +background language. + +### The form: an attribute, not a keyword + +WGSL reserves `public` but not `private`. WGSL's +[reserved-words list](https://www.w3.org/TR/WGSL/#reserved-words) includes +`public`, `pub`, `priv`, `package`, `export`, and `protected`; `private` is +absent. Adopting `public` and `private` as bare visibility keywords would +require WGSL to also reserve `private`, which would break any existing shader +that uses `private` as an identifier and requires coordination with the WGSL +working group. WGSL attributes are an open namespace by design, so `@public` and +`@private` slot in as a symmetric pair without coordination. + +The attribute form also matches how WESL already extends WGSL. Other +declaration-level metadata (`@wildcardable`, `@diagnostic`) is expressed as +attributes, and WGSL itself uses attributes for `@vertex`, `@workgroup_size`, +`@group`, `@binding`, and more. The cost is a small amount of visual noise (an +extra `@` on every visibility marker); shaders are already attribute-heavy +enough that this fits the local style. + +WESL does not pin a position for the visibility attribute in the grammar, just +as WGSL does not pin a position for `@group` or `@workgroup_size`. By convention +(a formatting rule, not a grammar rule), the visibility attribute sits last, +immediately before the declaration keyword, so a reader scanning down the left +margin can find it without hunting through the attribute list. + +### Alternatives considered + +**`pub` / `priv` (Rust).** WGSL reserves both `pub` and `priv`, so they could be +adopted as bare keywords without coordination. `public` still reads more clearly +to newcomers and to authors coming from non-Rust languages, and `priv` has +little prior art outside early Rust, which dropped it. + +**`export` / `hide` (TypeScript).** TypeScript uses `export` to mean "this +declaration is visible outside this module." That works in TS because TS has +only two visibility levels at the module boundary (exported / not), and because +TS modules are the privacy unit (there is no package-internal level). WESL has +three levels and needs both extremes nameable, so `export` would have to be +paired with a separate `@hide` (or similar). Keeping declaration and visibility +orthogonal (a declaration always declares; an optional attribute sets +visibility) is cleaner. + +**Capitalization (Go).** Go encodes visibility in the first letter of an +identifier: capital means exported, lowercase means unexported. The rule is +elegantly minimal but locks the language to two levels (see +[Why three levels and not two](#why-three-levels-and-not-two)). It would also +impose a retroactive visibility semantics on existing WGSL identifiers, which +follow no convention tying capitalization to visibility: a `.wgsl` file dropped +into a WESL project would have its visibility silently determined by how its +names happen to be cased. + +## Why re-exports cannot widen + +A `@public import` cannot widen visibility: re-exporting a *package* or +`@private` item as `@public` is a hard error. + +The reason is that the visibility attribute on a declaration is meant to be a +local contract. An author looking at a *package* item should be able to rely on +the attribute without scanning every `@public import` in the package for a +republication that would silently widen it. Widening would turn the attribute +into a hint rather than a guarantee: an author refactoring what they believed +was an internal helper could break downstream callers who reached it through a +widened re-export elsewhere in the package. + +The cost of the rule is that items exposed through a curated prelude must be +declared `@public` at their definition, leaving the original module path +reachable too. Restricting reach to a single canonical path is sketched under +[Future possibilities](#future-possibilities). + +## Future possibilities + +### `@package import` + +Only `@public import` is specified for now. A `@package import` form could cover +a "re-export within the package, hide externally" use case. + +### `@package` attribute on declarations + +A `@package` attribute would make the middle level explicit on a declaration, +rather than leaving it implied by the absence of `@public` or `@private`. The +unannotated form covers every present use, so the attribute isn't part of the +current design; it would mainly help where an author or a tool wants to see that +a `package` level was chosen deliberately rather than left unmarked. + +### Submodule re-export + +`@public import` re-exports individual items, not whole submodules. Module +re-export would let a package present an internal `mylib::internal::math` module +under a cleaner path like `mylib::math`. + +### Canonical prelude path + +The curated-prelude pattern leaves an item's original module path reachable +alongside the prelude path, so internal module layout is part of what consumers +can reach. A `@canonical` annotation on the re-export could mark the prelude +path as the intended one, letting tooling steer consumers to it (and flag use of +the original path) without any change to name resolution. + +### re-export widening from root + +A plain `.wgsl` file with an entry point cannot be aggregated into a root module +via `@public import` without modification (see +[Aggregating entry points](Visibility.md#aggregating-entry-points)). A +relaxation would let the root module `@public import` *package* items from the +same package, since the root defines the package's external and pipeline-visible +API. The cost is loss of orthogonality: what `@public import` accepts would +depend on whether the importer is the root. + +### Wildcard re-export + +`@public import path::*` would re-export every public item of a target module. +It is deferred because resolution would have to walk a set of modules +recursively (modules can form cycles, so resolution would need to track visited +modules), and it interacts awkwardly with potential future parameterized modules +design, while enabling nothing that named re-exports cannot already express. Two +questions need answers before it could be specified: how latent collisions +across a library's wildcard re-exports are caught at publish time rather than by +consumers, and what happens when wildcard re-exporting from a module that itself +has wildcard imports. Publishing tools (see +[#183](https://github.com/webgpu-tools/wesl-spec/issues/183)) could also expand +wildcard re-exports at publish time into explicit named re-exports. diff --git a/VisibilityLanguages.md b/VisibilityLanguages.md new file mode 100644 index 0000000..4b95c5e --- /dev/null +++ b/VisibilityLanguages.md @@ -0,0 +1,113 @@ +Survey of visibility, re-export, and wildcard handling. +[Visibility.md](Visibility.md) and +[VisibilityDesign.md](VisibilityDesign.md). + +_this is background for reviewers, not a published part of the spec, remove before merging_ + +Carbon +- 3 levels at present: library private, file private, public +- uses a separate api file +- cross-package re-export is banned +- no wildcards + +Go +- 2 levels +- the directory is the package boundary (subdirectories are separate packages) +- partial 3rd level: `internal/` directories scope packages to a subtree +- first letter determines visibility; convention is private by default +- no re-exports +- has wildcards (`import . "pkg"`), strongly discouraged except for tests + +Java +- 4 levels: public, protected (= package + subclasses), package-private + (default), private +- JPMS adds module-level visibility; `requires transitive` propagates modules + only, not items +- no item-level re-exports +- `@VisibleForTesting` (Guava, AndroidX) marks members whose visibility has been + widened to enable tests +- wildcards, but forbidden by e.g. Google style guide + +Kotlin +- 4 levels +- `internal` means compilation unit +- public by default +- no re-exports +- no named friends/super/etc. +- wildcards, but largely discouraged (some tools auto-expand) + +Rust +- 5 levels (also `pub(in crate::a::b)`, `pub(super)`) +- private by default +- re-exporting allowed internally and externally; illegal to re-export + package-visible as public-visible +- wildcards, some style guides discourage + +Scala +- 4 levels (`private[pkg]`, `protected[pkg]`) +- visibility control is also hierarchical: `private[pkg.foo]` widens access to + anything inside `pkg.foo`, including all subpackages (it can only widen, never + narrow, plain `private`) +- public by default +- re-exporting, also allows filtering `export printer.{status as _, *}`; +- wildcard imports allowed (also with filtering) + +Slang +- 3 levels: public, private, and `internal` +- `internal` is per module (no package-level visibility control) +- modern modules default to `internal`; legacy modules remain public by default + for compatibility +- re-export via `__exported` (underscore-prefixed, not formally part of the + language) +- module design is still evolving in Slang, e.g. + [issue 9183](https://github.com/shader-slang/slang/issues/9183) +- wildcards by default: `import` flattens into the common namespace; authors can + use `namespace`. (I wonder if this design comes from compatibility goals with + HLSL `#include`.) + +Swift +- six visibility levels: `open`, `public`, `package`, `internal`, `fileprivate`, + `private` (the open/public split governs subclass and override permission; + Swift's `package` means within a Swift Package Manager package (a manifest + grouping several Swift modules); Swift's `internal` means within a single + Swift module, where a Swift "module" is one compile target / library, closer + in role to a WESL package than to a WESL module) +- default is Swift's `internal` (within one Swift module / library), which is + roughly WESL's `package` default; (Swift's `package` is a distinct, wider + level, not the default) +- re-exporting is supported via `public import` and via the unstable + `@_exported import` +- imports flatten into the local namespace by default + +Typescript +- two levels for declarations (+ levels for classes) +- `package.json` `exports` field for package-level visibility; combined with a + barrel file (the `exports` entry point), gives a full 3rd level +- private by default +- re-exporting is supported +- wildcard syntax imports are not flattening (not really wildcards in our sense) + +Zig +- two levels +- private by default (`pub` exports) +- re-exporting is allowed +- `usingnamespace` (the wildcard-ish mechanism) is on the way out + +## Summary + +| Language | Levels | Default | Package-level analog | Re-exports | Wildcards | +| --- | --- | --- | --- | --- | --- | +| Carbon | 3: file-private, library-private, public | api file public; impl file library/file-private | library-private | banned cross-package | none | +| C# | 6: `public`, `internal`, `protected internal`, `private protected`, `protected`, `private` | `internal` (top-level), `private` (nested) | `internal` | none | (not surveyed) | +| C++ | `public`, `protected`, `private` (class-level); modules orthogonal | `private` (class) | none (no module visibility pre-C++20) | (not surveyed) | (not surveyed) | +| Go | 2: exported (capital), unexported | unexported (by convention) | none (package is the unit) | none | `import . "pkg"` (test-only) | +| Java | 4: `public`, `protected` (package + subclasses), package-private, `private` | package-private | package-private (no keyword) | item-level: none; modules via `requires transitive` | yes (style guides discourage) | +| Kotlin | 4: `public`, `internal`, `protected`, `private` | `public` | `internal` (= compilation unit) | none | yes (discouraged) | +| Rust | 5: `pub`, `pub(crate)`, `pub(super)`, `pub(in path)`, private | private | `pub(crate)` | yes, internally and externally (no widening) | yes (some style guides discourage) | +| Scala 3 | `private`, `private[X]`, `protected`, `protected[X]`, public | public | `private[pkg]` | yes (with filtering); no wildcard re-export of packages | yes (with filtering) | +| Slang | 3: public, `internal`, private | `internal` (modern modules); public (legacy) | `internal` (per module) | `__exported` (underscore-prefixed) | imports flatten by default | +| Swift | 6: `open`, `public`, `package`, `internal`, `fileprivate`, `private` | `internal` | `package` (added in 5.9) | `public import`; `@_exported import` (flattening, unstable) | imports flatten by default | +| TypeScript | 2-3: exported, unexported (+ `package.json` `exports`) | unexported | none (module is the unit; `exports` field is the closest) | yes | namespaced under an identifier (never flattened) | +| WGSL | none | always module-public at module scope | none | n/a | n/a | +| Zig | 2: `pub`, private | private | none | yes | `usingnamespace` (on the way out) | +| **WESL** | **3: `public`, *package*, `private`** | ***package*** | ***package* (default, no keyword)** | **`public import` (public items only, no widening)** | **`import path::*` (flattens top-level decls); external needs `@wildcardable`** |