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:
Malte Ubl
2026-10-25 07:34:05 -07:00
co-authored by Claude Opus 5.5
parent fc3f0de981
commit df02c72407
6 changed files with 198 additions and 40 deletions
@@ -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