mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-26 06:34:20 +08:00
Read emitter analyses from one recorded walk of the module (#1990)
This commit is contained in:
Binary file not shown.
@@ -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))
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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>();
|
||||
|
||||
@@ -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> => {
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
@@ -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).
|
||||
|
||||
@@ -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([]);
|
||||
});
|
||||
Reference in New Issue
Block a user