This is pretty fascinating actually from the language design point of view! The basic problem is this:
There are many number types (f32
, f64
, i32
, u64
, etc). But we want to spell zero as just 0
(or maybe 0.0
for floats to make them explicit). So more or less every language has this puzzle:
There’s 42
literal in the source code. It obviously evaluate to the value of 42
. But of which type?
Figuring the answer to this question can often illuminate something non-obvious about language design.
One option, in languages like C or OCaml, is to say that you actually can’t use 0
to stand for arbitrary type, and that 0
is always of type int
, and than there are 0u
, 0l
, 0ul
, 0ull
, 0z
to mean various other types.
Another option is to make literals fully polymorphic. Eg, I think in Haskell the type of 0
is Num n => n
. That is, 0
is a generic constant, which can become any type you want, as long as that type satisfies Num
. This of course requires that your language has a hefty machinery for generic programming.
Yet another option is to just hack this into type inference. In Rust, a literal 92
in type inference is treated as a new inference variable which is marked specially as “this particular variable stands for some integer”. If, during the normal unification, that inference variable gets inferred to be a specific type, like u64
, than that becomes the type of the literal. If the type isn’t constant, it gets defaulted to i32
. This is similar to what Haskell does, except that this is special-cased, and is not opened for extension to the user.
What Zig does here though, is truly fascinating! The insight is that a literal like 92
is always comptime-known, it is the platonic ideal of “things that happen at comptime”.
So, in Zig, the type of 92
is comptime_int
, and the type of 9.2
is comptime_float
. That is a type which exists only at comptime
, and whose values are always exact. That is, they are represented as big integers in general case, but the language doesn’t allow silently spilling these big ints into runtime, they are restricted to comptime
.
But then, a type like comptime_int
allows coercion to any other type. So when you write const x: i32 = 92
, what happens is that 92
starts as comptime_int
, and then compiler inserts a coercion from it to i32
. Because the number is comptime known, such coercions are always safe (that is, you know at compile time if you would truncate any useful bits).
So this finally explains the error message:
main.zig:2:9: error: variable of type 'comptime_float' must be const or comptime
var mouse_movement_x = 0.0;
On the right, we have comptime_float
. We don’t have any type ascribed on the left, so the type of mouse_movement_x
gets set to comptime_float
. But that is comptime-only type, which means that the variable itself must be comptime
, hence the error message.
In contrast, if you write
var mouse_movement_x: f64 = 0.0;
than, at comptime, we convert from comptime-known comptime_float
to comptime-known f64
. But f64
can exist at runtime, so we don’t get an error.
Of course, this is an instance of confusing error message, where the error reporting (variable not being comptime) is not actually the root cause (absence of ascription), and it’ll probably get fixed at some point.
But for now, we can enjoy this fascinating peek into the language’s inner workings!