#why does this comptime function still get called

1 messages · Page 1 of 1 (latest)

somber pewter
#

I have this code

const std = @import("std");

const numbers = [_]u8{ 1, 2, 3, 4};

fn sum_list(comptime list: []const u8) u32 {
    var sum: u32 = 0;

    for (list) |num| {
        sum += num;
    }

    return sum;
}

pub fn main() anyerror!void {
    const sum = sum_list(&numbers);
    std.debug.print("summed = {d}\n", .{sum});
}
``` and I want it to figure out that sum is 10 at comptime

but for some reason it's still calling a function in the binary

example.main:
push rbp
mov rbp, rsp
sub rsp, 16
call example.sum_list__anon_3411
mov dword ptr [rbp - 8], eax
lea rdi, [rbp - 8]
call debug.print__anon_3413
xor eax, eax
add rsp, 16
pop rbp
ret


can see this at <https://godbolt.org/z/WTY1Tz96b>
honest pasture
#

The parameter type comptime list: []const u8 actually only means that list must be a comptime-known value -- it doesn't actually mean that the function is implicitly evaluated at compile time.

What you want is to force comptime evaluation at the callsite: const sum = comptime sum_list(&numbers);

somber pewter
#

I see

#

thanks

honest pasture
#

👍

sterile field
#

Zig's comptime evaluation won't try to do everything at comptime. Imagine you instead had this function:

fn sumList() u32 {
    const list = [_]u8{ 1, 2, 3, 4 };
    var sum: u32 = 0;
    for (list) |num| sum += num;
    return sum;
}

This is semantically equivalent to what you have - the important thing is that list is comptime-known to it. Despite that, the result of this function is still calculated at runtime, because unless otherwise specified, var and for are explicitly runtime-only concepts. Your only hope at optimisation of that code would be LLVM itself optimising it away, which it's clearly not doing.
There are a few approaches here. If you only need sumList to work at comptime, you can wrap its whole body in a comptime block to force compile-time evaluation:

fn sumList(comptime list: []const u8) u32 {
    comptime {
        var sum: u32 = 0;
        for (list) |num| sum += num;
        return sum;
    }
}

(That's equivalent to the following:)

comptime var sum: u32 = 0; // a 'comptime var' is a variable which can be manipulated at comptime (and only at comptime)
comptime {
    // a 'for' loop is usually a runtime construct, which in this case would cause
    // compile errors since you'd be modifying 'sum' at runtime; tell Zig to evaluate
    // it at comptime instead
    for (list) |num| sum += num;
}
return sum;
#

This approach works, but for this function, it's not how you'd normally do it. Because the function makes perfect sense at both runtime and comptime (i.e. contains no comptime-specific logic), the usual approach would be to mark the call site as comptime, like hryx said:

fn sumList(list: []const u8) u32 { // note, no need for a comptime parameter!
    var sum: u32 = 0;
    for (list) |num| sum += num;
    return sum;
}

pub fn main() void {
    const numbers = [_]u8{ 1, 2, 3, 4 };
    const sum = comptime sumList(&numbers);
    std.debug.print("summed = {d}\n", .{sum});
}

Marking an expression as comptime (or, equivalently, putting it somewhere in a comptime block) tells Zig that the entire thing has to be comptime-evaluated (and you'll get a compile error if it can't be).

#

In general, these things aren't implicitly comptime-evaluated, even if it's theoretically possible:

  • anything involving vars
  • anything involving for/while
  • results of function calls (except for comptime-only and inline functions)
#

A function itself is only comptime-only if its return type is comptime-only; for instance, a function returning a type is comptime-only, because it's literally impossible for that function to exist at runtime

#

Sorry, I know I've been slightly rambling, but one more point of clarification as to what a comptime arg actually does. It causes the compiler to make separate instantiations of the function every time you call it with a different parameter. It's basically like a template argument in C++; if I had fn foo(comptime x: u32) void { ... }, then foo(1) and foo(2) would actually create two different functions in the binary, say foo_1() and foo_2(). That's why in your asm, the function was calledsum_list__anon_3411 - the last bit was appended to it to identify the specific instantiation in question

somber pewter
#

I see

#

it makes a lot more sense now