From ba3c09428346c74d2064e8fc66a01e8372fcce36 Mon Sep 17 00:00:00 2001 From: agra Date: Mon, 1 Jun 2026 19:28:35 +0300 Subject: [PATCH] fix(lower): infer no-annotation return type with params in scope (issue 0059) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A function with no explicit return type (arrow `=> expr`, or a block whose `return ` drives the type) has its return type inferred from the body — but the body references the function's own params. resolveReturnType ran that inference before the params were pushed into self.scope (they're bound later, at body lowering), so inferExprType couldn't resolve them and yielded .unresolved, which reached LLVM emission and panicked. It only worked when a same-named binding lingered in scope from earlier lowering (e.g. inside the big smoke file). Bind the function's plain annotated value params into a temporary scope during return-type inference. Resolve their types via resolveTypeWithBindings rather than resolveParamType — the latter does variadic/pack bookkeeping that must run exactly once, at body lowering; calling it here too corrupted the format/index path. Variadic/pack/comptime/unannotated params are skipped (no by-name return dependency; their types come from substitution). Regression: examples/0308-closures-arrow-inferred-return.sx (arrow + block inferred-return, top-level + local). Resolves issue 0059. Suite: 293 passed. --- .../0308-closures-arrow-inferred-return.sx | 24 ++++++++++++++ .../0308-closures-arrow-inferred-return.exit | 1 + ...0308-closures-arrow-inferred-return.stderr | 1 + ...0308-closures-arrow-inferred-return.stdout | 4 +++ ...-lambda-inferred-return-unresolved-type.md | 13 ++++++++ ...-lambda-inferred-return-unresolved-type.sx | 15 --------- src/ir/lower.zig | 33 ++++++++++++++++--- 7 files changed, 72 insertions(+), 19 deletions(-) create mode 100644 examples/0308-closures-arrow-inferred-return.sx create mode 100644 examples/expected/0308-closures-arrow-inferred-return.exit create mode 100644 examples/expected/0308-closures-arrow-inferred-return.stderr create mode 100644 examples/expected/0308-closures-arrow-inferred-return.stdout delete mode 100644 issues/0059-expr-lambda-inferred-return-unresolved-type.sx diff --git a/examples/0308-closures-arrow-inferred-return.sx b/examples/0308-closures-arrow-inferred-return.sx new file mode 100644 index 0000000..f5935a6 --- /dev/null +++ b/examples/0308-closures-arrow-inferred-return.sx @@ -0,0 +1,24 @@ +// Regression (issue 0059): a function with NO explicit return type infers it +// from the body, which references the function's own parameters. The inference +// must see those params — before the fix they weren't in scope during +// return-type resolution, so the inferred type came out `.unresolved` and tripped +// the LLVM-emission guard ("unresolved type reached LLVM emission"). Whether it +// slipped through used to depend on a same-named binding lingering from earlier +// lowering. Covers the arrow (`=>`) and inferred-via-`return` forms, at top level +// and as locals. + +#import "modules/std.sx"; + +dbl :: (x: s32) => x * 2; // top-level arrow, inferred return +inc :: (x: s32) { return x + 1; } // top-level block, inferred via `return` + +main :: () { + print("{}\n", dbl(7)); // 14 + print("{}\n", inc(41)); // 42 + + tripl :: (x: s32) => x * 3; // local arrow, inferred return + print("{}\n", tripl(4)); // 12 + + half :: (x: f32) => x / 2.0; // inferred float return + print("{}\n", half(9.0)); // 4.500000 +} diff --git a/examples/expected/0308-closures-arrow-inferred-return.exit b/examples/expected/0308-closures-arrow-inferred-return.exit new file mode 100644 index 0000000..573541a --- /dev/null +++ b/examples/expected/0308-closures-arrow-inferred-return.exit @@ -0,0 +1 @@ +0 diff --git a/examples/expected/0308-closures-arrow-inferred-return.stderr b/examples/expected/0308-closures-arrow-inferred-return.stderr new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/examples/expected/0308-closures-arrow-inferred-return.stderr @@ -0,0 +1 @@ + diff --git a/examples/expected/0308-closures-arrow-inferred-return.stdout b/examples/expected/0308-closures-arrow-inferred-return.stdout new file mode 100644 index 0000000..d123bda --- /dev/null +++ b/examples/expected/0308-closures-arrow-inferred-return.stdout @@ -0,0 +1,4 @@ +14 +42 +12 +4.500000 diff --git a/issues/0059-expr-lambda-inferred-return-unresolved-type.md b/issues/0059-expr-lambda-inferred-return-unresolved-type.md index 0c7aa54..21138dc 100644 --- a/issues/0059-expr-lambda-inferred-return-unresolved-type.md +++ b/issues/0059-expr-lambda-inferred-return-unresolved-type.md @@ -1,5 +1,18 @@ # 0059 — expression-bodied lambda with inferred return type reaches LLVM emission unresolved +> **✅ RESOLVED.** Root cause: `resolveReturnType` ([src/ir/lower.zig]) infers a +> no-annotation function's return type from its body, but the body references the +> function's own params — which weren't in `self.scope` yet (they're bound later, +> at body lowering). So `inferExprType` couldn't resolve `x` in `(x: s32) => x * 2` +> and returned `.unresolved`, which reached LLVM emission. It only slipped through +> when a same-named binding happened to linger in scope from earlier lowering. +> Fix: bind the function's plain annotated value params into a temporary scope +> during return-type inference (resolving types directly via +> `resolveTypeWithBindings`, not `resolveParamType`, whose variadic/pack +> bookkeeping must run exactly once at body lowering). Covers both the arrow (`=>`) +> and inferred-via-`return` forms. Regression test: +> `examples/0308-closures-arrow-inferred-return.sx`. + ## Symptom An expression-bodied lambda (`name :: (params) => expr;`) **without** an explicit diff --git a/issues/0059-expr-lambda-inferred-return-unresolved-type.sx b/issues/0059-expr-lambda-inferred-return-unresolved-type.sx deleted file mode 100644 index 3506cc3..0000000 --- a/issues/0059-expr-lambda-inferred-return-unresolved-type.sx +++ /dev/null @@ -1,15 +0,0 @@ -// Repro for issue 0059: an expression-bodied `=>` lambda with an INFERRED -// return type reaches LLVM emission with an unresolved return type and panics. -// Adding an explicit `-> s32` makes it work; so does burying the same lambda -// inside a large program (examples/.../50-smoke had it and ran fine). -// -// Expected: prints `14`, exit 0. -// Actual: panic "unresolved type reached LLVM emission" (SIGABRT, exit 134). - -#import "modules/std.sx"; - -f :: (x: s32) => x * 2; - -main :: () { - print("{}\n", f(7)); -} diff --git a/src/ir/lower.zig b/src/ir/lower.zig index ff10617..1c5b5d5 100644 --- a/src/ir/lower.zig +++ b/src/ir/lower.zig @@ -11617,13 +11617,38 @@ pub const Lowering = struct { if (fd.return_type) |rt| { return self.resolveTypeWithBindings(rt); } - // Arrow functions without explicit return type: infer from body expression + // No explicit annotation — the type is inferred from the body, which + // references the function's own parameters (`(x: s32) => x * 2`). Those + // params aren't pushed into `self.scope` until body lowering, so bind + // them into a temporary scope here; otherwise `inferExprType` can't + // resolve `x`, the inference yields `.unresolved`, and that reaches LLVM + // emission as `func.ret` (issue 0059). Whether it slipped through used to + // depend on a same-named binding lingering from earlier lowering. + var tmp_scope = Scope.init(self.alloc, self.scope); + defer tmp_scope.deinit(); + const saved_scope = self.scope; + self.scope = &tmp_scope; + defer self.scope = saved_scope; + for (fd.params, 0..) |p, i| { + // Bind only plain annotated value params — that's all the body's + // return type can depend on by name. Skip variadic / pack / comptime + // params (their concrete types come from per-call substitution) and + // unannotated ones (no context here). Resolve the type directly via + // resolveTypeWithBindings rather than resolveParamType: the latter + // does variadic/pack bookkeeping that must run exactly once, at body + // lowering — calling it here too corrupts that state. + if (p.is_variadic or p.is_pack or p.is_comptime) continue; + if (p.type_expr.data == .inferred_type) continue; + const pty = self.resolveTypeWithBindings(p.type_expr); + tmp_scope.put(p.name, .{ .ref = Ref.fromIndex(@intCast(i)), .ty = pty, .is_alloca = false }); + } + // Arrow functions without explicit return type: infer from body expression. if (fd.is_arrow) { return self.inferExprType(fd.body); } - // No annotation, not arrow: an explicit `return ` statement - // wins. Otherwise default to void — the body's tail expression is - // a side-effect statement, not an implicit return. + // Not arrow: an explicit `return ` statement wins. Otherwise + // default to void — the body's tail expression is a side-effect + // statement, not an implicit return. if (self.findReturnValueType(fd.body)) |ty| return ty; return .void; }