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"));