Surface rename of the signed integer family: s1..s64 become i1..i64
(u1..u64, usize, isize unchanged). 'string' keeps the s-prefix arm in
name classification; width parsing moves to the i-prefix arm next to
isize.
Internal TypeId tags follow the surface (.s8/.s16/.s32/.s64 ->
.i8/.i16/.i32/.i64), as do mono-key mangle fragments (ptr_i64,
tu_i64_bool) and all display/diagnostic formatting (i{d}).
Migrated in the same sweep: stdlib + examples + issue repros + FFI C
companions (shared symbol names like ffi_id_i64), expected
stdout/stderr/ir snapshots, specs.md, readme.md, CLAUDE.md/AGENTS.md,
implementation_plan.md, docs/, issue writeups. Vendored stb_image and
historical flow state left untouched.
zig build test: 426/426; examples suite: 595/595.
5.0 KiB
issue 0073 — closure literal inside a defer body segfaults the compiler
✅ RESOLVED (2026-06-02). Root cause:
lowerLambdanever opened its owndeferwindow. Every other function-lowering entry (lowerFunction,monomorphizeFunction,monomorphizePackFn) savesfunc_defer_base, sets it todefer_stack.items.len, and restores it — butlowerLambdadidn't, so a lambda'sreturndrained the enclosing function's defers. When the defer body itself declared the lambda, draining re-lowered the lambda, whichreturned, which drained again → infinite recursion → stack-overflow SIGSEGV. Fix:lowerLambdanow opens a fresh defer window (savefunc_defer_base+defer_stacklength, set base to the current length, restore both on exit) —src/ir/lower.zig. Regression test:examples/0310-closures-closure-literal-in-defer.sx(a closure declared + called inside adefer; verifiesbodythendefer closure: 42at scope exit). Suite 358/0.
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 :: () -> i32 {
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.