Keep callback loop versions from nesting

A callback loop version emits its loop twice. A loop inside either copy
could be versioned again, so a nest of loops that each call a callback
built just before them (`const g = adder(i)`) doubled at every level: a
three-deep nest was emitted seven times per function.

A loop inside a callback version, or the versioned loop itself emitted
again for each copy, is no longer versioned, so a loop nest is emitted at
most twice. The outer loop keeps its version; inner loops keep their
calls as they are.
This commit is contained in:
Chris Tate
2026-10-22 09:55:01 -05:00
parent 60c51b9535
commit cd2b1e2574
2 changed files with 38 additions and 10 deletions
@@ -1910,9 +1910,11 @@ export function emitSwitch(emitter: LlEmitter, s: IrStmt & { kind: "switch" }):
/** The loop copies a callback loop version needs: the bindings the loop
* calls that the constant-callback census resolves to one function, each
* with that function's direct entry. Only small loops of optimized
* synchronous functions, and only bindings declared before the loop that
* an enclosing version has not already proved, qualify: the guard reads
* each binding before the loop runs. */
* synchronous functions, and only bindings declared before the loop,
* qualify: the guard reads each binding before the loop runs. Versions do
* not nest: a loop inside either copy of a version (including the
* versioned loop itself, emitted again for each copy) keeps its calls as
* they are, so a loop nest is emitted at most twice. */
function callbackVersionBindings(
emitter: LlEmitter,
s: IrStmt & { kind: "for" | "while" | "doWhile" },
@@ -1920,7 +1922,7 @@ function callbackVersionBindings(
if (
!emitter.callbackVersionsEnabled ||
emitter.currentConstantCallbacks.size === 0 ||
emitter.callbackVersioning.has(s)
emitter.callbackVersioning.size > 0
)
return null;
const found = new Map<string, { ref: IrExpr & { kind: "varRef" }; direct: string }>();
@@ -1930,12 +1932,7 @@ function callbackVersionBindings(
const expr = (e: IrExpr): boolean => {
if (++cost > MAX_CALLBACK_VERSION_COST) return false;
if (e.kind === "assignExpr") bound.add(e.localId);
if (
e.kind === "callValue" &&
e.callee.kind === "varRef" &&
!emitter.knownCallbacks.has(e.callee.localId) &&
!found.has(e.callee.localId)
) {
if (e.kind === "callValue" && e.callee.kind === "varRef" && !found.has(e.callee.localId)) {
const local = emitter.currentLocals.get(e.callee.localId);
const target =
local && local.tdz !== true ? emitter.knownCallbackTarget(e.callee) : undefined;
@@ -81,3 +81,34 @@ test("a callback declared in the loop body is not read before the loop", async (
`);
expect(llvm).not.toContain("callback.version");
});
test("loops inside a callback version are not versioned again", async () => {
// Each inner loop calls a callback a factory builds just before it; without
// the limit, every copy of a version would version the next loop again.
const llvm = await emitLlvm(`
function adder(k: number): (v: number) => number {
return (v: number) => v + k;
}
function nest(n: number, f: (v: number) => number): number {
let s = 0;
for (let i = 0; i < n; i++) {
s += f(i);
const g = adder(i);
for (let j = 0; j < n; j++) {
s += g(j);
const h = adder(j);
for (let k = 0; k < n; k++) s += h(k);
}
}
return s;
}
console.log(nest(3, (v) => v - 1), nest(2, (v) => v * 3));
`);
// One version for the outer loop of each compiled `nest`; its two copies
// keep the inner loops whole.
const versions = [...llvm.matchAll(/^define [^\n]*@sc_bf_nest[^]*?^}/gm)].map(
([body]) => body.match(/^callback\.version\.fast\d+:$/gm)?.length ?? 0,
);
expect(versions.every((count) => count <= 1)).toBe(true);
expect(versions.some((count) => count === 1)).toBe(true);
});