Leave counters unbounded when a nested scan is not walked pass by pass

highestCount walks a nested while loop from its bound only when the loop
is a statement of the body or of its blocks and branches. A nested scan
inside a labeled block or an expression reached the generic branch,
which counted its increments once although the loop can repeat them.
Such a loop now leaves the counter without a range.
This commit is contained in:
Chris Tate
2026-10-22 00:44:22 -05:00
parent 956bdf4ec0
commit b59da7529e
2 changed files with 25 additions and 5 deletions
+16 -5
View File
@@ -395,11 +395,15 @@ export function guardedLoopCounterFacts(
/** The largest value counter `id` takes in one pass of `body`, which starts
* at most at `entry`. The caller has checked that the body changes the
* counter only by positive literal increments outside nested loops, switch,
* exception and labeled regions, so those regions leave it unchanged, and
* every exit from a straight-line path leaves the body. An `if` whose
* condition does not write the counter caps it in the then branch by the
* condition's bounds; any other statement adds the increments it holds. */
* counter only by positive literal increments outside switch, exception and
* labeled loop regions and loops other than nested while loops whose
* conditions bound it, and every exit from a straight-line path leaves the
* body. An `if` whose condition does not write the counter caps it in the
* then branch by the condition's bounds. A nested while loop that is a
* statement of the body, or of its blocks and branches, is walked from its
* bound; any other statement adds the increments it holds, and one that
* holds a loop writing the counter (in an expression or a labeled block)
* leaves it unbounded. */
function highestCount(
body: readonly IrStmt[],
id: string,
@@ -416,6 +420,13 @@ function highestCount(
};
const stmt = (x: IrStmt): boolean => {
if (x.kind === "assign" && x.localId === id) total += increment(id, x.value) ?? 0;
// A loop reached here, inside an expression or a labeled region rather
// than through `walk`, can repeat its increments: no bound.
if (
(x.kind === "while" || x.kind === "doWhile" || x.kind === "for" || x.kind === "forOf") &&
writtenLocals([x]).has(id)
)
total = Infinity;
return everyStmtChild(x, expr, stmt);
};
if (expression) expr(e as IrExpr);
@@ -863,6 +863,15 @@ test("a nested while loop may advance a counter its own condition bounds", () =>
),
);
expect(run(writing).after).toBeNull();
// The same scan inside a labeled block or an expression is not walked
// pass by pass; its increments could repeat, so the counter stays unbounded.
expect(run({ loc, kind: "block", labels: ["scan"], body: [bounded] }).after).toBeNull();
const inExpression: IrStmt = {
loc,
kind: "exprStmt",
expr: { loc, kind: "seqExpr", stmts: [bounded], result: num(0), type: F64 },
};
expect(run(inExpression).after).toBeNull();
});
test("an array length bounds while and for counters even when the body resizes the array", () => {