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 β\beta, precision pp and exponent range [emin⁡,emax⁡][e_{\min}, e_{\max}] is the finite set

F={0}∪{ ±m⋅β e−p+1  :  m∈Z, βp−1≤m<βp, emin⁡≤e≤emax⁡ }∪{ ±m⋅β emin⁡−p+1  :  0<m<βp−1 }.\mathbb{F} = \{0\} \cup \{\, \pm m \cdot \beta^{\,e-p+1} \;:\; m \in \mathbb{Z},\ \beta^{p-1} \le m < \beta^{p},\ e_{\min} \le e \le e_{\max} \,\} \cup \{\, \pm m \cdot \beta^{\,e_{\min}-p+1} \;:\; 0 < m < \beta^{p-1} \,\}.

The second set holds the normal numbers, whose leading digit sits at βe\beta^{e} (ee is the adjusted exponent); the third holds the subnormal numbers below βemin⁡\beta^{e_{\min}}. IEEE 754 extends F\mathbb{F} to F‾=F∪{−∞,+∞,NaN}\overline{\mathbb{F}} = \mathbb{F} \cup \{-\infty, +\infty, \mathrm{NaN}\}, with signed zeros. FpClass is the map

class⁡:F‾→{Finite,Infinity,NaN},class⁡(x)={Finitex∈F,Infinityx=±∞,NaNx=NaN.\operatorname{class} : \overline{\mathbb{F}} \to \{\texttt{Finite}, \texttt{Infinity}, \texttt{NaN}\}, \qquad \operatorname{class}(x) = \begin{cases} \texttt{Finite} & x \in \mathbb{F},\\ \texttt{Infinity} & x = \pm\infty,\\ \texttt{NaN} & x = \mathrm{NaN}. \end{cases}
Formatβ\betappemin⁡e_{\min}emax⁡e_{\max}Source
binary32224−126-126127127Float
binary64253−1022-102210231023Double
decimal32107−95-959696ArithmeticContext::decimal32
decimal641016−383-383384384ArithmeticContext::decimal64
decimal1281034−6143-614361446144ArithmeticContext::decimal128

ArithmeticContext stores pp 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 emin⁡=1−emax⁡e_{\min} = 1 - e_{\max}, the IEEE 754 relation that keeps the reciprocal of the smallest normal number, β−emin⁡=βemax⁡−1\beta^{-e_{\min}} = \beta^{e_{\max}-1}, inside the finite range.

Machine epsilon. The successor of 11 in F\mathbb{F} is 1+β1−p1 + \beta^{1-p}: writing 1=βp−1⋅β 0−p+11 = \beta^{p-1} \cdot \beta^{\,0-p+1} gives m=βp−1m = \beta^{p-1} and e=0e = 0, and the next significand m+1m + 1 gives

(βp−1+1) β1−p=1+β1−p=:1+ε.(\beta^{p-1} + 1)\,\beta^{1-p} = 1 + \beta^{1-p} =: 1 + \varepsilon .

More generally, in the binade [βe,βe+1)[\beta^{e}, \beta^{e+1}) consecutive numbers are βe−p+1=ε βe\beta^{e-p+1} = \varepsilon\,\beta^{e} apart. epsilon_contextual returns ε\varepsilon: 2−232^{-23} for Float and 2−522^{-52} for Double.

Rounding

A rounding function ∘:R→F‾\circ : \mathbb{R} \to \overline{\mathbb{F}} maps a real number to a representable one. If a<x<ba < x < b are the neighbours of x∉Fx \notin \mathbb{F}:

RN⁡(x)=the nearer of a,b (ties to the even significand)ToNearestEvenRZ⁡(x)=the one of smaller magnitudeTowardZeroRU⁡(x)=bTowardPositiveRD⁡(x)=aTowardNegativeRA⁡(x)=the one of larger magnitudeAwayFromZero\begin{aligned} \operatorname{RN}(x) &= \text{the nearer of } a, b \text{ (ties to the even significand)} && \texttt{ToNearestEven}\\ \operatorname{RZ}(x) &= \text{the one of smaller magnitude} && \texttt{TowardZero}\\ \operatorname{RU}(x) &= b && \texttt{TowardPositive}\\ \operatorname{RD}(x) &= a && \texttt{TowardNegative}\\ \operatorname{RA}(x) &= \text{the one of larger magnitude} && \texttt{AwayFromZero} \end{aligned}

and every mode returns xx itself when x∈Fx \in \mathbb{F}. Every mode is monotone, x≤y⇒∘(x)≤∘(y)x \le y \Rightarrow \circ(x) \le \circ(y), and idempotent on F\mathbb{F}; the certification argument below uses only these two properties.

Relative error. Let xx lie in the normal range, βe≤∣x∣<βe+1\beta^{e} \le |x| < \beta^{e+1} with emin⁡≤e≤emax⁡e_{\min} \le e \le e_{\max}. The neighbours of xx are εβe\varepsilon\beta^{e} apart, so a directed mode moves xx by less than one spacing and round-to-nearest by at most half of one:

∣RN⁡(x)−x∣≤12 ε βe≤12 ε ∣x∣,∣∘dir(x)−x∣<ε βe≤ε ∣x∣.\begin{aligned} |\operatorname{RN}(x) - x| &\le \tfrac12\,\varepsilon\,\beta^{e} \le \tfrac12\,\varepsilon\,|x|,\\ |\circ_{\text{dir}}(x) - x| &< \varepsilon\,\beta^{e} \le \varepsilon\,|x|. \end{aligned}

Hence ∘(x)=x(1+δ)\circ(x) = x(1 + \delta) with ∣δ∣≤u|\delta| \le u, where the unit roundoff is

u={12 β1−pToNearestEven,β1−pdirected modes.u = \begin{cases} \tfrac12\,\beta^{1-p} & \texttt{ToNearestEven},\\ \beta^{1-p} & \text{directed modes}. \end{cases}
Formatuu for ToNearestEven
binary322−24≈5.96×10−82^{-24} \approx 5.96 \times 10^{-8}
binary642−53≈1.11×10−162^{-53} \approx 1.11 \times 10^{-16}
decimal325×10−75 \times 10^{-7}
decimal645×10−165 \times 10^{-16}
decimal1285×10−345 \times 10^{-34}

Below βemin⁡\beta^{e_{\min}} the spacing stops shrinking, and the relative bound becomes an absolute one: ∣RN⁡(x)−x∣≤12 βemin⁡−p+1|\operatorname{RN}(x) - x| \le \tfrac12\,\beta^{e_{\min}-p+1}. Above the largest finite number, ∘(x)\circ(x) is ±∞\pm\infty 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 +,−,×,/+, -, \times, / and  \sqrt{\ } to be correctly rounded: the computed result is the rounding of the exact one. With the bound above, for operands in F\mathbb{F} and a result in the normal range,

fl⁡(x∘y)=(x∘y)(1+δ),∣δ∣≤u,∘∈{+,−,×,/}.\operatorname{fl}(x \circ y) = (x \circ y)(1 + \delta), \qquad |\delta| \le u, \qquad \circ \in \{+, -, \times, /\}.

This is the contract of the contextual arithmetic traits: a context-faithful AddContextual returns fl⁡(x+y)\operatorname{fl}(x + y) for the context’s pp and rounding mode, and raises inexact exactly when δ≠0\delta \ne 0. It is also the reason the context matters: for decimal64 and ToNearestEven, an algorithm can rely on ∣δ∣≤5×10−16|\delta| \le 5 \times 10^{-16} 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 δ≠0\delta \ne 0. 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 uu.

Adjacent values and the IEEE encoding

AdjacentContextual computes

succ⁡(x)=min⁡{ y∈F‾:y>x },pred⁡(x)=max⁡{ y∈F‾:y<x }.\operatorname{succ}(x) = \min\{\, y \in \overline{\mathbb{F}} : y > x \,\}, \qquad \operatorname{pred}(x) = \max\{\, y \in \overline{\mathbb{F}} : y < x \,\}.

The Float and Double instances compute them on the bit pattern. An IEEE binary value with sign ss, biased exponent field EE and fraction field FF is stored as the unsigned integer bits⁡(x)=s⋅2k−1+E⋅2p−1+F\operatorname{bits}(x) = s \cdot 2^{k-1} + E \cdot 2^{p-1} + F for a kk-bit format. On the non-negative values this map is order-preserving:

  • for a fixed EE, the value 2E−bias(1+F 21−p)2^{E - \text{bias}}(1 + F\,2^{1-p}) (or 21−bias F 21−p2^{1-\text{bias}}\,F\,2^{1-p} when E=0E = 0) increases strictly with FF;
  • the largest value with field EE is 2E−bias(2−21−p)<2E+1−bias2^{E-\text{bias}}(2 - 2^{1-p}) < 2^{E+1-\text{bias}}, the smallest value with field E+1E + 1; the subnormals (E=0E = 0) all lie below the smallest normal 21−bias2^{1-\text{bias}};
  • +∞+\infty is E=2k−p−1E = 2^{k-p} - 1, F=0F = 0, above every finite pattern.

Because the encoding is lexicographic in (E,F)(E, F) and lexicographic order on (E,F)(E, F) is integer order on E⋅2p−1+FE \cdot 2^{p-1} + F, it follows that for 0≤x<y0 \le x < y, bits⁡(x)<bits⁡(y)\operatorname{bits}(x) < \operatorname{bits}(y), and no pattern lies strictly between consecutive values. Therefore

succ⁡(x)={bits⁡−1(bits⁡(x)+1)x>0,bits⁡−1(bits⁡(x)−1)x<0(since succ⁡(x)=−pred⁡(∣x∣)),smallest positive subnormalx=±0,\operatorname{succ}(x) = \begin{cases} \operatorname{bits}^{-1}(\operatorname{bits}(x) + 1) & x > 0,\\ \operatorname{bits}^{-1}(\operatorname{bits}(x) - 1) & x < 0 \quad (\text{since } \operatorname{succ}(x) = -\operatorname{pred}(|x|)),\\ \text{smallest positive subnormal} & x = \pm 0, \end{cases}

and symmetrically for pred⁡\operatorname{pred}. The zero case is separate because +0+0 and −0-0 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 +∞+\infty, and succ⁡(1)−1=ε\operatorname{succ}(1) - 1 = \varepsilon. Since succ⁡(x)\operatorname{succ}(x) is in F‾\overline{\mathbb{F}} by definition, no rounding happens and the empty diagnostics of these instances are exact, not merely undetected.

Enclosures and three-valued comparison

An enclosure X⊆RX \subseteq \mathbb{R} stands for an unknown real xx known to satisfy x∈Xx \in X: an interval [a,b][a, b], or a ball B(m,r)=[m−r,m+r]B(m, r) = [m - r, m + r]. The enclosure relation traits answer questions about the unknown values from the enclosures alone, by quantifying over every admissible pair:

definitely_lt(X,Y)  ⟺  ∀x∈X, ∀y∈Y: x<y,definitely_le(X,Y)  ⟺  ∀x∈X, ∀y∈Y: x≤y,maybe_eq(X,Y)  ⟺  ∃x∈X, ∃y∈Y: x=y  ⟺  X∩Y≠∅,overlaps(X,Y)  ⟺  X∩Y≠∅,contains(X,Y)  ⟺  Y⊆X.\begin{aligned} \texttt{definitely\_lt}(X, Y) &\iff \forall x \in X,\ \forall y \in Y:\ x < y,\\ \texttt{definitely\_le}(X, Y) &\iff \forall x \in X,\ \forall y \in Y:\ x \le y,\\ \texttt{maybe\_eq}(X, Y) &\iff \exists x \in X,\ \exists y \in Y:\ x = y \iff X \cap Y \ne \emptyset,\\ \texttt{overlaps}(X, Y) &\iff X \cap Y \ne \emptyset,\\ \texttt{contains}(X, Y) &\iff Y \subseteq X. \end{aligned}

Interval formulas. Let X=[a,b]X = [a, b] and Y=[c,d]Y = [c, d] be non-empty.

definitely_lt(X,Y)  ⟺  b<c.\texttt{definitely\_lt}(X, Y) \iff b < c .

(⇒\Rightarrow) take x=b∈Xx = b \in X and y=c∈Yy = c \in Y. (⇐\Leftarrow) for any x∈Xx \in X, y∈Yy \in Y: x≤b<c≤yx \le b < c \le y. The same argument with ≤\le gives definitely_le(X,Y)  ⟺  b≤c\texttt{definitely\_le}(X, Y) \iff b \le c. The possible relation is the existential one:

∃x∈X, ∃y∈Y: x<y  ⟺  a<d.\exists x \in X,\ \exists y \in Y:\ x < y \iff a < d .

(⇒\Rightarrow) a≤x<y≤da \le x < y \le d. (⇐\Leftarrow) take x=ax = a, y=dy = d. It needs no trait of its own, because it is the negation of a definite relation with the arguments swapped:

¬ definitely_le(Y,X)  ⟺  ¬ ∀y,x: y≤x  ⟺  ∃x,y: x<y  ⟺  a<d.\neg\,\texttt{definitely\_le}(Y, X) \iff \neg\,\forall y, x:\ y \le x \iff \exists x, y:\ x < y \iff a < d .

Finally X∩Y≠∅  ⟺  a≤d∧c≤bX \cap Y \ne \emptyset \iff a \le d \wedge c \le b: if both hold, max⁡(a,c)≤min⁡(b,d)\max(a, c) \le \min(b, d) is a common point; conversely a common point zz gives a≤z≤da \le z \le d and c≤z≤bc \le z \le b. For balls, substituting the endpoints gives definitely_lt(B(m1,r1),B(m2,r2))  ⟺  m1+r1<m2−r2\texttt{definitely\_lt}(B(m_1, r_1), B(m_2, r_2)) \iff m_1 + r_1 < m_2 - r_2.

Three-valued truth. From enclosures alone, "x<yx < y" has one of three truth values:

[ ⁣[ x<y ] ⁣]={Tdefinitely_lt(X,Y),Fdefinitely_le(Y,X),Uotherwise.[\![\, x < y \,]\!] = \begin{cases} \mathsf{T} & \texttt{definitely\_lt}(X, Y),\\ \mathsf{F} & \texttt{definitely\_le}(Y, X),\\ \mathsf{U} & \text{otherwise}. \end{cases}

The value is well defined: T\mathsf{T} and F\mathsf{F} together would need b<cb < c and d≤ad \le a, hence a≤b<c≤d≤aa \le b < c \le d \le a, a contradiction. In the same way [ ⁣[ x=y ] ⁣][\![\, x = y \,]\!] is F\mathsf{F} when ¬ maybe_eq(X,Y)\neg\,\texttt{maybe\_eq}(X, Y) and U\mathsf{U} otherwise, unless both enclosures are the same single point. Compound conditions combine with Kleene’s strong three-valued logic:

ppqq¬p\neg pp∧qp \wedge qp∨qp \vee q
T\mathsf{T}T\mathsf{T}F\mathsf{F}T\mathsf{T}T\mathsf{T}
T\mathsf{T}U\mathsf{U}F\mathsf{F}U\mathsf{U}T\mathsf{T}
T\mathsf{T}F\mathsf{F}F\mathsf{F}F\mathsf{F}T\mathsf{T}
U\mathsf{U}T\mathsf{T}U\mathsf{U}U\mathsf{U}T\mathsf{T}
U\mathsf{U}U\mathsf{U}U\mathsf{U}U\mathsf{U}U\mathsf{U}
U\mathsf{U}F\mathsf{F}U\mathsf{U}F\mathsf{F}U\mathsf{U}
F\mathsf{F}T\mathsf{T}T\mathsf{T}F\mathsf{F}T\mathsf{T}
F\mathsf{F}U\mathsf{U}T\mathsf{T}F\mathsf{F}U\mathsf{U}
F\mathsf{F}F\mathsf{F}T\mathsf{T}F\mathsf{F}F\mathsf{F}

Reading U\mathsf{U} as “true for some admissible values and false for others”, each entry is the strongest statement that holds for all of them; for example F∧U=F\mathsf{F} \wedge \mathsf{U} = \mathsf{F} 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 ∘pt(f(x))\circ_{p_t}(f(x)), the correct rounding of a transcendental ff at target precision ptp_t, by a pipeline whose stages are the values of CertificationStage. For f=exp⁡f = \exp as an illustration:

  1. RangeReduction: write x=kln⁡2+rx = k \ln 2 + r with k∈Zk \in \mathbb{Z} and ∣r∣≤12ln⁡2|r| \le \tfrac12 \ln 2, so that exp⁡(x)=2kexp⁡(r)\exp(x) = 2^{k}\exp(r). The reduced argument rr must itself be enclosed, which costs about log⁡2∣x∣\log_2 |x| extra bits of ln⁡2\ln 2.

  2. SeriesEvaluation: sum ∑j<Nrj/j!\sum_{j<N} r^{j}/j! and bound the tail. For ∣r∣<N+1|r| < N + 1,

    ∣∑j≥Nrjj!∣≤∣r∣NN!∑i≥0(∣r∣N+1)i=∣r∣NN!⋅11−∣r∣/(N+1),\Bigl|\sum_{j \ge N} \frac{r^{j}}{j!}\Bigr| \le \frac{|r|^{N}}{N!}\sum_{i \ge 0}\Bigl(\frac{|r|}{N+1}\Bigr)^{i} = \frac{|r|^{N}}{N!}\cdot\frac{1}{1 - |r|/(N+1)},

    using N!(N+i)!≤(N+1)−i\frac{N!}{(N+i)!} \le (N+1)^{-i}.

  3. EnclosurePropagation: carry the truncation bound and every rounding error of the working precision pw>ptp_w > p_t through the remaining operations, obtaining an enclosure [ℓ,h]∋f(x)[\ell, h] \ni f(x).

  4. TargetRounding: if ∘pt(ℓ)=∘pt(h)\circ_{p_t}(\ell) = \circ_{p_t}(h), that value is the answer, because by monotonicity

    ℓ≤f(x)≤h  ⟹  ∘pt(ℓ)≤∘pt(f(x))≤∘pt(h)=∘pt(ℓ).\ell \le f(x) \le h \;\Longrightarrow\; \circ_{p_t}(\ell) \le \circ_{p_t}(f(x)) \le \circ_{p_t}(h) = \circ_{p_t}(\ell).

    Otherwise [ℓ,h][\ell, h] straddles a rounding boundary: the backend raises pwp_w 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 f(x)f(x) can come to a boundary is the table maker’s dilemma; for most functions no useful a-priori bound on pwp_w 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, ptp_t (target_precision), the last pwp_w (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:

T⏟unchecked  →  v ↦ Ok(v)    Result[T,E]⏟checked  →  Ok(v) ↦ Ok(v,0)    Result[(T,D),E]⏟contextual,\underbrace{T}_{\text{unchecked}} \;\xrightarrow{\;v \,\mapsto\, \mathrm{Ok}(v)\;}\; \underbrace{\mathrm{Result}[T, E]}_{\text{checked}} \;\xrightarrow{\;\mathrm{Ok}(v) \,\mapsto\, \mathrm{Ok}(v, \mathbf{0})\;}\; \underbrace{\mathrm{Result}[(T, D), E]}_{\text{contextual}},

where DD is the diagnostics set and 0\mathbf{0} 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

op⁡:Tn×ArithmeticContext→Result[(T,D),E],\operatorname{op} : T^{n} \times \texttt{ArithmeticContext} \to \mathrm{Result}[(T, D), E],

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:

SituationChannelIEEE 754 counterpart
no meaningful value (domain, indeterminate form)Err, DomainErrorinvalid operation
a pole: finite non-zero over zeroErr, DivisionByZerodivision by zero
a value in F‾\overline{\mathbb{F}} that differs from the exact oneOk with inexact, roundedinexact
a value beyond the finite range or below the normal rangeOk with overflow, underflow, subnormaloverflow, underflow
a valid input whose result could not be certifiedErr, CertificationFailurenone

A flag must not hide an error, and an error must not be invented to carry a flag: fl⁡(10300×10300)=+∞\operatorname{fl}(10^{300} \times 10^{300}) = +\infty 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 (b<ab < a fails for [a,b][a, b]) and transitive:

b1<c2  ∧  b2<c3  ⟹  b1<c2≤b2<c3  ⟹  b1<c3,b_1 < c_2 \;\wedge\; b_2 < c_3 \;\Longrightarrow\; b_1 < c_2 \le b_2 < c_3 \;\Longrightarrow\; b_1 < c_3 ,

but not total: for overlapping X,YX, Y neither definitely_lt(X,Y)\texttt{definitely\_lt}(X, Y) nor definitely_lt(Y,X)\texttt{definitely\_lt}(Y, X) 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 ConstantsContextual and HyperbolicContextual, 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 x−n∉Zx^{-n} \notin \mathbb{Z} in general. Code that needs a defined failure uses PowNatChecked (exponent UInt) or PowIntChecked (exponent Int).

Correctness and invariants

Context invariants

ArithmeticContext::new establishes p≥1p \ge 1 by clamping and emin⁡≤emax⁡e_{\min} \le e_{\max} (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 D={0,1}6D = \{0, 1\}^{6}, and combine is the componentwise ∨\vee. Because each component satisfies the Boolean laws, for all d1,d2,d3∈Dd_1, d_2, d_3 \in D:

(d1∨d2)∨d3=d1∨(d2∨d3)associativityd1∨d2=d2∨d1commutativityd∨d=didempotenced∨0=d0=empty()\begin{aligned} (d_1 \vee d_2) \vee d_3 &= d_1 \vee (d_2 \vee d_3) && \text{associativity}\\ d_1 \vee d_2 &= d_2 \vee d_1 && \text{commutativity}\\ d \vee d &= d && \text{idempotence}\\ d \vee \mathbf{0} &= d && \mathbf{0} = \texttt{empty()} \end{aligned}

So (D,∨,0)(D, \vee, \mathbf{0}) 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 f:A→Result[(B,D),E]f : A \to \mathrm{Result}[(B, D), E] and g:B→Result[(C,D),E]g : B \to \mathrm{Result}[(C, D), E] gives

(g∘ˉf)(a)={Err(e)f(a)=Err(e),Err(e)f(a)=Ok(b,d1), g(b)=Err(e),Ok(c,d1∨d2)f(a)=Ok(b,d1), g(b)=Ok(c,d2).(g \mathbin{\bar\circ} f)(a) = \begin{cases} \mathrm{Err}(e) & f(a) = \mathrm{Err}(e),\\ \mathrm{Err}(e) & f(a) = \mathrm{Ok}(b, d_1),\ g(b) = \mathrm{Err}(e),\\ \mathrm{Ok}(c, d_1 \vee d_2) & f(a) = \mathrm{Ok}(b, d_1),\ g(b) = \mathrm{Ok}(c, d_2). \end{cases}

This is the writer monad over (D,∨,0)(D, \vee, \mathbf{0}) stacked on the error monad, and the monoid laws above are exactly what makes ∘ˉ\bar\circ associative with ArithmeticOutcome::exact as its identity.33 Associativity of ∘ˉ\bar\circ reduces to associativity of ∨\vee on the diagnostics and of function composition on the values; the identity law reduces to d∨0=dd \vee \mathbf{0} = d. 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 p=24p = 24. An integer nn with ∣n∣≤224|n| \le 2^{24} has at most 24 significant bits (or is 2242^{24} itself, a power of two), so it is representable; 224+12^{24} + 1 needs 25 significant bits and is not, and round-to-nearest-even sends it to 2242^{24}. The Float instance detects the loss without a wide-integer comparison: binary64 has p=53>31p = 53 > 31, so Double::from_int is exact on every Int, and every binary32 value is a binary64 value, so widening is exact. Hence

double⁡(RN⁡32(n))=double⁡(n)  ⟺  RN⁡32(n)=n,\operatorname{double}(\operatorname{RN}_{32}(n)) = \operatorname{double}(n) \iff \operatorname{RN}_{32}(n) = n ,

and the instance sets inexact and rounded exactly when the conversion lost information. Overflow cannot occur, since ∣n∣≤231<2128|n| \le 2^{31} < 2^{128}.

Error bound of binary powering

PowNatChecked for Float and Double computes xnx^{n} with the loop

acc←1, f←x, k←n;while k>0: if k odd:acc←acc⋅f; k←⌊k/2⌋; if k>0:f←f2.\textit{acc} \leftarrow 1,\ \textit{f} \leftarrow x,\ k \leftarrow n;\quad \text{while } k > 0:\ \text{if } k \text{ odd}: \textit{acc} \leftarrow \textit{acc}\cdot \textit{f};\ k \leftarrow \lfloor k/2 \rfloor;\ \text{if } k > 0: \textit{f} \leftarrow \textit{f}^{2}.

Correctness. In exact arithmetic acc⋅f k=xn\textit{acc}\cdot \textit{f}^{\,k} = x^{n} holds at the start of every iteration. It holds initially, and if k=2j+1k = 2j + 1 then acc f⋅(f2)j=acc f k\textit{acc}\,\textit{f}\cdot(\textit{f}^{2})^{j} = \textit{acc}\,\textit{f}^{\,k}, while if k=2jk = 2j then acc (f2)j=acc f k\textit{acc}\,(\textit{f}^{2})^{j} = \textit{acc}\,\textit{f}^{\,k}. At k=0k = 0 the invariant gives acc=xn\textit{acc} = x^{n}. The loop runs ⌊log⁡2n⌋+1\lfloor \log_2 n \rfloor + 1 times and performs ⌊log⁡2n⌋\lfloor\log_2 n\rfloor squarings and popcount⁡(n)\operatorname{popcount}(n) products, the first of which (1⋅f1 \cdot \textit{f}) is exact. The integer Power instances use the same invariant in Z/2k\mathbb{Z}/2^{k}, where every step is exact.

Rounding error. Give each computed quantity qq that approximates xmx^{m} an error count c(q)c(q) such that q=xm∏i(1+δi)kiq = x^{m}\prod_i (1 + \delta_i)^{k_i} with ∣δi∣≤u|\delta_i| \le u and ∑iki≤c(q)\sum_i k_i \le c(q). Then c(x)=0c(x) = 0, and one rounded product of q1≈xm1q_1 \approx x^{m_1} and q2≈xm2q_2 \approx x^{m_2} gives c≤c(q1)+c(q2)+1c \le c(q_1) + c(q_2) + 1. By induction c(q)≤m−1c(q) \le m - 1:

c(q1q2)≤(m1−1)+(m2−1)+1=(m1+m2)−1,c(q_1 q_2) \le (m_1 - 1) + (m_2 - 1) + 1 = (m_1 + m_2) - 1,

and a squaring is the case q1=q2q_1 = q_2, where the shared error is counted twice. Therefore, in the absence of overflow and underflow,

fl⁡(xn)=xn(1+θn−1),∣θn−1∣≤(1+u)n−1−1≤γn−1:=(n−1)u1−(n−1)u.\operatorname{fl}(x^{n}) = x^{n}(1 + \theta_{n-1}), \qquad |\theta_{n-1}| \le (1 + u)^{n-1} - 1 \le \gamma_{n-1} := \frac{(n-1)u}{1 - (n-1)u}.

The last step is the standard lemma ∣∏i=1k(1+δi)±1−1∣≤γk|\prod_{i=1}^{k}(1+\delta_i)^{\pm 1} - 1| \le \gamma_k for ku<1ku < 1.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 (1+δ)/(1+θn−1)(1 + \delta)/(1 + \theta_{n-1}) gives ∣θ∣≤γn|\theta| \le \gamma_{n}. Binary powering does not improve on the worst-case bound of n−1n - 1 successive multiplications; it reduces the work from n−1n - 1 to O(log⁡n)O(\log n) multiplications.

Checked division

The Float and Double DivChecked instances reject every zero divisor and use the kind to say why. 0/00/0 and ∞/∞\infty/\infty are indeterminate: the limits lim⁡(λt)/t=λ\lim (\lambda t)/t = \lambda take every value as t→0t \to 0 or t→∞t \to \infty, so no quotient is meaningful and the kind is DomainError. For x≠0x \ne 0, x/tx/t diverges as t→0t \to 0, a pole, and the kind is DivisionByZero. This matches IEEE 754’s invalid operation and division by zero exceptions, but is stricter: IEEE returns ±∞\pm\infty for ∞/0\infty/0 and NaN for NaN/0/0 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 x∈Xx \in X, y∈Yy \in Y and definitely_lt(X,Y)\texttt{definitely\_lt}(X, Y), then x<yx < y: the relation is a universal statement over X×YX \times Y, which contains (x,y)(x, y). Dually, if ¬ maybe_eq(X,Y)\neg\,\texttt{maybe\_eq}(X, Y), then x≠yx \ne y.

Monotonicity under refinement. If X′⊆XX' \subseteq X and Y′⊆YY' \subseteq Y, then

definitely_lt(X,Y)⇒definitely_lt(X′,Y′),maybe_eq(X′,Y′)⇒maybe_eq(X,Y),\texttt{definitely\_lt}(X, Y) \Rightarrow \texttt{definitely\_lt}(X', Y'), \qquad \texttt{maybe\_eq}(X', Y') \Rightarrow \texttt{maybe\_eq}(X, Y),

because a universal statement survives shrinking its domain and an existential one survives growing it. In three-valued terms, refining the enclosures can turn U\mathsf{U} into T\mathsf{T} or F\mathsf{F} but never turns T\mathsf{T} into F\mathsf{F}. 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 X′X' is inside the old one, contains(X,X′)\texttt{contains}(X, X').

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 T\mathsf{T} 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 Python decimal. Rejected for the reasons under explicit values: it makes results depend on hidden state and leaks flags between computations.
  • A Real or Number super-trait. Rejected because it hides the differences between exact, approximate and enclosure-valued types.
  • Compare for enclosures. Rejected because the definite order is not total.
  • A three-valued result type (True | False | Unknown) for comparisons. The two Boolean projections definitely_* and maybe_eq are enough to reconstruct it, as shown above, and they compose with ordinary if; a separate type would force every caller to handle U\mathsf{U} even when it only asks one direction.
  • A definitely_eq relation. 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 Err would make every rounded operation fail.
  • Option or raise for checked results. Option loses the reason; MoonBit’s raise would put the failure outside the return type. Luna-Flow uses Result with a structured error across repositories.
  • Implementing ConstantsContextual and HyperbolicContextual for 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 Float and Double instances 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 Float and Double; they come from Kaida-Amethyst/math.
  • It does not define algebraic structure (Ring, Field, …), which belongs to luna-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

  1. S. C. Kleene, Introduction to Metamathematics, 1952, §64. Interval comparison with three outcomes goes back to R. E. Moore, Interval Analysis, 1966. ↩

  2. A. Ziv, “Fast evaluation of elementary mathematical functions with correctly rounded last bit”, ACM TOMS 17(3), 1991. How close f(x)f(x) can come to a boundary is the table maker’s dilemma; for most functions no useful a-priori bound on pwp_w is known, which is why the loop needs a budget. ↩

  3. Associativity of ∘ˉ\bar\circ reduces to associativity of ∨\vee on the diagnostics and of function composition on the values; the identity law reduces to d∨0=dd \vee \mathbf{0} = d. See E. Moggi, “Notions of computation and monads”, 1991. ↩

  4. N. J. Higham, Accuracy and Stability of Numerical Algorithms, 2nd ed., SIAM, 2002, Lemma 3.1 and §3.1. ↩