diff --git a/packages/compiler/src/frontend/lowering/call-signatures.ts b/packages/compiler/src/frontend/lowering/call-signatures.ts index 45329d077..98e78953f 100644 --- a/packages/compiler/src/frontend/lowering/call-signatures.ts +++ b/packages/compiler/src/frontend/lowering/call-signatures.ts @@ -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. */ diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/void-slot-returned-arrow.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/void-slot-returned-arrow.ts.json new file mode 100644 index 000000000..cc7e1cea7 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/void-slot-returned-arrow.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/void-slot-returned-arrow.ts" + ], + "diags": [] +} diff --git a/tests/corpus/void-slot-returned-arrow.ts b/tests/corpus/void-slot-returned-arrow.ts new file mode 100644 index 000000000..0abe87eac --- /dev/null +++ b/tests/corpus/void-slot-returned-arrow.ts @@ -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"); diff --git a/tests/harness/corpus-costs/void-slot-returned-arrow.ts.json b/tests/harness/corpus-costs/void-slot-returned-arrow.ts.json new file mode 100644 index 000000000..c321beddb --- /dev/null +++ b/tests/harness/corpus-costs/void-slot-returned-arrow.ts.json @@ -0,0 +1 @@ +{ "plain": 1600, "san": 2400 }