mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-02 00:25:34 +08:00
fix: distinguish arguments property names from reads (#475)
* fix: distinguish arguments property names from reads - Share one runtime arguments scan across type mapping and call lowering. - Add Node differential coverage for names in keys, types, bindings, and labels. Fixes #431 Co-authored-by: Artem Gurtovoi <1233869+temich@users.noreply.github.com> * test: record TypeScript 7 parity for arguments properties - Add the new corpus case to the preflight and module order baseline. Fixes #431 Co-authored-by: Artem Gurtovoi <1233869+temich@users.noreply.github.com> --------- Co-authored-by: Artem Gurtovoi <1233869+temich@users.noreply.github.com>
This commit is contained in:
co-authored by
Artem Gurtovoi
parent
0ad3fc7d43
commit
c1634d4167
@@ -0,0 +1,34 @@
|
||||
import * as ts from "./ts7/adapter.js";
|
||||
|
||||
/** Whether an identifier in a function body reads the runtime arguments object. */
|
||||
function isArgumentsRead(id: ts.Identifier): boolean {
|
||||
const parent = id.parent;
|
||||
// A shorthand object property reads its value, but other `name` positions
|
||||
// declare a binding or spell a property (including `o.arguments`).
|
||||
if (ts.isShorthandPropertyAssignment(parent)) return true;
|
||||
if ((parent as ts.Node & { name?: ts.Node }).name === id) return false;
|
||||
if (ts.isBindingElement(parent) && parent.propertyName === id) return false;
|
||||
if (ts.isLabeledStatement(parent) && parent.label === id) return false;
|
||||
if ((ts.isBreakStatement(parent) || ts.isContinueStatement(parent)) && parent.label === id) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
/** Does this function's own body read `arguments`? Nested plain functions and
|
||||
* methods own theirs; arrows inherit the enclosing one. */
|
||||
export function bodyReadsArguments(fn: { body?: ts.Node | undefined }): boolean {
|
||||
if (fn.body === undefined) return false;
|
||||
let found = false;
|
||||
// Iterative walking preserves the frontend's nesting fence on deep trees.
|
||||
ts.walkPreorder(fn.body, (node) => {
|
||||
if (ts.isTypeNode(node)) return "skip";
|
||||
if (ts.isIdentifier(node) && node.text === "arguments" && isArgumentsRead(node)) {
|
||||
found = true;
|
||||
return "stop";
|
||||
}
|
||||
if (ts.isFunctionLike(node) && !ts.isArrowFunction(node) && node !== fn) {
|
||||
return "skip";
|
||||
}
|
||||
return undefined;
|
||||
});
|
||||
return found;
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import { InternalCompilerError } from "../../errors.js";
|
||||
* function/lambda lowering and signature collection, and monomorphizing
|
||||
* generic instantiation (bounded by MAX_GENERIC_INSTANCES). */
|
||||
import * as ts from "../ts7/adapter.js";
|
||||
import { bodyReadsArguments } from "../arguments-usage.js";
|
||||
import type { Lowerer } from "./lowerer.js";
|
||||
import { lowerGenMethodCall } from "./lower-generators.js";
|
||||
import { BIGINT_T, BOOL, CAUGHT, DYN, F64, IrExpr, IrFunction, IrLocal, IrParam, IrStmt, IrType, JSVAL, STRING, SYMBOL_T, SrcLoc, UNDEFINED_T, VOID, arrayOf, canBoxFuncIntoDyn, canConvertToDyn, canDynCheckTo, canMarshalTypedFuncIntoIsland, ffiClassType, ffiSourceParamTypes, funcOf, isFfiCallbackParam, isFfiContextParam, isFfiReleaseParam, isUnitType, shapeHasAccessorSlots, typeEquals } from "../../ir/ir.js";
|
||||
@@ -37,6 +38,8 @@ import { rejectStaticThis } from "./static-this.js";
|
||||
import { fenceNodeModuleMutationCall, lowerRequireCacheKeys } from "./lower-node-module.js";
|
||||
import { defaultAfterUndefined, lowerOptionalArgument, lowerStaticallyUndefinedArgument, positionNumber } from "./optional-arguments.js";
|
||||
|
||||
export { bodyReadsArguments };
|
||||
|
||||
/** How a parameter participates in CALL-SITE COMPLETION (the frontend
|
||||
* completes every call to the one full signature, so the IR and backends
|
||||
* stay count-exact — see docs/ir.md). `required` params must be passed;
|
||||
@@ -6865,33 +6868,6 @@ function loweredTemplateStrings(
|
||||
return { shapes, funcType };
|
||||
}
|
||||
|
||||
/** Does this function's OWN body read `arguments`? Nested plain functions
|
||||
* and methods have their own `arguments` (the walk skips them); arrows
|
||||
* see the enclosing one (the walk descends). Exported for the lowerer's
|
||||
* dynFallbackType: tsgo does not synthesize the `arguments` rest
|
||||
* parameter into inferred signatures (5.9.3 did — its param-count
|
||||
* mismatch was the detector), so the 7 world asks the BODY directly. */
|
||||
export function bodyReadsArguments(fn: { body?: ts.Node | undefined }): boolean {
|
||||
let found = false;
|
||||
if (fn.body === undefined) return false;
|
||||
// Iterative walk (walkPreorder): function bodies can hold pathologically
|
||||
// deep expression chains that a recursive visit would die on.
|
||||
ts.walkPreorder(fn.body, (n) => {
|
||||
if (ts.isIdentifier(n) && n.text === "arguments" && !(ts.isPropertyAccessExpression(n.parent) && n.parent.name === n)) {
|
||||
found = true;
|
||||
return "stop";
|
||||
}
|
||||
if (
|
||||
(ts.isFunctionExpression(n) || ts.isFunctionDeclaration(n) || ts.isMethodDeclaration(n)) &&
|
||||
n !== fn
|
||||
) {
|
||||
return "skip"; // own `arguments` scope
|
||||
}
|
||||
return undefined;
|
||||
});
|
||||
return found;
|
||||
}
|
||||
|
||||
/** Lifts an arrow function / function expression / nested declaration /
|
||||
* object-literal shorthand method to a module-level function and yields
|
||||
* the `closure` expression creating it. */
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { InternalCompilerError } from "../errors.js";
|
||||
import * as ts from "./ts7/adapter.js";
|
||||
import { bodyReadsArguments } from "./arguments-usage.js";
|
||||
import type { IrRecordShape, IrType, IrUnionDef } from "../ir/ir.js";
|
||||
import { arrayOf, BOOL, bytesOf, canConvertToDyn, CHILD_T, CRYPTOHASH_T, CRYPTOHMAC_T, DATE_T, DYN, F64, funcOf, isSupportedArrayElem, isSupportedIndexValue, isSupportedMapKey, isSupportedMapValue, isSupportedSetElem, isUnitType, JSVAL, mapOf, NULL_T, PROCSTREAM_T, RUNTIME_EMITTER_CLASS, RUNTIME_ERROR_CLASSES, RUNTIME_STREAM_CLASSES, setOf, STRING, SYMBOL_T, typeEquals, typeKey, UNDEFINED_T, VOID } from "../ir/ir.js";
|
||||
import { BIGINT_T } from "../ir/ir.js";
|
||||
@@ -752,30 +753,6 @@ export interface TypeMapperCtx {
|
||||
* embedded engine there. */
|
||||
moduleNamespaceId?: (type: ts.Type) => string | null;
|
||||
}
|
||||
|
||||
|
||||
/** lower-calls.ts's bodyReadsArguments, duplicated here (type-mapper.ts must not
|
||||
* import from lowering/ — that edge is a module cycle): does the function's
|
||||
* OWN body read `arguments`? Nested plain functions/methods own theirs
|
||||
* (skipped); arrows see the enclosing one (descended). */
|
||||
function bodyReadsArgumentsLocal(fn: { body?: ts.Node | undefined }): boolean {
|
||||
let found = false;
|
||||
if (fn.body === undefined) return false;
|
||||
// Iterative walk (walkPreorder): function bodies can hold pathologically
|
||||
// deep expression chains that a recursive visit would die on.
|
||||
ts.walkPreorder(fn.body, (n) => {
|
||||
if (ts.isIdentifier(n) && n.text === "arguments" && !(ts.isPropertyAccessExpression(n.parent) && n.parent.name === n)) {
|
||||
found = true;
|
||||
return "stop";
|
||||
}
|
||||
if ((ts.isFunctionExpression(n) || ts.isFunctionDeclaration(n) || ts.isMethodDeclaration(n)) && (n as unknown) !== fn) {
|
||||
return "skip"; // own `arguments` scope
|
||||
}
|
||||
return undefined;
|
||||
});
|
||||
return found;
|
||||
}
|
||||
|
||||
/** A generator type's normalized value channels (Generator<T, TReturn,
|
||||
* TNext> and the IteratorResult alias share this):
|
||||
* - yield: `never` (a generator that never yields) rides the VOID
|
||||
@@ -2592,7 +2569,7 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null {
|
||||
// tsgo never SYNTHESIZES that rest param into the inferred signature
|
||||
// (5.9.3 did — the count mismatch above was the whole detector
|
||||
// there), so the declaration's own body answers directly.
|
||||
if (sigDecl !== undefined && ts.isFunctionLike(sigDecl) && bodyReadsArgumentsLocal(sigDecl as { body?: ts.Node })) {
|
||||
if (sigDecl !== undefined && ts.isFunctionLike(sigDecl) && bodyReadsArguments(sigDecl as { body?: ts.Node })) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -3975,7 +3952,7 @@ export function describeComponentBlocker(widened: ts.Type, ctx: TypeMapperCtx):
|
||||
sigDecl !== undefined &&
|
||||
ts.isFunctionLike(sigDecl) &&
|
||||
(sigDecl.parameters.length !== sig.getParameters().length ||
|
||||
bodyReadsArgumentsLocal(sigDecl as { body?: ts.Node }))
|
||||
bodyReadsArguments(sigDecl as { body?: ts.Node }))
|
||||
) {
|
||||
return `the function shape is supported, but its signature is variadic ('arguments'-reading), and a compiled signature is fixed-arity`;
|
||||
}
|
||||
|
||||
@@ -7195,6 +7195,12 @@
|
||||
],
|
||||
"diags": []
|
||||
},
|
||||
"<repo>/tests/corpus/3059-arguments-property-names.ts": {
|
||||
"order": [
|
||||
"<repo>/tests/corpus/3059-arguments-property-names.ts"
|
||||
],
|
||||
"diags": []
|
||||
},
|
||||
"<repo>/tests/corpus/400-fib.ts": {
|
||||
"order": [
|
||||
"<repo>/tests/corpus/400-fib.ts"
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
// Property names in an arrow body are not reads of the enclosing arguments object.
|
||||
interface Options { keys: string[]; arguments: string[] }
|
||||
|
||||
const make = (): { arguments: number } => ({ arguments: 1 });
|
||||
console.log("literal", make().arguments);
|
||||
|
||||
const count = (options: Options): number => options.keys.length + options.arguments.length;
|
||||
const later = (): number => count({ keys: ["a"], arguments: ["b", "c"] });
|
||||
console.log("options", later());
|
||||
|
||||
const unpack = (): number => {
|
||||
const { arguments: value } = { arguments: 4 };
|
||||
return value;
|
||||
};
|
||||
console.log("binding", unpack());
|
||||
|
||||
const typed = (): number => {
|
||||
const value: { arguments: number } = { arguments: 5 };
|
||||
return value.arguments;
|
||||
};
|
||||
console.log("type", typed());
|
||||
|
||||
const withMethod = (): number => {
|
||||
const value = { arguments(): number { return 6; } };
|
||||
return value.arguments();
|
||||
};
|
||||
console.log("method", withMethod());
|
||||
|
||||
const withLabel = (): number => {
|
||||
let result = 0;
|
||||
arguments: {
|
||||
result = 7;
|
||||
break arguments;
|
||||
}
|
||||
return result;
|
||||
};
|
||||
console.log("label", withLabel());
|
||||
Reference in New Issue
Block a user