mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-11-02 07:43:42 +08:00
Accept a wider lambda whose own signature is already its completed ABI
A lambda declaring an optional or defaulted trailing parameter — the `(a, b, loose?) => …` semver comparator, the sort callback whose surplus position is a flag — was refused at its birth fence (SC1090) even though every read of it spells that position as `T | undefined`, the completed native ABI. The fence compared the slot's mapped type against the contextual lambda's, which misses two shapes the checker accepts: a lambda whose own `any` parameter mapType leaves unmapped, and a *contextually* typed lambda whose OWN signature is wider than the slot it is written under. requireExactArityValue now admits a lambda whose own call signature maps to the slot position for position, and cleanFuncAdaptable accepts a source with extra trailing parameters when each of them completes with the undefined value, the same absence a call through the slot passes. `sort`/`toSorted` separately required a comparator of at most two parameters; it now adapts the first two and lets the surplus ones complete as undefined, so the imported three-parameter comparator sorts. Corpus 8529 pins the surplus-parameter comparators (optional, defaulted, `any`-typed), the direct calls through them, `toSorted`'s untouched receiver, and remains byte-exact against Node. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
49b133751c
commit
5afe93093f
@@ -630,6 +630,41 @@ export function checkDefaultParamBodyType(
|
||||
}
|
||||
}
|
||||
|
||||
/** Whether a lambda's OWN signature maps to its completed ABI position for
|
||||
* position — the contract its reads carry (mapType spells `x?: T` as the
|
||||
* `T | undefined` slot). Two positions can escape the mapped-TYPE
|
||||
* comparison in requireExactArityValue: a parameter the checker spells
|
||||
* `any` holds the checked-dynamic value that mapType leaves unmapped when
|
||||
* its spelling is context-free, and a *contextually* typed lambda's own
|
||||
* signature need not be the slot's (a comparator with a surplus optional
|
||||
* parameter under a two-parameter slot). */
|
||||
function lambdaSpellsCompletedAbi(
|
||||
lowerer: Lowerer,
|
||||
lambda: ts.ArrowFunction | ts.FunctionExpression,
|
||||
funcType: IrType,
|
||||
): boolean {
|
||||
if (funcType.kind !== "func" || funcType.rest === true) return false;
|
||||
const sigs = lowerer.checker.getCallSignatures(lowerer.typeOf(lambda));
|
||||
if (sigs.length !== 1) return false;
|
||||
const sig = sigs[0]!;
|
||||
const params = sig.getParameters();
|
||||
if (params.length !== funcType.params.length) return false;
|
||||
for (let i = 0; i < params.length; i++) {
|
||||
const pTs = lowerer.checker.getTypeOfSymbol(params[i]!);
|
||||
const slot = funcType.params[i]!;
|
||||
if ((pTs.flags & ts.TypeFlags.Any) !== 0) {
|
||||
if (slot.kind !== "dyn") return false;
|
||||
continue;
|
||||
}
|
||||
const mappedParam = lowerer.mapTypeOf(pTs);
|
||||
if (!mappedParam || !typeEquals(mappedParam, slot)) return false;
|
||||
}
|
||||
const retTs = lowerer.checker.getReturnTypeOfSignature(sig);
|
||||
if ((retTs.flags & ts.TypeFlags.Any) !== 0) return funcType.ret.kind === "dyn";
|
||||
const mappedRet = lowerer.mapTypeOf(retTs);
|
||||
return mappedRet !== null && typeEquals(mappedRet, funcType.ret);
|
||||
}
|
||||
|
||||
/** DECISION (docs/ir.md): function VALUES carry one completed native ABI.
|
||||
* Optional/default parameters appear as literal `T | undefined` slots;
|
||||
* typed rest parameters appear as one trailing typed-array slot plus the
|
||||
@@ -675,6 +710,20 @@ export function requireExactArityValue(
|
||||
? lowerer.mapTypeOf(lowerer.typeOf(contextual))
|
||||
: null;
|
||||
if (mapped && typeEquals(mapped, funcType)) return;
|
||||
// A lambda whose OWN signature already spells the completed ABI is a
|
||||
// value: every read of it types it that way, and its slot converts it
|
||||
// (the surplus-parameter and undefined-arm adapters). The mapped-type
|
||||
// comparison above misses two such lambdas — one whose `any` parameter
|
||||
// mapType leaves unmapped (`(a: any, b: any, loose?: boolean) => number`
|
||||
// beside a two-parameter slot), and one whose signature is wider than
|
||||
// the contextual slot it is written under.
|
||||
if (
|
||||
contextual !== null &&
|
||||
(ts.isArrowFunction(contextual) || ts.isFunctionExpression(contextual)) &&
|
||||
lambdaSpellsCompletedAbi(lowerer, contextual, funcType)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
// An 'any' slot (`C.prototype.m = function (a, b) {…}` through an
|
||||
// untyped receiver), or a slot whose type maps to a checked-dynamic
|
||||
// value (a member of a literal lowered with a generic function's type
|
||||
|
||||
@@ -114,10 +114,22 @@ export function coercibleValue(lowerer: Lowerer, src: IrType, dst: IrType): bool
|
||||
return false;
|
||||
}
|
||||
|
||||
/** Whether an omitted parameter position completes with the native
|
||||
* undefined value: a checked-dynamic slot takes undefined as-is, and an
|
||||
* undefined-armed union (`x?: T`, `x: T = e`) is the completed ABI
|
||||
* spelling. */
|
||||
export function omittableTrailingParam(lowerer: Lowerer, type: IrType): boolean {
|
||||
if (type.kind === "dyn" || typeEquals(type, UNDEFINED_T)) return true;
|
||||
return type.kind === "union" && lowerer.armTag(type.unionId, UNDEFINED_T) >= 0;
|
||||
}
|
||||
|
||||
/** Whether all parameters and the result adapt without a trap-only path.
|
||||
* Rest signatures are excluded, except a typed-rest source in a slot with
|
||||
* fixed parameters (the rest packing adapter); extra destination
|
||||
* parameters are ignored. */
|
||||
* parameters are ignored. A source with MORE parameters than the slot is
|
||||
* clean when every extra trailing one is omittable — the adapter passes
|
||||
* that absent value explicitly (funcCoerceAdapter's omittedParams), the
|
||||
* same way a call through the slot would. */
|
||||
export function cleanFuncAdaptable(
|
||||
lowerer: Lowerer,
|
||||
src: IrType & { kind: "func" },
|
||||
@@ -125,8 +137,10 @@ export function cleanFuncAdaptable(
|
||||
): boolean {
|
||||
if (restPackingAdapterFits(lowerer, src, dst)) return true;
|
||||
if (src.rest === true || dst.rest === true) return false;
|
||||
if (src.params.length > dst.params.length) return false;
|
||||
for (let i = 0; i < src.params.length; i++) {
|
||||
for (let i = dst.params.length; i < src.params.length; i++) {
|
||||
if (!omittableTrailingParam(lowerer, src.params[i]!)) return false;
|
||||
}
|
||||
for (let i = 0; i < src.params.length && i < dst.params.length; i++) {
|
||||
if (!lowerer.coercibleValue(dst.params[i]!, src.params[i]!)) return false;
|
||||
}
|
||||
if (dst.ret.kind === "void") return true;
|
||||
|
||||
@@ -70,6 +70,7 @@ import {
|
||||
import { dynamicReceiverThrows } from "./dynamic-receivers.js";
|
||||
import { defaultAfterUndefined, lowerStaticallyUndefinedArgument } from "../optional-arguments.js";
|
||||
import { checkedFilterNarrowing } from "./filter-predicates.js";
|
||||
import { omittableTrailingParam } from "../coercions/function-adapters.js";
|
||||
|
||||
/** `a.map(fn)` / `a.filter(fn)` / `a.forEach(fn)` desugar to a direct
|
||||
* call of a synthetic module function — one per method + element/result
|
||||
@@ -2657,11 +2658,15 @@ export function lowerArraySortCall(
|
||||
let fnArg =
|
||||
undefinedArg ?? lowerOptionalElementComparator(lowerer, argNode, unwrappedArg, elem, arrT);
|
||||
// Reusable JS comparators often accept checked values. Adapt the array's
|
||||
// element ABI just as other array callbacks do before sorting.
|
||||
// element ABI just as other array callbacks do before sorting. A
|
||||
// comparator declaring MORE parameters than sort passes (`(a, b, loose?)`,
|
||||
// the imported semver shape) receives the surplus ones as undefined — the
|
||||
// completion an ordinary two-argument call performs — so it adapts to the
|
||||
// (a, b) slot like any other wider signature.
|
||||
if (
|
||||
fnArg.type.kind === "func" &&
|
||||
fnArg.type.params.length <= 2 &&
|
||||
fnArg.type.params.every((param) => lowerer.coercibleValue(elem, param))
|
||||
fnArg.type.params.slice(2).every((param) => omittableTrailingParam(lowerer, param)) &&
|
||||
fnArg.type.params.slice(0, 2).every((param) => lowerer.coercibleValue(elem, param))
|
||||
) {
|
||||
// Arithmetic on inferred JS values retains a checked result because
|
||||
// it can produce BigInt. The sort callback still requires a number.
|
||||
@@ -2670,7 +2675,7 @@ export function lowerArraySortCall(
|
||||
? F64
|
||||
: fnArg.type.ret;
|
||||
const expected = funcOf(
|
||||
fnArg.type.params.map(() => elem),
|
||||
fnArg.type.params.slice(0, 2).map(() => elem),
|
||||
result,
|
||||
);
|
||||
if (!typeEquals(fnArg.type, expected)) fnArg = lowerer.coerceToExpected(fnArg, expected);
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/8529-sort-comparator-surplus-params.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
// A comparator may declare more parameters than `sort` passes: the surplus
|
||||
// positions receive the native undefined value, exactly as in a two-argument
|
||||
// call. The imported `(a, b, loose) => …` comparators and lambdas with an
|
||||
// optional or defaulted trailing parameter take this shape, and a lambda
|
||||
// whose own signature already spells its completed ABI is a value wherever
|
||||
// its slot converts.
|
||||
|
||||
type Comparer = (a: string, b: string, loose?: boolean) => number;
|
||||
|
||||
const seen: string[] = [];
|
||||
const cmp: Comparer = (a, b, loose) => {
|
||||
seen.push(loose === undefined ? "absent" : String(loose));
|
||||
return a < b ? -1 : a > b ? 1 : 0;
|
||||
};
|
||||
|
||||
const versions = ["1.9.0", "1.10.0", "1.2.0"];
|
||||
console.log([...versions].sort(cmp).join(","));
|
||||
console.log(seen.length > 0 && seen.every((entry) => entry === "absent"));
|
||||
console.log(versions.join(","));
|
||||
|
||||
// `toSorted` adapts the same way and leaves the receiver untouched.
|
||||
console.log(versions.toSorted(cmp).join(","), versions.join(","));
|
||||
|
||||
// Direct calls through the value complete the same parameters.
|
||||
console.log(cmp("a", "b"), cmp("b", "a"), cmp("a", "a", true));
|
||||
|
||||
// An inline comparator with an optional surplus parameter sorts by number.
|
||||
const nums = [3, 1, 2];
|
||||
const descending = (a: number, b: number, dir?: boolean): number =>
|
||||
dir === undefined ? b - a : a - b;
|
||||
console.log(nums.toSorted(descending).join(","));
|
||||
console.log([...nums].sort((a, b, dir?: boolean) => (dir ? a - b : b - a)).join(","));
|
||||
console.log(nums.join(","));
|
||||
|
||||
// A defaulted surplus parameter keeps its default when sort omits it.
|
||||
const withDefault = (a: string, b: string, scale = 1): number =>
|
||||
(a < b ? -1 : a > b ? 1 : 0) * scale;
|
||||
console.log(["b", "a", "c"].sort(withDefault).join(""));
|
||||
console.log(withDefault("a", "b"), withDefault("a", "b", 3));
|
||||
|
||||
// A one-parameter comparator is still a prefix of the slot.
|
||||
console.log(["d", "c", "a"].sort((left) => left.charCodeAt(0) - 97).join(","));
|
||||
@@ -0,0 +1 @@
|
||||
{ "plain": 3100, "san": 3700 }
|
||||
Reference in New Issue
Block a user