How should the term "comptime value" be defined?

I’m not sure if the following definition is precise.

A `comptime value` is a value determined at compile time.

Maybe, if we don’t treat a pointer as a specified address, then it is precise.
For example, in the following code, &x is a comptime value, but the address value stored in it is not.

var x = true;

pub fn main() void {
    // The two lines compile okay.
    _ = comptime &type;
    _ = comptime &x;
    
    // The two lines fail to compile when enabled.
    // _ = comptime @intFromPtr(&type);
    // _ = comptime @intFromPtr(&x);
}

From the docs:

At Namespace level (outside of any function), all expressions are implicitly comptime expressions

x itself is comptime in your example and so &x itself is known at compile time.

Look also on this example, here &x is not only taken but also used

const std = @import("std");

// Try to add comptime keyword here and will get error: redundant comptime keyword in already comptime scope
//              |
//              |
//              *
var compt_var =  compt_fn();

fn compt_fn() u8 {
    var x: u8 = 0;
    const y = &x;
    y.* = 10;
    return x;
}
pub fn main() void {
    std.debug.print("{}\n", .{compt_var}); // 10
}

So in my opinion it is more of a problem of @intFromPtr rather than %x itself but I am still new to the language and moreover have a lot of unresolved questions about comptime by myself so maybe probably I am wrong

EDIT: My point is that compiler needs to actually run the code so there should be real pointers with real values which are obviously only comptime known and then discarded but they are valid…?

1 Like

This might be not accurate. Namespace-level mutable declarations are absolutely not comptime values. For example the x in my code. Their initializer expressions indeed are.

I don’t know if there is a simple way to verify whether or not an expression is an always-comptime value. I think my way _ = comptime anExpression is not rigorous. The way just verifies that anExpression can be used as a comptime value. In fact, it might be a runtime value. Though I think the conclusion of the address of a namespace-level mutable declaration is an always-comptime value should be right.

Maybe comptime values should be categorized into three groups:

  • Explicit comptime var values and comptime parameters. They can be called absolute-comptime values.
  • Values of comptime-only types. They can be called as comptime-only values.
  • Implicit comptime vlaues, which depend on compilers to decide them as comptime. They can be called comptime-capable values.

It is interesting that comptime var and comptime parameters are handled differently.
Maybe just an undefined behavior.

const print = @import("std").debug.print;

var runtimeP: *bool = undefined;

fn foo(comptime x: bool) void {
    runtimeP = @constCast(&x);
    
    runtimeP.* = false;             // oaky
    print("{}\n", .{ runtimeP.* }); // false
    print("{}\n", .{ x });          // true
}

pub fn main() void {
    comptime var b = true;
    
    foo(b);

    var runtimePb = &b; // error: runtime value contains reference to comptime var
    _ = &runtimePb;
}

BTW, the following code causes “Segmentation fault” in compilation. (Filed at https://codeberg.org/ziglang/zig/issues/36033)

var runtimeP: *bool = undefined;

pub fn main() void {
    comptime var b = true;
    runtimeP = &b;
}

Zig generally disallows taking pointers to comptime variables with you into runtime.

Definitely, casting const away from a reference to an object that was declared as const (which is true for a function parameter) is illegal behaviour.

3 Likes

In first place there is comptime pointer to comptime address, in another one there is runtime pointer to comptime address (which is illegal)


I kinda can’t wrap my head around comptime values but I clearly see that there are really two ways of how comptime variables* treated:

  • Implicit comptime var (the ones outside of function scope) are known at compile and then pushed to some real memory accessible in runtime, after that you have just runtime variable to which you can have pointer.

    • (And it is really a bit confusing that it is not mentioned anywhere. At the same time it is kinda obvious that variables outside functions scope has runtime address…)
  • comptime and explicit comptime var exist only in compile, can be used there and inserted by compiler to runtime with “inlining semantics”


Btw if you really need a “create runtime variable with value evaluated at comptime” semantic similar to what happens with implicit comptime var you can use comptime expressions:

var run_var: u32 = comptime 10;
const p: *u32 = &run_var;
p.* = 100;
std.debug.print("{}\n", .{run_var}); // 100

No, Both are runtime pointer to comptime address.

Btw if you really need a “create runtime variable with value evaluated at comptime” semantic

No, I never have this need. I just have a small interest in the semantics. :smiley:

Here the x is comptime and so whole function is comptime so runtimeP is actually comptime..?

Surely not. runtimeP is absolutely a runtime value.

A global var not only is runtime, but it can’t be comptime. Try using it in a comptime block.

comptime expression absolutely enforces that the expression must be comptime known.

You’ve been confused by your pointer example by assuming a pointer and a raw address are the same thing, don’t equate under the hood runtime details with a programming languages semantics, they are different realms of thinking.

You absolutely can use pointers at comptime, so long as the pointer is comptime known.
But a comptime known pointer address may not map to a runtime address; and comptime memory semantics are a lot less defined, so zig just enforces that addresses and raw memory of undefined layout types can only be accessed at runtime.

2 Likes

At least, for some function calls, this is not true. For example the double(3) calls in the following code. It can be used as either a runtime value or a comptime value,

noinline fn double(n: i64) i64 {
    return n + n;
}

pub fn main() void {
    const x = double(3);
    const y = comptime double(3);
    _, _ = . {x, y};
}

But a comptime known pointer address may not map to a runtime address;

I think this basically right. However, the current compiler handles different cases differently. For example:

const print = @import("std").debug.print;

noinline fn double(n: i64) i64 {
    return n + n;
}

pub fn main() void {
    var x = &type;               // okay
    var y = &comptime double(3); // okay
    print("{}\n", .{ x });
    print("{}\n", .{ y });
    _ = &x;
    _ = &y;
    
    comptime var b = true;
    var z = &b;                 // error
    print("{}\n", .{ z });
    _ = &z;
}

I am confused, this is compatible with what I said:

To be more explicit; expressions can still be comptime even without the comptime keyword, I never said otherwise.

I never denied this. I just meant that, in comptime expr, expr may be used as a runtime value.

this is a denial of what I said, clearly you did not intend that, but it can’t be interpreted differently.

yes, an expression without other context can be comptime or runtime.

plenty of expressions can be either, but some can only be comptime or runtime.

But you have been quite loose with using these terms, so I will clarify:

  • an expression is something produces a value, it can be some operations or a direct value itself
  • a value is actual data with a type

You referred to double(3) as a value, but it is not, rather it is an expression, a function call, that returns a value.

again, this is compatible with what I said, in fact I explained this explicitly:

x and y are const pointers to comptime immutible data, this allows the compiler to safely map the comptime data to runtime memory.

&type does not get mapped to runtime memory, but the pointer is allowed to exist to simplify code so long as it does not try to de reference it.

However, b is comptime mutable data, and &b produces a non-const pointer. It simply cannot be mapped to runtime memory.
Even if you coerce it to a const pointer, what data would it point to? the data at the time you took the pointer? or the final value after analysis?
Zig gets around all this, and more I can’t remember right now, by simply forcing you to copy the data to comptime immutable memory, which can be safely mapped to runtime, just as x and y were.

You do this like so

comptime var b = true;
const b_copy = b; 
var z = &b_copy;
print("{}\n", .{ z });
_ = &z;
2 Likes

Let’s not mistake “value” and “variable” here. A runtime variable can hold a comptime value. A comptime variable can’t “leak” in runtime, but its value might.

3 Likes

Not true for x, which is not a const pointer. It is more like z than y. Aha, maybe you are right. :smiley:

const print = @import("std").debug.print;

pub fn main() void {
    var x = &type;
    print("{}\n", .{ x });          // type@aaaaaaaaaaaaaaaa
    print("{}\n", .{ @TypeOf(x) }); // *const type
    _ = &x;
}

Though, I would argument the situation of &z is similar, by treating comptime var as runtime immutable values?

No it is a const pointer, you can check:

var x = &type;
_ = &x;
std.debug.print("{}", .{@TypeOf(x)});
// prints `*const type`

I did clarify:

2 Likes

Sorry. I updated my last comment.