diff --git a/BREAKING-CHANGES.md b/BREAKING-CHANGES.md index 16975f719..ed8e67ccc 100644 --- a/BREAKING-CHANGES.md +++ b/BREAKING-CHANGES.md @@ -659,6 +659,18 @@ made `0^(-1)` an infinity and `0^0` 1 ([#2286](https://github.com/asc-community/ | `"x^(-1)".Compile("x")(0)` | `+Infinity` | `NaN` | | `"x^x".Compile("x")(0)` | `1` | `NaN` | +### A compiled remainder of two integers by zero has no value + +**Not silent: a compiled function that threw is `NaN`, as in evaluation.** The remainder of two integers is compiled +over the integers, and the runtime's `%` throws for a divisor of 0. So `2 mod 0`, or `abs(2) mod (2 mod (-1))`, compiled +by the LINQ compilers threw `DivideByZeroException` when the function was called. No issue was filed; a check of compiled +expressions against evaluation found it. + +| Input | Was (2.6.0) | Now | +|---|---|---| +| `"x + (2 mod 0)".Compile("x")(1)` | `DivideByZeroException` | `NaN` | +| `"x + (2 mod 0)".Compile("x")(1)` | `DivideByZeroException` | `NaN` | + ### `tanh` and `coth` of a large number are their limits **Silent: `tanh` and `coth` of a large positive number evaluate and compile to `1`, where they were `NaN`, and at diff --git a/Sources/AngouriMath/Functions/Compilation/IntoLinq/CompilationProtocol.cs b/Sources/AngouriMath/Functions/Compilation/IntoLinq/CompilationProtocol.cs index 62cd9670e..4494677c4 100644 --- a/Sources/AngouriMath/Functions/Compilation/IntoLinq/CompilationProtocol.cs +++ b/Sources/AngouriMath/Functions/Compilation/IntoLinq/CompilationProtocol.cs @@ -217,6 +217,9 @@ private Expression NumericOperand(Expression expr) ? ConvertType(expr, typeof(double)) : expr; + private static bool IsIntegral(Type type) + => type == typeof(int) || type == typeof(long) || type == typeof(BigInteger); + private Expression ShouldBeAtLeastDouble(Expression expr) { if (typeLevelInHierarchy[expr.Type] < typeLevelInHierarchy[typeof(double)]) @@ -259,6 +262,13 @@ Divf when ShouldBeAtLeastDouble(left) is var newLeft && ShouldBeAtLeastDouble(ri // https://github.com/asc-community/AngouriMath/issues/2195 Modf when left.Type == typeof(System.Numerics.Complex) && right.Type == typeof(System.Numerics.Complex) => Expression.Call(complexRemainder, left, right), + // A remainder by zero has no value, as in evaluation. Of two integers, which over + // double or Complex are constants, it is compiled over the integers, where the + // runtime's % throws for a divisor of 0: 2 mod 0 over double threw + // DivideByZeroException when called. + Modf { Divisor: var divisor } when IsIntegral(left.Type) && divisor.Vars.Count == 0 + && divisor.Evaled is Integer { IsZero: true } + => Expression.Constant(double.NaN), Modf => Operator(ExpressionType.Modulo, Operator(ExpressionType.Add, Operator(ExpressionType.Modulo, left, right), right), right), diff --git a/Sources/Tests/UnitTests/Common/DivisionByZeroOverDoubleTest.cs b/Sources/Tests/UnitTests/Common/DivisionByZeroOverDoubleTest.cs index 6a1a8a21f..1a2d913b4 100644 --- a/Sources/Tests/UnitTests/Common/DivisionByZeroOverDoubleTest.cs +++ b/Sources/Tests/UnitTests/Common/DivisionByZeroOverDoubleTest.cs @@ -53,6 +53,34 @@ public void AMatrixOverZeroHasNoEntries() Assert.True(double.IsNaN(divided[i, j]), $"[{i}, {j}] is {divided[i, j]}"); } + /// + /// A remainder by zero, the same. Of two integers the remainder is compiled over the integers, and the + /// runtime's % threw DivideByZeroException for a divisor of 0 when the function was called. Found by a check + /// of compiled expressions against evaluation; no issue was filed. + /// + [Theory] + [InlineData("2 mod 0")] + [InlineData("x + (2 mod 0)")] + [InlineData("abs(2) mod (2 mod (-1))")] + [InlineData("x mod 0")] + [InlineData("x mod (x - x)")] + public void ARemainderByZeroHasNoValue(string expression) + { + var entity = expression.ToEntity(); + Assert.True(double.IsNaN(entity.Compile("x")(0.7)), $"{expression} over double"); + Assert.True(float.IsNaN(entity.Compile("x")(0.7f)), $"{expression} over float"); + Assert.True(double.IsNaN(entity.Compile("x")(0.7).Real), $"{expression} over Complex"); + Assert.True(double.IsNaN(entity.Compile("x").Call(0.7).Real), $"{expression} by FastExpression"); + Assert.True(entity.Substitute("x", 0.7).Evaled.IsNaN, $"{expression} evaluates to a value"); + } + + [Theory] + [InlineData("7 mod 3 + x", 0.5, 1.5)] + [InlineData("(-7) mod 3 + x", 0.5, 2.5)] + [InlineData("7 mod (-3) + x", 0.5, -1.5)] + public void ARemainderOfIntegersIsTheFlooredOne(string expression, double at, double value) + => Assert.Equal(value, expression.ToEntity().Compile("x")(at)); + [Theory] [InlineData("1/x", 4, 0.25)] [InlineData("x^(1/2)", 0, 0)]