Does the quota set by @setEvalBranchQuota take effect in the whole function scope?

I haven’t get the sure answer via web searching, but the following code verifies my guess, though, I’m still not sure about the exact rules.

fn sum(v: u64) @TypeOf(v) {
    var r = v;
    if (v > 1) {
        var i = v-1;
        while (i > 0) {
            r += i;
            i -= 1;
        }
    }
    return r;
}

pub fn main() void {
    const n = 10_000;

    {
        @setEvalBranchQuota(n); // This line works (for the next sum call).
    }
    {
        const x = comptime sum(n);
        @import("std").debug.print("x = {}\n", .{ x });
    }
    
 
    opaque {
        fn foo() void {
           @setEvalBranchQuota(n); // This line doesn't work (for the next sum call).
        }
    }.foo();
    {
        const y = comptime sum(n);
        @import("std").debug.print("y = {}\n", .{ y });
    }
}

Yes, the branch quota is at function scope.

1 Like

In the following code, the last @setEvalBranchQuota call works, but the second one to last one (being commented out) doesn’t.

This is by design:

Increase the maximum number of backwards branches that compile-time code execution can use before giving up and making a compile error.

But, is it better to increase the minimum remaining quota instead?

fn sum(n: u64) @TypeOf(n) {
    var r = n;
    if (n > 1) {
        var i = n-1;
        while (i > 0) {
            r += i;
            i -= 1;
        }
    }
    return r;
}

pub fn main() void {
    const print = @import("std").debug.print;
    const n = 10_000;
    
    @setEvalBranchQuota(n);
    const y = comptime sum(n);
    print("y = {}\n", .{ y });
    
    //@setEvalBranchQuota(n); // This line doesn't work.
    @setEvalBranchQuota(n + n); // This line works.
    comptime var z = sum(n);
    z = z;
    print("z = {}\n", .{ z });
}

I still have confusions in using the @setEvalBranchQuota function.

The following are some workable and non-workable examples.

Workable 1:

fn sum(n: u64) @TypeOf(n) {
    var r = n;
    for (1..n) |v| r += v;
    return r;
}

pub fn main() void {
    const n = 10_000;
    
    @setEvalBranchQuota(n);
    const y = comptime sum(n);
    @import("std").debug.print("y = {}\n", .{ y });
}

Workable 2:

fn sum(n: u64) @TypeOf(n) {
    @setEvalBranchQuota(n);
    
    var r = n;
    for (1..n) |v| r += v;
    return r;
}

pub fn main() void {
    const n = 10_000;
    
    const y = comptime sum(n);
    @import("std").debug.print("y = {}\n", .{ y });
}

Non-workable 1:

fn sum(n: u64) @TypeOf(n) {
    var r = n;
    for (1..n) |v| r += v;
    return r;
}

fn wrapper(n: u64) @TypeOf(n) {
    @setEvalBranchQuota(n);
    return sum(n);
}

pub fn main() void {
    const n = 10_000;
    
    const y = comptime wrapper(n);
    @import("std").debug.print("y = {}\n", .{ y });
}

Non-workable 2:

fn sum(n: u64) @TypeOf(n) {
    @setEvalBranchQuota(n);
    var r = n;
    for (1..n) |v| r += v;
    return r;
}

pub fn main() void {
    const n = 10_000;
    
    const y = comptime sum(n); // this is okay
    @import("std").debug.print("y = {}\n", .{ y });
    
    const z = comptime sum(n); // comppilation stops here
    @import("std").debug.print("z = {}\n", .{ z });
}

What are the exact rules?

There is a bug smell:

fn sum(n: u64) @TypeOf(n) {
    @setEvalBranchQuota(n + 1234);
    var r = n;
    for (1..n) |v| r += v;
    return r;
}

test "foo" {
    comptime {
        @setEvalBranchQuota(1234);
        var i = 0;
        // Now, the @setEvalBranchQuota call in the "sum"
        // function doesn't work. However, changing the
        // next 1234 to 1233 will make it work.
        while (i < 1234) : (i += 1) {}
    }
    
    const n = 10_000;
    const y = comptime sum(n); // this is okay
    @import("std").debug.assert(y == 50005000);
}

[Edit]: not a bug. It is the same as @TerenceTux described in the next comment.

Your non-workable 1 does work if you change wrapper to

fn wrapper(n: u64) @TypeOf(n) {
	@setEvalBranchQuota(n + 1); // added 1
	return sum(n);
}

So it seems calling a function consumes one branch quota.

Non-workable 2 does not work because @setEvalBranchQuota only sets the limit of the global branch counter. It does not add the value.
The reason for this might be because you could otherwise call sum in an infinite loop.

Aha, cool finding!

But then why does the following work:

fn sum(n: u64) @TypeOf(n) {
    var r = n;
    for (1..n) |v| r += v;
    return r;
}

fn foo(comptime n: comptime_int) void {
    @setEvalBranchQuota(n); // works
    
    const y = comptime sum(n);
    @import("std").debug.print("y = {}\n", .{ y });
}

pub fn main() void {
    const n = 10_000;
    
    @setEvalBranchQuota(n); // works
    const x = comptime sum(n);
    @import("std").debug.print("x = {}\n", .{ x });
    
    foo(n - 1);
    foo(n + 1);
}

The last two foo calls are not at comptime, so the sum is computed at runtime. If you mark one of them with comptime, it will fail.

But the foo function calls comptime sum(n).

So it looks the branch counter is not global. It still has a scope. A common runtime function scope for some comptime evaluations?

Now I’m confused too. It seems to matter that foo has a comptime parameter:

fn sum(n: u64) @TypeOf(n) {
    var r = n;
    for (1..n) |v| r += v;
    return r;
}

fn foo(comptime n: comptime_int) void {
    const y = comptime sum(n);
    @import("std").debug.print("y = {}\n", .{ y });
}

fn foo2() void {
    const y = comptime sum(10_000);
    @import("std").debug.print("y = {}\n", .{ y });
}

pub fn main() void {
    const n = 10_000;
    
    @setEvalBranchQuota(n);
    
    foo(n); // this works
    foo2(); // this does not work
}

Edit: also, the branch quota exists to prevent an infinite loop, but it can’t detect this one:

fn foo(comptime n: comptime_int) void {
    foo(n + 1);
}

pub fn main() void {
    foo(0);
}

I almost get the rules, I think. The @setEvalBranchQuota call in your code doesn’t take effect. The error message says error: evaluation exceeded 1000 backwards branches. Note that the quota is still 1000.

It looks my guess in my last comment is right:

So it looks the branch counter is not global. It still has a scope. A common runtime function scope for some comptime evaluations?

If you move the @setEvalBranchQuota call to the foo and foo2 functions (two runtime scopes), then they work.

fn foo(comptime n: comptime_int) void {
    @setEvalBranchQuota(n);
    const y = comptime sum(n);
    @import("std").debug.print("y = {}\n", .{ y });
}

fn foo2() void {
    @setEvalBranchQuota(10_000);
    const y = comptime sum(10_000);
    @import("std").debug.print("y = {}\n", .{ y });
}

If we merge foo and foo2 into the same comptime scope, then the code will not work again:

    comptime {
        foo(n); // this works
        foo2(); // this does not work
    }

We must set enough quota before the comptime block:

    @setEvalBranchQuota(n + n + 1 + 1);
    
    comptime {
        foo(n); // this works
        foo2(); // this does not work
    }

Because the foo calls are runtime expressions. So the compiler doesn’t prevent them from being infinite.

And your example is more a code generation problem. It looks the compiler doesn’t prevent infinite code generation?