Handle errors and work limits¶
Polynomial algorithms can encounter invalid input, grow beyond representable
sizes, or need more work than an application can afford. Computational entry
points raise PolyError and share a caller-owned Budget, so you can keep
these cases separate from mathematical results.
"""Handle work exhaustion separately from division by the zero polynomial."""
import poly
from polynomials import PolyErrorKind
from std.testing import assert_true
def main() raises:
var saw_limit = False
try:
_ = poly.dense.power([1, 1], 100, max_steps=1000)
except error:
saw_limit = error.kind == PolyErrorKind.LIMIT
assert_true(saw_limit)
var saw_domain = False
try:
_ = poly.dense.divmod([1, 1], [0])
except error:
saw_domain = error.kind == PolyErrorKind.DOMAIN
assert_true(saw_domain)
print("Work exhaustion handled:", saw_limit)
print("Zero divisor handled:", saw_domain)
Run from the repository root pixi run mojo run -I build docs/examples/tutorial_errors.mojo
Output
Work exhaustion handled: True
Zero divisor handled: True
The first error is deliberate work exhaustion. The second is division by the zero polynomial. Neither produces a polynomial that could be mistaken for a successful result.
Set a budget for a workflow¶
Pass the same Budget through related calls to limit their combined work.
Budget() uses the maximum UInt64 allowance; choose a finite allowance when
your application needs one. A rejected charge leaves the remaining allowance
unchanged. Algorithms charge before the corresponding work and preserve
borrowed inputs on failure.
What a step costs¶
A budget unit is one step of the library's work model. Each call charges its steps before doing the corresponding work:
| Work | Steps charged |
|---|---|
| Visiting a term, coefficient, or pending subproblem | 1 |
| Allocating storage | 1 per element |
| Adding or subtracting two integers | the sum of their sizes |
| Multiplying two integers; adding, subtracting, or multiplying two rationals | the square of the sum of their sizes |
| An operation on prime-field residues | 1 |
An integer's size is its magnitude in bytes, plus one; a rational's size is
the sum of its numerator's and denominator's sizes. Numerical coefficients are
charged the same way, by the storage their precision needs. Computing
\((1+x)^{100}\) with poly.dense.power, for example, takes about 330,000 steps;
its coefficients have up to 30 digits.
The same call on the same input charges the same steps on every machine,
whatever its load or speed. Reusing prepared state from an earlier call can
lower the charge without changing the result. To measure a call, pass it a
fresh Budget() and subtract its remaining() from UInt64.MAX afterwards,
as the first tutorial
shows.
Steps do not convert to time or memory. On one laptop core, different operations ran at anywhere from about 100 million to over 10 billion steps per second: the table charges a multiplication of large numbers by the square of their sizes, while the processor multiplies eight bytes at a time. An allocation is charged per element, not per byte, so steps are not a hard memory limit either. Step counts can change between releases, so measure representative inputs when choosing an application limit, and check it again after upgrading.
Respond to the error kind¶
| Kind | Typical cause | Useful response |
|---|---|---|
DOMAIN |
Zero divisor, incompatible domains or moduli, invalid nodes, or an invalid axis | Correct the input or choose an appropriate domain |
LIMIT |
Exhausted work allowance or an unmet strict search limit | Increase the relevant limit if more work is acceptable |
CAPACITY |
A size, exponent, or encoding exceeds its representable bound | Reduce or restructure the problem |
UNSUPPORTED |
The requested operation cannot handle this case | Choose a supported formulation or algorithm |
ENCODING |
Malformed or noncanonical interchange data | Reject or correct the data |
Do not interpret an error as zero, nonmembership, absence of roots, or a false
constraint. Those are mathematical answers with their own successful return
values. Catch PolyError where the application can make a recovery decision;
let it propagate when it cannot.
Numerical outcomes are separate¶
A numerical producer can return useful certified information without meeting
all requested targets. Its NumericOutcome explains the remaining obstacle:
| Outcome | What to inspect |
|---|---|
SUCCESS |
The result and its checker, plus the claims your application needs |
UNCERTAIN_DEGREE |
Whether the leading coefficient can be zero |
INPUT_UNCERTAINTY |
Whether different members of the input family require different answers |
PRECISION_CEILING |
initial_precision, max_precision, and the requested width |
SUBDIVISION_LIMIT |
max_subdivisions and unresolved or overly wide regions |
UNRESOLVED_BOUNDARY |
Whether the search has certified a zero-free boundary |
For root covers, inspect complete, accurate, and isolated independently.
A complete cover may still be too wide, while an incomplete cover cannot
establish a global root count. complete_only=True turns failure to meet the
requested root properties into an exception. Budget exhaustion always raises,
including in modes that otherwise return inconclusive numerical results.
The root tutorial
shows how to read and refine checked evidence. The
API reference defines Budget,
PolyError, and PolyErrorKind.