Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Requiring integral-valued float literals to have .0 seems gratuitous #739

Closed
litherum opened this issue Apr 30, 2020 · 37 comments
Closed

Requiring integral-valued float literals to have .0 seems gratuitous #739

litherum opened this issue Apr 30, 2020 · 37 comments
Labels
wgsl:tokens WGSL tokenization and literals wgsl WebGPU Shading Language Issues
Projects
Milestone

Comments

@litherum
Copy link
Contributor

It's really common for shaders to have things like float4(1, 2, 3, 4) rather than float4(1.0, 2.0, 3.0, 4.0). Given that most operations in shaders are floating point operations, it seems gratuitous to require the extra two characters.

@litherum litherum added the wgsl WebGPU Shading Language Issues label Apr 30, 2020
@kainino0x
Copy link
Contributor

With #723 this would require a suffix on int and uint literals (2i, 2u). Alternatively you could require 2. or 2f for floats.

@kainino0x
Copy link
Contributor

cc #575

@kainino0x kainino0x reopened this Apr 30, 2020
@kainino0x
Copy link
Contributor

Also GLSL requires . or .0 on floats.

@dj2
Copy link
Member

dj2 commented Apr 30, 2020

What does it mean if I do float_val == 1 am I doing a float comparison or an int comparison? This seems like it will open up more edge cases than desirable for little benefit.

I believe you can do float4(1., 2., 3., 4.) already

@grorg grorg added this to Under Discussion in WGSL May 1, 2020
@dneto0
Copy link
Contributor

dneto0 commented May 4, 2020

This restriction reduces the attack surface.

@johnkslang
Copy link

johnkslang commented May 5, 2020

There is a tricky conflation getting missed here between:

  • constructors
  • implicit conversions

Constructors are explicit conversions, between the arguments and the result type.

So, even in flavors of GLSL that disallow implicit conversions, you can still make a float vector from integers:

vec4 v;
v.x = 1;              // ERROR, implicit conversion needed
v.x = float(1);       // OKAY, explicit conversion requested
v = vec4(1, 1, 1, 1); // OKAY, explicit conversion requested

You need to address the subject of implicit conversions outside of constructors separately from whether they are allowed inside constructors.

In trying to be lower-level, with fewer complexities to impact portability, I assume WGSL will not allow implicit conversions in general, but must then still state exactly what operations are counted as being explicit conversions. This gets more tricky when you want to start saying that some constructors count as explicit converters and some do not. That's why GLSL just says all constructors request explicit conversions.

@johnkslang
Copy link

johnkslang commented May 5, 2020

A compromise with usability, BTW, has often been requested, which is to treat literals differently:

float f;
int i;
f = 1;  // OKAY, it is totally clear what's requested and what to do
f = i;  // ERROR, maybe this type mismatch is a mistake, or gets handled non-portably

@grorg grorg moved this from Under Discussion to For Next Meeting in WGSL May 9, 2020
@litherum
Copy link
Contributor Author

litherum commented May 12, 2020

There’s another possible solution here that hasn’t been listed yet: simply switch the default from int to float. The literal “1” could be a float, and “1i” could be an int. Most scalars are floats in shaders anyway, so this at least passes the smell test.

Edit: looks like @kainino0x already listed this

@johnkslang
Copy link

johnkslang commented May 12, 2020

The literal “1” could be a float

Really? Is that from the tokenizer (meaning standalone versus accounting for context) saying it's a float?

  • If not, and and the tokenizer says it's an int, then this is just an implicit conversion in disguise. (An implicit conversion only for literals might be okay, and what I recommend, but let's be clear that "1" is an int.)
  • If so, then what type will it tokenize the number 2,000,000,000 to? As a 32-bit float? That loses precision. Will that happen silently?

[edited due to premature send]

@kainino0x
Copy link
Contributor

My intent was that it would be from the tokenizer.

@kainino0x
Copy link
Contributor

If undecorated numbers are always 32-bit floats, we should probably disallow integer-style literals of magnitude greater than 224 − 1 = 16777215, requiring 1e1 notation. Or at least disallow integer-style literals that can't be represented.

@kainino0x
Copy link
Contributor

On the topic of having a "special" literal type, FWIW here's what Rust does:

fn use_i32(x: i32) {}
fn use_u32(x: u32) {}
fn main() {
    {
        let x = 1; // x has no concrete type yet
        use_i32(x); // causes x to be inferred as i32
        use_u32(x); // error (expected u32, found i32)
    }
    {
        let x = 1; // x has no concrete type yet
        use_u32(x); // causes x to be inferred as u32
        use_i32(x); // error (expected i32, found u32)
    }
}

Obviously I don't support doing this in WGSL since it's quite complicated.

@grorg
Copy link
Contributor

grorg commented May 12, 2020

This was discussed at the 2020-05-12 meeting.

@dneto0
Copy link
Contributor

dneto0 commented May 12, 2020

In Go, this discussion of literals extends to constants. Both literals and constants are untyped and evaluations take place in an infinite-precision context. See https://blog.golang.org/constants

@dneto0 dneto0 moved this from For Next Meeting to Needs Investigation in WGSL May 26, 2020
@litherum
Copy link
Contributor Author

litherum commented May 27, 2020

Here's a proposal for generic literals:

Goals

  • It should be valid to say var x : f32 = 1;
  • It should be valid to say var x : f16 = 1.0;
  • If the only overload for foo() is fn foo(x : f32) { … } it should be valid to say foo(1);
  • It should be valid to say float4(1, 2, 3, 4)
  • It should not be valid to say 1 + 2.0 * 3u because this requires a pass over the entire expression tree to find some common type
  • It should be valid to say 1 + 1 (maybe - more on this later)

Types

Some literals have a generic type, and some literals have a concrete type.

  • 1 has a generic type. For the sake of conversation, let’s name this type GenericLooksLikeInt. (Note that, inside a program, there is nothing you can write that will identify this type by name.)
  • 1i and 1u have concrete type. They are 32-bit signed integer and 32-bit unsigned integer, respectively. We can add more suffixes when we add 16-bit and 8-bit ints.
  • 1.0 has a generic type. Let’s name this type GenericLooksLikeFloat
  • 1.0f and 1.0h have concrete type. They are float and half, respectively.

Every generic type has an associated “preferred” concrete type. The preferred type for GenericLooksLikeInt is “32-bit signed integer.” The preferred type for GenericLooksLikeFloat is “float.”

Unification Rules

There are a few specific situations, listed below, where a generic type may be unified with a concrete type. There is no situation where a generic type can be unified with a generic type.

Here are the unification rules:

  • GenericLooksLikeInt can be unified with all integral scalar types and all floating point scalar types. When GenericLooksLikeInt is unified with an integral type, something well-defined will happen for out-of-bound values (probably a compile error, though the specific behavior is not important to us). When GenericLooksLikeInt is unified with a floating point type, it is impossible to be out-of-bounds, either by using infinity or clamping to the max finite value. Edit: Out of bounds values just have to be well-defined, either by compile error, or by rounding; the specific behavior is not important to us. We’ll also have to have some rounding mode specified for when the specified values is in-between representable values of the type.
  • GenericLooksLikeFloat can be unified with all floating point scalar types (but not integral types). Again, it is impossible to be out-of-bounds, using the same rules as above.

As a general principle, for every literal of generic type G, for every concrete type C, if G can unify with C, then it should be possible to add a suffix (or prefix??) to that literal to cause it to have concrete type C.

Unification Situations

Assuming paren_rhs_stmt nodes have been transitively replaced by their constituent logical_or_expression nodes (meaning expressions like ((((((6)))))) have already been collapsed down to just 6), A generic literal will be unified in only these situations:

  • As the direct RHS child of a variable_stmt or an assignment_stmt (unified with the type of the LHS)
    • If we add type inference on variable_stmts, unification only occurs when the variable has an explicit type written.
  • As a direct argument passed to a function call, an as<> cast, a logical cast, or a constructor, iff:
    • For such a call, there is exactly one function overload for which the types of all the arguments may be unified with the types of their respective parameters.
    • It gets unified with the type of the appropriate parameter.
    • Operators (* / % + - << >> >>> < > <= >= & ^ | && ||) as well as array indexing operations (foo[5]) are treated as function calls for this purpose. Note most operators have multiple overloads.
  • As a direct child of a return statement. It gets unified with the return type of the function.

After performing all the unifications possible in the above situations, if a generic type remains in the program, it assumes its preferred type. For example, 1 has type GenericLooksLikeInt, but 1 + 1 has type int.

This last rule about a literal assuming its preferred type may be confusing to authors - it means that var x : f32 = 1; and var x: i32 = 1 + 1; are valid, but var x: f32 = 1 + 1; isn't. Another alternative here is to just reject 1 + 1 entirely because there are multiple overloads of +. If we do this, it means it will be illegal to write var x = 1; once we add type inference. It's a judgement call about whether the consistency of 1 vs 1 + 1 is more important than being able to write these kinds of statements.

Unification Non-situations

I went through the grammar and found all the places where a literal can show up. The remaining places where a literal won’t be unified are:

  • Conditionals: if (3), elseif, break if, continue if, and unless. The CG discussed this in Implicit conversion to bool? #733 and we decided to not have unification with bool types.
  • switch (3) { … }. There’s no obvious type to try to unify with.
  • Unary - and ! as well as (almost?) all the operators have multiple overloads, so literals won’t get unified with them in (almost?) all cases
  • 5.foo. This is meaningless and should be an error
  • We just added for loops, so literals can be in the 3 clauses. However, the second clause needs to have bool type, so a generic literal can’t go there. And if a literal is written as the first or third clause, it has no effect, so it doesn’t matter and it doesn’t need to be unified.

@kainino0x
Copy link
Contributor

kainino0x commented May 27, 2020

  • If we add type inference on variable_stmts, unification only occurs when the variable has an explicit type written.

I don't understand this line; if you don't perform the unification then the variable now has type GenericLooksLikeX? But it sounds like we don't want that to be possible (for very good reason).

Overall, while, for usability, I generally like when a language has this feature, I think it's much too complex for WGSL (especially the (very important) rules on unifications with overloads/operators). I really don't think typing 1. is so huge of a burden over 1 that it justifies this degree of complexity.

@kvark
Copy link
Contributor

kvark commented May 28, 2020

Implicit typing is something I'd like us to steer away as much as possible. The proposed integer and float literals are useful though, for using in contexts where their expected types are well known and understood, i.e. let v: f32 = 1. I think it's good to have them, but without trying to use them in more places (which also means - simpler spec).

I don't think it's worth trying to support 1 + 1 case at all. There is no context here to infer the type for the first 1 when we are seeing it, so we should just plain error out.

@litherum
Copy link
Contributor Author

litherum commented May 28, 2020

  • If we add type inference on variable_stmts, unification only occurs when the variable has an explicit type written.

I don't understand this line; if you don't perform the unification then the variable now has type GenericLooksLikeX? But it sounds like we don't want that to be possible (for very good reason).

The key is this part:

After performing all the unifications possible in the above situations, if a generic type remains in the program, it assumes its preferred type.

It means if the program says var x = 1; then 1 doesn't get unified (because there's no type to unify it with), and it assumes its preferred type (int). Then, the type inference machinery assigns this type to x, so everything here becomes int. On the other hand, if the program says var x : f32 = 1; then the 1 gets unified with f32.

@litherum litherum moved this from Needs Investigation to Under Discussion in WGSL May 28, 2020
@kainino0x
Copy link
Contributor

@kvark I don't think there's value in adding an inference mechanism to the language just to allow
let v: f32 = 1; instead of let v = 1.;
(and maybe fnTakingF32(1) instead of fnTakingF32(1.), not sure if that was covered).
I think that represents most of the cases affected by your proposal?

@kvark
Copy link
Contributor

kvark commented May 28, 2020

@kainino0x
It looks like you are mixing two different things here:

  1. having number literals in general, whose type depends on the immediate context. Both 1 and 1. are number literals (integer and floating-point), they don't have exact types.
  2. having integer literals be a subset of float literals, such that one can use integers in the float context.

I expressed concerns over complicating the notion of "context", e.g for arithmetic expressions of literals (1 + 1). I don't think we need any complexity here.

@kainino0x
Copy link
Contributor

Sorry, what I'm proposing is that literals' types never depend on context.

  • 1 is always i32
  • 1. is always f32
  • maybe 1u is always u32
  • maybe 1f is always f32
  • anything else requires suffixes (1u32, 1f32, 1f16)

@kvark
Copy link
Contributor

kvark commented May 29, 2020

@kainino0x that would be wonderful. I just cringe a little bit at the u32 discrimination... I'd prefer to call signed integers with i suffix (e.g. 1i) for consistency.

@grorg grorg moved this from Under Discussion to For Next Meeting in WGSL Jun 1, 2020
@grorg
Copy link
Contributor

grorg commented Jun 16, 2020

Discussed at the 2020-06-16 meeting.

@grorg
Copy link
Contributor

grorg commented Jun 16, 2020

Resolution: General agreement to the proposal above. @litherum will turn it into a PR.

@dneto0
Copy link
Contributor

dneto0 commented Jun 16, 2020

I am not in "general agreement".

  • This seems like a lot of machinery to avoid doing something that's already customary in GLSL ES (and hence WebGL).
  • These rules are different from any other language I know of. That induces cognitive overload and some risk that we've overlooked important cases.
  • The rule about counting overloads opens us to the risk that as we expand the language, we'll accidentally break existing programs, because we might add "overloads" of existing operators and built-in functions.

To me the cost and potential for confusion outweighs the purported benefits. For lots of other reasons, users have to understand that values belong to specific types. Forcing you to write what you mean, explicitly, is highly beneficial to clarity. And the short letter suffixes that we seem to be assuming, seems to me very cheap.

@kainino0x
Copy link
Contributor

This is a complex topic and is taking a lot of time. I don't think it ought to be a blocker for MVP. Can we come up with a futureproof middle ground for now so we can make progress on actual functionality? Here's a proposal:

  • Bare 1, 1., 1.0, etc. is just not allowed for now (to leave the syntax open for later).
  • Suffixes are supported and required, e.g. 1u32, 1i32, 1f32.
  • Optionally, 1u means 1u32, 1i means 1i32, and 1f means 1f32.

@dj2
Copy link
Member

dj2 commented Jun 17, 2020

+1 to Kai's suggestion. It seems like a solid base that could be expanded after MVP but without the possibility we'd have to walk anything back.

@kvark
Copy link
Contributor

kvark commented Jun 17, 2020

I like the follow Kai's suggestion as well. It has a problem though: it introduces the type suffixes. They duplicate the semantic of the types.
If we had integer/float literals, we would have an option to not have any type suffixes at all. I.e. if you want precisely type uint32, you can do as<uint32>(4) and so forth.

@kainino0x
Copy link
Contributor

kainino0x commented Jun 18, 2020

That's only a problem if (1) we don't want to have type suffixes at all (why wouldn't we?) and (2) we end up with a solution where as<uint32>(4) is even allowed (it wouldn't be with Myles's proposal, since it matches multiple overloads, unless there's magic that says there's an "overload" that takes a value of un-narrowed-number-literal-type and it takes priority over other overloads)

@kvark
Copy link
Contributor

kvark commented Jun 18, 2020

we don't want to have type suffixes at all (why wouldn't we?)

Well, that's something to consider (not necessarily my position!). The question should be the opposite: why having the suffixes if we don't have to? Removing them later would be more difficult.

it wouldn't be with Myles's proposal, since it matches multiple overloads

I don't see it as an overload. It's just a generic function, as<T>() is not overloaded, it has a concrete expected argument (of type T). I think that fits into @litherum proposal that would unify the literal with the expected type T, unambiguously.

@kainino0x
Copy link
Contributor

If you treat it like a regular templated function, it is overloaded on the input type (as<To>(x: From))

@litherum
Copy link
Contributor Author

litherum commented Jun 22, 2020

Suffixes are supported and required, e.g. 1u32, 1i32, 1f32.

This These specific suffixes seem downright hostile to authors.

@litherum
Copy link
Contributor Author

It seems like there's still significant more discussion to be had here; I'll hold off on making a pull request until we (the CG) know the direction we'd like to be heading.

@kainino0x
Copy link
Contributor

They are the suffixes found in Rust.

The slightly inconvenient spelling is why I suggested, "Optionally, 1u means 1u32, 1i means 1i32, and 1f means 1f32." Are you advocating for that, or against the whole proposal?

We're talking about an explicit syntax for users to use when they want it, and also for making WGSL possible to use before we have a nicer solution. I am not worried about it being a little verbose.

@grorg grorg moved this from For Next Meeting to Under Discussion in WGSL Jun 23, 2020
@kdashg kdashg added this to the post-MVP milestone Jun 29, 2021
@kdashg
Copy link
Contributor

kdashg commented Jun 29, 2021

WGSL meeting minutes 2021-06-29
  • DN: We now have type inference for let-decl and var-decl, so it interacts.
  • (consensus: post-mvp)

@kdashg kdashg moved this from Needs Discussion to Reconsider Post MVP in WGSL Aug 17, 2021
@dneto0 dneto0 added the wgsl:tokens WGSL tokenization and literals label Aug 27, 2021
@litherum
Copy link
Contributor Author

litherum commented Oct 25, 2021

One of our internal teams is hitting this, for both .0 suffixes and for u suffixes. They'd like to not need so many suffixes. #2213

@dneto0
Copy link
Contributor

dneto0 commented Apr 12, 2022

fixed by #2227

@dneto0 dneto0 closed this as completed Apr 12, 2022
WGSL automation moved this from Reconsider Post MVP to Done Apr 12, 2022
ben-clayton pushed a commit to ben-clayton/gpuweb that referenced this issue Sep 6, 2022
…web#739)

* Add operation tests on CopyTextureToTexture with multisampled color textures

* Add comments

* Address reviewer's comments

* Fix [[builtin(position)]] in fragment shader

* Revert "Fix [[builtin(position)]] in fragment shader"

This reverts commit 39719ab2273e1bc4b6446c1ce80262742fac15db.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
wgsl:tokens WGSL tokenization and literals wgsl WebGPU Shading Language Issues
Projects
WGSL
Done
Development

No branches or pull requests

8 participants