core design
Design goal
arithmetic is the layer between algebraic structure and concrete numbers.
luna-generic says what a type is
(a ring, a field); arithmetic says what analytic operations a type can do
and how honestly it can do them: whether a square root may silently return
NaN, whether it reports a rejected argument, or whether it also reports how the
result was rounded under a stated precision. A generic algorithm states exactly
the capabilities it relies on, and a numeric backend implements exactly the
capabilities whose semantics it can honour.
The package ships the vocabulary (traits, the context, diagnostics and
errors) and a baseline of instances for the native Float, Double and
integer types. Correctly rounded, context-faithful and certified arithmetic
lives in numeric backends, which implement these traits.
Mathematical background
Floating-point formats
A floating-point format with radix , precision and exponent range is the finite set
The second set holds the normal numbers, whose leading digit sits at
( is the adjusted exponent); the third holds the subnormal
numbers below . IEEE 754 extends to
,
with signed zeros. FpClass is the map
| Format | Source | ||||
|---|---|---|---|---|---|
| binary32 | 2 | 24 | Float | ||
| binary64 | 2 | 53 | Double | ||
| decimal32 | 10 | 7 | ArithmeticContext::decimal32 | ||
| decimal64 | 10 | 16 | ArithmeticContext::decimal64 | ||
| decimal128 | 10 | 34 | ArithmeticContext::decimal128 |
ArithmeticContext stores as precision and the adjusted-exponent bounds
as e_min and e_max; the radix is a property of the backend type, not of
the context. All three decimal presets satisfy , the
IEEE 754 relation that keeps the reciprocal of the smallest normal number,
, inside the finite range.
Machine epsilon. The successor of in is : writing gives and , and the next significand gives
More generally, in the binade consecutive numbers
are apart. epsilon_contextual
returns : for Float and for Double.
Rounding
A rounding function maps a real number to a representable one. If are the neighbours of :
and every mode returns itself when . Every mode is monotone, , and idempotent on ; the certification argument below uses only these two properties.
Relative error. Let lie in the normal range, with . The neighbours of are apart, so a directed mode moves by less than one spacing and round-to-nearest by at most half of one:
Hence with , where the unit roundoff is
| Format | for ToNearestEven |
|---|---|
| binary32 | |
| binary64 | |
| decimal32 | |
| decimal64 | |
| decimal128 |
Below the spacing stops shrinking, and the relative bound
becomes an absolute one: .
Above the largest finite number, is or the largest
finite number, depending on the mode. These are the subnormal, underflow
and overflow conditions of ArithmeticDiagnostics.
The standard model of rounding error
IEEE 754 requires and to be correctly rounded: the computed result is the rounding of the exact one. With the bound above, for operands in and a result in the normal range,
This is the contract of the contextual arithmetic traits: a context-faithful
AddContextual returns for the context’s and
rounding mode, and raises inexact exactly when . It is also the
reason the context matters: for decimal64 and ToNearestEven, an algorithm
can rely on per operation, whatever the
backend type.
The built-in Float and Double instances satisfy the model for their own
fixed format under round-to-nearest, because the hardware operations are
correctly rounded, but they ignore the context’s precision and mode and do not
detect . Their elementary functions come from
Kaida-Amethyst/math and are not guaranteed to be correctly rounded, so the
model holds for them only with an unspecified small multiple of .
Adjacent values and the IEEE encoding
AdjacentContextual computes
The Float and Double instances compute them on the bit pattern. An IEEE
binary value with sign , biased exponent field and fraction field
is stored as the unsigned integer
for a -bit format. On the non-negative values this map is order-preserving:
- for a fixed , the value (or when ) increases strictly with ;
- the largest value with field is , the smallest value with field ; the subnormals () all lie below the smallest normal ;
- is , , above every finite pattern.
Because the encoding is lexicographic in and lexicographic order on is integer order on , it follows that for , , and no pattern lies strictly between consecutive values. Therefore
and symmetrically for . The zero case is separate because and are equal values with different patterns. The formula reproduces the boundary cases listed in the API: the largest finite pattern plus one is the pattern of , and . Since is in by definition, no rounding happens and the empty diagnostics of these instances are exact, not merely undetected.
Enclosures and three-valued comparison
An enclosure stands for an unknown real known to satisfy : an interval , or a ball . The enclosure relation traits answer questions about the unknown values from the enclosures alone, by quantifying over every admissible pair:
Interval formulas. Let and be non-empty.
() take and . () for any , : . The same argument with gives . The possible relation is the existential one:
() . () take , . It needs no trait of its own, because it is the negation of a definite relation with the arguments swapped:
Finally : if both hold, is a common point; conversely a common point gives and . For balls, substituting the endpoints gives .
Three-valued truth. From enclosures alone, "" has one of three truth values:
The value is well defined: and together would need and , hence , a contradiction. In the same way is when and otherwise, unless both enclosures are the same single point. Compound conditions combine with Kleene’s strong three-valued logic:
Reading as “true for some admissible values and false for others”, each entry is the strongest statement that holds for all of them; for example because a conjunction with a false conjunct is false whatever the other one is.11 S. C. Kleene, Introduction to Metamathematics, 1952, §64. Interval comparison with three outcomes goes back to R. E. Moore, Interval Analysis, 1966.
Certification stages
A proof-backed backend computes , the correct rounding of a
transcendental at target precision , by a pipeline whose stages are
the values of CertificationStage. For as an illustration:
-
RangeReduction: write with and , so that . The reduced argument must itself be enclosed, which costs about extra bits of . -
SeriesEvaluation: sum and bound the tail. For ,using .
-
EnclosurePropagation: carry the truncation bound and every rounding error of the working precision through the remaining operations, obtaining an enclosure . -
TargetRounding: if , that value is the answer, because by monotonicityOtherwise straddles a rounding boundary: the backend raises and repeats, which is Ziv’s strategy.22 A. Ziv, “Fast evaluation of elementary mathematical functions with correctly rounded last bit”, ACM TOMS 17(3), 1991. How close can come to a boundary is the table maker’s dilemma; for most functions no useful a-priori bound on is known, which is why the loop needs a budget.
When the loop gives up, the backend returns
ArithmeticError::certification_failure with the stage, the reason,
(target_precision), the last (work_precision) and the number of
increases (refinements). RefinementBudgetExhausted is the expected reason
at TargetRounding; the other reasons belong to the earlier stages. This
package defines the vocabulary only; it evaluates nothing.
Design decisions
Three tiers instead of one signature
Problem. A square root on Double in a tight loop wants
fn sqrt(Double) -> Double and accepts NaN for a negative argument. A decimal
backend needs the precision and rounding mode and must report whether the
result was rounded. One signature cannot serve both without either forcing a
Result and a context on every native call or dropping information that the
decimal caller needs.
Options. (a) unchecked traits only; (b) contextual traits only; (c) three independent tiers.
Choice. (c). The tiers carry increasing information, and each result type embeds into the next one:
where is the diagnostics set and its empty value. The
built-in contextual adapters, except the Float integer embedding, are
exactly these embeddings applied to the checked or unchecked result. A bound states what the algorithm handles:
T : Sqrt accepts the backend’s own behaviour, T : SqrtChecked handles
rejection, T : SqrtContextual needs context and diagnostics. The tiers have
no supertrait links, so a type implements exactly the tiers it honours: an
interval type can implement DivChecked and the enclosure relations without
pretending to have a context-free Sqrt.
Context and diagnostics are explicit values
Problem. IEEE 754 describes the rounding direction and the status flags
as attributes of the execution environment; C exposes them through
<fenv.h>, and Python’s decimal keeps a thread-local current context. Both
are hidden state: the result of a + b depends on something that is not an
argument, and a flag raised in one computation is still set in the next.
Options. (a) a global mutable context; (b) a thread- or task-local one; (c) the context as an argument and the flags in the return value.
Choice. (c). Every contextual operation is a function
so equal inputs give equal outputs, and the diagnostics of a computation are exactly those of the operations it combined. This works the same on every MoonBit target (none of them needs thread-local storage), makes concurrent use safe, and lets a test state the full input of an operation. The cost is verbosity, which a backend can reduce with its own helpers.
Errors, diagnostics and certification failures are separate
Problem. IEEE 754 has five exceptions: invalid operation, division by zero, overflow, underflow and inexact. Some describe results that do not exist, others describe results that exist but were rounded.
Choice. The package splits them by whether a value is returned:
| Situation | Channel | IEEE 754 counterpart |
|---|---|---|
| no meaningful value (domain, indeterminate form) | Err, DomainError | invalid operation |
| a pole: finite non-zero over zero | Err, DivisionByZero | division by zero |
| a value in that differs from the exact one | Ok with inexact, rounded | inexact |
| a value beyond the finite range or below the normal range | Ok with overflow, underflow, subnormal | overflow, underflow |
| a valid input whose result could not be certified | Err, CertificationFailure | none |
A flag must not hide an error, and an error must not be invented to carry a
flag: is a correct
IEEE answer, so it is a value with overflow, not a failure. A certification
failure is an error but not a domain error: the input is valid and a larger
budget may succeed, so it carries the data a caller needs to decide whether to
retry. The package imposes no retry policy.
Enclosure relations are not an order
Problem. Intervals look ordered, and implementing Compare for them would
let generic sorting code accept them.
Choice. Five separate relation traits. Compare promises a total order,
but definitely_lt on non-empty intervals is only a strict partial order. It
is irreflexive ( fails for ) and transitive:
but not total: for overlapping neither
nor holds, and neither are they equal. A
Compare instance would have to answer one of there and so would
assert something false about the unknown values.
Native scalars implement only what they can honour
Problem. Float and Double could implement every trait by ignoring the
context.
Choice. They implement a capability only when the result is meaningful:
- all unchecked traits and
Power, which promise nothing beyond the backend; - the checked traits, whose only extra promise is to reject invalid arguments;
- the contextual arithmetic, absolute value, square root and exponential, integer embedding, adjacent values and format queries, as adapters so that generic contextual code also runs on native scalars;
- not
ConstantsContextualandHyperbolicContextual, whose purpose is a result that honours an arbitrary precision with meaningful diagnostics; a fixed-precision library function cannot provide that.
The adapters are honest where the fixed format makes them exact (adjacent
values, Double integer embedding) and detect loss where it is cheap (Float
integer embedding, below). The arithmetic adapters do not detect rounding:
their empty diagnostics mean “not detected”. The API page
states this at the point of use.
One capability per trait, no Real
A trait such as Real : Field + Sqrt + Exponential + Trigonometric + Compare
would be convenient, but it would hide differences that matter: an interval
type has no total order, a decimal type has no cheap sin, and an integer
type has Power but no Sqrt. Every trait in this package is one capability,
and an algorithm composes the bounds it uses, such as
T : Add + Mul + Sqrt for a hypotenuse. Radical is the only conjunction,
because square and cube roots are routinely needed together.
Three floating-point classes
IEEE 754 class distinguishes ten classes (signalling and quiet NaN,
negative and positive infinity, normal, subnormal and zero). FpClass keeps
three, because these are the cases generic code branches on: a finite value
can enter further arithmetic, an infinity is a valid limit, and NaN is
invalid. Sign, zero and subnormality are tested with the format’s own API.
IntegralContextual embeds Int only
Every backend can receive a MoonBit Int, and loop counters and indices are
Int. An embedding of BigInt would require arbitrary-precision rounding in
every backend; it is left to a separate capability so that implementing the
common case stays cheap.
Power keeps one signature
Power::pow(Self, Self) is the same for floating and integer types, so a
generic power does not depend on the family. The price is that the exponent
type is the base type: the signed and BigInt instances must abort on a
negative exponent, since in general. Code that
needs a defined failure uses PowNatChecked (exponent UInt) or
PowIntChecked (exponent Int).
Correctness and invariants
Context invariants
ArithmeticContext::new establishes by clamping and
(when both are present) by aborting, and the fields are
read-only outside the package. Every context value therefore satisfies both,
and a backend need not re-check them.
Laws of combine
ArithmeticDiagnostics is the Boolean lattice , and
combine is the componentwise . Because each component satisfies the
Boolean laws, for all :
So is a commutative idempotent monoid, that is a join semilattice with a least element. The diagnostics of a computation are the join of the diagnostics of its steps, independent of evaluation order and grouping, and a flag once raised cannot be cleared by combining.
Sequencing contextual operations
Composing two contextual operations and gives
This is the writer monad over stacked on the error
monad, and the monoid laws above are exactly what makes
associative with ArithmeticOutcome::exact as its identity.33 Associativity of reduces to associativity of
on the diagnostics and of function composition on the values; the
identity law reduces to . See E. Moggi, “Notions of
computation and monads”, 1991.
The package ships the pieces (exact, with_diagnostics, combine) rather
than a combinator; the tutorial shows a short
helper.
Integer embedding into Float
binary32 has . An integer with has at most 24
significant bits (or is itself, a power of two), so it is
representable; needs 25 significant bits and is not, and round-to-nearest-even
sends it to . The Float instance detects the loss without a
wide-integer comparison: binary64 has , so
Double::from_int is exact on every Int, and every binary32 value is a
binary64 value, so widening is exact. Hence
and the instance sets inexact and rounded exactly when the conversion lost
information. Overflow cannot occur, since .
Error bound of binary powering
PowNatChecked for Float and Double computes with the loop
Correctness. In exact arithmetic
holds at the start of every iteration. It holds initially, and if
then ,
while if then .
At the invariant gives . The loop runs
times and performs
squarings and products, the first of which
() is exact. The integer Power instances use the same
invariant in , where every step is exact.
Rounding error. Give each computed quantity that approximates an error count such that with and . Then , and one rounded product of and gives . By induction :
and a squaring is the case , where the shared error is counted twice. Therefore, in the absence of overflow and underflow,
The last step is the standard lemma
for .44 N. J. Higham, Accuracy and Stability of Numerical Algorithms,
2nd ed., SIAM, 2002, Lemma 3.1 and §3.1.
PowIntChecked with a negative exponent divides once more, and
gives . Binary
powering does not improve on the worst-case bound of successive
multiplications; it reduces the work from to
multiplications.
Checked division
The Float and Double DivChecked instances reject every zero divisor and
use the kind to say why. and are indeterminate: the
limits take every value as or
, so no quotient is meaningful and the kind is DomainError.
For , diverges as , a pole, and the kind is
DivisionByZero. This matches IEEE 754’s invalid operation and division by
zero exceptions, but is stricter: IEEE returns for and
NaN for NaN silently, where the checked instance returns
DivisionByZero. A NaN dividend with a non-zero divisor still propagates as
Ok(NaN), and SqrtChecked lets NaN pass in the same way: a checked
operation rejects invalid arguments, it does not re-report an earlier
invalid result.
Soundness and monotonicity of enclosure relations
Soundness. If , and , then : the relation is a universal statement over , which contains . Dually, if , then .
Monotonicity under refinement. If and , then
because a universal statement survives shrinking its domain and an
existential one survives growing it. In three-valued terms, refining the
enclosures can turn into or but never
turns into . This is what makes “refine until decided”
loops, such as the target-rounding stage above, correct: a decision once made
stays valid. Contains is how such a loop checks that a refined enclosure
is inside the old one, .
The traits do not fix the convention for empty enclosures; each backend
documents its own. Under the quantifier reading the definite relations would
hold vacuously for an empty argument, so a backend that wants never
to come from an absence of information returns false for them instead.
Alternatives rejected
- A global or thread-local context with sticky flags, as in C
<fenv.h>and Pythondecimal. Rejected for the reasons under explicit values: it makes results depend on hidden state and leaks flags between computations. - A
RealorNumbersuper-trait. Rejected because it hides the differences between exact, approximate and enclosure-valued types. Comparefor enclosures. Rejected because the definite order is not total.- A three-valued result type (
True | False | Unknown) for comparisons. The two Boolean projectionsdefinitely_*andmaybe_eqare enough to reconstruct it, as shown above, and they compose with ordinaryif; a separate type would force every caller to handle even when it only asks one direction. - A
definitely_eqrelation. For non-degenerate enclosures it is always false, so it would only be a test for equal single points. - Diagnostics reported as errors. Rejected because an inexact or
overflowing IEEE result is a correct answer, and turning it into
Errwould make every rounded operation fail. Optionorraisefor checked results.Optionloses the reason; MoonBit’sraisewould put the failure outside the return type. Luna-Flow usesResultwith a structured error across repositories.- Implementing
ConstantsContextualandHyperbolicContextualfor native scalars by ignoring the context. Rejected because those traits exist to promise context-faithful results.
Boundaries
- The package does not implement arbitrary-precision, decimal, interval or ball arithmetic, and it does not evaluate certified functions; it defines the traits those backends implement.
- The built-in
FloatandDoubleinstances do not honour the context’s precision, rounding mode or exponent range, and their arithmetic adapters do not detect rounding, overflow or underflow. - It does not promise correctly rounded elementary functions for
FloatandDouble; they come fromKaida-Amethyst/math. - It does not define algebraic structure (
Ring,Field, …), which belongs toluna-generic, nor vectors, matrices, complex numbers or polynomials. - It does not choose branch cuts or special-value conventions for the unchecked traits beyond what each shipped instance inherits.
- It does not prescribe a retry or precision-escalation policy for certification failures.
Footnotes
-
S. C. Kleene, Introduction to Metamathematics, 1952, §64. Interval comparison with three outcomes goes back to R. E. Moore, Interval Analysis, 1966. ↩
-
A. Ziv, “Fast evaluation of elementary mathematical functions with correctly rounded last bit”, ACM TOMS 17(3), 1991. How close can come to a boundary is the table maker’s dilemma; for most functions no useful a-priori bound on is known, which is why the loop needs a budget. ↩
-
Associativity of reduces to associativity of on the diagnostics and of function composition on the values; the identity law reduces to . See E. Moggi, “Notions of computation and monads”, 1991. ↩
-
N. J. Higham, Accuracy and Stability of Numerical Algorithms, 2nd ed., SIAM, 2002, Lemma 3.1 and §3.1. ↩