ffi issue-0046 fix: save/restore outer state in createComptimeFunction

`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.
This commit is contained in:
agra
2026-05-27 16:57:19 +03:00
parent 13efc565fa
commit 248d6e669c
2 changed files with 57 additions and 2 deletions

View File

@@ -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

View File

@@ -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