The reason for this behavior is peer type resolution, which is the mechanism the compiler uses to make all operands of an operation agree on a common type. For a switch statement, only its prongs are peers, so it will determine its type from its prongs and its result type.
In foo1 there is no explicit result type, so switch will resolve to comptime_int and (correctly) complain about a comptime value depending on rumtime control flow.
In foo2 the same thing happens: the u8 annotation is only valid for the assignment and not for all operands of the + expression so there’s still no result type. That’s a consequence of how zig propagates result types, if there was something like Result type spreading it would compile (I’m not implying that that would be a good idea though).
In foo3, there’s an explicit result type of u8 provided. All switch prongs have that as their result type and u8 is not comptime-only, so all is well and it compiles.
foo4 is basically equivalent to foo3: the switch has a result type via val and coerces its prongs to that type.
There’s actually a very common example where this exact issue would cause an annoying compile error if the compiler didn’t special-case it:
fn foo(eu: anyerror!u64) u64 {
const result = eu catch |err| switch (err) {
error.MyError => 123,
else => 456,
};
return result;
}
If the switch statement only considered its prongs, it would resolve its type to comptime_int, detect that it relies on runtime control flow and cause a compile error. For that reason the compiler actually treats the non-error case of catch (or an equivalent if (eu) |payload| {} else |err| {} construct) like an additional switch prong, which makes it a peer of the other prongs and makes the entire switch resolve to u64 instead of comptime_int. No compile error ![]()