Read the node index in more LLVM emission analyses (#2108)

This commit is contained in:
Chris Tate
2026-10-24 10:19:42 -05:00
committed by GitHub
7 changed files with 135 additions and 62 deletions
@@ -159,6 +159,7 @@ export function emitFunction(emitter: LlEmitter, fn: IrFunction): string {
emitter.optionalArrayReads,
emitter.referenceWrites,
emitter.fnByName,
emitter.nodeIndex,
)
: new Set();
emitter.localArrayReads = findLocalArrayReads(
@@ -324,7 +325,7 @@ export function emitFunction(emitter: LlEmitter, fn: IrFunction): string {
const paramIds = new Set(fn.params.map((p) => p.localId));
if (emitter.debug === null) {
for (const id of findIntegerViews(numericFn)) {
for (const id of findIntegerViews(numericFn, emitter.nodeIndex)) {
// An i32 slot is its own view.
if (emitter.int32Locals.has(id)) continue;
const slot = B.slot();
+31 -19
View File
@@ -176,7 +176,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 { eachNodeOfKinds, IrNodeIndex, type IrNodeKind } from "../../ir/node-index.js";
import { lastUseRead } from "./local-extents.js";
import { findReboundParameters } from "./rebound-parameters.js";
import { ReferenceEffects } from "./reference-effects.js";
@@ -621,6 +621,11 @@ export function llStrBytes(text: string): string {
const NO_BORROWED: ReadonlySet<number> = new Set();
/** The nodes that write a binding by its id (numberGlobalWriters). */
const GLOBAL_WRITE_KINDS: readonly IrNodeKind[] = ["assign", "varDecl", "assignExpr", "incDec"];
/** Runtime library calls (readsLoopMetrics). */
const LIB_CALL_KINDS: readonly IrNodeKind[] = ["libCall"];
/** [A-Za-z0-9_] */
function isIdentifierByte(code: number): boolean {
return (
@@ -1842,9 +1847,9 @@ export class LlEmitter {
)
: NO_INTEGER_CLONES;
this.stackCallbacks = new StackCallbacks(this.fnByName);
this.receiverReaders = new AmbientReceiverReaders(this.fnByName);
this.receiverReaders = new AmbientReceiverReaders(this.fnByName, this.nodeIndex);
this.stackCaptures = new StackCaptures(this.fnByName, this.callLifetimes, this.stackCallbacks);
this.lazyCaptures = new LazyCaptures(this.fnByName);
this.lazyCaptures = new LazyCaptures(this.fnByName, this.nodeIndex);
const traced = computeTraced(mod, dynamicStores, cycleMutability);
this.tracedShapes = traced.shapes;
this.tracedUnions = traced.unions;
@@ -3621,15 +3626,14 @@ export class LlEmitter {
set.add(fn.name);
writers.set(localId, set);
};
everyStmtList(fn.body, {
stmt: (s) => {
if (s.kind === "assign" || s.kind === "varDecl") note(s.localId);
return true;
},
expr: (e) => {
if (e.kind === "assignExpr" || e.kind === "incDec") note(e.localId);
return true;
},
eachNodeOfKinds(fn, this.nodeIndex.scan(GLOBAL_WRITE_KINDS), (node) => {
if (
node.kind === "assign" ||
node.kind === "varDecl" ||
node.kind === "assignExpr" ||
node.kind === "incDec"
)
note(node.localId);
});
}
this.globalWriters = writers;
@@ -5409,13 +5413,21 @@ export class LlEmitter {
private loopMetricsRead: boolean | undefined;
readsLoopMetrics(): boolean {
if (this.loopMetricsRead !== undefined) return this.loopMetricsRead;
const visitor = {
expr: (e: IrExpr): boolean =>
!(e.kind === "libCall" && (e.fn === "perf.loopStart" || e.fn === "perf.idleTime")),
stmt: (): boolean => true,
};
this.loopMetricsRead = !this.mod.functions.every((fn) => everyStmtList(fn.body, visitor));
return this.loopMetricsRead;
let found = false;
const scan = this.nodeIndex.scan(LIB_CALL_KINDS);
for (const fn of this.mod.functions) {
eachNodeOfKinds(fn, scan, (node) => {
if (
node.kind === "libCall" &&
(node.fn === "perf.loopStart" || node.fn === "perf.idleTime")
)
found = true;
return !found;
});
if (found) break;
}
this.loopMetricsRead = found;
return found;
}
private lastUseMovesAllowed(): boolean {
@@ -1,6 +1,9 @@
import type { IrExpr, IrFunction, IrStmt } from "../../ir/ir.js";
import { everyExprChild, everyStmtChild } from "../../ir/traverse.js";
import { byteNumberAccess } from "../../ir/byte-numbers.js";
import { eachNodeOfKinds, type IrNodeIndex, type IrNodeKind } from "../../ir/node-index.js";
const LOOP_KINDS: readonly IrNodeKind[] = ["for", "forOf", "while", "doWhile"];
/** Keep a computed ToUint32 view beside an ordinary number when integer
* consumers in loops can reuse it. The number remains authoritative for floating
@@ -14,9 +17,16 @@ import { byteNumberAccess } from "../../ir/byte-numbers.js";
* compare converts the stored number back, and LLVM moves that number's
* conversion into the join of a NaN-coded typed-array read (`phi [sitofp x,
* 0.0]`), where nothing folds it back to the integer. */
export function findIntegerViews(fn: IrFunction): Set<string> {
export function findIntegerViews(fn: IrFunction, nodes?: IrNodeIndex): Set<string> {
const wanted = new Set<string>();
if (fn.async || fn.generator) return wanted;
// Every view is wanted inside a loop: a body without one wants none.
let loops = false;
eachNodeOfKinds(fn, nodes?.scan(LOOP_KINDS), (node, statement) => {
if (statement && LOOP_KINDS.includes(node.kind)) loops = true;
return !loops;
});
if (!loops) return wanted;
const initialized = new Set(fn.params.map((p) => p.localId));
const excluded = new Set(
[...(fn.captures ?? []), ...(fn.classCaptures ?? [])].map((c) => c.localId),
@@ -1,5 +1,8 @@
import type { IrExpr, IrFunction, IrStmt } from "../../ir/ir.js";
import { everyStmtList } from "../../ir/traverse.js";
import { eachNodeOfKinds, type IrNodeIndex, type IrNodeKind } from "../../ir/node-index.js";
const CLOSURE_KINDS: readonly IrNodeKind[] = ["closure"];
/** The local a node rebinds. Writes through the bound reference, such as a
* field store or an array push, leave the binding unchanged. Catch bindings
@@ -37,7 +40,11 @@ export class LazyCaptures {
private readonly pending = new Set<IrFunction>();
private sites: Map<string, { owner: string; captures: string[] }[]> | null = null;
constructor(private readonly functions: ReadonlyMap<string, IrFunction>) {}
constructor(
private readonly functions: ReadonlyMap<string, IrFunction>,
/** The module's node index, read instead of walking the bodies it holds. */
private readonly nodes?: IrNodeIndex,
) {}
/** The captures of closure body `fnName` that hold one value for the
* closure's whole life: at every creation site the captured binding is
@@ -103,16 +110,15 @@ export class LazyCaptures {
private creationSites(): Map<string, { owner: string; captures: string[] }[]> {
if (this.sites !== null) return this.sites;
const sites = new Map<string, { owner: string; captures: string[] }[]>();
const scan = this.nodes?.scan(CLOSURE_KINDS);
for (const fn of this.functions.values()) {
const visit = (node: IrStmt | IrExpr): boolean => {
eachNodeOfKinds(fn, scan, (node) => {
if (node.kind === "closure") {
let list = sites.get(node.fnName);
if (!list) sites.set(node.fnName, (list = []));
list.push({ owner: fn.name, captures: node.captures });
}
return true;
};
everyStmtList(fn.body, { stmt: visit, expr: visit });
});
}
this.sites = sites;
return sites;
@@ -1,5 +1,6 @@
import { F64, isRefCounted, type IrExpr, type IrFunction, type IrStmt } from "../../ir/ir.js";
import { everyStmtList, transformStmtList } from "../../ir/traverse.js";
import { eachNodeOfKinds, type IrNodeIndex, type IrNodeKind } from "../../ir/node-index.js";
import type { CallLifetimes } from "./call-lifetimes.js";
import type { ReferenceEffects } from "./reference-effects.js";
import type { OptionalArrayReads } from "./local-array-reads.js";
@@ -57,6 +58,8 @@ export function dynHoleRead(value: IrExpr): (IrExpr & { kind: "arrayGet" }) | nu
return same(cond.left.arr, then.arr) && same(cond.left.index, then.index) ? then : null;
}
const LOOP_KINDS: readonly IrNodeKind[] = ["forOf", "for", "while", "doWhile"];
/** Limit a borrow to a loop whose complete iteration preserves heap edges.
* Initialization and unrelated work elsewhere in the function may mutate.
* The array remains owned by its unchanged local or global binding; the
@@ -72,6 +75,8 @@ export function findLoopArrayBorrows(
optionalReads?: OptionalArrayReads,
writes?: ReferenceWrites | null,
functions?: ReadonlyMap<string, IrFunction>,
/** The module's node index, read instead of walking for the loops. */
nodes?: IrNodeIndex,
): ReadonlySet<IrStmt> {
const result = new Set<IrStmt>();
if (fn.async || fn.generator) return result;
@@ -88,9 +93,10 @@ export function findLoopArrayBorrows(
(!local.boxed && !local.tdz && (bindings?.has(local.id) || parameters.has(local.id)))
);
};
everyStmtList(fn.body, {
expr: () => true,
stmt: (loop) => {
eachNodeOfKinds(fn, nodes?.scan(LOOP_KINDS), (node, statement) => {
if (!statement) return true;
const loop = node as IrStmt;
{
if (loop.kind === "forOf") {
const local = locals.get(loop.localId);
if (
@@ -187,7 +193,7 @@ export function findLoopArrayBorrows(
},
});
return true;
},
}
});
return result;
}
+15 -5
View File
@@ -36,7 +36,11 @@ export function foldConstantGlobals(mod: IrModule): IrModule {
if (value && (value.kind === "numLit" || value.kind === "boolLit")) values.set(id, value);
else values.delete(id);
};
for (const fn of mod.functions)
// Per function, the candidates it reads: the rewrite below visits only the
// functions that read a constant.
const readsByFunction = new Map<IrFunction, Set<string>>();
for (const fn of mod.functions) {
let read: Set<string> | undefined;
everyStmtList(fn.body, {
// Every node other than a read that names the binding is a write
// (assignments, loop and catch bindings, compound updates); only a
@@ -48,16 +52,21 @@ export function foldConstantGlobals(mod: IrModule): IrModule {
return true;
},
expr: (e) => {
if (e.kind !== "varRef" && "localId" in e && typeof e.localId === "string")
write(e.localId, null);
if (e.kind === "varRef") {
if (candidates.has(e.localId)) {
if (read === undefined) read = new Set();
read.add(e.localId);
}
} else if ("localId" in e && typeof e.localId === "string") write(e.localId, null);
return true;
},
});
if (read !== undefined) readsByFunction.set(fn, read);
}
const constants = new Map<string, Literal>();
for (const [id, value] of values) if (writes.get(id) === 1) constants.set(id, value);
if (constants.size === 0) return mod;
const reads = (e: IrExpr): boolean => !(e.kind === "varRef" && constants.has(e.localId));
const rewriteExpr = (e: IrExpr): IrExpr => {
if (e.kind === "varRef") {
const value = constants.get(e.localId);
@@ -68,7 +77,8 @@ export function foldConstantGlobals(mod: IrModule): IrModule {
const rewriteStmt = (s: IrStmt): IrStmt => mapStmtChildren(s, rewriteExpr, rewriteStmt);
let changed = false;
const functions = mod.functions.map((fn): IrFunction => {
if (everyStmtList(fn.body, { stmt: () => true, expr: reads })) return fn;
const read = readsByFunction.get(fn);
if (read === undefined || ![...read].some((id) => constants.has(id))) return fn;
changed = true;
return { ...fn, body: fn.body.map(rewriteStmt) };
});
+55 -27
View File
@@ -1,6 +1,6 @@
import { isBorrowSafeMath } from "./analysis.js";
import type { IrExpr, IrFunction, IrLibFn, IrStmt } from "./ir.js";
import { everyStmtList } from "./traverse.js";
import { eachNodeOfKinds, type IrNodeIndex } from "./node-index.js";
/** Expression kinds that run no user code of their own and never consult
* the ambient receiver stack. Calls are admitted here and resolved through
@@ -85,6 +85,25 @@ const RECEIVER_FREE_STMTS = new Set<IrStmt["kind"]>([
"runtimeFence",
]);
/** The kinds whose nodes the readers walk answers true for without looking
* at them: every receiver-free statement, and the receiver-free expressions
* other than calls, library calls, string conversions and data intrinsics. */
const TRIVIALLY_RECEIVER_FREE = new Set<string>([
...RECEIVER_FREE_STMTS,
...[...RECEIVER_FREE_EXPRS].filter(
(kind) =>
kind !== "call" &&
kind !== "new" &&
kind !== "virtualCall" &&
kind !== "libCall" &&
kind !== "toString" &&
kind !== "arrIntrinsic" &&
kind !== "strIntrinsic" &&
kind !== "mapIntrinsic" &&
kind !== "setIntrinsic",
),
]);
/** Runtime library calls beyond scalar Math that only compute or build and
* park a native error: the f64[] folds behind Math.max/min(...xs),
* `new Error(message)` (scr_error_new), and the checked-read TypeError
@@ -133,7 +152,11 @@ function opaqueOperand(e: IrExpr): boolean {
export class AmbientReceiverReaders {
private readers: Set<string> | null = null;
constructor(private readonly functions: ReadonlyMap<string, IrFunction>) {}
constructor(
private readonly functions: ReadonlyMap<string, IrFunction>,
/** The module's node index, read instead of walking the bodies it holds. */
private readonly nodes?: IrNodeIndex,
) {}
ignores(fn: IrFunction): boolean {
if (fn.async || fn.generator) return false;
@@ -153,6 +176,7 @@ export class AmbientReceiverReaders {
}
const readers = new Set<string>();
const callers = new Map<string, string[]>();
const scan = this.nodes?.scanExcept(TRIVIALLY_RECEIVER_FREE);
for (const fn of this.functions.values()) {
const callees = new Set<string>();
let reads = fn.async === true || fn.generator !== undefined;
@@ -161,32 +185,36 @@ export class AmbientReceiverReaders {
callees.add(name);
return true;
};
const expr = (expr: IrExpr): boolean => {
if (!RECEIVER_FREE_EXPRS.has(expr.kind)) return false;
switch (expr.kind) {
case "call":
return edge(expr.callee);
case "new":
return edge(`%${expr.className}.constructor`);
case "virtualCall": {
// Any class's override may run: admit only when every
// same-named method is itself receiver-free.
const targets = byMethod.get(expr.method) ?? [];
return targets.length > 0 && targets.every(edge);
}
case "libCall":
return isBorrowSafeMath(expr.fn) || RECEIVER_FREE_LIB_FNS.has(expr.fn);
case "toString":
return !opaqueOperand(expr.operand);
default: {
const operands = dataIntrinsicOperands(expr);
return operands === null || !operands.some(opaqueOperand);
}
}
};
if (!reads)
reads = !everyStmtList(fn.body, {
stmt: (stmt) => RECEIVER_FREE_STMTS.has(stmt.kind),
expr: (expr) => {
if (!RECEIVER_FREE_EXPRS.has(expr.kind)) return false;
switch (expr.kind) {
case "call":
return edge(expr.callee);
case "new":
return edge(`%${expr.className}.constructor`);
case "virtualCall": {
// Any class's override may run: admit only when every
// same-named method is itself receiver-free.
const targets = byMethod.get(expr.method) ?? [];
return targets.length > 0 && targets.every(edge);
}
case "libCall":
return isBorrowSafeMath(expr.fn) || RECEIVER_FREE_LIB_FNS.has(expr.fn);
case "toString":
return !opaqueOperand(expr.operand);
default: {
const operands = dataIntrinsicOperands(expr);
return operands === null || !operands.some(opaqueOperand);
}
}
},
eachNodeOfKinds(fn, scan, (node, statement) => {
const free = statement
? RECEIVER_FREE_STMTS.has((node as IrStmt).kind)
: expr(node as IrExpr);
if (!free) reads = true;
return free;
});
if (reads) readers.add(fn.name);
for (const callee of callees) {