Files
sx/issues/0187-lambda-inferred-return-block-early-return.md
agra 493469fd74 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.
2026-06-26 09:28:29 +03:00

4.3 KiB

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 returns 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) => exprinferExprType(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 returns mis-infers its return type (LLVM verifier failure)

Symptom

A :=-bound lambda (closure literal) that has NO explicit -> T return type and whose body is a BLOCK containing return statements infers the WRONG return type (apparently void). Calling it and using the value fails LLVM verification (Call parameter type does not match function signature! ... i64 undef / Function arguments must have first-class types!). Adding an explicit -> T makes it work. No optionals or flow narrowing are involved — found while verifying issue 0186.

Reproduction

#import "modules/std.sx";
main :: () {
  // inferred return type, block body with early returns — NO optionals
  f := () => { if 1 > 0 { return 11; } return 22; };
  print("f: {}\n", f());   // LLVM verification failed (return type inferred void/undef)
}

Workaround / contrast (works): annotate the return type — f := () -> i64 => { if 1 > 0 { return 11; } return 22; };

Root cause (hypothesis)

The lambda return-type inference in lowerLambda (src/ir/lower/closure.zig, the ret_ty computation around line 164: const inferred = self.inferExprType(lam.body);) does not infer the type from the body's return statements when the body is a block. For a block whose value is produced only via early returns (not a trailing tail expression), inferExprType likely yields .void, so the lambda is built with a void return while the body actually returns i64 — the mismatch surfaces at the call site / LLVM verifier.

Investigation prompt

In src/ir/lower/closure.zig, the lambda return-type inference path (inferExprType(lam.body) ~line 164) must, for a block body, infer the return type from the body's return statement operands (matching how lowerValueBody / the function-decl return inference handles bodies with early returns), not just the block's tail-expression value. Reuse the existing return-type inference the top-level fn path uses (a top-level f :: () { if c { return 11; } return 22; } with inferred return works — see why, and apply the same to lambdas). Verify:

  1. The repro prints f: 11.
  2. examples/optionals/0919/0921 and examples/closures/0312 still pass (0312 deliberately uses explicit -> i64 to dodge this bug — once fixed, an inferred-return variant should also work).
  3. Add a regression examples/closures/03xx-lambda-inferred-return-early.sx.

Unrelated to the optional-unwrap family (0179/0185) and the closure-arg coercion fix (0186); purely lambda return-type inference.