# 0050 — `monomorphizeFunction` leaks outer pack-fn state into the mono body ## Symptom A generic function (one with `$T: Type` type params) called from inside a pack-fn's monomorphisation inherits the outer pack maps during its OWN body lowering. The familiar fall-out: every identifier in the generic body whose name happens to match the outer pack's name gets routed through `lowerFieldAccess`'s `.len` intercept (or `pack_arg_types` / `pack_arg_nodes` substitutions) and silently picks up the WRONG value. Same root cause as issue-0048, just a different lowering path. 0048 fixed `lazyLowerFunction`. The generic-monomorphisation path (`monomorphizeFunction` in `src/ir/lower.zig` around line 8718) has the same omission: it saves and nulls `type_bindings` / `scope` / builder state, but does NOT save/null `pack_arg_nodes` / `pack_param_count` / `pack_arg_types` / `inline_return_target`. When the body of the mono'd function references an identifier named after the outer pack (typical: `args`), the outer `pack_param_count["args"]` entry is still present and the `args.len` lowers to a baked-in constant — the pack arity of whichever shape triggered the FIRST mono. Subsequent shapes call the same cached mono and read the same wrong constant. ## Reproduction ```sx #import "modules/std.sx"; build :: (args: []Type, $ret: Type) -> string { return concat("len=", int_to_string(args.len)); } probe :: (..$args) -> string { return build($args, void); } #run print("0: {}\n", probe()); #run print("1: {}\n", probe(true)); #run print("2: {}\n", probe(42, "hi")); main :: () {} ``` Observed: ``` 0: len=0 1: len=0 2: len=0 ``` Expected: `len=0`, `len=1`, `len=2`. The slice passing across the function-call boundary already works post-0048; what's wrong here is that the callee's `args.len` is being constant-folded at lower time before the runtime slice ever gets a chance to be read. Negative control — same shape WITHOUT the `$ret: Type` type-param (so `build` is not generic, gets lowered via `lazyLowerFunction` + the 0048-protected isolation): ```sx build :: (args: []Type) -> string { ... } // no $ret probe :: (..$args) -> string { return build($args); } #run print("0: {}\n", probe()); // → 0 #run print("1: {}\n", probe(true)); // → 1 #run print("2: {}\n", probe(42, "hi")); // → 2 ``` This works correctly, which confirms the bug is specifically on the generic-mono path. The IR for the failing case shows `int_to_string(i64 0)` baked into `@build__void` — the args.len fold landed at monomorphisation time, not at the call site. Suite green at commit `952dc0e` (master, 2026-05-27); the bug surfaces the moment a builder fn (FFI plan's `build_block_convert(args: []Type, $ret: Type) -> string`) is written using the new bare-`$args` + `$ret: Type` shape that step 5 of the FFI plan calls for. ## Investigation prompt Apply the same isolation pattern that landed for `lazyLowerFunction` (commit `0ede097`, issue-0048): `monomorphizeFunction` in `src/ir/lower.zig` (~line 8718) needs a save+null+defer-restore block covering: - `self.pack_arg_nodes` - `self.pack_param_count` - `self.pack_arg_types` - `self.inline_return_target` Place the block alongside the existing `saved_func` / `saved_block` / `saved_scope` / `saved_bindings` saves, before the body lowering begins. Mirror the defer pattern from `lazyLowerFunction` so all early-return paths restore correctly. Verification: Run the reproduction above; the three probes must print 0 / 1 / 2 respectively. Then run `bash tests/run_examples.sh` (must stay 214/214) and `zig build test`. A regression test goes in `examples/NNN-generic-fn-pack-state-leak.sx` once the fix lands. The shape is exactly the reproduction above. ## Why this matters Step 5 of the FFI plan (`current/CHECKPOINT-FFI.md` — generic `Into(Block) for Closure(..$args) -> $R` impl in stdlib) calls a builder `build_block_convert(args: []Type, $ret: Type) -> string` from inside the impl body. The impl body itself is mono'd per call shape (where the closure's pack types and return type are substituted in), and the builder is called from inside that mono. With this leak, every call shape sees the same wrong source string from the builder — the trampoline emission silently produces zero-arity blocks regardless of the actual closure. Without the fix, step 5 can't proceed.