diff --git a/packages/compiler/src/backend/llvm/emit-function.ts b/packages/compiler/src/backend/llvm/emit-function.ts index d7efafd36..e015c2d72 100644 --- a/packages/compiler/src/backend/llvm/emit-function.ts +++ b/packages/compiler/src/backend/llvm/emit-function.ts @@ -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(); diff --git a/packages/compiler/src/backend/llvm/emitter.ts b/packages/compiler/src/backend/llvm/emitter.ts index 59b4b64a4..6729da197 100644 --- a/packages/compiler/src/backend/llvm/emitter.ts +++ b/packages/compiler/src/backend/llvm/emitter.ts @@ -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 = 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 { diff --git a/packages/compiler/src/backend/llvm/integer-views.ts b/packages/compiler/src/backend/llvm/integer-views.ts index c38770160..579f010a2 100644 --- a/packages/compiler/src/backend/llvm/integer-views.ts +++ b/packages/compiler/src/backend/llvm/integer-views.ts @@ -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 { +export function findIntegerViews(fn: IrFunction, nodes?: IrNodeIndex): Set { const wanted = new Set(); 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), diff --git a/packages/compiler/src/backend/llvm/lazy-captures.ts b/packages/compiler/src/backend/llvm/lazy-captures.ts index 2014869af..85acff2ef 100644 --- a/packages/compiler/src/backend/llvm/lazy-captures.ts +++ b/packages/compiler/src/backend/llvm/lazy-captures.ts @@ -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(); private sites: Map | null = null; - constructor(private readonly functions: ReadonlyMap) {} + constructor( + private readonly functions: ReadonlyMap, + /** 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 { if (this.sites !== null) return this.sites; const sites = new Map(); + 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; diff --git a/packages/compiler/src/backend/llvm/loop-array-borrows.ts b/packages/compiler/src/backend/llvm/loop-array-borrows.ts index 9335894d9..cc9e03a5c 100644 --- a/packages/compiler/src/backend/llvm/loop-array-borrows.ts +++ b/packages/compiler/src/backend/llvm/loop-array-borrows.ts @@ -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, + /** The module's node index, read instead of walking for the loops. */ + nodes?: IrNodeIndex, ): ReadonlySet { const result = new Set(); 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; } diff --git a/packages/compiler/src/ir/constant-globals.ts b/packages/compiler/src/ir/constant-globals.ts index f18764223..76b086f40 100644 --- a/packages/compiler/src/ir/constant-globals.ts +++ b/packages/compiler/src/ir/constant-globals.ts @@ -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>(); + for (const fn of mod.functions) { + let read: Set | 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(); 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) }; }); diff --git a/packages/compiler/src/ir/receiver-readers.ts b/packages/compiler/src/ir/receiver-readers.ts index 3d0e6f35d..f5270aa7d 100644 --- a/packages/compiler/src/ir/receiver-readers.ts +++ b/packages/compiler/src/ir/receiver-readers.ts @@ -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([ "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([ + ...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 | null = null; - constructor(private readonly functions: ReadonlyMap) {} + constructor( + private readonly functions: ReadonlyMap, + /** 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(); const callers = new Map(); + const scan = this.nodes?.scanExcept(TRIVIALLY_RECEIVER_FREE); for (const fn of this.functions.values()) { const callees = new Set(); 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) {