Why static value is treated as comptime var?

I get the following error for below code

error: runtime value contains reference to comptime var
    std.debug.print("{any}", .{y_p});
                             ~^~~~~
scratchpad/dummy.zig:5:31: note: comptime var pointers are not available at runtime
const std = @import("std");
pub fn main() !void {
    const y = .{[_]usize{10}};
    const y_p = &y[0];
    std.debug.print("{any}", .{y_p});
}

y tags to static value i.e, an anonymous struct and when I refer to it’s internal value i get the above error.

I don’t understand why it’s considered as comptime variable ?

Tuples can only exist at comptime.

2 Likes

When you have a declaration without specifying a type and the value is a tuple (or int or float), it’ll be assigned as a comptime-only type. If you explicitly give it a type, it’ll be available at runtime:

    const y: struct{[1]usize} = .{[_]usize{10}};
5 Likes
const std = @import("std");
pub fn main() !void {
    const y = .{[_]usize{10}};
    const y_p = &y;
    std.debug.print("{any}", .{y_p});
}

Why the above works isn’t y a comptime variable and y_p is referring to a comptime variable pointer ?

If you print the type of y, you get struct{comptime [1]usize = .{ 10 }} so it appears that the tuple itself isn’t comptime but the array inside is. So the pointer to the tuple is OK whereas with the inner array it’s not. Now why this is so is a good question that maybe someone else can shed some light on.

1 Like

So the tuple isn’t comptime but inner fields are comptime.

std.debug.print() is perfectly capable of printing out things that only exist in comptime. Example:

const std = @import("std");

pub fn main() !void {
    std.debug.print("{any} {any} {any}\n", .{ .hello, i32, null });
}
.hello i32 null

Basically, they’re converted to their string representation at comptime. And that’s what gets printed at runtime.

Tuples are definitely comptime only.

But then how come using a pointer to the tuple field doesn’t compile but using a pointer to the tuple itself compiles just fine? It seems like an inconsistency here.

That’s due to the change from 0.11.0 to 0.12.0. Comptime pointers are no longer treated as comptime known. As such, you cannot pass them into a function that does things at runtime.

Consider the following code, which does compile in 0.11.0:

const std = @import("std");

fn a(comptime ptr: *comptime_int) void {
    std.debug.print("A: {d}\n", .{ptr.*});
    ptr.* += 1;
}

fn b(comptime ptr: *comptime_int) void {
    std.debug.print("B: {d}\n", .{ptr.*});
    ptr.* += 1;
}

pub fn main() void {
    comptime var i = 0;
    a(&i);
    b(&i);
    a(&i);
}
A: 0
B: 1
A: 0

Based on the output we can deduce that a() is compiled first, then b() is compiled. The target of the comptime pointer will have a consistent value (comptime known, in another word) only if the compiler always compiles everything and does so in the same order. As you know, incremental compilation is a major feature being developed right now. Hence these restrictions on comptime pointers.

That’s a great example, but my confusion arises from code like the following:

const std = @import("std");

fn a(ptr: anytype) void {
    ptr.* = 42;
    std.debug.print("ptr: {}\n", .{ptr.*});
}

fn b(ptr: anytype) void {
    ptr.*[0] = 42;
    ptr.* = .{42};
    std.debug.print("ptr: {}\n", .{ptr.*});
}

pub fn main() !void {
    // I have to write comptime var here.
    // comptime var x = 42;

    // This desn't compile as of 0.12.0 and is
    // expected behavior.
    // a(&x);

    // I don't have to write comptime var here,
    // which is inconsistent with other comptime
    // only types.
    var y = .{42};

    // This compiles and runs, which is inconsistent
    // with what happens with other comptime only types.
    b(&y);

    // This doesn't compile, which is consistent with
    // the fact that tuples are comptime only and y is
    // a tuple.
    // for (y) |item| std.debug.print("{}\n", .{item});
}

So it’s as if tuples behave like comptime-only sometimes.

You’re right, actually. Tuples are structs defined on the fly. When a field is comptime known, it’s a comptime field in the struct. When it’s not, then it’s a regular field. Something like this is possible:

const std = @import("std");

var int: i32 = 0;
var float: f32 = 0;

pub fn main() void {
    const Tuple = @TypeOf(.{ .first = int, .second = int, .third = float });
    var array: [10]Tuple = undefined;
    for (&array) |*el| {
        el.first = 123;
        el.second = 456;
        el.third = 0.5;
    }
    std.debug.print("{any}\n", .{array});
}

I’ll go back and strike out the incorrect info :stuck_out_tongue:

1 Like

Tuples are full-fledged types, they don’t have to be anonymously constructed either.

I have a function with the following signature:

fn counts(self: SomeStructType) struct { usize, usize, usize, usize }

Which is pretty useful, because the return value can be deconstructed into variables, which we can’t do with named fields (yet).

They can also be given names, like any other struct:


const ATupleType = struct { usize, u8, f64 };

fn returnTuple() ATupleType {
    return .{ 1, 2, 3 };
}

test "named tuple" {
    const returned_tuple: ATupleType = returnTuple();
    std.debug.print("type has name: {s}\n", .{@typeName(ATupleType)});
    try expectEqual(1, returned_tuple[0]);
    try expectEqual(2, returned_tuple[1]);
    try expectEqual(3.0, returned_tuple[2]);
}

I think the current documentation is a bit unclear about this, since it says “Anonymous structs can be created without specifying field names, and are referred to as “tuples”.”

It would be more accurate to drop the anonymous part.

2 Likes

The tuple has what’s called a “comptime field” – these are effectively immutable fields whose value is a part of the type. You can think of them as like declarations, but accessed via instances rather than the type. The main reason these exist for tuples is for @call, and they also exist for regular structs – which can absolutely be handy sometimes!

Having a runtime pointer to an aggregate with a comptime field is totally fine, because the field isn’t actually stored in memory alongside normal fields – as I say, it’s more like a declaration. However, there is an annoyance in the current language spec: comptime fields are semantically mutable at comptime, as long as the value you store is equal to the canonical value. This is a weird rule which I think exists for reasons to do with RLS. I would like to get rid of it, but we’ve not gotten around to discussing that yet. The point is, this design quirk has as an unfortunate consequence that pointers to comptime fields have to be treated as though they were pointers to comptime vars to prevent soundness issues in the type system. Hopefully this will all change at some point.

5 Likes