Keep a returned arrow's own return type in a void slot

A value-returning arrow returned through a `() => void` interface member
was adopted as void, so the enclosing function promised `() => number`
while the lambda built a void closure: "a void result where the
'() => number' slot promises ..." natively, though Node runs it
(`repros/d2-void-slot-generic.ts`, pi `event-bus.ts:27`).

A lambda that is the returned value of the function being lowered reaches
that function's caller, so its own return type must survive the checker's
void contextual type; the void adoption in lambdaSignature now skips such
a lambda. Functions with a void result keep the existing discard
semantics.

tests/corpus/void-slot-returned-arrow.ts pins the shape both ways.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Malte Ubl
2026-10-29 05:39:21 -07:00
co-authored by Claude Opus 5.5
parent 97f4d092f1
commit 6014bb4ea5
4 changed files with 58 additions and 0 deletions
@@ -1148,6 +1148,15 @@ export function lambdaSignature(
// is the one the adoption block below takes for mappable inferred
// returns, so this only changes programs that do not compile today.
//
// A lambda RETURNED from the function being lowered flows into that
// function's result type, which OBSERVES its value — the checker's void
// contextual type at such a site comes from an outer assignment target
// that would discard the enclosing function's own result. So the void
// adoption below must not fire there: `{ on: (h) => () => seen.push("y") }`
// against a `(h) => void` field types the inner arrow `() => number`
// (its own type) while its contextual type is the discarded `() => void`,
// and the outer arrow's result is the `() => number` that must return it.
const returnedValue = returnedIntoValueSlot(lowerer, node);
// The contextual signature is looked up at most once per lambda: both
// this decision and the adoption block below need it, and the checker
// call is not free in a compile this hot.
@@ -1165,6 +1174,7 @@ export function lambdaSignature(
ts.isArrowFunction(node) && !ts.isBlock(node.body) && isStreamUndefCallExpr(lowerer, node.body)
? VOID
: (ts.isArrowFunction(node) || ts.isFunctionExpression(node)) &&
!returnedValue &&
voidContextualSlot(lowerer, node, contextualSigs)
? VOID
: lowerer.declaredReturnType(node, node);
@@ -1292,6 +1302,7 @@ export function lambdaSignature(
if (
!literalMethod &&
!isAsyncLike &&
!returnedValue &&
!node.type &&
!inferredLiteral &&
ret.kind !== "void" &&
@@ -1352,6 +1363,20 @@ function voidContextualSlot(
);
}
/** Whether `node` is the expression of a `return` in the function being
* lowered, and that function's result is a function type promising a
* value (`() => number`, not `() => void`). Such a lambda's value reaches
* the caller of the enclosing function, so its own return type is kept
* even when the checker's contextual type for it is `void` (see the ABI
* decision in lambdaSignature). */
function returnedIntoValueSlot(lowerer: Lowerer, node: ts.Node): boolean {
let parent: ts.Node | undefined = node.parent;
while (parent !== undefined && ts.isParenthesizedExpression(parent)) parent = parent.parent;
if (parent === undefined || !ts.isReturnStatement(parent) || !parent.expression) return false;
const result = lowerer.ctx.returnType;
return result.kind === "func" && result.ret.kind !== "void";
}
/** The type of the property an object-literal method fills on the
* literal's contextual type; undefined when the literal has no contextual
* type or the method has a computed name. */
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/void-slot-returned-arrow.ts"
],
"diags": []
}
+26
View File
@@ -0,0 +1,26 @@
// A value-returning arrow RETURNED through a `() => void` interface member.
// TypeScript types the inner arrow `() => number` (its own type) while the
// checker contextually types it `() => void`; the enclosing arrow's result
// is `() => number`, so the value is observed and must be produced. Node
// runs this; the closure is called for its effect only.
interface Subscriber {
on(handler: (data: string) => void): () => void;
}
function makeSubscriber(): Subscriber {
const seen: string[] = [];
return {
on: (handler) => {
seen.push("registered");
handler("first");
// Concise arrow whose body returns a number: the result is discarded
// at the `() => void` slot but the arrow's own type keeps it.
return () => seen.push("unsubscribed");
},
};
}
const off = makeSubscriber().on((data) => console.log("got", data));
console.log("off is", typeof off);
off();
console.log("done");
@@ -0,0 +1 @@
{ "plain": 1600, "san": 2400 }