mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-11-09 14:53:16 +08:00
Analyze init factories once per call site
Static-object specialization's stage 3 prerequisite (DESIGN-constant-objects.md): a function called from module initialization (top-level code outside loops, or another such clone) is analyzed once per call site, up to 64 sites per function and 4 nested clones deep. The objects and arrays it creates are one allocation per call site, and each clone runs at most once, so they are singletons: the schemas zod's and valibot's builders create at init become constant objects instead of "init-factory" near misses. Stage 1 report (SCRIPTC_DEBUG_SHAPES=const): - zod 3.25 (z.object bench): constant objects 7 -> 48 (fields 57 -> 155), init-factory near misses 12 -> 1, foldable reads 22 -> 72. - valibot 1.1: constant objects 2 -> 5, init-factory 6 -> 0. The overlay's annotations and the zod and namespace benchmarks are unchanged. The overlay counts union arms rather than atoms, so the per-site allocations of one record shape stay one typedef. SCRIPTC_SHAPE_SITE_CLONES sets the per-function limit (0: off). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
fc3f0de981
commit
df02c72407
@@ -86,6 +86,8 @@ export interface LiteralSite {
|
||||
readonly hasComputedKey: boolean;
|
||||
readonly spreads: number[];
|
||||
readonly inPackage: boolean;
|
||||
/** The per-call-site clone that allocates it ("" outside clones). */
|
||||
readonly ctx: string;
|
||||
}
|
||||
|
||||
export interface Op {
|
||||
@@ -263,6 +265,11 @@ const PRIMITIVE_MEMBERS: Readonly<Record<"string" | "number" | "boolean", Readon
|
||||
boolean: new Set(["toString", "valueOf", "constructor"]),
|
||||
};
|
||||
|
||||
/** Per-call-site clones of an init factory, at most (SCRIPTC_SHAPE_SITE_CLONES;
|
||||
* 0 turns them off), and the deepest chain of nested clones. */
|
||||
const SITE_CLONES = Number(process.env["SCRIPTC_SHAPE_SITE_CLONES"] ?? 64);
|
||||
const SITE_CLONE_DEPTH = 4;
|
||||
|
||||
/** The most atoms a value node tracks before it widens to ⊤
|
||||
* (SCRIPTC_SHAPE_WIDEN overrides). */
|
||||
const WIDEN_AT = Number(process.env["SCRIPTC_SHAPE_WIDEN"] ?? 32);
|
||||
@@ -400,8 +407,9 @@ class Analyzer {
|
||||
* which may run after the call's own handling. */
|
||||
private readonly repeatingSites = new Set<Node>();
|
||||
|
||||
edgeFor(site: Node, callee: FunctionInfo): void {
|
||||
const ctx = this.siteCtx.get(site);
|
||||
edgeFor(site: Node, callee: FunctionInfo, args?: number[]): void {
|
||||
const ctx =
|
||||
(args !== undefined ? this.argsFrom.get(args) : undefined) ?? this.siteCtx.get(site);
|
||||
this.calls.push({
|
||||
caller: ctx?.caller ?? null,
|
||||
callee,
|
||||
@@ -1250,7 +1258,9 @@ class Analyzer {
|
||||
spread: boolean,
|
||||
site: Node,
|
||||
): void {
|
||||
const key = `ctor:${this.nodeKey(decl)}:${instance.id}:${this.nodeKey(site)}`;
|
||||
// The arguments' nodes too: one `new` site walked in several contexts
|
||||
// (receiver classes, per-site clones) passes each context's values.
|
||||
const key = `ctor:${this.nodeKey(decl)}:${instance.id}:${this.nodeKey(site)}:${args.join(",")}`;
|
||||
if (this.boundCalls.has(key)) return;
|
||||
this.boundCalls.add(key);
|
||||
const entry = this.ensureClass(decl);
|
||||
@@ -1263,7 +1273,7 @@ class Analyzer {
|
||||
this.activate(info);
|
||||
this.add(info.thisNode, instance);
|
||||
this.bindArgs(info, args, spread);
|
||||
this.edgeFor(site, info);
|
||||
this.edgeFor(site, info, args);
|
||||
return;
|
||||
}
|
||||
const base = this.baseClass(decl);
|
||||
@@ -1299,7 +1309,7 @@ class Analyzer {
|
||||
|
||||
/** Call of a function atom with a receiver node. */
|
||||
callFunction(
|
||||
info: FunctionInfo,
|
||||
callee: FunctionInfo,
|
||||
thisNode: number | null,
|
||||
args: number[],
|
||||
spread: boolean,
|
||||
@@ -1308,7 +1318,7 @@ class Analyzer {
|
||||
): void {
|
||||
// `() => Object.create(null)` helpers: each call site gets its own
|
||||
// dictionary, as if the helper were inlined.
|
||||
if (this.isFactory(info.node)) {
|
||||
if (this.isFactory(callee.node)) {
|
||||
const d = this.atoms.intern(`dict:${this.nodeKey(site)}`, () => ({
|
||||
kind: "dictionary",
|
||||
label: `Object.create@${locLabel(site)}`,
|
||||
@@ -1317,6 +1327,7 @@ class Analyzer {
|
||||
this.add(dst, d);
|
||||
return;
|
||||
}
|
||||
const info = this.siteClone(callee, site, args) ?? callee;
|
||||
this.activate(info);
|
||||
const fnNode = info.node;
|
||||
const key = `call:${info.ctx}|${this.nodeKey(fnNode)}:${this.nodeKey(site)}:${thisNode ?? -1}:${dst}`;
|
||||
@@ -1331,7 +1342,47 @@ class Analyzer {
|
||||
} else if (info.isGenerator) {
|
||||
this.add(dst, this.top);
|
||||
} else this.edge(info.ret, dst);
|
||||
this.edgeFor(site, info);
|
||||
this.edgeFor(site, info, args);
|
||||
}
|
||||
|
||||
/** Per-call-site clones of init factories: a function called from
|
||||
* module initialization (top-level code outside loops, or another such
|
||||
* clone) is analyzed once per call site, so the objects it creates are
|
||||
* one allocation per site (`z.string()` and `z.object(…)` schemas,
|
||||
* valibot's schema literals) instead of one per function. Clones run
|
||||
* at most once, which makes their objects singletons. */
|
||||
private readonly siteClones = new Map<Node, number>();
|
||||
/** The site-clone context a literal at `node` is allocated in, or "". */
|
||||
private siteCloneCtx(node: Node): string {
|
||||
const root = this.ctxRoot;
|
||||
if (!this.ctx.includes("@S") || root === null) return "";
|
||||
const inside =
|
||||
node.getSourceFile() === root.getSourceFile() && node.pos >= root.pos && node.end <= root.end;
|
||||
return inside ? this.ctx : "";
|
||||
}
|
||||
private siteClone(info: FunctionInfo, site: Node, args: number[]): FunctionInfo | null {
|
||||
if (SITE_CLONES === 0 || info.isGenerator || ts.isSourceFile(info.node)) return null;
|
||||
if (ts.isClassLikeDeclaration(info.node)) return null;
|
||||
const sc = this.argsFrom.get(args);
|
||||
if (sc === undefined || sc.inLoop || this.forceLoop || this.repeatingSites.has(site))
|
||||
return null;
|
||||
const caller = sc.caller;
|
||||
if (caller === null) return null;
|
||||
// The clone itself, not a closure created inside it (which inherits
|
||||
// its context but may run any number of times).
|
||||
const callerInit =
|
||||
ts.isSourceFile(caller.node) || (caller.ctx.includes("@S") && caller.root === caller.node);
|
||||
if (!callerInit) return null;
|
||||
// A clone inside a clone keys on the whole chain of sites.
|
||||
const chain = caller.ctx.includes("@S") ? caller.ctx.slice(caller.ctx.indexOf("@S")) : "";
|
||||
if (chain.split("@S").length > SITE_CLONE_DEPTH) return null;
|
||||
const ctx = `${info.ctx}${chain}@S${this.nodeKey(site)}`;
|
||||
const existing = this.functions.get(`${ctx}|${this.nodeKey(info.node)}`);
|
||||
if (existing !== undefined) return existing;
|
||||
const count = this.siteClones.get(info.node) ?? 0;
|
||||
if (count >= SITE_CLONES) return null;
|
||||
this.siteClones.set(info.node, count + 1);
|
||||
return this.functionInfo(info.node, info.ownerClass, ctx);
|
||||
}
|
||||
|
||||
private readonly factories = new Map<Node, boolean>();
|
||||
@@ -1982,11 +2033,11 @@ class Analyzer {
|
||||
hasProto = true;
|
||||
if (!keys.includes(key)) keys.push(key);
|
||||
}
|
||||
const atom = this.atoms.intern(`obj:${this.nodeKey(e)}`, () => ({
|
||||
kind: "object",
|
||||
label: label(),
|
||||
node: e,
|
||||
}));
|
||||
const sctx = this.siteCloneCtx(e);
|
||||
const atom = this.atoms.intern(
|
||||
sctx === "" ? `obj:${this.nodeKey(e)}` : `obj:${sctx}|${this.nodeKey(e)}`,
|
||||
() => ({ kind: "object", label: label(), node: e, ...(sctx === "" ? {} : { ctx: sctx }) }),
|
||||
);
|
||||
const n = this.newNode();
|
||||
this.add(n, atom);
|
||||
for (const p of list(e.properties)) {
|
||||
@@ -2036,6 +2087,7 @@ class Analyzer {
|
||||
hasComputedKey,
|
||||
spreads,
|
||||
inPackage,
|
||||
ctx: sctx,
|
||||
});
|
||||
return n;
|
||||
}
|
||||
@@ -2174,11 +2226,16 @@ class Analyzer {
|
||||
}
|
||||
|
||||
arrayLiteral(e: ts.ArrayLiteralExpression): number {
|
||||
const atom = this.atoms.intern(`arr:${this.nodeKey(e)}`, () => ({
|
||||
kind: "array",
|
||||
label: `[]@${locLabel(e)}`,
|
||||
node: e,
|
||||
}));
|
||||
const sctx = this.siteCloneCtx(e);
|
||||
const atom = this.atoms.intern(
|
||||
sctx === "" ? `arr:${this.nodeKey(e)}` : `arr:${sctx}|${this.nodeKey(e)}`,
|
||||
() => ({
|
||||
kind: "array",
|
||||
label: `[]@${locLabel(e)}`,
|
||||
node: e,
|
||||
...(sctx === "" ? {} : { ctx: sctx }),
|
||||
}),
|
||||
);
|
||||
const n = this.newNode();
|
||||
this.add(n, atom);
|
||||
const el = this.element(atom);
|
||||
@@ -2203,6 +2260,7 @@ class Analyzer {
|
||||
hasComputedKey: false,
|
||||
spreads: [],
|
||||
inPackage: isPackageJsFile(e.getSourceFile()),
|
||||
ctx: sctx,
|
||||
});
|
||||
return n;
|
||||
}
|
||||
@@ -2701,9 +2759,24 @@ class Analyzer {
|
||||
}
|
||||
out.push(this.expr(a));
|
||||
}
|
||||
// The walk context of this call (one argument list per call site and
|
||||
// context): calls fire during solving, after the walk moved on.
|
||||
this.argsFrom.set(out, {
|
||||
caller: this.currentFn,
|
||||
inLoop: this.loopDepth > 0 || this.forceLoop,
|
||||
});
|
||||
return { args: out, spread };
|
||||
}
|
||||
|
||||
/** Per argument list (args()), the calling function and whether the call
|
||||
* is in a loop, in the context the call was walked in. siteCtx keeps
|
||||
* the first context per syntax node, which a site walked in several
|
||||
* contexts (receiver classes, per-site clones) does not determine. */
|
||||
private readonly argsFrom = new WeakMap<
|
||||
number[],
|
||||
{ caller: FunctionInfo | null; inLoop: boolean }
|
||||
>();
|
||||
|
||||
call(e: ts.CallExpression): number {
|
||||
this.noteSite(e);
|
||||
const callee = unparen(e.expression);
|
||||
@@ -2847,7 +2920,7 @@ class Analyzer {
|
||||
this.add(info.thisNode, inst);
|
||||
this.bindArgs(info, args, spread);
|
||||
this.edge(info.ret, dst);
|
||||
this.edgeFor(e, info);
|
||||
this.edgeFor(e, info, args);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -42,6 +42,8 @@ export interface Candidate {
|
||||
/** Whether the creating syntax is a plain data literal. */
|
||||
readonly plain: boolean;
|
||||
readonly inPackage: boolean;
|
||||
/** The per-call-site clone that allocates it ("" outside clones). */
|
||||
readonly ctx?: string;
|
||||
}
|
||||
|
||||
export interface ConstantObject {
|
||||
@@ -187,6 +189,34 @@ export function computeConstantGraphs(analysis: ShapeAnalysis): ConstantGraphs {
|
||||
return result;
|
||||
};
|
||||
|
||||
// A per-call-site clone (analyze.ts siteClone) runs once when its one
|
||||
// call site runs once: its caller is module code, or another such clone.
|
||||
const edgesToInfo = new Map<FunctionInfo, (typeof analysis.calls)[number][]>();
|
||||
for (const e of analysis.calls) {
|
||||
let l = edgesToInfo.get(e.callee);
|
||||
if (l === undefined) edgesToInfo.set(e.callee, (l = []));
|
||||
l.push(e);
|
||||
}
|
||||
const onceInfoMemo = new Map<FunctionInfo, boolean>();
|
||||
const onceInfo = (info: FunctionInfo): boolean => {
|
||||
if (ts.isSourceFile(info.node)) return true;
|
||||
if (!info.ctx.includes("@S")) return once(info.node);
|
||||
const known = onceInfoMemo.get(info);
|
||||
if (known !== undefined) return known;
|
||||
onceInfoMemo.set(info, false);
|
||||
const edges = edgesToInfo.get(info) ?? [];
|
||||
const result =
|
||||
!escapedNodes.has(info.node) &&
|
||||
edges.length > 0 &&
|
||||
new Set(edges.map((e) => e.site)).size === 1 &&
|
||||
edges.every(
|
||||
(e) =>
|
||||
!e.inLoop && e.caller !== null && !inLoop(e.site, e.caller.node) && onceInfo(e.caller),
|
||||
);
|
||||
onceInfoMemo.set(info, result);
|
||||
return result;
|
||||
};
|
||||
|
||||
// Whether every call of a function comes from once-only code outside
|
||||
// loops, through any number of call sites: an init factory, whose
|
||||
// allocations are one object per call site (schema builders called at
|
||||
@@ -240,6 +270,7 @@ export function computeConstantGraphs(analysis: ShapeAnalysis): ConstantGraphs {
|
||||
site.keys === null
|
||||
),
|
||||
inPackage: site.inPackage,
|
||||
ctx: site.ctx,
|
||||
});
|
||||
}
|
||||
const inPackage = (n: Node): boolean =>
|
||||
@@ -271,7 +302,13 @@ export function computeConstantGraphs(analysis: ShapeAnalysis): ConstantGraphs {
|
||||
// A function object is created where its expression or declaration
|
||||
// runs; its prototype with it.
|
||||
const owner = ownerFunction(site.node);
|
||||
if (!once(owner) || inLoop(site.node, owner)) {
|
||||
// Allocated in a per-call-site clone: once when that clone runs once.
|
||||
const cloneOwner =
|
||||
site.ctx !== undefined && site.ctx !== ""
|
||||
? infosByNode.get(owner)?.find((i) => i.ctx === site.ctx)
|
||||
: undefined;
|
||||
const runsOnce = cloneOwner !== undefined ? onceInfo(cloneOwner) : once(owner);
|
||||
if (!runsOnce || inLoop(site.node, owner)) {
|
||||
const factory = !inLoop(site.node, owner) && initOnly(owner);
|
||||
if (site.inPackage || factory)
|
||||
nearMisses.push({
|
||||
|
||||
@@ -166,7 +166,6 @@ export function planShapeOverlay(
|
||||
const typeOf = (atoms: ReadonlySet<Atom>, inner: boolean, depth: number): string | null => {
|
||||
if (atoms.size === 0) return inner ? "any" : null;
|
||||
const arms = new Set<string>();
|
||||
let objects = 0;
|
||||
// Every array arm becomes one Array<…> of the joined element values.
|
||||
const elements = new Set<Atom>();
|
||||
let hasArray = false;
|
||||
@@ -189,17 +188,14 @@ export function planShapeOverlay(
|
||||
break;
|
||||
case "instance":
|
||||
t = a.node !== undefined ? className(a.node) : null;
|
||||
objects++;
|
||||
break;
|
||||
case "object": {
|
||||
objects++;
|
||||
const v = pSites.get(a);
|
||||
if (v === undefined || depth >= 4 || !siteKeys.has(a)) return inner ? "any" : null;
|
||||
recordSites.push(a);
|
||||
continue;
|
||||
}
|
||||
case "array": {
|
||||
objects++;
|
||||
if (analysis.escaped.has(a) || (siteVerdicts.has(a) && !pSites.has(a)))
|
||||
return inner ? "any" : null;
|
||||
// Only arrays the program writes as literals: a builtin's array
|
||||
@@ -246,7 +242,9 @@ export function planShapeOverlay(
|
||||
// A promise beside other values cannot cross into untyped code
|
||||
// (maybe-promise unions stay typed-only), so it stays untyped here.
|
||||
if (hasPromise) return inner ? "any" : null;
|
||||
if (objects > MAX_ARMS) return inner ? "any" : null;
|
||||
// Arms, not atoms: per-site allocations of one record shape are one
|
||||
// typedef.
|
||||
if (arms.size > MAX_ARMS) return inner ? "any" : null;
|
||||
if (arms.has("any")) return inner ? "any" : null;
|
||||
return [...arms].join(" | ");
|
||||
};
|
||||
|
||||
@@ -81,6 +81,18 @@ export function hotFunctions(analysis: ShapeAnalysis): Set<FunctionInfo> {
|
||||
return hot;
|
||||
}
|
||||
|
||||
/** Operations that reach an escaped site through a ⊤ receiver. */
|
||||
const TOP_REACHING: ReadonlySet<string> = new Set([
|
||||
"delete",
|
||||
"defineProperty",
|
||||
"setPrototypeOf",
|
||||
"seal",
|
||||
"proxy",
|
||||
"assign",
|
||||
"freeze",
|
||||
"lengthWrite",
|
||||
]);
|
||||
|
||||
export function buildShapeReport(analysis: ShapeAnalysis, filter: string): ShapeReport {
|
||||
const lines: string[] = [];
|
||||
const all = filter === "all" || filter === "verbose";
|
||||
@@ -106,6 +118,23 @@ export function buildShapeReport(analysis: ShapeAnalysis, filter: string): Shape
|
||||
}
|
||||
if (atoms.has(analysis.top)) topOps.push(o);
|
||||
}
|
||||
// Through ⊤, only reflective operations and length writes reach an
|
||||
// escaped site (writes, reads and computed writes count only when the
|
||||
// site is a direct receiver). One per syntax node and kind: contexts repeat ops.
|
||||
const reflectiveTopOps: typeof analysis.ops = [];
|
||||
{
|
||||
const seen = new Set<string>();
|
||||
const ids = new Map<ts.Node, number>();
|
||||
for (const o of topOps) {
|
||||
if (!TOP_REACHING.has(o.kind)) continue;
|
||||
let id = ids.get(o.node);
|
||||
if (id === undefined) ids.set(o.node, (id = ids.size));
|
||||
const key = `${o.kind}:${id}`;
|
||||
if (seen.has(key)) continue;
|
||||
seen.add(key);
|
||||
reflectiveTopOps.push(o);
|
||||
}
|
||||
}
|
||||
|
||||
// Key-order groups: sites with the same key set and different orders need brands.
|
||||
const orders = new Map<string, Set<string>>();
|
||||
@@ -129,7 +158,7 @@ export function buildShapeReport(analysis: ShapeAnalysis, filter: string): Shape
|
||||
for (const sp of s.spreads)
|
||||
if (atomsOf(analysis, sp).has(analysis.top)) reasons.push("spread of ⊤");
|
||||
const direct = byAtom.get(s.atom) ?? [];
|
||||
const ops = escapes ? [...direct, ...topOps] : direct;
|
||||
const ops = escapes ? [...direct, ...reflectiveTopOps] : direct;
|
||||
const keys = new Set(s.keys ?? []);
|
||||
const fieldsOf = analysis.fields.get(s.atom);
|
||||
const knownKeys =
|
||||
@@ -144,31 +173,33 @@ export function buildShapeReport(analysis: ShapeAnalysis, filter: string): Shape
|
||||
};
|
||||
for (const o of ops) {
|
||||
const viaTop = !atomsOf(analysis, o.recv).has(s.atom);
|
||||
const where = `${locLabel(o.node)}${viaTop ? " via ⊤" : ""}`;
|
||||
if (viaTop && !TOP_REACHING.has(o.kind)) continue;
|
||||
// Formatted only for a reason that is reported.
|
||||
const at = (): string => `${locLabel(o.node)}${viaTop ? " via ⊤" : ""}`;
|
||||
switch (o.kind) {
|
||||
case "delete":
|
||||
case "defineProperty":
|
||||
case "setPrototypeOf":
|
||||
case "seal":
|
||||
case "proxy":
|
||||
note(`${o.kind} at ${where}`);
|
||||
note(`${o.kind} at ${at()}`);
|
||||
break;
|
||||
case "assign":
|
||||
note(`Object.assign target at ${where}`);
|
||||
note(`Object.assign target at ${at()}`);
|
||||
break;
|
||||
case "freeze":
|
||||
freezable = true;
|
||||
break;
|
||||
case "computedWrite":
|
||||
if (!viaTop)
|
||||
note(s.kind === "array" ? `index write at ${where}` : `computed write at ${where}`);
|
||||
note(s.kind === "array" ? `index write at ${at()}` : `computed write at ${at()}`);
|
||||
break;
|
||||
case "lengthWrite":
|
||||
if (s.kind === "array") note(`length write at ${where}`);
|
||||
if (s.kind === "array") note(`length write at ${at()}`);
|
||||
break;
|
||||
case "write":
|
||||
if (s.kind === "object" && !viaTop && o.name !== null && !knownKeys.has(o.name))
|
||||
note(`adds key '${o.name}' at ${where}`);
|
||||
note(`adds key '${o.name}' at ${at()}`);
|
||||
break;
|
||||
case "read":
|
||||
if (
|
||||
@@ -180,7 +211,7 @@ export function buildShapeReport(analysis: ShapeAnalysis, filter: string): Shape
|
||||
!knownKeys.has(o.name) &&
|
||||
!OBJECT_PROTOTYPE_MEMBERS.has(o.name)
|
||||
)
|
||||
note(`reads absent key '${o.name}' at ${where} (escapes)`);
|
||||
note(`reads absent key '${o.name}' at ${at()} (escapes)`);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
|
||||
@@ -325,6 +325,25 @@ export function planStaticObjectRewrites(
|
||||
for (const op of analysis.ops)
|
||||
if (op.kind !== "read" && op.kind !== "write" && op.kind !== "lengthWrite")
|
||||
for (const a of analysis.nodes[op.recv]!.atoms) computedRecv.add(a);
|
||||
// One entry per (kind, syntax node): a node the analysis walked in
|
||||
// several contexts (receiver classes, per-site clones) is rewritten only
|
||||
// when the receivers of every context together satisfy the conditions.
|
||||
const mergedOps: { op: (typeof analysis.ops)[number]; atoms: Set<Atom> }[] = [];
|
||||
{
|
||||
const byKey = new Map<string, { op: (typeof analysis.ops)[number]; atoms: Set<Atom> }>();
|
||||
const ids = new Map<Node, number>();
|
||||
for (const op of analysis.ops) {
|
||||
let id = ids.get(op.node);
|
||||
if (id === undefined) ids.set(op.node, (id = ids.size));
|
||||
const key = `${op.kind}:${op.name ?? ""}:${id}`;
|
||||
let entry = byKey.get(key);
|
||||
if (entry === undefined) {
|
||||
byKey.set(key, (entry = { op, atoms: new Set() }));
|
||||
mergedOps.push(entry);
|
||||
}
|
||||
for (const a of analysis.nodes[op.recv]!.atoms) entry.atoms.add(a);
|
||||
}
|
||||
}
|
||||
const forIns: ts.Expression[] = [];
|
||||
const reflective: ts.CallExpression[] = [];
|
||||
for (const sf of analysis.files) {
|
||||
@@ -686,9 +705,8 @@ export function planStaticObjectRewrites(
|
||||
// Direct references: `e.k` (called, constructed or read) in the same file
|
||||
// whose receiver is exactly the constant object holding a hoisted
|
||||
// function or class.
|
||||
for (const op of analysis.ops) {
|
||||
for (const { op, atoms } of mergedOps) {
|
||||
if (op.kind !== "read" || op.name === null) continue;
|
||||
const atoms = analysis.nodes[op.recv]!.atoms;
|
||||
if (atoms.size !== 1) continue;
|
||||
const recv = [...atoms][0]!;
|
||||
const o = constantAtoms.get(recv);
|
||||
@@ -729,9 +747,8 @@ export function planStaticObjectRewrites(
|
||||
// member stored after the object was created could be read before the
|
||||
// store; those stay reads.)
|
||||
let foldedReads = 0;
|
||||
for (const op of analysis.ops) {
|
||||
for (const { op, atoms } of mergedOps) {
|
||||
if (op.kind !== "read" || op.name === null) continue;
|
||||
const atoms = analysis.nodes[op.recv]!.atoms;
|
||||
if (atoms.size !== 1) continue;
|
||||
const o = constantAtoms.get([...atoms][0]!);
|
||||
if (o === undefined || o.site.kind !== "literal") continue;
|
||||
@@ -775,14 +792,13 @@ export function planStaticObjectRewrites(
|
||||
// table has no accessors, and comparing a string runs no code, so only
|
||||
// the representation of the call changes.
|
||||
const tableCalls: { sf: ts.SourceFile; call: ts.CallExpression; arms: [string, string][] }[] = [];
|
||||
for (const op of analysis.ops) {
|
||||
for (const { op, atoms } of mergedOps) {
|
||||
if (op.kind !== "computedRead") continue;
|
||||
const node = op.node;
|
||||
if (!ts.isElementAccessExpression(node) || node.questionDotToken !== undefined) continue;
|
||||
const call = node.parent;
|
||||
if (call === undefined || !ts.isCallExpression(call) || call.expression !== node) continue;
|
||||
if (call.questionDotToken !== undefined || topStatement(call) === null) continue;
|
||||
const atoms = analysis.nodes[op.recv]!.atoms;
|
||||
const where = `table call ${node.getText()}`;
|
||||
if (atoms.size !== 1) {
|
||||
kept.push(`${where}: the table may be ${atoms.size} values`);
|
||||
|
||||
@@ -101,8 +101,11 @@ test("constant graphs: namespaces, revealing modules and prototypes, with near m
|
||||
// A deleted key, and an object stored into the global object.
|
||||
expect(text).toMatch(/near miss ns-fixture\/index\.js:16: reflective-write \(delete/);
|
||||
expect(text).toMatch(/near miss ns-fixture\/index\.js:18: escapes/);
|
||||
// A factory called from two init sites: one object per call site.
|
||||
expect(text).toMatch(/near miss ns-fixture\/index\.js:20: init-factory \(.*2 call sites\)/);
|
||||
// A factory called from two init sites is analyzed per call site: each
|
||||
// call allocates its own constant object.
|
||||
expect(text).not.toMatch(/init-factory/);
|
||||
// (7 objects without per-site clones: the two options were one.)
|
||||
expect(text).toMatch(/ns-fixture: 9 constant objects/);
|
||||
});
|
||||
|
||||
// Functions that one call site runs repeatedly: an ES5 constructor and an
|
||||
|
||||
Reference in New Issue
Block a user