mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-11-02 07:43:42 +08:00
Complete function-value arguments in optional calls
The `f?.()` branch of the optional-chain lowering forwarded a spread
argument list through completeFuncValueArgs but mapped a plain argument
list itself, so an omitted optional parameter was never padded with the
callee's undefined arm. The lib-boundary arity fence then refused the call
("2 arguments where its lowered signature takes 3"), which is what
`callbacks?.onRetryFinished?.(false, attempt)` hit: an unguarded call
completes its arguments, a guarded one did not.
Run both spellings through the same completion. It evaluates inside the
guard, so a nullish callee still evaluates no argument, and a typed rest
now packs from the guarded call too.
Corpus: optional-chained callbacks with a trailing optional parameter and
with a typed rest, through a present, absent, and undefined receiver.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit f574073e2392ec24cd2cfca61651759d3359e603)
This commit is contained in:
@@ -1,5 +1,4 @@
|
||||
import { isNavigatorGlobal } from "../builtins/navigator.js";
|
||||
import { isSafeToDiscard } from "./evaluation-safety.js";
|
||||
import { completeFuncValueArgs } from "../call-arguments.js";
|
||||
import { dynUndefinedExpr } from "../../../ir/build.js";
|
||||
import * as ts from "../../ts7/adapter.js";
|
||||
@@ -449,24 +448,20 @@ export function lowerOptionalChain(
|
||||
// directly (no member dispatch exists to re-enter).
|
||||
if (dotNode === expr && ts.isCallExpression(expr)) {
|
||||
if (narrowed.kind !== "func") lowerer.badType(recvNode, lowerer.typeOf(recvNode));
|
||||
const params = narrowed.params;
|
||||
// Forwarded fixed tuples (`f?.(...args)`) complete into the slots.
|
||||
if (expr.arguments.nodes.some(ts.isSpreadElement)) {
|
||||
const completed = completeFuncValueArgs(lowerer, expr, narrowed, loc);
|
||||
const call: IrExpr = {
|
||||
kind: "callValue",
|
||||
callee: recvRef,
|
||||
args: completed,
|
||||
type: narrowed.ret,
|
||||
loc,
|
||||
};
|
||||
return lowerer.finishOptionalChain(expr, id, receiver, call, loc);
|
||||
}
|
||||
const args = expr.arguments.nodes.map((a, i) => lowerer.lowerExprExpecting(a, params[i]));
|
||||
// Effect-free surplus arguments (a dropped `void` slot) are ignored.
|
||||
while (narrowed.rest !== true && args.length > params.length && isSafeToDiscard(args.at(-1)!))
|
||||
args.pop();
|
||||
const body: IrExpr = { kind: "callValue", callee: recvRef, args, type: narrowed.ret, loc };
|
||||
// The same completed function-value ABI an unguarded call uses: an
|
||||
// omitted optional argument still reaches the callee as undefined
|
||||
// (`cb?.onRetryFinished?.(false, attempt)` must see its third
|
||||
// parameter), a forwarded tuple completes into the slots, and an
|
||||
// effect-free surplus argument is ignored. The completion runs INSIDE
|
||||
// the guard, so a nullish callee still evaluates no argument.
|
||||
const completed = completeFuncValueArgs(lowerer, expr, narrowed, loc);
|
||||
const body: IrExpr = {
|
||||
kind: "callValue",
|
||||
callee: recvRef,
|
||||
args: completed,
|
||||
type: narrowed.ret,
|
||||
loc,
|
||||
};
|
||||
return lowerer.finishOptionalChain(expr, id, receiver, body, loc);
|
||||
}
|
||||
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/function-value-arity-completion.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
// Calling a function value short is legal TypeScript whenever the omitted
|
||||
// suffix is optional: the native callee still receives every parameter, so
|
||||
// the call completes the missing slots with undefined exactly as the JS call
|
||||
// would.
|
||||
//
|
||||
// The same holds for a call through an optional-chained property
|
||||
// (`cb?.onRetryFinished?.(false, attempt)`), which evaluates the callee and
|
||||
// its arguments once, in order.
|
||||
|
||||
interface RetryCallbacks {
|
||||
onRetryFinished?: (success: boolean, attempt: number, finalError?: string) => void;
|
||||
onAttempt?: (attempt: number, ...notes: string[]) => string;
|
||||
}
|
||||
|
||||
function finish(callbacks: RetryCallbacks | undefined, ok: boolean, attempt: number): string[] {
|
||||
const log: string[] = [];
|
||||
const sink: RetryCallbacks = {
|
||||
onRetryFinished: (success, n, finalError) => {
|
||||
log.push(`finished ${success ? "ok" : "no"} ${n} ${finalError === undefined ? "none" : finalError}`);
|
||||
},
|
||||
onAttempt: (n, ...notes) => `${n}:${notes.join("+")}`,
|
||||
};
|
||||
sink.onRetryFinished?.(ok, attempt);
|
||||
sink.onRetryFinished?.(ok, attempt, "boom");
|
||||
callbacks?.onRetryFinished?.(ok, attempt + 1);
|
||||
callbacks?.onAttempt?.(attempt, "a", "b");
|
||||
return log;
|
||||
}
|
||||
|
||||
console.log(finish(undefined, true, 1).join("; "));
|
||||
console.log(
|
||||
finish(
|
||||
{
|
||||
onRetryFinished: (success, attempt, finalError) => {
|
||||
console.log(`callback ${success} ${attempt} ${finalError ?? "-"}`);
|
||||
},
|
||||
onAttempt: (attempt, ...notes) => {
|
||||
console.log(`attempt ${attempt} ${notes.length}`);
|
||||
return "ignored";
|
||||
},
|
||||
},
|
||||
false,
|
||||
2,
|
||||
).join("; "),
|
||||
);
|
||||
|
||||
// An omitted trailing argument for a callback whose parameter is optional in
|
||||
// the middle of a longer signature.
|
||||
type Reporter = (message: string, code?: number, detail?: string) => string;
|
||||
|
||||
function report(to: Reporter): void {
|
||||
console.log(to("first"));
|
||||
console.log(to("second", 42));
|
||||
console.log(to("third", undefined, "why"));
|
||||
}
|
||||
|
||||
report((message, code, detail) => `${message}/${code ?? 0}/${detail ?? "-"}`);
|
||||
@@ -0,0 +1 @@
|
||||
{ "plain": 5094, "san": 6883 }
|
||||
Reference in New Issue
Block a user