diff --git a/BREAKING-CHANGES.md b/BREAKING-CHANGES.md index 148ef6384..7ff9add37 100644 --- a/BREAKING-CHANGES.md +++ b/BREAKING-CHANGES.md @@ -431,6 +431,24 @@ one where both sides are real, and `NaN` where either is not |---|---|---| | `"x mod 3".ToEntity().Compile("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("x")(0)` | `(NaN, 0)` | `(6.123233995736766E-17, 1)` | +| `"x + 1/0".ToEntity().Compile("x")(0)` | `(Infinity, 0)` | `(NaN, NaN)` | +| `"x + sgn(-1 provided pi > 3)".ToEntity().Compile("x")(1)` | throws `AngouriBugException` | `(0, 0)` | +| `"sgn(x)".ToEntity().Compile("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.** diff --git a/Sources/.editorconfig b/Sources/.editorconfig index eae3ab026..611744d82 100644 --- a/Sources/.editorconfig +++ b/Sources/.editorconfig @@ -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 diff --git a/Sources/AngouriMath/Functions/Compilation/IntoLinq/CompilationProtocol.cs b/Sources/AngouriMath/Functions/Compilation/IntoLinq/CompilationProtocol.cs index f63cfbff3..e84a5ae7f 100644 --- a/Sources/AngouriMath/Functions/Compilation/IntoLinq/CompilationProtocol.cs +++ b/Sources/AngouriMath/Functions/Compilation/IntoLinq/CompilationProtocol.cs @@ -130,6 +130,7 @@ public virtual Expression ConvertType(Expression expr, Type type) /// 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)) @@ -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)]) @@ -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), diff --git a/Sources/AngouriMath/Functions/Compilation/IntoLinq/IntoLinqCompiler.cs b/Sources/AngouriMath/Functions/Compilation/IntoLinq/IntoLinqCompiler.cs index 0046f0f05..9ac042250 100644 --- a/Sources/AngouriMath/Functions/Compilation/IntoLinq/IntoLinqCompiler.cs +++ b/Sources/AngouriMath/Functions/Compilation/IntoLinq/IntoLinqCompiler.cs @@ -47,7 +47,12 @@ internal static TDelegate Compile( // 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(entireExpression, functionArguments); @@ -68,7 +73,8 @@ internal static Expression BuildTree( Dictionary cachedSubexpressions, List variableAssignments, List newLocalVars, - CompilationProtocol protocol) + CompilationProtocol protocol, + Type? constantType = null) { if (cachedSubexpressions.TryGetValue(expr, out var readyVar)) return readyVar; @@ -76,7 +82,7 @@ internal static Expression BuildTree( 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. @@ -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); diff --git a/Sources/AngouriMath/Functions/Compilation/IntoLinq/MathAllMethods.cs b/Sources/AngouriMath/Functions/Compilation/IntoLinq/MathAllMethods.cs index 18f2c70e5..0365ca9f7 100644 --- a/Sources/AngouriMath/Functions/Compilation/IntoLinq/MathAllMethods.cs +++ b/Sources/AngouriMath/Functions/Compilation/IntoLinq/MathAllMethods.cs @@ -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 }; diff --git a/Sources/AngouriMath/Functions/Compilation/IntoLinq/MathAllMethods.tt b/Sources/AngouriMath/Functions/Compilation/IntoLinq/MathAllMethods.tt index de6c9d098..48f192596 100644 --- a/Sources/AngouriMath/Functions/Compilation/IntoLinq/MathAllMethods.tt +++ b/Sources/AngouriMath/Functions/Compilation/IntoLinq/MathAllMethods.tt @@ -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 }; diff --git a/Sources/Tests/UnitTests/Common/LinqCompilationValueTest.cs b/Sources/Tests/UnitTests/Common/LinqCompilationValueTest.cs new file mode 100644 index 000000000..a3d791c7d --- /dev/null +++ b/Sources/Tests/UnitTests/Common/LinqCompilationValueTest.cs @@ -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 +{ + /// + /// The LINQ compiler gives what evaluation and FastExpression give, where it did not: a constant whose value is + /// not real, over Complex (#2235); an + /// Iverson bracket or a provided integer inside a function + /// (#2237); and sgn(0) + /// (#2238). + /// + [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("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"); + } + + /// A constant is taken over Complex where the result is one, whatever the argument's type. + [Fact] + public void ToAComplexResultAConstantIsComplex() + => Assert.True(Agree(new Complex(2, 1), "x + sqrt(-1)".ToEntity().Compile("x")(2))); + + /// Over double a value that is not real has none, as before. + [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("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("x")(at))); + } +}