From 93d0fa20ca4b14939d95272380ed90a5df2562f2 Mon Sep 17 00:00:00 2001 From: Rafael Vuijk Date: Sat, 14 Nov 2026 17:20:31 +0000 Subject: [PATCH] gcd and lcm compile at whole arguments; the binders are refused alike, saying to evaluate first gcd and lcm compile in FastExpression and in the LINQ compiler, over Complex and over double: exactly at whole arguments, rounded once by EFloat, and NaN at any other. Evaluation takes them over the rationals too, but a compiled argument is a double, and the double nearest 0.1 is not 1/10. A whole double past 2^53 is read as the integer it is. Both are non-negative, as evaluation gives them: gcd(-4, 6) = 2, gcd(0, 5) = 5, gcd(0, 0) = 0, lcm(-4, 6) = 12, lcm(0, 3) = 0. gcd(12, 18, 8) is gcd(gcd(12, 18), 8) and compiles nested. The calculus nodes and the binders -- derivative, integral, limit, sum, product, a sum, a maximum, a minimum, argmax and argmin over a set, apply and lambda -- stay refused with UncompilableNodeException, by one check both compilers run, whose message names the node and says to call .Evaled or Simplify() first and compile the result. The LINQ compiler said the bound variable was not among the arguments, and FastExpression asked for a bug report for a sum, a product or a lambda. The tests compare all three compiled forms with evaluation exactly, as doubles, at every pair of 14 whole arguments, among them -12, 0, 2^60, 3 * 2^58 and two numbers near 10^15 whose lcm is past 2^53; check NaN at 1/2, 5/2, -3/2, i, 1 + i, NaN and infinity beside 0, 1 and 6; and assert the refusal and its message for each binder in both compilers, and that the evaluated expression compiles. gcd(x, 6) at 4, by Compile: UncompilableNodeException (2.7.0), (2, 0) now. Part of #2195. Co-Authored-By: Claude Opus 5.5 --- BREAKING-CHANGES.md | 23 ++++ Sources/.editorconfig | 9 ++ Sources/AngouriMath/Docs/Usage/Exceptions.md | 5 +- .../Functions/Compilation/Binders.cs | 53 +++++++++ .../Functions/Compilation/IntoFE/Compiler.cs | 45 +++++--- .../Compilation/IntoFE/FastExpression.cs | 8 ++ .../IntoLinq/CompilationProtocol.cs | 5 + .../Compilation/IntoLinq/IntoLinqCompiler.cs | 1 + .../Compilation/IntoLinq/MathAllMethods.cs | 12 +- .../Compilation/IntoLinq/MathAllMethods.tt | 6 +- .../Numerics/NumberTheoreticFunctions.cs | 34 +++++- Sources/Tests/AotSmokeTest/Program.cs | 5 + .../Algebra/SolveTest/NewtonFallbackTest.cs | 4 +- .../Common/BinderCompilationRefusalTest.cs | 71 ++++++++++++ .../UnitTests/Common/GcdLcmCompilationTest.cs | 104 ++++++++++++++++++ .../UnitTests/Core/UserInvalidExceptions.cs | 4 +- 16 files changed, 359 insertions(+), 30 deletions(-) create mode 100644 Sources/AngouriMath/Functions/Compilation/Binders.cs create mode 100644 Sources/Tests/UnitTests/Common/BinderCompilationRefusalTest.cs create mode 100644 Sources/Tests/UnitTests/Common/GcdLcmCompilationTest.cs diff --git a/BREAKING-CHANGES.md b/BREAKING-CHANGES.md index b0c248ea3..daba72519 100644 --- a/BREAKING-CHANGES.md +++ b/BREAKING-CHANGES.md @@ -40,6 +40,29 @@ A not-equal written `<>` is read as `not (a = b)`, and printed that way. --- +## Unreleased — since 2.7.0 + +### `gcd` and `lcm` compile at whole arguments + +**Not silent: what was refused is compiled.** `gcd` and `lcm` were refused by both compilers with +`UncompilableNodeException`. They compile now, exactly at whole arguments, rounded once, and to `NaN` at any other. +Evaluation takes them over the rationals too, but a compiled argument is a double, and the double nearest `0.1` is not +`1/10`, so a compiled value there would be exact for the wrong number. Both are non-negative, as evaluation has them +([#2195](https://github.com/asc-community/AngouriMath/issues/2195)). + +| Input | Was (2.7.0) | Now | +|---|---|---| +| `"gcd(x, 6)".ToEntity().Compile("x").Call(4)` | `UncompilableNodeException` | `(2, 0)` | +| `"lcm(x, 6)".ToEntity().Compile("x")(4)` | `UncompilableNodeException` | `12` | +| `"gcd(x, 6)".ToEntity().Compile("x")(2.5)` | `UncompilableNodeException` | `NaN` | + +The calculus nodes and the binders — `derivative`, `integral`, `limit`, `sum`, `product`, an extremum over a set, +`lambda` and `apply` — are still refused, by both compilers with `UncompilableNodeException` as before; the message now +names the node and says to compile `.Evaled` or `Simplify()` instead. The LINQ compiler said the bound variable was not +among the arguments. + +--- + ## 2.7.0 — since 2.6.0 **Read these first: names that read differently.** Each of these was read as something else on 2.6.0. Code that used diff --git a/Sources/.editorconfig b/Sources/.editorconfig index 2cd71c90b..d40b4ba27 100644 --- a/Sources/.editorconfig +++ b/Sources/.editorconfig @@ -1022,6 +1022,15 @@ file_header_template=\nCopyright (c) 2019-2026 Angouri.\nAngouriMath is licensed [AngouriMath/Functions/Compilation/StatementPositions.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 +[AngouriMath/Functions/Compilation/Binders.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/GcdLcmCompilationTest.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/BinderCompilationRefusalTest.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/StatementInANumericPositionTest.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/Docs/Usage/Exceptions.md b/Sources/AngouriMath/Docs/Usage/Exceptions.md index 17826ebef..107239c07 100644 --- a/Sources/AngouriMath/Docs/Usage/Exceptions.md +++ b/Sources/AngouriMath/Docs/Usage/Exceptions.md @@ -154,7 +154,10 @@ take. `Compile` was asked for a function it cannot build. Two distinct causes, and the message says which: -- a node with no compiled form — a derivative, an integral, a limit, a comparison of a matrix; +- a node with no compiled form — a comparison of a matrix, or a calculus node or a binder: a + derivative, an integral, a limit, a sum, a product, an extremum over a set, a lambda and its + application. For those the message says to call `.Evaled` or `Simplify()` first and compile the + result, which is computed once there rather than at every call; - a variable the expression mentions that the compilation was not given. The message names the variable and lists the ones it *was* given, since `Compile("x")` on `a * x` failing used to surface as a bare `KeyNotFoundException` about some dictionary. diff --git a/Sources/AngouriMath/Functions/Compilation/Binders.cs b/Sources/AngouriMath/Functions/Compilation/Binders.cs new file mode 100644 index 000000000..51e22a130 --- /dev/null +++ b/Sources/AngouriMath/Functions/Compilation/Binders.cs @@ -0,0 +1,53 @@ +// +// 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.Core.Exceptions; +using static AngouriMath.Entity; + +namespace AngouriMath.Core.Compilation +{ + /// + /// The calculus nodes and the binders -- a derivative, an integral, a limit, a sum, a product, an extremum over a + /// set, a lambda and its application -- have no compiled form, and both compilers refuse them alike. Each is + /// computed by evaluation, which compiling them would have to repeat at every call, as slowly as evaluating; so the + /// caller evaluates once and compiles what comes back. The LINQ compiler reached the bound variable first and said + /// it was not among the arguments, which named the symptom rather than the node. + /// https://github.com/asc-community/AngouriMath/issues/2195 + /// + internal static class Binders + { + internal static void Check(Entity expression) + { + foreach (var node in expression.Nodes) + if (Kind(node) is { } kind) + throw new UncompilableNodeException( + $"`{node.Stringize()}` is {kind}, which has no compiled form. " + + (node is Lambda + ? "Apply it to an argument, call .Evaled or Simplify() on that, and compile the result." + : "Call .Evaled or Simplify() on the expression first, and compile the result.")); + } + + private static string? Kind(Entity node) => node switch + { + Derivativef => "a derivative", + Integralf { Range: null } => "an integral", + Integralf => "a definite integral", + Limitf => "a limit", + Summationf => "a sum", + Productf => "a product", + SumOverSetf => "a sum over a set", + Maximumf => "a maximum over a set", + Minimumf => "a minimum over a set", + Argmaxf => "the set of points where a maximum is taken", + Argminf => "the set of points where a minimum is taken", + CalculusOperator => "a binder", + Application => "an application of a function", + Lambda => "a function rather than a number", + _ => null + }; + } +} diff --git a/Sources/AngouriMath/Functions/Compilation/IntoFE/Compiler.cs b/Sources/AngouriMath/Functions/Compilation/IntoFE/Compiler.cs index a91b2dfa3..5dad56f92 100644 --- a/Sources/AngouriMath/Functions/Compilation/IntoFE/Compiler.cs +++ b/Sources/AngouriMath/Functions/Compilation/IntoFE/Compiler.cs @@ -340,24 +340,6 @@ private protected override void CompileNode(Compiler compiler) } } - public partial record Derivativef - { - private protected override void CompileNode(Compiler compiler) => - throw new UncompilableNodeException($"A derivative cannot be compiled: `{Stringize()}`"); - } - - public partial record Integralf - { - private protected override void CompileNode(Compiler compiler) => - throw new UncompilableNodeException($"An integral cannot be compiled: `{Stringize()}`"); - } - - public partial record Limitf - { - private protected override void CompileNode(Compiler compiler) => - throw new UncompilableNodeException($"A limit cannot be compiled: `{Stringize()}`"); - } - // The floors, the rounding and the extremes, which the solver's numerical search needs // compiled to run on `floor(x) = x/2 + 1/3` or `max(x, 1) = 2 x` at all. // https://github.com/asc-community/AngouriMath/issues/1603 @@ -761,6 +743,32 @@ private protected override void CompileNode(Compiler compiler) } } + // The gcd and the lcm, exact at whole arguments and NaN at any other. + // https://github.com/asc-community/AngouriMath/issues/2195 + public partial record Gcdf + { + private protected override bool CompilesItself => true; + + private protected override void CompileNode(Compiler compiler) + { + Right.InnerCompile(compiler); + Left.InnerCompile(compiler); + compiler.Instructions.Add(new(InstructionType.CALL_GCD)); + } + } + + public partial record Lcmf + { + private protected override bool CompilesItself => true; + + private protected override void CompileNode(Compiler compiler) + { + Right.InnerCompile(compiler); + Left.InnerCompile(compiler); + compiler.Instructions.Add(new(InstructionType.CALL_LCM)); + } + } + // A membership whose set the compilers rewrite into a statement about the element is gone by the time // a node is compiled, and what remains is one in a set of numbers, decided where evaluation decides it. // https://github.com/asc-community/AngouriMath/issues/2195 @@ -1076,6 +1084,7 @@ internal static FastExpression Compile(Entity func, IEnumerable variab func = func.InnerSimplified; func = Compilation.CompiledMembership.Rewritten(func); func = Functions.ExponentialQuotients.Rewritten(func); + Compilation.Binders.Check(func); Compilation.StatementPositions.Check(func); var visited = new HashSet(); var cache = new Dictionary(); diff --git a/Sources/AngouriMath/Functions/Compilation/IntoFE/FastExpression.cs b/Sources/AngouriMath/Functions/Compilation/IntoFE/FastExpression.cs index dc88de476..917d68954 100644 --- a/Sources/AngouriMath/Functions/Compilation/IntoFE/FastExpression.cs +++ b/Sources/AngouriMath/Functions/Compilation/IntoFE/FastExpression.cs @@ -100,6 +100,8 @@ internal enum InstructionType CALL_HARMONIC_NUMBER, CALL_DIVISOR_SIGMA, CALL_VALUATION, + CALL_GCD, + CALL_LCM, CALL_DIVIDES, // 3-arg: whether two numbers are congruent modulo a third. @@ -418,6 +420,12 @@ public System.Numerics.Complex Substitute(params System.Numerics.Complex[] value case InstructionType.CALL_VALUATION: stack.Push(AngouriMath.Numerics.NumberTheoreticFunctions.Valuation(stack.Pop(), stack.Pop())); break; + case InstructionType.CALL_GCD: + stack.Push(AngouriMath.Numerics.NumberTheoreticFunctions.Gcd(stack.Pop(), stack.Pop())); + break; + case InstructionType.CALL_LCM: + stack.Push(AngouriMath.Numerics.NumberTheoreticFunctions.Lcm(stack.Pop(), stack.Pop())); + break; case InstructionType.CALL_MEMBER: stack.Push(Truth(AngouriMath.Numerics.NumberStatements.In(stack.Pop(), instruction.Reference))); break; diff --git a/Sources/AngouriMath/Functions/Compilation/IntoLinq/CompilationProtocol.cs b/Sources/AngouriMath/Functions/Compilation/IntoLinq/CompilationProtocol.cs index 32ffcb079..9c5b52744 100644 --- a/Sources/AngouriMath/Functions/Compilation/IntoLinq/CompilationProtocol.cs +++ b/Sources/AngouriMath/Functions/Compilation/IntoLinq/CompilationProtocol.cs @@ -321,6 +321,11 @@ DivisorSigmaf when ShouldBeAtLeastDouble(left) is var newLeft && ShouldBeAtLeast => Expression.Call(GetDef("DivisorSigma", 2, newLeft.Type), newLeft, newRight), Valuationf when ShouldBeAtLeastDouble(left) is var newLeft && ShouldBeAtLeastDouble(right) is var newRight => Expression.Call(GetDef("Valuation", 2, newLeft.Type), newLeft, newRight), + // Exact at whole arguments and NaN at any other. https://github.com/asc-community/AngouriMath/issues/2195 + Gcdf when ShouldBeAtLeastDouble(left) is var newLeft && ShouldBeAtLeastDouble(right) is var newRight + => Expression.Call(GetDef("Gcd", 2, newLeft.Type), newLeft, newRight), + Lcmf when ShouldBeAtLeastDouble(left) is var newLeft && ShouldBeAtLeastDouble(right) is var newRight + => Expression.Call(GetDef("Lcm", 2, newLeft.Type), newLeft, newRight), Dividesf when ShouldBeAtLeastDouble(left) is var newLeft && ShouldBeAtLeastDouble(right) is var newRight => newLeft.Type == typeof(System.Numerics.Complex) ? Expression.Call(complexDivisibility, newLeft, ConvertType(newRight, typeof(System.Numerics.Complex))) diff --git a/Sources/AngouriMath/Functions/Compilation/IntoLinq/IntoLinqCompiler.cs b/Sources/AngouriMath/Functions/Compilation/IntoLinq/IntoLinqCompiler.cs index 6fa2929c7..920fa8cf3 100644 --- a/Sources/AngouriMath/Functions/Compilation/IntoLinq/IntoLinqCompiler.cs +++ b/Sources/AngouriMath/Functions/Compilation/IntoLinq/IntoLinqCompiler.cs @@ -34,6 +34,7 @@ internal static TDelegate Compile( expr = TensorCompilation.SimplifiedAroundMatrixArguments(expr, IsMatrixArgument); expr = CompiledMembership.Rewritten(expr); expr = Functions.ExponentialQuotients.Rewritten(expr); + Binders.Check(expr); StatementPositions.Check(expr); // Two matrices whose shapes are written into the expression and do not fit are refused here, rather than by // a delegate that throws at every point it is called at. https://github.com/asc-community/AngouriMath/issues/526 diff --git a/Sources/AngouriMath/Functions/Compilation/IntoLinq/MathAllMethods.cs b/Sources/AngouriMath/Functions/Compilation/IntoLinq/MathAllMethods.cs index db44183e8..44620cd1e 100644 --- a/Sources/AngouriMath/Functions/Compilation/IntoLinq/MathAllMethods.cs +++ b/Sources/AngouriMath/Functions/Compilation/IntoLinq/MathAllMethods.cs @@ -369,8 +369,8 @@ public static System.Numerics.Complex Round(System.Numerics.Complex a) public static System.Numerics.Complex HarmonicNumber(System.Numerics.Complex a, System.Numerics.Complex b) => AngouriMath.Numerics.PolygammaFunction.HarmonicNumber(a, b); public static double HarmonicNumber(double a, double b) => AngouriMath.Numerics.PolygammaFunction.HarmonicNumber(a, b); - // The functions of a factorisation, the valuation, the n-th prime and the prime-counting function, by the - // interpreter's routines. https://github.com/asc-community/AngouriMath/issues/2195 + // The functions of a factorisation, the valuation, the n-th prime, the prime-counting function, the gcd and + // the lcm, by the interpreter's routines. https://github.com/asc-community/AngouriMath/issues/2195 public static System.Numerics.Complex Mobius(System.Numerics.Complex a) => AngouriMath.Numerics.NumberTheoreticFunctions.Mobius(a); public static double Mobius(double a) => AngouriMath.Numerics.NumberTheoreticFunctions.Mobius(a); public static System.Numerics.Complex PrimeNu(System.Numerics.Complex a) => AngouriMath.Numerics.NumberTheoreticFunctions.PrimeNu(a); @@ -385,6 +385,10 @@ public static System.Numerics.Complex Round(System.Numerics.Complex a) public static double DivisorSigma(double a, double b) => AngouriMath.Numerics.NumberTheoreticFunctions.DivisorSigma(a, b); public static System.Numerics.Complex Valuation(System.Numerics.Complex a, System.Numerics.Complex b) => AngouriMath.Numerics.NumberTheoreticFunctions.Valuation(a, b); public static double Valuation(double a, double b) => AngouriMath.Numerics.NumberTheoreticFunctions.Valuation(a, b); + public static System.Numerics.Complex Gcd(System.Numerics.Complex a, System.Numerics.Complex b) => AngouriMath.Numerics.NumberTheoreticFunctions.Gcd(a, b); + public static double Gcd(double a, double b) => AngouriMath.Numerics.NumberTheoreticFunctions.Gcd(a, b); + public static System.Numerics.Complex Lcm(System.Numerics.Complex a, System.Numerics.Complex b) => AngouriMath.Numerics.NumberTheoreticFunctions.Lcm(a, b); + public static double Lcm(double a, double b) => AngouriMath.Numerics.NumberTheoreticFunctions.Lcm(a, b); /// /// True where System.Numerics.Complex.IsNaN is, spelled out because that overload is @@ -602,6 +606,10 @@ public static bool IsNaN(System.Numerics.Complex a) { ("DivisorSigma", 2, typeof(double)), Def((a, b) => DivisorSigma(a, b)) }, { ("Valuation", 2, typeof(System.Numerics.Complex)), Def((a, b) => Valuation(a, b)) }, { ("Valuation", 2, typeof(double)), Def((a, b) => Valuation(a, b)) }, + { ("Gcd", 2, typeof(System.Numerics.Complex)), Def((a, b) => Gcd(a, b)) }, + { ("Gcd", 2, typeof(double)), Def((a, b) => Gcd(a, b)) }, + { ("Lcm", 2, typeof(System.Numerics.Complex)), Def((a, b) => Lcm(a, b)) }, + { ("Lcm", 2, typeof(double)), Def((a, b) => Lcm(a, b)) }, }; // An expression tree is how C# spells "the MethodInfo of this method" without a string: diff --git a/Sources/AngouriMath/Functions/Compilation/IntoLinq/MathAllMethods.tt b/Sources/AngouriMath/Functions/Compilation/IntoLinq/MathAllMethods.tt index 5fc8d061f..105f72b36 100644 --- a/Sources/AngouriMath/Functions/Compilation/IntoLinq/MathAllMethods.tt +++ b/Sources/AngouriMath/Functions/Compilation/IntoLinq/MathAllMethods.tt @@ -21,7 +21,7 @@ var combinatorialBinary = new[]{ "Binomial", "FallingFactorial", "RisingFactorial" }; // The number-theoretic functions, exact at a whole argument and NaN at any other. var numberTheoretic = new[]{ "Mobius", "PrimeNu", "PrimeOmega", "Prime", "PrimePi" }; - var numberTheoreticBinary = new[]{ "DivisorSigma", "Valuation" }; + var numberTheoreticBinary = new[]{ "DivisorSigma", "Valuation", "Gcd", "Lcm" }; var types = new[]{ "System.Numerics.Complex", "double", "float", "long", "int", "BigInteger" }; #> // @@ -266,8 +266,8 @@ namespace AngouriMath.Core.Compilation.IntoLinq public static System.Numerics.Complex HarmonicNumber(System.Numerics.Complex a, System.Numerics.Complex b) => AngouriMath.Numerics.PolygammaFunction.HarmonicNumber(a, b); public static double HarmonicNumber(double a, double b) => AngouriMath.Numerics.PolygammaFunction.HarmonicNumber(a, b); - // The functions of a factorisation, the valuation, the n-th prime and the prime-counting function, by the - // interpreter's routines. https://github.com/asc-community/AngouriMath/issues/2195 + // The functions of a factorisation, the valuation, the n-th prime, the prime-counting function, the gcd and + // the lcm, by the interpreter's routines. https://github.com/asc-community/AngouriMath/issues/2195 <# foreach (var name in numberTheoretic) { #> public static System.Numerics.Complex <#= name #>(System.Numerics.Complex a) => AngouriMath.Numerics.NumberTheoreticFunctions.<#= name #>(a); public static double <#= name #>(double a) => AngouriMath.Numerics.NumberTheoreticFunctions.<#= name #>(a); diff --git a/Sources/AngouriMath/Numerics/NumberTheoreticFunctions.cs b/Sources/AngouriMath/Numerics/NumberTheoreticFunctions.cs index 7b659ebc0..b7256377d 100644 --- a/Sources/AngouriMath/Numerics/NumberTheoreticFunctions.cs +++ b/Sources/AngouriMath/Numerics/NumberTheoreticFunctions.cs @@ -16,7 +16,8 @@ namespace AngouriMath.Numerics /// The functions of a factorisation, the p-adic valuation, the n-th prime and the prime-counting /// function, for the compilers, by the routines the interpreter uses: exact at a whole argument, and /// NaN at any other, where none of them has a value, and where the interpreter leaves one as - /// written, a number too large to factorise or a prime too far to build towards. + /// written, a number too large to factorise or a prime too far to build towards. The gcd and the lcm + /// likewise, though evaluation has them at a rational too. /// https://github.com/asc-community/AngouriMath/issues/2195 /// internal static class NumberTheoreticFunctions @@ -98,6 +99,35 @@ public static NumericsComplex PrimePi(NumericsComplex x) return Primes.CountUpTo(EInteger.FromInt64((long)Math.Floor(x.Real))) is { } count ? count : NaN; } + /// + /// A whole number held exactly, of any size a has: every finite double past + /// 2^53 is whole, and is the integer it reads as. + /// + private static EInteger? Whole(NumericsComplex z) + => z.Imaginary != 0 || double.IsNaN(z.Real) || double.IsInfinity(z.Real) || Math.Floor(z.Real) != z.Real + ? null + : EFloat.FromDouble(z.Real).ToEInteger(); + + // gcd and lcm compile at whole arguments only. Evaluation takes them over the rationals, and every finite double + // is a rational, so a compiled gcd(0.1, 1) would be exact for the double nearest 0.1 and mean nothing for 0.1. + // Both are non-negative, as evaluation has them: gcd(-4, 6) = 2, gcd(0, 5) = gcd(0, -5) = 5, gcd(0, 0) = 0, + // lcm(-4, 6) = lcm(-4, -6) = 12, lcm(0, 3) = lcm(0, 0) = 0. Rounded once by EFloat, as (double)BigInteger + // truncates. https://github.com/asc-community/AngouriMath/issues/2195 + + /// The greatest common divisor of two whole numbers, and NaN where either is not whole. + public static NumericsComplex Gcd(NumericsComplex a, NumericsComplex b) + => Whole(a) is { } left && Whole(b) is { } right + ? EFloat.FromEInteger(left.GreatestCommonDivisor(right)).ToDouble() + : NaN; + + /// The least common multiple of two whole numbers, 0 where either is, and NaN where either is not whole. + public static NumericsComplex Lcm(NumericsComplex a, NumericsComplex b) + => Whole(a) is { } left && Whole(b) is { } right + ? left.IsZero || right.IsZero + ? 0 + : EFloat.FromEInteger((left * right).Abs() / left.GreatestCommonDivisor(right)).ToDouble() + : NaN; + public static double Mobius(double n) => Real(Mobius(new NumericsComplex(n, 0))); public static double PrimeNu(double n) => Real(PrimeNu(new NumericsComplex(n, 0))); public static double PrimeOmega(double n) => Real(PrimeOmega(new NumericsComplex(n, 0))); @@ -105,6 +135,8 @@ public static NumericsComplex PrimePi(NumericsComplex x) public static double Valuation(double n, double p) => Real(Valuation(new NumericsComplex(n, 0), new NumericsComplex(p, 0))); public static double Prime(double n) => Real(Prime(new NumericsComplex(n, 0))); public static double PrimePi(double x) => Real(PrimePi(new NumericsComplex(x, 0))); + public static double Gcd(double a, double b) => Real(Gcd(new NumericsComplex(a, 0), new NumericsComplex(b, 0))); + public static double Lcm(double a, double b) => Real(Lcm(new NumericsComplex(a, 0), new NumericsComplex(b, 0))); private static double Real(NumericsComplex z) => z.Imaginary == 0 ? z.Real : double.NaN; } diff --git a/Sources/Tests/AotSmokeTest/Program.cs b/Sources/Tests/AotSmokeTest/Program.cs index 129658f38..a8bd722af 100644 --- a/Sources/Tests/AotSmokeTest/Program.cs +++ b/Sources/Tests/AotSmokeTest/Program.cs @@ -325,6 +325,11 @@ private static void Compilation() var linqNumberTheory = "divisorsigma(1, x) + valuation(x, 2)".Compile(x); Check("compiled mobius(x) + prime(x) + primepi(x) at 30, divisorsigma(1, x) + valuation(x, 2) at 12", fastNumberTheory.Call(30) == 122 && linqNumberTheory(12) == 30, $"{fastNumberTheory.Call(30)}, {linqNumberTheory(12)}"); + // gcd(12, 18) = 6 and lcm(12, 18) = 36, and no value at 5/2. + var fastGcd = "gcd(x, 18)".Compile(x); + var linqLcm = "lcm(x, 18)".Compile(x); + Check("compiled gcd(x, 18) and lcm(x, 18) at 12 and 5/2", + fastGcd.Call(12) == 6 && linqLcm(12) == 36 && double.IsNaN(linqLcm(2.5)), $"{fastGcd.Call(12)}, {linqLcm(12)}, {linqLcm(2.5)}"); // 7 is a prime in [0; 10] and 3 divides 6; i is in no interval. var fastMembership = "iverson(x in (PP /\\ [0; 10])) + iverson(3 divides x - 1)".Compile(x); var linqMembership = "x in [0; 1] or 3 divides x".Compile(x); diff --git a/Sources/Tests/UnitTests/Algebra/SolveTest/NewtonFallbackTest.cs b/Sources/Tests/UnitTests/Algebra/SolveTest/NewtonFallbackTest.cs index b85dce666..d2b21ebb5 100644 --- a/Sources/Tests/UnitTests/Algebra/SolveTest/NewtonFallbackTest.cs +++ b/Sources/Tests/UnitTests/Algebra/SolveTest/NewtonFallbackTest.cs @@ -46,8 +46,8 @@ public void TheFloorsAndTheExtremesAreSearched(string equation, double[] expecte } /// - /// No analytical route answers these, and the compiler has no form for the binomial - /// coefficient or for gcd, nor for their unevaluated derivatives. + /// No analytical route answers these, and the compiler has no form for the unevaluated + /// derivatives of the binomial coefficient and of gcd. /// [Theory] [InlineData("binomial(x, 3/2) = x/2")] diff --git a/Sources/Tests/UnitTests/Common/BinderCompilationRefusalTest.cs b/Sources/Tests/UnitTests/Common/BinderCompilationRefusalTest.cs new file mode 100644 index 000000000..025606919 --- /dev/null +++ b/Sources/Tests/UnitTests/Common/BinderCompilationRefusalTest.cs @@ -0,0 +1,71 @@ +// +// 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.Core.Exceptions; +using AngouriMath.Extensions; +using Xunit; + +namespace AngouriMath.Tests.Common +{ + /// + /// The calculus nodes and the binders have no compiled form, and both compilers refuse them with + /// , naming the node and saying to evaluate first. The LINQ compiler + /// said the bound variable was not among the arguments. + /// #2195 + /// + [Trait("Area", "Common")] + public sealed class BinderCompilationRefusalTest + { + [Theory] + [InlineData("derivative(x^2, x)", "derivative(x ^ 2, x)")] + [InlineData("integral(x^2, x)", "integral(x ^ 2, x)")] + [InlineData("integral(x t, t, 0, 1)", "integral(x * t, t, 0, 1)")] + [InlineData("limit(sin(x t)/t, t, 0)", "limit(sin(x * t) / t, t, 0)")] + [InlineData("sum(x^k, k, 1, 3)", "sum(x ^ k, k, 1, 3)")] + [InlineData("product(x + k, k, 1, 3)", "product(x + k, k, 1, 3)")] + [InlineData("sum(x k, k in {1, 2})", "sum(x * k, k in { 1, 2 })")] + [InlineData("max(x k, k in {1, 2})", "max(x * k, k in { 1, 2 })")] + [InlineData("min(x k, k in {1, 2})", "min(x * k, k in { 1, 2 })")] + [InlineData("argmax(x k, k in {1, 2})", "argmax(x * k, k in { 1, 2 })")] + [InlineData("argmin(x k, k in {1, 2})", "argmin(x * k, k in { 1, 2 })")] + [InlineData("apply(lambda(t, t + 1), x)", "apply(lambda(t, t + 1), x)")] + [InlineData("lambda(t, t + x)", "lambda(t, t + x)")] + [InlineData("1 + sin(derivative(x^2, x))", "derivative(x ^ 2, x)")] + public void EachCompilerRefusesItAndSaysToEvaluateFirst(string expression, string node) + { + var entity = expression.ToEntity(); + AssertRefused(Assert.Throws(() => entity.Compile("x")), node); + AssertRefused(Assert.Throws(() => entity.Compile("x")), node); + AssertRefused(Assert.Throws(() => entity.Compile("x")), node); + } + + private static void AssertRefused(UncompilableNodeException thrown, string node) + { + Assert.Contains($"`{node}`", thrown.Message); + Assert.Contains(".Evaled", thrown.Message); + Assert.Contains("Simplify()", thrown.Message); + } + + /// What the message advises compiles, and agrees with evaluation. + [Theory] + [InlineData("derivative(x^3, x)")] + [InlineData("integral(x t, t, 0, 1)")] + [InlineData("limit(sin(x t)/t, t, 0)")] + [InlineData("sum(x^k, k, 1, 3)")] + [InlineData("product(x + k, k, 1, 3)")] + [InlineData("apply(lambda(t, t^2 + 1), x)")] + public void TheEvaluatedExpressionCompiles(string expression) + { + var evaled = expression.ToEntity().Evaled; + var expected = ((Entity.Number.Complex)evaled.Substitute("x", 3).Evaled).ToNumerics(); + Assert.True(Complex.Abs(evaled.Compile("x").Call(3) - expected) < 1e-9); + Assert.True(Complex.Abs(evaled.Compile("x")(3) - expected) < 1e-9); + } + } +} diff --git a/Sources/Tests/UnitTests/Common/GcdLcmCompilationTest.cs b/Sources/Tests/UnitTests/Common/GcdLcmCompilationTest.cs new file mode 100644 index 000000000..e49d541f4 --- /dev/null +++ b/Sources/Tests/UnitTests/Common/GcdLcmCompilationTest.cs @@ -0,0 +1,104 @@ +// +// 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 PeterO.Numbers; +using Xunit; + +namespace AngouriMath.Tests.Common +{ + /// + /// gcd and lcm compile, in FastExpression and in the LINQ compiler over Complex and over + /// double: exactly what evaluation gives at whole arguments, rounded once, and NaN at any other. + /// Evaluation takes them over the rationals too, but a compiled argument is a double, and the double nearest + /// 0.1 is not 1/10. Both were refused. + /// #2195 + /// + [Trait("Area", "Common")] + public sealed class GcdLcmCompilationTest + { + // 2^60 and 3 * 2^58 are past 2^53, where a double is still the whole number it reads as; 999999999999989 + // times 999999999999947 is past it too, so the lcm of the two is rounded. + private static readonly string[] wholes = + { "-12", "-4", "-1", "0", "1", "4", "6", "7", "12", "18", "2^60", "3 * 2^58", "999999999999989", "999999999999947" }; + + private static readonly Complex[] notWhole = + { 0.5, 2.5, -1.5, new Complex(0, 1), new Complex(1, 1), new Complex(4, 1e-300), double.NaN, + double.PositiveInfinity, new Complex(double.NaN, 0) * new Complex(0, 1) }; + + private static Complex Numeric(string value) => ((Entity.Number.Complex)value.ToEntity().Evaled).ToNumerics(); + + [Theory] + [InlineData("gcd(x, y)")] + [InlineData("lcm(x, y)")] + public void AtWholeArgumentsItIsExactlyWhatEvaluationSays(string expression) + { + var fast = expression.ToEntity().Compile("x", "y"); + var linq = expression.ToEntity().Compile("x", "y"); + var real = expression.ToEntity().Compile("x", "y"); + foreach (var a in wholes) + foreach (var b in wholes) + { + var evaluated = Assert.IsType(expression.ToEntity().Substitute("x", a).Substitute("y", b).Evaled); + var expected = EFloat.FromEInteger(evaluated.EInteger).ToDouble(); + var (x, y) = (Numeric(a), Numeric(b)); + Assert.True(fast.Call(x, y) == expected, $"{expression} at ({a}, {b}): {expected} evaluated, {fast.Call(x, y)} by FastExpression"); + Assert.True(linq(x, y) == expected, $"{expression} at ({a}, {b}): {expected} evaluated, {linq(x, y)} by LINQ"); + Assert.True(real(x.Real, y.Real) == expected, $"{expression} at ({a}, {b}): {expected} evaluated, {real(x.Real, y.Real)} over double"); + } + } + + // Evaluation, measured: gcd(-4, 6) = 2, gcd(0, 5) = gcd(0, -5) = 5, gcd(0, 0) = 0, lcm(-4, 6) = 12, lcm(0, 3) = 0. + [Theory] + [InlineData("gcd(x, y)", -4, 6, 2)] + [InlineData("gcd(x, y)", 0, 5, 5)] + [InlineData("gcd(x, y)", 0, -5, 5)] + [InlineData("gcd(x, y)", 0, 0, 0)] + [InlineData("lcm(x, y)", -4, 6, 12)] + [InlineData("lcm(x, y)", -4, -6, 12)] + [InlineData("lcm(x, y)", 0, 3, 0)] + [InlineData("lcm(x, y)", 0, 0, 0)] + public void BothAreNonNegativeAsEvaluationHasThem(string expression, int a, int b, int expected) + { + Assert.Equal(expected, ((Entity.Number.Integer)expression.ToEntity().Substitute("x", a).Substitute("y", b).Evaled).EInteger.ToInt32Checked()); + Assert.Equal(new Complex(expected, 0), expression.ToEntity().Compile("x", "y").Call(a, b)); + Assert.Equal(new Complex(expected, 0), expression.ToEntity().Compile("x", "y")(a, b)); + Assert.Equal(expected, expression.ToEntity().Compile("x", "y")(a, b)); + } + + [Theory] + [InlineData("gcd(x, y)")] + [InlineData("lcm(x, y)")] + public void AtAnyOtherArgumentItIsNaN(string expression) + { + var fast = expression.ToEntity().Compile("x", "y"); + var linq = expression.ToEntity().Compile("x", "y"); + var real = expression.ToEntity().Compile("x", "y"); + foreach (var other in notWhole) + foreach (var whole in new Complex[] { 0, 1, 6 }) + foreach (var (x, y) in new[] { (other, whole), (whole, other) }) + { + Assert.True(IsNaN(fast.Call(x, y)), $"{expression} at ({x}, {y}): {fast.Call(x, y)} by FastExpression"); + Assert.True(IsNaN(linq(x, y)), $"{expression} at ({x}, {y}): {linq(x, y)} by LINQ"); + if (x.Imaginary == 0 && y.Imaginary == 0) + Assert.True(double.IsNaN(real(x.Real, y.Real)), $"{expression} at ({x}, {y}): {real(x.Real, y.Real)} over double"); + } + } + + // gcd(12, 18, 8) is read as gcd(gcd(12, 18), 8), and so compiled. + [Fact] + public void SeveralArgumentsAreNested() + { + Assert.Equal(new Complex(2, 0), "gcd(x, 18, y)".ToEntity().Compile("x", "y").Call(12, 8)); + Assert.Equal(12, "lcm(x, 3, y)".ToEntity().Compile("x", "y")(2, 4)); + } + + private static bool IsNaN(Complex z) => double.IsNaN(z.Real) || double.IsNaN(z.Imaginary); + } +} diff --git a/Sources/Tests/UnitTests/Core/UserInvalidExceptions.cs b/Sources/Tests/UnitTests/Core/UserInvalidExceptions.cs index e06e22ac3..067ccd2cf 100644 --- a/Sources/Tests/UnitTests/Core/UserInvalidExceptions.cs +++ b/Sources/Tests/UnitTests/Core/UserInvalidExceptions.cs @@ -57,12 +57,10 @@ [Fact] public void UncompilableNode4() => // of these were nodes 2.0 added and never taught the compiler; floor, ceil and round have // been taught since (https://github.com/asc-community/AngouriMath/issues/1603), phi, // gamma and the factorial (https://github.com/asc-community/AngouriMath/issues/1607), and - // the binomial coefficient, the polygamma functions and the n-th prime + // the binomial coefficient, the polygamma functions, the n-th prime, the gcd and the lcm // (https://github.com/asc-community/AngouriMath/issues/2195). [Theory] [InlineData("gamma(2, x)")] - [InlineData("gcd(x, 6)")] - [InlineData("lcm(x, 6)")] public void ANodeWithNoCompiledFormSaysSoRatherThanAskingForABugReport(string expression) => Assert.Throws(() => expression.ToEntity().Compile("x"));