From 696b7eb85703997b1cbf7cb388622ade6c4a6d92 Mon Sep 17 00:00:00 2001 From: Rafael Vuijk Date: Fri, 30 Oct 2026 04:11:40 +0000 Subject: [PATCH] Membership and divisibility compile x in S and a divides b were refused by both compilers. Each now compiles to the truth value evaluation gives: 1, 0 or NaN in FastExpression, and true, false or null in the LINQ compiler. The set is fixed when the expression is compiled, so only the element is computed. CompiledMembership rewrites a membership, before either compiler sees it, into the statement it makes about its element: - a union, an intersection and a difference become Kleene's or, and, and and-not of the memberships in their sides, as evaluation decides them since #2221; - an interval becomes the element's imaginary part being 0 and the two comparisons with its ends, since a number that is not real is in no interval, and i in [0; 1] is False rather than undecided; - a finite set becomes an equality with one of its members; - a conditional set becomes its predicate of the element. A membership in RR, CC, ZZ, ZZ+, ZZ* or PP is left for each compiler, which calls NumberStatements.In: a new FastExpression instruction, CALL_MEMBER, whose reference names the set, and a method call in LINQ. It has no truth value at NaN. An infinity is in RR and CC, as evaluation holds it until #2220 is decided, and in none of the sets of whole numbers. Primality comes from Primes.IsPrime, the interpreter's trial division, and is left open past 2^63, where the interpreter leaves it. QQ stays refused, because every finite double is a rational while pi in QQ is False. BB stays refused too. So does any set with one of those inside it. Divisibility is NumberStatements.Divides, through CALL_DIVIDES in FastExpression. Between whole numbers it is whether the remainder is 0, with 0 dividing only 0, and a double's remainder is exact however large. It has no truth value anywhere else, as evaluation gives NaN for 2 divides 5/2. MembershipCompilationTest compares both compilers with evaluation: - 21 sets at 20 points, among them i, 1 + i, 2i, +oo and -oo; - divisibility on a 10 by 9 grid, with 10^20, 5/2, 2i and +oo; - memberships inside an Iverson bracket, a piecewise and connectives; - the refusals. Infinities are left out of the piecewise case, since a compiled -x at +oo has a NaN imaginary part. That is #2222, and it predates this change. Part of #2195. Co-Authored-By: Claude Opus 5.5 (1M context) --- BREAKING-CHANGES.md | 16 +++ Sources/.editorconfig | 9 ++ .../Compilation/CompiledMembership.cs | 49 +++++++ .../Functions/Compilation/IntoFE/Compiler.cs | 36 +++++ .../Compilation/IntoFE/FastExpression.cs | 9 ++ .../IntoLinq/CompilationProtocol.cs | 27 ++++ .../Compilation/IntoLinq/IntoLinqCompiler.cs | 7 + .../AngouriMath/Numerics/NumberStatements.cs | 89 ++++++++++++ Sources/Tests/AotSmokeTest/Program.cs | 6 + .../Common/MembershipCompilationTest.cs | 134 ++++++++++++++++++ 10 files changed, 382 insertions(+) create mode 100644 Sources/AngouriMath/Functions/Compilation/CompiledMembership.cs create mode 100644 Sources/AngouriMath/Numerics/NumberStatements.cs create mode 100644 Sources/Tests/UnitTests/Common/MembershipCompilationTest.cs diff --git a/BREAKING-CHANGES.md b/BREAKING-CHANGES.md index 6d2609c2a..baf38c55a 100644 --- a/BREAKING-CHANGES.md +++ b/BREAKING-CHANGES.md @@ -354,6 +354,22 @@ interpreter leaves them as written ([#2195](https://github.com/asc-community/Ang | `"valuation(x, 2)".ToEntity().Compile("x").Call(12)` | `UncompilableNodeException` | `(2, 0)` | | `"valuation(x, 2)".ToEntity().Compile("x")(0)` | `UncompilableNodeException` | `Infinity` | +### Membership and divisibility compile + +**Not silent: what was refused is compiled.** `x in S` and `a divides b` were refused by both compilers with +`UncompilableNodeException`. A membership compiles to the truth value evaluation gives, `1`, `0` or `NaN` in +FastExpression and `true`, `false` or `null` in the LINQ compiler. The set is fixed when the expression is compiled: +a union, an intersection and a difference by Kleene's connectives, an interval by comparing the element with its ends, +a finite set by equality with its members, a conditional set by its predicate, and `RR`, `CC`, `ZZ`, `ZZ+`, `ZZ*` and +`PP` as evaluation decides them. Membership in `QQ` stays refused, since every finite double is a rational and +`pi in QQ` is `False`, and so does membership in `BB`. A divisibility holds or fails between whole numbers and has no +truth value at any other ([#2195](https://github.com/asc-community/AngouriMath/issues/2195)). + +| Input | Was (2.6.0) | Now | +|---|---|---| +| `"x in [0; 1]".ToEntity().Compile("x").Call(0.5)` | `UncompilableNodeException` | `(1, 0)` | +| `"3 divides x".ToEntity().Compile("x").Call(6)` | `UncompilableNodeException` | `(1, 0)` | + ### `mod` compiles over `Complex` **Not silent: what was refused is compiled.** `Compile` refused `mod` with `UncompilableNodeException`, diff --git a/Sources/.editorconfig b/Sources/.editorconfig index 988cc116c..fb2115c35 100644 --- a/Sources/.editorconfig +++ b/Sources/.editorconfig @@ -378,9 +378,18 @@ file_header_template=\nCopyright (c) 2019-2026 Angouri.\nAngouriMath is licensed [AngouriMath/Numerics/NumberTheoreticFunctions.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/Numerics/NumberStatements.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/CompiledMembership.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/NumberTheoreticCompilationTest.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/MembershipCompilationTest.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/CompileMatrixTest.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/CompiledMembership.cs b/Sources/AngouriMath/Functions/Compilation/CompiledMembership.cs new file mode 100644 index 000000000..54d0c384c --- /dev/null +++ b/Sources/AngouriMath/Functions/Compilation/CompiledMembership.cs @@ -0,0 +1,49 @@ +// +// 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.Linq; +using NumberStatements = AngouriMath.Numerics.NumberStatements; +using static AngouriMath.Entity; +using static AngouriMath.Entity.Set; + +namespace AngouriMath.Core.Compilation +{ + /// + /// A membership written, for the compilers, as the statement it makes about its element; the set is fixed + /// when the expression is compiled, so only the element is computed. In a union, an intersection or a + /// difference, Kleene's connectives of the memberships in its sides, as evaluation decides them; in an + /// interval, the element real and between the ends; in a finite set, equal to one of its members; and in a + /// conditional set, the set's predicate of it. A membership in a set of numbers is left, for each compiler to + /// compile itself, and one in any other set, or with any other set inside, is left as written, which neither + /// compiles. + /// https://github.com/asc-community/AngouriMath/issues/2195 + /// + internal static class CompiledMembership + { + internal static Entity Rewritten(Entity expression) + => expression.Nodes.Any(node => node is Inf) + ? expression.Replace(node => node is Inf(var element, var set) && Statement(element, set) is { } statement ? statement : node) + : expression; + + private static Entity? Statement(Entity element, Entity set) => set switch + { + Unionf(var left, var right) => Statement(element, left) is { } inLeft && Statement(element, right) is { } inRight ? inLeft | inRight : null, + Intersectionf(var left, var right) => Statement(element, left) is { } inLeft && Statement(element, right) is { } inRight ? inLeft & inRight : null, + SetMinusf(var left, var right) => Statement(element, left) is { } inLeft && Statement(element, right) is { } inRight ? inLeft & !inRight : null, + FiniteSet finite => finite.Count == 0 ? Boolean.False : finite.Select(member => element.Equalizes(member)).Aggregate((one, another) => one | another), + // A number that is not real is in no interval, which the comparisons alone would leave with no truth + // value: i in [0; 1] is False. + Interval(var from, var fromClosed, var to, var toClosed) + => new Equalsf(new Imf(element), Number.Integer.Zero) + & (fromClosed ? new LessOrEqualf(from, element) : (Entity)new Lessf(from, element)) + & (toClosed ? new LessOrEqualf(element, to) : (Entity)new Lessf(element, to)), + ConditionalSet(var variable, var predicate) => predicate.Substitute(variable, element), + SpecialSet special when NumberStatements.Of(special) is not null => new Inf(element, special), + _ => null + }; + } +} diff --git a/Sources/AngouriMath/Functions/Compilation/IntoFE/Compiler.cs b/Sources/AngouriMath/Functions/Compilation/IntoFE/Compiler.cs index ac2f221f0..aebf61969 100644 --- a/Sources/AngouriMath/Functions/Compilation/IntoFE/Compiler.cs +++ b/Sources/AngouriMath/Functions/Compilation/IntoFE/Compiler.cs @@ -734,6 +734,41 @@ private protected override void CompileNode(Compiler compiler) } } + // 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 + public partial record Set + { + public partial record Inf + { + private protected override bool CompilesItself => SupSet is SpecialSet special && AngouriMath.Numerics.NumberStatements.Of(special) is not null; + + private protected override void CompileNode(Compiler compiler) + { + if (SupSet is not SpecialSet special || AngouriMath.Numerics.NumberStatements.Of(special) is not { } set) + { + base.CompileNode(compiler); + return; + } + Element.InnerCompile(compiler); + compiler.Instructions.Add(new(InstructionType.CALL_MEMBER, (int)set)); + } + } + } + + // Divisibility of whole numbers, with no truth value at any other. + public partial record Dividesf + { + private protected override bool CompilesItself => true; + + private protected override void CompileNode(Compiler compiler) + { + Dividend.InnerCompile(compiler); + Divisor.InnerCompile(compiler); + compiler.Instructions.Add(new(InstructionType.CALL_DIVIDES)); + } + } + public partial record Maxf { private protected override bool CompilesItself => true; @@ -966,6 +1001,7 @@ internal static FastExpression Compile(Entity func, IEnumerable variab varNamespace[varName] = id++; foreach (var constant in Variable.NamedConstants.Values) func = func.Substitute(constant, constant.Value); + func = Compilation.CompiledMembership.Rewritten(func); var visited = new HashSet(); var cache = new Dictionary(); foreach (var node in func.Nodes) diff --git a/Sources/AngouriMath/Functions/Compilation/IntoFE/FastExpression.cs b/Sources/AngouriMath/Functions/Compilation/IntoFE/FastExpression.cs index cab923c8f..f83b427a4 100644 --- a/Sources/AngouriMath/Functions/Compilation/IntoFE/FastExpression.cs +++ b/Sources/AngouriMath/Functions/Compilation/IntoFE/FastExpression.cs @@ -62,6 +62,8 @@ internal enum InstructionType CALL_PRIME_OMEGA, CALL_PRIME, CALL_PRIME_PI, + // Membership in the set of numbers its reference names, a NumberStatements.NumberSet. + CALL_MEMBER, // 2-arg functions CALL_SUM = 100, @@ -92,6 +94,7 @@ internal enum InstructionType CALL_HARMONIC_NUMBER, CALL_DIVISOR_SIGMA, CALL_VALUATION, + CALL_DIVIDES, // 3-arg: a value where its predicate holds, and the one below it elsewhere. CALL_SELECT = 150, @@ -396,6 +399,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_MEMBER: + stack.Push(Truth(AngouriMath.Numerics.NumberStatements.In(stack.Pop(), instruction.Reference))); + break; + case InstructionType.CALL_DIVIDES: + stack.Push(Truth(AngouriMath.Numerics.NumberStatements.Divides(stack.Pop(), stack.Pop()))); + break; case InstructionType.CALL_CATALAN_NUMBER: stack.Push(AngouriMath.Numerics.CombinatorialFunctions.CatalanNumber(stack.Pop())); break; diff --git a/Sources/AngouriMath/Functions/Compilation/IntoLinq/CompilationProtocol.cs b/Sources/AngouriMath/Functions/Compilation/IntoLinq/CompilationProtocol.cs index f5dbb737c..0a57a5ea9 100644 --- a/Sources/AngouriMath/Functions/Compilation/IntoLinq/CompilationProtocol.cs +++ b/Sources/AngouriMath/Functions/Compilation/IntoLinq/CompilationProtocol.cs @@ -260,6 +260,10 @@ 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), + 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))) + : Expression.Call(realDivisibility, ConvertType(newLeft, typeof(double)), ConvertType(newRight, typeof(double))), Maxf => Expression.Call(GetDef("Max", 2, left.Type), left, right), Minf => Expression.Call(GetDef("Min", 2, left.Type), left, right), @@ -485,6 +489,29 @@ private static MethodInfo Conv(Expression> e) { (ExpressionType.GreaterThanOrEqual, typeof(BigInteger)), Cmp((a, b) => a >= b) }, }; + // Membership in a set of numbers and divisibility, with no truth value where evaluation has none. + // https://github.com/asc-community/AngouriMath/issues/2195 + [ConstantField] private static readonly MethodInfo complexMembership = + ((MethodCallExpression)((Expression>)((z, set) => AngouriMath.Numerics.NumberStatements.In(z, set))).Body).Method; + [ConstantField] private static readonly MethodInfo realMembership = + ((MethodCallExpression)((Expression>)((x, set) => AngouriMath.Numerics.NumberStatements.In(x, set))).Body).Method; + [ConstantField] private static readonly MethodInfo complexDivisibility = + ((MethodCallExpression)((Expression>)((a, b) => AngouriMath.Numerics.NumberStatements.Divides(a, b))).Body).Method; + [ConstantField] private static readonly MethodInfo realDivisibility = + ((MethodCallExpression)((Expression>)((a, b) => AngouriMath.Numerics.NumberStatements.Divides(a, b))).Body).Method; + + /// + /// Membership of a compiled element in a set of numbers, over where the + /// element is one and over otherwise. + /// + internal Expression ConvertMembership(Expression element, AngouriMath.Numerics.NumberStatements.NumberSet set) + { + var widened = ShouldBeAtLeastDouble(element); + return widened.Type == typeof(System.Numerics.Complex) + ? Expression.Call(complexMembership, widened, Expression.Constant((int)set)) + : Expression.Call(realMembership, ConvertType(widened, typeof(double)), Expression.Constant((int)set)); + } + [ConstantField] private static readonly MethodInfo complexRemainder = ((MethodCallExpression)((Expression>)((a, b) => RealOnly.Mod(a, b))).Body).Method; diff --git a/Sources/AngouriMath/Functions/Compilation/IntoLinq/IntoLinqCompiler.cs b/Sources/AngouriMath/Functions/Compilation/IntoLinq/IntoLinqCompiler.cs index fb9665c1c..5b8b3ed96 100644 --- a/Sources/AngouriMath/Functions/Compilation/IntoLinq/IntoLinqCompiler.cs +++ b/Sources/AngouriMath/Functions/Compilation/IntoLinq/IntoLinqCompiler.cs @@ -30,6 +30,7 @@ internal static TDelegate Compile( // Only expressions that mention a matrix pay for this. if (expr.Nodes.Any(node => node is Entity.Matrix)) expr = expr.InnerSimplified; + expr = CompiledMembership.Rewritten(expr); var subexpressionsCache = typesAndNames.ToDictionary(c => (Entity)c.variable, c => Expression.Parameter(c.type)); var functionArguments = subexpressionsCache.Select(c => c.Value).ToArray(); // copying @@ -95,6 +96,12 @@ IUnaryNode oneArg BuildTree(oneArg.NodeChild, cachedSubexpressions, variableAssignments, newLocalVars, protocol), expr), + // A membership in a set of numbers, the set not being a value: only the element is computed. + // https://github.com/asc-community/AngouriMath/issues/2195 + Set.Inf(var element, Set.SpecialSet special) when AngouriMath.Numerics.NumberStatements.Of(special) is { } set + => protocol.ConvertMembership( + BuildTree(element, cachedSubexpressions, variableAssignments, newLocalVars, protocol), set), + IBinaryNode twoArg => protocol.ConvertBinaryNode( BuildTree(twoArg.NodeFirstChild, cachedSubexpressions, variableAssignments, newLocalVars, protocol), diff --git a/Sources/AngouriMath/Numerics/NumberStatements.cs b/Sources/AngouriMath/Numerics/NumberStatements.cs new file mode 100644 index 000000000..ba79475b4 --- /dev/null +++ b/Sources/AngouriMath/Numerics/NumberStatements.cs @@ -0,0 +1,89 @@ +// +// 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; +using AngouriMath.Functions; +using PeterO.Numbers; +using NumericsComplex = System.Numerics.Complex; + +namespace AngouriMath.Numerics +{ + /// + /// Membership in the sets of numbers and divisibility, for the compilers, on the terms evaluation decides + /// them: no truth value at NaN, an infinity in RR and CC as evaluation has it and in + /// none of the sets of whole numbers, and primality left open past the machine word, where the interpreter + /// leaves it. Divisibility is a statement about whole numbers, and has no truth value at any other. + /// https://github.com/asc-community/AngouriMath/issues/2195 + /// + internal static class NumberStatements + { + /// The sets of numbers membership in which compiles, as FastExpression's instruction names them. + internal enum NumberSet { Complexes, Reals, Integers, PositiveIntegers, NonNegativeIntegers, Primes } + + /// + /// The set of numbers is, or for one membership in which does + /// not compile: QQ, of which every finite double is a member while pi in QQ is + /// False, and BB, whose members are not numbers. + /// + internal static NumberSet? Of(Entity.Set.SpecialSet set) => set switch + { + Entity.Set.SpecialSet.Complexes => NumberSet.Complexes, + Entity.Set.SpecialSet.Reals => NumberSet.Reals, + Entity.Set.SpecialSet.Integers => NumberSet.Integers, + Entity.Set.SpecialSet.PositiveIntegers => NumberSet.PositiveIntegers, + Entity.Set.SpecialSet.NonNegativeIntegers => NumberSet.NonNegativeIntegers, + Entity.Set.SpecialSet.Primes => NumberSet.Primes, + _ => null + }; + + // 2^63: a whole double below it is a long, and the trial division the interpreter decides primality by + // takes it. + private const double PastTheWord = 9223372036854775808.0; + + /// Whether is in the set of numbers , a . + public static bool? In(NumericsComplex z, int set) + { + if (double.IsNaN(z.Real) || double.IsNaN(z.Imaginary)) + return null; + // +oo and -oo are reals as evaluation holds them, which #1019 item 15 takes up for v3: + // https://github.com/asc-community/AngouriMath/issues/2220 + switch ((NumberSet)set) + { + case NumberSet.Complexes: + return true; + case NumberSet.Reals: + return z.Imaginary == 0; + } + if (z.Imaginary != 0 || double.IsInfinity(z.Real) || Math.Floor(z.Real) != z.Real) + return false; + return (NumberSet)set switch + { + NumberSet.Integers => true, + NumberSet.PositiveIntegers => z.Real > 0, + NumberSet.NonNegativeIntegers => z.Real >= 0, + _ => z.Real < 2 ? false : z.Real >= PastTheWord ? null : Primes.IsPrime(EInteger.FromInt64((long)z.Real)) + }; + } + + public static bool? In(double x, int set) => In(new NumericsComplex(x, 0), set); + + /// + /// Whether divides : for whole numbers, whether the + /// remainder is 0, and 0 dividing only 0; for any other, no truth value. + /// + public static bool? Divides(NumericsComplex divisor, NumericsComplex dividend) + => IsWhole(divisor) && IsWhole(dividend) + ? divisor.Real == 0 ? dividend.Real == 0 : dividend.Real % divisor.Real == 0 + : null; + + public static bool? Divides(double divisor, double dividend) => Divides(new NumericsComplex(divisor, 0), new NumericsComplex(dividend, 0)); + + // A double's remainder by another is exact, so a whole one is divided exactly however large. + private static bool IsWhole(NumericsComplex z) + => z.Imaginary == 0 && !double.IsNaN(z.Real) && !double.IsInfinity(z.Real) && Math.Floor(z.Real) == z.Real; + } +} diff --git a/Sources/Tests/AotSmokeTest/Program.cs b/Sources/Tests/AotSmokeTest/Program.cs index e1df0093f..df1c7b02e 100644 --- a/Sources/Tests/AotSmokeTest/Program.cs +++ b/Sources/Tests/AotSmokeTest/Program.cs @@ -320,6 +320,12 @@ 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)}"); + // 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); + Check("compiled membership and divisibility at 7, and at 6 and i", + fastMembership.Call(7) == 2 && linqMembership(6) == true && linqMembership(Complex.ImaginaryOne) == null, + $"{fastMembership.Call(7)}, {linqMembership(6)}, {linqMembership(Complex.ImaginaryOne)}"); // FastExpression: a statement is its truth value, and a piecewise the case that holds. var fastPiecewise = "piecewise(x provided x > 0, -x)".Compile(x); var fastStatement = "x > 1 or x < 0".Compile(x); diff --git a/Sources/Tests/UnitTests/Common/MembershipCompilationTest.cs b/Sources/Tests/UnitTests/Common/MembershipCompilationTest.cs new file mode 100644 index 000000000..bc714c4c4 --- /dev/null +++ b/Sources/Tests/UnitTests/Common/MembershipCompilationTest.cs @@ -0,0 +1,134 @@ +// +// 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.Linq; +using System.Numerics; +using AngouriMath; +using AngouriMath.Core.Exceptions; +using AngouriMath.Extensions; +using Xunit; + +namespace AngouriMath.Tests.Common +{ + /// + /// A membership and a divisibility compile, in FastExpression and in the LINQ compiler, to the truth value + /// evaluation gives: 1 or where it holds, 0 or where it does + /// not, and NaN or where it has none. Both were refused. + /// #2195 + /// + [Trait("Area", "Common")] + public sealed class MembershipCompilationTest + { + private static readonly string[] points = + { "-3", "-1", "-1/2", "0", "1/2", "1", "2", "5/2", "3", "5", "7", "10", "15", "97", "100", "i", "1 + i", "2i", "+oo", "-oo" }; + + private static Complex Numeric(string value) => ((Entity.Number.Complex)value.ToEntity().Evaled).ToNumerics(); + + // The truth value evaluation gives, or nothing where it leaves the statement as written. + private static bool? Evaluated(Entity statement, out bool decided) + { + var evaluated = statement.Evaled; + decided = evaluated is Entity.Boolean || evaluated.IsNaN; + return evaluated is Entity.Boolean truth ? (bool)truth : null; + } + + private static bool? Truth(Complex value) => value == 1 ? true : value == 0 ? false : null; + + private static void Compare(string statement, string[] xs, string[] ys) + { + var fast = statement.ToEntity().Compile("x", "y"); + var linq = statement.ToEntity().Compile("x", "y"); + var real = statement.ToEntity().Compile("x", "y"); + var compared = 0; + foreach (var a in xs) + foreach (var b in ys) + { + var expected = Evaluated(statement.ToEntity().Substitute("x", a).Substitute("y", b), out var decided); + if (!decided) + continue; + var (x, y) = (Numeric(a), Numeric(b)); + Assert.True(expected == Truth(fast.Call(x, y)), $"{statement} at ({a}, {b}): {expected?.ToString() ?? "none"} evaluated, {fast.Call(x, y)} by FastExpression"); + Assert.True(expected == linq(x, y), $"{statement} at ({a}, {b}): {expected?.ToString() ?? "none"} evaluated, {linq(x, y)?.ToString() ?? "none"} by LINQ"); + if (x.Imaginary == 0 && y.Imaginary == 0) + Assert.True(expected == real(x.Real, y.Real), $"{statement} at ({a}, {b}): {expected?.ToString() ?? "none"} evaluated, {real(x.Real, y.Real)?.ToString() ?? "none"} over double"); + compared++; + } + Assert.True(compared >= xs.Length * ys.Length * 3 / 4, $"only {compared} of {xs.Length * ys.Length} of {statement} decided"); + } + + [Theory] + [InlineData("[0; 1]")] + [InlineData("(0; 1]")] + [InlineData("[-2; 3)")] + [InlineData("(-oo; 0)")] + [InlineData("[0; +oo)")] + [InlineData("{ 1, 2, 3 }")] + [InlineData("{ }")] + [InlineData("{ i, 2 }")] + [InlineData("RR")] + [InlineData("CC")] + [InlineData("ZZ")] + [InlineData("ZZ+")] + [InlineData("ZZ*")] + [InlineData("PP")] + [InlineData("[0; 1] \\/ { 5 }")] + [InlineData("ZZ /\\ [0; 10]")] + [InlineData("RR \\ ZZ")] + [InlineData("PP \\/ [10; 20]")] + [InlineData("(PP /\\ [0; 100]) \\ { 2 }")] + [InlineData("{ t : abs(t) < 2 }")] + [InlineData("{ t : t in ZZ and t > 2 }")] + public void AMembershipIsWhatEvaluationSays(string set) + => Compare($"x in ({set}) and y = 0", points, new[] { "0" }); + + [Fact] + public void ADivisibilityIsWhatEvaluationSays() + => Compare("x divides y", + new[] { "-3", "-2", "0", "1", "2", "3", "7", "1/2", "i", "5/2" }, + new[] { "0", "6", "-6", "7", "12", "5/2", "2i", "100000000000000000000", "+oo" }); + + [Theory] + [InlineData("iverson(x in ZZ) + y")] + [InlineData("piecewise(x provided x in [0; 1], -x) + y")] + [InlineData("x in [0; 1] or 3 divides x + y")] + [InlineData("not x in PP and y = 0")] + public void AMembershipInAnExpressionIsWhatEvaluationSays(string expression) + { + var fast = expression.ToEntity().Compile("x", "y"); + // Not at an infinity, where a compiled -x has a NaN imaginary part: + // https://github.com/asc-community/AngouriMath/issues/2222 + foreach (var a in points.Where(point => !point.Contains("oo"))) + { + var evaluated = expression.ToEntity().Substitute("x", a).Substitute("y", 0).Evaled; + var value = fast.Call(Numeric(a), 0); + if (evaluated is Entity.Boolean truth) + Assert.True(Truth(value) == (bool)truth, $"{expression} at {a}: {truth} evaluated, {value} by FastExpression"); + else if (evaluated is Entity.Number.Complex number) + { + var expected = number.ToNumerics(); + Assert.True(double.IsNaN(expected.Real) ? double.IsNaN(value.Real) : expected == value, + $"{expression} at {a}: {expected} evaluated, {value} by FastExpression"); + } + } + } + + /// + /// Every finite double is a rational, and pi in QQ is False, so a membership in QQ has no + /// compiled form; nor has one in BB, whose members are not numbers. + /// + [Theory] + [InlineData("x in QQ")] + [InlineData("x in BB")] + [InlineData("x in ([0; 1] \\/ QQ)")] + [InlineData("x in (QQ /\\ [0; 1])")] + public void AMembershipInAnyOtherSetIsRefused(string statement) + { + Assert.Throws(() => statement.ToEntity().Compile("x")); + Assert.Throws(() => statement.ToEntity().Compile("x")); + } + } +}