From 1fdf4901ca10b85cb4d45710df27e722f2109443 Mon Sep 17 00:00:00 2001 From: Rafael Vuijk Date: Sat, 31 Oct 2026 11:52:04 +0000 Subject: [PATCH 1/2] Compiling over the reals can refuse what is not real Compiled into a function returning double or float, every value computed on the way is real, so sin(arcsin(x)) at 2, which is 2, is NaN, as 1/x is at 0 where there is no value. That is the behaviour, and it is now written on every Compile overload. MathS.Settings.RefuseNonRealCompilation, off by default, refuses such an expression instead: with it on, an expression that cannot be shown real wherever it is defined throws UncompilableNodeException, so a NaN from the compiled function means only that there is no value. The showing is TreeAnalyzer.IsRealValued, with each argument written as one fresh variable, and it is conservative: sqrt(x) and ln(x) are refused too. The Complex compilers and statements are unaffected. IsRealValued now reads floor, ceil, round, min and max of real arguments as real, which the setting needs and which also answers the derivative of a modulus or a sign of one: d/dx abs(floor(x)) was left unevaluated. limit(sin(floor(x)) / x, x, +oo) is 0 again, as in 2.6.0: #2284 took away the l'Hopital route that had answered it. RefuseNonRealCompilationTest takes the default NaN, the refusals over double and float, what is real still compiled, a division by zero still NaN, and the Complex compiler and a statement unaffected. PublicApi.txt gains the setting. The suite, the trio, F#, AOT and the eight harnesses pass. Fixes #2286. Co-Authored-By: Claude Opus 5.5 (1M context) --- BREAKING-CHANGES.md | 18 +++++ Sources/.editorconfig | 3 + .../Convenience/CompilationExtensions.cs | 45 +++++++++++ Sources/AngouriMath/Convenience/MathS.cs | 37 +++++++++ .../Compilation/Compile.Linq.Definition.cs | 45 +++++++++++ .../Compilation/IntoLinq/IntoLinqCompiler.cs | 21 +++++ .../Functions/TreeAnalyzer/RealValued.cs | 8 ++ Sources/Tests/UnitTests/Common/PublicApi.txt | 1 + .../Common/RefuseNonRealCompilationTest.cs | 77 +++++++++++++++++++ 9 files changed, 255 insertions(+) create mode 100644 Sources/Tests/UnitTests/Common/RefuseNonRealCompilationTest.cs diff --git a/BREAKING-CHANGES.md b/BREAKING-CHANGES.md index 2212639e4..7a602ae02 100644 --- a/BREAKING-CHANGES.md +++ b/BREAKING-CHANGES.md @@ -607,6 +607,24 @@ 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` | +### Compiling over the reals can refuse what is not real + +**Not silent, and only where asked for: `MathS.Settings.RefuseNonRealCompilation`, off by default, refuses to compile into a +function returning `double` or `float` an expression that is not real wherever it is defined.** Over the reals every value +computed on the way is real, so `sin(arcsin(x))` at 2, which is 2, is NaN there, as `1/x` is at 0. That is now documented +on every `Compile` overload, and with the setting on, such an expression throws `UncompilableNodeException`, so that a NaN +means only that there is no value. The check is structural and conservative: `sqrt(x)` and `ln(x)` are refused too. + +**Silent: `floor`, `ceil`, `round`, `min` and `max` of real arguments are known to be real**, which the derivative of a +modulus or a sign needs, so these are answered where they were left unevaluated +([#2286](https://github.com/asc-community/AngouriMath/issues/2286)). + +| Input | Was (2.6.0) | Now | +|---|---|---| +| `"x^y".Compile("x", "y")` under the setting | (no setting) | throws `UncompilableNodeException` | +| `"derivative(abs(floor(x)), x)".ToEntity().Simplify()` | unevaluated | `0 provided not floor(x) = 0 and not x in ZZ` | +| `"derivative(abs(min(x, 1)), x)".ToEntity().Simplify()` | unevaluated | `sgn(min(x, 1)) * (1 - sgn(x - 1)) / 2 provided not min(x, 1) = 0 and not x - 1 = 0` | + ### Membership and divisibility compile **Not silent: what was refused is compiled.** `x in S` and `a divides b` were refused by both compilers with diff --git a/Sources/.editorconfig b/Sources/.editorconfig index f19e17474..d3925edc9 100644 --- a/Sources/.editorconfig +++ b/Sources/.editorconfig @@ -1013,5 +1013,8 @@ file_header_template=\nCopyright (c) 2019-2026 Angouri.\nAngouriMath is licensed [Tests/UnitTests/Common/DivisionByZeroOverDoubleTest.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/RefuseNonRealCompilationTest.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/BranchCutCompilationTest.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/Convenience/CompilationExtensions.cs b/Sources/AngouriMath/Convenience/CompilationExtensions.cs index 5e4b42e48..8a0b07f3d 100644 --- a/Sources/AngouriMath/Convenience/CompilationExtensions.cs +++ b/Sources/AngouriMath/Convenience/CompilationExtensions.cs @@ -53,6 +53,11 @@ public static partial class AngouriMathExtensions /// 3 /// /// + /// + /// Into a function that returns or , a value that is not real is NaN, and + /// so is every value computed from one, though the expression may be real there: sin(arcsin(x)) at 2. + /// refuses such an expression instead. + /// public static TDelegate Compile(this string @this, CompilationProtocol protocol, Type returnType, IEnumerable<(Type type, Variable variable)> typesAndNames) where TDelegate : Delegate => IntoLinqCompiler.Compile(@this, returnType, protocol, typesAndNames); @@ -86,6 +91,11 @@ public static TDelegate Compile(this string @this, CompilationProtoco /// 10 /// /// + /// + /// Into a function that returns or , a value that is not real is NaN, and + /// so is every value computed from one, though the expression may be real there: sin(arcsin(x)) at 2. + /// refuses such an expression instead. + /// public static Func Compile(this string @this, Variable var1) => IntoLinqCompiler.Compile>(@this, typeof(TOut), new(), new[] { (typeof(TIn1), var1) }); @@ -126,6 +136,11 @@ public static Func Compile(this string @this, Variable v /// 30 /// /// + /// + /// Into a function that returns or , a value that is not real is NaN, and + /// so is every value computed from one, though the expression may be real there: sin(arcsin(x)) at 2. + /// refuses such an expression instead. + /// public static Func Compile(this string @this, Variable var1, Variable var2) => IntoLinqCompiler.Compile>(@this, typeof(TOut), new(), new[] { (typeof(TIn1), var1), (typeof(TIn2), var2) }); @@ -172,6 +187,11 @@ public static Func Compile(this string @this /// 60 /// /// + /// + /// Into a function that returns or , a value that is not real is NaN, and + /// so is every value computed from one, though the expression may be real there: sin(arcsin(x)) at 2. + /// refuses such an expression instead. + /// public static Func Compile(this string @this, Variable var1, Variable var2, Variable var3) => IntoLinqCompiler.Compile>(@this, typeof(TOut), new(), new[] { (typeof(TIn1), var1), (typeof(TIn2), var2) , (typeof(TIn3), var3) }); @@ -224,6 +244,11 @@ public static Func Compile(this /// 100 /// /// + /// + /// Into a function that returns or , a value that is not real is NaN, and + /// so is every value computed from one, though the expression may be real there: sin(arcsin(x)) at 2. + /// refuses such an expression instead. + /// public static Func Compile(this string @this, Variable var1, Variable var2, Variable var3, Variable var4) => IntoLinqCompiler.Compile>(@this, typeof(TOut), new(), new[] { (typeof(TIn1), var1), (typeof(TIn2), var2) , (typeof(TIn3), var3) , (typeof(TIn4), var4) }); @@ -282,6 +307,11 @@ public static Func Compile /// + /// + /// Into a function that returns or , a value that is not real is NaN, and + /// so is every value computed from one, though the expression may be real there: sin(arcsin(x)) at 2. + /// refuses such an expression instead. + /// public static Func Compile(this string @this, Variable var1, Variable var2, Variable var3, Variable var4, Variable var5) => IntoLinqCompiler.Compile>(@this, typeof(TOut), new(), new[] { (typeof(TIn1), var1), (typeof(TIn2), var2) , (typeof(TIn3), var3) , (typeof(TIn4), var4) , (typeof(TIn5), var5) }); @@ -346,6 +376,11 @@ public static Func Compile /// + /// + /// Into a function that returns or , a value that is not real is NaN, and + /// so is every value computed from one, though the expression may be real there: sin(arcsin(x)) at 2. + /// refuses such an expression instead. + /// public static Func Compile(this string @this, Variable var1, Variable var2, Variable var3, Variable var4, Variable var5, Variable var6) => IntoLinqCompiler.Compile>(@this, typeof(TOut), new(), new[] { (typeof(TIn1), var1), (typeof(TIn2), var2) , (typeof(TIn3), var3) , (typeof(TIn4), var4) , (typeof(TIn5), var5) , (typeof(TIn6), var6) }); @@ -416,6 +451,11 @@ public static Func Compile /// + /// + /// Into a function that returns or , a value that is not real is NaN, and + /// so is every value computed from one, though the expression may be real there: sin(arcsin(x)) at 2. + /// refuses such an expression instead. + /// public static Func Compile(this string @this, Variable var1, Variable var2, Variable var3, Variable var4, Variable var5, Variable var6, Variable var7) => IntoLinqCompiler.Compile>(@this, typeof(TOut), new(), new[] { (typeof(TIn1), var1), (typeof(TIn2), var2) , (typeof(TIn3), var3) , (typeof(TIn4), var4) , (typeof(TIn5), var5) , (typeof(TIn6), var6) , (typeof(TIn7), var7) }); @@ -492,6 +532,11 @@ public static Func Compile /// + /// + /// Into a function that returns or , a value that is not real is NaN, and + /// so is every value computed from one, though the expression may be real there: sin(arcsin(x)) at 2. + /// refuses such an expression instead. + /// public static Func Compile(this string @this, Variable var1, Variable var2, Variable var3, Variable var4, Variable var5, Variable var6, Variable var7, Variable var8) => IntoLinqCompiler.Compile>(@this, typeof(TOut), new(), new[] { (typeof(TIn1), var1), (typeof(TIn2), var2) , (typeof(TIn3), var3) , (typeof(TIn4), var4) , (typeof(TIn5), var5) , (typeof(TIn6), var6) , (typeof(TIn7), var7) , (typeof(TIn8), var8) }); diff --git a/Sources/AngouriMath/Convenience/MathS.cs b/Sources/AngouriMath/Convenience/MathS.cs index c1537e472..2cfee6d72 100644 --- a/Sources/AngouriMath/Convenience/MathS.cs +++ b/Sources/AngouriMath/Convenience/MathS.cs @@ -6285,6 +6285,43 @@ internal static EDecimal DowncastingTolerance /// public static Setting AllowNewton { get; } = true; + /// + /// Whether compiling into a function that returns or refuses an + /// expression that is not real wherever it is defined. Off by default. + /// + /// + /// Compiled over the reals, every value the function computes on the way is a real number. So an expression + /// whose value at a real argument is not real, or is real only by way of a value that is not -- sqrt(x) + /// at -4, sin(arcsin(x)) at 2 -- evaluates to NaN there, as it does where it has no value at all, as + /// 1/x at 0. With this off, that is how such an expression compiles, and a NaN from the compiled + /// function means that the expression has no value there or that its computation left the reals. With it on, + /// an expression that cannot be shown to be real wherever it is defined is refused with + /// , and a NaN means only that there is no value. The + /// showing is structural and so conservative: sqrt(x) and ln(x) are refused, though both are + /// real for positive x. Compile over for the value itself. + /// #2286 + /// + /// + /// + /// using static AngouriMath.MathS; + /// + /// Console.WriteLine("sin(arcsin(x))".Compile<double, double>("x")(2)); + /// + /// using var _ = Settings.RefuseNonRealCompilation.Set(true); + /// + /// Console.WriteLine("x / (1 + x^2)".Compile<double, double>("x")(2)); + /// try { "sin(arcsin(x))".Compile<double, double>("x"); } + /// catch (AngouriMath.Core.Exceptions.UncompilableNodeException) { Console.WriteLine("refused"); } + /// + /// Prints + /// + /// NaN + /// 0.4 + /// refused + /// + /// + public static Setting RefuseNonRealCompilation { get; } = false; + /// /// Criteria for simplifier so you could control which expressions are considered easier by you. /// The higher the value - the more complicated an expression is. diff --git a/Sources/AngouriMath/Functions/Compilation/Compile.Linq.Definition.cs b/Sources/AngouriMath/Functions/Compilation/Compile.Linq.Definition.cs index 6237b3a93..c4c3ef572 100644 --- a/Sources/AngouriMath/Functions/Compilation/Compile.Linq.Definition.cs +++ b/Sources/AngouriMath/Functions/Compilation/Compile.Linq.Definition.cs @@ -37,6 +37,11 @@ partial record Entity /// /// Returnes a natively compiled expression of type /// + /// + /// Into a function that returns or , a value that is not real is NaN, and + /// so is every value computed from one, though the expression may be real there: sin(arcsin(x)) at 2. + /// refuses such an expression instead. + /// public TDelegate Compile(CompilationProtocol protocol, Type returnType, IEnumerable<(Type type, Variable variable)> typesAndNames) where TDelegate : Delegate => IntoLinqCompiler.Compile(this, returnType, protocol, typesAndNames); @@ -57,6 +62,11 @@ public TDelegate Compile(CompilationProtocol protocol, Type returnTyp /// /// Returns a natively-compiled delegate /// + /// + /// Into a function that returns or , a value that is not real is NaN, and + /// so is every value computed from one, though the expression may be real there: sin(arcsin(x)) at 2. + /// refuses such an expression instead. + /// public Func Compile(Variable var1) => IntoLinqCompiler.Compile>(this, typeof(TOut), new(), new[] { (typeof(TIn1), var1) }); @@ -84,6 +94,11 @@ public Func Compile(Variable var1) /// /// Returns a natively-compiled delegate /// + /// + /// Into a function that returns or , a value that is not real is NaN, and + /// so is every value computed from one, though the expression may be real there: sin(arcsin(x)) at 2. + /// refuses such an expression instead. + /// public Func Compile(Variable var1, Variable var2) => IntoLinqCompiler.Compile>(this, typeof(TOut), new(), new[] { (typeof(TIn1), var1), (typeof(TIn2), var2) }); @@ -117,6 +132,11 @@ public Func Compile(Variable var1, Variable /// /// Returns a natively-compiled delegate /// + /// + /// Into a function that returns or , a value that is not real is NaN, and + /// so is every value computed from one, though the expression may be real there: sin(arcsin(x)) at 2. + /// refuses such an expression instead. + /// public Func Compile(Variable var1, Variable var2, Variable var3) => IntoLinqCompiler.Compile>(this, typeof(TOut), new(), new[] { (typeof(TIn1), var1), (typeof(TIn2), var2) , (typeof(TIn3), var3) }); @@ -156,6 +176,11 @@ public Func Compile(Variable var /// /// Returns a natively-compiled delegate /// + /// + /// Into a function that returns or , a value that is not real is NaN, and + /// so is every value computed from one, though the expression may be real there: sin(arcsin(x)) at 2. + /// refuses such an expression instead. + /// public Func Compile(Variable var1, Variable var2, Variable var3, Variable var4) => IntoLinqCompiler.Compile>(this, typeof(TOut), new(), new[] { (typeof(TIn1), var1), (typeof(TIn2), var2) , (typeof(TIn3), var3) , (typeof(TIn4), var4) }); @@ -201,6 +226,11 @@ public Func Compile( /// /// Returns a natively-compiled delegate /// + /// + /// Into a function that returns or , a value that is not real is NaN, and + /// so is every value computed from one, though the expression may be real there: sin(arcsin(x)) at 2. + /// refuses such an expression instead. + /// public Func Compile(Variable var1, Variable var2, Variable var3, Variable var4, Variable var5) => IntoLinqCompiler.Compile>(this, typeof(TOut), new(), new[] { (typeof(TIn1), var1), (typeof(TIn2), var2) , (typeof(TIn3), var3) , (typeof(TIn4), var4) , (typeof(TIn5), var5) }); @@ -252,6 +282,11 @@ public Func Compile /// Returns a natively-compiled delegate /// + /// + /// Into a function that returns or , a value that is not real is NaN, and + /// so is every value computed from one, though the expression may be real there: sin(arcsin(x)) at 2. + /// refuses such an expression instead. + /// public Func Compile(Variable var1, Variable var2, Variable var3, Variable var4, Variable var5, Variable var6) => IntoLinqCompiler.Compile>(this, typeof(TOut), new(), new[] { (typeof(TIn1), var1), (typeof(TIn2), var2) , (typeof(TIn3), var3) , (typeof(TIn4), var4) , (typeof(TIn5), var5) , (typeof(TIn6), var6) }); @@ -309,6 +344,11 @@ public Func Compile /// Returns a natively-compiled delegate /// + /// + /// Into a function that returns or , a value that is not real is NaN, and + /// so is every value computed from one, though the expression may be real there: sin(arcsin(x)) at 2. + /// refuses such an expression instead. + /// public Func Compile(Variable var1, Variable var2, Variable var3, Variable var4, Variable var5, Variable var6, Variable var7) => IntoLinqCompiler.Compile>(this, typeof(TOut), new(), new[] { (typeof(TIn1), var1), (typeof(TIn2), var2) , (typeof(TIn3), var3) , (typeof(TIn4), var4) , (typeof(TIn5), var5) , (typeof(TIn6), var6) , (typeof(TIn7), var7) }); @@ -372,6 +412,11 @@ public Func Compile /// Returns a natively-compiled delegate /// + /// + /// Into a function that returns or , a value that is not real is NaN, and + /// so is every value computed from one, though the expression may be real there: sin(arcsin(x)) at 2. + /// refuses such an expression instead. + /// public Func Compile(Variable var1, Variable var2, Variable var3, Variable var4, Variable var5, Variable var6, Variable var7, Variable var8) => IntoLinqCompiler.Compile>(this, typeof(TOut), new(), new[] { (typeof(TIn1), var1), (typeof(TIn2), var2) , (typeof(TIn3), var3) , (typeof(TIn4), var4) , (typeof(TIn5), var5) , (typeof(TIn6), var6) , (typeof(TIn7), var7) , (typeof(TIn8), var8) }); diff --git a/Sources/AngouriMath/Functions/Compilation/IntoLinq/IntoLinqCompiler.cs b/Sources/AngouriMath/Functions/Compilation/IntoLinq/IntoLinqCompiler.cs index 9ac042250..e6a981f99 100644 --- a/Sources/AngouriMath/Functions/Compilation/IntoLinq/IntoLinqCompiler.cs +++ b/Sources/AngouriMath/Functions/Compilation/IntoLinq/IntoLinqCompiler.cs @@ -32,6 +32,10 @@ internal static TDelegate Compile( expr = expr.InnerSimplified; expr = CompiledMembership.Rewritten(expr); StatementPositions.Check(expr); + if (MathS.Settings.RefuseNonRealCompilation && returnType is { } real && IsRealType(real) + && typesAndNames.All(c => IsRealType(c.type)) && !IsRealWhereverDefined(expr, typesAndNames)) + throw new UncompilableNodeException( + $"`{expr.Stringize()}` is not real wherever it is defined, and MathS.Settings.RefuseNonRealCompilation is on: compile it over Complex"); var subexpressionsCache = typesAndNames.ToDictionary(c => (Entity)c.variable, c => Expression.Parameter(c.type)); var functionArguments = subexpressionsCache.Select(c => c.Value).ToArray(); // copying @@ -68,6 +72,23 @@ or NotSupportedException } } + private static bool IsRealType(Type type) + => type == typeof(double) || type == typeof(float) || type == typeof(int) || type == typeof(long) + || type == typeof(System.Numerics.BigInteger); + + /// + /// Whether is real wherever it is defined with every argument real, by + /// . That reads one variable as real, and reads its shapes + /// alone and never the values, so each argument is written as one fresh variable and its verdict is the + /// verdict for them all. https://github.com/asc-community/AngouriMath/issues/2286 + /// + private static bool IsRealWhereverDefined(Entity expr, IEnumerable<(Type type, Variable variable)> typesAndNames) + { + var real = Variable.CreateTemp(expr.Vars); + var renamed = typesAndNames.Aggregate(expr, (e, argument) => e.Substitute(argument.variable, real)); + return Functions.TreeAnalyzer.IsRealValued(renamed, real); + } + internal static Expression BuildTree( Entity expr, Dictionary cachedSubexpressions, diff --git a/Sources/AngouriMath/Functions/TreeAnalyzer/RealValued.cs b/Sources/AngouriMath/Functions/TreeAnalyzer/RealValued.cs index a13eb8c26..5345ef5ff 100644 --- a/Sources/AngouriMath/Functions/TreeAnalyzer/RealValued.cs +++ b/Sources/AngouriMath/Functions/TreeAnalyzer/RealValued.cs @@ -67,6 +67,14 @@ internal static bool IsRealValued(Entity expr, Variable x) or Arctanf or Arccotanf or Signumf or Erff or Erfcf or Erfif or Eif or Sif or Shif or Conjugatef: return IsRealValued(expr.DirectChildren[0], x); + // The integer below, above or nearest to a real number is real, and so is the larger or the smaller + // of two. https://github.com/asc-community/AngouriMath/issues/2286 + case Floorf or Ceilf or Roundf: + return IsRealValued(expr.DirectChildren[0], x); + case Maxf(var left, var right): + return IsRealValued(left, x) && IsRealValued(right, x); + case Minf(var left, var right): + return IsRealValued(left, x) && IsRealValued(right, x); // A polygamma function of a whole order is real on the real line between its poles. case Polygammaf(var order, var argument) when !order.ContainsNode(x): return IsRealValued(argument, x); diff --git a/Sources/Tests/UnitTests/Common/PublicApi.txt b/Sources/Tests/UnitTests/Common/PublicApi.txt index 935c06bcb..d4ab90591 100644 --- a/Sources/Tests/UnitTests/Common/PublicApi.txt +++ b/Sources/Tests/UnitTests/Common/PublicApi.txt @@ -3498,6 +3498,7 @@ AngouriMath.MathS+Settings.MaxExpansionTermCount { } : AngouriMath.Convenience.S AngouriMath.MathS+Settings.NewtonSolver { } : AngouriMath.Convenience.Setting AngouriMath.MathS+Settings.PrecisionErrorCommon { } : AngouriMath.Convenience.Setting AngouriMath.MathS+Settings.PrecisionErrorZeroRange { } : AngouriMath.Convenience.Setting +AngouriMath.MathS+Settings.RefuseNonRealCompilation { } : AngouriMath.Convenience.Setting AngouriMath.MathS+UnsafeAndInternal.AreEqualNumerically(AngouriMath.Entity, AngouriMath.Entity) : System.Boolean AngouriMath.MathS+UnsafeAndInternal.AreEqualNumerically(AngouriMath.Entity, AngouriMath.Entity, AngouriMath.Entity[]) : System.Boolean AngouriMath.MathS+UnsafeAndInternal.CheckPoints { } : AngouriMath.Convenience.Setting diff --git a/Sources/Tests/UnitTests/Common/RefuseNonRealCompilationTest.cs b/Sources/Tests/UnitTests/Common/RefuseNonRealCompilationTest.cs new file mode 100644 index 000000000..4fd932cf7 --- /dev/null +++ b/Sources/Tests/UnitTests/Common/RefuseNonRealCompilationTest.cs @@ -0,0 +1,77 @@ +// +// 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 +{ + /// + /// Compiled over , a value that is not real is NaN, and so is everything computed from it, though + /// the expression may be real there: sin(arcsin(x)) at 2 is NaN. That is documented, and + /// refuses such an expression instead, so that a NaN means only + /// that there is no value. + /// #2286 + /// + [Trait("Area", "Common")] + public sealed class RefuseNonRealCompilationTest + { + [Theory] + [InlineData("sin(arcsin(x))", 2)] + [InlineData("sqrt(x)^2", -4)] + public void ByDefaultAValueOffTheRealsIsNaN(string expression, double at) + => Assert.True(double.IsNaN(expression.Compile("x")(at))); + + [Theory] + [InlineData("sin(arcsin(x))")] + [InlineData("sqrt(x)")] + [InlineData("ln(x)")] + [InlineData("x^(1/3)")] + public void WhenRefusingWhatIsNotRealItIsRefused(string expression) + { + using var _ = MathS.Settings.RefuseNonRealCompilation.Set(true); + Assert.Throws(() => expression.Compile("x")); + Assert.Throws(() => expression.Compile("x")); + } + + [Fact] + public void WhenRefusingAPowerOfTwoArgumentsIsRefused() + { + using var _ = MathS.Settings.RefuseNonRealCompilation.Set(true); + Assert.Throws(() => "x^y".Compile("x", "y")); + } + + [Theory] + [InlineData("x / (1 + x^2)", 2, 0.4)] + [InlineData("floor(x) + max(x, 1)", 2.5, 4.5)] + [InlineData("abs(x) + e^x - e^x", -3, 3)] + [InlineData("x^3 - 2 * x", 2, 4)] + public void WhenRefusingWhatIsRealIsCompiled(string expression, double at, double value) + { + using var _ = MathS.Settings.RefuseNonRealCompilation.Set(true); + Assert.Equal(value, expression.Compile("x")(at), 12); + } + + [Fact] + public void WhenRefusingADivisionByZeroIsStillNaN() + { + using var _ = MathS.Settings.RefuseNonRealCompilation.Set(true); + Assert.True(double.IsNaN("1/x".Compile("x")(0))); + } + + [Fact] + public void WhenRefusingTheComplexCompilerAndAStatementAreUnaffected() + { + using var _ = MathS.Settings.RefuseNonRealCompilation.Set(true); + Assert.Equal(new Complex(2, 0), "sqrt(x)".Compile("x")(4)); + Assert.True("sqrt(x) > 0".Compile("x")(4)); + } + } +} From 7fdea1ab58ae418ce709c43680bcef5a52d667f3 Mon Sep 17 00:00:00 2001 From: Rafael Vuijk Date: Sat, 31 Oct 2026 12:24:36 +0000 Subject: [PATCH 2/2] Write the Compile remark into the templates the overloads come from CompilationExtensions.cs and Compile.Linq.Definition.cs are generated from their .tt templates, and the remark had been written into the output only, so regenerating dropped it and Generated Files Up To Date failed. regenerate_all.sh now reproduces the committed files exactly. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../AngouriMath/Convenience/CompilationExtensions.tt | 10 ++++++++++ .../Functions/Compilation/Compile.Linq.Definition.tt | 10 ++++++++++ 2 files changed, 20 insertions(+) diff --git a/Sources/AngouriMath/Convenience/CompilationExtensions.tt b/Sources/AngouriMath/Convenience/CompilationExtensions.tt index 4f9c27455..61c3a3804 100644 --- a/Sources/AngouriMath/Convenience/CompilationExtensions.tt +++ b/Sources/AngouriMath/Convenience/CompilationExtensions.tt @@ -53,6 +53,11 @@ namespace AngouriMath.Extensions /// 3 /// /// + /// + /// Into a function that returns or , a value that is not real is NaN, and + /// so is every value computed from one, though the expression may be real there: sin(arcsin(x)) at 2. + /// refuses such an expression instead. + /// public static TDelegate Compile(this string @this, CompilationProtocol protocol, Type returnType, IEnumerable<(Type type, Variable variable)> typesAndNames) where TDelegate : Delegate => IntoLinqCompiler.Compile(@this, returnType, protocol, typesAndNames); @@ -104,6 +109,11 @@ namespace AngouriMath.Extensions /// <#= total #> /// /// + /// + /// Into a function that returns or , a value that is not real is NaN, and + /// so is every value computed from one, though the expression may be real there: sin(arcsin(x)) at 2. + /// refuses such an expression instead. + /// public static Func<<# for(var t=1;t<=i;t++){ #>TIn<#= t #>, <# } #>TOut> Compile<<# for(var t=1;t<=i;t++){ #>TIn<#= t #>, <# } #>TOut>(this string @this, Variable var1<# for(var t=2; t<=i; t++){ #>, Variable var<#= t #><# } #>) => IntoLinqCompiler.CompileTIn<#= t #>, <# } #>TOut>>(@this, typeof(TOut), new(), new[] { (typeof(TIn1), var1)<# for(var t=2;t<=i;t++){ #>, (typeof(TIn<#= t #>), var<#= t #>) <# } #> }); diff --git a/Sources/AngouriMath/Functions/Compilation/Compile.Linq.Definition.tt b/Sources/AngouriMath/Functions/Compilation/Compile.Linq.Definition.tt index 5f41acc5b..aa58c4713 100644 --- a/Sources/AngouriMath/Functions/Compilation/Compile.Linq.Definition.tt +++ b/Sources/AngouriMath/Functions/Compilation/Compile.Linq.Definition.tt @@ -37,6 +37,11 @@ namespace AngouriMath /// /// Returnes a natively compiled expression of type /// + /// + /// Into a function that returns or , a value that is not real is NaN, and + /// so is every value computed from one, though the expression may be real there: sin(arcsin(x)) at 2. + /// refuses such an expression instead. + /// public TDelegate Compile(CompilationProtocol protocol, Type returnType, IEnumerable<(Type type, Variable variable)> typesAndNames) where TDelegate : Delegate => IntoLinqCompiler.Compile(this, returnType, protocol, typesAndNames); @@ -62,6 +67,11 @@ namespace AngouriMath /// /// Returns a natively-compiled delegate /// + /// + /// Into a function that returns or , a value that is not real is NaN, and + /// so is every value computed from one, though the expression may be real there: sin(arcsin(x)) at 2. + /// refuses such an expression instead. + /// public Func<<# for(var t=1;t<=i;t++){ #>TIn<#= t #>, <# } #>TOut> Compile<<# for(var t=1;t<=i;t++){ #>TIn<#= t #>, <# } #>TOut>(Variable var1<# for(var t=2; t<=i; t++){ #>, Variable var<#= t #><# } #>) => IntoLinqCompiler.CompileTIn<#= t #>, <# } #>TOut>>(this, typeof(TOut), new(), new[] { (typeof(TIn1), var1)<# for(var t=2;t<=i;t++){ #>, (typeof(TIn<#= t #>), var<#= t #>) <# } #> });