Keep the while-loop bound off counters written inside expressions

flow follows a while body's statements and applies each increment it
meets as a statement. An increment inside an expression, such as an if
condition's comma expression `if ((i++, i > 0))`, lowers to a statement
nested in the expression; whileBound accepted it as an increment, but
flow never applied it, so a read after it kept the pre-increment bound
and lost its bounds check. whileBound now gives no bound when any
expression in the body holds a write of the counter.
This commit is contained in:
Chris Tate
2026-10-24 18:32:42 -05:00
parent 5dc3a051b7
commit ecd103d1fb
3 changed files with 39 additions and 2 deletions
@@ -300,10 +300,18 @@ export function findBytesBounds(fn: IrFunction, ranges: IntegerRanges): Readonly
const receiver = length.receiver;
const comparison = lengthComparison(cond, { receiver, counter, offset: 0, minimum });
if (comparison?.op !== "<" && comparison?.op !== "<=") return null;
// An increment is a statement of the body: one inside an expression
// (a comma expression's statements) would run where flow does not
// follow it.
const writesOk = everyStmtList(s.body, {
expr: (e) =>
(e.kind !== "assignExpr" && e.kind !== "incDec") ||
(e.localId !== counter && e.localId !== receiver),
((e.kind !== "assignExpr" && e.kind !== "incDec") ||
(e.localId !== counter && e.localId !== receiver)) &&
everyExprChild(
e,
() => true,
(child) => !writes(child, counter),
),
stmt: (child) => {
if (child.kind === "assign" && child.localId === counter)
return increment(child, counter) !== null;
@@ -119,13 +119,27 @@ function nested(line: Uint8Array): number {
}
return total;
}
function comma(line: Uint8Array): number {
const len = line.length;
let i = 0;
let total = 0;
while (i < len) {
if ((i++, i > 0)) total += line[len - 1 - i]!;
total += line[len - 1 - i]!;
}
return total;
}
console.log(
comma(new Uint8Array(2)),
inclusive(new Uint8Array(2)),
unguarded(new Uint8Array(2)),
ahead(new Uint16Array(3)),
nested(new Uint8Array(3)),
);
`);
// The increment in the comma expression is not a statement of the
// body, so the loop has no bound and both reads keep their checks.
expect(checks(ll, "comma")).toBe(2);
expect(checks(ll, "inclusive")).toBe(1);
// The read before the increment is in bounds; the one after it is not.
expect(checks(ll, "unguarded")).toBe(1);
@@ -166,3 +166,18 @@ function throwing(line: Uint8Array): string {
return String(total);
}
console.log(throwing(new Uint8Array([4, 0, 2, 3])), throwing(new Uint8Array([1, 2])));
// An increment inside an if condition's comma expression: both reads are
// at index -1 on the last pass.
function commaIncrement(line: Uint8Array): string {
const len = line.length;
let i = 0;
const out: (number | undefined)[] = [];
while (i < len) {
if ((i++, i > 0)) out.push(line[len - 1 - i]);
const v = line[len - 1 - i]!;
out.push(v);
}
return JSON.stringify(out);
}
console.log(commaIncrement(new Uint8Array([1, 2, 3])), commaIncrement(new Uint8Array(0)));