shader-slang / slang

Making it easier to work with shaders
MIT License
1.79k stars 160 forks source link

Figure out the right relationship between casts and “constructor calls” #525

Open tangent-vector opened 6 years ago

tangent-vector commented 6 years ago

At the moment, Slang takes cast syntax like this:

int i = ...;
float f = (float) i;

and treats it equivalently to a “constructor call” like:

float f = float(i);

This then turns into a search for an initializer (think “constructor”) in the type float that can be applied to an int argument. That’s all we’ll and good, to make the two equivalent.

A similar thing happens of code needs implicit conversion:

float f = i;

In that case the compiler sees that the user wants a float and starts looking for initializers. The main differences from the explicit case are:

  1. If the expression being converted already has the desired type, we don’t do anything.

  2. If a usable initializer is found, but it isn’t registered as suitable for use in implicit conversion, an error is diagnosed.

Item (2) is maybe obvious, since more conversions should be allowed explicitly than implicitly. Item (1) is important, because we don’t have that rule in the explicit case, so code like the following would be an error by default:

SomeStruct s;
SomeStruct t = SomeStruct(s);

Because we treat casts and “constructor calls” the same, this is true even when cast syntax is used. We work around this for builtin types for now by adding initializers to each type that take an argument of the same type. This is a bit silly, and doesn’t seem like something healthy to impose on user-defined types.

(Aside: no, we aren’t going to go down the C++ “copy constructor” route. Copying a value should be a trivial operation under compiler control, not a chance to run arbitrary user-defined code)

It would be good to eliminate these dummy initializers, but there is also HLSL code out that that contains unnecessary casts (casting a value to the type it already has).

One approach to fix this would be to treat a cast (float) x as similar to the implicit conversion case, including item (1) above, but not item (2). This would handle “identity” conversions without needing explicit initializer declarations.

The place where if gets weird is: should we apply the same logic to float(x) so that “identity” cases become a no-op? If we say “yes” then casts and “constructor” syntax are consistent, but we’d have to special-case single-argument call expressions in a way that isn’t consistent with other calls. If we say “no” then call syntax is consistent, but there is now a difference between casts and “constructor” calls.

I honestly don’t know which of the two options to prefer.

natduca commented 7 months ago

Putting in Q3 with language polish topics