From 6ef7d40f1ae610d02a6b631bc2a7ae06772e7db2 Mon Sep 17 00:00:00 2001 From: Rafael Vuijk Date: Thu, 29 Oct 2026 18:54:24 +0000 Subject: [PATCH 1/2] A compiled statement with no truth value has none A comparison over Complex was refused, as the operators are not defined for it, so piecewise and provided compiled over double but not over Complex. Over double a comparison with NaN was false, so that not (x > 1) was true at NaN, where evaluation says it has no truth value. The LINQ compiler now gives a comparison over Complex, double or float the truth value evaluation does: it holds or fails where both sides are real numbers, and is null where one is NaN or not real. The connectives follow Kleene's tables, as evaluation's do, so (i > 1) or true is true and not (i > 1) is null. A provided or a piecewise whose predicate is null has no value, nor does an Iverson bracket. Compiled to bool? a statement with no truth value is null, and compiled to bool it is false, as one that does not hold. Each is checked against evaluation over a grid of statements and points. Part of #2195 Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_012sonx8iAspMiwRwokT1Ura --- BREAKING-CHANGES.md | 19 ++++ Sources/.editorconfig | 6 ++ .../IntoLinq/CompilationProtocol.cs | 53 ++++++++-- .../Compilation/IntoLinq/ThreeValuedLogic.cs | 99 +++++++++++++++++++ Sources/Tests/AotSmokeTest/Program.cs | 9 ++ .../Common/ThreeValuedCompilationTest.cs | 86 ++++++++++++++++ 6 files changed, 264 insertions(+), 8 deletions(-) create mode 100644 Sources/AngouriMath/Functions/Compilation/IntoLinq/ThreeValuedLogic.cs create mode 100644 Sources/Tests/UnitTests/Common/ThreeValuedCompilationTest.cs diff --git a/BREAKING-CHANGES.md b/BREAKING-CHANGES.md index 9d3709ea1..f1479e026 100644 --- a/BREAKING-CHANGES.md +++ b/BREAKING-CHANGES.md @@ -323,6 +323,25 @@ negative even integer or off the integers, since the extension through the gamma | `"n!!".ToEntity()` | throws `UnhandledParseException` | `n!!` | | `"n!!!".ToEntity()` | throws `UnhandledParseException` | throws `UnrecognizedFunctionParseException` | +### A compiled statement with no truth value has none + +**Silent in part: a compiled comparison has the truth value evaluation gives it.** A comparison over `Complex` was refused +with `UncompilableNodeException`, and now compiles: it holds or fails where both sides are real, and has no truth value +where one is not, as `i > 1` has none when evaluated. Over `double` and `float` a comparison with `NaN` was false, +so that its negation was true. It has no truth value now either. The connectives follow Kleene's tables, as evaluation's +do: `(i > 1) or true` is true and `not (i > 1)` has none. A `provided` or `piecewise` whose predicate has no truth +value has no value, and neither does an Iverson bracket. Compiled to `bool?`, a statement with no truth value is `null`; +compiled to `bool`, it is `false`, as one that does not hold +([#2195](https://github.com/asc-community/AngouriMath/issues/2195)). + +| Input | Was (2.6.0) | Now | +|---|---|---| +| `"not (x > 1)".ToEntity().Compile("x")(double.NaN)` | `True` | `False` | +| `"x > 1".ToEntity().Compile("x")(Complex.ImaginaryOne)` | `UncompilableNodeException` | `False` | +| `"x > 1 or true".ToEntity().Compile("x")(Complex.ImaginaryOne)` | `UncompilableNodeException` | `True` | +| `"x provided x > 1".ToEntity().Compile("x")(2)` | `UncompilableNodeException` | `(2, 0)` | +| `"x provided x > 1".ToEntity().Compile("x")(Complex.ImaginaryOne)` | `UncompilableNodeException` | `(NaN, 0)` | + ### The conjugate, the argument, the parts and the Iverson bracket compile **Not silent: these compile where they threw `UncompilableNodeException`.** `conj`, `arg`, `Re` and `Im` compile diff --git a/Sources/.editorconfig b/Sources/.editorconfig index cb1b7bac9..4eff3ecb3 100644 --- a/Sources/.editorconfig +++ b/Sources/.editorconfig @@ -348,6 +348,12 @@ file_header_template=\nCopyright (c) 2019-2026 Angouri.\nAngouriMath is licensed [AngouriMath/Functions/Compilation/IntoLinq/TensorCompilation.cs] file_header_template=\nCopyright (c) 2019-2026 Angouri.\nAngouriMath is licensed under MIT.\nDetails: https://github.com/asc-community/AngouriMath/blob/master/LICENSE.md.\nWebsite: https://am.angouri.org.\n +[AngouriMath/Functions/Compilation/IntoLinq/ThreeValuedLogic.cs] +file_header_template=\nCopyright (c) 2019-2026 Angouri.\nAngouriMath is licensed under MIT.\nDetails: https://github.com/asc-community/AngouriMath/blob/master/LICENSE.md.\nWebsite: https://am.angouri.org.\n + +[Tests/UnitTests/Common/ThreeValuedCompilationTest.cs] +file_header_template=\nCopyright (c) 2019-2026 Angouri.\nAngouriMath is licensed under MIT.\nDetails: https://github.com/asc-community/AngouriMath/blob/master/LICENSE.md.\nWebsite: https://am.angouri.org.\n + [Tests/UnitTests/Common/CompileMatrixTest.cs] file_header_template=\nCopyright (c) 2019-2026 Angouri.\nAngouriMath is licensed under MIT.\nDetails: https://github.com/asc-community/AngouriMath/blob/master/LICENSE.md.\nWebsite: https://am.angouri.org.\n diff --git a/Sources/AngouriMath/Functions/Compilation/IntoLinq/CompilationProtocol.cs b/Sources/AngouriMath/Functions/Compilation/IntoLinq/CompilationProtocol.cs index 054cde6d3..d855dae6f 100644 --- a/Sources/AngouriMath/Functions/Compilation/IntoLinq/CompilationProtocol.cs +++ b/Sources/AngouriMath/Functions/Compilation/IntoLinq/CompilationProtocol.cs @@ -74,6 +74,12 @@ public virtual Expression ConvertType(Expression expr, Type type) if (expr.Type == type) return expr; + // A statement with no truth value is null as a bool?, and as a bool is not one that + // holds. https://github.com/asc-community/AngouriMath/issues/2195 + if (expr.Type == typeof(bool?) && type == typeof(bool)) + return Expression.Call(ThreeValuedLogic.HoldsMethod, expr); + if (expr.Type == typeof(bool) && type == typeof(bool?)) + return Expression.Convert(expr, type); // A tensor converts to nothing but itself: a matrix over one element type is not // a matrix over another, and it is not a number. if (TensorCompilation.IsTensor(expr.Type) || TensorCompilation.IsTensor(type)) @@ -160,7 +166,9 @@ Argf when ShouldBeAtLeastDouble(e) is var newE => Expression.Call(GetDef(" Ref when ShouldBeAtLeastDouble(e) is var newE => Expression.Call(GetDef("Re", 1, newE.Type), newE), Imf when ShouldBeAtLeastDouble(e) is var newE => Expression.Call(GetDef("Im", 1, newE.Type), newE), Iversonf when e.Type == typeof(bool) => Expression.Condition(e, Expression.Constant(1), Expression.Constant(0)), + Iversonf when e.Type == typeof(bool?) => Expression.Call(ThreeValuedLogic.IversonMethod, e), + Notf when e.Type == typeof(bool?) => Expression.Call(ThreeValuedLogic.NotMethod, e), Notf => Expression.Not(e), // A node this converter has no case for has no compiled form, and that is a gap @@ -222,16 +230,23 @@ Logf when Maxf => Expression.Call(GetDef("Max", 2, left.Type), left, right), Minf => Expression.Call(GetDef("Min", 2, left.Type), left, right), + // A connective of a statement that may have no truth value follows Kleene's tables. + // https://github.com/asc-community/AngouriMath/issues/2195 + Andf when IsUndecidable(left, right) => Expression.Call(ThreeValuedLogic.AndMethod, Undecidable(left), Undecidable(right)), + Orf when IsUndecidable(left, right) => Expression.Call(ThreeValuedLogic.OrMethod, Undecidable(left), Undecidable(right)), + Xorf when IsUndecidable(left, right) => Expression.Call(ThreeValuedLogic.XorMethod, Undecidable(left), Undecidable(right)), + Impliesf when IsUndecidable(left, right) => Expression.Call(ThreeValuedLogic.ImpliesMethod, Undecidable(left), Undecidable(right)), + Equalsf when IsUndecidable(left, right) => Expression.Equal(Undecidable(left), Undecidable(right), liftToNull: true, method: null), Andf => Expression.And(left, right), Orf => Expression.Or(left, right), Xorf => Expression.ExclusiveOr(left, right), Impliesf => Expression.Or(Expression.Not(left), right), - Lessf => Operator(ExpressionType.LessThan, left, right), - LessOrEqualf => Operator(ExpressionType.LessThanOrEqual, left, right), - Greaterf => Operator(ExpressionType.GreaterThan, left, right), - GreaterOrEqualf => Operator(ExpressionType.GreaterThanOrEqual, left, right), - Equalsf => Operator(ExpressionType.Equal, left, right), + Lessf => Compared(ExpressionType.LessThan, left, right), + LessOrEqualf => Compared(ExpressionType.LessThanOrEqual, left, right), + Greaterf => Compared(ExpressionType.GreaterThan, left, right), + GreaterOrEqualf => Compared(ExpressionType.GreaterThanOrEqual, left, right), + Equalsf => Compared(ExpressionType.Equal, left, right), Providedf => HandleProvidedf(left, right), @@ -243,7 +258,7 @@ Expression HandleProvidedf(Expression expr, Expression cond) { var nan = ConvertNaN(expr.Type); (expr, nan) = EqualizeTypesIfAble(expr, nan); - return Expression.Condition(cond, expr, nan); + return Expression.Condition(Holds(cond), expr, nan); } } @@ -301,7 +316,7 @@ private Expression HandlePiecewise(IEnumerable en) (expression, nan) = EqualizeTypesIfAble(expression, nan); - Expression piecewiseExpr = Expression.Condition(predicate, expression, nan); // could be ConstantExpression in certain cases if values of children could be known + Expression piecewiseExpr = Expression.Condition(Holds(predicate), expression, nan); // could be ConstantExpression in certain cases if values of children could be known // additional cases for (int i = children.Length - 3; i >= 1; i -= 2) @@ -311,7 +326,7 @@ private Expression HandlePiecewise(IEnumerable en) (expression, piecewiseExpr) = EqualizeTypesIfAble(expression, piecewiseExpr); - piecewiseExpr = Expression.Condition(predicate, expression, piecewiseExpr); + piecewiseExpr = Expression.Condition(Holds(predicate), expression, piecewiseExpr); } return piecewiseExpr; @@ -445,6 +460,28 @@ private static MethodInfo Cmp(Expression> e) => ((BinaryExpression)e.Body).Method ?? throw new AngouriBugException($"{typeof(T)} defines this operator as an instruction, not a method"); + /// + /// A comparison, which has no truth value where an operand is NaN or the two are not both + /// real, for the types that have a NaN: i > 1 is over + /// , where it was refused, and NaN > 1 over + /// , where it was false, so that its negation was true. Other types + /// compare as they did. https://github.com/asc-community/AngouriMath/issues/2195 + /// + private static Expression Compared(ExpressionType kind, Expression left, Expression right) + => left.Type == right.Type && ThreeValuedLogic.Comparison(kind, left.Type) is { } method + ? Expression.Call(method, left, right) + : Operator(kind, left, right); + + private static bool IsUndecidable(Expression left, Expression right) + => left.Type == typeof(bool?) || right.Type == typeof(bool?); + + private static Expression Undecidable(Expression statement) + => statement.Type == typeof(bool) ? Expression.Convert(statement, typeof(bool?)) : statement; + + /// A predicate as a : one with no truth value does not hold. + private static Expression Holds(Expression predicate) + => predicate.Type == typeof(bool?) ? Expression.Call(ThreeValuedLogic.HoldsMethod, predicate) : predicate; + // MakeBinary with a null method is exactly what Expression.Add and its neighbours do, so a // type that is not in the table is built the way it always was. liftToNull: false is their // default too, and MakeBinary ignores it for everything but the comparisons. diff --git a/Sources/AngouriMath/Functions/Compilation/IntoLinq/ThreeValuedLogic.cs b/Sources/AngouriMath/Functions/Compilation/IntoLinq/ThreeValuedLogic.cs new file mode 100644 index 000000000..2df503218 --- /dev/null +++ b/Sources/AngouriMath/Functions/Compilation/IntoLinq/ThreeValuedLogic.cs @@ -0,0 +1,99 @@ +// +// Copyright (c) 2019-2026 Angouri. +// AngouriMath is licensed under MIT. +// Details: https://github.com/asc-community/AngouriMath/blob/master/LICENSE.md. +// Website: https://am.angouri.org. +// + +using System; +using System.Linq.Expressions; +using Complex = System.Numerics.Complex; +using System.Reflection; + +namespace AngouriMath.Core.Compilation.IntoLinq +{ + /// + /// The compiled statements, with a third truth value for one that has none: i > 1 + /// compares numbers that are not ordered, and NaN > 1 compares with no number at all. + /// Each is , as EvalNumerical leaves either as NaN, and the + /// connectives follow Kleene's tables, as EvalNumerical's do: (i > 1) or true is + /// true, (i > 1) and false is false, and not (i > 1) has no value. + /// https://github.com/asc-community/AngouriMath/issues/2195 + /// + internal static class ThreeValuedLogic + { + internal static bool? Less(Complex a, Complex b) => Ordered(a, b) ? a.Real < b.Real : null; + internal static bool? LessOrEqual(Complex a, Complex b) => Ordered(a, b) ? a.Real <= b.Real : null; + internal static bool? Greater(Complex a, Complex b) => Ordered(a, b) ? a.Real > b.Real : null; + internal static bool? GreaterOrEqual(Complex a, Complex b) => Ordered(a, b) ? a.Real >= b.Real : null; + internal static bool? Equal(Complex a, Complex b) => IsNaN(a) || IsNaN(b) ? null : a == b; + + internal static bool? Less(double a, double b) => double.IsNaN(a) || double.IsNaN(b) ? null : a < b; + internal static bool? LessOrEqual(double a, double b) => double.IsNaN(a) || double.IsNaN(b) ? null : a <= b; + internal static bool? Greater(double a, double b) => double.IsNaN(a) || double.IsNaN(b) ? null : a > b; + internal static bool? GreaterOrEqual(double a, double b) => double.IsNaN(a) || double.IsNaN(b) ? null : a >= b; + internal static bool? Equal(double a, double b) => double.IsNaN(a) || double.IsNaN(b) ? null : a == b; + + internal static bool? Less(float a, float b) => float.IsNaN(a) || float.IsNaN(b) ? null : a < b; + internal static bool? LessOrEqual(float a, float b) => float.IsNaN(a) || float.IsNaN(b) ? null : a <= b; + internal static bool? Greater(float a, float b) => float.IsNaN(a) || float.IsNaN(b) ? null : a > b; + internal static bool? GreaterOrEqual(float a, float b) => float.IsNaN(a) || float.IsNaN(b) ? null : a >= b; + internal static bool? Equal(float a, float b) => float.IsNaN(a) || float.IsNaN(b) ? null : a == b; + + internal static bool? Not(bool? a) => a is { } value ? !value : null; + internal static bool? And(bool? a, bool? b) => a == false || b == false ? false : a == true && b == true ? true : null; + internal static bool? Or(bool? a, bool? b) => a == true || b == true ? true : a == false && b == false ? false : null; + internal static bool? Xor(bool? a, bool? b) => a is { } left && b is { } right ? left != right : null; + internal static bool? Implies(bool? a, bool? b) => Or(Not(a), b); + + /// Whether the statement holds: where it does not, and where it has no truth value. + internal static bool Holds(bool? a) => a == true; + + /// The Iverson bracket of a statement, which has no value where the statement has none. + internal static int? Iverson(bool? a) => a is { } value ? (value ? 1 : 0) : null; + + // Two numbers are ordered where both are real: the complex numbers are not. + private static bool Ordered(Complex a, Complex b) + => a.Imaginary == 0 && b.Imaginary == 0 && !double.IsNaN(a.Real) && !double.IsNaN(b.Real); + + private static bool IsNaN(Complex a) => double.IsNaN(a.Real) || double.IsNaN(a.Imaginary); + + /// + /// The comparison of two operands of one type, for the types whose comparisons can have no + /// truth value, or . Named rather than looked up by name, for the trimmer. + /// + internal static MethodInfo? Comparison(ExpressionType kind, Type type) + => comparisons.TryGetValue((kind, type), out var method) ? method : null; + + [ConstantField] internal static readonly MethodInfo NotMethod = Unary(a => Not(a)); + [ConstantField] internal static readonly MethodInfo AndMethod = Binary((a, b) => And(a, b)); + [ConstantField] internal static readonly MethodInfo OrMethod = Binary((a, b) => Or(a, b)); + [ConstantField] internal static readonly MethodInfo XorMethod = Binary((a, b) => Xor(a, b)); + [ConstantField] internal static readonly MethodInfo ImpliesMethod = Binary((a, b) => Implies(a, b)); + [ConstantField] internal static readonly MethodInfo HoldsMethod = ((MethodCallExpression)((Expression>)(a => Holds(a))).Body).Method; + [ConstantField] internal static readonly MethodInfo IversonMethod = ((MethodCallExpression)((Expression>)(a => Iverson(a))).Body).Method; + + [ConstantField] private static readonly Dictionary<(ExpressionType Kind, Type Type), MethodInfo> comparisons = new() + { + { (ExpressionType.LessThan, typeof(Complex)), Compared((a, b) => Less(a, b)) }, + { (ExpressionType.LessThanOrEqual, typeof(Complex)), Compared((a, b) => LessOrEqual(a, b)) }, + { (ExpressionType.GreaterThan, typeof(Complex)), Compared((a, b) => Greater(a, b)) }, + { (ExpressionType.GreaterThanOrEqual, typeof(Complex)), Compared((a, b) => GreaterOrEqual(a, b)) }, + { (ExpressionType.Equal, typeof(Complex)), Compared((a, b) => Equal(a, b)) }, + { (ExpressionType.LessThan, typeof(double)), Compared((a, b) => Less(a, b)) }, + { (ExpressionType.LessThanOrEqual, typeof(double)), Compared((a, b) => LessOrEqual(a, b)) }, + { (ExpressionType.GreaterThan, typeof(double)), Compared((a, b) => Greater(a, b)) }, + { (ExpressionType.GreaterThanOrEqual, typeof(double)), Compared((a, b) => GreaterOrEqual(a, b)) }, + { (ExpressionType.Equal, typeof(double)), Compared((a, b) => Equal(a, b)) }, + { (ExpressionType.LessThan, typeof(float)), Compared((a, b) => Less(a, b)) }, + { (ExpressionType.LessThanOrEqual, typeof(float)), Compared((a, b) => LessOrEqual(a, b)) }, + { (ExpressionType.GreaterThan, typeof(float)), Compared((a, b) => Greater(a, b)) }, + { (ExpressionType.GreaterThanOrEqual, typeof(float)), Compared((a, b) => GreaterOrEqual(a, b)) }, + { (ExpressionType.Equal, typeof(float)), Compared((a, b) => Equal(a, b)) }, + }; + + private static MethodInfo Compared(Expression> e) => ((MethodCallExpression)e.Body).Method; + private static MethodInfo Unary(Expression> e) => ((MethodCallExpression)e.Body).Method; + private static MethodInfo Binary(Expression> e) => ((MethodCallExpression)e.Body).Method; + } +} diff --git a/Sources/Tests/AotSmokeTest/Program.cs b/Sources/Tests/AotSmokeTest/Program.cs index 8e1d03fd6..ebb6ae5fd 100644 --- a/Sources/Tests/AotSmokeTest/Program.cs +++ b/Sources/Tests/AotSmokeTest/Program.cs @@ -295,6 +295,15 @@ private static void Compilation() var piecewise = "piecewise(x provided x > 0, 0 provided x <= 0)".Compile(x); Check("compiled piecewise at -3 and 3", piecewise(-3) == 0 && piecewise(3) == 3, $"{piecewise(-3)}, {piecewise(3)}"); + // A comparison of numbers that are not ordered has no truth value, and the connectives + // follow Kleene's tables, in the interpreter a native build compiles to. + // https://github.com/asc-community/AngouriMath/issues/2195 + var undecided = "x > 1 or y".Compile(x, y); + Check("compiled (i > 1) or true, and or false", undecided(Complex.ImaginaryOne, true) == true && undecided(Complex.ImaginaryOne, false) is null, + $"{undecided(Complex.ImaginaryOne, true)}, {undecided(Complex.ImaginaryOne, false)?.ToString() ?? "null"}"); + var underIt = "x provided x > 1".Compile(x); + Check("compiled x provided x > 1 at 2 and at i", underIt(2) == 2 && double.IsNaN(underIt(Complex.ImaginaryOne).Real), $"{underIt(2)}, {underIt(Complex.ImaginaryOne)}"); + // A matrix, which compiles to a tensor: the arithmetic on it must stay ahead-of-time // compilable, which GenericTensor's own expression-compiled loops are not. // https://github.com/asc-community/AngouriMath/issues/526 diff --git a/Sources/Tests/UnitTests/Common/ThreeValuedCompilationTest.cs b/Sources/Tests/UnitTests/Common/ThreeValuedCompilationTest.cs new file mode 100644 index 000000000..5893d6e7b --- /dev/null +++ b/Sources/Tests/UnitTests/Common/ThreeValuedCompilationTest.cs @@ -0,0 +1,86 @@ +// +// Copyright (c) 2019-2026 Angouri. +// AngouriMath is licensed under MIT. +// Details: https://github.com/asc-community/AngouriMath/blob/master/LICENSE.md. +// Website: https://am.angouri.org. +// + +using System.Numerics; +using AngouriMath; +using AngouriMath.Extensions; +using Xunit; + +namespace AngouriMath.Tests.Common +{ + /// + /// A compiled statement has the truth value evaluation gives it, and none where it + /// has none: i > 1 compares numbers that are not ordered, and NaN > 1 compares + /// with no number. Over a comparison was refused, and over + /// it was false at NaN, so that its negation was true. + /// #2195 + /// + [Trait("Area", "Common")] + public sealed class ThreeValuedCompilationTest + { + public static readonly TheoryData Statements = new() + { + "x > 1", "x < 1", "x >= 1", "x <= 1", "x = 1", + "not (x > 1)", + "x > 1 or true", "x > 1 and false", "x > 1 or x < 2", "x > 0 and x < 2", + "x > 1 xor true", "x > 1 implies false", "false implies x > 1", + "(x > 1) = (x < 2)", + }; + + private static bool? Interpreted(string statement, Entity at) + => statement.ToEntity().Substitute("x", at).Evaled switch + { + Entity.Boolean value => (bool)value, + Entity.Number.Complex { IsNaN: true } => null, + var other => throw new Xunit.Sdk.XunitException($"{statement} at {at} is {other}") + }; + + [Theory] + [MemberData(nameof(Statements))] + public void OverTheComplexNumbersItIsWhatEvalNumericalSays(string statement) + { + var compiled = statement.ToEntity().Compile("x"); + foreach (var (at, value) in new (Entity, Complex)[] { (2, 2), (1, 1), ("1/2", 0.5), ("i", Complex.ImaginaryOne), ("1 + i", new(1, 1)), (MathS.NaN, double.NaN) }) + Assert.True(Interpreted(statement, at) == compiled(value), $"{statement} at {at}: {Interpreted(statement, at)?.ToString() ?? "none"} interpreted, {compiled(value)?.ToString() ?? "none"} compiled"); + } + + [Theory] + [MemberData(nameof(Statements))] + public void OverTheRealsItIsWhatEvalNumericalSays(string statement) + { + var compiled = statement.ToEntity().Compile("x"); + foreach (var (at, value) in new (Entity, double)[] { (2, 2), (1, 1), ("1/2", 0.5), (MathS.NaN, double.NaN) }) + Assert.True(Interpreted(statement, at) == compiled(value), $"{statement} at {at}: {Interpreted(statement, at)?.ToString() ?? "none"} interpreted, {compiled(value)?.ToString() ?? "none"} compiled"); + } + + /// As a , a statement with no truth value is not one that holds. + [Fact] + public void AsABooleanAStatementWithNoTruthValueDoesNotHold() + { + Assert.False("x > 1".ToEntity().Compile("x")(Complex.ImaginaryOne)); + Assert.False("not (x > 1)".ToEntity().Compile("x")(Complex.ImaginaryOne)); + Assert.True("x > 1 or true".ToEntity().Compile("x")(Complex.ImaginaryOne)); + Assert.False("not (x > 1)".ToEntity().Compile("x")(double.NaN)); + Assert.True("not (x > 1)".ToEntity().Compile("x")(0)); + } + + /// A value provided a statement with no truth value has none, nor has its Iverson bracket. + [Fact] + public void AValueUnderAStatementWithNoTruthValueHasNone() + { + var provided = "x provided x > 1".ToEntity().Compile("x"); + Assert.Equal(new Complex(2, 0), provided(2)); + Assert.True(double.IsNaN(provided(Complex.ImaginaryOne).Real)); + var piecewise = "piecewise(1 provided x > 0, -1 provided x < 0)".ToEntity().Compile("x"); + Assert.Equal(new Complex(-1, 0), piecewise(-3)); + Assert.True(double.IsNaN(piecewise(Complex.ImaginaryOne).Real)); + var iverson = "iverson(x > 1)".ToEntity().Compile("x"); + Assert.Equal(new Complex(1, 0), iverson(2)); + Assert.True(double.IsNaN(iverson(Complex.ImaginaryOne).Real)); + } + } +} From 60883f0bee036554d7915d85655ed37d64d0d48e Mon Sep 17 00:00:00 2001 From: Rafael Vuijk Date: Thu, 29 Oct 2026 19:24:12 +0000 Subject: [PATCH 2/2] A piecewise passes over a case with no truth value As evaluation does: at i, piecewise(1 provided x > 0, 3) is 3. The compiled forms already did; the entry said otherwise, and a test now holds both to it. Part of #2195 Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_012sonx8iAspMiwRwokT1Ura --- BREAKING-CHANGES.md | 5 +++-- .../Tests/UnitTests/Common/ThreeValuedCompilationTest.cs | 9 ++++++++- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/BREAKING-CHANGES.md b/BREAKING-CHANGES.md index f1479e026..b8e73e5c5 100644 --- a/BREAKING-CHANGES.md +++ b/BREAKING-CHANGES.md @@ -329,8 +329,9 @@ negative even integer or off the integers, since the extension through the gamma with `UncompilableNodeException`, and now compiles: it holds or fails where both sides are real, and has no truth value where one is not, as `i > 1` has none when evaluated. Over `double` and `float` a comparison with `NaN` was false, so that its negation was true. It has no truth value now either. The connectives follow Kleene's tables, as evaluation's -do: `(i > 1) or true` is true and `not (i > 1)` has none. A `provided` or `piecewise` whose predicate has no truth -value has no value, and neither does an Iverson bracket. Compiled to `bool?`, a statement with no truth value is `null`; +do: `(i > 1) or true` is true and `not (i > 1)` has none. A `provided` whose predicate has no truth value has no +value, and neither does an Iverson bracket, and a `piecewise` passes over such a case, as evaluation does: at `i`, +`piecewise(1 provided x > 0, 3)` is `3`. Compiled to `bool?`, a statement with no truth value is `null`; compiled to `bool`, it is `false`, as one that does not hold ([#2195](https://github.com/asc-community/AngouriMath/issues/2195)). diff --git a/Sources/Tests/UnitTests/Common/ThreeValuedCompilationTest.cs b/Sources/Tests/UnitTests/Common/ThreeValuedCompilationTest.cs index 5893d6e7b..422d32453 100644 --- a/Sources/Tests/UnitTests/Common/ThreeValuedCompilationTest.cs +++ b/Sources/Tests/UnitTests/Common/ThreeValuedCompilationTest.cs @@ -68,7 +68,10 @@ public void AsABooleanAStatementWithNoTruthValueDoesNotHold() Assert.True("not (x > 1)".ToEntity().Compile("x")(0)); } - /// A value provided a statement with no truth value has none, nor has its Iverson bracket. + /// + /// A value provided a statement with no truth value has none, nor has its Iverson bracket, and a + /// piecewise passes over such a case. + /// [Fact] public void AValueUnderAStatementWithNoTruthValueHasNone() { @@ -78,6 +81,10 @@ public void AValueUnderAStatementWithNoTruthValueHasNone() var piecewise = "piecewise(1 provided x > 0, -1 provided x < 0)".ToEntity().Compile("x"); Assert.Equal(new Complex(-1, 0), piecewise(-3)); Assert.True(double.IsNaN(piecewise(Complex.ImaginaryOne).Real)); + // A case whose predicate has no truth value is passed over, as evaluation passes it over. + var fallingThrough = "piecewise(1 provided x > 0, 3)".ToEntity(); + Assert.Equal(Entity.Number.Integer.Create(3), fallingThrough.Substitute("x", "i").Evaled); + Assert.Equal(new Complex(3, 0), fallingThrough.Compile("x")(Complex.ImaginaryOne)); var iverson = "iverson(x > 1)".ToEntity().Compile("x"); Assert.Equal(new Complex(1, 0), iverson(2)); Assert.True(double.IsNaN(iverson(Complex.ImaginaryOne).Real));