mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-11-07 08:33:50 +08:00
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:
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,
|
||||
|
||||
Reference in New Issue
Block a user