From 08f263c6e4e4faf6caee17db2d06a245cc24e06a Mon Sep 17 00:00:00 2001 From: agra Date: Tue, 2 Jun 2026 23:29:49 +0300 Subject: [PATCH] fix(ir): open a fresh defer window when lowering a lambda body (issue 0073) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A closure literal declared inside a `defer` body segfaulted the compiler. Root cause: lowerLambda never opened its own `func_defer_base` window. Every other function-lowering entry (lowerFunction / monomorphizeFunction / monomorphizePackFn) saves func_defer_base, sets it to defer_stack.items.len, and restores it — lowerLambda didn't. So a lambda's `return` drained the ENCLOSING function's defers; when the defer body itself declared the lambda, draining re-lowered the lambda, which returned, which drained again → infinite recursion → stack-overflow SIGSEGV (the failable variant surfaced one frame out, in expandCallDefaults→lookupFn reading a clobbered scope). Fix: lowerLambda now saves func_defer_base + the defer_stack length, sets the base to the current length (a fresh window), and restores both on exit — so a lambda's `return` drains only its own defers. Regression: examples/0310-closures-closure-literal-in-defer.sx — a closure declared and called inside a `defer`; verifies `body` then `defer closure: 42` at scope exit (exit 0). Issue 0073 marked RESOLVED; repro promoted from issues/0073-*.sx. zig build, zig build test, tests/run_examples.sh (358/0) all green. --- .../0310-closures-closure-literal-in-defer.sx | 22 +++++++++++++++++++ ...310-closures-closure-literal-in-defer.exit | 1 + ...0-closures-closure-literal-in-defer.stderr | 1 + ...0-closures-closure-literal-in-defer.stdout | 2 ++ .../0073-closure-literal-in-defer-segfault.md | 13 +++++++++++ .../0073-closure-literal-in-defer-segfault.sx | 17 -------------- src/ir/lower.zig | 13 +++++++++++ 7 files changed, 52 insertions(+), 17 deletions(-) create mode 100644 examples/0310-closures-closure-literal-in-defer.sx create mode 100644 examples/expected/0310-closures-closure-literal-in-defer.exit create mode 100644 examples/expected/0310-closures-closure-literal-in-defer.stderr create mode 100644 examples/expected/0310-closures-closure-literal-in-defer.stdout delete mode 100644 issues/0073-closure-literal-in-defer-segfault.sx diff --git a/examples/0310-closures-closure-literal-in-defer.sx b/examples/0310-closures-closure-literal-in-defer.sx new file mode 100644 index 0000000..ac8f5f3 --- /dev/null +++ b/examples/0310-closures-closure-literal-in-defer.sx @@ -0,0 +1,22 @@ +// Closure literal declared (and used) inside a `defer` body. +// +// Regression (issue 0073): this used to segfault lowering. A lambda inherited +// the enclosing function's `func_defer_base`, so the lambda's `return` re-drained +// the enclosing function's defers — and when the defer body itself declared the +// lambda, that re-lowered the lambda forever (infinite recursion). A lambda now +// opens its own defer window (like every other function-lowering entry). + +#import "modules/std.sx"; + +run :: () { + defer { + cb := (n: s32) -> s32 { return n * 2; }; + print("defer closure: {}\n", cb(21)); // 42, at scope exit + } + print("body\n"); +} + +main :: () -> s32 { + run(); + return 0; +} diff --git a/examples/expected/0310-closures-closure-literal-in-defer.exit b/examples/expected/0310-closures-closure-literal-in-defer.exit new file mode 100644 index 0000000..573541a --- /dev/null +++ b/examples/expected/0310-closures-closure-literal-in-defer.exit @@ -0,0 +1 @@ +0 diff --git a/examples/expected/0310-closures-closure-literal-in-defer.stderr b/examples/expected/0310-closures-closure-literal-in-defer.stderr new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/examples/expected/0310-closures-closure-literal-in-defer.stderr @@ -0,0 +1 @@ + diff --git a/examples/expected/0310-closures-closure-literal-in-defer.stdout b/examples/expected/0310-closures-closure-literal-in-defer.stdout new file mode 100644 index 0000000..75ad37c --- /dev/null +++ b/examples/expected/0310-closures-closure-literal-in-defer.stdout @@ -0,0 +1,2 @@ +body +defer closure: 42 diff --git a/issues/0073-closure-literal-in-defer-segfault.md b/issues/0073-closure-literal-in-defer-segfault.md index acf5abd..33ee4ee 100644 --- a/issues/0073-closure-literal-in-defer-segfault.md +++ b/issues/0073-closure-literal-in-defer-segfault.md @@ -1,5 +1,18 @@ # issue 0073 — closure literal inside a `defer` body segfaults the compiler +> **✅ RESOLVED (2026-06-02).** Root cause: `lowerLambda` never opened its own +> `defer` window. Every other function-lowering entry (`lowerFunction`, +> `monomorphizeFunction`, `monomorphizePackFn`) saves `func_defer_base`, sets it +> to `defer_stack.items.len`, and restores it — but `lowerLambda` didn't, so a +> lambda's `return` drained the *enclosing* function's defers. When the defer +> body itself declared the lambda, draining re-lowered the lambda, which `return`ed, +> which drained again → infinite recursion → stack-overflow SIGSEGV. +> Fix: `lowerLambda` now opens a fresh defer window (save `func_defer_base` + +> `defer_stack` length, 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 a `defer`; verifies `body` then +> `defer closure: 42` at scope exit). Suite 358/0. + ## Symptom One-line: declaring a **closure literal inside a `defer` body** crashes the diff --git a/issues/0073-closure-literal-in-defer-segfault.sx b/issues/0073-closure-literal-in-defer-segfault.sx deleted file mode 100644 index efac4fb..0000000 --- a/issues/0073-closure-literal-in-defer-segfault.sx +++ /dev/null @@ -1,17 +0,0 @@ -// Repro for issue 0073 — a closure literal declared inside a `defer` body -// segfaults the compiler during lowering. Minimal: the closure is non-failable, -// unused, and never called; merely declaring it in the defer body crashes. -// -// Expected: either lowers fine, or a clean diagnostic — NEVER a segfault. -// Observed: SIGSEGV in `lowerLambda` (src/ir/lower.zig) during `work`'s lowering. - -#import "modules/std.sx"; - -work :: () { - defer { cb := () { return; }; } -} - -main :: () -> s32 { - work(); - return 0; -} diff --git a/src/ir/lower.zig b/src/ir/lower.zig index 316146b..5c58fb5 100644 --- a/src/ir/lower.zig +++ b/src/ir/lower.zig @@ -8313,6 +8313,19 @@ pub const Lowering = struct { defer self.current_ctx_ref = saved_ctx_ref_lam; if (lambda_wants_ctx) self.current_ctx_ref = Ref.fromIndex(0); + // A lambda is its own function: its `return` must drain only ITS OWN + // `defer`s, not the enclosing function's. Open a fresh defer window + // (like `lowerFunction`/`monomorphizeFunction`) and restore on exit — + // otherwise lowering a closure literal inside a `defer` body re-enters + // the enclosing function's defer drain (infinite recursion — issue 0073). + const saved_func_defer_base = self.func_defer_base; + const saved_defer_len = self.defer_stack.items.len; + defer { + self.func_defer_base = saved_func_defer_base; + self.defer_stack.shrinkRetainingCapacity(saved_defer_len); + } + self.func_defer_base = saved_defer_len; + // Create entry block const entry_name = self.module.types.internString("entry"); const entry = self.builder.appendBlock(entry_name, &.{});