diff --git a/BREAKING-CHANGES.md b/BREAKING-CHANGES.md index 5215ce8da..23b93cb4b 100644 --- a/BREAKING-CHANGES.md +++ b/BREAKING-CHANGES.md @@ -47,6 +47,8 @@ read first. | **silent** | two integrals | answered correctly | unevaluated — a deliberate loss | | **silent** | `k/(a x^2 + c)` and `k/sqrt(a x^2 + c)` for symbolic `a` | `NaN` | a piecewise on the sign of the discriminant | | loud | `Compile` over a missing variable | `KeyNotFoundException` | `UncompilableNodeException` | +| loud | `Compile` of `floor`, `ceil`, `round`, `phi`, `gamma`, `!` | `AngouriBugException`, asking to be reported | `UncompilableNodeException` | +| loud | `Compile` of a boolean or lambda node as `double` | `InvalidOperationException` from Linq | `UncompilableNodeException` | | loud | `Expand` of a quotient of factorials | `AngouriBugException` | the expanded polynomial | | loud | parsing a `provided` in a parenthesised comma list | `NullReferenceException` | `UnhandledParseException` | | loud | `floor(x)`, `ceil(x)`, `ceiling(x)` | `UnrecognizedFunctionParseException` | the functions | @@ -244,6 +246,37 @@ when in fact it merely has another value — trading a wrong value for a wrong d asserts that the expression is left alone. Issue [#884](https://github.com/asc-community/AngouriMath/issues/884). +### `Compile` fails with its own exception rather than someone else's + +Two families, both reported as `UncompilableNodeException` — the exception +[`Docs/Usage/Exceptions.md`](Sources/AngouriMath/Docs/Usage/Exceptions.md) documents for a node with +no compiled form. Neither the node set nor the compiled output changes; only what is thrown when +compilation is impossible. + +| input, compiled as `` | was | is | +|---|---|---| +| `floor(x)`, `ceil(x)`, `round(x)`, `phi(x)`, `gamma(x)`, `x!` | `AngouriBugException`: *An unary node seems to be not added* | `UncompilableNodeException` | +| `not x`, `x and 2`, `x or 2`, `x xor 2`, `x implies 2`, `x -> x + 1` | `InvalidOperationException` from `System.Linq.Expressions` | `UncompilableNodeException` | +| `x provided 2` | `ArgumentException`: *Argument must be boolean* | `UncompilableNodeException` | + +`AngouriBugException` means "an internal error occurred, report it", and none of the first row is one +— the converter has no case for the node, which is a gap in coverage. **Four of those nodes are ones +2.0 added**: `floor`, `ceil` and `round` arrived with #809 and the compiler was never taught them, so +a caller compiling a 2.0 feature was asked to file a bug report. + +The second family matters for a caller who followed the documentation: `catch (AngouriMathBaseException)` +is what the exception reference tells you to write around a library call, and Linq's exceptions are +not under it, so those escaped the handler entirely. + +**What this does not do** is teach the compiler `floor`, `ceil`, `round` and the rest. That is worth +doing and is separate; this is only about the failure being honest rather than either a request for a +bug report or an exception nobody was told to expect. Issue +[#894](https://github.com/asc-community/AngouriMath/issues/894). + +Found by `crashcheck`, which runs each case in a child process so that a crash is a result rather +than the end of the run. On 1652 cases it reports 0 crashes and 0 hangs, and these 16 were every +remaining finding. + ### A logarithm behaves like the division it is defined as `log_b(z)` is `ln(z) / ln(b)`, and three answers did not follow from that. Every division by zero in diff --git a/Sources/AngouriMath/Functions/Compilation/IntoLinq/CompilationProtocol.cs b/Sources/AngouriMath/Functions/Compilation/IntoLinq/CompilationProtocol.cs index 9a18bca94..cdc54dc25 100644 --- a/Sources/AngouriMath/Functions/Compilation/IntoLinq/CompilationProtocol.cs +++ b/Sources/AngouriMath/Functions/Compilation/IntoLinq/CompilationProtocol.cs @@ -128,7 +128,17 @@ Arccosecantf when ShouldBeAtLeastDouble(e) is var newE => Expression.Call(GetDef Notf => Expression.Not(e), - _ => throw new AngouriBugException("An unary node seems to be not added") + // A node this converter has no case for has no compiled form, and that is a gap + // in coverage rather than an internal error. It threw AngouriBugException, which + // asks the caller to report a bug -- so compiling floor(x), ceil(x), round(x), + // x!, gamma(x) or phi(x) told the user to file an issue for something the + // library already knows it cannot do. UncompilableNodeException is the exception + // documented for exactly this, and #872 moved the other known gaps off + // AngouriBugException for the same reason. + // https://github.com/asc-community/AngouriMath/issues/894 + _ => throw new UncompilableNodeException( + $"There is no compiled form for {typeHolder.GetType().Name}. " + + "Define a CompilationProtocol which overrides ConvertUnaryNode to add one.") }; } diff --git a/Sources/AngouriMath/Functions/Compilation/IntoLinq/IntoLinqCompiler.cs b/Sources/AngouriMath/Functions/Compilation/IntoLinq/IntoLinqCompiler.cs index ac3996c5f..fb9665c1c 100644 --- a/Sources/AngouriMath/Functions/Compilation/IntoLinq/IntoLinqCompiler.cs +++ b/Sources/AngouriMath/Functions/Compilation/IntoLinq/IntoLinqCompiler.cs @@ -36,12 +36,29 @@ internal static TDelegate Compile( var localVars = new List(); var variableAssignments = new List(); - var tree = BuildTree(expr, subexpressionsCache, variableAssignments, localVars, protocol); - var treeWithLocals = Expression.Block(localVars, variableAssignments.Append(tree)); - Expression entireExpression = returnType is not null ? protocol.ConvertType(treeWithLocals, returnType) : treeWithLocals; - var finalLambda = Expression.Lambda(entireExpression, functionArguments); + // Linq.Expression refuses a mismatch by throwing its own exceptions -- an + // InvalidOperationException for `not x` where x is a double, an ArgumentException + // for a Providedf whose condition is not boolean -- and those were reaching the + // caller unwrapped, so a CAS raised exceptions outside its own documented hierarchy + // for input it simply cannot compile. The mismatch is real and the answer is the + // exception written down for it. + // https://github.com/asc-community/AngouriMath/issues/894 + try + { + var tree = BuildTree(expr, subexpressionsCache, variableAssignments, localVars, protocol); + var treeWithLocals = Expression.Block(localVars, variableAssignments.Append(tree)); + Expression entireExpression = returnType is not null ? protocol.ConvertType(treeWithLocals, returnType) : treeWithLocals; + var finalLambda = Expression.Lambda(entireExpression, functionArguments); - return finalLambda.Compile(); + return finalLambda.Compile(); + } + catch (Exception e) when (e is InvalidOperationException or ArgumentException + or NotSupportedException + && e is not AngouriMathBaseException) + { + throw new UncompilableNodeException( + $"`{expr.Stringize()}` has no compiled form for the types requested: {e.Message}"); + } } internal static Expression BuildTree( diff --git a/Sources/Tests/UnitTests/Core/UserInvalidExceptions.cs b/Sources/Tests/UnitTests/Core/UserInvalidExceptions.cs index 0d847312a..c1ce1d562 100644 --- a/Sources/Tests/UnitTests/Core/UserInvalidExceptions.cs +++ b/Sources/Tests/UnitTests/Core/UserInvalidExceptions.cs @@ -51,6 +51,38 @@ [Fact] public void UncompilableNode4() => Assert.Throws(() => "x + { x, x }".Compile("x")); + // https://github.com/asc-community/AngouriMath/issues/894 + // A node the Linq converter has no case for threw AngouriBugException, which asks the + // caller to report a bug -- for a gap in coverage the library already knows about. Four + // of these are nodes 2.0 added and never taught the compiler. + [Theory] + [InlineData("floor(x)")] + [InlineData("ceil(x)")] + [InlineData("round(x)")] + [InlineData("phi(x)")] + [InlineData("gamma(x)")] + [InlineData("x!")] + [InlineData("(x + 1)! / x!")] + public void ANodeWithNoCompiledFormSaysSoRatherThanAskingForABugReport(string expression) => + Assert.Throws(() => + expression.ToEntity().Compile("x")); + + // The same, for a mismatch Linq.Expression refuses rather than the converter: a boolean + // node has no double-valued compiled form, and the exception used to be + // System.Linq.Expressions' own, so `catch (AngouriMathBaseException)` -- which is what + // Docs/Usage/Exceptions.md tells a caller to write -- did not catch it. + [Theory] + [InlineData("not x")] + [InlineData("x and 2")] + [InlineData("x or 2")] + [InlineData("x xor 2")] + [InlineData("x implies 2")] + [InlineData("x provided 2")] + [InlineData("x -> x + 1")] + public void ATypeMismatchInTheCompiledFormStaysInsideTheLibrarysHierarchy(string expression) => + Assert.Throws(() => + expression.ToEntity().Compile("x")); + [Fact] public void CannotEvalNum1() => Assert.Throws(() => "x".EvalNumerical());