diff --git a/packages/compiler/src/backend/llvm/bytes-bounds.ts b/packages/compiler/src/backend/llvm/bytes-bounds.ts index b35a949ff..29b1c8e15 100644 --- a/packages/compiler/src/backend/llvm/bytes-bounds.ts +++ b/packages/compiler/src/backend/llvm/bytes-bounds.ts @@ -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; diff --git a/packages/compiler/test/while-bytes-bounds-emission.test.ts b/packages/compiler/test/while-bytes-bounds-emission.test.ts index 2b6dc1d3c..a2604eed1 100644 --- a/packages/compiler/test/while-bytes-bounds-emission.test.ts +++ b/packages/compiler/test/while-bytes-bounds-emission.test.ts @@ -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); diff --git a/tests/corpus/8003-while-reversed-byte-reads.ts b/tests/corpus/8003-while-reversed-byte-reads.ts index 9b2efd4f0..1407b9324 100644 --- a/tests/corpus/8003-while-reversed-byte-reads.ts +++ b/tests/corpus/8003-while-reversed-byte-reads.ts @@ -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)));