mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-25 06:24:27 +08:00
Lower Promise.try with arguments for the callback (#1212)
This commit is contained in:
@@ -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
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/5920-promise-try-args.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/5921-promise-try-args-js.js"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
@@ -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));
|
||||
@@ -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));
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user