Combine Promise.race entries whose combined type has no representation

When the checker's combined type has no representation (events.once's
any[] arrays, or 'void | any[]'), the race result takes the one type the
entries that can fulfill share, or 'unknown' when each of them widens into
it; Promise<never> entries do not take part. An entry whose payload widens
into the result type converts through the checked promise payload
widening, which keeps its settlement turn.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Malte Ubl
2026-10-20 08:47:52 -07:00
co-authored by Claude Opus 5.5
parent 2429eaf556
commit 2630e8438a
@@ -23,7 +23,11 @@ import {
* type is the checker's combined promise, and each entry's inner type
* must be that inner type, one of its union arms, or a sub-union of it
* (the backend's interned adapters wrap/re-tag fulfillments; a wider
* entry would need machinery that doesn't exist and fences).
* entry would need machinery that doesn't exist and fences). A
* Promise<never> entry fits any result, and an entry whose payload widens
* into the result's converts. When the checker's combined type has no
* representation, the result takes the type the entries that can fulfill
* share, or 'unknown'.
* Promise.all/allSettled/any fence with the sequential-await hint;
* resolve/reject and the rest fall to the member fence. Null for
* non-Promise receivers. */
@@ -50,14 +54,47 @@ export function lowerPromiseStaticCall(
"a non-empty array LITERAL of promises is the supported form: Promise.race([p, q])",
);
}
const resultT = lowerer.mapTypeOf(lowerer.typeOf(call));
if (resultT?.kind !== "promise") {
lowerer.noLowering(
"Promise.race with this combined result type",
call,
"the entries' value types must combine into a representable union",
);
// A Promise<never> entry (the deadline idiom: `new Promise<never>((_,
// reject) => setTimeout(reject, ms))`) can only reject, so it races
// under any result type; its inner type maps to void.
const neverEntry = (el: ts.Expression): boolean => {
const t = lowerer.typeOf(el);
const arg =
t.getSymbol()?.name === "Promise"
? lowerer.checker.getTypeArguments(t as ts.TypeReference)[0]
: undefined;
return arg !== undefined && (arg.flags & ts.TypeFlags.Never) !== 0;
};
const elements = argNode.elements.nodes;
let combined = lowerer.mapTypeOf(lowerer.typeOf(call));
let lowered: IrExpr[] | null = null;
if (combined?.kind !== "promise") {
// The checker's combined type has no representation (an 'any'
// element array such as events.once's any[], or 'void | any[]'): the
// result takes the one type the entries that can fulfill share, or
// else 'unknown' when each of them widens into it.
lowered = elements.map((el) => lowerer.lowerExpr(el));
const fulfilling = lowered.filter((_, i) => !neverEntry(elements[i]!));
const first = fulfilling[0]?.type;
const dynResult: IrType & { kind: "promise" } = { kind: "promise", inner: DYN };
if (first?.kind === "promise" && fulfilling.every((e) => typeEquals(e.type, first))) {
combined = first;
} else if (
fulfilling.length > 0 &&
fulfilling.every(
(e) => e.type.kind === "promise" && lowerer.promisePayloadWidens(e.type, dynResult),
)
) {
combined = dynResult;
} else {
lowerer.noLowering(
"Promise.race with this combined result type",
call,
"the entries' value types must combine into a representable union",
);
}
}
const resultT = combined;
const resultArms =
resultT.inner.kind === "union"
? (lowerer.unions.get(resultT.inner.unionId)?.arms ?? [])
@@ -74,19 +111,8 @@ export function lowerPromiseStaticCall(
}
return resultArms.some((a) => typeEquals(a, inner));
};
// A Promise<never> entry (the deadline idiom: `new Promise<never>((_,
// reject) => setTimeout(reject, ms))`) can only reject, so it races
// under any result type; its inner type maps to void.
const neverEntry = (el: ts.Expression): boolean => {
const t = lowerer.typeOf(el);
const arg =
t.getSymbol()?.name === "Promise"
? lowerer.checker.getTypeArguments(t as ts.TypeReference)[0]
: undefined;
return arg !== undefined && (arg.flags & ts.TypeFlags.Never) !== 0;
};
const entries = argNode.elements.nodes.map((el) => {
const entry = lowerer.lowerExpr(el);
const entries = elements.map((el, i) => {
const entry = lowered?.[i] ?? lowerer.lowerExpr(el);
if (entry.type.kind !== "promise") {
lowerer.noLowering(
"Promise.race over non-promise entries",
@@ -95,6 +121,10 @@ export function lowerPromiseStaticCall(
);
}
if (!compatible(entry.type.inner) && !(entry.type.inner.kind === "void" && neverEntry(el))) {
// A payload that widens into the result's (into 'unknown', or a
// checked conversion) keeps its promise and settlement turn.
if (lowerer.promisePayloadWidens(entry.type, resultT))
return lowerer.coerceToExpected(entry, resultT);
lowerer.unsupported(
"SC1090",
el,