From 248d6e669c669bb16fdc4969362c634b3e0aa126 Mon Sep 17 00:00:00 2001 From: agra Date: Wed, 27 May 2026 16:57:19 +0300 Subject: [PATCH] ffi issue-0046 fix: save/restore outer state in createComptimeFunction MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `createComptimeFunction` wraps a comptime expression into a fresh fn that the interp executes in isolation. The wrapper must not inherit the enclosing call's lowering state — any leaked slot, binding, or scope flag corrupts the wrapper's own lowering. Pre-fix, only `func` / `current_block` / `inst_counter` / `scope` / `current_ctx_ref` were saved. Specifically NOT saved: - `inline_return_target` — set by `lowerComptimeCall` for an outer comptime body with `return X;`. The wrapper's body was lowering through this slot, routing the wrapper's `ret` into a basic block from a different function. - `pack_arg_nodes`, `pack_param_count`, `pack_arg_types` — active during a pack-fn mono's body lowering. (Pack-fn face of 0046 was already fixed by step 2b moving pack-fn calls off the inline path; these saves close a latent cross-contamination if any future pack-mono body invokes the comptime interp.) - `comptime_param_nodes` — active during an outer `lowerComptimeCall` to bind `$fmt`-style substitutions. - `block_terminated`, `target_type`, `func_defer_base` — fn- local flags that the wrapper's lowering needs fresh. All eight now save/restore in `createComptimeFunction`. The wrapper runs in a clean state. `examples/issue-0046.sx` flips from the non-deterministic interp panic to "inside\n" + "n=42\n". 204/204 example tests + `zig build test` green. Issue file marked FIXED with a pointer to the regression test. --- ...46-comptime-fn-nested-print-with-return.md | 19 +++++++++ src/ir/lower.zig | 40 ++++++++++++++++++- 2 files changed, 57 insertions(+), 2 deletions(-) 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