ziglang / zig

General-purpose programming language and toolchain for maintaining robust, optimal, and reusable software.
https://ziglang.org
MIT License
33.63k stars 2.46k forks source link

Inferred @splat for easier scalar-vector operation? #17274

Open expikr opened 11 months ago

expikr commented 11 months ago

I was reading https://nelari.us/post/raytracer_with_rust_and_zig/ and the post in 2019 described the pains of doing vector operations when Zig has no operator overloading:

    return uv.sub(n.mul(dt)).mul(ni_over_nt).sub(n.mul(math.sqrt(discriminant)));

Obviously since then the vector type has been added with elementwise operation which drastically simplified vector-vector operations.

However, for scalar-vector multiplication it is not allowed to mix, and the language reference states that @splat is an escape hatch for creating an equal length vector of uniform scalar values.

However, it requires explicit initialization as a variable, meaning that if it is to be used inline, it'll also have to be wrapped inside an @as(@Vector(<veclength>,<numformat>) , @splat(<actualscalar>)).

So using the refraction function in the blog post as a toy example, it results in the following monstrosity:

    return @as(@Vector(3,f64),@splat(ni_over_nt))*(uv - @as(@Vector(3,f64),@splat(dt))*n) - @as(@Vector(3,f64),@splat(@sqrt(discriminant)))*n ;

If, however, the @splat builtin is able to infer the type based on the context of what vector it's being applied against, then it reduces to the much more manageable following:

    return @splat(ni_over_nt)*(uv - @splat(dt)*n) - @splat(@sqrt(discriminant))*n ;

which nicely self-annotates its fact of being a scalar-vector operation.

xdBronch commented 11 months ago

i believe this is planned in some form not just for splat but the other builtins as well, e.g. @intFromFloat, @intCast, etc. but it needs to be worked out how exactly it behaves, but since doing math on vectors requires the same type anyway i think splat would be much easier to work out semantics wise. this would be a very good change :pray:

mlugg commented 11 months ago

The basic problem here is that * (and other binary arithmetic operators) does not forward result type information to operands - so far we've not figured out a good way to do this that plays nicely with our status quo arithmetic semantics. The basic problem is: given an expression a * b which we know has type T, how do we give either a or b an exact known type which it must fit in any valid code? With our current semantics (which, do note, will likely not remain in their exact current form) the main blocking issue here is that any comptime-known integer can implicitly coerce to a smaller integer type fitting the value.

expikr commented 5 months ago

@splat in its current form is useful for pre-computing a reusable SIMD vector that will be applied many times. Using it as a one-off scaling operation results in the verbose problem above.

But unequivocally there will certainly be many use cases of one-off scaling of a vector, and so we have a problem of using a tool in a situation for which it was not optimally designed for.

So really the crux of the issue comes down to needing a syntax to express a one-off operations on a vector as an expession.

My suggestion would be to add builtin methods to vector types, e.g. vec.@scale(s)

Notationally this introduces a new category of builtins easily distinguishable from @"name" style decls in the absence of quotes, indicating that it is part of the language itself rather than some custom definition.

This syntax might also be useful for built-in complex numbers (https://github.com/ziglang/zig/issues/16278#issuecomment-1979811231) where field access via z.@re and z.@im clearly distinguishes it from struct field access, which would otherwise indicate that they should be initialized with @Complex(F){.re=x, .im=y} rather than @Complex(F){x,y}

So the linked ray-tracing example might end up looking like:

    return (uv - n.@scale(dt)).@scale(ni_over_nt) - n.@scale(@sqrt(discriminant)) ;
sjb3d commented 4 months ago

I was going to create a proposal issue for @splat to be able to infer its result type through binary ops, but I just found this issue and #17492, so instead I'd like to add a +1 to the original proposal here.

Personally I find that mixing scalars and vectors is the main cause of readability/verbosity issues when using vector types. I think it would help a lot if Zig could find a way to propagate this type info for vector types, so that this:

some_v * @as(@Vector(4, f32), @splat(2.0)) - @as(@Vector(4, f32), @splat(1.0))

could be written as this:

some_v * @splat(2.0) - @splat(1.0)