From fd28e275dc7d38164765dbe7a24b016ef9de32da Mon Sep 17 00:00:00 2001 From: Rafael Vuijk Date: Fri, 30 Oct 2026 22:09:12 +0000 Subject: [PATCH 1/2] A compiled division by zero over double has no value Compiled over double, 1/x at 0 was +Infinity, where evaluation, FastExpression and the compiler over Complex all say NaN. The compiler emitted the IEEE quotient, and Math.Pow made 0^(-1) an infinity and 0^0 1, where evaluation makes both NaN too. A division over double or float now calls MathAllMethods.Divide, which answers a zero divisor with NaN, and Pow answers 0 to a power that is not positive with NaN. The elementwise division of a compiled matrix does as the scalar one does. DivisionByZeroOverDoubleTest takes seven expressions that have no value, over double and float, and four that have one; the seven fail on master. The suite, the trio, F#, AOT and the eight harnesses pass. work/compilesweep found 19 of these in 3000 random compositions. Fixes #2286. Co-Authored-By: Claude Opus 5.5 (1M context) --- BREAKING-CHANGES.md | 12 +++++ Sources/.editorconfig | 3 ++ .../IntoLinq/CompilationProtocol.cs | 5 ++ .../Compilation/IntoLinq/MathAllMethods.cs | 15 +++++- .../Compilation/IntoLinq/MathAllMethods.tt | 16 ++++++- .../Compilation/IntoLinq/TensorCompilation.cs | 4 +- .../Common/DivisionByZeroOverDoubleTest.cs | 47 +++++++++++++++++++ 7 files changed, 96 insertions(+), 6 deletions(-) create mode 100644 Sources/Tests/UnitTests/Common/DivisionByZeroOverDoubleTest.cs diff --git a/BREAKING-CHANGES.md b/BREAKING-CHANGES.md index 10896a986..911ee0051 100644 --- a/BREAKING-CHANGES.md +++ b/BREAKING-CHANGES.md @@ -275,6 +275,18 @@ Pythagorean identity with a coefficient is now a rule of its own, found wherever | `"2 + (-2)*cos(x)^2 + (-2)*sin(x)^2".Simplify()` | `2 + (-2) * cos(x) ^ 2 + (-2) * sin(x) ^ 2` | `0` | | `"2 + (-2) * cos(2 * x) + (-4) * sin(x) ^ 2".Simplify()` | unchanged | `0` | +### A compiled division by zero over `double` has no value + +**Silent: compiled over `double` or `float`, a division by zero and 0 to a power that is not positive are `NaN`, as in +evaluation, FastExpression and the compiler over `Complex`.** The IEEE quotient made `1/0` an infinity, and `Math.Pow` +made `0^(-1)` an infinity and `0^0` 1 ([#2286](https://github.com/asc-community/AngouriMath/issues/2286)). + +| Input | Was (2.6.0) | Now | +|---|---|---| +| `"1/x".Compile("x")(0)` | `+Infinity` | `NaN` | +| `"x^(-1)".Compile("x")(0)` | `+Infinity` | `NaN` | +| `"x^x".Compile("x")(0)` | `1` | `NaN` | + ### A lambda applied to a vector multiplies by it as a vector **Silent: a matrix of vectors becomes the product.** Applying a lambda simplified its body first, and a simplified diff --git a/Sources/.editorconfig b/Sources/.editorconfig index 901a04714..dfeaa0e3f 100644 --- a/Sources/.editorconfig +++ b/Sources/.editorconfig @@ -968,5 +968,8 @@ file_header_template=\nCopyright (c) 2019-2026 Angouri.\nAngouriMath is licensed [Tests/UnitTests/Common/PowerOfNaNOverDoubleTest.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/DivisionByZeroOverDoubleTest.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/BranchCutCompilationTest.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 39e17b585..7643d85c8 100644 --- a/Sources/AngouriMath/Functions/Compilation/IntoLinq/CompilationProtocol.cs +++ b/Sources/AngouriMath/Functions/Compilation/IntoLinq/CompilationProtocol.cs @@ -242,6 +242,11 @@ public virtual Expression ConvertBinaryNode(Expression left, Expression right, E Sumf => Operator(ExpressionType.Add, left, right), Minusf => Operator(ExpressionType.Subtract, left, right), Mulf => Operator(ExpressionType.Multiply, left, right), + // Over the reals by MathAllMethods.Divide, which gives a division by zero no value, as + // evaluation does. https://github.com/asc-community/AngouriMath/issues/2286 + Divf when ShouldBeAtLeastDouble(left) is var newLeft && ShouldBeAtLeastDouble(right) is var newRight + && (newLeft.Type == typeof(double) || newLeft.Type == typeof(float)) && newLeft.Type == newRight.Type + => Expression.Call(GetDef("Divide", 2, newLeft.Type), newLeft, newRight), Divf => Operator(ExpressionType.Divide, ShouldBeAtLeastDouble(left), ShouldBeAtLeastDouble(right)), // ((a % b) + b) % b, and not the bare a % b: the runtime truncates, where the // node is the floored remainder that takes the sign of the divisor, and this is diff --git a/Sources/AngouriMath/Functions/Compilation/IntoLinq/MathAllMethods.cs b/Sources/AngouriMath/Functions/Compilation/IntoLinq/MathAllMethods.cs index edfb5bd9c..dfee97c14 100644 --- a/Sources/AngouriMath/Functions/Compilation/IntoLinq/MathAllMethods.cs +++ b/Sources/AngouriMath/Functions/Compilation/IntoLinq/MathAllMethods.cs @@ -41,10 +41,19 @@ public static System.Numerics.Complex Pow(System.Numerics.Complex a, System.Nume => AngouriMath.Core.Compilation.RealProducts.Power(a, b); // A power of a number that does not exist, or to such a power, has no value, as in evaluation and the other // compilers: Math.Pow takes NaN^0 and 1^NaN as 1. https://github.com/asc-community/AngouriMath/issues/2250 + // Nor does 0 to a power that is not positive, which evaluation and the other compilers answer NaN: Math.Pow + // takes 0^0 as 1 and 0^(-1) as +oo. https://github.com/asc-community/AngouriMath/issues/2286 public static double Pow(double a, double b) - => double.IsNaN(a) || double.IsNaN(b) ? double.NaN : Math.Pow(a, b); + => double.IsNaN(a) || double.IsNaN(b) || a == 0 && b <= 0 ? double.NaN : Math.Pow(a, b); public static float Pow(float a, float b) - => float.IsNaN(a) || float.IsNaN(b) ? float.NaN : (float)Math.Pow(a, b); + => float.IsNaN(a) || float.IsNaN(b) || a == 0 && b <= 0 ? float.NaN : (float)Math.Pow(a, b); + + // A division by zero has no value, as in evaluation and the other compilers, where the IEEE quotient is an + // infinity. https://github.com/asc-community/AngouriMath/issues/2286 + public static double Divide(double a, double b) + => b == 0 ? double.NaN : a / b; + public static float Divide(float a, float b) + => b == 0 ? float.NaN : a / b; public static long Pow(long a, long b) => (long)Math.Pow(a, b); public static int Pow(int a, int b) @@ -425,6 +434,8 @@ public static bool IsNaN(System.Numerics.Complex a) { ("Min", 2, typeof(long)), Def((a, b) => Min(a, b)) }, { ("Min", 2, typeof(int)), Def((a, b) => Min(a, b)) }, { ("Min", 2, typeof(BigInteger)), Def((a, b) => Min(a, b)) }, + { ("Divide", 2, typeof(double)), Def((a, b) => Divide(a, b)) }, + { ("Divide", 2, typeof(float)), Def((a, b) => Divide(a, b)) }, { ("Sin", 1, typeof(System.Numerics.Complex)), Def(a => Sin(a)) }, { ("Sin", 1, typeof(double)), Def(a => Sin(a)) }, { ("Sin", 1, typeof(float)), Def(a => Sin(a)) }, diff --git a/Sources/AngouriMath/Functions/Compilation/IntoLinq/MathAllMethods.tt b/Sources/AngouriMath/Functions/Compilation/IntoLinq/MathAllMethods.tt index df4545585..1cb7e022d 100644 --- a/Sources/AngouriMath/Functions/Compilation/IntoLinq/MathAllMethods.tt +++ b/Sources/AngouriMath/Functions/Compilation/IntoLinq/MathAllMethods.tt @@ -66,10 +66,19 @@ namespace AngouriMath.Core.Compilation.IntoLinq => AngouriMath.Core.Compilation.RealProducts.Power(a, b); // A power of a number that does not exist, or to such a power, has no value, as in evaluation and the other // compilers: Math.Pow takes NaN^0 and 1^NaN as 1. https://github.com/asc-community/AngouriMath/issues/2250 + // Nor does 0 to a power that is not positive, which evaluation and the other compilers answer NaN: Math.Pow + // takes 0^0 as 1 and 0^(-1) as +oo. https://github.com/asc-community/AngouriMath/issues/2286 public static double Pow(double a, double b) - => double.IsNaN(a) || double.IsNaN(b) ? double.NaN : Math.Pow(a, b); + => double.IsNaN(a) || double.IsNaN(b) || a == 0 && b <= 0 ? double.NaN : Math.Pow(a, b); public static float Pow(float a, float b) - => float.IsNaN(a) || float.IsNaN(b) ? float.NaN : (float)Math.Pow(a, b); + => float.IsNaN(a) || float.IsNaN(b) || a == 0 && b <= 0 ? float.NaN : (float)Math.Pow(a, b); + + // A division by zero has no value, as in evaluation and the other compilers, where the IEEE quotient is an + // infinity. https://github.com/asc-community/AngouriMath/issues/2286 + public static double Divide(double a, double b) + => b == 0 ? double.NaN : a / b; + public static float Divide(float a, float b) + => b == 0 ? float.NaN : a / b; public static long Pow(long a, long b) => (long)Math.Pow(a, b); public static int Pow(int a, int b) @@ -297,6 +306,9 @@ namespace AngouriMath.Core.Compilation.IntoLinq { ("<#= name #>", 2, typeof(<#= type #>)), Def<<#= type #>>((a, b) => <#= name #>(a, b)) }, <# } #> <# } #> +<# foreach (var type in new[]{ "double", "float" }) { #> + { ("Divide", 2, typeof(<#= type #>)), Def<<#= type #>>((a, b) => Divide(a, b)) }, +<# } #> <# foreach (var name in unary) { #> <# foreach (var type in types) { #> { ("<#= name #>", 1, typeof(<#= type #>)), Def<<#= type #>>(a => <#= name #>(a)) }, diff --git a/Sources/AngouriMath/Functions/Compilation/IntoLinq/TensorCompilation.cs b/Sources/AngouriMath/Functions/Compilation/IntoLinq/TensorCompilation.cs index 287671e14..230f08c90 100644 --- a/Sources/AngouriMath/Functions/Compilation/IntoLinq/TensorCompilation.cs +++ b/Sources/AngouriMath/Functions/Compilation/IntoLinq/TensorCompilation.cs @@ -36,7 +36,7 @@ namespace AngouriMath.Core.Compilation.IntoLinq /// public double Negate(double a) => -a; /// - public double Divide(double a, double b) => a / b; + public double Divide(double a, double b) => b == 0 ? double.NaN : a / b; /// public double CreateOne() => 1; /// @@ -67,7 +67,7 @@ namespace AngouriMath.Core.Compilation.IntoLinq /// public float Negate(float a) => -a; /// - public float Divide(float a, float b) => a / b; + public float Divide(float a, float b) => b == 0 ? float.NaN : a / b; /// public float CreateOne() => 1; /// diff --git a/Sources/Tests/UnitTests/Common/DivisionByZeroOverDoubleTest.cs b/Sources/Tests/UnitTests/Common/DivisionByZeroOverDoubleTest.cs new file mode 100644 index 000000000..aa1e36108 --- /dev/null +++ b/Sources/Tests/UnitTests/Common/DivisionByZeroOverDoubleTest.cs @@ -0,0 +1,47 @@ +// +// 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 AngouriMath; +using AngouriMath.Extensions; +using Xunit; + +namespace AngouriMath.Tests.Common +{ + /// + /// Compiled over , a division by zero, and 0 to a power that is not positive, have no value, as in + /// evaluation, FastExpression and the compiler over : the IEEE quotient made 1/0 + /// +oo, and made 0^(-1) +oo and 0^0 1. + /// #2286 + /// + [Trait("Area", "Common")] + public sealed class DivisionByZeroOverDoubleTest + { + [Theory] + [InlineData("1/x", 0)] + [InlineData("-1/x", 0)] + [InlineData("1/(x - x)", 3)] + [InlineData("x / floor(x)", 0.5)] + [InlineData("x^(-1)", 0)] + [InlineData("x^(-1/2)", 0)] + [InlineData("x^x", 0)] + public void ItHasNoValue(string expression, double at) + { + var overDoubles = expression.ToEntity().Compile("x")(at); + Assert.True(double.IsNaN(overDoubles), $"{expression} at {at}: {overDoubles} over double"); + Assert.True(float.IsNaN(expression.ToEntity().Compile("x")((float)at)), $"{expression} at {at} over float"); + Assert.True(expression.ToEntity().Substitute("x", at).Evaled.IsNaN, $"{expression} at {at} evaluates to a value"); + } + + [Theory] + [InlineData("1/x", 4, 0.25)] + [InlineData("x^(1/2)", 0, 0)] + [InlineData("x^2", 0, 0)] + [InlineData("0 / x", 2, 0)] + public void WhereItHasOneItIsThatValue(string expression, double at, double value) + => Assert.Equal(value, expression.ToEntity().Compile("x")(at)); + } +} From 13545a30b6ece1398c79e7b6432e44625a598d58 Mon Sep 17 00:00:00 2001 From: Rafael Vuijk Date: Fri, 30 Oct 2026 22:13:46 +0000 Subject: [PATCH 2/2] Test a compiled matrix argument over zero A matrix written into the expression is divided entry by entry before it is compiled, and only a matrix argument reaches the compiled matrix division. AMatrixOverZeroHasNoEntries divides one by 0, and fails with master's TensorCompilation, whose entries were +Infinity. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../Common/DivisionByZeroOverDoubleTest.cs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/Sources/Tests/UnitTests/Common/DivisionByZeroOverDoubleTest.cs b/Sources/Tests/UnitTests/Common/DivisionByZeroOverDoubleTest.cs index aa1e36108..6a1a8a21f 100644 --- a/Sources/Tests/UnitTests/Common/DivisionByZeroOverDoubleTest.cs +++ b/Sources/Tests/UnitTests/Common/DivisionByZeroOverDoubleTest.cs @@ -6,7 +6,9 @@ // using AngouriMath; +using AngouriMath.Core.Compilation.IntoLinq; using AngouriMath.Extensions; +using GenericTensor.Core; using Xunit; namespace AngouriMath.Tests.Common @@ -36,6 +38,21 @@ public void ItHasNoValue(string expression, double at) Assert.True(expression.ToEntity().Substitute("x", at).Evaled.IsNaN, $"{expression} at {at} evaluates to a value"); } + /// + /// A matrix argument divided by a zero scalar, entry by entry as a scalar is. A matrix written into the expression + /// is divided before it is compiled, and only an argument reaches the compiled matrix division. + /// + [Fact] + public void AMatrixOverZeroHasNoEntries() + { + var matrix = GenTensor.CreateMatrix(new double[,] { { 1, 2 }, { 3, 4 } }); + var divided = "A / x".ToEntity() + .Compile, double, GenTensor>("A", "x")(matrix, 0); + for (var i = 0; i < 2; i++) + for (var j = 0; j < 2; j++) + Assert.True(double.IsNaN(divided[i, j]), $"[{i}, {j}] is {divided[i, j]}"); + } + [Theory] [InlineData("1/x", 4, 0.25)] [InlineData("x^(1/2)", 0, 0)]