Inline causes panic to elide the wrong stack frame?

Using inline fn with a @panic inside still prints that source line, but not the callers lines. This is the opposite of what I expected.

I have some code that has a version of asserts that persist through optimized release builds (ie, they don’t rely on unreachable and they have tags to allow you to turn various assert groups off or on overriding the global NASSERT flag). Here is the untagged version:

const Asserts = struct {
    const nassert: bool = false;
    asd: bool = true,
};

fn assert(cond: bool) void {
    if (comptime !Asserts.nassert) {
        if (!cond) {
            @panic("assertion failed");
        }
    }
}

const pp = std.debug.print;
pub fn main() void {
    pp("{?}\n", .{assert(false)});
}

which prints this on failure:

 zig run assert.zig 
thread 40098 panic: assertion failed
/home/nyc/devel/nassert/src/assert.zig:24:13: 0x1035ded in assert (assert)
            @panic("assertion failed");
            ^
/home/nyc/devel/nassert/src/assert.zig:47:25: 0x1033d5a in main (assert)
    pp("{?}\n", .{assert(false)});
                        ^
/usr/lib/zig/std/start.zig:501:22: 0x1033609 in posixCallMainAndExit (assert)
            root.main();
                     ^
/usr/lib/zig/std/start.zig:253:5: 0x1033171 in _start (assert)
    asm volatile (switch (native_arch) {
    ^
???:?:?: 0x0 in ??? (???)
Aborted (core dumped)

The top frame is the assert function, and the second frame is the main, just as you would expect. I wanted to see if inline would elide the top frame and just print the main frame since it would be inlined into the main body. So I changed assert to inline fn assert(bool) void and then I get this error trace:

 zig run assert.zig 
thread 40638 panic: assertion failed
/home/nyc/devel/nassert/src/assert.zig:24:13: 0x1033bf8 in main (assert)
            @panic("assertion failed");
            ^
/usr/lib/zig/std/start.zig:501:22: 0x1033489 in posixCallMainAndExit (assert)
            root.main();
                     ^
/usr/lib/zig/std/start.zig:253:5: 0x1032ff1 in _start (assert)
    asm volatile (switch (native_arch) {
    ^
???:?:?: 0x0 in ??? (???)
Aborted (core dumped)

line 24 is inside the assert function, but the reference to the call in main is missing. This is all in Debug compilation.

Why is main no longer in the panic error trace? That’s the one I actually wanted to be printed.

If you mean the assert(false) with “the call in main” then this is completely intended.
Zig removes the assert because when Zig finds a code block marked with inline, it will execute that code block at compile-time. This execution will remove the assert function and embed that code directly in the main function. So Zig should make something like this:

const std = @import("std");

pub fn main() void {
    std.debug.print("{?}\n", .{if (!false) {
        @panic("assertion failed");
    }});
}

And this will be processed further. But you can see that there is no assert function, so no stack frame for that.

1 Like

I’m not understanding you. So inline causes comptime evaluation? It is considered a comptime context as if I wrapped the body of the function in a comptime block?

Why is there there no main referenced in the panic message? It jumps from the start entry point to the assert function that is marked inline?

I no saying there is no call info for the assert function – that is totally what I expected. There is no call info for main!

I would consider this a bug or at least very unhellpful for debugging – either both main and assert should have their call information printed, or just main should have its call information in the error trace. Having only assert skipping main, and going back to start is the least intuitive. It is also the least helpful because you have idea where to look for the panic problem now since that info is completely gone.

I think this is happening. The source position is annotated for the inline so you know here to look in the source code, but that annotation is then brought into the main call when it inlines the assert call. But it overwrite the current source position of where it is in main.

It needs to keep a stack of posiiton info for when it does these inlines. I’m guessing if had an inline function called inside assert, that would disappear too.

To the first paragraph: Yes.

If you want the assert call to be printed, do not use inlines, use normal functions.

No I don’t want it to be printed. I would rather have the main call printed without the assert.

So what do you want exactly? What do you mean with “the main call”?

The stack info for assert() isn’t very useful. It is the main() stack info that helps to find bugs. I was hoping when when I inlined the call to assert() that its info wouldnt’ show up. Instead it still shows up and the main() stack info is missing – that is totally unintuitive to me, but I can see how there might be a bug there if the current source position gets overwritten by the inlined assert() call.

There are a couple ways for panic() to print the error trace when assert() is given the inline decoration: print both assert() and main() or just print main(). Just printing assert() seems to be the worse of all words and least intuitive.

Now I understand what you mean. With “the assert call” I meant the main stack frame that actually calls assert.
Then you could write a custom panic function. Andrew Kelley once described this (for embedded systems, but anyways very interesting): https://andrewkelley.me/post/zig-stack-traces-kernel-panic-bare-bones-os.html.

1 Like

The panic happened inside the function main, as you intended. The line number points to where the assert is written, line 24, but it doesn’t reflect which stack frame is currently on top.

1 Like

The weirdness is that inline on assert() removes the main() stack frame. Why is that?

Very interesting. Thank you. But I just read through the panic handling code, and the @errorStackTrace() call is what generates it, the rest of the code just walks down it. So I think the issue probably lies in the error trace generation. I don’t have a copy of zig locally, and don’t have time for it this week, but I’ll try to get one installed and see what I can see maybe week after next.

Thanks, but interesting panic handler information.

This is not a Zig bug.
When you inline the assert function, then “the main frame” is not dropped. Instead, the @panic is basically embedded into the main function but is still at the old source location. Small example (from original representation to representation after compile-time code execution):

Original representation:

inline fn assert(ok: bool) void {
    if (!ok) @panic("assertion failed");
}

pub fn main() void {
    assert(false);
}

Representation after compile-time code execution:

pub fn main() void {
    if (!false) {
        @panic("assertion failed");
    }
}

Note that the source location of the @panic is still the same before and after the compile-time code execution.
The result is that you will get your default panic error trace with @panic being at your “line 24” but with that being the main frame.

My suggested solution was to keep the assert without inline and to write an own panic handler that generates the same error trace but starts one frame after the assert frame. This way, the @panics (assert frames) aren’t printed out, but the calls to assert (main frames) are printed out.

that’s highly unintuitive. I don’t see any code that skips over printing the main frame. I’ll write some code to see if it is in the @errorsacktrace at all.

While intentional behavior, I wouldn’t call it correct. I’m still not understanding why it wouldn’t print main. Yes the panic is now in the main() call, but why would it stop printing that stack info? It makes debugging so much worse.

The assert is inlined at compile-time, the panic is executed at run-time.
How could @panic know that there is some function that maybe has been inlined? The default panic just walks over the error trace. The error trace is just the call stack with some additional information.
The call stack, however, is made at run-time (and not at compile-time). The @errorStackTrace just finds out the correct source locations for each call stack frame (using DWARF), and the default panic function gives us the neat output.
Even in your original post we can see in the second trace, as Lucas Santos already pointed out, that the @panic is in the main frame and so the main frame is being printed out.

Your original problem was that you wanted directly the main frame and you wanted to skip the assert frame. So why can’t you just keep the assert without inline and then write an own panic handler?
Using an own panic handler, you could test if your current frame is an assert frame and then just skip it and continue with the main frame.

1 Like

I totally undestand. I totally get why. It keeps the source annotation from the assert call. It is not that I don’t undetsand. I just think it is wrong. An inlined piece of code has more than one source location essentially, and the compiler doens’t keep track of that.

In the event of inlined calls, source locations are no longer singular, but a stack of the inlined calls.

I completely see it in my head. But it is still unintuitive and the answer to “what line causes the panic” is wrong because it ignores the calling frame. The compiler isnt keeping track of source locations writh enough datail to aid in debugging.

You can tell me how it is implemented all you want, but undeniably and without question, not showing both frames makes debugging a much worse experience. The compiler should put both frames (in should have the info when it generates the error trace). Printing the main() and then print “inlined from” and the assert() frame would be the correct way to do it.

Scala, java, gcc, etc… they all can report that.