mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-27 06:43:19 +08:00
Merge pull request #772 from vercel-labs/fix/method-census-data-writes
Fix compiling tsc-ts: keep data writes out of the method replacement census
This commit is contained in:
@@ -3,6 +3,26 @@ import { literalValues } from "../literal-values.js";
|
|||||||
import { isJsSourceFile } from "../program.js";
|
import { isJsSourceFile } from "../program.js";
|
||||||
import type { Lowerer } from "./lowerer.js";
|
import type { Lowerer } from "./lowerer.js";
|
||||||
|
|
||||||
|
/** Members every function value has (Function.prototype and
|
||||||
|
* Object.prototype, plus own `length`/`name`/`prototype`). */
|
||||||
|
const FUNCTION_MEMBER_NAMES = new Set([
|
||||||
|
"apply",
|
||||||
|
"arguments",
|
||||||
|
"bind",
|
||||||
|
"call",
|
||||||
|
"caller",
|
||||||
|
"constructor",
|
||||||
|
"hasOwnProperty",
|
||||||
|
"isPrototypeOf",
|
||||||
|
"length",
|
||||||
|
"name",
|
||||||
|
"propertyIsEnumerable",
|
||||||
|
"prototype",
|
||||||
|
"toLocaleString",
|
||||||
|
"toString",
|
||||||
|
"valueOf",
|
||||||
|
]);
|
||||||
|
|
||||||
/** Census observable slots before choosing native calls. Names are shared
|
/** Census observable slots before choosing native calls. Names are shared
|
||||||
* conservatively across classes: aliases cannot hide a named replacement. */
|
* conservatively across classes: aliases cannot hide a named replacement. */
|
||||||
export function collectClassMethodMutations(
|
export function collectClassMethodMutations(
|
||||||
@@ -17,6 +37,7 @@ export function collectClassMethodMutations(
|
|||||||
receiver: ts.Expression | undefined;
|
receiver: ts.Expression | undefined;
|
||||||
site: ts.Node;
|
site: ts.Node;
|
||||||
}[] = [];
|
}[] = [];
|
||||||
|
const propertyWrites: { target: ts.PropertyAccessExpression; site: ts.Node }[] = [];
|
||||||
const reflection: ts.PropertyAccessExpression[] = [];
|
const reflection: ts.PropertyAccessExpression[] = [];
|
||||||
const globals: ts.Identifier[] = [];
|
const globals: ts.Identifier[] = [];
|
||||||
const jsAssignments: (ts.PropertyAccessExpression | ts.ElementAccessExpression)[] = [];
|
const jsAssignments: (ts.PropertyAccessExpression | ts.ElementAccessExpression)[] = [];
|
||||||
@@ -88,26 +109,53 @@ export function collectClassMethodMutations(
|
|||||||
lowerer.stdlibGlobalMember(value.expression, "Object") === "create"
|
lowerer.stdlibGlobalMember(value.expression, "Object") === "create"
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
const primitiveDictionary = (value: ts.Expression): boolean => {
|
// Whether no function value can inhabit `type`. Primitives qualify, and so
|
||||||
const primitive = (type: ts.Type): boolean =>
|
// do non-callable object types a function is not assignable to: one that
|
||||||
type.isUnionType()
|
// requires a property functions lack, or a weak (all-optional) type that
|
||||||
? ts.constituentTypes(type).every(primitive)
|
// shares no property with functions (TypeScript's weak-type rule rejects
|
||||||
: (type.flags &
|
// callable sources too).
|
||||||
(ts.TypeFlags.StringLike |
|
const functionFree = (type: ts.Type): boolean => {
|
||||||
ts.TypeFlags.NumberLike |
|
if (type.isUnionType()) return ts.constituentTypes(type).every(functionFree);
|
||||||
ts.TypeFlags.BooleanLike |
|
if (
|
||||||
ts.TypeFlags.BigIntLike |
|
(type.flags &
|
||||||
ts.TypeFlags.Null |
|
(ts.TypeFlags.StringLike |
|
||||||
ts.TypeFlags.Undefined |
|
ts.TypeFlags.NumberLike |
|
||||||
ts.TypeFlags.ESSymbolLike)) !==
|
ts.TypeFlags.BooleanLike |
|
||||||
0;
|
ts.TypeFlags.BigIntLike |
|
||||||
return lowerer.checker
|
ts.TypeFlags.Null |
|
||||||
|
ts.TypeFlags.Undefined |
|
||||||
|
ts.TypeFlags.ESSymbolLike)) !==
|
||||||
|
0
|
||||||
|
)
|
||||||
|
return true;
|
||||||
|
if (
|
||||||
|
!(type.flags & ts.TypeFlags.Object) ||
|
||||||
|
lowerer.checker.getCallSignatures(type).length > 0 ||
|
||||||
|
lowerer.checker.getConstructSignatures(type).length > 0
|
||||||
|
)
|
||||||
|
return false;
|
||||||
|
const properties = lowerer.checker.getPropertiesOfType(type);
|
||||||
|
if (properties.length === 0) return false;
|
||||||
|
const required = properties.filter(
|
||||||
|
(property) => (property.flags & ts.SymbolFlags.Optional) === 0,
|
||||||
|
);
|
||||||
|
if (required.some((property) => !FUNCTION_MEMBER_NAMES.has(property.name))) return true;
|
||||||
|
return (
|
||||||
|
required.length === 0 &&
|
||||||
|
properties.every((property) => !FUNCTION_MEMBER_NAMES.has(property.name))
|
||||||
|
);
|
||||||
|
};
|
||||||
|
// A string index whose values cannot be functions excludes callable
|
||||||
|
// method slots. Class instances have no implicit index signature, so such
|
||||||
|
// a dictionary is only a class view when the class declares that index
|
||||||
|
// signature, and then every method must be assignable to its value type.
|
||||||
|
const functionFreeDictionary = (value: ts.Expression): boolean =>
|
||||||
|
lowerer.checker
|
||||||
.getIndexInfosOfType(lowerer.typeOf(value))
|
.getIndexInfosOfType(lowerer.typeOf(value))
|
||||||
.some(
|
.some(
|
||||||
(index) =>
|
(index) =>
|
||||||
(index.keyType.flags & ts.TypeFlags.StringLike) !== 0 && primitive(index.valueType),
|
(index.keyType.flags & ts.TypeFlags.StringLike) !== 0 && functionFree(index.valueType),
|
||||||
);
|
);
|
||||||
};
|
|
||||||
const literalLoopKeys = (key: ts.Expression): (string | number)[] | null => {
|
const literalLoopKeys = (key: ts.Expression): (string | number)[] | null => {
|
||||||
if (!ts.isIdentifier(key)) return null;
|
if (!ts.isIdentifier(key)) return null;
|
||||||
const symbol = lowerer.checker.getSymbolAtLocation(key);
|
const symbol = lowerer.checker.getSymbolAtLocation(key);
|
||||||
@@ -201,8 +249,15 @@ export function collectClassMethodMutations(
|
|||||||
};
|
};
|
||||||
const markTarget = (target: ts.Expression, site: ts.Node, arrow = false): void => {
|
const markTarget = (target: ts.Expression, site: ts.Node, arrow = false): void => {
|
||||||
target = unwrap(target);
|
target = unwrap(target);
|
||||||
if (ts.isPropertyAccessExpression(target)) markName(target.name.text, site, arrow);
|
if (ts.isPropertyAccessExpression(target)) {
|
||||||
else if (ts.isElementAccessExpression(target))
|
const name = target.name.text;
|
||||||
|
// A named write to a method name is judged by the written property's
|
||||||
|
// type once types are prefetched (see propertyWrites below).
|
||||||
|
if (methods.has(name) && name !== "prototype" && name !== "__proto__") {
|
||||||
|
if (!arrow) receiverFields.add(name);
|
||||||
|
propertyWrites.push({ target, site });
|
||||||
|
} else markName(name, site, arrow);
|
||||||
|
} else if (ts.isElementAccessExpression(target))
|
||||||
markKey(target.argumentExpression, site, target.expression);
|
markKey(target.argumentExpression, site, target.expression);
|
||||||
else if (ts.isArrayLiteralExpression(target)) {
|
else if (ts.isArrayLiteralExpression(target)) {
|
||||||
for (const element of target.elements) {
|
for (const element of target.elements) {
|
||||||
@@ -392,6 +447,20 @@ export function collectClassMethodMutations(
|
|||||||
...(write.receiver ? [write.receiver] : []),
|
...(write.receiver ? [write.receiver] : []),
|
||||||
]),
|
]),
|
||||||
);
|
);
|
||||||
|
// A named write can replace a method only when the written property can
|
||||||
|
// hold a function: a class instance seen through the receiver's type must
|
||||||
|
// have its method assignable to that property's type. Data fields (an AST
|
||||||
|
// node, a string, a JSON record) cannot shadow a same-named method of an
|
||||||
|
// unrelated class, so they stay out of the observed-slot census.
|
||||||
|
lowerer.checker.prefetchClassCollection(
|
||||||
|
propertyWrites.map((write) => write.target),
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
for (const { target, site } of propertyWrites) {
|
||||||
|
const name = target.name.text;
|
||||||
|
if (lowerer.prototypeMethodAccesses.has(name) || functionFree(lowerer.typeOf(target))) continue;
|
||||||
|
lowerer.prototypeMethodAccesses.set(name, site);
|
||||||
|
}
|
||||||
for (const { key, receiver, site } of computedWrites) {
|
for (const { key, receiver, site } of computedWrites) {
|
||||||
const type = key ? lowerer.typeOf(key) : undefined;
|
const type = key ? lowerer.typeOf(key) : undefined;
|
||||||
const names = type && (literalValues(type) ?? (key ? literalLoopKeys(key) : null));
|
const names = type && (literalValues(type) ?? (key ? literalLoopKeys(key) : null));
|
||||||
@@ -406,9 +475,10 @@ export function collectClassMethodMutations(
|
|||||||
unknownOwnField = true;
|
unknownOwnField = true;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
// A primitive-only string index excludes callable method slots. Unlike
|
// A string index whose values cannot be functions excludes callable
|
||||||
// structural interfaces, it cannot be a live class-method view.
|
// method slots. Unlike structural interfaces, it cannot be a live
|
||||||
if (receiver && (freshReceiver(receiver) || primitiveDictionary(receiver))) continue;
|
// class-method view.
|
||||||
|
if (receiver && (freshReceiver(receiver) || functionFreeDictionary(receiver))) continue;
|
||||||
const flags = type?.flags ?? ts.TypeFlags.Unknown;
|
const flags = type?.flags ?? ts.TypeFlags.Unknown;
|
||||||
if (flags & ts.TypeFlags.NumberLike) {
|
if (flags & ts.TypeFlags.NumberLike) {
|
||||||
// Numeric writes can replace numeric names but not ordinary names.
|
// Numeric writes can replace numeric names but not ordinary names.
|
||||||
|
|||||||
@@ -174,9 +174,17 @@ export function classPrototypeData(
|
|||||||
const helper = lowerer.liftedFns.find((fn) => fn.name === info.def.prototypeDataHelper)!;
|
const helper = lowerer.liftedFns.find((fn) => fn.name === info.def.prototypeDataHelper)!;
|
||||||
const value = varRef(`%g.${info.def.prototypeDataHelper}`, DYN, loc);
|
const value = varRef(`%g.${info.def.prototypeDataHelper}`, DYN, loc);
|
||||||
// Materialize only observed method slots. Own declarations stop lookup
|
// Materialize only observed method slots. Own declarations stop lookup
|
||||||
// at the correct prototype even if an ancestor is replaced later.
|
// at the correct prototype even if an ancestor is replaced later. An
|
||||||
|
// abstract declaration emits no prototype member in JavaScript, so it
|
||||||
|
// gets no slot: lookup continues to the ancestors, and the concrete
|
||||||
|
// descendants' prototypes carry the implementations.
|
||||||
for (const [method, access] of lowerer.prototypeMethodAccesses) {
|
for (const [method, access] of lowerer.prototypeMethodAccesses) {
|
||||||
if (!info.methods.has(method) || method.startsWith("get:") || method.startsWith("set:"))
|
if (
|
||||||
|
!info.methods.has(method) ||
|
||||||
|
info.methods.get(method)?.abstract ||
|
||||||
|
method.startsWith("get:") ||
|
||||||
|
method.startsWith("set:")
|
||||||
|
)
|
||||||
continue;
|
continue;
|
||||||
const compiled = classMethodValue(lowerer, access, info, method, locOf(access));
|
const compiled = classMethodValue(lowerer, access, info, method, locOf(access));
|
||||||
if (!compiled) continue;
|
if (!compiled) continue;
|
||||||
|
|||||||
@@ -14867,6 +14867,12 @@
|
|||||||
],
|
],
|
||||||
"diags": []
|
"diags": []
|
||||||
},
|
},
|
||||||
|
"<repo>/tests/corpus/3171-method-census-data-writes.ts": {
|
||||||
|
"order": [
|
||||||
|
"<repo>/tests/corpus/3171-method-census-data-writes.ts"
|
||||||
|
],
|
||||||
|
"diags": []
|
||||||
|
},
|
||||||
"<repo>/tests/corpus/4032-inline-rc-fast-paths.ts": {
|
"<repo>/tests/corpus/4032-inline-rc-fast-paths.ts": {
|
||||||
"order": [
|
"order": [
|
||||||
"<repo>/tests/corpus/4032-inline-rc-fast-paths.ts"
|
"<repo>/tests/corpus/4032-inline-rc-fast-paths.ts"
|
||||||
|
|||||||
@@ -0,0 +1,90 @@
|
|||||||
|
// Writes that cannot hold a function never replace a class method, so they
|
||||||
|
// must not turn same-named method calls elsewhere into dynamic dispatch:
|
||||||
|
// a string-keyed dictionary of data records, and constructor writes to data
|
||||||
|
// fields that share a name with an unrelated class's method.
|
||||||
|
interface Entry {
|
||||||
|
name: string;
|
||||||
|
weight: number;
|
||||||
|
aliases: Map<string, number> | undefined;
|
||||||
|
}
|
||||||
|
interface Changes {
|
||||||
|
changed?: string[];
|
||||||
|
deleted?: string[];
|
||||||
|
}
|
||||||
|
type Json =
|
||||||
|
| { type: "null" }
|
||||||
|
| { type: "boolean"; value: boolean }
|
||||||
|
| { type: "object"; entries: Map<string, Json> };
|
||||||
|
|
||||||
|
class Registry {
|
||||||
|
private readonly byName = new Map<string, Entry>();
|
||||||
|
constructor(entries: readonly Entry[]) {
|
||||||
|
for (const entry of entries) this.byName.set(entry.name.toLowerCase(), entry);
|
||||||
|
}
|
||||||
|
get(name: string): Entry | undefined {
|
||||||
|
return this.byName.get(name.toLowerCase());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Scanner {
|
||||||
|
private pos = 0;
|
||||||
|
private readonly text: string;
|
||||||
|
constructor(text: string) {
|
||||||
|
this.text = text;
|
||||||
|
}
|
||||||
|
literal(word: string, value: Json): Json | undefined {
|
||||||
|
if (!this.text.startsWith(word, this.pos)) return undefined;
|
||||||
|
this.pos += word.length;
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Token {
|
||||||
|
readonly text: string;
|
||||||
|
constructor(text: string) {
|
||||||
|
this.text = text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
class LiteralNode {
|
||||||
|
literal: Token;
|
||||||
|
value: string;
|
||||||
|
constructor(literal: Token) {
|
||||||
|
this.literal = literal;
|
||||||
|
this.value = literal.text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function collect(ids: readonly string[]): Record<string, Changes> | undefined {
|
||||||
|
let changes: Record<string, Changes> | undefined;
|
||||||
|
for (const id of ids) {
|
||||||
|
changes ??= {};
|
||||||
|
changes[id] = { changed: [id] };
|
||||||
|
}
|
||||||
|
return changes;
|
||||||
|
}
|
||||||
|
|
||||||
|
function weightOf(entry: Entry | undefined): number {
|
||||||
|
return entry === undefined ? -1 : entry.weight;
|
||||||
|
}
|
||||||
|
function show(json: Json | undefined): string {
|
||||||
|
if (json === undefined) return "none";
|
||||||
|
if (json.type === "object") return `object ${json.entries.size}`;
|
||||||
|
return json.type === "boolean" ? `boolean ${json.value}` : "null";
|
||||||
|
}
|
||||||
|
|
||||||
|
const registry = new Registry([
|
||||||
|
{ name: "Alpha", weight: 3, aliases: new Map([["a", 1]]) },
|
||||||
|
{ name: "beta", weight: 5, aliases: undefined },
|
||||||
|
]);
|
||||||
|
console.log(weightOf(registry.get("alpha")), weightOf(registry.get("BETA")), weightOf(registry.get("gamma")));
|
||||||
|
const scanner = new Scanner("truenull");
|
||||||
|
console.log(show(scanner.literal("true", { type: "boolean", value: true })));
|
||||||
|
console.log(show(scanner.literal("true", { type: "boolean", value: true })));
|
||||||
|
console.log(show(scanner.literal("null", { type: "null" })));
|
||||||
|
console.log(show(new Scanner("{}").literal("{}", { type: "object", entries: new Map() })));
|
||||||
|
console.log(registry.get("alpha")?.aliases?.get("a"), registry.get("beta")?.aliases);
|
||||||
|
const node = new LiteralNode(new Token("42"));
|
||||||
|
console.log(node.literal.text, node.value);
|
||||||
|
const changes = collect(["p1", "p2"]);
|
||||||
|
for (const [id, change] of Object.entries(changes ?? {})) console.log(id, change.changed?.join(","), change.deleted);
|
||||||
|
console.log(collect([]));
|
||||||
Reference in New Issue
Block a user