Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions BREAKING-CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -323,6 +323,26 @@ 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` 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)).

| Input | Was (2.6.0) | Now |
|---|---|---|
| `"not (x > 1)".ToEntity().Compile<double, bool>("x")(double.NaN)` | `True` | `False` |
| `"x > 1".ToEntity().Compile<Complex, bool>("x")(Complex.ImaginaryOne)` | `UncompilableNodeException` | `False` |
| `"x > 1 or true".ToEntity().Compile<Complex, bool>("x")(Complex.ImaginaryOne)` | `UncompilableNodeException` | `True` |
| `"x provided x > 1".ToEntity().Compile<Complex, Complex>("x")(2)` | `UncompilableNodeException` | `(2, 0)` |
| `"x provided x > 1".ToEntity().Compile<Complex, Complex>("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
Expand Down
6 changes: 6 additions & 0 deletions Sources/.editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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),

Expand All @@ -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);
}
}

Expand Down Expand Up @@ -301,7 +316,7 @@ private Expression HandlePiecewise(IEnumerable<Expression> 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)
Expand All @@ -311,7 +326,7 @@ private Expression HandlePiecewise(IEnumerable<Expression> en)

(expression, piecewiseExpr) = EqualizeTypesIfAble(expression, piecewiseExpr);

piecewiseExpr = Expression.Condition(predicate, expression, piecewiseExpr);
piecewiseExpr = Expression.Condition(Holds(predicate), expression, piecewiseExpr);
}

return piecewiseExpr;
Expand Down Expand Up @@ -445,6 +460,28 @@ private static MethodInfo Cmp<T>(Expression<Func<T, T, bool>> e)
=> ((BinaryExpression)e.Body).Method
?? throw new AngouriBugException($"{typeof(T)} defines this operator as an instruction, not a method");

/// <summary>
/// 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: <c>i &gt; 1</c> is <see langword="null"/> over
/// <see cref="System.Numerics.Complex"/>, where it was refused, and <c>NaN &gt; 1</c> over
/// <see cref="double"/>, where it was false, so that its negation was true. Other types
/// compare as they did. https://github.com/asc-community/AngouriMath/issues/2195
/// </summary>
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;

/// <summary>A predicate as a <see cref="bool"/>: one with no truth value does not hold.</summary>
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.
Expand Down
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// The compiled statements, with a third truth value for one that has none: <c>i &gt; 1</c>
/// compares numbers that are not ordered, and <c>NaN &gt; 1</c> compares with no number at all.
/// Each is <see langword="null"/>, as <c>EvalNumerical</c> leaves either as <c>NaN</c>, and the
/// connectives follow Kleene's tables, as <c>EvalNumerical</c>'s do: <c>(i &gt; 1) or true</c> is
/// true, <c>(i &gt; 1) and false</c> is false, and <c>not (i &gt; 1)</c> has no value.
/// https://github.com/asc-community/AngouriMath/issues/2195
/// </summary>
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);

/// <summary>Whether the statement holds: <see langword="false"/> where it does not, and where it has no truth value.</summary>
internal static bool Holds(bool? a) => a == true;

/// <summary>The Iverson bracket of a statement, which has no value where the statement has none.</summary>
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);

/// <summary>
/// The comparison of two operands of one type, for the types whose comparisons can have no
/// truth value, or <see langword="null"/>. Named rather than looked up by name, for the trimmer.
/// </summary>
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<Func<bool?, bool>>)(a => Holds(a))).Body).Method;
[ConstantField] internal static readonly MethodInfo IversonMethod = ((MethodCallExpression)((Expression<Func<bool?, int?>>)(a => Iverson(a))).Body).Method;

[ConstantField] private static readonly Dictionary<(ExpressionType Kind, Type Type), MethodInfo> comparisons = new()
{
{ (ExpressionType.LessThan, typeof(Complex)), Compared<Complex>((a, b) => Less(a, b)) },
{ (ExpressionType.LessThanOrEqual, typeof(Complex)), Compared<Complex>((a, b) => LessOrEqual(a, b)) },
{ (ExpressionType.GreaterThan, typeof(Complex)), Compared<Complex>((a, b) => Greater(a, b)) },
{ (ExpressionType.GreaterThanOrEqual, typeof(Complex)), Compared<Complex>((a, b) => GreaterOrEqual(a, b)) },
{ (ExpressionType.Equal, typeof(Complex)), Compared<Complex>((a, b) => Equal(a, b)) },
{ (ExpressionType.LessThan, typeof(double)), Compared<double>((a, b) => Less(a, b)) },
{ (ExpressionType.LessThanOrEqual, typeof(double)), Compared<double>((a, b) => LessOrEqual(a, b)) },
{ (ExpressionType.GreaterThan, typeof(double)), Compared<double>((a, b) => Greater(a, b)) },
{ (ExpressionType.GreaterThanOrEqual, typeof(double)), Compared<double>((a, b) => GreaterOrEqual(a, b)) },
{ (ExpressionType.Equal, typeof(double)), Compared<double>((a, b) => Equal(a, b)) },
{ (ExpressionType.LessThan, typeof(float)), Compared<float>((a, b) => Less(a, b)) },
{ (ExpressionType.LessThanOrEqual, typeof(float)), Compared<float>((a, b) => LessOrEqual(a, b)) },
{ (ExpressionType.GreaterThan, typeof(float)), Compared<float>((a, b) => Greater(a, b)) },
{ (ExpressionType.GreaterThanOrEqual, typeof(float)), Compared<float>((a, b) => GreaterOrEqual(a, b)) },
{ (ExpressionType.Equal, typeof(float)), Compared<float>((a, b) => Equal(a, b)) },
};

private static MethodInfo Compared<T>(Expression<Func<T, T, bool?>> e) => ((MethodCallExpression)e.Body).Method;
private static MethodInfo Unary(Expression<Func<bool?, bool?>> e) => ((MethodCallExpression)e.Body).Method;
private static MethodInfo Binary(Expression<Func<bool?, bool?, bool?>> e) => ((MethodCallExpression)e.Body).Method;
}
}
9 changes: 9 additions & 0 deletions Sources/Tests/AotSmokeTest/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,15 @@ private static void Compilation()
var piecewise = "piecewise(x provided x > 0, 0 provided x <= 0)".Compile<double, double>(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<Complex, bool, bool?>(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<Complex, Complex>(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
Expand Down
Loading
Loading