Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions BREAKING-CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand Down Expand Up @@ -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 `<double, double>` | 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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.")
};
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,12 +36,29 @@ internal static TDelegate Compile<TDelegate>(
var localVars = new List<ParameterExpression>();
var variableAssignments = new List<Expression>();

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<TDelegate>(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<TDelegate>(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(
Expand Down
32 changes: 32 additions & 0 deletions Sources/Tests/UnitTests/Core/UserInvalidExceptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,38 @@ [Fact] public void UncompilableNode4() =>
Assert.Throws<UncompilableNodeException>(() =>
"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<UncompilableNodeException>(() =>
expression.ToEntity().Compile<double, double>("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<UncompilableNodeException>(() =>
expression.ToEntity().Compile<double, double>("x"));

[Fact] public void CannotEvalNum1() =>
Assert.Throws<CannotEvalException>(() =>
"x".EvalNumerical());
Expand Down
Loading