Skip to content

Carry the condition an order comparison is decided under (#876), and read a set-builder's predicate inside its binder (#878) - #879

Merged
Rafael-SOWNet merged 2 commits into
masterfrom
fix/comparison-truth-condition
Aug 11, 2026
Merged

Rafael-SOWNet merged 2 commits into
masterfrom
fix/comparison-truth-condition

Conversation

@Rafael-SOWNet

@Rafael-SOWNet Rafael-SOWNet commented Aug 11, 2026 •

Copy link
Copy Markdown
Member

Closes #876 and #878. Based on #877 — that PR is the first commit's parent, so merge it first and this diff shrinks to its own two commits.

What was wrong

The complex numbers are not ordered, so i < 0 is NaN. Three rules decided a pair of order comparisons as though a comparison always had a truth value:

input was at x := i, as written now
x < 0 and x >= 0 False NaN False provided x in RR
x > 0 and x <= 0 False NaN False provided x in RR
x < x False NaN False provided x in RR
x >= x True NaN True provided x in RR
not (x < 0) or (x < 0) True NaN True provided x in RR
x < 0 or x >= 0 not reduced NaN True provided x in RR
x <= 0 or x >= 0 not reduced NaN True provided x in RR

Simplify did not commute with Substitute, and the default codomain is Domain.Complex. The reductions themselves are right — they are the answers over the reals — so this attaches the condition rather than withdrawing the answer. Entity.Provided drops a condition that is True, so nothing is attached where the condition already holds:

2 < 0 and 2 >= 0          ->  False          (operands are numbers)
abs(x) < abs(x)           ->  False          (Absf.Codomain is Real)
(a = b) or not (a = b)    ->  True           (equality is total over C)
x < 0 and x >= 0          ->  False          under Codomain = Domain.Real

That last line is the first time anything outside the limit machinery reads MathS.Settings.Codomain, which is what #876 asked for.

DomainCondition is not the mechanism, despite the neighbouring rules using it. It records singularities — division by zero, log poles — and says nothing about where an order comparison is defined. x < x needs both.

The missing half

OppositeSigns decided the unsatisfiable conjunction; there was no counterpart for the disjunction, so the library took the half of excluded middle that needs the operands to be real and skipped the half that needs exactly the same thing. ExhaustiveSigns is its mirror, and over the reals it settles #876 §3's stated consequence:

piecewise(0 provided x < 0, 0 provided x >= 0)   ->   0

Why #877's test rows moved

#877 pinned not (x < 0) or (x < 0) to True, which its own body records as the unsound answer left for later. The six order-comparison rows move to a test that sets Codomain = Domain.Real, where True is honest; the rows that hold unconditionally — equality, set membership, a boolean variable — stay pinned to True outright. The new §2 test asserts a commutation rather than a printed form, so it holds whatever shape the condition takes.

#878, which this had to fix to avoid shipping a crash

{ x : x > 0 or x <= 0 } reaches a pre-existing abort once §3's rule fires. Both defects are on master with no help from this branch:

{ x : 1/x = 0 }.Simplify()    ->  {  } provided not x = 0
{ x : 1 = 1 }.Simplify()      ->  AngouriBugException
{ x : x = x }.Simplify()      ->  AngouriBugException
{ x : x/x = 1 }.Simplify()    ->  AngouriBugException

The first is a scope error: ConditionalSet passed its predicate to the generic argument-expanding helper, which lifts a Providedf onto the whole expression — right for a function, wrong for a binder, so the bound variable ended up outside its own binder. Reading membership as the predicate holding removes the need to hoist anything: a predicate that is False where its condition holds and undefined where it does not admits nothing, and one that is True under a condition admits exactly what the condition admits, so the condition becomes the predicate.

The crash was a branch that had never worked — for a true predicate the code returned the node's Codomain, a set-builder's Codomain is Domain.Any, and SpecialSet.Create has no case for Any. Nothing names every value a symbol could take, so such a predicate is now left as written.

TryContains needed the same reading and is fixed with it. Answers gained rather than lost:

{ x : 1/x = 0 }              ->  {  }
{ x : x/x = 1 }              ->  { x : not x = 0 }
{ x : x > 0 or x <= 0 }      ->  RR
{ x : x > 0 } /\ { z : z < 0 }  ->  {  }

What this deliberately does not fix

and and or are strict in NaN, not Kleene: (i<0) and False is NaN and (i<0) or True is NaN. Under that semantics the annihilation and absorption rules are unsound too — True or (True and (x<0)) simplifies to True and is NaN at x := i — but by a different root cause, and conditioning them one at a time is not the fix. Filed as #880 rather than smuggled in here.

{ x : True } is left as written because Domain.Any has no set. Naming the universal set is its own question.

Verification

  • 6253 C# tests, 130 F# tests — 0 failures. 32 test rows are new across 6 new methods, and 16 of them fail with the four source files reverted to master; a further 6 rows moved out of Hold excluded middle whichever operand carries the negation (#876) #877's theory into the real-codomain test
  • casbench 117/119, 0 wrong, 0 error, 0 timeout — every verdict and answer in coverage.md byte-identical, only timings moved
  • propcheck 1340 checks, 0 failures
  • simpsweep 62778 point comparisons over 10463 expressions, 0 disagreements
  • rootcheck 596 cases, 0 incomplete, 0 unsound
  • boolmin unchanged at 6/9

🤖 Generated with Claude Code

Rafael-SOWNet and others added 2 commits August 11, 2026 10:28
ConditionalSet.InnerSimplify handed the predicate to the generic
argument-expanding helper, which lifts a Providedf out of an argument and puts
it on the whole expression. That is right for a function, whose arguments are
values, and wrong for a node that binds a variable: `{ x : 1/x = 0 }` came back
as `{ } provided not x = 0`, where the x named in the condition is no longer the
x the set ranges over.

Membership is the predicate holding, so the condition did not need hoisting
anywhere. A predicate that is False where its condition holds and undefined
where it does not admits nothing, and the answer is `{ }`. A predicate that is
True under a condition admits exactly the points the condition admits, so the
condition becomes the predicate and stays inside the binder --
`{ x : x/x = 1 }` is `{ x : not x = 0 }`.

The other branch had never worked. For a predicate that holds everywhere the
code returned the node's Codomain, a set-builder's Codomain is Domain.Any, and
SpecialSet.Create has no case for Any -- so `{ x : 1 = 1 }`, `{ x : x = x }` and
`{ x : x/x = 1 }` all threw AngouriBugException out of Simplify on valid input.
No set names every value a symbol could take, which is why Any has none, so a
predicate that holds everywhere is left as written; naming the universal set is
a separate question.

TryContains needed the same reading. At x := a the predicate of
`{ x : x = a and x > a }` is False for real a and undefined for the rest of the
plane, so a is not a member anywhere and membership is decided -- but the code
read only EvaluableBoolean and gave up on a Providedf.

Measured: 6253 tests pass, 0 fail; casbench 117/119 with every verdict and
answer in coverage.md unchanged; propcheck, simpsweep and rootcheck clean.
`{ x : 1/x = 0 }` -> `{ }`, `{ x : x/x = 1 }` -> `{ x : not x = 0 }`,
`{ x : 1 = 1 }` -> `{ x : True }` instead of a crash.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The complex numbers are not ordered, so `i < 0` is NaN and a comparison need not
have a truth value. Three rules decided a pair of comparisons as though it
always did:

  x < 0 and x >= 0   ->  False       at x := i the statement is NaN
  x < x              ->  False       likewise
  not (x < 0) or (x < 0)  ->  True   likewise

So Simplify did not commute with Substitute, silently, and the default codomain
is Domain.Complex. The reductions themselves are right -- they are the answers
over the reals -- and what was missing is the condition they hold under. Each now
carries it, and Provided drops a condition that is True, so nothing is attached
where the operands are already real: `2 < 0 and 2 >= 0` and
`abs(x) < abs(x)` still answer False outright, and under
MathS.Settings.Codomain = Domain.Real so does `x < 0 and x >= 0`. Nothing outside
the limit machinery read that setting before this.

The disjunction was the other half of the same law and was missing entirely. The
unsatisfiable conjunction was decided while the valid disjunction was not, so the
library took the half of excluded middle that needs the operands to be real and
skipped the half that needs exactly the same thing. ExhaustiveSigns is the mirror
of OppositeSigns: `x < 0 or x >= 0` is True where x is real, and over the reals
`piecewise(0 provided x < 0, 0 provided x >= 0)` now reaches 0, which #876 §3
gives as the consequence.

DomainCondition is not the mechanism, despite the neighbouring rules using it: it
records singularities -- division by zero, log poles -- and says nothing about
where an order comparison is defined. Both are needed on `x < x`.

The excluded-middle rules keep answering True unconditionally where the
proposition has a truth value everywhere, which is why equality, set membership
and a boolean variable are unaffected: `(a = b) or not (a = b)` is True with no
condition. Only the order comparisons gained one.

Measured: 6253 C# tests and 130 F# tests pass, 0 fail; casbench 117/119, 0 wrong,
0 error, 0 timeout, with every verdict and answer in coverage.md unchanged and
only timings moved; propcheck 1340 checks, simpsweep 62778 point comparisons over
10463 expressions and rootcheck 596 cases all clean; boolmin unchanged at 6/9.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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.

p or not p depends on which side the not is written, and reduces to True where the proposition is undefined

1 participant