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.
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 buildSIGSEGVs inlowerLambda(src/ir/lower.zig) while lowering the enclosing function. With a failable closure (() -> !E { ... }) the crash surfaces one frame out, inlowerCall→expandCallDefaults→scope.lookupFn→hash_map.get(a corrupted/garbage scope pointer), suggesting a stale/clobberedself.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.zig — lowerLambda (~: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:
- with a scope pointer that has since been popped/freed (use-after-free →
garbage
fn_names/mapinlookupFn), or - in a builder state where
lowerLambda's assumptions (current function, insert block) don't hold, or - re-entrantly / unbounded (the failable-variant trace looked like deep
recursion through
expandCallDefaults→lowerCall→lowerLambda).
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.