Skip to content

Giving up on a resource is spelled the same as a mathematical negative #1036

Description

@Rafael-SOWNet

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:

  1. 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.
  2. Give the limit pipeline a ledger, and make AsymptoticSeries say its series is truncated.
  3. Separate the integration recursion bound from MaxExpansionTermCount.
  4. Decide whether depth guards should follow the flow rather than the thread, and make the two
    mechanisms agree.
  5. 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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions