mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-27 06:43:19 +08:00
Reuse deferred initializer scans while only bindings are pending
This commit is contained in:
@@ -91,6 +91,26 @@ test("finds replaced blocks and references appended to an existing body in a lat
|
||||
expect(functions[0]!.body).toEqual([{ kind: "block", body: [value(7)], loc }]);
|
||||
});
|
||||
|
||||
test("reuses the scan of an unchanged body while only bindings are pending", () => {
|
||||
const { initializers, pending, lowerer } = fixture();
|
||||
const placeholder = block(loc);
|
||||
const functions = [fn("init", [placeholder]), fn("use", [value(1)])];
|
||||
pending.set("registry", [{ loc, lower: () => [value(7)] }]);
|
||||
expect(initializers.process(lowerer, functions)).toBe(false);
|
||||
const walk = vi.spyOn(traversal, "everyStmtList");
|
||||
try {
|
||||
expect(initializers.process(lowerer, functions)).toBe(false);
|
||||
expect(walk).not.toHaveBeenCalled();
|
||||
// A statement added to the same list is found.
|
||||
functions[1]!.body.push(read("registry"));
|
||||
expect(initializers.process(lowerer, functions)).toBe(true);
|
||||
expect(walk.mock.calls.filter(([body]) => body === functions[0]!.body)).toHaveLength(0);
|
||||
} finally {
|
||||
walk.mockRestore();
|
||||
}
|
||||
expect(placeholder.body).toEqual([value(7)]);
|
||||
});
|
||||
|
||||
test("indexes new subtrees for other demanded initializers in the same pass", () => {
|
||||
const { initializers, pending, lowerer } = fixture();
|
||||
const nestedLoc = { ...loc, start: 10, end: 11 };
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
} from "./lower-classes.js";
|
||||
import { classPrototypeData } from "./class-prototypes.js";
|
||||
import { bindingNeverReassigned } from "./binding-analysis.js";
|
||||
import { InternalCompilerError } from "../../errors.js";
|
||||
|
||||
/** A registry of existing class/function values allocates data, but does
|
||||
* not execute those bodies. Keep its initializer at its source position
|
||||
@@ -20,6 +21,10 @@ import { bindingNeverReassigned } from "./binding-analysis.js";
|
||||
export class DeferredModuleInitializers {
|
||||
private readonly pending = new Map<string, { loc: IrStmt["loc"]; lower: () => IrStmt[] }[]>();
|
||||
private readonly registryGlobals = new Set<string>();
|
||||
/** Every key ever deferred, pending or materialized. */
|
||||
private readonly deferredKeys = new Set<string>();
|
||||
/** The last scan of each function (scanOf). */
|
||||
private readonly scans = new Map<IrFunction, FunctionScan>();
|
||||
|
||||
defer(lowerer: Lowerer, statement: ts.Statement): IrStmt | null {
|
||||
let key: string;
|
||||
@@ -126,6 +131,83 @@ export class DeferredModuleInitializers {
|
||||
|
||||
process(lowerer: Lowerer, functions: readonly IrFunction[]): boolean {
|
||||
if (this.pending.size === 0) return false;
|
||||
// Scans keep only the reads of deferred bindings.
|
||||
for (const key of this.pending.keys()) {
|
||||
if (this.deferredKeys.has(key)) continue;
|
||||
this.deferredKeys.add(key);
|
||||
this.scans.clear();
|
||||
}
|
||||
// Class and prototype keys follow the classes IR types reach, and passes
|
||||
// change those inside statements they keep (a bag initializer gains a
|
||||
// prototype). While only bindings are pending, the scans of unchanged
|
||||
// statement lists are reused; SCRIPTC_CHECK_DEFERRED_SCANS=1 checks each
|
||||
// such pass against fresh scans.
|
||||
const bindingsOnly = ![...this.pending.keys()].some(
|
||||
(key) => key.startsWith("class:") || key.startsWith("prototype:"),
|
||||
);
|
||||
const scans = functions.map((fn) => this.scanOf(lowerer, fn, bindingsOnly));
|
||||
const demanded = this.demanded(lowerer, functions, scans);
|
||||
if (CHECK_SCANS && bindingsOnly) {
|
||||
const fresh = functions.map((fn) => scanFunction(lowerer, fn, this.deferredKeys));
|
||||
const expected = this.demanded(lowerer, functions, fresh);
|
||||
const blocks = (list: readonly FunctionScan[]): PlaceholderBlock[] =>
|
||||
list.flatMap((scan) => scan.placeholders.filter((block) => block.body.length === 0));
|
||||
const reused = blocks(scans);
|
||||
const live = blocks(fresh);
|
||||
if (
|
||||
[...demanded].join("\0") !== [...expected].join("\0") ||
|
||||
reused.length !== live.length ||
|
||||
reused.some((block, index) => block !== live[index])
|
||||
)
|
||||
throw new InternalCompilerError(
|
||||
"reused deferred initializer scans disagree with a fresh scan",
|
||||
);
|
||||
}
|
||||
if (demanded.size === 0) return false;
|
||||
// The placeholders come from the scans, which are fresh for every list a
|
||||
// transform replaced between waves. Each is kept with the functions whose
|
||||
// bodies hold it. Searching every body again for each initializer would
|
||||
// make materializing large module registries quadratic in the program size.
|
||||
const placeholders = new Map<string, { block: PlaceholderBlock; fns: IrFunction[] }[]>();
|
||||
const add = (block: PlaceholderBlock, fns: IrFunction[]): void => {
|
||||
const key = locationKey(block.loc);
|
||||
const blocks = placeholders.get(key) ?? [];
|
||||
blocks.push({ block, fns });
|
||||
placeholders.set(key, blocks);
|
||||
};
|
||||
functions.forEach((fn, index) => {
|
||||
for (const block of scans[index]!.placeholders) if (block.body.length === 0) add(block, [fn]);
|
||||
});
|
||||
for (const id of demanded) {
|
||||
const initialize = this.pending.get(id)!;
|
||||
this.pending.delete(id);
|
||||
for (const action of initialize) {
|
||||
const body = action.lower();
|
||||
const holders: IrFunction[] = [];
|
||||
for (const { block, fns } of placeholders.get(locationKey(action.loc)) ?? []) {
|
||||
if (block.body.length === 0) {
|
||||
block.body.push(...body);
|
||||
holders.push(...fns);
|
||||
}
|
||||
}
|
||||
if (holders.length === 0) continue;
|
||||
// These bodies changed inside unchanged statement lists.
|
||||
for (const fn of holders) this.scans.delete(fn);
|
||||
// An inserted body can introduce a placeholder needed by another
|
||||
// action in this pass. Unattached bodies are not part of the live IR.
|
||||
for (const nested of scanStatements(body).placeholders) add(nested, holders);
|
||||
}
|
||||
}
|
||||
return demanded.size !== 0;
|
||||
}
|
||||
|
||||
/** The pending keys the scanned functions reference, in the order the
|
||||
* functions and their walks meet them. */
|
||||
private demanded(
|
||||
lowerer: Lowerer,
|
||||
functions: readonly IrFunction[],
|
||||
scans: readonly FunctionScan[],
|
||||
): Set<string> {
|
||||
const demanded = new Set<string>();
|
||||
const classHelpers = new Map<string, string>();
|
||||
for (const info of lowerer.classes.values()) {
|
||||
@@ -133,116 +215,190 @@ export class DeferredModuleInitializers {
|
||||
if (info.def.prototypeDataHelper)
|
||||
classHelpers.set(info.def.prototypeDataHelper, `prototype:${info.def.name}`);
|
||||
}
|
||||
const seen = new Set<IrType>();
|
||||
const byName = new Map(functions.map((fn) => [fn.name, fn]));
|
||||
const inspectedHelpers = new Set<string>();
|
||||
const byName = new Map(functions.map((fn, index) => [fn.name, index]));
|
||||
const usedHelpers: string[] = [];
|
||||
const demandType = (type: IrType): void => {
|
||||
if (seen.has(type)) return;
|
||||
seen.add(type);
|
||||
if (type.kind === "object" || type.kind === "classval") {
|
||||
for (let info = lowerer.classes.get(type.className); info; info = info.base ?? undefined) {
|
||||
const key = `class:${info.def.name}`;
|
||||
if (this.pending.has(key)) demanded.add(key);
|
||||
const prototype = `prototype:${info.def.name}`;
|
||||
if (type.kind === "object" && this.pending.has(prototype)) demanded.add(prototype);
|
||||
const demand = (events: readonly ScanEvent[]): void => {
|
||||
for (const event of events) {
|
||||
if (event.kind === "read") {
|
||||
if (this.pending.has(event.id)) demanded.add(event.id);
|
||||
} else if (event.kind === "call") {
|
||||
const owner = classHelpers.get(event.callee);
|
||||
if (owner) {
|
||||
if (this.pending.has(owner)) demanded.add(owner);
|
||||
usedHelpers.push(event.callee);
|
||||
}
|
||||
} else {
|
||||
for (
|
||||
let info = lowerer.classes.get(event.className);
|
||||
info;
|
||||
info = info.base ?? undefined
|
||||
) {
|
||||
const key = `class:${info.def.name}`;
|
||||
if (this.pending.has(key)) demanded.add(key);
|
||||
const prototype = `prototype:${info.def.name}`;
|
||||
if (event.kind === "object" && this.pending.has(prototype)) demanded.add(prototype);
|
||||
}
|
||||
}
|
||||
} else if (type.kind === "array") demandType(type.elem);
|
||||
else if (type.kind === "union") lowerer.unions.get(type.unionId)?.arms.forEach(demandType);
|
||||
else if (type.kind === "record") {
|
||||
const shape = lowerer.shapes.get(type.shapeId);
|
||||
shape?.fields.forEach((field) => demandType(field.type));
|
||||
if (shape?.indexValue) demandType(shape.indexValue);
|
||||
} else if (type.kind === "func") {
|
||||
type.params.forEach(demandType);
|
||||
demandType(type.ret);
|
||||
} else if (type.kind === "promise") demandType(type.inner);
|
||||
}
|
||||
};
|
||||
for (const fn of functions) {
|
||||
functions.forEach((fn, index) => {
|
||||
// Prototype helpers are allocated eagerly while collecting metadata.
|
||||
// Their base-helper calls must not make unused classes reachable.
|
||||
if (classHelpers.has(fn.name)) continue;
|
||||
fn.params.forEach((param) => demandType(param.type));
|
||||
demandType(fn.returnType);
|
||||
everyStmtList(fn.body, {
|
||||
stmt: () => true,
|
||||
expr: (expr) => {
|
||||
demandType(expr.type);
|
||||
if (readsBinding(expr) && this.pending.has(expr.localId)) demanded.add(expr.localId);
|
||||
if (expr.kind === "call") {
|
||||
const owner = classHelpers.get(expr.callee);
|
||||
if (owner) {
|
||||
if (this.pending.has(owner)) demanded.add(owner);
|
||||
usedHelpers.push(expr.callee);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
},
|
||||
});
|
||||
}
|
||||
if (classHelpers.has(fn.name)) return;
|
||||
demand(scans[index]!.signature);
|
||||
demand(scans[index]!.body);
|
||||
});
|
||||
const inspectedHelpers = new Set<string>();
|
||||
while (usedHelpers.length) {
|
||||
const name = usedHelpers.pop()!;
|
||||
if (inspectedHelpers.has(name)) continue;
|
||||
inspectedHelpers.add(name);
|
||||
const fn = byName.get(name);
|
||||
if (!fn) continue;
|
||||
everyStmtList(fn.body, {
|
||||
stmt: () => true,
|
||||
expr: (expr) => {
|
||||
demandType(expr.type);
|
||||
if (readsBinding(expr) && this.pending.has(expr.localId)) demanded.add(expr.localId);
|
||||
if (expr.kind === "call") {
|
||||
const owner = classHelpers.get(expr.callee);
|
||||
if (owner) {
|
||||
if (this.pending.has(owner)) demanded.add(owner);
|
||||
usedHelpers.push(expr.callee);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
},
|
||||
});
|
||||
const index = byName.get(name);
|
||||
if (index !== undefined) demand(scans[index]!.body);
|
||||
}
|
||||
if (demanded.size === 0) return false;
|
||||
// Transforms replace blocks between waves, so index the live placeholders
|
||||
// anew in each pass. Searching every body again for each initializer makes
|
||||
// materializing large module registries quadratic in the program size.
|
||||
const placeholders = new Map<string, Extract<IrStmt, { kind: "block" }>[]>();
|
||||
const locationKey = (loc: IrStmt["loc"]): string =>
|
||||
JSON.stringify([loc.file, loc.start, loc.end]);
|
||||
const indexPlaceholders = (body: IrStmt[]): void => {
|
||||
everyStmtList(body, {
|
||||
stmt: (statement) => {
|
||||
if (statement.kind === "block" && statement.body.length === 0) {
|
||||
const key = locationKey(statement.loc);
|
||||
const blocks = placeholders.get(key) ?? [];
|
||||
blocks.push(statement);
|
||||
placeholders.set(key, blocks);
|
||||
}
|
||||
return true;
|
||||
},
|
||||
expr: () => true,
|
||||
});
|
||||
};
|
||||
for (const fn of functions) indexPlaceholders(fn.body);
|
||||
for (const id of demanded) {
|
||||
const initialize = this.pending.get(id)!;
|
||||
this.pending.delete(id);
|
||||
for (const action of initialize) {
|
||||
const body = action.lower();
|
||||
let inserted = false;
|
||||
for (const block of placeholders.get(locationKey(action.loc)) ?? []) {
|
||||
if (block.body.length === 0) {
|
||||
block.body.push(...body);
|
||||
inserted = true;
|
||||
}
|
||||
}
|
||||
// An inserted body can introduce a placeholder needed by another
|
||||
// action in this pass. Unattached bodies are not part of the live IR.
|
||||
if (inserted) indexPlaceholders(body);
|
||||
}
|
||||
}
|
||||
return demanded.size !== 0;
|
||||
return demanded;
|
||||
}
|
||||
|
||||
/** What `fn` references that can demand an initializer, in the order a
|
||||
* walk of it meets them. While only bindings are pending (`reuse`), a
|
||||
* function's scan is reused while its statement list is the same array
|
||||
* with the same length and the same first and last statements: passes that
|
||||
* change a body either replace its list or add statements to it, and this
|
||||
* pass forgets the scan of a function whose placeholder it fills. */
|
||||
private scanOf(lowerer: Lowerer, fn: IrFunction, reuse: boolean): FunctionScan {
|
||||
const cached = this.scans.get(fn);
|
||||
if (
|
||||
reuse &&
|
||||
cached !== undefined &&
|
||||
cached.statements === fn.body &&
|
||||
cached.length === fn.body.length &&
|
||||
cached.first === fn.body[0] &&
|
||||
cached.last === fn.body[fn.body.length - 1]
|
||||
) {
|
||||
return cached;
|
||||
}
|
||||
const scan = scanFunction(lowerer, fn, this.deferredKeys);
|
||||
this.scans.set(fn, scan);
|
||||
return scan;
|
||||
}
|
||||
}
|
||||
|
||||
type PlaceholderBlock = Extract<IrStmt, { kind: "block" }>;
|
||||
|
||||
/** One reference a scan records: a class an IR type reaches (`object` also
|
||||
* reaches its prototype), a binding read, or a call to a class data
|
||||
* helper. */
|
||||
type ScanEvent =
|
||||
| { kind: "object" | "classval"; className: string }
|
||||
| { kind: "read"; id: string }
|
||||
| { kind: "call"; callee: string };
|
||||
|
||||
interface FunctionScan {
|
||||
statements: IrStmt[];
|
||||
length: number;
|
||||
first: IrStmt | undefined;
|
||||
last: IrStmt | undefined;
|
||||
/** References of the parameter and return types. */
|
||||
signature: ScanEvent[];
|
||||
/** References of the body's expressions. */
|
||||
body: ScanEvent[];
|
||||
/** Empty blocks, in walk order: an initializer's position. */
|
||||
placeholders: PlaceholderBlock[];
|
||||
}
|
||||
|
||||
/** Class data helpers (classStaticData, classPrototypeData). */
|
||||
const CLASS_HELPER_PREFIXES = ["%static.data.", "%prototype.data."];
|
||||
|
||||
/** SCRIPTC_CHECK_DEFERRED_SCANS=1 checks each pass that reuses scans against fresh scans. */
|
||||
const CHECK_SCANS = process.env["SCRIPTC_CHECK_DEFERRED_SCANS"] === "1";
|
||||
|
||||
function locationKey(loc: IrStmt["loc"]): string {
|
||||
return JSON.stringify([loc.file, loc.start, loc.end]);
|
||||
}
|
||||
|
||||
/** The references of `fn` (FunctionScan). Each reference is kept at its
|
||||
* first occurrence, except calls, and only reads of `deferred` keys. */
|
||||
function scanFunction(
|
||||
lowerer: Lowerer,
|
||||
fn: IrFunction,
|
||||
deferred: ReadonlySet<string>,
|
||||
): FunctionScan {
|
||||
const signature: ScanEvent[] = [];
|
||||
const typeEvents = typeScanner(lowerer, signature);
|
||||
for (const param of fn.params) typeEvents(param.type);
|
||||
typeEvents(fn.returnType);
|
||||
const { body, placeholders } = scanStatements(fn.body, lowerer, deferred);
|
||||
return {
|
||||
statements: fn.body,
|
||||
length: fn.body.length,
|
||||
first: fn.body[0],
|
||||
last: fn.body[fn.body.length - 1],
|
||||
signature,
|
||||
body,
|
||||
placeholders,
|
||||
};
|
||||
}
|
||||
|
||||
/** Records, once each, the classes the types it is given reach. */
|
||||
function typeScanner(lowerer: Lowerer, events: ScanEvent[]): (type: IrType) => void {
|
||||
const seen = new Set<IrType>();
|
||||
const classes = new Set<string>();
|
||||
const visit = (type: IrType): void => {
|
||||
if (seen.has(type)) return;
|
||||
seen.add(type);
|
||||
if (type.kind === "object" || type.kind === "classval") {
|
||||
const key = `${type.kind}:${type.className}`;
|
||||
if (!classes.has(key)) {
|
||||
classes.add(key);
|
||||
events.push({ kind: type.kind, className: type.className });
|
||||
}
|
||||
} else if (type.kind === "array") visit(type.elem);
|
||||
else if (type.kind === "union") lowerer.unions.get(type.unionId)?.arms.forEach(visit);
|
||||
else if (type.kind === "record") {
|
||||
const shape = lowerer.shapes.get(type.shapeId);
|
||||
shape?.fields.forEach((field) => visit(field.type));
|
||||
if (shape?.indexValue) visit(shape.indexValue);
|
||||
} else if (type.kind === "func") {
|
||||
type.params.forEach(visit);
|
||||
visit(type.ret);
|
||||
} else if (type.kind === "promise") visit(type.inner);
|
||||
};
|
||||
return visit;
|
||||
}
|
||||
|
||||
/** The references and placeholders of a statement list. Without a lowerer
|
||||
* it collects only placeholders. */
|
||||
function scanStatements(
|
||||
statements: IrStmt[],
|
||||
lowerer?: Lowerer,
|
||||
deferred?: ReadonlySet<string>,
|
||||
): { body: ScanEvent[]; placeholders: PlaceholderBlock[] } {
|
||||
const body: ScanEvent[] = [];
|
||||
const placeholders: PlaceholderBlock[] = [];
|
||||
const typeEvents = lowerer ? typeScanner(lowerer, body) : null;
|
||||
const reads = new Set<string>();
|
||||
everyStmtList(statements, {
|
||||
stmt: (statement) => {
|
||||
if (statement.kind === "block" && statement.body.length === 0) placeholders.push(statement);
|
||||
return true;
|
||||
},
|
||||
expr: (expr) => {
|
||||
if (typeEvents === null) return true;
|
||||
typeEvents(expr.type);
|
||||
if (readsBinding(expr) && deferred!.has(expr.localId) && !reads.has(expr.localId)) {
|
||||
reads.add(expr.localId);
|
||||
body.push({ kind: "read", id: expr.localId });
|
||||
}
|
||||
// Every call, as the helpers are visited from a stack in call order.
|
||||
if (
|
||||
expr.kind === "call" &&
|
||||
CLASS_HELPER_PREFIXES.some((prefix) => expr.callee.startsWith(prefix))
|
||||
)
|
||||
body.push({ kind: "call", callee: expr.callee });
|
||||
return true;
|
||||
},
|
||||
});
|
||||
return { body, placeholders };
|
||||
}
|
||||
|
||||
/** A read of a binding's current value: a plain read, or an update
|
||||
|
||||
Reference in New Issue
Block a user