Skip to content

fix: Unsound interval rules for *, div, mod, / and ** - #1159

Draft
fabiomadge wants to merge 2 commits into
masterfrom
fix/interval-arithmetic-rules
Draft

fabiomadge wants to merge 2 commits into
masterfrom
fix/interval-arithmetic-rules

Conversation

@fabiomadge

@fabiomadge fabiomadge commented Aug 31, 2026 •

Copy link
Copy Markdown
Contributor

Five of the domain's arithmetic rules inferred bounds that need not hold. * with two negative lower
bounds claimed the product is at most their product. div, mod and / accepted a divisor whose lower
bound is zero, where division is underspecified. ** claimed a lower bound of 1 for a nonzero exponent
and took its upper bound from the exponent.

procedure P(x: int, y: int) returns (p: int)
{
  assume -3 <= x;
  assume -3 <= y;
  p := 0;
  while (p == 0) { p := x * y; }
  assert p <= 9;
}

-infer:j → 1 verified, 0 errors, though x * y is unbounded above. The loop is there only because
inferred facts reach the prover at loop heads.

Fix. * keeps the nonnegative case alone, since a negative operand needs all four corner products and
so an upper bound on both. The three divisions require 1 <= lo1, which also makes div and / identical
and lets them share a case — the merged rule is correct under both the half-open reading ints get and the
closed one reals get. ** loses its rule.

Testing. Three files covering all five rules. ** cannot be caught by a false assertion, because it is
emitted as real_pow, which the background predicates declare only under a non-monomorphic encoding, so
any verification condition mentioning it dies in the solver (#1166 fixes that). PowerBounds.bpl checks
the inferred invariant directly under -noVerify, where master infers the false 1e0 <= p && p <= 2e0
for x ** y with x >= 0 and 1e0 <= y <= 2e0.

One of six.

Four transfer functions in `PEVisitor` claimed a bound that their premises do not
give, so `-infer:j` proved false assertions. Each is reported correctly without
inference.

`Mul` had a branch for two negative lower bounds that set `Hi = lo0 * lo1`,
deriving an upper bound from two lower bounds. It holds only if both operands are
also bounded above by zero, which nothing established. From `-3 <= x` and
`-3 <= y` it inferred `x * y < 10`, refuted by x = y = 1000. The branch goes;
recovering it soundly needs both upper bounds known and non-positive, and all four
corner products. The real form of this is reachable from Dafny, which emits `*` on
reals natively and enables this analysis unconditionally, and there it proves
`false` from a program with no floating point and no flags.

`Div`, `Mod` and `RealDiv` bounded their result whenever both operands had a lower
bound of at least zero, which admits a zero divisor. Division and mod by zero are
underspecified in SMT-LIB, so the result is an arbitrary value and no bound holds
of it. Each now requires a divisor of at least one. `RealDiv` already required
that for its upper bound and not for its lower; an integer bound cannot say
"positive but below one", so a real divisor in that range now yields nothing.

`Pow` gave no correct bound at all and is removed. It claimed `Hi = hi1`, bounding
the power by the *exponent's* upper bound -- 2.0 ** 3.0 is 8.0, which no bound on
3.0 constrains -- and read the exponent again for the lower bound, where a base of
at least one was meant: 0.5 ** 2.0 is 0.25. `real_pow` is also underspecified at
0 ** 0. Boogie's `**` typechecks only for reals and the solver rejects what Boogie
emits for it, so this was unreachable rather than harmful.

Found with `Test/infer-fuzz.py`, added here, which generates programs over int and
real arithmetic and runs each under `/checkInfer` -- turning every inferred
invariant into an obligation the prover must discharge. On its default 400
programs: 75 inferred something it could not, every one implicating `*`, `div`,
`mod` or `/`, and none implicating anything else. After this, none do. It is a
tool rather than a test, since it is slow and a run says nothing about the
programs it did not generate, so it is not wired into CI. The existing corpus flagged only one file, because no test happens
to multiply two variables that both have a negative lower bound -- which is also
why `Test/aitest1/Linear5.bpl` recorded `x < 2` after `assume -1 <= x; x := x*x;`.
That expectation was pinning the defect; x = 10 refutes it.
@fabiomadge
fabiomadge force-pushed the fix/interval-arithmetic-rules branch from 64c9b66 to 33727e8 Compare September 20, 2026 15:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant