Question about comptime evaluation of functions

I’m trying to understand better how comptime works. For that, I’m trying to write a function that returns an array of bytes.

The signature I want is the following:

fn range(comptime start: comptime_int, comptime end: comptime_int) [end - start]u8

The behavior I want that when i call range, like this:

const v = range(1, 6);

I get:

const v = [5]u8{1, 2, 3, 4, 5};

The key point is that I want to force the compiler to always evaluate my function at compile time. I want it to act as a macro.

I came up with the following:

pub inline fn range(comptime start: comptime_int, comptime end: comptime_int) [end - start]u8 {
    return comptime blk: {
        var array: [end - start]u8 = undefined;

        for (&array, start..end) |*item, n|
            item.* = n;

        break :blk array;
    };
}

First I put all the logic in a comptime block, to force the compile time execution. So, If I evaluate the function with parameters start = 1 and end = 6, it should be equivalent to the following:

fn range(comptime start: comptime_int, comptime end: comptime_int) [end - start]u8 {
   return [5]u8{1, 2, 3, 4, 5};
}

I also added the inline keyword to force the function to be semantically inlined. With that I intend that when the compiler sees:

range(1, 6)

It actually replaces it with:

[5]u8{1, 2, 3, 4, 5}.

Am I right? Is there a simpler way to achieve what I want?

I know that I could write the function without the inline keyword and the comptime block, and then evaluate it like this:

const v = comptime range(1, 6);

This would simplify my function definition, but I would have to always prepend the function evaluation with the comptime keyword.

This is not what I want, I want my function to act just as a macro, so If I want an array having the values 1 … 21, I don’t have to write it by hand. But I want to explicitly force comptime evaluation.

Update: The AI gave me the following equivalent implementation that I find interesting:

inline fn range(comptime start: comptime_int, comptime end: comptime_int) [end - start]u8 {
    comptime {
        var array: [end - start]u8 = undefined;

        for (0..array.len) |i|
            array[i] = start + i;

        return array;
    }
}

This compiles and works as expected. And also seems a little bit cleaner to my eye.

The interesting point here is that if I remove the inline keyword I get the following error:

error: function called at runtime cannot return value at comptime

I think this is interesting. The compiler only lets me return from inside the comptime block if I add the inline keyword. I still don’t understand everything that is happening here. Explanations are welcome :P.

As a comment, I think it would be nice to be able to prepend the function declaration with the comptime keyword to actually turn it into a macro, like this:

comptime fn range(comptime start: comptime_int, comptime end: comptime_int) [end - start]u8 {
    // ...
}

If you do a comptime return, your function must return at compile-time, and therefore be called in a comptime context, hence the error.

If you inline the function, the comptime context is in the function already.

2 Likes

This won’t happen, as when calling the function, you wouldn’t be able to know if it’s in comptime or not without checking the definition. Which goes against zig’s goal of being explicit and readable.

1 Like

Let me see if I understood correctly.

In general, you can’t return from a function from within a comptime block, since the function can be being evaluated at runtime.

So, when I do this:

fn range(comptime start: comptime_int, comptime end: comptime_int) [end - start]u8 {
    comptime {
        var array: [end - start]u8 = undefined;

        for (0..array.len) |i|
            array[i] = start + i;

        return array;
    }
}

And then I call it like this:

const v = range(1, 6);

I get an error. To get rid of the error, I should call the function like this:

const v = comptime range(1, 6);.

On the other hand, if I prepend the function definition with the inline keyword, when I call the function like this:

const v = range(1, 6);

what is actually happening is the following:

const v = comptime blk: {
    var array: [5]u8 = undefined;

    for (0..array.len) |i|
        array[i] = 1 + i;

    break :blk array;
};

which this is perfectly legal.

Am I right?

Almost.

In general, you can return from a function at runtime or comptime.

Some things, such as using inlined assembly or @ptrFromInt, will prevent you from returning at comptime.

Some things, such as returning a type or using comptime return will prevent you from using it at runtime.

And unless the function is inlined, calling a function outside a comptime context (default value, init value, comptime block) is considered calling the function at runtime.

3 Likes

Nice! Thank you!

Ran into this just now. Had a struct with a field of type type and was confused for a bit why the compiler complained about me using a comptime only struct…

Yep, this happens with any kind of comptime only type:

  • comptime_int
  • comptime_float
  • type
  • user-defined type with a comptime-only child.
3 Likes

I just noticed that this pattern is being used in the standard library, here: