Read emitter analyses from one recorded walk of the module (#1990)

This commit is contained in:
Chris Tate
2026-10-23 06:29:19 -05:00
committed by GitHub
20 changed files with 1006 additions and 354 deletions
Binary file not shown.
+26 -24
View File
@@ -3,6 +3,7 @@
import type { DynamicFieldStores } from "../ir/int32-slots.js";
import type { IrExpr, IrModule, IrStmt } from "../ir/ir.js";
import { everyExprChild, everyStmtChild } from "../ir/traverse.js";
import { eachNodeOfKinds, type IrNodeIndex, type IrNodeKind } from "../ir/node-index.js";
import {
computeCycleMutability,
constructorEscapePoint,
@@ -97,7 +98,10 @@ export interface ConstructionFacts {
privateStores: Set<IrStmt>;
}
export function computeConstructionFacts(mod: IrModule): ConstructionFacts {
/** The nodes that can reach a constructor with a reused receiver. */
const CONSTRUCTOR_USE_KINDS: readonly IrNodeKind[] = ["call", "closure"];
export function computeConstructionFacts(mod: IrModule, nodes?: IrNodeIndex): ConstructionFacts {
const facts: ConstructionFacts = { privateStores: new Set() };
if (mod.lib !== undefined) return facts;
const classes = new Map((mod.classes ?? []).map((c) => [c.name, c] as const));
@@ -122,6 +126,7 @@ export function computeConstructionFacts(mod: IrModule): ConstructionFacts {
c = classes.get(c)!.base;
}
};
const scan = nodes?.scan(CONSTRUCTOR_USE_KINDS);
for (const fn of mod.functions) {
const own = ctorClass.get(fn.name);
const thisId = own !== undefined ? thisOf(own) : null;
@@ -129,22 +134,19 @@ export function computeConstructionFacts(mod: IrModule): ConstructionFacts {
own !== undefined && classes.get(own)!.base !== undefined
? `%${classes.get(own)!.base}.constructor`
: null;
const expr = (e: IrExpr): boolean => {
if (e.kind === "call") {
const target = ctorClass.get(e.callee);
eachNodeOfKinds(fn, scan, (node) => {
if (node.kind === "call") {
const target = ctorClass.get(node.callee);
if (
target !== undefined &&
!(e.callee === baseCtor && e.args.length > 0 && isThisRef(e.args[0]!, thisId))
!(node.callee === baseCtor && node.args.length > 0 && isThisRef(node.args[0]!, thisId))
)
markReused(target);
} else if (e.kind === "closure") {
const target = ctorClass.get(e.fnName);
} else if (node.kind === "closure") {
const target = ctorClass.get(node.fnName);
if (target !== undefined) markReused(target);
}
return everyExprChild(e, expr, stmt);
};
const stmt = (s: IrStmt): boolean => everyStmtChild(s, expr, stmt);
fn.body.every(stmt);
});
}
// The first escaping statement of each constructor (body length: none).
@@ -192,32 +194,32 @@ export function computeConstructionFacts(mod: IrModule): ConstructionFacts {
return facts;
}
/** The nodes that write a binding: statement and expression assignments. */
const ASSIGNMENT_KINDS: readonly IrNodeKind[] = ["assign", "assignExpr"];
/** Module constants whose value never changes once initialized: immutable,
* not TDZ-checked (every read follows the initialization), no import-cycle
* init flag, and at most one assignment in the whole module. Such a global
* owns its value for the rest of the program, so a read of it can be
* borrowed across any later operand or call. */
export function stableGlobals(mod: IrModule): Set<string> {
export function stableGlobals(mod: IrModule, nodes?: IrNodeIndex): Set<string> {
const candidates = new Set(
(mod.globals ?? [])
.filter((g) => !g.mutable && g.tdz !== true && g.initFlag === undefined)
.map((g) => g.id),
);
const assigned = new Set<string>();
const expr = (e: IrExpr): boolean => {
if (e.kind === "assignExpr" && candidates.has(e.localId)) candidates.delete(e.localId);
return everyExprChild(e, expr, stmt);
};
const stmt = (s: IrStmt): boolean => {
if (s.kind === "assign" && candidates.has(s.localId)) {
if (assigned.has(s.localId)) candidates.delete(s.localId);
assigned.add(s.localId);
}
return everyStmtChild(s, expr, stmt);
};
const scan = nodes?.scan(ASSIGNMENT_KINDS);
for (const fn of mod.functions) {
if (candidates.size === 0) break;
fn.body.every(stmt);
eachNodeOfKinds(fn, scan, (node) => {
if (node.kind === "assignExpr" && candidates.has(node.localId))
candidates.delete(node.localId);
else if (node.kind === "assign" && candidates.has(node.localId)) {
if (assigned.has(node.localId)) candidates.delete(node.localId);
assigned.add(node.localId);
}
});
}
return candidates;
}
@@ -9,8 +9,8 @@
* one directly or through a binding read from it. The choice never changes
* behavior. */
import type { IrExpr, IrModule, IrStmt } from "../../ir/ir.js";
import { everyStmtList } from "../../ir/traverse.js";
import type { IrExpr, IrModule } from "../../ir/ir.js";
import { eachNodeOfKinds, type IrNodeIndex, type IrNodeKind } from "../../ir/node-index.js";
const APPENDING_METHODS: ReadonlySet<string> = new Set([
"push",
@@ -30,7 +30,27 @@ function isEmptyLiteral(e: IrExpr): boolean {
);
}
export function findAppendedArrayLiterals(module: IrModule): ReadonlySet<IrExpr> {
/** The nodes that name an appended binding or field, and the stores that
* initialize one. */
const TARGET_KINDS: readonly IrNodeKind[] = [
"arrIntrinsic",
"assignExpr",
"arraySet",
"varDecl",
"assign",
];
const STORE_KINDS: readonly IrNodeKind[] = [
"assignExpr",
"varDecl",
"assign",
"fieldSet",
"recordSet",
];
export function findAppendedArrayLiterals(
module: IrModule,
index?: IrNodeIndex,
): ReadonlySet<IrExpr> {
// Bindings are function-scoped by id; module globals are shared.
const localsOf = new Map<string, ReadonlySet<string>>();
for (const fn of module.functions) {
@@ -65,20 +85,15 @@ export function findAppendedArrayLiterals(module: IrModule): ReadonlySet<IrExpr>
const field = fieldOf(target);
if (field !== null) appendedFields.add(field);
};
const targets = index?.scan(TARGET_KINDS);
for (const fn of module.functions) {
everyStmtList(fn.body, {
expr: (e) => {
if (e.kind === "arrIntrinsic" && APPENDING_METHODS.has(e.method))
noteTarget(fn.name, e.receiver);
else if (e.kind === "assignExpr") noteAlias(fn.name, e.localId, e.value);
return true;
},
stmt: (s) => {
if (s.kind === "arraySet") noteTarget(fn.name, s.arr);
else if (s.kind === "varDecl") noteAlias(fn.name, s.localId, s.init);
else if (s.kind === "assign") noteAlias(fn.name, s.localId, s.value);
return true;
},
eachNodeOfKinds(fn, targets, (node) => {
if (node.kind === "arrIntrinsic" && APPENDING_METHODS.has(node.method))
noteTarget(fn.name, node.receiver);
else if (node.kind === "assignExpr") noteAlias(fn.name, node.localId, node.value);
else if (node.kind === "arraySet") noteTarget(fn.name, node.arr);
else if (node.kind === "varDecl") noteAlias(fn.name, node.localId, node.init);
else if (node.kind === "assign") noteAlias(fn.name, node.localId, node.value);
});
}
for (const binding of appendedBindings)
@@ -89,23 +104,18 @@ export function findAppendedArrayLiterals(module: IrModule): ReadonlySet<IrExpr>
const consider = (value: IrExpr | null, appended: boolean): void => {
if (value && appended && isEmptyLiteral(value)) result.add(value);
};
const stores = index?.scan(STORE_KINDS);
for (const fn of module.functions) {
const binding = (localId: string): boolean =>
appendedBindings.has(bindingKey(fn.name, localId));
everyStmtList(fn.body, {
expr: (e) => {
if (e.kind === "assignExpr") consider(e.value, binding(e.localId));
return true;
},
stmt: (s: IrStmt) => {
if (s.kind === "varDecl") consider(s.init, binding(s.localId));
else if (s.kind === "assign") consider(s.value, binding(s.localId));
else if (s.kind === "fieldSet")
consider(s.value, appendedFields.has(fieldKey(s.className, s.field)));
else if (s.kind === "recordSet")
consider(s.value, appendedFields.has(fieldKey(s.shapeId, s.field)));
return true;
},
eachNodeOfKinds(fn, stores, (node) => {
if (node.kind === "assignExpr") consider(node.value, binding(node.localId));
else if (node.kind === "varDecl") consider(node.init, binding(node.localId));
else if (node.kind === "assign") consider(node.value, binding(node.localId));
else if (node.kind === "fieldSet")
consider(node.value, appendedFields.has(fieldKey(node.className, node.field)));
else if (node.kind === "recordSet")
consider(node.value, appendedFields.has(fieldKey(node.shapeId, node.field)));
});
}
return result;
@@ -1,9 +1,28 @@
import type { IrExpr, IrFunction, IrModule } from "../../ir/ir.js";
import { exprFunctionReference, moduleFunctionReferences } from "../../ir/function-references.js";
import {
exprFunctionReference,
FUNCTION_REFERENCE_KINDS,
moduleFunctionReferences,
} from "../../ir/function-references.js";
import { writtenLocals } from "../../ir/callback-specialization.js";
import { eachNodeOfKinds, type IrNodeIndex, type IrNodeKind } from "../../ir/node-index.js";
import { everyStmtList } from "../../ir/traverse.js";
import type { CallLifetimes } from "./call-lifetimes.js";
/** The nodes the body census below reads. */
const CENSUS_KINDS: readonly IrNodeKind[] = [
"varDecl",
"assign",
"forOf",
"rethrow",
"tryCatch",
"call",
"callValue",
"assignExpr",
"incDec",
...FUNCTION_REFERENCE_KINDS,
];
/** Closed direct-call consumers can devirtualize a callback when every
* incoming call supplies the same function. Keep its evaluated environment,
* arguments and ordinary ownership; only the loaded code pointer changes.
@@ -15,6 +34,7 @@ import type { CallLifetimes } from "./call-lifetimes.js";
export function findConstantCallbacks(
mod: IrModule,
lifetimes: CallLifetimes,
index?: IrNodeIndex,
): Map<string, Map<string, string>> {
const functions = new Map(mod.functions.map((fn) => [fn.name, fn]));
const definitions = new Map<string, Map<string, IrExpr>>();
@@ -50,7 +70,7 @@ export function findConstantCallbacks(
const writes = new Set<string>();
// Parameters and captures each function (or a closure sharing the
// binding) writes after entry.
const rebinds = writtenLocals(mod);
const rebinds = writtenLocals(mod, index);
// A function named anywhere but as a direct call's callee can be entered
// by a generated adapter or the runtime, which a direct-call census cannot
// specialize: the entry, exports, class helpers and methods here, and
@@ -66,6 +86,7 @@ export function findConstantCallbacks(
// The initializer of each immutable function-typed local declared once:
// its only write, even when a capture box (not a stable slot) holds it.
const constDefinitions = new Map<string, Map<string, IrExpr | null>>();
const scan = index?.scan(CENSUS_KINDS);
for (const fn of mod.functions) {
const defs = new Map<string, IrExpr>();
const consts = new Map<string, IrExpr | null>();
@@ -85,51 +106,64 @@ export function findConstantCallbacks(
const emptyDecls = new Map<string, number>();
const assigned = new Map<string, IrExpr[]>();
const rebound = new Set<string>();
everyStmtList(fn.body, {
stmt: (stmt) => {
if (stmt.kind === "varDecl" && stmt.init?.type.kind === "func" && stable?.has(stmt.localId))
defs.set(stmt.localId, stmt.init);
if (stmt.kind === "varDecl" && immutable.has(stmt.localId))
consts.set(stmt.localId, consts.has(stmt.localId) ? null : stmt.init);
if (stmt.kind === "varDecl" && hoisted.has(stmt.localId)) {
if (stmt.init !== null) rebound.add(stmt.localId);
emptyDecls.set(stmt.localId, (emptyDecls.get(stmt.localId) ?? 0) + 1);
}
if (stmt.kind === "assign" && hoisted.has(stmt.localId)) {
const values = assigned.get(stmt.localId) ?? [];
values.push(stmt.value);
assigned.set(stmt.localId, values);
}
if (stmt.kind === "forOf" || stmt.kind === "rethrow") rebound.add(stmt.localId);
if (stmt.kind === "tryCatch" && stmt.catchLocalId !== null) rebound.add(stmt.catchLocalId);
if (stmt.kind === "assign") {
if (globalIds.has(stmt.localId)) {
if (globals.has(stmt.localId)) writes.add(stmt.localId);
else globals.set(stmt.localId, stmt.value);
eachNodeOfKinds(fn, scan, (node) => {
switch (node.kind) {
case "varDecl":
if (node.init?.type.kind === "func" && stable?.has(node.localId))
defs.set(node.localId, node.init);
if (immutable.has(node.localId))
consts.set(node.localId, consts.has(node.localId) ? null : node.init);
if (hoisted.has(node.localId)) {
if (node.init !== null) rebound.add(node.localId);
emptyDecls.set(node.localId, (emptyDecls.get(node.localId) ?? 0) + 1);
}
return;
case "assign":
if (hoisted.has(node.localId)) {
const values = assigned.get(node.localId) ?? [];
values.push(node.value);
assigned.set(node.localId, values);
}
if (globalIds.has(node.localId)) {
if (globals.has(node.localId)) writes.add(node.localId);
else globals.set(node.localId, node.value);
}
return;
case "forOf":
case "rethrow":
rebound.add(node.localId);
return;
case "tryCatch":
if (node.catchLocalId !== null) rebound.add(node.catchLocalId);
return;
case "call":
if (consumers.has(node.callee)) {
const calls = incoming.get(node.callee) ?? [];
calls.push({ owner: fn.name, args: node.args });
incoming.set(node.callee, calls);
}
return;
case "callValue":
if (node.callee.kind === "varRef") callees.add(node.callee.localId);
return;
case "closure": {
escaped.add(node.fnName);
const sites = closureSites.get(node.fnName) ?? [];
sites.push({ owner: fn.name, captures: node.captures });
closureSites.set(node.fnName, sites);
return;
}
return true;
},
expr: (expr) => {
if (expr.kind === "call" && consumers.has(expr.callee)) {
const calls = incoming.get(expr.callee) ?? [];
calls.push({ owner: fn.name, args: expr.args });
incoming.set(expr.callee, calls);
case "orDefault": {
const referenced = exprFunctionReference(node);
if (referenced !== undefined) escaped.add(referenced);
return;
}
if (expr.kind === "callValue" && expr.callee.kind === "varRef")
callees.add(expr.callee.localId);
const referenced = exprFunctionReference(expr);
if (referenced !== undefined) escaped.add(referenced);
if (expr.kind === "closure") {
const sites = closureSites.get(expr.fnName) ?? [];
sites.push({ owner: fn.name, captures: expr.captures });
closureSites.set(expr.fnName, sites);
}
if ((expr.kind === "assignExpr" || expr.kind === "incDec") && globalIds.has(expr.localId))
writes.add(expr.localId);
if (expr.kind === "assignExpr" || expr.kind === "incDec") rebound.add(expr.localId);
return true;
},
case "assignExpr":
case "incDec":
if (globalIds.has(node.localId)) writes.add(node.localId);
rebound.add(node.localId);
return;
}
});
for (const [id, values] of assigned)
if (emptyDecls.get(id) === 1 && values.length === 1 && !rebound.has(id))
+25 -10
View File
@@ -169,6 +169,7 @@ import { type MapEntryFusion } from "./map-entry-fusion.js";
import { emitBorrowedFieldSequence, guardedValue } from "./borrowed-receivers.js";
import { emitBorrowedInputs } from "./borrowed-inputs.js";
import { findWalkBorrows, type WalkBorrowHost } from "./walk-borrows.js";
import { IrNodeIndex } from "../../ir/node-index.js";
import { lastUseRead } from "./local-extents.js";
import { findReboundParameters } from "./rebound-parameters.js";
import { ReferenceEffects } from "./reference-effects.js";
@@ -1183,6 +1184,9 @@ export class LlEmitter {
/** Module constants that keep one value once initialized
* (immutability.ts stableGlobals). */
stableGlobals: ReadonlySet<string> = new Set();
/** Every function body's nodes in walk order (ir/node-index.ts), for the
* constructor's analyses. */
readonly nodeIndex: IrNodeIndex;
private readonly tdzGlobals = new Map<string, string>();
readonly constantNumericTables: ReadonlyMap<string, ConstantNumericTable>;
/** Constant-key reads on checked-dynamic objects, each with its own
@@ -1251,7 +1255,7 @@ export class LlEmitter {
/** The module's runtime features (moduleRuntimeFeatures), found with one
* walk of the module the first time any of them is read. */
get runtimeFeatures(): RuntimeFeatures {
return (this.runtimeFeaturesValue ??= moduleRuntimeFeatures(this.mod));
return (this.runtimeFeaturesValue ??= moduleRuntimeFeatures(this.mod, this.nodeIndex));
}
/** Whether timer and tick scheduling goes through the *_als entry
* points (RuntimeFeatures asyncContext). */
@@ -1548,8 +1552,15 @@ export class LlEmitter {
((options.functionWorkers ?? 1) > 1 &&
options.forkPrimitives !== undefined &&
options.debugSources === undefined);
this.stackTraces = observesStacks(mod);
this.nullableUnions = new NullableUnions(mod, options.debugSources === undefined);
// One walk of every body, which the analyses below read instead of
// walking the bodies themselves.
this.nodeIndex = new IrNodeIndex(mod);
this.stackTraces = observesStacks(mod, this.nodeIndex);
this.nullableUnions = new NullableUnions(
mod,
options.debugSources === undefined,
this.nodeIndex,
);
this.mockTimers = this.runtimeFeatures.mockTimers;
this.debug =
options.debugSources === undefined
@@ -1562,19 +1573,20 @@ export class LlEmitter {
this.nullableUnions.armTypes(),
);
this.constantNumericTables = findConstantNumericTables(mod);
this.initializerBindings = findInitializerBindings(mod);
this.initializerBindings = findInitializerBindings(mod, this.nodeIndex);
// One walk finds the dynamic field stores every analysis below consults.
const dynamicStores = dynamicFieldStores(mod);
const dynamicStores = dynamicFieldStores(mod, undefined, undefined, this.nodeIndex);
this.int32Slots = analyzeInt32Slots(mod, {
dynamicStores,
initializerBindings: this.initializerBindings,
nodes: this.nodeIndex,
});
this.liveViewsReadOnly =
mod.lib === undefined && !dynamicStores.all && dynamicStores.names.size === 0;
this.int32Abis = this.debug === null ? findInt32Abis(mod, this.int32Slots) : new Map();
this.immortalGlobals = new Set(mod.contracts?.immortalGlobals ?? []);
this.programLifetime = new Set(mod.contracts?.programLifetime ?? []);
this.finalFields = analyzeFinalFields(mod, false, dynamicStores);
this.finalFields = analyzeFinalFields(mod, false, dynamicStores, this.nodeIndex);
this.sizeType = options.pointerBits === 32 ? "i32" : "i64";
this.ffiExtendNarrowIntegers =
options.wasi === true || ffiExtendsNarrowIntegers(options.targetTriple);
@@ -1622,7 +1634,7 @@ export class LlEmitter {
this.ffiHasRetainedCallback = hasRetainedFfiCallback(mod.ffiImports ?? []);
this.ffiHasForeignCallback = hasForeignFfiCallback(mod.ffiImports ?? []);
for (const fn of mod.functions) this.fnByName.set(fn.name, fn);
this.appendedArrayLiterals = findAppendedArrayLiterals(mod);
this.appendedArrayLiterals = findAppendedArrayLiterals(mod, this.nodeIndex);
for (const entry of mod.ffiImports ?? []) {
this.ffiByName.set(entry.name, entry);
}
@@ -1630,10 +1642,11 @@ export class LlEmitter {
// and per async fiber; WASI, Windows and library builds have none.
const triple = (options.targetTriple ?? "").toLowerCase();
const windows = triple === "" ? process.platform === "win32" : triple.includes("windows");
this.publishedTypes = computePublishedTypes(mod);
this.publishedTypes = computePublishedTypes(mod, this.nodeIndex);
const mt = computeMayThrow(mod, {
stackChecks: !this.wasi && mod.lib === undefined && !windows,
published: this.publishedTypes,
nodes: this.nodeIndex,
});
this.mayThrow = mt.fns;
this.workerEntryPolls = mt.workerEntryPolls ?? null;
@@ -1692,12 +1705,14 @@ export class LlEmitter {
new Set((mod.globals ?? []).map((g) => g.id)),
{ unions: mod.unions, records: mod.records },
this.immutableFields,
this.nodeIndex,
);
const walkParameters = new Map<string, Set<number>>();
if (this.debug === null) {
const host = {
pointerLocal: (local: IrLocal) => this.plainReference(local.type),
borrowsWithoutOwning: (e: IrExpr) => this.borrowsWithoutOwning(e),
nodes: this.nodeIndex,
};
for (const fn of this.fnByName.values()) {
const preserving = this.referenceEffects.functions.has(fn.name);
@@ -1750,7 +1765,7 @@ export class LlEmitter {
);
this.lentStringParameters =
this.debug === null ? findLentStringParameters(this.fnByName, this.callLifetimes) : new Map();
this.constantCallbacks = findConstantCallbacks(mod, this.callLifetimes);
this.constantCallbacks = findConstantCallbacks(mod, this.callLifetimes, this.nodeIndex);
// Integer clones signal a stack overflow through their result, which
// needs the single-threaded guard; worker programs also poll for
// termination, and debug builds and stack traces keep every frame.
@@ -1892,7 +1907,7 @@ export class LlEmitter {
this.globalTypes.set(g.id, g.type);
if (g.tdz) this.tdzGlobals.set(g.id, g.name);
}
this.stableGlobals = stableGlobals(mod);
this.stableGlobals = stableGlobals(mod, this.nodeIndex);
// Runtime error, EventEmitter, stream, EventTarget and Event classes
// have known layouts. Subclasses embed the runtime prefixes from
// classes.ts.
@@ -6,6 +6,7 @@ import {
type IrUnionDef,
} from "../../ir/ir.js";
import { everyModuleNode } from "../../ir/traverse.js";
import type { IrNodeIndex } from "../../ir/node-index.js";
/** A union of exactly one reference arm and one or two unit arms
* (`C | undefined`, `C | null`, `C | null | undefined`) whose VALUES are
@@ -120,12 +121,12 @@ export class NullableUnions {
/** `shapes`: allow shape unions (debug builds keep boxes, whose
* debug-info layouts describe every arm). */
constructor(mod: IrModule, shapes = true) {
constructor(mod: IrModule, shapes = true, nodes?: IrNodeIndex) {
const emitted = new Set<string>();
for (const cls of mod.classes ?? []) if (!cls.runtime) emitted.add(cls.name);
const records = new Set<string>();
for (const shape of mod.records ?? []) records.add(shape.id);
const boxed = boxedUnionIds(mod);
const boxed = boxedUnionIds(mod, nodes);
// Shape unions stay out of library builds (their host ABI marshals
// boxes) and of programs with lifetime regions (whose walks report a
// reference slot by its type's release entry point).
@@ -277,13 +278,20 @@ function shapeUnion(
/** Unions the runtime itself inspects as tagged boxes keep that
* representation: Map keys and Set elements hash and compare union keys by
* tag and payload inside the C map. */
function boxedUnionIds(mod: IrModule): Set<string> {
function boxedUnionIds(mod: IrModule, nodes?: IrNodeIndex): Set<string> {
const boxed = new Set<string>();
// Only the module's union definitions consult the result.
if (!mod.unions?.length) return boxed;
const mark = (t: IrType): void => {
if (t.kind === "union") boxed.add(t.unionId);
};
if (nodes !== undefined) {
for (const t of nodes.types) {
if (t.kind === "map") mark(t.key);
else if (t.kind === "set") mark(t.elem);
}
return boxed;
}
everyModuleNode(mod, {
expr: () => true,
stmt: () => true,
@@ -342,6 +342,7 @@ export function analyzeBorrowedReturns(emitter: LlEmitter): void {
borrowsWithoutOwning: (e: IrExpr) => emitter.borrowsWithoutOwning(e),
borrowedReturn: (callee: string) => emitter.borrowedReturns.has(callee),
parameterAllowed: (id: string) => allowed.has(id),
nodes: emitter.nodeIndex,
};
return emitter.referenceEffects.functions.has(fn.name)
? host
@@ -9,6 +9,7 @@ import {
type IrUnionDef,
} from "../../ir/ir.js";
import { everyExprChild, everyStmtChild } from "../../ir/traverse.js";
import { eachNodeOfKinds, type IrNodeIndex, type IrNodeKind } from "../../ir/node-index.js";
import { borrowsMapMutationReceiver, borrowsMapReadInputs } from "./map-read-lifetimes.js";
import { preservesRegexInputs, VirtualTargets } from "./reference-effects.js";
import { borrowsStringInputs } from "./string-lifetimes.js";
@@ -227,6 +228,8 @@ export class ReferenceWrites {
/** Fields no code writes once their object is constructed
* (immutability.ts, from cycle-mutability's construction-only facts). */
private readonly immutable: ImmutableFields | null = null,
/** The module's node index, read instead of walking the bodies. */
private readonly nodes?: IrNodeIndex,
) {
this.key(HEAP_KEY);
this.unions = new Map((types.unions ?? []).map((u) => [u.id, u]));
@@ -236,25 +239,25 @@ export class ReferenceWrites {
const constructors = new Set(classes.map((cls) => `%${cls.name}.constructor`));
this.constructors = constructors;
this.freshConstructors = [...functions.values()].every((fn) =>
fn.body.every((s) => this.onlySuperCalls(fn, s, constructors)),
this.onlySuperCalls(fn, constructors),
);
this.callTargets = new CallTargets(functions, this.virtualTargets);
this.callTargets = new CallTargets(functions, this.virtualTargets, nodes);
this.solve(constructors);
}
/** Every direct call of a constructor is a `super` call from a
/** Every direct call of a constructor in `fn` is a `super` call from a
* constructor on its own `this`. */
private onlySuperCalls(fn: IrFunction, s: IrStmt, constructors: ReadonlySet<string>): boolean {
private onlySuperCalls(fn: IrFunction, constructors: ReadonlySet<string>): boolean {
const thisId = constructors.has(fn.name) ? fn.params[0]?.localId : undefined;
const expr = (e: IrExpr): boolean => {
if (e.kind === "call" && constructors.has(e.callee)) {
const self = e.args[0];
if (thisId === undefined || !self || !isThisRef(self, thisId)) return false;
let only = true;
eachNodeOfKinds(fn, this.nodes?.scan(CALL_KINDS), (node) => {
if (node.kind === "call" && constructors.has(node.callee)) {
const self = node.args[0];
if (thisId === undefined || !self || !isThisRef(self, thisId)) only = false;
}
return everyExprChild(e, expr, stmt);
};
const stmt = (node: IrStmt): boolean => everyStmtChild(node, expr, stmt);
return stmt(s);
return only;
});
return only;
}
private key(k: string): number {
@@ -299,7 +302,7 @@ export class ReferenceWrites {
}
const thisId = external && constructors.has(fn.name) ? (fn.params[0]?.localId ?? null) : null;
const boxed = boxedLocals(fn);
const privates = privateArrays(fn, boxed);
const privates = privateArrays(fn, boxed, this.nodes);
const sink: WriteSink = {
add: (k) => {
// Interned for queries; a callee's locals are private to it.
@@ -749,7 +752,7 @@ export class ReferenceWrites {
enter(fn: IrFunction): void {
this.current = fn;
this.currentBoxed = boxedLocals(fn);
this.currentPrivateArrays = privateArrays(fn, this.currentBoxed);
this.currentPrivateArrays = privateArrays(fn, this.currentBoxed, this.nodes);
this.currentLocals = new Set(fn.locals.map((local) => local.id));
this.inConstructor = this.constructors.has(fn.name);
for (const p of fn.params) this.currentLocals.add(p.localId);
@@ -973,6 +976,29 @@ function privateReceiver(arr: IrExpr, privates: ReadonlySet<string>): boolean {
* element out nor let the array escape. */
const PRIVATE_ARRAY_METHODS = new Set(["length", "push"]);
/** The nodes the scans below read. */
const CALL_KINDS: readonly IrNodeKind[] = ["call"];
const CALL_SITE_KINDS: readonly IrNodeKind[] = ["call", "new", "virtualCall", "closure"];
const DEFINITION_KINDS: readonly IrNodeKind[] = ["assignExpr", "incDec", "varDecl", "assign"];
/** The nodes privateArrays reads. */
const PRIVATE_ARRAY_KINDS: readonly IrNodeKind[] = [
"varRef",
"assignExpr",
"incDec",
"closure",
"classRef",
"arrIntrinsic",
"arrayHas",
"varDecl",
"assign",
"arraySet",
"arraySetUndefined",
"arrayDelete",
"arraySetLength",
"return",
];
/** Array locals whose storage the function allocated itself and that
* nothing else can reach before the function returns them: declared once
* from an array literal or `new Array(n)`, never rebound, unboxed, and
@@ -985,7 +1011,11 @@ const PRIVATE_ARRAY_METHODS = new Set(["length", "push"]);
* stay out of the function's summary (a caller's borrows predate the
* array). The literal map, filter and fused-pipeline helpers fill their
* results this way. */
function privateArrays(fn: IrFunction, boxed: ReadonlySet<string>): ReadonlySet<string> {
function privateArrays(
fn: IrFunction,
boxed: ReadonlySet<string>,
nodes: IrNodeIndex | undefined,
): ReadonlySet<string> {
if (fn.async || fn.generator) return new Set();
const candidates = new Set<string>();
for (const local of fn.locals)
@@ -1001,54 +1031,50 @@ function privateArrays(fn: IrFunction, boxed: ReadonlySet<string>): ReadonlySet<
if (e.kind === "varRef" && candidates.has(e.localId))
permitted.set(e, (permitted.get(e) ?? 0) + 1);
};
const expr = (e: IrExpr): boolean => {
switch (e.kind) {
eachNodeOfKinds(fn, nodes?.scan(PRIVATE_ARRAY_KINDS), (node) => {
switch (node.kind) {
case "varRef": {
const left = permitted.get(e) ?? 0;
if (left === 0) rejected.add(e.localId);
else permitted.set(e, left - 1);
const left = permitted.get(node) ?? 0;
if (left === 0) rejected.add(node.localId);
else permitted.set(node, left - 1);
break;
}
case "assignExpr":
case "incDec":
rejected.add(e.localId);
rejected.add(node.localId);
break;
case "closure":
case "classRef":
for (const id of e.captures ?? []) rejected.add(id);
for (const id of node.captures ?? []) rejected.add(id);
break;
case "arrIntrinsic":
if (PRIVATE_ARRAY_METHODS.has(e.method)) permit(e.receiver);
if (PRIVATE_ARRAY_METHODS.has(node.method)) permit(node.receiver);
break;
case "arrayHas":
permit(e.arr);
permit(node.arr);
break;
}
return everyExprChild(e, expr, stmt);
};
const stmt = (s: IrStmt): boolean => {
switch (s.kind) {
case "varDecl":
declarations.set(s.localId, (declarations.get(s.localId) ?? 0) + 1);
if (s.init === null || (s.init.kind !== "arrayLit" && s.init.kind !== "arrayNewLen"))
rejected.add(s.localId);
declarations.set(node.localId, (declarations.get(node.localId) ?? 0) + 1);
if (
node.init === null ||
(node.init.kind !== "arrayLit" && node.init.kind !== "arrayNewLen")
)
rejected.add(node.localId);
break;
case "assign":
rejected.add(s.localId);
rejected.add(node.localId);
break;
case "arraySet":
case "arraySetUndefined":
case "arrayDelete":
case "arraySetLength":
permit(s.arr);
permit(node.arr);
break;
case "return":
if (s.value !== null) permit(s.value);
if (node.value !== null) permit(node.value);
break;
}
return everyStmtChild(s, expr, stmt);
};
fn.body.every(stmt);
});
const out = new Set<string>();
for (const id of candidates) if (!rejected.has(id) && declarations.get(id) === 1) out.add(id);
return out;
@@ -1081,30 +1107,30 @@ class CallTargets {
constructor(
private readonly functions: ReadonlyMap<string, IrFunction>,
virtualTargets: VirtualTargets,
private readonly nodes: IrNodeIndex | undefined,
) {
const site = (callee: string, caller: IrFunction, args: readonly IrExpr[]): void => {
let list = this.sites.get(callee);
if (!list) this.sites.set(callee, (list = []));
list.push({ caller, args });
};
const scan = nodes?.scan(CALL_SITE_KINDS);
for (const fn of functions.values()) {
if (fn.captures !== undefined || fn.classCaptures !== undefined) this.opaque.add(fn.name);
const expr = (e: IrExpr): boolean => {
if (e.kind === "call") site(e.callee, fn, e.args);
else if (e.kind === "new") site(`%${e.className}.constructor`, fn, [e, ...e.args]);
else if (e.kind === "virtualCall") {
const targets = virtualTargets.targets(e.className, e.method);
for (const t of targets ?? []) site(t, fn, e.args);
} else if (e.kind === "closure") {
this.opaque.add(e.fnName);
let list = this.creators.get(e.fnName);
if (!list) this.creators.set(e.fnName, (list = []));
eachNodeOfKinds(fn, scan, (node) => {
if (node.kind === "call") site(node.callee, fn, node.args);
else if (node.kind === "new")
site(`%${node.className}.constructor`, fn, [node, ...node.args]);
else if (node.kind === "virtualCall") {
const targets = virtualTargets.targets(node.className, node.method);
for (const t of targets ?? []) site(t, fn, node.args);
} else if (node.kind === "closure") {
this.opaque.add(node.fnName);
let list = this.creators.get(node.fnName);
if (!list) this.creators.set(node.fnName, (list = []));
list.push(fn);
}
return everyExprChild(e, expr, stmt);
};
const stmt = (st: IrStmt): boolean => everyStmtChild(st, expr, stmt);
fn.body.every(stmt);
});
}
// Fixpoint over function-typed parameter positions.
const positions: [IrFunction, number][] = [];
@@ -1225,18 +1251,13 @@ class CallTargets {
defs = new Map();
const boxed = new Set(fn.locals.filter((l) => l.boxed).map((l) => l.id));
const counts = new Map<string, number>();
const expr = (e: IrExpr): boolean => {
if (e.kind === "assignExpr" || e.kind === "incDec") counts.set(e.localId, 2);
return everyExprChild(e, expr, stmt);
};
const stmt = (st: IrStmt): boolean => {
if (st.kind === "varDecl") {
counts.set(st.localId, (counts.get(st.localId) ?? 0) + 1);
defs!.set(st.localId, st.init);
} else if (st.kind === "assign") counts.set(st.localId, 2);
return everyStmtChild(st, expr, stmt);
};
fn.body.every(stmt);
eachNodeOfKinds(fn, this.nodes?.scan(DEFINITION_KINDS), (node) => {
if (node.kind === "assignExpr" || node.kind === "incDec") counts.set(node.localId, 2);
else if (node.kind === "varDecl") {
counts.set(node.localId, (counts.get(node.localId) ?? 0) + 1);
defs!.set(node.localId, node.init);
} else if (node.kind === "assign") counts.set(node.localId, 2);
});
for (const [local, n] of counts) if (n !== 1 || boxed.has(local)) defs.set(local, null);
this.definitions.set(fn, defs);
}
@@ -1,5 +1,5 @@
import { isRefCounted, type IrExpr, type IrFunction, type IrLocal } from "../../ir/ir.js";
import { everyStmtList } from "../../ir/traverse.js";
import { eachNodeOfKinds, type IrNodeIndex, type IrNodeKind } from "../../ir/node-index.js";
/** What the emitter knows about a projection's storage. */
export interface WalkBorrowHost {
@@ -22,8 +22,24 @@ export interface WalkBorrowHost {
* (whose own rebinding releases nothing). Absent: the body preserves
* every edge. */
unwritten?(value: IrExpr, candidates: ReadonlySet<string>): boolean;
/** The module's node index, read instead of walking the body. */
nodes?: IrNodeIndex;
}
/** The node kinds analyzeWalks reads from a body. */
const WALK_KINDS: readonly IrNodeKind[] = [
"varDecl",
"assign",
"forOf",
"rethrow",
"tryCatch",
"return",
"assignExpr",
"incDec",
"closure",
"classRef",
];
export interface WalkFacts {
locals: ReadonlySet<string>;
/** Every return yields a walk (or a throw), so the body can return its
@@ -71,44 +87,36 @@ export function analyzeWalks(fn: IrFunction, host: WalkBorrowHost): WalkFacts {
if (!list) definitions.set(id, (list = []));
list.push(value);
};
everyStmtList(fn.body, {
stmt: (s) => {
switch (s.kind) {
case "varDecl":
define(s.localId, s.init);
break;
case "assign":
define(s.localId, s.value);
break;
case "forOf":
excluded.add(s.localId);
break;
case "rethrow":
excluded.add(s.localId);
break;
case "tryCatch":
guarded = true;
if (s.catchLocalId !== null) excluded.add(s.catchLocalId);
break;
case "return":
returns.push(s.value);
break;
}
return true;
},
expr: (e) => {
switch (e.kind) {
case "assignExpr":
case "incDec":
excluded.add(e.localId);
break;
case "closure":
case "classRef":
for (const id of e.captures ?? []) excluded.add(id);
break;
}
return true;
},
eachNodeOfKinds(fn, host.nodes?.scan(WALK_KINDS), (node) => {
switch (node.kind) {
case "varDecl":
define(node.localId, node.init);
break;
case "assign":
define(node.localId, node.value);
break;
case "forOf":
excluded.add(node.localId);
break;
case "rethrow":
excluded.add(node.localId);
break;
case "tryCatch":
guarded = true;
if (node.catchLocalId !== null) excluded.add(node.catchLocalId);
break;
case "return":
returns.push(node.value);
break;
case "assignExpr":
case "incDec":
excluded.add(node.localId);
break;
case "closure":
case "classRef":
for (const id of node.captures ?? []) excluded.add(id);
break;
}
});
const plain = (id: string): boolean => {
const local = locals.get(id);
+27 -11
View File
@@ -3,7 +3,8 @@
import type { IrExpr, IrStmt, IrModule, IrType, SrcLoc } from "../ir/ir.js";
import { isFfiCallbackParam, MAY_THROW_ARR_METHODS, MAY_THROW_BYTES_METHODS } from "../ir/ir.js";
import { MAY_THROW_LIB_FNS } from "../ir/builtin-effects.js";
import { everyExprChild, everyStmtChild, everyStmtList } from "../ir/traverse.js";
import { everyExprChild, everyStmtChild } from "../ir/traverse.js";
import { eachNodeOfKinds, type IrNodeIndex } from "../ir/node-index.js";
import { hasRetainedFfiCallback } from "./ffi-callbacks.js";
import { computePublishedTypes, writesPublished } from "./publication.js";
@@ -40,6 +41,8 @@ export function computeMayThrow(
explain?: boolean;
/** computePublishedTypes(mod), when the caller already has it. */
published?: ReturnType<typeof computePublishedTypes>;
/** The module's node index, read instead of walking the bodies. */
nodes?: IrNodeIndex;
} = {},
): {
fns: Set<string>;
@@ -52,8 +55,9 @@ export function computeMayThrow(
* entry (and may therefore throw the stack-overflow RangeError). */
stackChecks?: Set<string>;
} {
const terminations = mod.workers === true ? workerTerminationPolls(mod) : null;
const stackChecks = options.stackChecks === true ? stackCheckedFunctions(mod) : null;
const terminations = mod.workers === true ? workerTerminationPolls(mod, options.nodes) : null;
const stackChecks =
options.stackChecks === true ? stackCheckedFunctions(mod, options.nodes) : null;
// Stores that can reach a published object throw the frozen TypeError.
const published =
options.published !== undefined ? options.published : computePublishedTypes(mod);
@@ -898,7 +902,19 @@ function boundedNode(rec: IrExpr | IrStmt): boolean {
* finishes after finitely many steps or reaches a polling callee. `polls`
* names the functions that poll (and therefore may unwind with the
* termination sentinel); `entry` the subset that polls on entry. */
export function workerTerminationPolls(mod: IrModule): {
/** The kinds call-graph census below can skip: boundedNode answers true
* for them, and the census reads nothing else from them. */
const CENSUS_SKIPPED_KINDS: ReadonlySet<string> = new Set(
[...BOUNDED_KINDS].filter(
(kind) =>
!["while", "doWhile", "for", "forOf", "call", "new", "virtualCall", "closure"].includes(kind),
),
);
export function workerTerminationPolls(
mod: IrModule,
nodes?: IrNodeIndex,
): {
entry: Set<string>;
polls: Set<string>;
} {
@@ -914,11 +930,12 @@ export function workerTerminationPolls(mod: IrModule): {
const edges = new Map<string, string[]>();
const entry = new Set<string>();
const polls = new Set<string>();
const scan = nodes?.scanExcept(CENSUS_SKIPPED_KINDS);
for (const fn of mod.functions) {
const out: string[] = [];
let bounded = fn.async !== true && fn.generator === undefined && fn.name !== mod.entry;
let loops = false;
const visit = (rec: IrExpr | IrStmt): boolean => {
const visit = (rec: IrExpr | IrStmt): void => {
if (!boundedNode(rec)) bounded = false;
switch (rec.kind) {
case "while":
@@ -937,9 +954,8 @@ export function workerTerminationPolls(mod: IrModule): {
out.push(...(methodImpls.get(rec.method) ?? []));
break;
}
return true;
};
everyStmtList(fn.body, { expr: visit, stmt: visit });
eachNodeOfKinds(fn, scan, visit);
edges.set(
fn.name,
out.filter((callee) => names.has(callee)),
@@ -960,7 +976,7 @@ export function workerTerminationPolls(mod: IrModule): {
* library call; see boundedNode) also reaches every function that such an
* operation can invoke: closure targets and class members. Non-recursive
* functions, including leaf callbacks, stay check-free. */
export function stackCheckedFunctions(mod: IrModule): Set<string> {
export function stackCheckedFunctions(mod: IrModule, nodes?: IrNodeIndex): Set<string> {
const reentry = "\0reentry";
const methodImpls = new Map<string, string[]>();
const memberPrefixes = new Set<string>();
@@ -975,12 +991,13 @@ export function stackCheckedFunctions(mod: IrModule): Set<string> {
const names = new Set(mod.functions.map((fn) => fn.name));
const edges = new Map<string, string[]>();
const targets = new Set<string>();
const scan = nodes?.scanExcept(CENSUS_SKIPPED_KINDS);
for (const fn of mod.functions) {
const dot = fn.name.indexOf(".");
if (dot > 0 && memberPrefixes.has(fn.name.slice(0, dot + 1))) targets.add(fn.name);
const out: string[] = [];
let reenters = false;
const visit = (rec: IrExpr | IrStmt): boolean => {
const visit = (rec: IrExpr | IrStmt): void => {
if (!boundedNode(rec)) reenters = true;
switch (rec.kind) {
case "call":
@@ -996,9 +1013,8 @@ export function stackCheckedFunctions(mod: IrModule): Set<string> {
targets.add(rec.fnName);
break;
}
return true;
};
everyStmtList(fn.body, { expr: visit, stmt: visit });
eachNodeOfKinds(fn, scan, visit);
const callees = out.filter((callee) => names.has(callee));
if (reenters) callees.push(reentry);
edges.set(fn.name, callees);
+16 -11
View File
@@ -3,7 +3,7 @@
* emitter guards them. */
import type { IrExpr, IrModule, IrStmt, IrType } from "../ir/ir.js";
import { typeKey } from "../ir/ir.js";
import { everyStmtList } from "../ir/traverse.js";
import { eachNodeOfKinds, type IrNodeIndex, type IrNodeKind } from "../ir/node-index.js";
import { computeConstructionFacts } from "./immutability.js";
/** Array methods that write their receiver (the guarded ones). */
@@ -71,17 +71,22 @@ export class PublishedTypes {
}
}
export function computePublishedTypes(mod: IrModule): PublishedTypes | null {
/** The nodes that publish a value: intrinsics. */
const PUBLISHING_KINDS: readonly IrNodeKind[] = ["intrinsic"];
export function computePublishedTypes(mod: IrModule, nodes?: IrNodeIndex): PublishedTypes | null {
const roots: IrType[] = [];
const visit = (e: IrExpr): boolean => {
// Values read from an InternTable were published by whichever thread
// stored them.
if (e.kind === "intrinsic" && (e.name === "threads.publish" || e.name === "itable.value"))
roots.push(e.type);
return true;
};
const scan = nodes?.scan(PUBLISHING_KINDS);
for (const fn of mod.functions)
everyStmtList(fn.body, { expr: visit, stmt: (_s: IrStmt) => true });
eachNodeOfKinds(fn, scan, (node) => {
// Values read from an InternTable were published by whichever thread
// stored them.
if (
node.kind === "intrinsic" &&
(node.name === "threads.publish" || node.name === "itable.value")
)
roots.push(node.type);
});
if (roots.length === 0) return null;
const classes = new Map((mod.classes ?? []).map((c) => [c.name, c]));
@@ -95,7 +100,7 @@ export function computePublishedTypes(mod: IrModule): PublishedTypes | null {
const records = new Map((mod.records ?? []).map((r) => [r.id, r]));
const unions = new Map((mod.unions ?? []).map((u) => [u.id, u]));
const out = new PublishedTypes();
out.privateStores = computeConstructionFacts(mod).privateStores;
out.privateStores = computeConstructionFacts(mod, nodes).privateStores;
const seen = new Set<string>();
const stack = [...roots];
const addHierarchy = (className: string): void => {
@@ -1,15 +1,32 @@
/* Whole-program stack-trace gate (see observesStacks). Pure function of the
* IR module; the emitter keeps a source frame in every function only when
* it answers true. */
import { exprFunctionReference, moduleFunctionReferences } from "../ir/function-references.js";
import {
exprFunctionReference,
FUNCTION_REFERENCE_KINDS,
moduleFunctionReferences,
} from "../ir/function-references.js";
import {
eachNodeOfKinds,
type IrKindScan,
type IrNodeIndex,
type IrNodeKind,
} from "../ir/node-index.js";
import { MAY_THROW_LIB_FNS } from "../ir/builtin-effects.js";
import type { IrExpr, IrFunction, IrModule, IrStmt, IrType } from "../ir/ir.js";
import { MAY_THROW_ARR_METHODS } from "../ir/ir.js";
import { everyExprChild, everyStmtChild, everyStmtList } from "../ir/traverse.js";
import { everyExprChild, everyStmtChild } from "../ir/traverse.js";
import { unreachableStatements } from "./may-throw.js";
import type { PublishedTypes } from "./publication.js";
import { writesPublished } from "./publication.js";
/** The nodes the scans below read: stack reads, the "stack" literal, call
* sites and function references, declarations. */
const STACK_READ_KINDS: readonly IrNodeKind[] = ["libCall", "dynKeyGet"];
const STRING_KINDS: readonly IrNodeKind[] = ["strLit"];
const CENSUS_KINDS: readonly IrNodeKind[] = ["call", ...FUNCTION_REFERENCE_KINDS];
const DECLARATION_KINDS: readonly IrNodeKind[] = ["varDecl"];
/** Lib calls that read a captured stack trace. */
const STACK_READS: ReadonlySet<string> = new Set(["error.stack"]);
@@ -40,10 +57,13 @@ class StackKeys {
// The module walks run on the first question that needs them: a program
// without a dynamic key read asks none.
constructor(private readonly mod: IrModule) {}
constructor(
private readonly mod: IrModule,
private readonly nodes: IrNodeIndex | undefined,
) {}
private spellsStack(): boolean {
return (this.spelled ??= spellsStack(this.mod));
return (this.spelled ??= spellsStack(this.mod, this.nodes));
}
private callCensus(): {
@@ -56,20 +76,18 @@ class StackKeys {
const named = new Set(moduleFunctionReferences(mod));
// Class methods, accessors and constructors are called through dispatch.
const classNames = new Set((mod.classes ?? []).map((cls) => cls.name));
const scan = this.nodes?.scan(CENSUS_KINDS);
for (const fn of mod.functions) {
if (isClassMember(fn.name, classNames)) named.add(fn.name);
everyStmtList(fn.body, {
stmt: () => true,
expr: (expr) => {
if (expr.kind === "call") {
const sites = calls.get(expr.callee) ?? [];
sites.push({ caller: fn, args: expr.args });
calls.set(expr.callee, sites);
}
const referenced = exprFunctionReference(expr);
eachNodeOfKinds(fn, scan, (node) => {
if (node.kind === "call") {
const sites = calls.get(node.callee) ?? [];
sites.push({ caller: fn, args: node.args });
calls.set(node.callee, sites);
} else if (node.kind === "closure" || node.kind === "orDefault") {
const referenced = exprFunctionReference(node);
if (referenced !== undefined) named.add(referenced);
return true;
},
}
});
}
return (this.census = { calls, named });
@@ -127,13 +145,9 @@ class StackKeys {
let inits = this.inits.get(fn);
if (inits === undefined) {
inits = new Map();
everyStmtList(fn.body, {
stmt: (stmt) => {
if (stmt.kind === "varDecl")
inits!.set(stmt.localId, inits!.has(stmt.localId) ? null : (stmt.init ?? null));
return true;
},
expr: () => true,
eachNodeOfKinds(fn, this.nodes?.scan(DECLARATION_KINDS), (node) => {
if (node.kind === "varDecl")
inits!.set(node.localId, inits!.has(node.localId) ? null : (node.init ?? null));
});
this.inits.set(fn, inits);
}
@@ -161,21 +175,25 @@ function readsStack(fn: IrFunction, expr: IrExpr, keys: StackKeys): boolean {
}
/** Whether the module contains the string literal "stack". */
function spellsStack(mod: IrModule): boolean {
return mod.functions.some(
(fn) =>
!everyStmtList(fn.body, {
stmt: () => true,
expr: (expr) => !(expr.kind === "strLit" && expr.value === "stack"),
}),
);
function spellsStack(mod: IrModule, nodes: IrNodeIndex | undefined): boolean {
const scan = nodes?.scan(STRING_KINDS);
return mod.functions.some((fn) => {
let found = false;
eachNodeOfKinds(fn, scan, (node) => {
found = node.kind === "strLit" && node.value === "stack";
return !found;
});
return found;
});
}
function containsStackRead(fn: IrFunction, keys: StackKeys): boolean {
return !everyStmtList(fn.body, {
stmt: () => true,
expr: (expr) => !readsStack(fn, expr, keys),
function containsStackRead(fn: IrFunction, keys: StackKeys, scan: IrKindScan | undefined): boolean {
let found = false;
eachNodeOfKinds(fn, scan, (node, statement) => {
found = !statement && readsStack(fn, node as IrExpr, keys);
return !found;
});
return found;
}
/** Visit every expression of `body` that execution can reach: statements
@@ -199,9 +217,10 @@ function visitLiveExprs(body: IrStmt[], visit: (expr: IrExpr) => void): void {
* Flight server keep development-only stack parsing behind `throw` stubs,
* and those reads must not cost every call of the program a frame.
* Programs without any stack read answer false without the walk. */
export function observesStacks(mod: IrModule): boolean {
const keys = new StackKeys(mod);
if (!mod.functions.some((fn) => containsStackRead(fn, keys))) return false;
export function observesStacks(mod: IrModule, nodes?: IrNodeIndex): boolean {
const keys = new StackKeys(mod, nodes);
const reads = nodes?.scan(STACK_READ_KINDS);
if (!mod.functions.some((fn) => containsStackRead(fn, keys, reads))) return false;
const byName = new Map(mod.functions.map((fn) => [fn.name, fn]));
const classNames = new Set((mod.classes ?? []).map((cls) => cls.name));
const pending: string[] = [...moduleFunctionReferences(mod)];
@@ -2,6 +2,7 @@ import type { IrContracts, IrExpr, IrFunction, IrModule, IrStmt } from "./ir.js"
import { exprFunctionReference, moduleFunctionReferences } from "./function-references.js";
import { reparseExpr, reparseStmt } from "./serialize.js";
import { everyStmtList, mapExprChildren, mapStmtChildren } from "./traverse.js";
import { eachNodeOfKinds, type IrNodeIndex, type IrNodeKind } from "./node-index.js";
/** Largest body (IR nodes) that is copied per callback target. */
const MAX_FUNCTION_NODES = 300;
@@ -15,11 +16,24 @@ const COPIED_NODE_FRACTION = 0.25;
* closure it creates writes through its environment (and transitively the
* closures those create). A parameter or capture absent from its function's
* set holds the value it entered with for the whole call. */
export function writtenLocals(mod: IrModule): Map<string, Set<string>> {
return rebindings(mod).written;
export function writtenLocals(mod: IrModule, nodes?: IrNodeIndex): Map<string, Set<string>> {
return rebindings(mod, nodes).written;
}
function rebindings(mod: IrModule): {
/** The nodes that rebind a local or capture bindings. */
const REBINDING_KINDS: readonly IrNodeKind[] = [
"assign",
"forOf",
"tryCatch",
"assignExpr",
"incDec",
"closure",
];
function rebindings(
mod: IrModule,
nodes?: IrNodeIndex,
): {
written: Map<string, Set<string>>;
/** Bindings of each function that a closure it creates writes. */
closureWritten: Map<string, Set<string>>;
@@ -27,20 +41,15 @@ function rebindings(mod: IrModule): {
const written = new Map<string, Set<string>>();
const closureWritten = new Map<string, Set<string>>();
const sites: { owner: string; fnName: string; captures: string[] }[] = [];
const scan = nodes?.scan(REBINDING_KINDS);
for (const fn of mod.functions) {
const ids = new Set<string>();
everyStmtList(fn.body, {
stmt: (stmt) => {
if (stmt.kind === "assign" || stmt.kind === "forOf") ids.add(stmt.localId);
if (stmt.kind === "tryCatch" && stmt.catchLocalId !== null) ids.add(stmt.catchLocalId);
return true;
},
expr: (expr) => {
if (expr.kind === "assignExpr" || expr.kind === "incDec") ids.add(expr.localId);
if (expr.kind === "closure" && expr.captures.length)
sites.push({ owner: fn.name, fnName: expr.fnName, captures: expr.captures });
return true;
},
eachNodeOfKinds(fn, scan, (node) => {
if (node.kind === "assign" || node.kind === "forOf") ids.add(node.localId);
else if (node.kind === "tryCatch" && node.catchLocalId !== null) ids.add(node.catchLocalId);
else if (node.kind === "assignExpr" || node.kind === "incDec") ids.add(node.localId);
else if (node.kind === "closure" && node.captures.length)
sites.push({ owner: fn.name, fnName: node.fnName, captures: node.captures });
});
written.set(fn.name, ids);
}
+15 -15
View File
@@ -1,6 +1,7 @@
import { isRefCounted, type IrClassDef, type IrExpr, type IrModule, type IrStmt } from "./ir.js";
import { dynamicFieldStores, type DynamicFieldStores } from "./int32-slots.js";
import { everyExprChild, everyStmtChild } from "./traverse.js";
import { eachNodeOfKinds, type IrNodeIndex, type IrNodeKind } from "./node-index.js";
/** Whole-program final reference fields.
*
@@ -75,6 +76,9 @@ export function constructorClass(fnName: string): string | null {
: null;
}
/** The nodes that write a class field. */
const FIELD_WRITE_KINDS: readonly IrNodeKind[] = ["fieldSet", "fieldIncDec"];
/** `scalars` extends the analysis from reference-counted fields to fields of
* every type, for consumers that read a final field's value once instead of
* borrowing it (field-values.ts). */
@@ -82,6 +86,7 @@ export function analyzeFinalFields(
mod: IrModule,
scalars = false,
stores?: DynamicFieldStores,
nodes?: IrNodeIndex,
): FinalFields {
const classes = new Map((mod.classes ?? []).map((c) => [c.name, c]));
const finals = new FinalFields(classes);
@@ -121,30 +126,25 @@ export function analyzeFinalFields(
}
if (finals.size === 0) return finals;
const scan = nodes?.scan(FIELD_WRITE_KINDS);
for (const fn of mod.functions) {
const ctor = constructorClass(fn.name);
const self = ctor !== null ? fn.params[0]?.localId : undefined;
const selfLocal = self !== undefined ? fn.locals.find((l) => l.id === self) : undefined;
// `this` must name the object under construction for the whole body.
const constructing = selfLocal !== undefined && !selfLocal.boxed && !reassigns(fn.body, self!);
const stmt = (s: IrStmt): boolean => {
if (s.kind === "fieldSet") {
const family = finals.family(s.className, s.field);
if (family !== null && finals.isFinal(s.className, s.field)) {
eachNodeOfKinds(fn, scan, (node) => {
if (node.kind === "fieldSet") {
const family = finals.family(node.className, node.field);
if (family !== null && finals.isFinal(node.className, node.field)) {
const own =
constructing && ctor !== null && inFamily(ctor, family) && isSelf(s.obj, self!);
constructing && ctor !== null && inFamily(ctor, family) && isSelf(node.obj, self!);
if (!own) finals.drop(family, `written in ${fn.name}`);
}
}
return everyStmtChild(s, expr, stmt);
};
// `o.n++` updates a numeric field in place; reference fields never see it.
const expr = (e: IrExpr): boolean => {
if (e.kind === "fieldIncDec" && finals.isFinal(e.className, e.field))
finals.drop(finals.family(e.className, e.field)!, `updated in ${fn.name}`);
return everyExprChild(e, expr, stmt);
};
for (const s of fn.body) stmt(s);
} else if (node.kind === "fieldIncDec" && finals.isFinal(node.className, node.field))
// `o.n++` updates a numeric field in place; reference fields never see it.
finals.drop(finals.family(node.className, node.field)!, `updated in ${fn.name}`);
});
}
return finals;
}
@@ -11,6 +11,9 @@ export function exprFunctionReference(expr: IrExpr): string | undefined {
return undefined;
}
/** The expression kinds exprFunctionReference answers for. */
export const FUNCTION_REFERENCE_KINDS = ["closure", "orDefault"] as const;
/** Functions the module's metadata names: the entry, library exports and
* the class prototype helpers the runtime calls. Class methods and
* constructors follow the `%<class>.<method>` convention instead of being
@@ -1,11 +1,27 @@
import type { IrExpr, IrFunction, IrGlobal, IrModule, IrStmt } from "./ir.js";
import { everyExprChild, everyStmtChild, everyStmtList } from "./traverse.js";
import { everyExprChild, everyStmtChild } from "./traverse.js";
import { eachNodeOfKinds, type IrNodeIndex, type IrNodeKind } from "./node-index.js";
/** The nodes findInitializerBindings reads: binding uses and captures. */
const USE_KINDS: readonly IrNodeKind[] = [
"varRef",
"assignExpr",
"incDec",
"closure",
"assign",
"varDecl",
"forOf",
"tryCatch",
];
/** Globals private to a guarded, synchronous initializer have local numeric
* dataflow: no other function can replace their slots, and reentrant module
* evaluation returns before reaching them. Keep the actual global storage
* and lifetime; these facts are only inputs to numeric and extent analysis. */
export function findInitializerBindings(mod: IrModule): ReadonlyMap<string, IrGlobal[]> {
export function findInitializerBindings(
mod: IrModule,
nodes?: IrNodeIndex,
): ReadonlyMap<string, IrGlobal[]> {
const result = new Map<string, IrGlobal[]>();
// Embedders can reset library state between entries. Async initialization
// and lifted captures also remain outside this synchronous proof.
@@ -17,6 +33,7 @@ export function findInitializerBindings(mod: IrModule): ReadonlyMap<string, IrGl
for (const cls of mod.classes ?? []) {
for (const capture of cls.localCaptures ?? []) excluded.add(capture.localId);
}
const scan = nodes?.scan(USE_KINDS);
for (const fn of mod.functions) {
for (const capture of [...(fn.captures ?? []), ...(fn.classCaptures ?? [])])
excluded.add(capture.localId);
@@ -26,18 +43,13 @@ export function findInitializerBindings(mod: IrModule): ReadonlyMap<string, IrGl
owners.set(id, owner === undefined || owner === fn.name ? fn.name : null);
if (write) writes.set(id, (writes.get(id) ?? 0) + 1);
};
everyStmtList(fn.body, {
expr: (e) => {
if (e.kind === "varRef") use(e.localId, false);
if (e.kind === "assignExpr" || e.kind === "incDec") use(e.localId, true);
if (e.kind === "closure") for (const id of e.captures) excluded.add(id);
return true;
},
stmt: (s) => {
if (s.kind === "assign" || s.kind === "varDecl" || s.kind === "forOf") use(s.localId, true);
if (s.kind === "tryCatch" && s.catchLocalId) excluded.add(s.catchLocalId);
return true;
},
eachNodeOfKinds(fn, scan, (node) => {
if (node.kind === "varRef") use(node.localId, false);
else if (node.kind === "assignExpr" || node.kind === "incDec") use(node.localId, true);
else if (node.kind === "closure") for (const id of node.captures) excluded.add(id);
else if (node.kind === "assign" || node.kind === "varDecl" || node.kind === "forOf")
use(node.localId, true);
else if (node.kind === "tryCatch" && node.catchLocalId) excluded.add(node.catchLocalId);
});
}
const guarded = new Set<string>();
+133 -33
View File
@@ -8,10 +8,20 @@ import {
type IntegerRanges,
type IntegerSlotFacts,
} from "./integer-ranges.js";
import { exprFunctionReference, moduleFunctionReferences } from "./function-references.js";
import {
exprFunctionReference,
FUNCTION_REFERENCE_KINDS,
moduleFunctionReferences,
} from "./function-references.js";
import {
eachNodeOfKinds,
type IrKindScan,
type IrNodeIndex,
type IrNodeKind,
} from "./node-index.js";
import { findInitializerBindings, withInitializerBindings } from "./initializer-bindings.js";
import { checkableInt32Allocations, int32ArrayStores } from "./int32-arrays.js";
import { everyExprChild, everyStmtChild, everyStmtList, everyTypeChild } from "./traverse.js";
import { everyExprChild, everyStmtChild, everyTypeChild } from "./traverse.js";
/** The storage slot an array-valued expression reads, as an
* IrContracts.int32Elements key: a local or parameter
@@ -467,10 +477,61 @@ const DYN_READ_ONLY = new Set<string>([
"text.decodeStream",
"worker.new",
]);
/** The node kinds each walk below reads. */
const LIB_CALL_KINDS: readonly IrNodeKind[] = ["libCall"];
const CALL_FACT_KINDS: readonly IrNodeKind[] = [
...FUNCTION_REFERENCE_KINDS,
"virtualCall",
"instanceOfValue",
"varRef",
"assignExpr",
"incDec",
"classRef",
"varDecl",
"assign",
"forOf",
];
const CENSUS_KINDS: readonly IrNodeKind[] = [
"call",
"new",
"varRef",
"fieldGet",
"fieldIncDec",
"closure",
"classRef",
"incDec",
"forOf",
"tryCatch",
];
const DECLARATION_KINDS: readonly IrNodeKind[] = ["varDecl"];
const CANDIDATE_WRITE_KINDS: readonly IrNodeKind[] = [
"assign",
"varDecl",
"fieldSet",
"return",
"call",
"new",
"assignExpr",
];
const CALL_KINDS: readonly IrNodeKind[] = ["call", "new"];
const SLOT_WRITE_KINDS: readonly IrNodeKind[] = ["return", "call", "new"];
const NARROW_WRITE_KINDS: readonly IrNodeKind[] = ["assign", "varDecl", "fieldSet", "assignExpr"];
/** Visits the nodes of `fn` in walk order; with `scan`, only those of its
* kinds, read from the module's node index (the visitors must ignore the
* other kinds). */
function walkFunction(
fn: IrFunction,
scan: IrKindScan | undefined,
visit: { expr?: (e: IrExpr) => void; stmt?: (s: IrStmt) => void },
): void {
if (scan !== undefined) {
eachNodeOfKinds(fn, scan, (node, statement) => {
if (statement) visit.stmt?.(node as IrStmt);
else visit.expr?.(node as IrExpr);
});
return;
}
const expr = (e: IrExpr): boolean => {
visit.expr?.(e);
return everyExprChild(e, expr, stmt);
@@ -498,6 +559,7 @@ export function dynamicFieldStores(
mod: IrModule,
records = new Map((mod.records ?? []).map((r) => [r.id, r])),
unions = new Map((mod.unions ?? []).map((u) => [u.id, u])),
nodes?: IrNodeIndex,
): DynamicFieldStores {
const dynTypes = new Map<IrType, boolean>();
const dynNamed = new Map<string, boolean>();
@@ -537,9 +599,10 @@ export function dynamicFieldStores(
let dynWritesAll = false;
// A store whose key is not a literal names every field: later stores add
// nothing, so the walk stops there.
const scan = nodes?.scan(LIB_CALL_KINDS);
for (const fn of mod.functions) {
if (dynWritesAll) break;
walkFunction(fn, {
walkFunction(fn, scan, {
expr: (e) => {
if (dynWritesAll) return;
if (
@@ -642,7 +705,7 @@ export function dynamicallyStoredFamilies(
}
};
for (const fn of mod.functions)
walkFunction(fn, {
walkFunction(fn, undefined, {
expr: (e) => {
const dynResult = DYN_KINDS.has(e.type.kind);
const boundary = BOUNDARY_KINDS.has(e.kind);
@@ -682,6 +745,7 @@ export function functionCallFacts(
mod: IrModule,
classes: ReadonlyMap<string, IrClassDef>,
subtree: (className: string) => string[],
nodes?: IrNodeIndex,
): {
valueEscaped: Set<string>;
virtualTargets: Set<string>;
@@ -716,7 +780,7 @@ export function functionCallFacts(
const count = (map: Map<string, number>, id: string): void => {
map.set(id, (map.get(id) ?? 0) + 1);
};
walkFunction(fn, {
walkFunction(fn, nodes?.scan(CALL_FACT_KINDS), {
expr: (e) => {
const referenced = exprFunctionReference(e);
if (referenced !== undefined) escaped.add(referenced);
@@ -752,9 +816,12 @@ export function functionCallFacts(
export interface Int32SlotInputs {
dynamicStores?: DynamicFieldStores;
initializerBindings?: ReadonlyMap<string, IrGlobal[]>;
/** The module's node index, read instead of walking the bodies. */
nodes?: IrNodeIndex;
}
export function analyzeInt32Slots(mod: IrModule, inputs: Int32SlotInputs = {}): Int32Slots {
const nodes = inputs.nodes;
const classes = new Map((mod.classes ?? []).map((c) => [c.name, c]));
const slots = new Int32Slots(classes);
if (mod.lib) return slots;
@@ -844,7 +911,7 @@ export function analyzeInt32Slots(mod: IrModule, inputs: Int32SlotInputs = {}):
/* ── functions: escapes, parameter and return candidates ── */
const functions = new Map(mod.functions.map((fn) => [fn.name, fn]));
const calls = functionCallFacts(mod, classes, subtree);
const calls = functionCallFacts(mod, classes, subtree, nodes);
const escaped = new Set([...calls.valueEscaped, ...calls.virtualTargets]);
const virtual = calls.runtimeVirtual;
@@ -901,7 +968,7 @@ export function analyzeInt32Slots(mod: IrModule, inputs: Int32SlotInputs = {}):
for (const capture of fn.classCaptures ?? [])
unstableBoxes.add(boxKey(fn.name, capture.localId));
const read = new Set<string>();
walkFunction(fn, {
walkFunction(fn, nodes?.scan(CENSUS_KINDS), {
expr: (e) => {
if (e.kind === "call") {
called.add(e.callee);
@@ -1001,7 +1068,7 @@ export function analyzeInt32Slots(mod: IrModule, inputs: Int32SlotInputs = {}):
let ids = initializedDeclarations.get(fn.name);
if (ids === undefined) {
const found = new Set<string>();
walkFunction(fn, {
walkFunction(fn, nodes?.scan(DECLARATION_KINDS), {
stmt: (s) => {
if (s.kind === "varDecl" && s.init) found.add(s.localId);
},
@@ -1069,28 +1136,45 @@ export function analyzeInt32Slots(mod: IrModule, inputs: Int32SlotInputs = {}):
}
return false;
};
const candidateWrites = nodes?.scan(CANDIDATE_WRITE_KINDS);
const hasCandidateWrite = (fn: IrFunction): boolean => {
if (arrayCandidates.has(fn.name)) return true;
const box = (localId: string): boolean => slots.hasBox(slots.boxGroup(fn.name, localId));
const returns = slots.hasReturn(fn.name);
return !everyStmtList(fn.body, {
stmt: (s) => {
if (s.kind === "assign") return !box(s.localId);
if (s.kind === "varDecl") return s.init === null || !box(s.localId);
if (s.kind === "fieldSet") {
const family = slots.family(s.className, s.field);
return !family || !slots.hasFamily(family);
let found = false;
eachNodeOfKinds(fn, candidateWrites, (node) => {
switch (node.kind) {
case "assign":
found = box(node.localId);
break;
case "varDecl":
found = node.init !== null && box(node.localId);
break;
case "fieldSet": {
const family = slots.family(node.className, node.field);
found = !!family && slots.hasFamily(family);
break;
}
return !(s.kind === "return" && returns);
},
expr: (e) => {
if (e.kind === "call") return !candidateArgs(functions.get(e.callee), e.args.length, 0);
if (e.kind === "new")
return !candidateArgs(functions.get(`%${e.className}.constructor`), e.args.length, 1);
if (e.kind === "assignExpr") return !box(e.localId);
return true;
},
case "return":
found = returns;
break;
case "call":
found = candidateArgs(functions.get(node.callee), node.args.length, 0);
break;
case "new":
found = candidateArgs(
functions.get(`%${node.className}.constructor`),
node.args.length,
1,
);
break;
case "assignExpr":
found = box(node.localId);
break;
}
return !found;
});
return found;
};
const provenByFunction = new Map<string, IrExpr[]>();
// Each function's parameter and result writes under its latest analysis:
@@ -1107,7 +1191,7 @@ export function analyzeInt32Slots(mod: IrModule, inputs: Int32SlotInputs = {}):
const ranges: IntegerRanges = candidate
? analyzeIntegerRanges(numeric(fn), slots.facts(fn.name))
: new Map();
if (ranges.size > 0) lastWrites.set(fn.name, slotWrites(fn, ranges, functions));
if (ranges.size > 0) lastWrites.set(fn.name, slotWrites(fn, ranges, functions, nodes));
else lastWrites.delete(fn.name);
const proven: IrExpr[] = [];
// The walk below acts only at writes into candidate slots.
@@ -1182,7 +1266,7 @@ export function analyzeInt32Slots(mod: IrModule, inputs: Int32SlotInputs = {}):
if (int32(value)) settle(`box:${group}`, value);
else if (dropBox(group, `write in ${fn.name} is not int32`)) invalidate(`box:${group}`);
};
walkFunction(fn, {
walkFunction(fn, nodes?.scan(CANDIDATE_WRITE_KINDS), {
stmt: (s) => {
if (s.kind === "assign") boxWrite(s.localId, s.value);
else if (s.kind === "varDecl" && s.init) boxWrite(s.localId, s.init);
@@ -1218,10 +1302,23 @@ export function analyzeInt32Slots(mod: IrModule, inputs: Int32SlotInputs = {}):
for (const id of ids)
if (slots.hasInt32ElementSlot(`l:${fnName}\0${id}`))
slots.int32Arrays.add(`${fnName}\0${id}`);
const ranged = rangedParameters(slots, functions, retry, numeric, (callee) =>
readers.get(`ret:${callee}`),
const ranged = rangedParameters(
slots,
functions,
retry,
numeric,
(callee) => readers.get(`ret:${callee}`),
nodes,
);
narrowSlotRanges(
slots,
functions,
numeric,
lastWrites,
ranged,
(slot) => readers.get(slot),
nodes,
);
narrowSlotRanges(slots, functions, numeric, lastWrites, ranged, (slot) => readers.get(slot));
return slots;
}
@@ -1256,6 +1353,7 @@ function rangedParameters(
retry: ReadonlySet<string>,
numeric: (fn: IrFunction) => IrFunction,
callersOf: (callee: string) => ReadonlySet<string> | undefined,
nodes: IrNodeIndex | undefined,
): Set<string> {
const changed = new Set<string>();
if (retry.size === 0) return changed;
@@ -1325,7 +1423,7 @@ function rangedParameters(
contribute(callee, param.localId, ranges.get(arg) ?? null);
});
};
walkFunction(fn, {
walkFunction(fn, nodes?.scan(CALL_KINDS), {
expr: (e) => {
if (e.kind === "call") args(functions.get(e.callee), e.args, 0);
else if (e.kind === "new") args(functions.get(`%${e.className}.constructor`), e.args, 1);
@@ -1371,6 +1469,7 @@ function slotWrites(
fn: IrFunction,
ranges: IntegerRanges,
functions: ReadonlyMap<string, IrFunction>,
nodes: IrNodeIndex | undefined,
): Map<string, IntegerRange | null> {
const writes = new Map<string, IntegerRange | null>();
const write = (key: string, value: IrExpr | null): void => {
@@ -1385,7 +1484,7 @@ function slotWrites(
if (param) write(`param:${callee.name}\0${param.localId}`, arg);
});
};
walkFunction(fn, {
walkFunction(fn, nodes?.scan(SLOT_WRITE_KINDS), {
stmt: (s) => {
if (s.kind === "return") write(`ret:${fn.name}`, s.value);
},
@@ -1428,6 +1527,7 @@ function narrowSlotRanges(
lastWrites: ReadonlyMap<string, ReadonlyMap<string, IntegerRange | null>>,
changed: ReadonlySet<string>,
readersOf: (slot: string) => ReadonlySet<string> | undefined,
nodes: IrNodeIndex | undefined,
): void {
// Every parameter and result slot with a fact, and that fact.
const proven = new Map<string, IntegerRange>();
@@ -1486,7 +1586,7 @@ function narrowSlotRanges(
const level = slots.isNonNegative(`box:${group}`) ? NON_NEGATIVE_INT32_RANGE : INT32_RANGE;
if (!rangeWithin(range, level)) sound = false;
};
walkFunction(fn, {
walkFunction(fn, nodes?.scan(NARROW_WRITE_KINDS), {
stmt: (s) => {
if (s.kind === "assign") box(s.localId, s.value);
else if (s.kind === "varDecl" && s.init) box(s.localId, s.init);
@@ -1497,7 +1597,7 @@ function narrowSlotRanges(
if (e.kind === "assignExpr") box(e.localId, e.value);
},
});
return { writes: slotWrites(fn, ranges, functions), sound };
return { writes: slotWrites(fn, ranges, functions, nodes), sound };
};
const writesByFunction = new Map(lastWrites);
const join = (): Map<string, IntegerRange | null> => {
+214
View File
@@ -0,0 +1,214 @@
/* One walk over a module's function bodies, recorded for analyses that
* only collect facts from nodes of a few kinds. Each such analysis used to
* walk every body itself; reading the recorded kind codes instead visits
* the same nodes in the same order without recursing through the tree. */
import { InternalCompilerError } from "../errors.js";
import type { IrExpr, IrFunction, IrModule, IrStmt, IrType } from "./ir.js";
import { everyStmtList, everyTypeChild } from "./traverse.js";
/** An expression or statement kind. The two sets of kinds are disjoint. */
export type IrNodeKind = IrExpr["kind"] | IrStmt["kind"];
/** The code of a kind no node has: it matches no recorded node. */
const ABSENT = 255;
/** Every expression and statement of each function body of a module, in
* the order everyStmtList visits them (a node before its children), with
* a kind code per node and, per function, the set of kinds present.
*
* The bodies must not change while the index is in use. Its owner (the
* LLVM emitter) builds it before its analyses run; no later pass rewrites
* a recorded body in place, and a rewritten copy of a function is walked. */
export class IrNodeIndex {
/** Every type object the typed module traversal (everyModuleNode)
* reaches, nested ones included, each once. */
readonly types: readonly IrType[];
private readonly nodes: (IrExpr | IrStmt)[] = [];
private readonly codes: Uint8Array;
private readonly kindCodes = new Map<string, number>();
/** By function object: an analysis may see a rewritten copy of a
* function under the same name, whose nodes are not recorded here. */
private readonly functionIndex = new Map<IrFunction, number>();
/** nodes[starts[i]] .. nodes[starts[i + 1] - 1] belong to function i. */
private readonly starts: Int32Array;
/** Per function, eight words of kind-present bits. */
private readonly present: Uint32Array;
/** By the kind list (or excluded set) a scan was made from. */
private readonly scans = new Map<object, IrKindScan>();
/** By kind code: 1 for statement kinds. */
private readonly statements = new Uint8Array(256);
constructor(mod: IrModule) {
const nodes = this.nodes;
const types: IrType[] = [];
const seenTypes = new Set<IrType>();
// A type object's children never change: one visit covers its tree.
const addType = (type: IrType): boolean => {
if (seenTypes.has(type)) return true;
seenTypes.add(type);
types.push(type);
return everyTypeChild(type, addType);
};
const statementKinds = new Set<string>();
const visitor = {
expr: (node: IrExpr): boolean => {
nodes.push(node);
addType(node.type);
return true;
},
stmt: (node: IrStmt): boolean => {
nodes.push(node);
statementKinds.add(node.kind);
return true;
},
};
const functions = mod.functions;
this.starts = new Int32Array(functions.length + 1);
for (let f = 0; f < functions.length; f++) {
this.functionIndex.set(functions[f]!, f);
this.starts[f] = nodes.length;
everyStmtList(functions[f]!.body, visitor);
}
this.starts[functions.length] = nodes.length;
// The other type slots everyModuleNode visits.
for (const fn of functions) {
for (const param of fn.params) addType(param.type);
addType(fn.returnType);
for (const local of fn.locals) addType(local.type);
for (const capture of fn.captures ?? []) addType(capture.type);
for (const capture of fn.classCaptures ?? []) addType(capture.type);
if (fn.generator !== undefined) {
addType(fn.generator.yieldT);
addType(fn.generator.nextT);
addType(fn.generator.resultType);
}
}
for (const cls of mod.classes ?? []) {
for (const capture of cls.localCaptures ?? []) addType(capture.type);
for (const field of cls.fields) addType(field.type);
}
for (const global of mod.globals ?? []) addType(global.type);
for (const record of mod.records ?? []) {
for (const field of record.fields) addType(field.type);
if (record.indexValue !== undefined) addType(record.indexValue);
}
for (const union of mod.unions ?? []) for (const arm of union.arms) addType(arm);
this.types = types;
const codes = new Uint8Array(nodes.length);
const present = new Uint32Array(functions.length * 8);
let previousKind = "";
let previousCode = ABSENT;
for (let f = 0; f < functions.length; f++) {
const end = this.starts[f + 1]!;
for (let i = this.starts[f]!; i < end; i++) {
const kind = nodes[i]!.kind;
if (kind !== previousKind) {
let code = this.kindCodes.get(kind);
if (code === undefined) {
code = this.kindCodes.size;
if (code >= ABSENT) throw new InternalCompilerError("more IR node kinds than codes");
this.kindCodes.set(kind, code);
}
previousKind = kind;
previousCode = code;
}
codes[i] = previousCode;
present[f * 8 + (previousCode >>> 5)]! |= 1 << (previousCode & 31);
}
}
this.codes = codes;
this.present = present;
for (const [kind, code] of this.kindCodes)
if (statementKinds.has(kind)) this.statements[code] = 1;
}
/** A scan for the nodes of `kinds`, for eachNodeOfKinds. Pass a
* constant list: scans are kept by the list's identity. */
scan(kinds: readonly IrNodeKind[]): IrKindScan {
const known = this.scans.get(kinds);
if (known !== undefined) return known;
const codes: number[] = [];
for (const kind of kinds) {
const code = this.kindCodes.get(kind);
if (code !== undefined) codes.push(code);
}
return this.scanOf(kinds, codes);
}
/** A scan for every expression. */
expressions(): IrKindScan {
const known = this.scans.get(this.statements);
if (known !== undefined) return known;
const codes: number[] = [];
for (const code of this.kindCodes.values()) if (this.statements[code] === 0) codes.push(code);
return this.scanOf(this.statements, codes);
}
/** A scan for the nodes of every kind but those in `excluded`. Pass a
* constant set: scans are kept by the set's identity. */
scanExcept(excluded: ReadonlySet<string>): IrKindScan {
const known = this.scans.get(excluded);
if (known !== undefined) return known;
const codes: number[] = [];
for (const [kind, code] of this.kindCodes) if (!excluded.has(kind)) codes.push(code);
return this.scanOf(excluded, codes);
}
private scanOf(key: object, codes: readonly number[]): IrKindScan {
const table = new Uint8Array(256);
const words = new Uint32Array(8);
for (const code of codes) {
table[code] = 1;
words[code >>> 5]! |= 1 << (code & 31);
}
const scan = { index: this, table, words };
this.scans.set(key, scan);
return scan;
}
/** Calls `note` with each recorded node of `fn` whose kind `scan` names,
* in walk order, until it answers false. False (nothing called) when
* `fn` is not recorded. */
visit(fn: IrFunction, scan: IrKindScan, note: IrNodeNote): boolean {
const f = this.functionIndex.get(fn);
if (f === undefined) return false;
let any = 0;
for (let w = 0; w < 8; w++) any |= this.present[f * 8 + w]! & scan.words[w]!;
if (any === 0) return true;
const { nodes, codes, statements } = this;
const table = scan.table;
const end = this.starts[f + 1]!;
for (let i = this.starts[f]!; i < end; i++) {
const code = codes[i]!;
if (table[code] !== 0 && note(nodes[i]!, statements[code] !== 0) === false) break;
}
return true;
}
}
/** Receives a node and whether it is a statement; false stops the visit. */
export type IrNodeNote = (node: IrExpr | IrStmt, statement: boolean) => boolean | void;
/** The kinds one analysis reads, resolved against one index. */
export interface IrKindScan {
readonly index: IrNodeIndex;
readonly table: Uint8Array;
readonly words: Uint32Array;
}
/** Calls `note` for the nodes of `fn` in walk order (everyStmtList's)
* until it answers false: with `scan`, only those of its kinds, read from
* the index; without one (or for a function the index did not record),
* every node, by walking. `note` must ignore the kinds the scan does not
* name, so both answer the same. */
export function eachNodeOfKinds(
fn: IrFunction,
scan: IrKindScan | undefined,
note: IrNodeNote,
): void {
if (scan !== undefined && scan.index.visit(fn, scan, note)) return;
everyStmtList(fn.body, {
expr: (node) => note(node, false) !== false,
stmt: (node) => note(node, true) !== false,
});
}
+22 -4
View File
@@ -8,6 +8,7 @@ import {
RUNTIME_STREAM_CLASSES,
} from "./ir.js";
import { everyModuleNode } from "./traverse.js";
import { eachNodeOfKinds, type IrNodeIndex } from "./node-index.js";
import { JsIcSites, hasJsIcSite } from "./js-ic.js";
const DYN_BUFFER_NUM_METHODS: ReadonlySet<string> = new Set(
@@ -400,8 +401,10 @@ export function isDynUndefined(e: IrExpr): boolean {
return e.kind === "dynFrom" && e.value.kind === "unitLit" && e.value.unit === "undefined";
}
export function moduleRuntimeFeatures(mod: IrModule): RuntimeFeatures {
return scanRuntimeFeatures(mod);
/** With `nodes` (the module's node index), the scan reads the recorded
* expressions and types instead of walking the module. */
export function moduleRuntimeFeatures(mod: IrModule, nodes?: IrNodeIndex): RuntimeFeatures {
return scanRuntimeFeatures(mod, undefined, nodes);
}
/** The module's record shapes, unions and class layouts by id. */
@@ -1126,7 +1129,11 @@ for (const name of RUNTIME_FEATURE_NAMES) {
if (rule.expr) FEATURE_EXPR_TESTS.push(name);
}
function scanRuntimeFeatures(mod: IrModule, stopAt?: RuntimeFeatureName): RuntimeFeatures {
function scanRuntimeFeatures(
mod: IrModule,
stopAt?: RuntimeFeatureName,
nodes?: IrNodeIndex,
): RuntimeFeatures {
const features = noRuntimeFeatures();
const scans: { name: RuntimeFeatureName; scan: RuleScan }[] = [];
for (const name of RUNTIME_FEATURE_NAMES) {
@@ -1161,7 +1168,18 @@ function scanRuntimeFeatures(mod: IrModule, stopAt?: RuntimeFeatureName): Runtim
for (const name of FEATURES_BY_TYPE.get(node.kind) ?? []) features[name] = true;
return keepGoing();
};
if (keepGoing()) everyModuleNode(mod, { expr, stmt: keepGoing, type });
// Each condition is a fact about one expression or one type (the rule
// scans combine such facts in no particular order), so the index's
// expressions and distinct types answer as the walk does. An early stop
// (stopAt) keeps the walk.
if (nodes !== undefined && stopAt === undefined) {
const expressions = nodes.expressions();
for (const fn of mod.functions)
eachNodeOfKinds(fn, expressions, (node) => {
expr(node as IrExpr);
});
for (const t of nodes.types) type(t);
} else if (keepGoing()) everyModuleNode(mod, { expr, stmt: keepGoing, type });
for (const { name, scan } of scans) if (scan.result()) features[name] = true;
// Inline-cache sites of dynamic-tier functions use the tier's shapes
// (scr_js_object.c, with the SCR_JS_TIER build of scr_json.c).
+157
View File
@@ -0,0 +1,157 @@
import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { beforeAll, expect, test } from "vitest";
import { compile, deserializeModule } from "../src/index.js";
import type { IrExpr, IrModule, IrStmt, IrType } from "../src/ir/ir.js";
import { eachNodeOfKinds, IrNodeIndex, type IrNodeKind } from "../src/ir/node-index.js";
import { everyModuleNode, everyStmtList } from "../src/ir/traverse.js";
async function lowerSource(source: string): Promise<IrModule> {
const dir = await mkdtemp(join(tmpdir(), "scriptc-node-index-"));
try {
const entry = join(dir, "main.ts");
await writeFile(entry, source);
const outPath = join(dir, "main.ir.json");
const result = await compile(entry, { outPath, outDir: dir, outputKind: "ir" });
if (!result.ok) throw new Error(result.diagnostics.map((d) => d.message).join("\n"));
return deserializeModule(await readFile(outPath, "utf8"));
} finally {
await rm(dir, { recursive: true, force: true });
}
}
let mod: IrModule;
let index: IrNodeIndex;
beforeAll(async () => {
mod = await lowerSource(`
class Shape {
constructor(public readonly name: string, protected sides: number[]) {}
area(): number { return 0; }
}
class Square extends Shape {
constructor(private side: number) { super("square", [side, side, side, side]); }
override area(): number { return this.side * this.side; }
}
type Result = { ok: true; value: number } | { ok: false; error: string };
function parse(text: string): Result {
const n = Number(text);
return Number.isNaN(n) ? { ok: false, error: "bad " + text } : { ok: true, value: n };
}
function* count(limit: number): Generator<number> {
for (let i = 0; i < limit; i++) yield i;
}
const totals = new Map<string, number>();
const seen = new Set<string>();
let calls = 0;
function record(key: string, value: number): void {
calls++;
totals.set(key, (totals.get(key) ?? 0) + value);
seen.add(key);
}
const adder = (k: number) => (x: number): number => x + k;
const inc = adder(1);
try {
for (const text of ["1", "x", "3"]) {
const r = parse(text);
if (r.ok) record("ok", inc(r.value));
else throw new Error(r.error);
}
} catch (e) {
record("error", String(e).length);
}
let sum = 0;
for (const v of count(4)) sum += v;
const shapes: Shape[] = [new Square(2), new Shape("none", [])];
console.log(sum, calls, [...totals], seen.size, shapes.map((s) => s.area()).join(","));
`);
index = new IrNodeIndex(mod);
});
/** Every node everyStmtList visits in `body`, with whether it is a statement. */
function walked(body: IrStmt[]): [IrExpr | IrStmt, boolean][] {
const out: [IrExpr | IrStmt, boolean][] = [];
everyStmtList(body, {
expr: (node) => (out.push([node, false]), true),
stmt: (node) => (out.push([node, true]), true),
});
return out;
}
function noted(
fn: IrModule["functions"][number],
scan: ReturnType<IrNodeIndex["scan"]> | undefined,
): [IrExpr | IrStmt, boolean][] {
const out: [IrExpr | IrStmt, boolean][] = [];
eachNodeOfKinds(fn, scan, (node, statement) => {
out.push([node, statement]);
});
return out;
}
const NO_KINDS: ReadonlySet<string> = new Set();
test("the index visits each body's nodes in everyStmtList's order", () => {
expect(mod.functions.length).toBeGreaterThan(5);
const all = index.scanExcept(NO_KINDS);
for (const fn of mod.functions) expect(noted(fn, all)).toEqual(walked(fn.body));
});
const SOME_KINDS: readonly IrNodeKind[] = ["call", "varDecl", "closure", "return", "fieldGet"];
test("a scan visits only its kinds, in walk order, and the walk answers the same", () => {
const scan = index.scan(SOME_KINDS);
let visited = 0;
for (const fn of mod.functions) {
const expected = walked(fn.body).filter(([node]) => SOME_KINDS.includes(node.kind));
expect(noted(fn, scan)).toEqual(expected);
// Without a scan, every node reaches `note`, as the walk visits them.
expect(noted(fn, undefined)).toEqual(walked(fn.body));
visited += expected.length;
}
expect(visited).toBeGreaterThan(20);
expect(index.scan(SOME_KINDS)).toBe(scan);
});
test("a false answer stops the visit, with or without the index", () => {
const all = index.scanExcept(NO_KINDS);
for (const fn of mod.functions) {
for (const scan of [all, undefined]) {
let calls = 0;
eachNodeOfKinds(fn, scan, () => ++calls < 3);
expect(calls).toBe(Math.min(3, walked(fn.body).length));
}
}
});
test("a function the index did not record is walked", () => {
const all = index.scanExcept(NO_KINDS);
for (const fn of mod.functions) {
const copy = { ...fn };
expect(index.visit(copy, all, () => {})).toBe(false);
expect(noted(copy, all)).toEqual(walked(fn.body));
}
});
test("the expression scan visits every expression", () => {
const expressions = index.expressions();
for (const fn of mod.functions)
expect(noted(fn, expressions)).toEqual(walked(fn.body).filter(([, statement]) => !statement));
});
test("the index's types are the type objects everyModuleNode visits, each once", () => {
const visited = new Set<IrType>();
everyModuleNode(mod, {
expr: () => true,
stmt: () => true,
type: (type) => (visited.add(type), true),
});
expect(new Set(index.types)).toEqual(visited);
expect(index.types.length).toBe(visited.size);
});
test("a kind no node has matches nothing", () => {
const scan = index.scan(["regexLit"]);
for (const fn of mod.functions) expect(noted(fn, scan)).toEqual([]);
});