Repository navigation
Document the exception hierarchy, and make a refusal name what it choked on (#746 item 11) - #1027
Merged
Merged
Conversation
`Docs/Usage/Exceptions.md` was three lines and covered one of the 25 types under `AngouriMathBaseException`. It now covers all of them: what each means, when a caller can expect it, and which ones are worth catching. The split the document is built around is the one a caller acts on -- `MathSException` means the input was wrong, `NotSufficientlySupportedException` means the library has no procedure for the question, and `AngouriBugException` is a defect that should not be caught at all. Two things the reading turned up and the document records rather than changes: `UnrecognizedDomainException` is thrown by nothing, and the place that would raise it -- `SpecialSet.ToDomain(string)`, behind the public `SpecialSet.Create(string)` -- answers a caller's typo with an `AngouriBugException` asking them to report it. Changing which type is thrown there is a breaking change and belongs in its own pull request. Then the messages. 106 of the 197 `throw new` sites passed a message that was a compile-time constant, so a refusal named neither the sub-expression nor the shape nor the value it was about. Where the offending thing was already in scope it is now in the message: the source string on a parse failure, what an expression did evaluate to when it did not evaluate to a number, both shapes on every matrix operation that compares them, the element and the set on an undecidable membership, the node on anything that cannot be compiled, the expression and its variables on a truth table over the wrong ones. That leaves 73 constants, and 67 messages carrying a runtime value against 31 before. Every interpolation sits inside the `throw new` expression, so a path that does not throw pays nothing for it. No exception type was added, removed or changed, and no site throws a different type than it did. Message text is not an answer, so there is no `BREAKING-CHANGES.md` entry -- but the messages that tests do assert on were checked first: `FromStringTest` pins the `line L:C` prefix of a parse failure (the source is appended, not prepended, for that reason) and the exact `FunctionArgumentCountException` wording, `MissingFunctionNamesRefusedTest` and `ArcHyperbolicRefusedTest` pin substrings of the parser's refusals, `MatrixCompilationTest` pins "Matrix", `CircleTest` pins the whole `CannotParseInstanceException` message and `CompileUnboundVariableTest` the named variable. None of those messages moved. Roadmap #746 item 11. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Bjumi5K7fg8yx6UK1mZTQd
Sources/.editorconfig pins the header per directory, so a new file needs its own scope or it ships with a copyright four years out of date. Three other branches append a scope to the same place; all four entries survive the conflict.
Rafael-SOWNet
added a commit
that referenced
this pull request
Aug 23, 2026
EvalEasy went 0.98 ns -> 3.52 ns on #1027, a branch that changes exception message strings and a documentation file, and failed the gate at 3.58x. Its allocation moved 0.0%, and so did almost everything else's: every one of the seventeen benchmarks came out slower, between 1.30x and 3.58x, because the baseline was taken on an 8-core runner and pull requests run on a 4-core one. The inflation tracks how small the benchmark is, which is what says it is overhead rather than code: EvalEasy 0.98 ns 3.58x ParseEasy 6,919 ns 1.59x RunEasy 23 ns 1.81x Derivate 17,215 ns 1.52x RunHard 346 ns 1.74x SimplifyHard 2.16e9 ns 1.30x RunMedium 203 ns 1.57x Allocation already has this guard -- AllocationFloorBytes exists so that a benchmark allocating almost nothing cannot fail on a percentage of almost nothing -- and the same argument applies to time with no floor at all. Below roughly a microsecond a ratio measures the runner: scheduling, cache state, one fewer core. So time under 1,000 ns/op is reported and never fails. The gate still catches what it was built for. SimplifyHard at 2.16 seconds would have to reach 6.5 seconds, and allocation is untouched -- it remains the metric that fails, at 3%, on every benchmark regardless of size. A gate that goes red without a defect is a gate people learn to ignore, and this one is a day old.
Rafael-SOWNet
added a commit
that referenced
this pull request
Aug 23, 2026
#1027 improved this site's message and left it an AngouriBugException; this branch changes what it throws. Merging kept both, which is two default arms in one switch and does not compile. The point of this change is that neither site is a library defect, so the AngouriBugException arms go and the messages that say what the caller passed stay.
Rafael-SOWNet
added a commit
that referenced
this pull request
Aug 23, 2026
* Do not call the caller's typo a library defect
SpecialSet.Create(string) and SpecialSet.Create(Domain) are both public, and
both answered an input they did not recognise with AngouriBugException, whose
message asks the caller to report the problem to this repository. Somebody who
writes "NN" is told their own typo is a defect here.
The string overload now throws UnrecognizedDomainException, which has existed
for exactly this since it was written and which nothing threw. The Domain
overload throws NotSufficientlySupportedException, because Domain.Any is a
documented member of the enum -- it means no restriction, which is not a set
this library has a node for -- and a value cast in from outside the enum is not
one either. Neither is the library's fault and neither now says it is.
Both stay under AngouriMathBaseException, so the catch the documentation tells
a caller to write is unaffected, and a test pins that.
Measured on a build of each side, with every accepted name and every domain
that is a set checked to still answer:
Create("NN") AngouriBugException -> UnrecognizedDomainException
Create(Domain.Any) AngouriBugException -> NotSufficientlySupportedException
Create((Domain)99) AngouriBugException -> NotSufficientlySupportedException
Create("RR") RR -> RR
Nothing inside the library reaches either arm: Domains.IsWithinDomain answers
Domain.Any before Create sees it, and the only other route into the string
overload is a SPECIALSET token, every spelling of which the switch lists.
* Resolve against master by taking the type change, not both arms
#1027 improved this site's message and left it an AngouriBugException; this
branch changes what it throws. Merging kept both, which is two default arms in
one switch and does not compile. The point of this change is that neither site
is a library defect, so the AngouriBugException arms go and the messages that
say what the caller passed stay.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Roadmap #746, item 11. One of that
issue's three founding complaints is that failure here is uninformative: "I could not settle
this" tells a caller nothing about what to try instead and a contributor nothing about what to
build. An exception that names the sub-expression it choked on is the cheapest instance of the
principle.
What was measured
On
masterat v2.3.0, overSources/AngouriMath/**/*.cs:throw newsites (excluding 9 that are inside XML doc<code>samples, not code)AngouriMathBaseExceptionDocs/Usage/Exceptions.mdDocs/Usage/Exceptions.mdRewritten from three lines to the whole hierarchy, derived by reading
Core/Exceptions/**and every throw site rather than from memory. Each type gets what it means,when a caller can expect it, and where it is raised from. The organising split is the one a caller
actually acts on:
MathSException— the input was wrong. Catch it and show the message.NotSufficientlySupportedException— the library has no procedure for the question. A featurerequest, and catchable.
AngouriBugException— a defect. Not a catch target; catching it turns a reproducible buginto a silent wrong answer.
It also states the overlaps rather than leaving them to be discovered:
FunctionArgumentCountExceptionis the arity complaint about a call written in a string andWrongNumberOfArgumentsExceptionthe one about a call made through the API — only the first isa
ParseException, so they are not interchangeable;BadMatrixShapeExceptionexists only forconcatenation and
InvalidMatrixOperationExceptionis the type any new shape check should use.And it documents
MathS.Diagnostic.DiagnosticCatchException, the one exception the library throwsthat is deliberately outside the hierarchy, so that a caller's
catch (AngouriMathBaseException)cannot swallow their own debugging aid.
One gap the reading found, recorded and not fixed here.
UnrecognizedDomainExceptionis thrownby nothing. The place that would raise it —
Entity.Set.SpecialSet.ToDomain(string), reachablethrough the public
SpecialSet.Create(string)— answers an unknown domain name withAngouriBugException, which asks the caller to report their own typo as a library defect. That isthe wrong answer to a wrong string, but correcting it changes which exception a caller sees, so it
wants a pull request of its own rather than this one.
The messages
Where the offending expression or value was already in scope, it is now in the message. The
user-reachable paths first:
once the string has travelled through a caller's own layers. The source is now appended (not
prepended:
FromStringTestasserts on theline L:Cprefix, and it is what a reader looks forfirst).
EvalNumerical/EvalBooleansay what the expression did evaluate to,instead of "Result cannot be represented as a simple number!".
AsScalar,Pow,WithElement,WithRow,WithColumnandMatrixBuilderprint the shape they got.operator +andoperator -no longer share one message that says which of them it was.Containsnames the element and the set.be compiled".
MathS.ToBaseN's base,Rational.Create's non-finite value, the decimal precision that didnot fit in an int32, and a handful of internal
AngouriBugExceptionasserts that now print thetwo sizes or the enum value that did not match.
Nothing was manufactured: where a site has nothing in scope to name —
"No rows were provided","Cannot concat 0 matrix"— it was left alone.Cost. Every interpolation is written inside the
throw newexpression, so it is evaluated onlywhen the exception is constructed and a path that does not throw pays nothing.
What was deliberately not done
did. That is a breaking change and this is not the PR for it.
BREAKING-CHANGES.mdentry: message text is not an answer.arcsinhand its neighbours,"Expected integer number for the third argument of derivative", theapply/lambdaarityrefusals) were left alone — they come from
AngouriMath.gand changing them means regeneratingthe parser, which is its own change.
Tests asserting on message text
Checked before touching anything, by grepping every string being changed. Six tests do assert on a
message:
FromStringTest(theline L:Cprefix, and the exactFunctionArgumentCountExceptionwording),
MissingFunctionNamesRefusedTestandArcHyperbolicRefusedTest(substrings of theparser's refusals),
MatrixCompilationTest("Matrix"),CircleTest(the wholeCannotParseInstanceExceptionmessage) andCompileUnboundVariableTest(the named variable).None of the messages they pin were moved, and all six still pass unmodified.
Measured
dotnet build Sources/AngouriMath -c Release— 0 warnings, 0 errors.dotnet test Sources/Tests/UnitTests -c Release:identical to
master's baseline.wall-clock limit test flaked under load and passes on its own).
dotnet test Sources/Tests/FSharpWrapperUnitTests -c Release— 134 passed / 0 failed.New file:
Sources/Tests/UnitTests/Core/ExceptionMessageNamesTheExpressionTest.cs, pinning theparse-failure input, the evaluated-to form, both matrix shapes, the element and the set, the
uncompilable node, the base, the truth-table variables and the builder widths — plus a test that
the same paths still succeed when nothing is wrong.