diff --git a/issues/0046-comptime-fn-nested-print-with-return.md b/issues/0046-comptime-fn-nested-print-with-return.md index c3e8397..1ee14c6 100644 --- a/issues/0046-comptime-fn-nested-print-with-return.md +++ b/issues/0046-comptime-fn-nested-print-with-return.md @@ -1,3 +1,22 @@ +**FIXED.** `createComptimeFunction` now saves/restores the +outer `lowerComptimeCall`'s state — specifically +`inline_return_target`, `pack_arg_nodes`, `pack_param_count`, +`pack_arg_types`, `comptime_param_nodes`, `block_terminated`, +`target_type`, and `func_defer_base` — so the wrapper fn it +builds for the nested comptime expression runs in isolation. +Without the saves, the wrapper inherited an inline-return slot +belonging to a different basic block; the interp executed it +and tripped a null pointer store at `storeAtRawPtr`. + +The pack-fn face of this bug (filed as face 2) was fixed +incidentally by step 2b's mono refactor — pack-fn calls +bypass the inline-return-slot setup entirely. Plain +`($x: s32)` comptime fns stay on the inline path; the +`createComptimeFunction` save/restore fix covers that path. + +Regression test: +[examples/issue-0046.sx](../examples/issue-0046.sx). + # Symptom A comptime fn body containing BOTH a nested comptime call diff --git a/src/ir/lower.zig b/src/ir/lower.zig index 4b9f8ed..a8ae85d 100644 --- a/src/ir/lower.zig +++ b/src/ir/lower.zig @@ -9548,13 +9548,49 @@ pub const Lowering = struct { const name = std.fmt.bufPrint(&buf, "{s}_{d}", .{ prefix, self.comptime_counter }) catch prefix; self.comptime_counter += 1; - // Save current builder state + // Save current builder + lowering state. The wrapper fn we're + // about to build runs the comptime expression in isolation — + // it must NOT inherit the enclosing call's `inline_return_target` + // (which would re-route a `return` inside the wrapper into a + // slot belonging to a different basic block), pack bindings + // (which would substitute caller's `args` inside the wrapper), + // or comptime-param bindings (which would substitute caller's + // `$fmt` inside the wrapper's #insert children). Without these + // saves, nested comptime calls leak outer state into the + // interp-executed wrapper, producing garbage stores (issue-0046 + // face 1 — storeAtRawPtr null). const saved_func = self.builder.func; const saved_block = self.builder.current_block; const saved_counter = self.builder.inst_counter; const saved_scope = self.scope; const saved_ctx_ref = self.current_ctx_ref; - defer self.current_ctx_ref = saved_ctx_ref; + const saved_iri = self.inline_return_target; + const saved_pan = self.pack_arg_nodes; + const saved_ppc = self.pack_param_count; + const saved_pat = self.pack_arg_types; + const saved_cpn = self.comptime_param_nodes; + const saved_block_terminated = self.block_terminated; + const saved_target_type = self.target_type; + const saved_func_defer_base = self.func_defer_base; + self.inline_return_target = null; + self.pack_arg_nodes = null; + self.pack_param_count = null; + self.pack_arg_types = null; + self.comptime_param_nodes = null; + self.block_terminated = false; + self.target_type = null; + self.func_defer_base = self.defer_stack.items.len; + defer { + self.current_ctx_ref = saved_ctx_ref; + self.inline_return_target = saved_iri; + self.pack_arg_nodes = saved_pan; + self.pack_param_count = saved_ppc; + self.pack_arg_types = saved_pat; + self.comptime_param_nodes = saved_cpn; + self.block_terminated = saved_block_terminated; + self.target_type = saved_target_type; + self.func_defer_base = saved_func_defer_base; + } // Build params: implicit `__sx_ctx` at slot 0 when the program // uses Context (so the body's `context.X` reads + transitive calls