What is wrong
AGENTS.md draws a line the code mostly does not: unevaluated means "I could not settle this",
NaN means "this does not exist", and confusing them ships a wrong answer. Resource limits are
where that line is thinnest, because a limit firing and a genuine negative both leave by the same
return null / return false / break.
A survey of Sources/AngouriMath/ for #1035 counted roughly 70 sites where the library gives up
on a resource. Of those, one throws (PrimeFieldPolynomial.cs:400, a representational
precondition rather than a budget) and one recorded which limit fired — GroebnerBudget.Exceeded,
which nothing ever read. Every other site is byte-identical to a mathematical negative.
#1035 gives the library a WorkBudget, a ledger, a BudgetOutcome that names where and why, and
an ambient BudgetRecording scope to read it out, and wires one consumer: Gröbner, whose fifteen
exits are now individually named. This issue is the rest.
The sites where the confusion is visible, not just possible
| site |
what it does |
reading |
Functions/Continuous/Solvers/Solvers.Definition.cs:346 |
can surface a resource-driven null as NaN |
"this does not exist" for something never attempted |
Functions/Algebra/Polynomials/PolynomialFactoring.cs:355 |
returns an empty rational-root candidate set when a coefficient exceeds MaxCoefficientToFactor (10⁶) |
"no rational roots" for "I did not look" |
Functions/Continuous/Limits/AsymptoticSeries.cs:204 |
returns a truncated series as if complete at MaxExpansionTerms (24) |
silently wrong tail |
Functions/Algebra/AnalyticalSolving/AnalyticalEquationSolver.cs:337 |
every limit below the solver converges here on an empty set |
indistinguishable from a proven-empty solution set |
On the second of these, one thing measured rather than assumed: on 6b93b401, both
(x - 2000000)(x⁴ + x + 1) = 0 and its expanded form return all five roots including 2000000,
and a control with the constant term under the ceiling returns the structurally identical set. So
the ceiling does not currently lose a root through Solve — the defect is in the function's
contract, not yet in an answer. That is the difference between "fix this before it bites" and "this
is a live wrong answer", and it is worth stating rather than leaving to be re-derived.
The fourth is the chokepoint: Solve returning an empty set both for "no solutions" and for "I gave
up" is named in #746's tier-4 assessment
as the place structured failure is absent outside the transformation layer.
Two structural findings from the same survey
Depth counters are [ThreadStatic] while settings are AsyncLocal. So a computation split
across an await keeps its settings and silently resets its depth guards. Eighteen limit-pipeline
guards are in this category.
One setting is doing two jobs. IndefiniteIntegralSolver.cs:106 uses
MathS.Settings.MaxExpansionTermCount — a term-count bound — as a 2000-deep recursion bound for
integration by parts. Changing the setting to control expansion also changes how deep integration
recurses.
Scope
Not one change. In dependency order:
- Retire the
null-as-NaN conversion at Solvers.Definition.cs:346 and give the
AnalyticalEquationSolver chokepoint a way to distinguish exhausted from empty. This is the one
with a user-visible payoff and it needs an API decision about what Solve returns.
- Give the limit pipeline a ledger, and make
AsymptoticSeries say its series is truncated.
- Separate the integration recursion bound from
MaxExpansionTermCount.
- Decide whether depth guards should follow the flow rather than the thread, and make the two
mechanisms agree.
- Sweep the remaining sites so each records where it stopped.
Each is separately landable and separately measurable. Step 1 is the one worth doing first.
Acceptance criteria (per step)
- A caller can tell "I could not settle this" from "this does not exist" for that site, and a test
distinguishes them.
- No input's answer changes silently; anything that does gets a
BREAKING-CHANGES.md entry measured
on a build of each side.
- The common case pays nothing when no budget is set — measured for time and allocation, both
arms in one process.
Dependencies
Depends on #1035 for WorkBudget / BudgetOutcome / BudgetRecording; do not build a second
mechanism alongside it. Related to
#373 and
#896, neither of which this closes.
What is wrong
AGENTS.mddraws a line the code mostly does not: unevaluated means "I could not settle this",NaNmeans "this does not exist", and confusing them ships a wrong answer. Resource limits arewhere that line is thinnest, because a limit firing and a genuine negative both leave by the same
return null/return false/break.A survey of
Sources/AngouriMath/for #1035 counted roughly 70 sites where the library gives upon a resource. Of those, one throws (
PrimeFieldPolynomial.cs:400, a representationalprecondition rather than a budget) and one recorded which limit fired —
GroebnerBudget.Exceeded,which nothing ever read. Every other site is byte-identical to a mathematical negative.
#1035 gives the library a
WorkBudget, a ledger, aBudgetOutcomethat names where and why, andan ambient
BudgetRecordingscope to read it out, and wires one consumer: Gröbner, whose fifteenexits are now individually named. This issue is the rest.
The sites where the confusion is visible, not just possible
Functions/Continuous/Solvers/Solvers.Definition.cs:346nullasNaNFunctions/Algebra/Polynomials/PolynomialFactoring.cs:355MaxCoefficientToFactor(10⁶)Functions/Continuous/Limits/AsymptoticSeries.cs:204MaxExpansionTerms(24)Functions/Algebra/AnalyticalSolving/AnalyticalEquationSolver.cs:337On the second of these, one thing measured rather than assumed: on
6b93b401, both(x - 2000000)(x⁴ + x + 1) = 0and its expanded form return all five roots including2000000,and a control with the constant term under the ceiling returns the structurally identical set. So
the ceiling does not currently lose a root through
Solve— the defect is in the function'scontract, not yet in an answer. That is the difference between "fix this before it bites" and "this
is a live wrong answer", and it is worth stating rather than leaving to be re-derived.
The fourth is the chokepoint:
Solvereturning an empty set both for "no solutions" and for "I gaveup" is named in #746's tier-4 assessment
as the place structured failure is absent outside the transformation layer.
Two structural findings from the same survey
Depth counters are
[ThreadStatic]while settings areAsyncLocal. So a computation splitacross an
awaitkeeps its settings and silently resets its depth guards. Eighteen limit-pipelineguards are in this category.
One setting is doing two jobs.
IndefiniteIntegralSolver.cs:106usesMathS.Settings.MaxExpansionTermCount— a term-count bound — as a 2000-deep recursion bound forintegration by parts. Changing the setting to control expansion also changes how deep integration
recurses.
Scope
Not one change. In dependency order:
null-as-NaNconversion atSolvers.Definition.cs:346and give theAnalyticalEquationSolverchokepoint a way to distinguish exhausted from empty. This is the onewith a user-visible payoff and it needs an API decision about what
Solvereturns.AsymptoticSeriessay its series is truncated.MaxExpansionTermCount.mechanisms agree.
Each is separately landable and separately measurable. Step 1 is the one worth doing first.
Acceptance criteria (per step)
distinguishes them.
BREAKING-CHANGES.mdentry measuredon a build of each side.
arms in one process.
Dependencies
Depends on #1035 for
WorkBudget/BudgetOutcome/BudgetRecording; do not build a secondmechanism alongside it. Related to
#373 and
#896, neither of which this closes.