Closed dneto0 closed 2 years ago
cc: @ben-clayton @dj2
I'm afraid I don't feel this argument is particularly compelling, and heads in a direction I hope we'd avoid.
Programs composed of multiple chunks of text are still likely to be maintained by the same project and developers, and I personally believe this 'silent change in behaviour' is unlikely to be a real problem - or at least an issue that wouldn't take a developer a few minutes of investigation to figure out and fix.
@kainino0x had proposed some time ago that WGSL could be a "living language", in that new language features (which are not dependent on GPU hardware capabilities), could be added over time, without the need of enable
directives, so long as the language remains backwards compatible. At the time Kai proposed this, I was concerned about offline tooling not being able to recognise what WGSL version it was processing - but after consideration, there are plenty of existing languages / compilers that work just fine with this model (Golang being one). As I believe Kai previously pointed out, this "living language" model has already been successfully used by JavaScript, so we'd not be breaking new ground here.
The alternative to the "living language" is explicitly enabling each and every new language feature with an enable
directive, and each implementation having to maintain a separate codepath for each enabled / disabled feature. This approach doesn't seem scalable, and adds a whole bunch of complexity for implementers and our web developers.
Allowing builtins to be shadowed at module scope is the only way we'd be able to maintain backwards compatibility without forcing each feature to be explicitly enable
d. For these reasons, I'd really like to see shadowing of builtins to be allowed, which could be spec'd easily by stating that builtins exist in a scope one higher than the module scope.
With all this said, what we have right now can be relaxed later to allow shadowing of builtins, so there's no need to change anything for V1. I just wanted to make clear that I think we should seriously consider the alternatives to requiring explicit enables.
Just to be clear, this would be shadowing of builtin functions and builtin types to allow expansion of types in the future as well?
So, I'd be able to do type i32 = f32;
if I really wanted?
So, I'd be able to do
type i32 = f32;
if I really wanted?
I wouldn't propose we explicitly forbid it in the spec, but as it currently stands i32
is a keyword and so no, you wouldn't be able to type this, even if we enabled shadowing.
You raise a good point though - the "living language" wouldn't be able to introduce any new keywords. New types, etc, would all have to be identifiers. For consistency, if we were to go down this path, maybe i32
and friends should be identifiers too.
The alternative to the "living language" is explicitly enabling each and every new language feature with an enable directive, and each implementation having to maintain a separate codepath for each enabled / disabled feature. This approach doesn't seem scalable, and adds a whole bunch of complexity for implementers and our web developers.
I disagree. Every once in a while you make a new enable that batches together old individual things.
Example: enable wgsl2024;
pulls together all desired core features.
Make a new one every couple of years, or whatever epoch is desired.
If tooling projects think there is too much complexity in maintaining old fine-grain subdivisions, then they can move to a new epoch, and acts as if wgsl2024 is the baseline.
This is a new language dialect, but formalizes the "living language" idea.
This would be much like how Rust evolves (apparently). https://github.com/rust-lang/rfcs/blob/master/text/2052-epochs.md (modulo the discussion of deprecations).
Analogy. This is valid K&R C:
#include <stdio.h>
foo(a)
int a;
{
return 2* a;
}
main() {
return foo(2);
}
Are you sure your toolchain supports it? It so happens that clang on my machine does. But I was surprised by it.
I expect that it's kind of understood that a C compiler you get today might not support K&R C.
At some point it would be understandable that a C++ compiler does not support C++1998 that is not also valid C++11.
At some point it would be understandable that the system C++ compiler would understand C++11 without having to ask nicely (-std=c++11
).
What I'm proposing is that formally, you always need an 'enable' for a new feature that isn't backward compatible.
But new epochs make new language dialects, and over time "all" the tools and browsers might care about will only implement the newer epoch dialects.
That doesn't work with the web though. You'll always have pages which enable foo;
, enable bar;
even after you have enable wgsl.2030;
which includes those two things. So, we always have to check and support the individual enables else we can break existing content.
So, for each enable
we'll have to have the code that supports it or not and we can't assume it won't be used after a certain amount of time.
I think the problem of adding new builtins to the prelude can be sidestepped by just not doing that. WGSL could reserve std
as an identifier and then add new things as std::newThing
, or use a sigil for new builtins such that they can't collide with user code (__newThing
or ::newThing
). Keywords are a different thing, but Rust for example has managed pretty well adding new keywords only "contextually" (they don't need to be reserved because they can't grammatically appear where identifiers would appear).
You can still do feature packs like enable wgsl2024
that dump buckets of things unprefixed into the prelude, but it still has maintenance cost and I don't think it's going to be necessary for a long time if ever.
(I'd be almost tempted to suggest that all builtin identifiers that aren't reserved words have some kind of sigil or convention that makes them visually distinct from user defined stuff, but that's most likely overkill.)
If tooling projects think there is too much complexity in maintaining old fine-grain subdivisions, then they can move to a new epoch, and acts as if wgsl2024 is the baseline. This is a new language dialect, but formalizes the "living language" idea.
When is an epoch? Whatever number you say, there will be shaders on the web that will be not be using the latest enable pack. The moment you decide to update your baseline, you have the symbol collision problem, and you've broken existing shaders.
How would you motivate developers to start their new shaders with enable wgsl1234
? If V1 contains all the features I need, I'm not going to type additional, unnecessary stuff. So I'd bet that most WGSL would be baseline.
That doesn't work with the web though. You'll always have pages which
enable foo;
,enable bar;
even after you haveenable wgsl.2030;
which includes those two things. So, we always have to check and support the individual enables else we can break existing content.
Completely agree with @dj2 here.
I think the problem of adding new builtins to the prelude can be sidestepped by just not doing that. WGSL could reserve
std
as an identifier and then add new things asstd::newThing
, or use a sigil for new builtins such that they can't collide with user code (__newThing
or::newThing
).
Ensuring that all new identifiers get placed in a new namespace would work, but:
using namespace metal;
vs those that don't.Restating my initial point: allowing shadowing of builtins / types would allow us to add new things to the language and avoid a forever expanding set of enables. The concern about code snippet joining and 'silent change in behaviour' is a trip hazard, but we have no evidence on how common or annoying this would be. I believe maintaining a sea of enables (by both implementor and user) would be more annoyance than this theoretical issue.
Concretely what would that mean for the spec:
Program Scope
which exists around the Module Scope
. It is not possible for a user program to define something at Program Scope
.Program Scope
.var i32 = 1.0;
var foo: i32 = 1i;
We know the right i32
based on context, do we allow or disallow?
Type Defining Keywords
section.As the language grows, we add new types and builtins without enables
entries. Warnings may be added to implementations when a shadow of a builtin/type are found.
@ben-clayton points out for the shadow type context question, the answer is probably no. This would get very confusing with a type constructor if the user declared an i32
function, we could no longer have context for what to call.
(Also, type aliases and structs could be problematic here)
It's also more consistent to treat shadows all the same, so the type would not be recognized as such until the shadow goes out of scope.
Changing to non-editorial, to address discussion about i16 and i64 in #2983
In the 2022-06-07 meeting we uncovered the same discrepancy about how/whether author's declarations at module scope can shadow predeclared items in WGSL.
Google has been discussing this internally, and think we first need to agree on how WGSL will evolve for "sugar" features. This addresses part of #600
Summary:
I see there as tension between:
using namespace std
struct u64 { lo: u32, hi: u32 };
, where we might later want to add a primitive u64
type.What I think might work well is to have a slightly more sophisticated identifier resolution.
What if you could do std.u64
to access some (future!) u64 type directly, but that a bare u64
would preferentially resolve to within the scope of the module, so to e.g. the struct u64
polyfill above.
Conceptually I imagine this as having a namespace std
, and using namespace std
being implicit, and further, that u64
(_.u64
?) would resolve preferentially to an in-scope declaration, and then secondarily to any of the "used namespaces" of the scope.
namespace std {
[...]
type fvec4 = vec4<f32>; // e.g.
}
namespace _ { // module-scope
using namespace std;
// Begin WGSL module
struct u64 { lo: u32, hi: u32 };
fn less(a: u64, b: u64) -> bool {
if a.hi < b.hi { return true; }
if a.lo < b.lo { return true; }
return false;
}
fn min(a: u64, b: u64) -> u64 {
if less(a, b) {
return a;
}
return b;
}
[...]
// From some copy&pasted code:
fn foo(a: std.u64) {
[...]
}
// End WGSL module
}
You could even imagine that we do something like:
namespace std22 {
[...]
type fvec4 = vec4<f32>; // e.g.
}
namespace std23 {
using namespace std22;
[...]
type u64;
}
namespace std {
using namespace std23;
}
Thus
let _: fvec4; // easy
let _: std.fvec4; // safe
let _: std22.fvec4; // precisely
let _: std23.fvec4; // technically
let _: u64; // easy
let _: std.u64; // safe
let _: std22.u64; // ERROR!
let _: std23.u64; // precisely
RESOLVED: Should WGSL allow shadowing of builtins [both types and functions] at module scope? Yes.
I forked the language evolution / versioning topic out to #3149 so I could mark this as "Resolved - needs spec"
WGSL does not have a module system.
So composing program fragments to make a big shader means textually concatenating shader text.
Suppose you have program parts A and B, and both of them use a builtin function XYZ. The program works as intended.
Now suppose B' is like B but provides its own definition of builtin XYZ. We assume the author of B' is fully aware of what they're doing, and makes B' work the way they want. But when making WGSL program A+B', the meaning of A has changed, silently. This is bad.
Why doesn't this problem occur with other module-scope declarations?
type i32 = f32
because things likei32
are keywords, and so are not classified as identifiers. This is an example of: When a predeclared type has a name which is a single token, that token is a keyword.What happens when we expand the language? If we add a new type like bf32 (made up), or a new builtin (or a new overload of a builtin) then you have to enable it with an enable directive. So anybody combining an A' that uses bf32 sees the directive at the top advertising that bf32 is a newly introduced thing that this module uses, e.g.
enable bf32_feature;
.If B has its own definition of bf32, then B can come with a warning saying "Sorry, this B program text is incompatible with
enable bf32_feature
". If bf32's definition is really for internal use within B, then B can be adapted over time to rename that builtin to again become compatible withenable bf32_feature
.If WGSL had a module system, then you might be able to relax things: A module author can declare the intent about whether a module-scope name is intended to be exported or not, i.e. whether its for users of the module, or for intra-module use only.