diff --git a/packages/compiler/src/ir/integer-ranges.test.ts b/packages/compiler/src/ir/integer-ranges.test.ts index a8c9978f3..289dd9f75 100644 --- a/packages/compiler/src/ir/integer-ranges.test.ts +++ b/packages/compiler/src/ir/integer-ranges.test.ts @@ -941,6 +941,25 @@ test("a counter plus or minus a literal compared with a bound narrows the counte expect(gRanges.get(wide)).toEqual({ min: 0, max: 100 }); }); +test("an offset comes off a fractional bound after the bound rounds to an integer", () => { + // x = unknown | 0; if (x + 2147483648 > -(2 ** -60)) observe(x): the sum is + // an integer, so the condition holds exactly when x + 2^31 >= 0. Subtracting + // the offset from the fractional bound first would round to -2^31 and drop + // x = -2^31 from the range. + const kept = ref(); + const f = fn([ + assign(bin("|", ref("unknown"), num(0))), + { + loc, + kind: "if", + cond: condition(">", bin("+", ref(), num(2147483648)), num(-(2 ** -60))), + then: [observe(kept)], + else_: null, + }, + ]); + expect(analyzeIntegerRanges(f).get(kept)).toEqual({ min: -2147483648, max: 2147483647 }); +}); + test("whole-program slot facts seed parameters, field reads and call results", () => { const field: IrExpr = { loc, diff --git a/packages/compiler/src/ir/integer-ranges.ts b/packages/compiler/src/ir/integer-ranges.ts index 410c1b6f7..59794afc6 100644 --- a/packages/compiler/src/ir/integer-ranges.ts +++ b/packages/compiler/src/ir/integer-ranges.ts @@ -560,7 +560,6 @@ export function analyzeIntegerRanges(fn: IrFunction, slots: IntegerSlotFacts = { Math.abs(literalBound.max) > 2 ** 52) ) return; - const boundRange = { min: literalBound.min - offset, max: literalBound.max - offset }; if (!truth) op = ( @@ -569,18 +568,27 @@ export function analyzeIntegerRanges(fn: IrFunction, slots: IntegerSlotFacts = { string > )[op] ?? ""; + // The sum `x + c` is an integer, so the bound rounds to an integer first + // and the offset comes off after: integer arithmetic below 2^53 is exact, + // where subtracting the offset from a fractional bound could round + // across an integer. let { min, max } = previous; - if (op === "<") max = Math.min(max, Math.ceil(boundRange.max) - 1); - else if (op === "<=") max = Math.min(max, Math.floor(boundRange.max)); - else if (op === ">") min = Math.max(min, Math.floor(boundRange.min) + 1); - else if (op === ">=") min = Math.max(min, Math.ceil(boundRange.min)); + if (op === "<") max = Math.min(max, Math.ceil(literalBound.max) - 1 - offset); + else if (op === "<=") max = Math.min(max, Math.floor(literalBound.max) - offset); + else if (op === ">") min = Math.max(min, Math.floor(literalBound.min) + 1 - offset); + else if (op === ">=") min = Math.max(min, Math.ceil(literalBound.min) - offset); else if (op === "===") { - min = Math.max(min, boundRange.min); - max = Math.min(max, boundRange.max); - } else if (op === "!==" && boundRange.min === boundRange.max) { + min = Math.max(min, Math.ceil(literalBound.min) - offset); + max = Math.min(max, Math.floor(literalBound.max) - offset); + } else if ( + op === "!==" && + literalBound.min === literalBound.max && + Number.isInteger(literalBound.min) + ) { // Excluding one value narrows a range that ends at it. - if (min === boundRange.min) min += 1; - else if (max === boundRange.max) max -= 1; + const excluded = literalBound.min - offset; + if (min === excluded) min += 1; + else if (max === excluded) max -= 1; } if (min <= max && Number.isInteger(min) && Number.isInteger(max)) facts.set(target.localId, { min, max });