Lower Promise.try with arguments for the callback (#1212)

This commit is contained in:
Chris Tate
2026-10-17 16:15:20 -05:00
committed by GitHub
7 changed files with 92 additions and 22 deletions
@@ -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<T>(f) with f returning T or Promise<T>",
);
}
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<void>; a PROMISE argument is returned as-is (the spec's
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/5920-promise-try-args.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/5921-promise-try-args-js.js"
],
"diags": []
}
+22
View File
@@ -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));
+7
View File
@@ -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));
+4 -4
View File
@@ -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"));
@@ -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