Why is the comptime keyword needed in this inline for loop condition?

Here’s a situation that exposed a gap in my thinking about comptime and when the keyword is actually needed. First the background.

I wrote a custom formatting function that prints out a MultiArrayList in a nice fashion:

pub fn formatTokens(tokens: Tokens, w: *io.Writer) io.Writer.Error!void {
    const fields = std.meta.FieldEnum(Token);
    const slice = tokens.slice();
    for (0..slice.len) |i| {
        const item = slice.get(i);
        try w.print("[{d}] ", .{i});
        inline for (comptime enums.values(fields)) |field| {
            const field_info = std.meta.fieldInfo(Token, field);
            try w.print("{s}: {any}, ", .{ field_info.name, @field(item, field_info.name) });
        }
        try w.writeAll("\n");     
    }
}

const Tokens = std.MultiArrayList(Token);

My question is why do I need (comptime enum.values(fields)) in the inline for loop? I do need it because without it i get the standard “unable to resolve comptime value” and “inline loop condition must be comptime-known” messages. At first I assumed that it was because enum.values must not return a comptime known value, but it actually does (or that’s how I read this code).

// std.enums
pub fn values(comptime E: type) []const E {
    return comptime valuesFromFields(E, @typeInfo(E).@"enum".fields);
}

My guess is that since []const E can be runtime, std.enums.values is considered a runtime function unless you use the comptime keyword. That the return value of std.enums.values is a constant (or a comptime expression) seems not to affect this.

A possible change to the zig std lib, which makes the comptime keyword unnecessary (Edit: this makes comptime only unnecessary if field is not needed to comptime known. See this comment):

diff --git a/lib/std/enums.zig b/lib/std/enums.zig
index 76deff5421..e0a3a383b3 100644
--- a/lib/std/enums.zig
+++ b/lib/std/enums.zig
@@ -40,7 +40,7 @@ pub fn EnumFieldStruct(comptime E: type, comptime Data: type, comptime field_def
 /// Looks up the supplied fields in the given enum type.
 /// Uses only the field names, field values are ignored.
 /// The result array is in the same order as the input.
-pub inline fn valuesFromFields(comptime E: type, comptime fields: []const EnumField) []const E {
+pub inline fn valuesFromFields(comptime E: type, comptime fields: []const EnumField) *const [fields.len]E {
     comptime {
         var result: [fields.len]E = undefined;
         for (&result, fields) |*r, f| {
@@ -53,7 +53,7 @@ pub inline fn valuesFromFields(comptime E: type, comptime fields: []const EnumFi
 
 /// Returns the set of all named values in the given enum, in
 /// declaration order.
-pub fn values(comptime E: type) []const E {
+pub fn values(comptime E: type) *const [@typeInfo(E).@"enum".fields.len]E {
     return comptime valuesFromFields(E, @typeInfo(E).@"enum".fields);
 }
 
1 Like

(1) The compiler won’t try to evaluate function calls at comptime. This change was made several versions ago but I can’t remember the exact date.

(2) The argument to an inline for does not need to be comptime known, so it doesn’t force comptime evaluation either.

2 Likes

I agree that this is somewhat confusing, it’s because of a few subtleties I myself have only gotten a grasp on somewhat recently. It’s a combination of two things:

  1. Functions only run at comptime if they’re in a comptime scope, or return a comptime only type.
  2. Even though the return value is evaluated at comptime, it isn’t comptime known in the calling function.
  3. inline for’s parameters aren’t implicitly comptime, because only the iteration condition is required to be comptime.

For the first point, look at the following examples:

pub fn main() void {
    const a: u8 = 1;
    const b: u8 = 2;
    const v = a + b;
    _ = comptime v;
}

Compiles just fine. However,

pub fn main() void {
    const a: u8 = 1;
    const b: u8 = 2;
    const v = add(a, b);
    _ = comptime v;
}
fn add(a: u8, b: u8) u8 {
    return a + b;
}

Gets

An error occurred:
/tmp/playground2832450235/play.zig:5:9: error: unable to resolve comptime value
    _ = comptime v;
        ^~~~~~~~~~

As add doesn’t implicitly run during comptime, v is not comptime known.

pub fn main() void {
    const a: u8 = 1;
    const b: u8 = 2;
    const v = comptime add(a, b);
    _ = comptime v;
}
fn add(a: u8, b: u8) u8 {
    return a + b;
}

If we explicitly ask to for the scope to be comptime, it will compile again.

As a general rule, functions usually don’t run in comptime implicitly, but there are exceptions:

pub fn main() void {
    const a: u8 = 1;
    const b: u8 = 2;
    const v = add(a, b);
    _ = comptime v;
}
fn add(a: u8, b: u8) comptime_int {
    return a + b;
}

Bizarrely, the above compiles just fine as well. add returns a comptime only type, so it will implicitly run in comptime.

pub fn main() void {
    const a: u8 = 1;
    const b: u8 = 2;
    const v, _ = @addWithOverflow(a, b);
    _ = comptime v;
}

Also works fine. Builtins look like functions, but are really just fancy looking syntax. So this behaves like + and not like fn add.

2. Even though the return value is evaluated at comptime, it isn’t comptime known in the calling function.

pub fn main() void {
    const a: u8 = 1;
    const b: u8 = 2;
    const v = add(a, b);
    _ = comptime v;
}
fn add(comptime a: u8, comptime b: u8) u8 {
    return comptime a + b;
}

This will give an error still. The arguments to add need to be comptime to avoid a separate error. What happens here is that during comptime, add will run with 1 and 2 as arguments, evaluate to 3, and simplify the function to be essentially:

fn add_1_2() u8 {
  return 3;
}

3. inline for’s parameters aren’t implicitly comptime, because only the iteration condition is required to be comptime.

Some expressions are implicitly comptime, such as the default value of container level declarations, type declarations, or functions which return a comptime only type. The parameters for inline for is not such a place.

It requires that the loop condition (essentially, how many times the loop will run) to be comptime known, but not the values themselves.

fn foo(a: [2]u8) u8 {
    var sum: u8 = 0;
    inline for (a) |v| {
        sum += v;
    }
    return sum;
}

Essentially boils down to:

fn foo(a: [2]u8) u8 {
  return a[0] + a[1];
}

Note that

fn foo(a: [2]u8) u8 {
    var sum: u8 = 0;
    inline for (a) |v| {
        _ = comptime v;
        sum += v;
    }
    return sum;
}

will error because of v not being comptime known, despite being the value provided by the inline for.

An error occurred:
/tmp/playground3674012945/play.zig:8:13: error: unable to resolve comptime value
        _ = comptime v;
            ^~~~~~~~~~

Re: @rpkak

I don’t believe your suggestion will work, since the OP code does require the iterated value to be comptime known. Eg,

fn bar() *const [2]u8 {
    return comptime &.{1, 2};
}
pub fn main() void {
    inline for (bar()) |v| {
        _ = comptime v;
    }
}

Gets error: unable to resolve comptime value. It would let inline for work for cases where the value doesn’t need to be comptime known, but you could (and likely should) just use a normal for loop in that case.

15 Likes

That is a great write up, and really elucidates what I was missing. If I boil it down, it is "It doesn’t matter what that a function’s return value is evaluated at comptime, a function will only be comptime evaluated if it’s return type is a comptime known value.

3 Likes

Nice examples!

As a sidenote: this is a great example where inline fn add would also make it compile thanks to comptime-parameter-inline-function-semantics

2 Likes

One more condition: a function will also be implicitly executed at comptime if it is in a comptime position. For example:

pub fn main() void {
    var arr: [add(20, 400)]u8 = undefined;
    _ = &arr;
}

pub fn add(a: i32, b: i32) i32 {
    return a + b;
}

This compiles fine (tested on 0.16.0-dev.2296+fd3657bf8). The array length is required to be comptime-known, and so add(20, 400) is executed at comptime.

I think this previous comment of mine would help shed some light onto the semantics of inline for, which will help explain why inline for doesn’t force the value of the condition to be known at comptime, like the above array length example does.

2 Likes

I tried seeing if I could modify std.enum.values and std.enum.valuesFromFields to work with how you’re trying to loop over the result without using comptime in the loop condition. It seems like returning slices is simply underspecified. Since the length will always be statically known, they should return arrays instead. I changed them to this:

// Added length in return type
pub inline fn valuesFromFields(comptime E: type, comptime fields: []const std.builtin.Type.EnumField) [fields.len]E {
    comptime {
        var result: [fields.len]E = undefined;
        for (&result, fields) |*r, f| {
            r.* = @enumFromInt(f.value);
        }
        const final = result;
        return final; // changed from &final
    }
}

// Added length in return type, and made inline to propagate the comptime-ness of the values in the array to the call-site 
pub inline fn values(comptime E: type) [@typeInfo(E).@"enum".fields.len]E {
    return comptime valuesFromFields(E, @typeInfo(E).@"enum".fields);
}

Which let me loop over the returned array like this:

const MyStruct = struct {
    foo: u8,
    bar: i128,
};

pub fn main() void {
    const fields = std.meta.FieldEnum(MyStruct);

    inline for (values(fields)) |field| {
        const field_info = std.meta.fieldInfo(MyStruct, field);
        std.debug.print("{s}: {any}\n", .{ field_info.name, field_info });
    }
}

Seems like maybe the std library should be patched :slight_smile:

Edit: I went overboard, values literally just needs inline as stated above.

Edit 2: I searched around in the std library, and you seem to have run into one of only two places where a function returns a comptime value and is not marked as inline. Made a PR :slight_smile:

1 Like