That’s certainly true, but my question is a little subtler.
As npc1054657282 says, in many cases the expression
@TypeOf(<expression>)
is able to be evaluated without evaluating <expression>. In fact, this capability is the raison d’etre of @TypeOf: for example we can evaluate (at comptime)
@TypeOf(std.testing.random_seed)
even though the inner expression cannot be evaluated.
Consider expressions of the form
switch (std.testing.random_seed) {
1234 => <leg0>,
else => <leg1>,
}
What does the compiler do when you stick that in a @TypeOf? Does it bail just because it doesn’t know the value of std.testing.random_seed? Not a chance. It doesn’t know which leg to use, but it does know that the result of the @TypeOf can be computed from the types of the legs, so it goes and tries to work them out. Proof: compiling
@TypeOf(switch (std.testing.random_seed) {
1234 => std.testing.random_seed,
else => @as(f32, 3.141),
}
gives an error:
src/root.zig:576:36: error: incompatible types: 'u32' and 'f32'
try std.testing.expect(@TypeOf(switch (std.testing.random_seed) {
^~~~~~
src/root.zig:577:28: note: type 'u32' here
1234 => std.testing.random_seed,
~~~~~~~~~~~^~~~~~~~~~~~
src/root.zig:578:17: note: type 'f32' here
else => @as(f32, 3.141),
^~~~~~~~~~~~~~~
which is expected: the compiler has worked out the types of the legs, and now helpfully tells us that they’re different, no dice. A nice glimpse into the inner workings.
Let’s fix this:
@TypeOf(switch (std.testing.random_seed) {
1234 => @as(f32, @floatFromInt(std.testing.random_seed)),
else => 3.141,
}
Now the compiler is happy, and the value of the @TypeOf expression is f32.
OK, let’s run the same experiment where we allow our legs to be of comptime-only type.
@TypeOf(switch (std.testing.random_seed) {
1234 => u32,
else => |x| x,
}
This gives the exact same sort of error as when the legs had types u32 and f32:
src/root.zig:576:36: error: incompatible types: 'type' and 'u32'
try std.testing.expect(@TypeOf(switch (std.testing.random_seed) {
^~~~~~
src/root.zig:577:17: note: type 'type' here
1234 => u32,
^~~
src/root.zig:578:21: note: type 'u32' here
else => |x| x,
^
only now, one of the legs has type type.
OK, so just like before, the compiler can see the types of the legs, one happens to be comptime-only, that’s cool, anyway we can’t proceed because they’re different. Let’s help it out and replace one of them with a value of type noreturn, which coerces to any type (including comptime-only apparently):
@TypeOf(switch (std.testing.random_seed) {
1234 => u32,
else => unreachable,
}
This compiles just fine and evaluates to type.
Now let’s get a little more ambitious and make that leg be of the right type, namely type. Should be fine, right?
@TypeOf(switch (std.testing.random_seed) {
1234 => u32,
else => f32,
}
Wrong!
src/root.zig:590:36: error: value with comptime-only type 'type' depends on runtime control flow
try std.testing.expect(@TypeOf(switch (std.testing.random_seed) {
^~~~~~
src/root.zig:590:55: note: runtime control flow here
try std.testing.expect(@TypeOf(switch (std.testing.random_seed) {
~~~~~~~~~~~^~~~~~~~~~~~
src/root.zig:590:36: note: types are not available at runtime
It knows the legs have the same type, it even tells us right here that any evaluation of the expression would be of type type. But, unlike the not-comptime-only case, it can’t skip to the @TypeOf expression. It demands a value for the inner expression, and everybody agrees that it can’t have that.
Note that in none of my testing am I actually instantiating <expression> at comptime (i.e. I’m not writing const x = <expression> at top level or anything like that, I’m just doing
test "<expression>" {
_ = @TypeOf(<expression>);
}
My high-level read on this situation is this.
Compiler expression type analysis pipeline:
- Type inference: Compute type of expression (using Peer Type Resolution if necessary).
- Comptime-only value resolution: If the result is a comptime-only type, evaluate expression.
@TypeOf implementation:
- Perform type analysis on expression (both steps)
- Get type from analyzed expression
If this summary is correct, my question really boils down to this:
Logically, @TypeOf does not need the type analysis pipeline to do comptime-only value resolution, it only needs it to do type inference. Why not skip step 2 in that case?
I’m asking from a design choices perspective, not a practicality perspective - I can guess that actually changing the compiler to behave this way, even if it were viable, would be a lot of effort for basically zero reward. Probably nobody (including me) would ever use it.