It seems that Zig does not guarantee evaluation order here, and that during the first form the union tag may be updated as part of the assignment before the nested bullet.state.alive expression is evaluated.
Is that the correct interpretation?
More generally:
Does Zig guarantee that the RHS of an assignment is fully evaluated before the LHS is mutated?
Is it considered undefined / unspecified to read from x in the same expression where x is assigned a different union tag?
Are there any other related gotchas to be aware of?
This is expected. See the Result Location section of the language reference. E.g.
When compiling the simple assignment expression x = e, many languages would create the temporary value e on the stack, and then assign it to x, potentially performing a type coercion in the process. Zig approaches this differently. The expression e is given a result type matching the type of x, and a result location of &x. For many syntactic forms of e, this has no practical impact. However, it can have important semantic effects when working with more complex syntax forms.
And the swap example in that section.
It’s a subtle, but important difference to C.
Edit: ok. I’ll defer to vupesx here. Ignore the above.
.{} writes directly to the destination, but T{} creates a new instance then copies to the destination.
For some reason, the payload is being written before the tag. Seems like a regression/unhandled edge case. Even if the payload is written first, the compiler should probably still allow it since the tag will be updated anyway.
No. Zig uses a strict LHS-to-RHS evaluation order. Assignments happen as they are evaluated in that order.
It’s not undefined, but you can’t do what you’re trying to do.
On this line:
bullet.state = .{
.exploding = .{ // <- now .exploding
You’ve assigned the .exploding tag to bullet.state.
Here:
.origin = bullet.state.alive.body.pos(),
You try to read from .alive, which is no longer the tag, so Zig won’t let you. Ironically, in .ReleaseFast mode this would work: please don’t count on that.
When you use the Exploding{ form, Zig creates an entirely new struct before copying it to the destination. Less efficient (presumably) but it avoids the bad interaction.
Best is probably to cache the value you need into a temp variable, then use .{ form to assign it.