visibility design and discussion#187
Conversation
|
This looks very solid to me and would definitely bring a big usability gain to libraries like lygia. The one thing that doesn't seem be discussed in much detail is
It seems like the docs here don't explain why this approach is chosen over an
Personally I don't have a strong opinion either way, but there is lots of prior art for the second option (most JS package systems, Python, Rust, Lua) while I can't think of anything that has this strict root/non-root separation off the top of my head. |
|
I believe that we should be able to dig up the Rust recommendations for that. IIRC, Rust discourages the For example, if I have 3 tabs open in VSCode and they all say |
|
Also quick and very inconvenient note: |
|
Without wildcards, resolving an import means going to the right module and finding the item. With wildcards, resolving an import means looking through a set of modules to find the right item. With wildcard re-exports, resolving an import means looking through a set of modules recursively to find the right item. And since modules can have cycles, one now needs to keep track of which modules have been visited during import resolution. In the language server, this is a fair amount of extra complexity. This particular feature also, I believe, interacts really badly with macros or other compile time code generation. Rust's import system ends up being a very complex fixed point finding algorithm which may sometimes simply fail. (I think it also interacts with parameterized modules. Since wildcard reexports is the missing piece to make import statements recursive. If imports can cause any sort of computations, which compile time generated code does, and which parameterized modules do, then one gets the halting problem. Or so the story goes.) Usually I wouldn't object to complexity too much. |
Not sure, always interesting to look around tho! Here I was only filling the gap where our current scheme doesn't allow something at the root level. I think small libraries especially might like to enable their users to It'd be a separate issue, but your comments about consistency... I suppose we could revise the existing rules so that |
wat!? good catch. Perhaps |
Worth thinking about. I like the idea of implementing the needed features w/o gilding the lily too much. The cycles/complexity concerns seem heavy for a feature that isn't must-have. And foreclosing options for parameterized modules would be unfortunate. Also I realize that the PR doesn't address some related issues:
I'm thinking best would be to pull wildcard re-exports from this PR. Side note, if we go with the publish tool route as planned with #183, I suppose we could also expand wildcard re-exports like we're planning for wildcard imports. That would help for the cross package cases. |
|
pushed a draft
|
|
We could also go with
Also, what does private mean? Rust goes all in on the hierarchical module system and says
|
I like this option too, a bit more cryptic but if perhaps best if we prefer keywords to attributes.
I like I suppose we could do
just the module, no hierarchy. we could expand hierarchically later, it's a nice idea. But I think of most WESL programs as smallish, so perhaps unnecessary complexity, better to defer. |
|
For the
|
Ah, that was my first instinct too. I like modularity. But I've come to think that package by default for WESL is better. See |
|
Also interesting tidbit: The other style is the global style, where I use 3 access modifiers. Public, internal (protected) and private. And apparently some of the very prolific Rust contributors, including epage who maintains a bunch of extremely well written libraries, vastly prefer the global style. So we are on the right track! https://www.reddit.com/r/rust/comments/1k5yv6p/two_ways_of_interpreting_visibility_in_rust/ |
|
More from Rust land:
They also include statistics which says that |
|
And from Kotlin lang: They went with public by default, only to realize that it might have been a mistake. So defaulting to a more restrictive choice like "internal" seems wise |
|
Note from Java: |
|
We really should ask the WGSL committee regarding the |
|
I dug into it and asked the WebGPU commitee gpuweb/gpuweb#6264 I couldn't quite figure out where it got lost though. It seems to have happened quite a while ago, judging from Naga's source code |
|
I talked to a friend regarding this. |
| transform it into a `const` if its default is a const-expression, or inline its | ||
| initializer at use sites. These strategies are observably equivalent to the | ||
| host. If the `override` doesn't have a default value, it is a link error if the | ||
| `override` is statically referenced from the root module. |
There was a problem hiding this comment.
I suppose one way of properly dealing with override would be to extend WESL to also cover a small bit of the WebGPU API. We'd extend the way createShaderModule works or something along those lines.
| ## Why re-exports cannot widen | ||
|
|
||
| A `@public import` cannot widen visibility: re-exporting a *package* or | ||
| `@private` item as `@public` is a hard error. |
There was a problem hiding this comment.
There is one amusing way of bypassing this restriction.
//- utils.wesl
struct InternalStuff { a: 32 }
//- package.wesl
import package::utils::InternalStuff;
public alias NowItIsPublic = InternalStuff;
There was a problem hiding this comment.
clever and amusing, indeed!
|
Should we see if we can get #183 over the finish line? Edit from the future Old proposal It would look like this @wildcardable
public import bevy::prelude;Upside: No new Sub-modules would be private by default. public import package::lights;
public import package::shadows;
internal import package::utils;Upside: Automodules would no longer be a feature. It adds a lot of complexity to the language server. (I also suspect that the current version of automodules is not implementable in browsers, since it relies on web servers returning a "not found" within a reasonable timeframe.)
@if(DESKTOP)
internal import package::desktop::raytraced_lights as lights;
@if(!DESKTOP)
internal import package::mobile::lights;and now
internal import package::vertex::vertex_main;Upside: Enables in-browser unit test runners. Finally, I took a stab at implementing this. It's about equal in terms of complexity, since this just requires re-exports. |
| 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. |
There was a problem hiding this comment.
New question: If it is an item in the current module, but the item is not accessible, what should happen?
- Throw an error. This always behaves consistently.
- Ignore it and continue on. Downside is that name resolution can now depend on where you're importing something from in a very funky way.
There was a problem hiding this comment.
New question: If it is an item in the current module, but the item is not accessible, what should happen?
- Throw an error. This always behaves consistently.
- Ignore it and continue on. Downside is that name resolution can now depend
on where you're importing something from, in a very funky way.
Option 1 (error). Naming an item that exists but isn't visible to you should be an error. You asked for that specific item, so you should be told you can't have it.
That doesn't block traversal through a same-named submodule, though: if foo has an inaccessible item bar and also a foo/bar.wesl, then foo::bar::baz still resolves into the submodule. Only the terminal foo::bar is the item (and errors if it's inaccessible). Direct resolution (#193) makes this especially clear.
|
New question: The default visibility of an import statement is private, right? So it's only visible to the current file. // my_file.wesl
import bevy::Bar;
private const Foo = 3;
public const Nya = 5;But what about self-imports? // my_file.wesl
import package::my_file::Foo as Bar;
private const Foo = 3;Rust argues that yes, it is legal, because use std::f32::NAN; // creates an item NAN with private visibility
use self::NAN as Nya; // imports NAN, which is allowed, because this is the same file as NAN |
(We've started discussing in #193; I agree direct resolution is better.)
Users consume wildcards per-module, so I think it's better to mark wildcardable per-module. Otherwise we risk inconsistencies between what can be imported directly vs what can be wildcard imported from the same module. That seems likely confusing for users. Saving a keyword is nice of course, and we found options for that in #183. (I just don't think we like them better.) Let's discuss further in #183 if we should revisit
A module whose items are all non-public is effectively private already, which falls out of item visibility rather than needing a module-private concept. I'd rather leave future module visibility questions undefined for now. We might not need the separate module-visibility axis.
Yep, we could add a keyword if we want package-internal re-exports. As to the keyword, the draft suggests
In the current proposal you can conditionally re-export items for the public API: (I see the ConditionalTranslation spec lags the test suite and wesl-rs/wesl-js on allowing conditions on import statements; we should fix that.) Also just noting that this kind of thing will be awkward until we have some interface contracts to help keep these desktop vs. mobile APIs cross-compatible. Re-exports become more ergonomic for library authors if we enable wildcard re-exports too: That's listed in the future section in this draft. It helps library prelude authors with maintenance, but doesn't add anything new for users of wildcards. We can add it later, by having the library packaging tool rewrite wildcard re-exports to named re-exports. The design doc lists a couple of other open questions we'll have to work through (module re-export cycles, etc.; all seem solvable). Library-safe wildcard re-exports are separable, so better in a separate PR when we want it. Re-export for package internal APIs would make sense in the current proposal if/when we add a
That's true, but I think the answer is at the tooling layer, not in visibility or imports. I agree that a test runner needs some way to find its tests. It finds them by searching for test files (a manifest, or a filename/path convention), not by traversing imports from
JavaScript test frameworks typically use tooling to search the filesystem for tests by path/filename suffix. wgsl-test does the same thing currently. Old school browser-only test frameworks would manually list their tests in a test file, AFAIK.
Yes! I imagine we'll need this one too (#187) to declare success on #183. (The immediate motivation for visibility was because we needed re-exports to merge items from multiple modules for |
Yes, import statements don't have visibility, only items. So only
Yes, legal: Note that |
Fixes to wesl.toml
replace the left-to-right segment walk with direct resolution:
the last path segment names the declared item, the preceding segments name the module.
declarations and modules naturally live in separate namespaces now
- `fn foo() {}` and `foo::bar();` do not conflict
lazy vs eager resolution is an implementation choice
clarify terms a bit
Conflict resolution per 193-into-187 merge checklist: - keep the public import re-export paragraph in the grammar section; take direct-imports' module attribute wording - replace the old import walk algorithm with the new 'Resolving a declaration path' model from direct-imports - reattach public import re-exports to item lookup in the new resolution section - carry over the resolution-vs-visibility paragraph and the wildcard visibility-skip rule after the resolution rules - point Filesystem Resolution at the new resolution section - keep the Visibility.md re-exports link in the wildcardable recommendations Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Add wildcard imports
- describe super:: flattening early, so resolution later can be clearer - write package module generically w/o being fs specific - drop wesl.toml mini-section (we have a whole page on it now) - Cleanup fs resolution section logic for direct imports - Add brief section on non-fs resolution
- demote the "tools can enumerate resolutions" paragraph to a [!NOTE] - clarify intermediate paths don't need to be modules
direct import resolution (more editorial fixes coming with #187)
TLDR
public(visible to any package),private(declaring module only), and package (same package, the unmarkeddefault).
public importre-exports the imported names under thecurrent module's path;
public import path::*does the same for a wildcard(re-uses
@wildcardablediagnostics).API: entry points, resource variables, and pipeline-overridable constants
reach the host only if the root module declares them or
public imports them.package.wesl. The file backing a package's top-level module, so itsitems are reachable as
<package>::item(e.g.,import wgsl_test::*). Oneplace a library can put a re-export prelude.
Why these four things are covered:
import wgsl_test::*;(Note that VisibilityLanguages.md is discussion background, it'll be removed before merging.)
Would resolve:
(largely covered already; this PR fills in the re-export piece)
Related