diff --git a/examples/closures/0313-closure-inferred-return-early.sx b/examples/closures/0313-closure-inferred-return-early.sx new file mode 100644 index 00000000..6b66745c --- /dev/null +++ b/examples/closures/0313-closure-inferred-return-early.sx @@ -0,0 +1,28 @@ +// A block-body closure (`closure((params) { ... })`) with an INFERRED return +// type whose value is produced via early `return`s must infer the return type +// from the `return` operands — not fall through to void. +// +// Regression (issue 0187): `closure(() { if c { return 11; } return 22; })` +// used to infer `void` (the block's last stmt is the `return`, not a value), so +// the call site fed an `i64 undef` to `print` and LLVM verification failed. The +// closure return-type inference now mirrors the function-decl path, scanning +// the body's `return` statements. +// +// Syntax note: a block body is the `closure((params) -> R? { ... })` form; the +// bare `(params) => expr` lambda is arrow + EXPRESSION only (no block). +#import "modules/std.sx"; + +main :: () { + // early returns only — inferred return, no optionals + f := closure(() { if 1 > 0 { return 11; } return 22; }); + print("f: {}\n", f()); + + // early return + trailing-expression block value + g := closure((n: i64) { if n > 0 { return 100; } 200 }); + print("g+: {}\n", g(1)); + print("g-: {}\n", g(-1)); + + // inferred float return via early returns + h := closure((n: i64) { if n > 0 { return 3.5; } return 1.5; }); + print("h: {}\n", h(1)); +} diff --git a/examples/closures/expected/0313-closure-inferred-return-early.exit b/examples/closures/expected/0313-closure-inferred-return-early.exit new file mode 100644 index 00000000..573541ac --- /dev/null +++ b/examples/closures/expected/0313-closure-inferred-return-early.exit @@ -0,0 +1 @@ +0 diff --git a/examples/closures/expected/0313-closure-inferred-return-early.stderr b/examples/closures/expected/0313-closure-inferred-return-early.stderr new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/examples/closures/expected/0313-closure-inferred-return-early.stderr @@ -0,0 +1 @@ + diff --git a/examples/closures/expected/0313-closure-inferred-return-early.stdout b/examples/closures/expected/0313-closure-inferred-return-early.stdout new file mode 100644 index 00000000..4659b969 --- /dev/null +++ b/examples/closures/expected/0313-closure-inferred-return-early.stdout @@ -0,0 +1,4 @@ +f: 11 +g+: 100 +g-: 200 +h: 3.500000 diff --git a/issues/0187-lambda-inferred-return-block-early-return.md b/issues/0187-lambda-inferred-return-block-early-return.md index c9ff2e55..c2769107 100644 --- a/issues/0187-lambda-inferred-return-block-early-return.md +++ b/issues/0187-lambda-inferred-return-block-early-return.md @@ -1,3 +1,30 @@ +> **RESOLVED.** Root cause: the lambda return-type inference in `lowerLambda` +> (`src/ir/lower/closure.zig`) always used `inferExprType(lam.body)`, which for a +> block body returns the *last statement's* value type — and a block whose value +> comes only from early `return`s ends in a `return` statement (typed +> void/noreturn), so the closure was built with a void return while the body +> returned `i64`. Fix: distinguish the two body forms exactly as a named fn does +> (`resolveReturnType` in `src/ir/lower.zig`) — +> - **arrow** `(params) => expr` → `inferExprType(expr)` (the expression IS the value); +> - **block** `(params) { stmts }` → first explicit `return ` type via +> `findReturnValueType`, else **void** (the block's tail is a discarded +> statement, not an implicit return — only an explicit `-> R` makes the tail +> the value). +> +> This also subsumes the block-tail-references-a-local case (a `closure(() { x +> := 5; x * 2 })` with no `-> R` is now correctly **void**, not an `.unresolved` +> type reaching LLVM and panicking). Regression test: +> `examples/closures/0313-closure-inferred-return-early.sx`. +> +> Syntax note: the original repro above used the malformed `() => { ... }` +> (arrow + block) form, which the parser currently accepts but the spec does +> not define — a block body is the `closure((params) -> R? { ... })` form, and +> the `=>` lambda takes an EXPRESSION (specs.md §Lambda / §Closures). The bug is +> real under the valid form too: `closure(() { if c { return 11; } return 22; })` +> with an inferred return failed identically before the fix. The regression test +> uses the valid `closure(...)` syntax. (The parser accepting `() => { block }` +> at all is a separate leniency gap, not tracked here.) + # 0187 — lambda with INFERRED return type + block body with early `return`s mis-infers its return type (LLVM verifier failure) ## Symptom diff --git a/src/ir/lower/closure.zig b/src/ir/lower/closure.zig index 3a3fbf51..5b7e8daf 100644 --- a/src/ir/lower/closure.zig +++ b/src/ir/lower/closure.zig @@ -161,8 +161,8 @@ pub fn lowerLambda(self: *Lowering, lam: *const ast.Lambda) Ref { } } } - // Arrow lambda without explicit return type — infer from body expression - // Temporarily bind params in scope so inferExprType can resolve param types + // Lambda without explicit return type — infer from the body. + // Temporarily bind params in scope so inference can resolve param types. var temp_scope = Scope.init(self.alloc, self.scope); const saved = self.scope; self.scope = &temp_scope; @@ -170,7 +170,23 @@ pub fn lowerLambda(self: *Lowering, lam: *const ast.Lambda) Ref { const pty = params.items[user_param_base + i].ty; temp_scope.put(p.name, .{ .ref = @enumFromInt(0), .ty = pty, .is_alloca = false }); } - const inferred = self.inferExprType(lam.body); + // Two body forms (parser.zig parseLambda), inferred exactly as a named + // fn (resolveReturnType in lower.zig): + // (params) => expr — arrow: the body expression IS the value. + // (params) { stmts } — block: an explicit `return ` sets the + // type; with none the return is VOID (the + // block's tail is a discarded statement, not an + // implicit return — only an explicit `-> R`, + // handled above, makes the tail the value). + // The old code always used inferExprType(lam.body); for a block body + // that mis-inferred — void/noreturn when the value came only from early + // `return`s (issue 0187), or `.unresolved` when the tail referenced a + // block-local the temp scope never bound (a variant the same fix + // subsumes) → LLVM panic. + const inferred = if (lam.body.data == .block) + self.findReturnValueType(lam.body) orelse .void + else + self.inferExprType(lam.body); self.scope = saved; temp_scope.deinit(); break :blk inferred;