From 39e3b4a5dd6947b9b26f2104fec566e33d60ab9a Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Tue, 20 Oct 2026 09:34:19 -0500 Subject: [PATCH] Adopt thenable entries and place tuple slots by position in mixed Promise.all - A checked-dynamic entry of a tuple Promise.all goes through PromiseResolve (`dyn.promiseResolve`): a native promise is itself, a thenable adopts its settlement through its resolve job, and any other value fulfills. Before, a thenable fulfilled the slot with the object. - Tuple fields read their entry by position. Shape fields are sorted by name, so tuples of more than ten entries paired fields with the wrong entries. --- .../compiler/src/backend/llvm/lib-shared.ts | 1 + .../frontend/lowering/builtins/promises.ts | 29 +++++++++---------- .../frontend/lowering/checked-promise-all.ts | 2 +- .../compiler/src/ir/builtin-signatures.ts | 4 +++ packages/compiler/src/ir/runtime-features.ts | 1 + packages/runtime/src/scr_async_dyn.c | 15 ++++++---- packages/runtime/src/scr_dyn.h | 4 +++ tests/corpus/6482-promise-all-tuples.ts | 23 ++++++++++++++- 8 files changed, 56 insertions(+), 23 deletions(-) diff --git a/packages/compiler/src/backend/llvm/lib-shared.ts b/packages/compiler/src/backend/llvm/lib-shared.ts index 44741edc8..9ddcb6240 100644 --- a/packages/compiler/src/backend/llvm/lib-shared.ts +++ b/packages/compiler/src/backend/llvm/lib-shared.ts @@ -922,6 +922,7 @@ export const LIB_FN_SYMS: Record = { "dyn.promiseAll": "scr_dyn_promise_all", "dyn.promiseAllSettled": "scr_dyn_promise_all_settled", "dyn.promiseAny": "scr_dyn_promise_any", + "dyn.promiseResolve": "scr_dyn_promise_resolve", "dyn.typeof": "scr_dyn_typeof", "dyn.objectTag": "scr_dyn_object_tag", "dyn.freeze": "scr_dyn_freeze", diff --git a/packages/compiler/src/frontend/lowering/builtins/promises.ts b/packages/compiler/src/frontend/lowering/builtins/promises.ts index 5d4a2e3f2..6ac1232b9 100644 --- a/packages/compiler/src/frontend/lowering/builtins/promises.ts +++ b/packages/compiler/src/frontend/lowering/builtins/promises.ts @@ -1,4 +1,4 @@ -import { checkedPromiseAll } from "../checked-promise-all.js"; +import { checkedPromiseAll, thenReader } from "../checked-promise-all.js"; import { InternalCompilerError } from "../../../errors.js"; import * as ts from "../../ts7/adapter.js"; import { newFnCtx, type Lowerer } from "../lowerer.js"; @@ -6,7 +6,6 @@ import { varRef } from "../../../ir/build.js"; import { maybePromiseResolve, maybePromiseUnionOf } from "../maybe-promise.js"; import { isJsSourceFile, locOf } from "../../program.js"; import { - BOOL, DYN, type IrExpr, type IrFunction, @@ -31,7 +30,8 @@ import { * checked-dynamic tree. Non-promise entries (numbers, strings, booleans, * unit values — never thenable) pass straight into their slot; a * checked-dynamic entry (a JavaScript function's result) joins through - * the promise it holds, or a promise fulfilled with its plain value. The tuple + * PromiseResolve: the promise it holds, a promise that adopts a thenable, + * or a promise fulfilled with its plain value. The tuple * fulfills one microtask turn after the combinator does. Null when the * result type or an entry has no typed home here. */ function lowerTuplePromiseAll( @@ -68,8 +68,9 @@ function lowerTuplePromiseAll( const ref = (i: number): IrExpr => varRef(`entry.${i}`, elems[i]!.type, loc); const dynPromise: IrType = { kind: "promise", inner: DYN }; const body: IrStmt[] = []; - // Each dyn entry's promise view: the promise it holds, or a promise - // fulfilled with the plain value (Promise.resolve's rule). + // Each dyn entry's promise view, in entry order: the promise it + // holds, a promise that adopts a thenable's settlement, or a promise + // fulfilled with the plain value (PromiseResolve's rule). const views = new Map(); elems.forEach((e, i) => { if (e.type.kind !== "dyn") return; @@ -78,16 +79,9 @@ function lowerTuplePromiseAll( kind: "varDecl", localId: view.id, init: { - kind: "ternary", - cond: { kind: "dynTest", test: "promise", value: ref(i), type: BOOL, loc }, - then: { kind: "dynCheck", value: ref(i), type: dynPromise, loc }, - else_: { - kind: "intrinsic", - name: "promise.resolve", - args: [ref(i)], - type: dynPromise, - loc, - }, + kind: "libCall", + fn: "dyn.promiseResolve", + args: [ref(i), thenReader(lowerer, loc)], type: dynPromise, loc, }, @@ -138,7 +132,10 @@ function lowerTuplePromiseAll( type: VOID, loc, }; - const fields = shape.fields.map((field, i) => { + // Shape fields are sorted by name ("10" before "2"); a tuple field's + // name is its position. + const fields = shape.fields.map((field) => { + const i = Number(field.name); const entry = elems[i]!; let value: IrExpr; const view = views.get(i); diff --git a/packages/compiler/src/frontend/lowering/checked-promise-all.ts b/packages/compiler/src/frontend/lowering/checked-promise-all.ts index c3be91833..6c24a8b2c 100644 --- a/packages/compiler/src/frontend/lowering/checked-promise-all.ts +++ b/packages/compiler/src/frontend/lowering/checked-promise-all.ts @@ -11,7 +11,7 @@ const COMBINATOR_LIB_CALLS = { /** `value.then` through the program's own property dispatch, so a class * instance's `then` method is visible to the runtime's thenable check. */ -function thenReader(lowerer: Lowerer, loc: SrcLoc): IrExpr { +export function thenReader(lowerer: Lowerer, loc: SrcLoc): IrExpr { const name = "%promise.thenOf"; const type = funcOf([DYN], DYN); if (!lowerer.liftedFns.some((fn) => fn.name === name)) { diff --git a/packages/compiler/src/ir/builtin-signatures.ts b/packages/compiler/src/ir/builtin-signatures.ts index c55444e60..55e66f667 100644 --- a/packages/compiler/src/ir/builtin-signatures.ts +++ b/packages/compiler/src/ir/builtin-signatures.ts @@ -239,6 +239,10 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ "dyn.promiseAll": { argTypes: [DYN, DYN], result: { kind: "promise", inner: DYN } }, "dyn.promiseAllSettled": { argTypes: [DYN, DYN], result: { kind: "promise", inner: DYN } }, "dyn.promiseAny": { argTypes: [DYN, DYN], result: { kind: "promise", inner: DYN } }, + /** PromiseResolve over one checked-dynamic value: the promise it holds, + * a promise that adopts a thenable's settlement, or a promise fulfilled + * with the value. The second argument reads `then` as above. */ + "dyn.promiseResolve": { argTypes: [DYN, DYN], result: { kind: "promise", inner: DYN } }, /** Bare `typeof v` on a dyn value AS A STRING (arg: the dyn value, * borrowed; result: an owned string) — the dyn kind's JS answer: * undefined→"undefined", null/object/array/bytes→"object" (JS's oldest diff --git a/packages/compiler/src/ir/runtime-features.ts b/packages/compiler/src/ir/runtime-features.ts index 9abe15345..46e44e0e1 100644 --- a/packages/compiler/src/ir/runtime-features.ts +++ b/packages/compiler/src/ir/runtime-features.ts @@ -21,6 +21,7 @@ const DYN_ASYNC_LIB_FNS: ReadonlySet = new Set([ "dyn.promiseAll", "dyn.promiseAllSettled", "dyn.promiseAny", + "dyn.promiseResolve", "process.onUncaughtException", "process.offUncaughtException", "process.onUnhandledRejection", diff --git a/packages/runtime/src/scr_async_dyn.c b/packages/runtime/src/scr_async_dyn.c index 1cdd466c3..ec4dad794 100644 --- a/packages/runtime/src/scr_async_dyn.c +++ b/packages/runtime/src/scr_async_dyn.c @@ -1437,24 +1437,29 @@ static void scr_dyn_thenable_schedule(ScrPromise *target, const ScrDyn *thenable * a thenable gets a fresh promise and its resolve job, anything else a * fulfilled promise (a throwing `then` read rejects it). The element * joins the combinator right after, so a thenable's job sits between - * the jobs of the entries around it, exactly Node's iteration. */ -static ScrPromise *scr_dyn_promise_entry(ScrCombine *c, ScrDyn *value, const ScrDyn *reader) { + * the jobs of the entries around it, exactly Node's iteration. Without a + * combinator (`c` NULL) this is PromiseResolve for one value. */ +static ScrPromise *scr_dyn_promise_entry(ScrCombine *c, const ScrDyn *value, const ScrDyn *reader) { if (value->kind == SCR_DYN_PROMISE) return scr_promise_retain(value->v.promise); ScrPromise *source = scr_promise_new(); ScrDyn *then = scr_dyn_then_of(value, reader); if (scr_exc_pending()) { scr_promise_reject_pending(source); } else if (then) { - scr_promise_combine_flush(c); + if (c) scr_promise_combine_flush(c); scr_dyn_thenable_schedule(source, value, then, reader); } else { - scr_promise_fulfill_ref(source, scr_dyn_retain(value), scr_dyn_retain_v, scr_dyn_release_v, - scr_dyn_trace_v); + scr_promise_fulfill_ref(source, scr_dyn_retain((ScrDyn *)value), scr_dyn_retain_v, + scr_dyn_release_v, scr_dyn_trace_v); } scr_dyn_release(then); return source; } +ScrPromise *scr_dyn_promise_resolve(const ScrDyn *value, const ScrDyn *reader) { + return scr_dyn_promise_entry(NULL, value, reader); +} + static void scr_dyn_settled_store(ScrArr *a, double i, ScrPromise *src) { ScrDyn *record = scr_dyn_new_obj(); bool fulfilled = !scr_promise_is_rejected(src); diff --git a/packages/runtime/src/scr_dyn.h b/packages/runtime/src/scr_dyn.h index a3d506a10..93170a1e3 100644 --- a/packages/runtime/src/scr_dyn.h +++ b/packages/runtime/src/scr_dyn.h @@ -1309,6 +1309,10 @@ extern SCR_TL ScrDyn *(*scr_dyn_promise_member_fn)(const char *key, size_t len); ScrPromise *scr_dyn_promise_all(const ScrDyn *entries, const ScrDyn *reader); ScrPromise *scr_dyn_promise_all_settled(const ScrDyn *entries, const ScrDyn *reader); ScrPromise *scr_dyn_promise_any(const ScrDyn *entries, const ScrDyn *reader); +/* PromiseResolve over one dyn value (borrowed): a native promise is + * itself, a thenable adopts through its resolve job, a throwing `then` + * read rejects, and any other value fulfills. +1. */ +ScrPromise *scr_dyn_promise_resolve(const ScrDyn *value, const ScrDyn *reader); /* Native predicates for typed reference capsules and boxed errors. */ bool scr_dyn_ref_typeof_is(const ScrDyn *d, const char *name); diff --git a/tests/corpus/6482-promise-all-tuples.ts b/tests/corpus/6482-promise-all-tuples.ts index ff1615604..3c5e38e29 100644 --- a/tests/corpus/6482-promise-all-tuples.ts +++ b/tests/corpus/6482-promise-all-tuples.ts @@ -1,7 +1,8 @@ // Promise.all over an array literal of DIFFERENT promise types (the // checker's tuple overload): destructuring with holes and nested // patterns, void entries, plain values, `any` entries that may or may not -// hold a promise, entry identity, and rejection — +// hold a promise or a thenable, tuples of more than ten entries, entry +// identity, and rejection — // the first rejection in settlement order wins, later ones count as // handled. interface Version { @@ -48,6 +49,18 @@ function untypedRecord(): any { return Promise.resolve({ label: "from a promise" }); } +// A thenable that is not a native promise: Promise.all adopts its +// settlement, calling `then` one microtask after the call. +class Deferred { + then(resolve: (value: string) => void): void { + console.log("then called"); + resolve("adopted"); + } +} +function untypedThenable(): any { + return new Deferred(); +} + async function main(): Promise { const [runtime, { tool: kind, schema: level }] = await Promise.all([version(), scan()]); console.log(runtime.major, kind, level, runtime === shared); @@ -64,6 +77,14 @@ async function main(): Promise { const [count, { label }, plain] = await Promise.all([untypedCount(), untypedRecord(), "p"]); console.log(count + 1, label, plain); + const pending = Promise.all([untypedThenable(), version()]); + console.log("after the call"); + const [adopted, latest] = await pending; + console.log(adopted, latest.major); + + const many = await Promise.all([version(), 1, 2, 3, 4, 5, 6, 7, 8, 9, "ten", scan()]); + console.log(many[0].major, many[2], many[9], many[10], many[11].tool); + try { await Promise.all([delay(20, 1, "slow failure"), delay(5, "s", "fast failure"), delay(1, true)]); } catch (e) {