From 2c048881c9b64397371f359ccd1347e55fd00a09 Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Fri, 16 Oct 2026 08:43:47 -0700 Subject: [PATCH] Ignore products that only bound a loop's condition A setup loop such as `for (i = 0; i < Math.floor(300000 * scale); i++) text.push(word())` multiplies only in its condition; its body does no integer work, so it keeps one copy. Also records the ts7 baseline of the speculative integer reads fixture. --- .../src/backend/llvm/speculative-integers.test.ts | 3 +++ .../compiler/src/backend/llvm/speculative-integers.ts | 11 +++++++++-- .../tests/corpus/speculative-integer-reads.ts.json | 6 ++++++ 3 files changed, 18 insertions(+), 2 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/speculative-integer-reads.ts.json diff --git a/packages/compiler/src/backend/llvm/speculative-integers.test.ts b/packages/compiler/src/backend/llvm/speculative-integers.test.ts index 132780937..ae9172298 100644 --- a/packages/compiler/src/backend/llvm/speculative-integers.test.ts +++ b/packages/compiler/src/backend/llvm/speculative-integers.test.ts @@ -182,6 +182,9 @@ test("invariants may come from bindings no shadow could be written to", () => { test("loops that only count and compare keep one copy", () => { const counter = loop([increment("i")]); expect(planSpeculativeLoop(counter as never, all)).toBeNull(); + // A product in the condition alone is not. + const bounded = loop([increment("i")], bin("<", ref("i"), bin("*", ref("n"), ref("scale")))); + expect(planSpeculativeLoop(bounded as never, all)).toBeNull(); // A variable step (a sieve's `j += i`) is worth a copy. const sieve = loop([assign("i", bin("+", ref("i"), ref("step")))]); expect(planSpeculativeLoop(sieve as never, all)?.locals.has("i")).toBe(true); diff --git a/packages/compiler/src/backend/llvm/speculative-integers.ts b/packages/compiler/src/backend/llvm/speculative-integers.ts index f3b75fb9a..cf1295fa2 100644 --- a/packages/compiler/src/backend/llvm/speculative-integers.ts +++ b/packages/compiler/src/backend/llvm/speculative-integers.ts @@ -259,6 +259,9 @@ export function planSpeculativeLoop( // double forms cost the most. A loop whose integers only count and compare // gains nothing measurable from a second copy, so it keeps one. const multiplied = new Set(); + // A product in the condition alone (`i < Math.floor(n * scale)`) does not + // make the body's work integer. + let inCondition = false; const expr = (e: IrExpr): boolean => { if (rejected) return false; @@ -272,7 +275,7 @@ export function planSpeculativeLoop( else if (e.kind === "bin" && CONSUMING_OPS.has(e.op)) { consume(e.left); consume(e.right); - if (e.op === "*" || e.op === "/" || e.op === "%") + if (!inCondition && (e.op === "*" || e.op === "/" || e.op === "%")) for (const operand of [e.left, e.right]) if (operand.kind === "varRef") multiplied.add(operand.localId); } else if (e.kind === "arrayGet" || e.kind === "arrayHas" || e.kind === "arrayState") @@ -321,7 +324,11 @@ export function planSpeculativeLoop( return everyStmtChild(s, expr, stmt); }; - if (loop.cond) expr(loop.cond); + if (loop.cond) { + inCondition = true; + expr(loop.cond); + inCondition = false; + } if (loop.kind === "for" && loop.update) stmt(loop.update); for (const s of loop.body) stmt(s); if (rejected || nodes > MAX_LOOP_NODES) return null; diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/speculative-integer-reads.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/speculative-integer-reads.ts.json new file mode 100644 index 000000000..b05283134 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/speculative-integer-reads.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/speculative-integer-reads.ts" + ], + "diags": [] +}