From d8b33ee59fd161645138cc19ed9d12a5f30de34e Mon Sep 17 00:00:00 2001 From: Rafael Vuijk Date: Mon, 2 Nov 2026 13:35:10 +0000 Subject: [PATCH] Compile a real remainder over double and float with one rounding The LINQ compiler wrote the floored remainder as ((a % b) + b) % b. Over the integers that is exact; over double the second % rounds even where the first remainder already had the divisor's sign. 2 mod e compiled to 1.9999999999999996, and floor(2 mod e) to 1, where both evaluate to 2. Over double and float the remainder is now taken once and the divisor added only where the signs differ, as RealOnly.Mod already did for Complex. The integer types keep the exact expression. Found by compilesweep at seed 11: pi / floor(2 mod e) compiled to pi. Co-Authored-By: Claude Opus 5.5 --- BREAKING-CHANGES.md | 12 +++++ .../IntoLinq/CompilationProtocol.cs | 10 +++++ .../Functions/Compilation/RealOnly.cs | 20 +++++++++ .../Common/ACompiledRealRemainderTest.cs | 44 +++++++++++++++++++ 4 files changed, 86 insertions(+) create mode 100644 Sources/Tests/UnitTests/Common/ACompiledRealRemainderTest.cs diff --git a/BREAKING-CHANGES.md b/BREAKING-CHANGES.md index ed8e67ccc..a9f4e4954 100644 --- a/BREAKING-CHANGES.md +++ b/BREAKING-CHANGES.md @@ -633,6 +633,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 remainder of two reals is rounded once + +**Silent: compiled over `double` or `float`, `a mod b` is the floored remainder rounded once, where it was rounded twice.** +It was compiled as `((a % b) + b) % b`, which takes the remainder a second time even where the first already has the +divisor's sign, and that second one rounds. Now the divisor is added only where the signs differ, as the compiled +remainder over `Complex` already did. + +| Input | Was (2.6.0) | Now | +|---|---|---| +| `"2 mod e".ToEntity().Compile("x")(0)` | `1.9999999999999996` | `2` | +| `"floor(2 mod e)".ToEntity().Compile("x")(0)` | `1` | `2`, as evaluated | + ### A compiled odd root of a negative real is real **Silent: compiled, a negative real to a rational power with an odd denominator below ten is the real root to the diff --git a/Sources/AngouriMath/Functions/Compilation/IntoLinq/CompilationProtocol.cs b/Sources/AngouriMath/Functions/Compilation/IntoLinq/CompilationProtocol.cs index 4494677c4..b60bd0c81 100644 --- a/Sources/AngouriMath/Functions/Compilation/IntoLinq/CompilationProtocol.cs +++ b/Sources/AngouriMath/Functions/Compilation/IntoLinq/CompilationProtocol.cs @@ -269,6 +269,13 @@ Divf when ShouldBeAtLeastDouble(left) is var newLeft && ShouldBeAtLeastDouble(ri Modf { Divisor: var divisor } when IsIntegral(left.Type) && divisor.Vars.Count == 0 && divisor.Evaled is Integer { IsZero: true } => Expression.Constant(double.NaN), + // Over the reals the remainder is taken once, and the divisor added only where the + // signs differ: the shortest expression below rounds a second time, which is exact + // for the integers and not for a double. + Modf when left.Type == typeof(double) && right.Type == typeof(double) + => Expression.Call(realRemainderOfDoubles, left, right), + Modf when left.Type == typeof(float) && right.Type == typeof(float) + => Expression.Call(realRemainderOfFloats, left, right), Modf => Operator(ExpressionType.Modulo, Operator(ExpressionType.Add, Operator(ExpressionType.Modulo, left, right), right), right), @@ -602,6 +609,9 @@ internal Expression ConvertMembership(Expression element, AngouriMath.Numerics.N [ConstantField] private static readonly MethodInfo complexRemainder = ((MethodCallExpression)((Expression>)((a, b) => RealOnly.Mod(a, b))).Body).Method; + [ConstantField] private static readonly MethodInfo realRemainderOfDoubles = Call((a, b) => RealOnly.Mod(a, b)); + [ConstantField] private static readonly MethodInfo realRemainderOfFloats = Call((a, b) => RealOnly.Mod(a, b)); + private static MethodInfo Call(Expression> e) => ((MethodCallExpression)e.Body).Method; private static MethodInfo Op(Expression> e) diff --git a/Sources/AngouriMath/Functions/Compilation/RealOnly.cs b/Sources/AngouriMath/Functions/Compilation/RealOnly.cs index 2e8542681..e1fa58089 100644 --- a/Sources/AngouriMath/Functions/Compilation/RealOnly.cs +++ b/Sources/AngouriMath/Functions/Compilation/RealOnly.cs @@ -46,6 +46,26 @@ public static NumericsComplex Mod(NumericsComplex a, NumericsComplex b) 0); } + /// + /// The floored remainder of two reals, which takes the sign of the divisor, as + /// takes it. Written as + /// ((a % b) + b) % b it is rounded a second time where the first remainder already + /// had the divisor's sign: 2 mod e came out 1.9999999999999996, and + /// floor(2 mod e) 1, where both evaluate to 2. + /// + public static double Mod(double a, double b) + { + var truncated = a % b; + return (truncated is 0 || truncated < 0 == b < 0 ? truncated : truncated + b) + 0.0; + } + + /// The floored remainder of two reals in single precision, as . + public static float Mod(float a, float b) + { + var truncated = a % b; + return (truncated is 0 || truncated < 0 == b < 0 ? truncated : truncated + b) + 0.0f; + } + /// /// The larger of two numbers, or NaN where they are not both real: the complex numbers are /// not ordered, and the interpreter leaves max(2, 1 + i) unevaluated and gives diff --git a/Sources/Tests/UnitTests/Common/ACompiledRealRemainderTest.cs b/Sources/Tests/UnitTests/Common/ACompiledRealRemainderTest.cs new file mode 100644 index 000000000..e7ad31f48 --- /dev/null +++ b/Sources/Tests/UnitTests/Common/ACompiledRealRemainderTest.cs @@ -0,0 +1,44 @@ +// +// 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 double and float, the floored remainder was ((a % b) + b) % b, which rounds a + /// second time where the first remainder already has the divisor's sign: 2 mod e was 1.9999999999999996 and + /// floor(2 mod e) 1, where both evaluate to 2. No issue was filed; found by work/compilesweep at seed 11. + /// + [Trait("Area", "Common")] + public sealed class ACompiledRealRemainderTest + { + [Theory] + [InlineData("2 mod e", 2.0)] + [InlineData("floor(2 mod e)", 2.0)] + [InlineData("(-2) mod (-e)", -2.0)] + [InlineData("(-6) mod 3", 0.0)] + [InlineData("7.5 mod 2", 1.5)] + public void IsTheFlooredRemainderRoundedOnce(string expression, double expected) + { + Assert.Equal(expected, expression.ToEntity().Compile("x")(0)); + Assert.Equal((float)expected, expression.ToEntity().Compile("x")(0)); + } + + // A remainder whose sign differs from the divisor's moves by one divisor, to the divisor's side. + [Theory] + [InlineData("(-2) mod e")] + [InlineData("2 mod (-e)")] + public void TakesTheSignOfTheDivisor(string expression) + { + var value = ((Entity.Number.Complex)expression.ToEntity().EvalNumerical()).RealPart.EDecimal.ToDouble(); + Assert.Equal(value, expression.ToEntity().Compile("x")(0), 15); + } + } +}