Skip to content

Commit 63bd11d

Browse files
Let a connective settle what its truth table settles (#880) (#907)
Simplify and evaluation disagreed about three-valued logic. Simplify gave the Kleene answer and evaluation absorbed everything into NaN, so "True or (True and (x < 0))" simplified to True and evaluated at x := i to NaN -- the same expression, two answers, and one of them claiming the thing does not exist. The tables were already three-valued. Andf reads (_, Boolean(false)) as False and (Boolean(true), _) as its right operand, which is Kleene as written. What overrode them was a single line in the shared ExpandOnTwoArguments -- if (left.IsNaN || right.IsNaN) return MathS.NaN -- running before the table was consulted. The four connectives now get first refusal on an undefined operand, through a settlesNaN flag, and hand back null where they cannot settle it, which is what still reaches NaN. Checked row by row against Kleene's tables first: and, or, implies and xor each already give the right answer for an unknown operand, including leaving it unknown where it decides the result. Opted in per node rather than changed in the helper for everything, because arithmetic must stay strict: a rule for a zero factor exists and NaN * 0 must not reach it. Measured, not assumed -- (0/0) * 0, (0/0) + 1 and (0/0) - (0/0) are all still NaN. #880 set this out as a fork between Kleene and strict and left it open for want of one measurement: how much of the suite pins strict propagation. The answer is one assertion of 6385, and it is that issue's own guard clause -- the row asserting `x < 0 and x = 0` is NaN at x = i, written to keep the test from going vacuous if a comparison ever gained a truth value. It has gained one indirectly: `i = 0` is decidably False, and False and u is False. That row moves to a test of its own, which also records the consequence: Simplify answers `False provided x in RR` there, and the condition is over-strong for that reduction, which needs one conjunct false rather than both operands real. So Simplify is now weaker than evaluation on it rather than stronger. The rules #876 conditioned want going through one at a time to see which still need it; that is not this change. Suite 6389 passed, F# wrapper 130 passed; casbench 117/119 with 0 wrong; rootcheck 596/596; simpsweep 10463/10463; propcheck 1340 checks 0 failures; crashcheck 1652 cases 0 crashes and 0 unexpected throws; boundcheck unchanged at 2 disagreements. Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent 8c56b59 commit 63bd11d

4 files changed

Lines changed: 152 additions & 10 deletions

File tree

BREAKING-CHANGES.md

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ read first.
7373
| **silent** | `DirectChildren` of a conditional set | a name off the predicate's hash, and one in 26^4 threw | `%1`, fresh by construction |
7474
| **silent** | `-(a - b)` inside a power, a function or a matrix | left as written | `b - a`, as at the root |
7575
| **silent** | `Expand` of a matrix | the matrix, unexpanded | expanded entry by entry |
76+
| **silent** | `false and u`, `true or u`, `false implies u` for an undefined `u` | `NaN` | `False`, `True`, `True` — what the truth table settles |
7677
| **silent** | `arctan(x) + arccotan(x)` | `pi/2`, wrong for every negative `x` | `pi/2` or `-pi/2` where the sign is known, else left as written |
7778
| **silent** | `log(1, 1)` | `0` | `NaN`, since it is `0/0` |
7879
| **silent** | `log(b, 1)` | `0` for any base | `0 provided not b = 1` |
@@ -407,6 +408,55 @@ have their own test asserting the unevaluated node, so a future fix flips them b
407408
`ln(x) + ln(x+1)`, both recorded elsewhere as wanting a decision rather than a guard. Issue
408409
[#902](https://github.com/asc-community/AngouriMath/issues/902).
409410

411+
### A logical connective is no longer strict in `NaN`
412+
413+
`Simplify` and evaluation disagreed about three-valued logic. `Simplify` gave the Kleene answer and
414+
evaluation absorbed everything into `NaN`, so the two contradicted each other on the same expression:
415+
416+
```
417+
"True or (True and (x < 0))".Simplify() -> True
418+
the same, at x := i, evaluated as written -> NaN (was)
419+
-> True (is)
420+
```
421+
422+
`i < 0` has no truth value — the default codomain is `Domain.Complex` and the complex numbers are not
423+
ordered — so it evaluates to `NaN`. What changed is what a connective does with such an operand.
424+
425+
| expression | was | is |
426+
|---|---|---|
427+
| `(i < 0) and False` | `NaN` | `False` |
428+
| `(i < 0) or True` | `NaN` | `True` |
429+
| `False implies (i < 0)` | `NaN` | `True` |
430+
| `(i < 0) implies True` | `NaN` | `True` |
431+
| `(i < 0) and True` | `NaN` | `NaN`, unchanged |
432+
| `(i < 0) or False` | `NaN` | `NaN`, unchanged |
433+
| `not (i < 0)` | `NaN` | `NaN`, unchanged |
434+
| `(i < 0) xor (i < 0)` | `NaN` | `NaN`, unchanged |
435+
| `(0/0) * 0`, `(0/0) + 1` | `NaN` | `NaN`, unchanged |
436+
437+
The rule is the ordinary one for three-valued logic: an operand with no truth value cannot change an
438+
answer the table settles without it, and where the answer does depend on it the result stays `NaN`.
439+
**Arithmetic is untouched**`NaN` still absorbs there, which is why this is opted into per node
440+
rather than changed for everything: a rule for a zero factor exists, and `NaN * 0` must not reach it.
441+
442+
The tables were already three-valued. `Andf` reads `(_, Boolean(false))` as `False` and
443+
`(Boolean(true), _)` as its right operand, which is Kleene as written; what overrode them was one line
444+
in the shared `ExpandOnTwoArguments`, `if (left.IsNaN || right.IsNaN) return MathS.NaN;`, running
445+
*before* the table was consulted. The connectives now get first refusal on an undefined operand and
446+
hand back `null` where they cannot settle it, which is what still reaches `NaN`.
447+
448+
**One consequence to know about.** For `x < 0 and x = 0` the evaluator now settles `False` for every
449+
`x`, since `x = 0` is decidably false at `x = i` and `False and u` is `False`. `Simplify` answers
450+
`False provided x in RR`, whose condition
451+
([#876](https://github.com/asc-community/AngouriMath/issues/876)) is over-strong for that row: the
452+
reduction needs one conjunct false, not both operands real. So `Simplify` is now weaker than evaluation
453+
there rather than stronger. It is recorded in a test rather than fixed here, because the rules #876
454+
conditioned want going through one at a time.
455+
456+
Issue [#880](https://github.com/asc-community/AngouriMath/issues/880), which set this out as a fork
457+
between Kleene and strict evaluation and left it open for want of a measurement. The measurement: one
458+
assertion in the suite changed, and it was that issue's own guard clause.
459+
410460
### A negated difference is turned round wherever it sits, and `Expand` descends into a matrix
411461

412462
`-(a - b)` became `b - a` for a whole expression and not for the same expression inside another node,

Sources/AngouriMath/Functions/Evaluation/Evaluation.Classes.cs

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -45,19 +45,37 @@ public partial record Variable
4545
/// <param name="propagateSet">
4646
/// Set operations should not be applied on all pairs of elements when it cannot be simplified.
4747
/// </param>
48+
/// <param name="settlesNaN">
49+
/// Whether <paramref name="operation"/> is asked about a <c>NaN</c> operand instead of the
50+
/// result being <c>NaN</c> outright. A logical connective can settle one -- <c>false and u</c>
51+
/// is <c>false</c> whatever <c>u</c> is -- and arithmetic cannot, so this is off by default:
52+
/// <c>NaN * 0</c> must not become <c>0</c> just because a rule for a zero factor exists.
53+
/// https://github.com/asc-community/AngouriMath/issues/880
54+
/// </param>
4855
private Entity ExpandOnTwoArguments(
4956
Entity left,
50-
Entity right,
51-
Func<Entity, Entity, Entity?> operation,
52-
Func<Entity, Entity, Entity, Entity> defaultCtor,
57+
Entity right,
58+
Func<Entity, Entity, Entity?> operation,
59+
Func<Entity, Entity, Entity, Entity> defaultCtor,
5360
bool isExact,
54-
bool propagateSet = true)
61+
bool propagateSet = true,
62+
bool settlesNaN = false)
5563
{
5664
if (isExact && this.Evaled is (Number { IsExact: true } or Boolean) and var n)
5765
return n;
5866
left = left.InnerSimplified(isExact);
5967
right = right.InnerSimplified(isExact);
60-
if (left.IsNaN || right.IsNaN) return MathS.NaN;
68+
if (left.IsNaN || right.IsNaN)
69+
{
70+
// A connective gets first refusal on an undefined operand, and hands back null where
71+
// it cannot settle the case, which is what falls through to NaN here. Its own table
72+
// is already the three-valued one: `and` reads (_, false) as false and (true, _) as
73+
// its right operand, so a NaN that genuinely decides nothing stays NaN by arriving
74+
// back out of the switch.
75+
if (settlesNaN && operation(left, right) is { } settled)
76+
return settled;
77+
return MathS.NaN;
78+
}
6179

6280
if (operation(left, right) is { } preRes)
6381
return preRes;

Sources/AngouriMath/Functions/Evaluation/Evaluation.Discrete/Evaluation.Discrete.Classes.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ protected override Entity InnerSimplify(bool isExact)
5252
(_, Boolean(true)) => left,
5353
_ => null
5454
},
55-
(@this, a, b) => ((Andf)@this).New(a, b), isExact);
55+
(@this, a, b) => ((Andf)@this).New(a, b), isExact, settlesNaN: true);
5656
}
5757

5858
partial record Orf
@@ -69,7 +69,7 @@ protected override Entity InnerSimplify(bool isExact)
6969
(_, Boolean(false)) => left,
7070
_ => null
7171
},
72-
(@this, a, b) => ((Orf)@this).New(a, b), isExact);
72+
(@this, a, b) => ((Orf)@this).New(a, b), isExact, settlesNaN: true);
7373
}
7474

7575
partial record Xorf
@@ -88,7 +88,7 @@ protected override Entity InnerSimplify(bool isExact)
8888
(_, Boolean(false)) => left,
8989
_ => null
9090
},
91-
(@this, a, b) => ((Xorf)@this).New(a, b), isExact);
91+
(@this, a, b) => ((Xorf)@this).New(a, b), isExact, settlesNaN: true);
9292
}
9393

9494
partial record Impliesf
@@ -107,7 +107,7 @@ protected override Entity InnerSimplify(bool isExact)
107107
(_, Boolean(false)) => !left,
108108
_ => null
109109
},
110-
(@this, a, b) => ((Impliesf)@this).New(a, b), isExact);
110+
(@this, a, b) => ((Impliesf)@this).New(a, b), isExact, settlesNaN: true);
111111
}
112112

113113
partial record Equalsf

Sources/Tests/UnitTests/Common/SimplificationRegressionTest.cs

Lines changed: 75 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -473,7 +473,6 @@ public void ExcludedMiddleHoldsWhicheverOperandCarriesTheNegation(string input)
473473
[Theory]
474474
[InlineData("x < 0 and x >= 0")]
475475
[InlineData("x > 0 and x <= 0")]
476-
[InlineData("x < 0 and x = 0")]
477476
[InlineData("x < 0 or x >= 0")]
478477
[InlineData("x <= 0 or x > 0")]
479478
[InlineData("x < x")]
@@ -490,6 +489,29 @@ public void DecidingAPairOfComparisonsKeepsItsValueOffTheRealLine(string input)
490489
Assert.Equal(atI, original.Simplify().Substitute("x", "i").Evaled);
491490
}
492491

492+
// https://github.com/asc-community/AngouriMath/issues/880
493+
// `x < 0 and x = 0` was a row of the theory above until evaluation became Kleene, and it
494+
// no longer belongs there: at x = i one conjunct is *decidably* false -- `i = 0` is False,
495+
// not NaN -- and `False and u` is False whatever `u` is. So there is something to decide
496+
// here, and the value is False rather than NaN.
497+
//
498+
// Which leaves the pair disagreeing the other way round from #876. Evaluation now settles
499+
// the conjunction everywhere, while Simplify answers `False provided x in RR`, whose
500+
// condition is unnecessary for this row: the reduction needs one conjunct to be false, not
501+
// both operands to be real. The condition is over-strong rather than wrong, so it is
502+
// recorded here rather than removed -- the rules #876 conditioned would want going through
503+
// one at a time to see which of them still need it, and that is not this change.
504+
[Fact]
505+
public void AConjunctionWithOneFalseConjunctIsFalseOffTheRealLineToo()
506+
{
507+
var original = "x < 0 and x = 0".ToEntity();
508+
Assert.Equal(Entity.Boolean.False, original.Substitute("x", "i").Evaled);
509+
510+
// And what Simplify gives is weaker, which is the follow-up rather than a regression:
511+
// it declines off the real line where the evaluator decides.
512+
Assert.Equal(MathS.NaN, original.Simplify().Substitute("x", "i").Evaled);
513+
}
514+
493515
// https://github.com/asc-community/AngouriMath/issues/876 §3
494516
// The unsatisfiable conjunction was decided and the valid disjunction was not, so the
495517
// library took the half of excluded middle that is unsound off the real line and
@@ -876,5 +898,57 @@ public void ASystemsAnswerSimplifiesEntryByEntry()
876898
$"the system's answer simplified to {simplified.Stringize()}, which is no shorter "
877899
+ $"than the {answer.Stringize()} it came from");
878900
}
901+
902+
// https://github.com/asc-community/AngouriMath/issues/880
903+
// A connective is no longer strict in NaN. `false and u` is false and `true or u` is true
904+
// whatever `u` is, so an operand with no truth value does not absorb an answer the truth
905+
// table settles without it. Simplify already answered this way -- `true or (true and
906+
// (x < 0))` is True -- while evaluation answered NaN, so the two contradicted each other.
907+
//
908+
// `i < 0` is the undefined operand throughout: the default codomain is Domain.Complex and
909+
// the complex numbers are not ordered.
910+
[Theory]
911+
[InlineData("(i < 0) and false", "false")]
912+
[InlineData("false and (i < 0)", "false")]
913+
[InlineData("(i < 0) or true", "true")]
914+
[InlineData("true or (i < 0)", "true")]
915+
[InlineData("false implies (i < 0)", "true")]
916+
[InlineData("(i < 0) implies true", "true")]
917+
public void AConnectiveSettlesWhatItsTruthTableSettles(string expression, string expected) =>
918+
Assert.Equal(expected.ToEntity(), expression.ToEntity().Evaled);
919+
920+
// And what the table does not settle stays unsettled: NaN means "this does not exist", so
921+
// a connective may not invent a value for it either.
922+
[Theory]
923+
[InlineData("(i < 0) and true")]
924+
[InlineData("(i < 0) or false")]
925+
[InlineData("not (i < 0)")]
926+
[InlineData("(i < 0) xor (i < 0)")]
927+
[InlineData("(i < 0) xor true")]
928+
[InlineData("(i < 0) implies false")]
929+
public void AConnectiveInventsNothingItCannotSettle(string expression) =>
930+
Assert.Equal(MathS.NaN, expression.ToEntity().Evaled);
931+
932+
// Arithmetic stays strict, which is the reason this is opted into per node rather than
933+
// done in the shared helper for everything: a rule for a zero factor exists, and NaN * 0
934+
// must not reach it.
935+
[Theory]
936+
[InlineData("(0/0) * 0")]
937+
[InlineData("(0/0) + 1")]
938+
[InlineData("(0/0) - (0/0)")]
939+
[InlineData("(0/0) ^ 0")]
940+
public void ArithmeticIsStillStrictInNaN(string expression) =>
941+
Assert.Equal(MathS.NaN, expression.ToEntity().Evaled);
942+
943+
// The contradiction this removes, stated as the commutation it broke.
944+
[Theory]
945+
[InlineData("true or (true and (x < 0))")]
946+
[InlineData("false and (x < 0)")]
947+
public void SimplifyAndEvaluationAgreeOffTheRealLine(string input)
948+
{
949+
var original = input.ToEntity();
950+
Assert.Equal(original.Substitute("x", "i").Evaled,
951+
original.Simplify().Substitute("x", "i").Evaled);
952+
}
879953
}
880954
}

0 commit comments

Comments
 (0)