diff --git a/packages/compiler/src/frontend/lowering/builtins/promises.ts b/packages/compiler/src/frontend/lowering/builtins/promises.ts index 5fd189242..2dc212f81 100644 --- a/packages/compiler/src/frontend/lowering/builtins/promises.ts +++ b/packages/compiler/src/frontend/lowering/builtins/promises.ts @@ -279,25 +279,39 @@ export function lowerPromiseStaticCall( // tick-for-tick in Node for plain results (probed), with the // promise-returning form riding the async return-adoption machinery // (its one-tick residue is SEMANTICS.md 358) — so the lowering IS - // that wrapper: one interned async helper per callback type. The - // ...args form fences (close over the values instead). + // that wrapper: one interned async helper per callback and argument + // types. `Promise.try(f, ...args)` evaluates the arguments at the call + // and passes them to f, which still runs synchronously. if (member === "try") { const fNode = call.arguments[0]; - if (call.arguments.length !== 1 || fNode === undefined || ts.isSpreadElement(fNode)) { + const argNodes = call.arguments.slice(1); + if (fNode === undefined || call.arguments.some(ts.isSpreadElement)) { lowerer.noLowering( - `Promise.try with ${call.arguments.length} arguments`, + `Promise.try with ${call.arguments.length === 0 ? "no" : "spread"} arguments`, call, - "the ...args form has no lowering — close over the values: Promise.try(() => f(a, b))", + "pass the callback and its arguments directly: Promise.try(f, a, b)", ); } const f = lowerer.lowerExpr(fNode); - if (f.type.kind !== "func" || f.type.params.length !== 0) { + // Arguments past the callback's parameters are evaluated and dropped; + // only an arrow function, which has no `arguments`, can ignore them. + if ( + f.type.kind !== "func" || + f.type.params.length > argNodes.length || + (f.type.params.length < argNodes.length && !ts.isArrowFunction(fNode)) + ) { lowerer.noLowering( - `Promise.try over a callback of type '${lowerer.checker.typeToString(lowerer.typeOf(fNode))}'`, + `Promise.try over a callback of type '${lowerer.checker.typeToString(lowerer.typeOf(fNode))}' with ${argNodes.length} argument${argNodes.length === 1 ? "" : "s"}`, fNode, - "a zero-parameter function is the supported form", + "pass one argument per callback parameter: Promise.try((a, b) => a + b, 1, 2)", ); } + const fnT = f.type; + const args = argNodes.map((node, i) => + i < fnT.params.length + ? lowerer.lowerExprExpecting(node, fnT.params[i]!) + : lowerer.lowerExpr(node), + ); const resultT = lowerer.mapTypeOf(lowerer.typeOf(call)); if (resultT?.kind !== "promise") { lowerer.noLowering( @@ -323,13 +337,22 @@ export function lowerPromiseStaticCall( "annotate both to one T: Promise.try(f) with f returning T or Promise", ); } - const key = `promise.try:${typeKey(f.type)}`; + const key = `promise.try:${typeKey(f.type)}:${args.map((a) => typeKey(a.type)).join(",")}`; let helper = lowerer.arrHofHelpers.get(key); if (!helper) { helper = `%promise.try.${lowerer.arrHofHelpers.size}`; lowerer.arrHofHelpers.set(key, helper); const fRef: IrExpr = { kind: "varRef", localId: "f.0", type: f.type, loc }; - const callF: IrExpr = { kind: "callValue", callee: fRef, args: [], type: ret, loc }; + const argLocals = args.map((a, i) => ({ id: `a${i}.0`, name: `a${i}`, type: a.type })); + const callF: IrExpr = { + kind: "callValue", + callee: fRef, + args: argLocals + .slice(0, fnT.params.length) + .map((a) => ({ kind: "varRef" as const, localId: a.id, type: a.type, loc })), + type: ret, + loc, + }; // Adopt a promise result before either returning its value or completing void. const value: IrExpr = ret.kind === "promise" ? { kind: "awaitExpr", value: callF, type: ret.inner, loc } : callF; @@ -342,16 +365,22 @@ export function lowerPromiseStaticCall( : [{ kind: "return", value, loc }]; const lifted: IrFunction = { name: helper, - params: [{ localId: "f.0", name: "f", type: f.type }], + params: [ + { localId: "f.0", name: "f", type: f.type }, + ...argLocals.map((a) => ({ localId: a.id, name: a.name, type: a.type })), + ], returnType: inner, - locals: [{ id: "f.0", name: "f", type: f.type, mutable: true }], + locals: [ + { id: "f.0", name: "f", type: f.type, mutable: true }, + ...argLocals.map((a) => ({ ...a, mutable: true })), + ], body, loc, async: true, }; lowerer.liftedFns.push(lifted); } - return { kind: "call", callee: helper, args: [f], type: resultT, loc }; + return { kind: "call", callee: helper, args: [f, ...args], type: resultT, loc }; } // Promise.resolve — the already-settled promise. Zero arguments is // Promise; a PROMISE argument is returned as-is (the spec's diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5920-promise-try-args.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5920-promise-try-args.ts.json new file mode 100644 index 000000000..b74ce6d45 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5920-promise-try-args.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5920-promise-try-args.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5921-promise-try-args-js.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5921-promise-try-args-js.js.json new file mode 100644 index 000000000..085d6415c --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5921-promise-try-args-js.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5921-promise-try-args-js.js" + ], + "diags": [] +} diff --git a/tests/corpus/5920-promise-try-args.ts b/tests/corpus/5920-promise-try-args.ts new file mode 100644 index 000000000..a5d309397 --- /dev/null +++ b/tests/corpus/5920-promise-try-args.ts @@ -0,0 +1,22 @@ +// Promise.try(f, ...args): the arguments are evaluated at the call and passed +// to f, which runs synchronously; throws reject and returned promises are +// adopted. +Promise.try(() => 5).then((v) => console.log("value", v)); +Promise.try((): number => { throw new Error("sync"); }).catch((e) => console.log("caught", (e as Error).message)); +Promise.try(() => Promise.resolve("inner")).then((v) => console.log("adopted", v)); +Promise.try(async () => 7).then((v) => console.log("async", v)); +const order: string[] = []; +const p = Promise.try(() => { order.push("run"); return 1; }); +order.push("after"); +p.then(() => console.log(order.join(","))); +Promise.try((a: number, b: number) => a + b, 1, 2).then((v) => console.log("args", v)); +Promise.try(() => undefined).then((v) => console.log("void", v)); +const log: string[] = []; +const v = (x: number): number => { log.push("arg" + x); return x; }; +const q = Promise.try((a: number, b: number) => { log.push("call"); return a + b; }, v(1), v(2)); +log.push("after"); +q.then((r) => console.log(r, log.join(","))); +Promise.try((s: string) => { throw new Error(s); }, "bad").catch((e) => console.log("caught", (e as Error).message)); +Promise.try((n: number) => Promise.resolve(n * 2), 21).then((r) => console.log("adopted", r)); +function named(this: void, x: number): number { return x + 1; } +Promise.try(named, 4).then((r) => console.log("named", r)); diff --git a/tests/corpus/5921-promise-try-args-js.js b/tests/corpus/5921-promise-try-args-js.js new file mode 100644 index 000000000..10171c11d --- /dev/null +++ b/tests/corpus/5921-promise-try-args-js.js @@ -0,0 +1,7 @@ +// Promise.try(f, ...args) in JavaScript: unannotated parameters take the +// passed values. +Promise.try((a, b) => a * b, 6, 7).then((v) => console.log("product", v)); +Promise.try((s) => s.toUpperCase(), "js").then((v) => console.log(v)); +Promise.try((x) => { + throw new RangeError("r" + x); +}, 1).catch((e) => console.log(e.name, e.message)); diff --git a/tests/diagnostics/es2025-fences.ts b/tests/diagnostics/es2025-fences.ts index 128c9d9ed..2df12226f 100644 --- a/tests/diagnostics/es2025-fences.ts +++ b/tests/diagnostics/es2025-fences.ts @@ -2,8 +2,8 @@ // ArrayBuffers and transfer, SharedArrayBuffer.grow, Atomics.waitAsync, // Float16Array and its Math/DataView companions, first-class iterator // objects, and the unlowered forms of the implemented statics -// (Promise.try's ...args, groupBy's literal-union keys, a set-like -// non-Set argument). +// (Promise.try with fewer arguments than parameters, groupBy's +// literal-union keys, a set-like non-Set argument). // Resizable ArrayBuffer + transfer (ES2024). const ab = new ArrayBuffer(8, { maxByteLength: 16 }); @@ -31,8 +31,8 @@ dv.setFloat16(0, 1.5); // First-class iterator objects. const it = Iterator.from([1, 2, 3].values()); -// Promise.try's ...args form (close over the values instead). -const tried = Promise.try((a: number, b: number) => a + b, 1, 2); +// Promise.try with fewer arguments than the callback's parameters. +const tried = Promise.try((a: number, b?: number) => a + (b ?? 0), 1); // Object.groupBy at a literal-union key type (a fixed-field record). const grouped = Object.groupBy([1, 2, 3], (n) => (n % 2 === 0 ? "even" : "odd")); diff --git a/tests/harness/__snapshots__/es2025-fences.ts.txt b/tests/harness/__snapshots__/es2025-fences.ts.txt index 0a91648ea..e1761f73a 100644 --- a/tests/harness/__snapshots__/es2025-fences.ts.txt +++ b/tests/harness/__snapshots__/es2025-fences.ts.txt @@ -97,14 +97,14 @@ es2025-fences.ts:32:12 - error SC2020: 'Iterator.from' is part of the standard l hint: first-class iterator objects have no lowering — iterator helpers compile as one chain on an array iterator, consumed in place: arr.values().map(f).take(n).toArray() -es2025-fences.ts:35:15 - error SC2020: 'Promise.try with 3 arguments' is part of the standard library types but has no scriptc lowering yet +es2025-fences.ts:35:27 - error SC2020: 'Promise.try over a callback of type '(a: number, b?: number | undefined) => number' with 1 argument' is part of the standard library types but has no scriptc lowering yet - 34 | // Promise.try's ...args form (close over the values instead). - 35 | const tried = Promise.try((a: number, b: number) => a + b, 1, 2); - | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 34 | // Promise.try with fewer arguments than the callback's parameters. + 35 | const tried = Promise.try((a: number, b?: number) => a + (b ?? 0), 1); + | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 36 | - hint: the ...args form has no lowering — close over the values: Promise.try(() => f(a, b)) + hint: pass one argument per callback parameter: Promise.try((a, b) => a + b, 1, 2) es2025-fences.ts:38:17 - error SC2020: 'Object.groupBy at this result type' is part of the standard library types but has no scriptc lowering yet