Surprising non-comptime behavior of comptime slices

I was surprised to find that the following code fails to compile:

test "std.meta.fieldNames" {
    const X = struct { a: u32 };
    const x: X = .{ .a = 0 };
    const name = std.meta.fieldNames(X)[0];
    std.debug.print("{}\n", .{@field(x, name)});
}

with a error: unable to resolve comptime value.
Meanwhile, either of these would work fine:

test "std.meta.fieldNames" {
    const X = struct { a: u32 };
    const x: X = .{ .a = 0 };
    std.debug.print("{}\n", .{@field(x, std.meta.fieldNames(X)[0])});
}
test "std.meta.fieldNames" {
    const X = struct { a: u32 };
    const x: X = .{ .a = 0 };
    const name = comptime std.meta.fieldNames(X)[0];
    std.debug.print("{}\n", .{@field(x, name)});
}

It’s unfortunate because it means that if I do an

inline for (std.meta.fieldNames(X)) |name| {
    ...
}

then name is not comptime-known in the block. That seems counterintuitive to me, especially considering the case of the analogous inline else => |_, tag| inside a switch statement.

If you change the implementation of std.meta.fieldNames to be an pub inline fn instead, it works as I expected. But I am surprised it’s necessary.

One more thing:

test "std.meta.fieldNames" {
    const X = struct { a: u32 };
    const x: X = .{ .a = 0 };
    const name = @typeInfo(X).@"struct".fields[0].name;
    std.debug.print("{}\n", .{@field(x, name)});
}

also succeeds, which is even more surprising, given that (from its implementation) std.meta.fieldNames is doing nothing more than assembling into an array the memory locations of the names of the fields of the typeInfo above, and returning a *const to that array.

You can use inline for (comptime std.meta.fieldNames(T)), which is very common in the wild

1 Like

Oh great! I didn’t think to try that. But can you explain why the error? It seems that assigning the const name has demoted a comptime-known value. Is this a general property of assignments outside comptime blocks?

The reason your pub inline fn change made it work without the comptime keyword is because comptime args can then propagate the result at comptime (per the language reference) - const’s can be runtime-known, it’s just that you can’t reassign.

Right, I know that const values can be runtime known, but somehow I managed to form the (I now think false) impression that const assignments of comptime-known expressions would always also be comptime-known.

I guess the rule is, they are only comptime-known if assigned in a comptime context, which is implicit if their type is comptime-only but otherwise you have to specify.

Does that sound right?

The reason for my mistaken impression was precisely that you don’t have to specify comptime when the expression you’re assigning has a comptime-only value!

That is actually true: if what you assign to the const variable is comptime known, then so is the const variable. In your example, for the expression to be comptime known you need to either prefix it with comptime or make the function inline, as you noticed.

Somewhat related, if you look at the implementation of fieldNames, you’ll see this:

const final = names;
break :blk &final;

…which copies the value from the comptime world to a const, whose pointer is returned at runtime (unless the callsite is comptime ofc). This is described here: https://ziggit.dev/t/comptime-mutable-memory-changes/3702

1 Like

I usually use inline for(std.meta.fields(T)) |f| something(f.name, ...) it also allows accessing the type via f.type (and other fields that may be useful), which means that f has to be comptime-known because it contains a type.

2 Likes

Ah, I think I understand now.
The point is that (non-inline) function calls will not propagate comptime-known-ness unless they’re called in a comptime context (which is implicit if their return type is comptime-only, otherwise not).

fn clone(x: anytype) @TypeOf(x) {
    return x;
}

test "a-ha" {
    const name = clone("a");
    const X = struct { a: u32 };
    const x: X = .{ .a = 1234 };
    std.debug.print("{}\n", .{@field(x, name)});
}

fails to compile.
Whereas, if I inline the function, or put a comptime at the call-site, it works as expected.

3 Likes

@Sze I think this (i.e. using std.meta.fields rather than std.meta.fieldNames) works because the type of f is

builtin.Type.StructField = struct {
    ...,
    type: type,
    ...,
};

which is comptime-only, so you don’t need to mark the callsite as comptime, unlike the case of std.meta.fieldNames.

1 Like