Skip to content

fix: !(x < y) is rewritten to y <= x even for floats - #1156

Draft
fabiomadge wants to merge 3 commits into
masterfrom
fix/negated-float-comparison
Draft

fabiomadge wants to merge 3 commits into
masterfrom
fix/negated-float-comparison

Conversation

@fabiomadge

@fabiomadge fabiomadge commented Aug 31, 2026 •

Copy link
Copy Markdown
Contributor

Expr.Not turned a negated order relation into the reverse relation for every type. That is valid only
where the order is total: every IEEE comparison is false when an operand is NaN, so !(a < b) holds of a
NaN while b <= a does not.

procedure P(f: float24e8)
{
  if (f < 0x1.0e0f24e8) { } else { assert 0x1.0e0f24e8 <= f; }
}

No flags → 1 verified, 0 errors. The else branch is reachable with a NaN, where the assertion is
false.

Fix. Expr.Not leaves an order relation alone unless the operand's type is known and not float. It
usually is not known, and cannot be made so: the guard is negated from the Implementation constructor,
which flattens the structured statements while parsing, before any identifier is resolved. So no
reordering inside resolution can supply a type, and Program.ReverseGuardNegations asks again once types
exist. The second commit has IntervalDomain.Constraint push a hand-written ! through the same helper,
which it never did.

Scope. Equality is untouched, == on floats being bit identity and so total. Inference gets sharper
where the domain can now read a negation: aitest1/ineq.bpl infers i == 10 where it inferred
0 <= i && i < 11. Two printed forms change. roundingmodes/InvalidOperators.bpl loses four phantom type
errors, which named operators the program does not contain. inline/test4.bpl keeps an unreversed guard,
because a -print of the program as parsed runs before resolution while the blocks already exist, so the
reversal cannot reach it.

Not addressed. The pass finds guards by {:partition}, which a user may write; such an assume gets the
same rewriting, semantics-preserving but visible through /print.

One of six. TryPushNegation sets Type and TypeParameters independently, which matters once #1155
lands: it types comparisons at construction, and a node with a type but no type parameters crashes
MonomorphizationDuplicator.

Expr.Not turned a negated order relation into the reverse relation for every type. That is only valid
where the order is total: on floats every IEEE comparison is false when an operand is NaN, so "!(a < b)"
holds of a NaN while "b <= a" does not. Equality is unaffected, since Boogie's == on floats is bit
identity and therefore total.

The type is not always available where Expr.Not is called: the parser negates the guard of an "if" or a
"while" while it is still building an implementation's blocks. Such a negation is now left alone, and
Program.Typecheck asks Expr.Not again once the types are in place, so an integer guard is still reversed
and a float's is not. That also stops Boogie reporting a type error against the reversed operator it
synthesised rather than the one the program contains.
A negation the parser built for the "!" operator never went through Expr.Not, so Constraint saw shapes it
had no case for and learned nothing from them. It now pushes the negation inwards with the same rewriting
Expr.Not applies everywhere else, which by then has the types it needs. Expr.Not declines to reverse an
order relation on a float, so a result that is still a negation is one there is nothing to learn from.
@fabiomadge
fabiomadge force-pushed the fix/negated-float-comparison branch from a639f9d to d3416a8 Compare September 20, 2026 15:50
@shazqadeer

Copy link
Copy Markdown
Contributor

@fabiomadge : Is it possible to solve the problem by avoiding any attempt to rewrite negations until the type is determined (and known not to be float)? That seems simpler at a first glance.

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.

2 participants