Evaluate every Promise.all entry before a dynamic entry's then lookup

A checked-dynamic entry's PromiseResolve reads its `then` property,
which can be a getter. The tuple form resolved each entry as the array
was built, so that getter ran before later entries evaluated; Node
evaluates the whole array first. With a checked-dynamic entry, every
entry now evaluates into a hidden local before the entries resolve.
This commit is contained in:
Chris Tate
2026-10-22 11:17:42 -05:00
parent ef8cc7f74f
commit 74fcda0ad9
2 changed files with 39 additions and 2 deletions
@@ -72,7 +72,28 @@ function lowerTuplePromiseAll(
const voidPromise: IrType & { kind: "promise" } = { kind: "promise", inner: VOID };
const entriesT = arrayOf(voidPromise);
const dynPromise: IrType = { kind: "promise", inner: DYN };
const entries = elems.map((e): IrExpr => {
// A checked-dynamic entry's PromiseResolve can run a `then` getter, which
// Node runs after the whole array literal evaluated. With such an entry,
// every entry evaluates into a hidden local first.
const staged: IrStmt[] = [];
const values = !elems.some((e) => e.type.kind === "dyn")
? elems
: elems.map((e): IrExpr => {
if (e.kind === "unitLit") return e;
if (isUnitType(e.type)) {
staged.push({ kind: "exprStmt", expr: e, loc });
return {
kind: "unitLit",
unit: e.type.kind === "nullT" ? "null" : "undefined",
type: e.type,
loc,
};
}
const local = lowerer.declareHiddenLocal("%allValue", e.type);
staged.push({ kind: "varDecl", localId: local.id, init: e, loc });
return varRef(local.id, e.type, loc);
});
const entries = values.map((e): IrExpr => {
let promise: IrExpr;
if (e.type.kind === "promise") promise = e;
else if (e.type.kind === "dyn")
@@ -164,7 +185,7 @@ function lowerTuplePromiseAll(
loc,
});
}
return {
const all: IrExpr = {
kind: "libCall",
fn: "promise.allTuple",
args: [
@@ -174,6 +195,9 @@ function lowerTuplePromiseAll(
type: resultT,
loc,
};
return staged.length === 0
? all
: { kind: "seqExpr", stmts: staged, result: all, type: resultT, loc };
}
/** `Promise.race([...])` on THE Promise global: the entries lower
@@ -26,6 +26,16 @@ async function text(): Promise<string> {
async function both(): Promise<[number, string]> {
return Promise.all([num(), text()]);
}
const thenable: any = {
get then() {
log.push("then getter");
return (resolve: (v: number) => void) => resolve(1);
},
};
function evaluated(): Promise<string> {
log.push("second entry evaluated");
return Promise.resolve("s");
}
interface Item {
id: number;
}
@@ -49,6 +59,9 @@ async function main(): Promise<void> {
log.push(`returned ${h}${i}`);
const mixed = await Promise.all([later("x", 3, "x"), 5, later("y", 1, true), undefined]);
log.push(`mixed ${mixed[0]} ${mixed[1]} ${mixed[2]} ${mixed[3]}`);
// A dynamic entry's `then` getter runs after the whole array evaluated.
const [fromThenable, second] = await Promise.all([thenable, evaluated()]);
log.push(`thenable ${fromThenable} ${second}`);
const raw: any = JSON.parse('{"value":41}');
const [dynamic, counted] = await Promise.all([raw.value, later("z", 2, 2)]);
log.push(`dynamic ${dynamic + counted}`);