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
18 changes: 18 additions & 0 deletions BREAKING-CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -431,6 +431,24 @@ one where both sides are real, and `NaN` where either is not
|---|---|---|
| `"x mod 3".ToEntity().Compile<Complex, Complex>("x")(7)` | `UncompilableNodeException` | `(1, 0)` |

### The LINQ compiler gives a constant, a nullable operand and `sgn(0)` the value evaluation gives

**Silent: three compiled values over `Complex` are evaluation's, and one compile that threw compiles.**
- A constant is taken over `Complex` where the expression is compiled over it. As a `long`, a constant widened only
to `double`, so `sqrt(-1)`, `ln(-1)` and `arcsin(2)` were `NaN` and `1/0` was `+oo`
([#2235](https://github.com/asc-community/AngouriMath/issues/2235)).
- An Iverson bracket, an `int?`, and a `provided` integer, a `long?`, are widened to `double` before a function takes
them. They threw `KeyNotFoundException` or `AngouriBugException`
([#2237](https://github.com/asc-community/AngouriMath/issues/2237)).
- `sgn(0)` is `0`, where `z / |z|` made it `NaN` ([#2238](https://github.com/asc-community/AngouriMath/issues/2238)).

| Input | Was (2.6.0) | Now |
|---|---|---|
| `"x + sqrt(-1)".ToEntity().Compile<Complex, Complex>("x")(0)` | `(NaN, 0)` | `(6.123233995736766E-17, 1)` |
| `"x + 1/0".ToEntity().Compile<Complex, Complex>("x")(0)` | `(Infinity, 0)` | `(NaN, NaN)` |
| `"x + sgn(-1 provided pi > 3)".ToEntity().Compile<Complex, Complex>("x")(1)` | throws `AngouriBugException` | `(0, 0)` |
| `"sgn(x)".ToEntity().Compile<Complex, Complex>("x")(0)` | `(NaN, NaN)` | `(0, 0)` |

### A compiled product of real numbers is real at an infinity

**Silent: a compiled product or quotient with a real infinity is that infinity, where it had a `NaN` imaginary part.**
Expand Down
3 changes: 3 additions & 0 deletions Sources/.editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -393,6 +393,9 @@ file_header_template=\nCopyright (c) 2019-2026 Angouri.\nAngouriMath is licensed
[Tests/UnitTests/Common/CongruenceCompilationTest.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/LinqCompilationValueTest.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 @@ -130,6 +130,7 @@ public virtual Expression ConvertType(Expression expr, Type type)
/// </summary>
public virtual Expression ConvertUnaryNode(Expression e, Entity typeHolder)
{
e = NumericOperand(e);
// A function of a matrix has no compiled form here: the tensor operations are the
// arithmetic ones, and sin(A) is not an element-by-element sine of a matrix.
if (TensorCompilation.IsTensor(e.Type))
Expand Down Expand Up @@ -207,6 +208,15 @@ Imf when ShouldBeAtLeastDouble(e) is var newE => Expression.Call(GetDef("
};
}

// A number that may have no value, an Iverson bracket's int? or a provided integer's long?, as a double, which
// has NaN for none: no function takes the nullable integer, and widening one threw KeyNotFoundException or
// AngouriBugException. A statement's bool? stays itself. https://github.com/asc-community/AngouriMath/issues/2237
private Expression NumericOperand(Expression expr)
=> Nullable.GetUnderlyingType(expr.Type) is { } underlying && underlying != typeof(bool)
&& underlying != typeof(System.Numerics.Complex) && underlying != typeof(double)
? ConvertType(expr, typeof(double))
: expr;

private Expression ShouldBeAtLeastDouble(Expression expr)
{
if (typeLevelInHierarchy[expr.Type] < typeLevelInHierarchy[typeof(double)])
Expand All @@ -226,7 +236,7 @@ public virtual Expression ConvertBinaryNode(Expression left, Expression right, E
?? throw new UncompilableNodeException(
$"The node of type {typeHolder.GetType()} has no compiled form over a matrix.");

(left, right) = EqualizeTypesIfAble(left, right);
(left, right) = EqualizeTypesIfAble(NumericOperand(left), NumericOperand(right));
return typeHolder switch
{
Sumf => Operator(ExpressionType.Add, left, right),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,12 @@ internal static TDelegate Compile<TDelegate>(
// https://github.com/asc-community/AngouriMath/issues/894
try
{
var tree = BuildTree(expr, subexpressionsCache, variableAssignments, localVars, protocol);
// Over Complex a constant is a Complex too, so that a function of it is taken over Complex: as a long,
// sqrt(-1) widened only to double and was NaN. https://github.com/asc-community/AngouriMath/issues/2235
var constantType = typesAndNames.Any(c => c.type == typeof(System.Numerics.Complex)) || returnType == typeof(System.Numerics.Complex)
? typeof(System.Numerics.Complex)
: null;
var tree = BuildTree(expr, subexpressionsCache, variableAssignments, localVars, protocol, constantType);
var treeWithLocals = Expression.Block(localVars, variableAssignments.Append(tree));
Expression entireExpression = returnType is not null ? protocol.ConvertType(treeWithLocals, returnType) : treeWithLocals;
var finalLambda = Expression.Lambda<TDelegate>(entireExpression, functionArguments);
Expand All @@ -68,15 +73,16 @@ internal static Expression BuildTree(
Dictionary<Entity, ParameterExpression> cachedSubexpressions,
List<Expression> variableAssignments,
List<ParameterExpression> newLocalVars,
CompilationProtocol protocol)
CompilationProtocol protocol,
Type? constantType = null)
{
if (cachedSubexpressions.TryGetValue(expr, out var readyVar))
return readyVar;

Expression subTree = expr switch
{
Variable { IsConstant: true } c
=> BuildTree(c.Evaled, cachedSubexpressions, variableAssignments, newLocalVars, protocol),
=> BuildTree(c.Evaled, cachedSubexpressions, variableAssignments, newLocalVars, protocol, constantType),

// A compiled expression is a function of the variables it was compiled over,
// so one it does not mention is not something it can be given a value for.
Expand All @@ -90,26 +96,27 @@ internal static Expression BuildTree(
? $", which are {string.Join(", ", over)}"
: ", which are none")),

Number when constantType is { } type => protocol.ConvertType(protocol.ConvertConstant(expr), type),
Entity.Boolean or Number => protocol.ConvertConstant(expr),

IUnaryNode oneArg
=> protocol.ConvertUnaryNode(
BuildTree(oneArg.NodeChild, cachedSubexpressions, variableAssignments, newLocalVars, protocol),
BuildTree(oneArg.NodeChild, cachedSubexpressions, variableAssignments, newLocalVars, protocol, constantType),
expr),

// A membership in a set of numbers, the set not being a value: only the element is computed.
// https://github.com/asc-community/AngouriMath/issues/2195
Set.Inf(var element, Set.SpecialSet special) when AngouriMath.Numerics.NumberStatements.Of(special) is { } set
=> protocol.ConvertMembership(
BuildTree(element, cachedSubexpressions, variableAssignments, newLocalVars, protocol), set),
BuildTree(element, cachedSubexpressions, variableAssignments, newLocalVars, protocol, constantType), set),

IBinaryNode twoArg
=> protocol.ConvertBinaryNode(
BuildTree(twoArg.NodeFirstChild, cachedSubexpressions, variableAssignments, newLocalVars, protocol),
BuildTree(twoArg.NodeSecondChild, cachedSubexpressions, variableAssignments, newLocalVars, protocol),
BuildTree(twoArg.NodeFirstChild, cachedSubexpressions, variableAssignments, newLocalVars, protocol, constantType),
BuildTree(twoArg.NodeSecondChild, cachedSubexpressions, variableAssignments, newLocalVars, protocol, constantType),
expr),

var other => protocol.ConvertOtherNode(other.DirectChildren.Select(c => BuildTree(c, cachedSubexpressions, variableAssignments, newLocalVars, protocol)), expr)
var other => protocol.ConvertOtherNode(other.DirectChildren.Select(c => BuildTree(c, cachedSubexpressions, variableAssignments, newLocalVars, protocol, constantType)), expr)
};

var newVar = Expression.Variable(subTree.Type);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -234,7 +234,8 @@ public static BigInteger Acsc(BigInteger a)
public static int Abs(int a) => Math.Abs(a);
public static BigInteger Abs(BigInteger a) => BigInteger.Abs(a);

public static System.Numerics.Complex Sgn(System.Numerics.Complex a) => a / System.Numerics.Complex.Abs(a);
// 0 at 0, where a / |a| is 0/0: https://github.com/asc-community/AngouriMath/issues/2238
public static System.Numerics.Complex Sgn(System.Numerics.Complex a) => AngouriMath.InternalAMExtensions.Signum(a);
public static double Sgn(double a) => a switch { > 0 or double.PositiveInfinity => 1, 0 => 0, < 0 or double.NegativeInfinity => -1, _ => double.NaN };
public static float Sgn(float a) => a switch { > 0 or float.PositiveInfinity => 1, 0 => 0, < 0 or float.NegativeInfinity => -1, _ => float.NaN };
public static long Sgn(long a) => a switch { > 0 => 1, 0 => 0, < 0 => -1 };
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,8 @@ namespace AngouriMath.Core.Compilation.IntoLinq
public static int Abs(int a) => Math.Abs(a);
public static BigInteger Abs(BigInteger a) => BigInteger.Abs(a);

public static System.Numerics.Complex Sgn(System.Numerics.Complex a) => a / System.Numerics.Complex.Abs(a);
// 0 at 0, where a / |a| is 0/0: https://github.com/asc-community/AngouriMath/issues/2238
public static System.Numerics.Complex Sgn(System.Numerics.Complex a) => AngouriMath.InternalAMExtensions.Signum(a);
public static double Sgn(double a) => a switch { > 0 or double.PositiveInfinity => 1, 0 => 0, < 0 or double.NegativeInfinity => -1, _ => double.NaN };
public static float Sgn(float a) => a switch { > 0 or float.PositiveInfinity => 1, 0 => 0, < 0 or float.NegativeInfinity => -1, _ => float.NaN };
public static long Sgn(long a) => a switch { > 0 => 1, 0 => 0, < 0 => -1 };
Expand Down
81 changes: 81 additions & 0 deletions Sources/Tests/UnitTests/Common/LinqCompilationValueTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
//
// 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
{
/// <summary>
/// The LINQ compiler gives what evaluation and FastExpression give, where it did not: a constant whose value is
/// not real, over <c>Complex</c> (<a href="https://github.com/asc-community/AngouriMath/issues/2235">#2235</a>); an
/// Iverson bracket or a provided integer inside a function
/// (<a href="https://github.com/asc-community/AngouriMath/issues/2237">#2237</a>); and <c>sgn(0)</c>
/// (<a href="https://github.com/asc-community/AngouriMath/issues/2238">#2238</a>).
/// </summary>
[Trait("Area", "Common")]
public sealed class LinqCompilationValueTest
{
private static Complex Evaluated(string expression, Complex x)
=> ((Entity.Number.Complex)expression.ToEntity().Substitute("x", (Entity.Number.Complex)x).Evaled).ToNumerics();

private static bool Agree(Complex expected, Complex actual)
=> double.IsNaN(expected.Real) || double.IsNaN(expected.Imaginary)
? double.IsNaN(actual.Real) || double.IsNaN(actual.Imaginary)
: Complex.Abs(expected - actual) <= 1e-12 * System.Math.Max(1, Complex.Abs(expected));

[Theory]
// A constant whose value is not real: it was taken over double, and NaN.
[InlineData("x + sqrt(-1)", 0)]
[InlineData("x + ln(-1)", 0)]
[InlineData("x + arcsin(2)", 0)]
[InlineData("x + arccos(-2)", 1)]
[InlineData("x * sqrt(-4)", 1)]
[InlineData("x + (-8)^(1/4)", 0)]
// 1/0 has no value, where over double it was +oo.
[InlineData("x + 1/0", 0)]
// An Iverson bracket and a provided integer inside a function.
[InlineData("x + tan(iverson(pi > 0))", 1)]
[InlineData("x + abs(iverson(e > 3))", 1)]
[InlineData("x + floor(iverson(x > 0))", 1)]
[InlineData("x + sgn(-1 provided pi > 3)", 1)]
[InlineData("max(iverson(x > 0), 1/2) + x", -1)]
// sgn(0) is 0.
[InlineData("sgn(x)", 0)]
[InlineData("sgn(x - x)", 1)]
[InlineData("sgn(x)", -3)]
public void OverComplexItIsWhatEvaluationAndFastExpressionGive(string expression, double at)
{
var expected = Evaluated(expression, at);
var linq = expression.ToEntity().Compile<Complex, Complex>("x")(at);
var fast = expression.ToEntity().Compile("x").Call(at);
Assert.True(Agree(expected, linq), $"{expression} at {at}: {expected} evaluated, {linq} by LINQ");
Assert.True(Agree(expected, fast), $"{expression} at {at}: {expected} evaluated, {fast} by FastExpression");
}

/// <summary>A constant is taken over Complex where the result is one, whatever the argument's type.</summary>
[Fact]
public void ToAComplexResultAConstantIsComplex()
=> Assert.True(Agree(new Complex(2, 1), "x + sqrt(-1)".ToEntity().Compile<double, Complex>("x")(2)));

/// <summary>Over double a value that is not real has none, as before.</summary>
[Theory]
[InlineData("x + sqrt(-1)", 0)]
[InlineData("x + ln(-1)", 0)]
public void OverDoubleAValueThatIsNotRealIsNaN(string expression, double at)
=> Assert.True(double.IsNaN(expression.ToEntity().Compile<double, double>("x")(at)));

[Theory]
[InlineData("x + tan(iverson(pi > 0))", 1)]
[InlineData("x + floor(iverson(x > 0))", 1)]
[InlineData("x + sgn(-1 provided pi > 3)", 1)]
public void OverDoubleANullableOperandIsWhatEvaluationGives(string expression, double at)
=> Assert.True(Agree(Evaluated(expression, at), expression.ToEntity().Compile<double, double>("x")(at)));
}
}
Loading