fix: lambda inferred return type from a block body's early returns (issue 0187)

A `:=`-bound closure with no explicit `-> T` and a BLOCK body inferred its
return type via inferExprType(lam.body), which yields the last statement's
type. A block whose value comes only from early `return`s ends in a return
statement (void/noreturn), so the closure was built with a void return while
the body returned i64 — the call site then fed `i64 undef` and LLVM
verification failed. (A block whose tail referenced a block-local hit the
sibling failure: inferExprType returned .unresolved → an LLVM panic.)

Infer the return type exactly as a named fn does (resolveReturnType in
lower.zig): an arrow body `(params) => expr` uses the expression type; a block
body `(params) { stmts }` takes the first explicit `return <val>` type via
findReturnValueType, else void (the block tail is a discarded statement unless
an explicit `-> R` makes it the value). Regression test:
examples/closures/0313-closure-inferred-return-early.sx.
This commit is contained in:
agra
2026-06-26 09:28:29 +03:00
parent 8d23aad4b9
commit 493469fd74
6 changed files with 80 additions and 3 deletions

View File

@@ -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));
}

View File

@@ -0,0 +1,4 @@
f: 11
g+: 100
g-: 200
h: 3.500000

View File

@@ -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 <val>` 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) # 0187 — lambda with INFERRED return type + block body with early `return`s mis-infers its return type (LLVM verifier failure)
## Symptom ## Symptom

View File

@@ -161,8 +161,8 @@ pub fn lowerLambda(self: *Lowering, lam: *const ast.Lambda) Ref {
} }
} }
} }
// Arrow lambda without explicit return type — infer from body expression // Lambda without explicit return type — infer from the body.
// Temporarily bind params in scope so inferExprType can resolve param types // Temporarily bind params in scope so inference can resolve param types.
var temp_scope = Scope.init(self.alloc, self.scope); var temp_scope = Scope.init(self.alloc, self.scope);
const saved = self.scope; const saved = self.scope;
self.scope = &temp_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; const pty = params.items[user_param_base + i].ty;
temp_scope.put(p.name, .{ .ref = @enumFromInt(0), .ty = pty, .is_alloca = false }); 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 <val>` 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; self.scope = saved;
temp_scope.deinit(); temp_scope.deinit();
break :blk inferred; break :blk inferred;