mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-11-03 07:54:40 +08:00
Read the node index in more LLVM emission analyses (#2108)
This commit is contained in:
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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) };
|
||||
});
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user