Comptime var behavior within defer

The following code results in i being 2. I think it might be a bug in Zig, but it’s sufficiently confusing that I’m not sure. Is this expected behavior? What’s going on?

fn nop() error{E}!void {}

pub fn main() !void {
    comptime var i = 0;
    {
        defer {
            i += 1;
        }
        try nop();
    }
    @compileLog(i); // outputs 2
}
4 Likes

Welcome to the forum!

Just to add a point: the behavior depends on whether nop has an inferred error set or not. Remove error{E} and it outputs 1.

1 Like

To elaborate on this.
In addition to the natural end of the block, try introduces another exit to propagate the error from nop.
defer places its statement at both exits as is expected and because i is a comptime var both get evaluated at compile time, hence i being 2.

If the nop returns an empty error set, then zig knows there is no error to propagate so try doesn’t introduce another exit, hence i being 1.

7 Likes

Yeah, that’s clearer. If we have fn nop() !void { return error.T; } then it once again outputs 2.

Well, what’s confuses me about this explanation is that the following code, which I think should be identical, instead outright fails to compile (even without the compileLog statement):

fn nop() error{E}!void {}

pub fn main() !void {
    comptime var i = 0;
    {
        defer {
            i += 1;
        }
        nop() catch |e| return e;
    }
    @compileLog(i); // this statement is never reached
}

It fails with:

src/main.zig:7:15: error: store to comptime variable depends on runtime condition
            i += 1;
            ~~^~~~
src/main.zig:9:15: note: runtime condition here
        nop() catch |e| return e;
        ~~~~~~^~~~~~~~~~~~~~~~~~

try shouldn’t compile either, create an issue about it on the repo, I couldn’t find any existing ones regarding this.

design flaw: inferred error sets at comptime · Issue #2736 · ziglang/zig · GitHub might be somewhat related

That is about calling a function with an inferred error set at comptime and the set being empty when it’s not supposed to be.

This is about a comptime defer statement being successfully evaluated on try but not catch |e| return e despite them being the same control flow.

Thanks. I filed the bug here: Discrepancy in handling of comptime vars in defer statements · Issue #24213 · ziglang/zig · GitHub

4 Likes

yeah somewhat related to OPs post, as in being about error sets and comptime. I couldn’t find anything else either.

I can replicate my odd behavior even without inferred error sets, i.e.:

fn nop() error{E}!void {}

pub fn main() error{E}!void {
    comptime var i = 0;
    {
        defer {
            i += 1;
        }
        nop() catch |e| return e;
    }
    @compileLog(i); // statement is never reached
}

etc.

1 Like

Placing the code from the defer block in all the exit points (the return statement and the end of block }), the code becomes:

fn nop() error{E}!void {}

pub fn main() error{E}!void {
    comptime var i = 0;
    {
        nop() catch |e| { i += 1; return e;};
        i += 1;
    }
    @compileLog(i); // outputs 2
}

Is it clear now?
The explanation, is in @vulpesx post: Comptime var behavior within defer - #3 by vulpesx

Sorry, my code comments were incorrect. I’ve edited them to fix this.

No, it’s not clear to me why my two examples (try vs catch |e| return e) compile/behave differently.