Errors with self-hosted but not with LLVM compiler

I am doing this year’s AoC with zig (again). For the first time I have noticed errors when running my code with the self-hosted compiler, which disappear when running with -OReleaseFast (which, I understand, uses the LLVM-based compiler). Two examples so far:

thread 413132 panic: reached unreachable code
/home/gonzo/.local/share/mise/installs/zig/0.15.2/lib/std/debug.zig:559:14: 0x1044179 in assert (std.zig)
if (!ok) unreachable; // assertion failure
^
/home/gonzo/.local/share/mise/installs/zig/0.15.2/lib/std/debug.zig:1735:15: 0x10ba133 in lock (std.zig)
assert(l.state == .unlocked);
^
/home/gonzo/.local/share/mise/installs/zig/0.15.2/lib/std/hash_map.zig:1113:44: 0x1150ca5 in getOrPutContextAdapted__anon_23417 (std.zig)
self.pointer_stability.lock();
^
/home/gonzo/.local/share/mise/installs/zig/0.15.2/lib/std/hash_map.zig:1100:56: 0x114836d in getOrPutContext (std.zig)
const gop = try self.getOrPutContextAdapted(allocator, key, ctx, ctx);

and

thread 413162 panic: integer overflow
/home/gonzo/.local/share/mise/installs/zig/0.15.2/lib/std/mem.zig:4357:61: 0x114b27c in sliceAsBytes__anon_23281 (std.zig)
return @as(cast_target, @ptrCast(slice))[0 .. slice.len * @sizeOf(std.meta.Elem(Slice))];
^
/home/gonzo/.local/share/mise/installs/zig/0.15.2/lib/std/mem/Allocator.zig:351:40: 0x11574b9 in remap__anon_23771 (std.zig)
const old_memory = mem.sliceAsBytes(allocation);
^
/home/gonzo/.local/share/mise/installs/zig/0.15.2/lib/std/array_list.zig:1227:26: 0x11530d5 in ensureTotalCapacityPrecise (std.zig)
if (gpa.remap(old_memory, new_capacity)) |new_memory| {
^
/home/gonzo/.local/share/mise/installs/zig/0.15.2/lib/std/array_list.zig:1207:51: 0x114ab35 in ensureTotalCapacity (std.zig)
return self.ensureTotalCapacityPrecise(gpa, growCapacity(self.capacity, new_capacity));
^
/home/gonzo/.local/share/mise/installs/zig/0.15.2/lib/std/array_list.zig:1261:41: 0x1147cdb in addOne (std.zig)
try self.ensureTotalCapacity(gpa, newlen);
^
/home/gonzo/.local/share/mise/installs/zig/0.15.2/lib/std/array_list.zig:894:49: 0x1143dad in append (std.zig)
const new_item_ptr = try self.addOne(gpa);

Are these known regressions, or should I make an effort to open issues for them?

This is not a difference between LLVM and the self-hosted backends. Both of these errors are results of runtime safety checks, which are present in Debug and ReleaseSafe, but in ReleaseFast and ReleaseSmall they result in undefined/illegal behavior instead.

I.e. in Debug and ReleaseSafe you get these crashes, but in ReleaseFast and ReleaseSmall the compiler assumes your program is written so that the checked behavior never happens and uses that as information for optimizations.

2 Likes

I have been looking into this, and your suggestion seems correct. But now I am having all kinds of existential doubts… For the longest time, when I have needed to write a struct which contais a list, I would do something like this:

const Module = struct {
    alloc: std.mem.Allocator,
    lines: std.ArrayList(i32),

    pub fn init(alloc: std.mem.Allocator) Module {
        return .{
            .alloc = alloc,
            .lines = .empty,
        };
    }

    pub fn deinit(self: *Module) void {
        self.lines.deinit(self.alloc);
    }

    pub fn add(self: *Module, value: i32) !void {
        try self.lines.append(self.alloc, value);
    }

    // etc
};

But now I notice that this works correctly in ReleaseFast but not in Debug: the list seems to never be properly initialized; calling add does not change the list. Even more, if I create a Module and try to print the list size right away, it returns 0xaaaaaaaaaaaaaaaa – clearly undefined.

This is obviously my mistake, and I’m sure a really basic one – how should is this done correctly with zig 0.15.2?

The code you posted looks correct to me. You can always double check whether the issue also occurs with an equivalent LLVM build by passing -fllvm to the compiler.

Ok, these errors sent me on a rabbit hole, but I found the end of it. The problem was that on my main function I was creating a module like this:

var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
defer arena.deinit();
const alloc = arena.allocator();

var module = Module.init(alloc);
defer module.deinit();

But the correct way to do this is:

var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
defer arena.deinit();

var module = Module.init(arena.allocator());
defer module.deinit();

Otherwise, by creating that temporary const alloc I was losing the ability to properly call methods on the allocator though its vtable.

All is well now, my code works in both Debug and ReleaseFast modes.

I’m confused. I think these 2 versions should be equivalent. Is the argument of Module.init() not copied anyway ?

1 Like

This is not the solution to the problem. Those code snippets have identical semantics.

I suggest to keep looking for the problem. The stack trace you posted earlier should be a good hint.

Taking it at face value it looks like you’re editing a hash map while iterating it, or equivalent.