Comptime variables

I had a problem in my code, where I forgot to specify the type of my variable, and in a later function used that and the compiler told me, that the variable either needs to be const or comptime.
I realized my mistake, and I think I understand the const part, but I’m totally lost on the comptime part.
Here’s the code I forgot to specify the type, it lives inside a function:
Zig version: 0.13.0

    var mouse_movement_x = 0.0;
    var mouse_movement_y = 0.0;

And here is where I used it:

    if (rl.isMouseButtonDown(.mouse_button_right)) {
        mouse_movement_x = rl.getMouseDelta().x * 0.08;
        mouse_movement_y = rl.getMouseDelta().y * 0.08;
    }

My question is, what does “needs to be comptime” means? Obviously if I add the f32 type to the line, it works, but in my mind it doesn’t mean that I made it comptime, or did I? This bothers me ever since.

This is what the documentation tells me:
Container level variables have static lifetime and are order-independent and lazily analyzed. The initialization value of container level variables is implicitly comptime. If a container level variable is const then its value is comptime-known, otherwise it is runtime-known.
I guess it’s container level variable, beacuse I’m changing the variable with the rl.getMouseDelta() function? But if the initialization is comptime, and I did initialized with 0.0, shouldn’t it know that it was an f32 when I used it with rl.getMouseDelta()? Or Zig doesn’t interpret the type?

I think you need to read the error message more carefully, in the future I think it helps if you post the complete error message.

Here I reproduced a similar scenario:

src/main.zig:45:9: error: variable of type 'comptime_float' must be const or comptime
    var mouse_x = 0.0;
        ^~~~~~~
src/main.zig:45:9: note: to modify this variable at runtime, it must be given an explicit fixed-size number type

It doesn’t tell you that the variable must be const or comptime, it tells you that a variable that has type ‘comptime_float’ must be const or comptime.

This basically means if you want to keep the type comptime_float which is inferred from the 0.0 value without giving it an explicit type, you either have to change the var to const (which would mean you can no longer modify it at all), or the var has to be modified by code that runs at comptime.

You can’t use a variable with a type that requires comptime at run-time.

This code:

Tries to set the value at run-time to a different value and this can’t work if the type of that value is a comptime only type (a type that can only be used at comptime) like comptime_float.

By changing the type to something like f32 the code works, because it now can run at run-time, instead of comptime.

In this context it doesn’t make sense to use comptime_float as the type, because you can’t run a raylib application at comptime, so the thing to remember is that certain types only work at comptime, while other types can be used both at run-time and comptime.

8 Likes

0.0 literal is of type compile_float so mouse_movement_x would be of type compile_float.
compile_float - comptime only type so the only way it can exist if it is const or comptime var.

The way to solve it in your case is to give explicit type to a variable like so

var mouse_movement_x: f32 = 0.0;
2 Likes

This has nothing to do with container level variables, because we are talking about local variables in a function, this is about whether the type has to be used at comptime.

To learn more about the differences I would recommend to you to go through the exercises in ziglings/exercises: Learn the ⚡Zig programming language by fixing tiny broken programs. - Codeberg.org
and after that write some small programs to familiarize yourself with how comptime programming works, once you understand comptime better these things become more clear.

A variable that doesn’t have an explicit type annotation gets an inferred type, but in your case that inferred type from the 0.0 value is just comptime_float, that is because at that point Zig just knows that you provided a literal value (which makes the value comptime known) that happens to be a float.

But once the type of the variable is determined as comptime_float it can’t just be ‘demoted’ to another type.

What you write sounds as if you are imagining Zig to look at the later code and how it tries to use the variable and infer from that, that you meant to declare it as a f32, but Zig doesn’t do this sort of inference, it expects you to specify types where they are explicitly required to make your program work.

Instead of trying to guess on your behalf, what you could have meant.
(Which would be difficult anyway because there could be multiple different ways in which the variable is used in later parts (also doing this sort of analysis would probably lead towards a language design that is a lot slower to compile and follows different guiding principles))

1 Like

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!

15 Likes

Thank you for your detailed answers. It clarified a lot. As you were suggesting I misunderstood the error message and tangented into a very bad conclusion.
I did a little Odin prior to Zig and there the compiler inferred the type and that was it. Compared to that I need to think in Zig, which can be painful, but more enjoyable for me.
I’ll do some ziglings after I transitioned to 0.14.0 until then I’m stuck with 0.13.0 because of the raylib bindings.

1 Like

Wow, this is awesome.
Correct me if I’m wrong, but the way how Zig handles this is in favor of faster compiling or better memory usage? I guess when it’s comptime know what the type is it can allocate as many memory as it needs and call it a day?

Ziglings also has a tagged version for 0.13.0 ziglings/exercises: Learn the ⚡Zig programming language by fixing tiny broken programs. - Codeberg.org

1 Like

Memory usage and performance nowadays correlate a lot. The less memory your program uses – the faster it runs, because cache misses are very expensive (around 100-150 cpu cycles, when cpu can perform multiple additions/subtractions in one cycle).
I think that main reason for comtime numbers behaviour in Zig is language design goal, good performance can be reached with almost any behaviour when given enough time and skilled developers.
Zig has reach compile time infrastructure and utilizes it for numbers too.

I don’t know if you’re aware, but there are a couple of software tools that I know of that allow you to have multiple versions of Zig installed on your system. I’m currently using mise, and there’s asdf too.

I find this especially useful for Zig, because some of the online documentation is quite old, relatively speaking, and it has been on occasion useful to install Zig 0.11.0 in order to get older code examples to run.

Yes, I’m aware of such tools, but I felt I don’t have to use them, at least until now, I may need to reconsider adding another tool to my repertoir.