Files
sx/issues/0073-closure-literal-in-defer-segfault.md
agra eb20b2ddb5 docs(issues): file 0073 — closure literal inside a defer body segfaults lowering
Minimal repro (issues/0073-...sx): a non-failable, uncalled closure literal
declared inside a `defer` body crashes the compiler with a SIGSEGV in
lowerLambda (src/ir/lower.zig). Isolation shows the trigger is "a closure
literal lowered inside a defer body" — not failability, not whether it's called
(closures and failable closures lower fine outside a defer). Pre-existing
lowering bug, unrelated to the A5 error-analysis extraction; surfaced while
writing an A5.2 cleanup-absorption test example.

Filed per the IMPASSIBLE RULE: work paused pending a fix in another session.
2026-06-02 23:20:19 +03:00

4.1 KiB

issue 0073 — closure literal inside a defer body segfaults the compiler

Symptom

One-line: declaring a closure literal inside a defer body crashes the compiler with a segfault during lowering.

  • Observed: sx run / sx build SIGSEGVs in lowerLambda (src/ir/lower.zig) while lowering the enclosing function. With a failable closure (() -> !E { ... }) the crash surfaces one frame out, in lowerCallexpandCallDefaultsscope.lookupFnhash_map.get (a corrupted/garbage scope pointer), suggesting a stale/clobbered self.scope (or builder/current-function state) while the deferred body is lowered.
  • Expected: the program either lowers normally or produces a clean diagnostic. A compiler segfault is never acceptable, regardless of whether the shape is intended to be supported.

Isolation (all on arch-refactor, current HEAD):

Probe Shape Result
(a) failable closure declared + cb() catch e {}no defer OK (exit 0)
(b) failable closure literal inside a defer body SIGSEGV (lowerCall/expandCallDefaults)
(c) non-failable () { return; } inside a defer body SIGSEGV (lowerLambda)
(d) failable closure literal inside a plain { … } block (not defer) OK (exit 0)

So the trigger is a closure literal lowered inside a defer body — not failability, not whether the closure is called. (a)/(d) prove closures and failable closures lower fine outside a defer; (c) proves a bare non-failable closure in a defer is enough to crash.

Reproduction

issues/0073-closure-literal-in-defer-segfault.sx (minimal — non-failable, uncalled closure in a defer):

#import "modules/std.sx";

work :: () {
    defer { cb := () { return; }; }
}

main :: () -> s32 {
    work();
    return 0;
}

Run: ./zig-out/bin/sx run issues/0073-closure-literal-in-defer-segfault.sx → "Segmentation fault" with a stack through lowerLambda (src/ir/lower.zig).

Investigation prompt

A defer body is captured at its declaration site and lowered into the function's exit/cleanup path (drained by lowerReturn / block-exit), not inline. Lowering a lambda literal while emitting a deferred body appears to run with invalid lowering state — most likely self.scope (note probe (b)'s crash is in scope.lookupFn reading a hash map at a garbage address) and/or the builder's current-function / block context is stale or not the one lowerLambda expects when it allocates the closure's trampoline + env.

Suspected area: src/ir/lower.ziglowerLambda (~:8145) and the defer capture/replay path (defer_stmt handling + the cleanup-drain in lowerReturn / block exit; grep defer_stack / func_defer_base). Check whether deferred bodies are lowered:

  1. with a scope pointer that has since been popped/freed (use-after-free → garbage fn_names/map in lookupFn), or
  2. in a builder state where lowerLambda's assumptions (current function, insert block) don't hold, or
  3. re-entrantly / unbounded (the failable-variant trace looked like deep recursion through expandCallDefaultslowerCalllowerLambda).

Likely fix shape: lower deferred bodies under a valid, live scope (re-establish or retain the declaring scope when replaying the defer body), or defer the lambda's trampoline emission to a context that has the right function/block.

Verification step: run the repro above — expect it to lower cleanly (or emit a clean diagnostic) and NOT segfault. Then confirm a closure that is actually used inside the defer (defer { cb := () { ... }; cb(); }) also works, and re-run bash tests/run_examples.sh (357/0) to confirm no regression.

Provenance

Found while writing an A5.2 (architecture stream) test-first scaffolding example for the ERR E1.7 "cleanup-absorption stops at nested closures" behavior — the closure-boundary probe (a closure literal inside a defer) crashed the compiler instead of exercising the diagnostic. The crash is a pre-existing lowering bug, unrelated to the A5 error-analysis extraction; surfaced by the probe.