diff --git a/packages/compiler/src/backend/llvm/emit-statements.ts b/packages/compiler/src/backend/llvm/emit-statements.ts index ffd47c467..4fe1a27bd 100644 --- a/packages/compiler/src/backend/llvm/emit-statements.ts +++ b/packages/compiler/src/backend/llvm/emit-statements.ts @@ -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(); @@ -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; diff --git a/packages/compiler/test/callback-loop-versions.test.ts b/packages/compiler/test/callback-loop-versions.test.ts index c26abe1c4..63a669fd4 100644 --- a/packages/compiler/test/callback-loop-versions.test.ts +++ b/packages/compiler/test/callback-loop-versions.test.ts @@ -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); +});