Stream: compiler development

Topic: zig-compiler: double free in Release builds


view this post on Zulip Brendan Hansknecht (Oct 16 2025 at 03:49):

This might be hard to debug. I have a sample roc file here: https://github.com/bhansconnect/chip8_op_test/blob/main/chip8_test_rom.roc

with a debug build of the compiler roc check works just fine. I have been running with --verbose --no-cache as well.

With a release build of the compiler, it segfaults due to a double free.

If i use a debug allocator in the release build to hopefully get a stack trace, but it just hangs.


I tried to debug this a little bit the other day.
This is where it hangs:
Captura de pantalla 2025-10-15 a la(s) 8.33.08 p.m..png

Which is the same location the double free happens:

lldb backtrace

I'm not sure what is going on, but if I add prints to ModuleEnv.deinit, I can see that it is called twice in release builds, but only once in debug builds.

release free locations

debug frees

Anyone have thoughts?

view this post on Zulip Luke Boswell (Oct 16 2025 at 03:53):

I'm really not sure

view this post on Zulip Brendan Hansknecht (Oct 16 2025 at 03:55):

Also, of note, the error specifically comes from freeing part of the type checking state I think. The DescStore in the TypeStore. So it could also be related to something type checking specfic and not moduleenv technically.

view this post on Zulip Luke Boswell (Oct 16 2025 at 03:56):

It's the debug vs release thing that has me a little stumped

view this post on Zulip Brendan Hansknecht (Oct 16 2025 at 03:56):

So I see two confusions:

  1. Why doesn't debug free things here? What is the delta?
  2. It isn't the entire moduleenv being double freed. just the DescStore. What is special about it?

view this post on Zulip Brendan Hansknecht (Oct 21 2025 at 14:48):

@Anton you should test gpt 5 codex high on this one

view this post on Zulip Anton (Oct 21 2025 at 15:50):

I will :)
On what OS did you hit the segfault, macOS?

view this post on Zulip Anton (Oct 21 2025 at 16:11):

Ok, I was able to reproduce it on macOS

view this post on Zulip Anton (Oct 21 2025 at 17:40):

Success :)

❯ git diff
diff --git a/src/eval/interpreter.zig b/src/eval/interpreter.zig
index a6e61957bb..42254a1dcf 100644
--- a/src/eval/interpreter.zig
+++ b/src/eval/interpreter.zig
@@ -92,6 +92,11 @@ pub const Interpreter = struct {
         }
     };
     const Binding = struct { pattern_idx: can.CIR.Pattern.Idx, value: StackValue };
+    const DefInProgress = struct {
+        pattern_idx: can.CIR.Pattern.Idx,
+        expr_idx: can.CIR.Expr.Idx,
+        value: ?StackValue,
+    };
     allocator: std.mem.Allocator,
     runtime_types: *types.store.Store,
     runtime_layout_store: layout.Store,
@@ -127,6 +132,7 @@ pub const Interpreter = struct {
     builtins: BuiltinTypes,
     /// Map from module name to ModuleEnv for resolving e_lookup_external expressions
     imported_modules: std.StringHashMap(*const can.ModuleEnv),
+    def_stack: std.array_list.Managed(DefInProgress),

     pub fn init(allocator: std.mem.Allocator, env: *can.ModuleEnv, builtin_types: BuiltinTypes, imported_modules_map: ?*const std.AutoHashMap(base_pkg.Ident.Idx, can.Can.AutoImportedType)) !Interpreter {
         // Convert imported modules map to other_envs slice
@@ -184,6 +190,7 @@ pub const Interpreter = struct {
             .scratch_tags = try std.array_list.Managed(types.Tag).initCapacity(allocator, 8),
             .builtins = builtin_types,
             .imported_modules = std.StringHashMap(*const can.ModuleEnv).init(allocator),
+            .def_stack = try std.array_list.Managed(DefInProgress).initCapacity(allocator, 4),
         };
         result.runtime_layout_store = try layout.Store.init(env, result.runtime_types);

@@ -195,6 +202,14 @@ pub const Interpreter = struct {
         return try self.evalExprMinimal(expr_idx, roc_ops, null);
     }

+    fn registerDefValue(self: *Interpreter, expr_idx: can.CIR.Expr.Idx, value: StackValue) void {
+        if (self.def_stack.items.len == 0) return;
+        var top = &self.def_stack.items[self.def_stack.items.len - 1];
+        if (top.expr_idx == expr_idx and top.value == null) {
+            top.value = value;
+        }
+    }
+
     pub fn startTrace(self: *Interpreter) void {
         _ = self;
     }
@@ -1236,6 +1251,7 @@ pub const Interpreter = struct {
                 // Expect a closure layout from type-to-layout translation
                 if (closure_layout.tag != .closure) return error.NotImplemented;
                 const value = try self.pushRaw(closure_layout, 0);
+                self.registerDefValue(expr_idx, value);
                 // Initialize the closure header
                 if (value.ptr) |ptr| {
                     const header: *layout.Closure = @ptrCast(@alignCast(ptr));
@@ -1298,7 +1314,24 @@ pub const Interpreter = struct {
                         for (all_defs) |def_idx| {
                             const def = self_interp.env.store.getDef(def_idx);
                             if (def.pattern == cap.pattern_idx) {
+                                var k: usize = self_interp.def_stack.items.len;
+                                while (k > 0) {
+                                    k -= 1;
+                                    const entry = self_interp.def_stack.items[k];
+                                    if (entry.pattern_idx == cap.pattern_idx) {
+                                        if (entry.value) |val| {
+                                            return val;
+                                        }
+                                    }
+                                }
                                 // Found the def! Evaluate it to get the captured value
+                                const new_entry = DefInProgress{
+                                    .pattern_idx = def.pattern,
+                                    .expr_idx = def.expr,
+                                    .value = null,
+                                };
+                                self_interp.def_stack.append(new_entry) catch return null;
+                                defer _ = self_interp.def_stack.pop();
                                 return self_interp.evalMinimal(def.expr, ops) catch null;
                             }
                         }
@@ -1309,14 +1342,16 @@ pub const Interpreter = struct {
                 for (caps, 0..) |cap_idx, i| {
                     const cap = self.env.store.getCapture(cap_idx);
                     field_names[i] = cap.name;
-                    const captured_val = resolveCapture(self, cap, roc_ops) orelse return error.NotImplemented;
-                    field_layouts[i] = captured_val.layout;
+                    const cap_ct_var = can.ModuleEnv.varFrom(cap.pattern_idx);
+                    const cap_rt_var = try self.translateTypeVar(self.env, cap_ct_var);
+                    field_layouts[i] = try self.getRuntimeLayout(cap_rt_var);
                 }

                 const captures_layout_idx = try self.runtime_layout_store.putRecord(field_layouts, field_names);
                 const captures_layout = self.runtime_layout_store.getLayout(captures_layout_idx);
                 const closure_layout = Layout.closure(captures_layout_idx);
                 const value = try self.pushRaw(closure_layout, 0);
+                self.registerDefValue(expr_idx, value);

                 // Initialize header
                 if (value.ptr) |ptr| {
@@ -3321,6 +3356,7 @@ pub const Interpreter = struct {
         self.stack_memory.deinit();
         self.bindings.deinit();
         self.active_closures.deinit();
+        self.def_stack.deinit();
         self.scratch_tags.deinit();
         self.imported_modules.deinit();
     }

view this post on Zulip Anton (Oct 21 2025 at 17:42):

Can you confirm that this is a sensible fix @Richard Feldman?

view this post on Zulip Anton (Oct 21 2025 at 17:48):

I put it up in https://github.com/roc-lang/roc/pull/8315

view this post on Zulip Brendan Hansknecht (Oct 21 2025 at 19:09):

A fix for a roc check fail is in the interpreter? I didn't realize roc check hit the interpreter. Interesting.

view this post on Zulip Brendan Hansknecht (Oct 21 2025 at 19:13):

Just ran the fix to double check. It's fixed!

Color me impressed.

view this post on Zulip Richard Feldman (Oct 21 2025 at 19:54):

yeah the interpreter runs to do compile-time evaluation of constants

view this post on Zulip Richard Feldman (Oct 21 2025 at 19:55):

that exists already! :smiley:

view this post on Zulip Richard Feldman (Oct 21 2025 at 19:55):

crashes and dbgs and failed expects all get reported at build time as part of roc check's normal output

view this post on Zulip Brendan Hansknecht (Oct 21 2025 at 20:26):

This was broken before compile time evaluation of constants existed (assuming that just got added with the PR shared the other day). I wonder if compile time evaluation of constants shifted the issue.

Either way, awesome it is fixed


Last updated: Nov 08 2025 at 12:13 UTC