decimal design

This page explains the arithmetic model of Luna-Flow/floating/decimal: what a decimal floating-point number is, how cohorts and preferred exponents carry information, how the interchange encodings pack digits into bits, how every result is rounded exactly once, which error bounds follow, how the exponent range is enforced, and how elementary functions are certified. The decimal API specifies each function; the decimal tutorial shows them in use.

Design goal

decimal implements the decimal arithmetic of IEEE 754-201911 IEEE Std 754-2019, Standard for Floating-Point Arithmetic, clauses 3.3–3.5 (decimal formats and encodings), 4 (attributes and rounding), 5 (operations), 7 (exceptions) and 9 (recommended operations). The General Decimal Arithmetic specification by M. F. Cowlishaw (version 1.70) gives the same model in an arbitrary-precision form; its terms coefficient, adjusted exponent, Etiny and clamp are used here. for any precision, with three properties:

  1. Every context operation is correctly rounded. The result is the exact mathematical result rounded once, in the selected direction, to the context’s precision and exponent range — for the basic operations and for the elementary functions alike.
  2. Nothing is lost silently. The exponent of a result (its quantum), the sign of zero, NaN payloads and every exceptional condition are part of the returned value or of the returned DecimalFlags.
  3. No hidden state. Precision, rounding, exponent range and flags are ordinary immutable values passed and returned explicitly, as everywhere in Luna-Flow.

Mathematical background

Decimal floating-point numbers

A decimal floating-point format with precision pp and adjusted-exponent range [emin⁡,emax⁡][e_{\min}, e_{\max}] is the set of numbers

x=(−1)s⋅c⋅10q,s∈{0,1},c∈Z, 0≤c<10p,Etiny≤q≤Etop,x = (-1)^s \cdot c \cdot 10^{q}, \qquad s \in \{0,1\},\quad c \in \mathbb{Z},\ 0 \le c < 10^{p},\quad E_{\text{tiny}} \le q \le E_{\text{top}},

together with ±∞\pm\infty and NaNs, where cc is the coefficient, qq the exponent or quantum, and

Etiny=emin⁡−p+1,Etop=emax⁡−p+1.E_{\text{tiny}} = e_{\min} - p + 1, \qquad E_{\text{top}} = e_{\max} - p + 1 .

The adjusted exponent of a non-zero xx is adj⁡(x)=q+digits⁡(c)−1=⌊log⁡10∣x∣⌋\operatorname{adj}(x) = q + \operatorname{digits}(c) - 1 = \lfloor \log_{10} |x| \rfloor: it is the exponent of xx written in scientific notation d0.d1d2…×10adj⁡(x)d_0.d_1d_2\ldots \times 10^{\operatorname{adj}(x)}. A non-zero xx is normal when adj⁡(x)≥emin⁡\operatorname{adj}(x) \ge e_{\min} and subnormal otherwise; the smallest positive subnormal is 10Etiny10^{E_{\text{tiny}}} and the largest finite value is

Nmax⁡=(10p−1)⋅10Etop=10emax⁡+1−10emax⁡−p+1.N_{\max} = (10^{p} - 1)\cdot 10^{E_{\text{top}}} = 10^{e_{\max}+1} - 10^{e_{\max}-p+1}.

DecimalContext stores exactly pp, emin⁡e_{\min}, emax⁡e_{\max}, the rounding mode, clamp (whether exponents above EtopE_{\text{top}} are allowed; see clamping) and the tininess rule. The interchange formats are:

Formatppemax⁡e_{\max}emin⁡e_{\min}EtinyE_{\text{tiny}}EtopE_{\text{top}}bias =−Etiny=-E_{\text{tiny}}exponents Etop−Etiny+1E_{\text{top}}-E_{\text{tiny}}+1
decimal32796−95-95−101-10190101192=3⋅26192 = 3\cdot 2^{6}
decimal6416384−383-383−398-398369398768=3⋅28768 = 3\cdot 2^{8}
decimal128346144−6143-6143−6176-61766111617612288=3⋅21212288 = 3\cdot 2^{12}

IEEE 754 fixes emin⁡=1−emax⁡e_{\min} = 1 - e_{\max}, so the number of exponents is Etop−Etiny+1=emax⁡−emin⁡+1=2emax⁡E_{\text{top}} - E_{\text{tiny}} + 1 = e_{\max} - e_{\min} + 1 = 2e_{\max}; the standard chooses emax⁡=3⋅2w−1e_{\max} = 3 \cdot 2^{w-1} so that this count is 3⋅2w3 \cdot 2^{w}, which is exactly what two leading exponent bits taking the values 0,1,20,1,2 and ww further bits can encode (see encodings). The bias that turns qq into a non-negative stored exponent is −Etiny=p−emin⁡−1-E_{\text{tiny}} = p - e_{\min} - 1: for decimal64, 16+383−1=39816 + 383 - 1 = 398.

Why decimal

A rational number n/mn/m in lowest terms has a finite expansion in base bb if and only if every prime factor of mm divides bb. For b=2b = 2 the only admissible denominators are powers of two; for b=10b = 10 they are 2i5j2^{i}5^{j}. So every binary floating-point number has a finite decimal expansion, but 0.1=1/(2⋅5)0.1 = 1/(2\cdot 5) has none in binary: the Double nearest to it is

0.1000000000000000055511151231257827021181583404541015625=3602879701896397255.0.1000000000000000055511151231257827021181583404541015625 = \frac{3602879701896397}{2^{55}} .

Quantities defined in decimal — prices, rates, measurements, protocol fields — are therefore represented exactly by decimal floating point, and decimal rounding happens at the decimal places a person or a regulation specifies. The price is a larger wobble (below) and more expensive digit arithmetic.

Cohorts and quantum

The map (s,c,q)↦(−1)sc 10q(s, c, q) \mapsto (-1)^s c\,10^{q} is not injective. All representations of the same non-zero value form its cohort. If cc has dd digits and kk trailing zeros, the members are (c⋅10j, q−j)(c\cdot 10^{j},\, q - j) for −k≤j≤p−d-k \le j \le p - d, ignoring the exponent range, so the cohort has p−d+k+1p - d + k + 1 members. For example, 10001000 in decimal32 (c=1c = 1, q=3q = 3, d=1d = 1, k=0k = 0) has the seven members 1E+3,10E+2,…,1000000E-31\text{E+}3, 10\text{E+}2, \ldots, 1000000\text{E-}3. A zero has a member for every exponent.

The cohort member carries information numeric equality does not: 12.30 states two decimal places, 1.2E+3 states two significant digits. IEEE 754 therefore specifies, for each operation, a preferred exponent, and an exact result is delivered in the member whose exponent is closest to it. The preferred exponents follow from where the exact result naturally lives:

ca10qa±cb10qb=(ca10qa−m±cb10qb−m) 10m,m=min⁡(qa,qb),ca10qa⋅cb10qb=(cacb) 10qa+qb,ca10qa/cb10qb=(ca/cb) 10qa−qb,c 10q=c 10q−2⌊q/2⌋  10⌊q/2⌋,x⋅y+z: min⁡(qx+qy, qz).\begin{aligned} c_a 10^{q_a} \pm c_b 10^{q_b} &= \bigl(c_a 10^{q_a - m} \pm c_b 10^{q_b - m}\bigr)\,10^{m}, & m &= \min(q_a, q_b),\\ c_a 10^{q_a} \cdot c_b 10^{q_b} &= (c_a c_b)\,10^{q_a + q_b},\\ c_a 10^{q_a} / c_b 10^{q_b} &= (c_a / c_b)\,10^{q_a - q_b},\\ \sqrt{c\,10^{q}} &= \sqrt{c\,10^{q - 2\lfloor q/2\rfloor}}\;10^{\lfloor q/2 \rfloor},\\ x\cdot y + z &: \ \min(q_x + q_y,\ q_z). \end{aligned}

For the sum and the product, the bracketed coefficient is an integer, so the preferred exponent is attained whenever the exact coefficient fits in pp digits: 1.20 + 3.40 = 4.60 and 1.25 × 2.50 = 3.1250. For the quotient the exact result exists only when ca/cbc_a / c_b has a finite decimal expansion; it is then moved toward qa−qbq_a - q_b as far as pp digits allow, so 2.400 / 1.2 = 2.00. An inexact result always uses all pp digits, which is the member with the smallest exponent. quantize makes the exponent an explicit argument, and reduce_ctx/normalized choose the member with the largest exponent.

///|
test "design: preferred exponents" {
  let ctx = @decimal.DecimalContext::decimal64()
  let d = fn(s : String) { @decimal.Decimal::from_string(s).unwrap() }
  inspect(d("1.20").add_ctx(d("3.40"), ctx).0, content="4.60")
  inspect(d("1.25").mul_ctx(d("2.50"), ctx).0, content="3.1250")
  inspect(d("2.400").div_ctx(d("1.2"), ctx).0, content="2.00")
  inspect(d("0.0400").sqrt_ctx(ctx).0, content="0.20")
  inspect(d("1.5").fma_ctx(d("2.0"), d("0.25"), ctx).0, content="3.25")
}

Rounding directions

Let x>0x > 0 be exact and let the target exponent be tt (the exponent that leaves pp digits, or EtinyE_{\text{tiny}} for a tiny result). Write

x⋅10−t=c+f,c∈Z≥0, 0≤f<1.x \cdot 10^{-t} = c + f, \qquad c \in \mathbb{Z}_{\ge 0},\ 0 \le f < 1 .

Each rounding direction returns cc or c+1c + 1 (times 10t10^{t}); the choice depends on ff, the last digit of cc and the sign:

ModeIEEE namereturns c+1c+1 when (f>0f > 0)
DownroundTowardZeronever
Up—always
CeilingroundTowardPositivex>0x > 0
FloorroundTowardNegativex<0x < 0
HalfUproundTiesToAwayf≥12f \ge \tfrac12
HalfDown—f>12f > \tfrac12
HalfEvenroundTiesToEvenf>12f > \tfrac12, or f=12f = \tfrac12 and cc odd
ZeroFiveUp—c mod 5=0c \bmod 5 = 0

For a negative xx the same rule is applied to ∣x∣|x| and the sign restored, so Ceiling and Floor swap. Every mode ∘\circ is monotone: x≤y⇒∘(x)≤∘(y)x \le y \Rightarrow \circ(x) \le \circ(y). Monotonicity is what the certification of elementary functions relies on.

Error model

Let xx be in the normal range, 10e≤∣x∣<10e+110^{e} \le |x| < 10^{e+1}. Representable numbers in that decade are spaced by one unit in the last place, ulp⁡(x)=10e−p+1\operatorname{ulp}(x) = 10^{e - p + 1}. Rounding to nearest is off by at most half of it, so

∣fl⁡(x)−x∣∣x∣≤12 10e−p+110e=12 101−p=:u,equivalentlyfl⁡(x)=x(1+δ), ∣δ∣≤u.\begin{aligned} \frac{|\operatorname{fl}(x) - x|}{|x|} \le \frac{\tfrac12\, 10^{e-p+1}}{10^{e}} = \tfrac12\, 10^{1-p} =: u , \end{aligned} \qquad\text{equivalently}\qquad \operatorname{fl}(x) = x(1 + \delta),\ |\delta| \le u .

The bound is attained near the bottom of a decade. Near the top, ∣x∣≈10e+1|x| \approx 10^{e+1}, the same absolute error is only 1210−p\tfrac12 10^{-p} relative. The ratio between the worst and the best relative error inside one decade is therefore

12 101−p12 10−p=10=β,\frac{\tfrac12\,10^{1-p}}{\tfrac12\,10^{-p}} = 10 = \beta ,

the wobble of base β=10\beta = 10.22 Goldberg, “What every computer scientist should know about floating-point arithmetic”, ACM Computing Surveys 23(1), 1991, §1.2; Higham, Accuracy and Stability of Numerical Algorithms, 2nd ed., SIAM 2002, §2.1–2.2. In binary the wobble is 2, so for the same storage decimal has a slightly worse worst-case relative error: decimal64 has u=1210−15=5⋅10−16u = \tfrac12 10^{-15} = 5\cdot 10^{-16}, binary64 has u=2−53≈1.1⋅10−16u = 2^{-53} \approx 1.1 \cdot 10^{-16}. The directed modes have ∣δ∣<101−p=2u|\delta| < 10^{1-p} = 2u. The machine epsilon, the distance from 1 to the next larger number, is 101−p10^{1-p} — epsilon_contextual returns it and next_plus(1) in decimal64 is 1.000000000000001. Below 1 the spacing is ten times smaller: next_minus(1) is 0.9999999999999999.

For a subnormal result the spacing is the fixed 10Etiny10^{E_{\text{tiny}}}, so the error bound becomes absolute, ∣fl⁡(x)−x∣≤1210Etiny|\operatorname{fl}(x) - x| \le \tfrac12 10^{E_{\text{tiny}}}. Because every context operation is correctly rounded, the standard model fl⁡(a∘b)=(a∘b)(1+δ)\operatorname{fl}(a \circ b) = (a \circ b)(1+\delta), ∣δ∣≤u|\delta| \le u, holds for +,−,×,/, +,-,\times,/,\sqrt{\ }, fma and every elementary function whose result is normal, so the classical forward and backward error analyses of Higham carry over with u=12101−pu = \tfrac12 10^{1-p}.

Design decisions

One representation, many contexts

Problem. Applications need decimal32/64/128 and arbitrary precision, IEEE semantics and GDA compatibility, without converting between types.

Options. One type per format (like hardware); one arbitrary-precision type with the format carried by a context; a type parameterised by the format.

Choice. One Decimal type holding a sign, a coefficient of any length, an exponent, a class, a NaN-kind bit and a working-precision field, and a separate immutable DecimalContext. A decimal64 computation is a computation under DecimalContext::decimal64(); the interchange encoders apply the format context before encoding. This keeps the arithmetic in one place, lets a caller work at 50 digits and round to decimal64 at the end, and matches the General Decimal Arithmetic model. The precision field in the value serves only the context-free operators and conversions.

Round once, in one place

Problem. Double rounding — rounding an already rounded value again — can change a correctly rounded result.

Choice. Every finite context result goes through one finalization routine that receives an exact result (s,C,Q)(s, C, Q), with CC possibly much longer than pp, and performs, in order: rounding to pp digits, the overflow check, rounding to the subnormal grid, the subnormal/underflow flags and the fold-down. Operation kernels compute exact integers; they never round. The exceptions are operations whose exact result is infinite — division, square root, elementary functions — which are described below; each of them still decides the last digit from exact information.

How the rounding digit and sticky information are obtained

Rounding CC (a non-negative integer with DD digits) to D−sD - s digits divides by a power of ten,

C=Q⋅10s+R,0≤R<10s,C = Q \cdot 10^{s} + R, \qquad 0 \le R < 10^{s},

and decides between QQ and Q+1Q+1 by comparing 2R2R with 10s10^{s}. This single comparison carries exactly the information of the classical rounding digit and sticky bit. Write R=r 10s−1+R′R = r\,10^{s-1} + R' with rounding digit r∈{0,…,9}r \in \{0,\ldots,9\} and rest 0≤R′<10s−10 \le R' < 10^{s-1}. Then

2R−10s=2(r−5) 10s−1+2R′,2R - 10^{s} = 2(r - 5)\,10^{s-1} + 2R' ,

and since 0≤2R′<2⋅10s−10 \le 2R' < 2\cdot 10^{s-1}:

r≥6  ⟹  2R−10s≥2⋅10s−1>0,r=5  ⟹  sign⁡(2R−10s)=sign⁡(R′),r≤4  ⟹  2R−10s≤−2⋅10s−1+2R′<0.\begin{aligned} r \ge 6 &\implies 2R - 10^{s} \ge 2\cdot 10^{s-1} > 0,\\ r = 5 &\implies \operatorname{sign}(2R - 10^{s}) = \operatorname{sign}(R'),\\ r \le 4 &\implies 2R - 10^{s} \le -2\cdot 10^{s-1} + 2R' < 0 . \end{aligned}

So 2R>10s2R > 10^{s}, 2R=10s2R = 10^{s} and 2R<10s2R < 10^{s} mean “above, at, below the midpoint”, which is all the half modes need; R≠0R \ne 0 is the sticky information the directed modes need; and the last digit of QQ is all ZeroFiveUp and HalfEven need. When s≥Ds \ge D the whole coefficient is discarded, Q=0Q = 0, and only s=Ds = D can reach the midpoint: the code then compares CC with 5⋅10D−15\cdot 10^{D-1}. A carry that turns 10p−110^{p}-1 into 10p10^{p} is removed by one exact division by 10 and an exponent increment.

rounded is raised whenever s>0s > 0 and inexact whenever R≠0R \ne 0; trailing zeros are dropped before rounding so that discarding zeros raises only rounded.

Division

The quotient a/ba/b of two finite non-zero values takes one of three exact routes, in this order.

  1. Divisor a power of ten. The quotient is aa with a shifted exponent.
  2. Terminating quotient. Reduce ca/cbc_a / c_b by g=gcd⁡(ca,cb)g = \gcd(c_a, c_b) to ca′/cb′c_a'/c_b'. The quotient has a finite decimal expansion if and only if cb′=2i5jc_b' = 2^{i}5^{j}; with k=max⁡(i,j)k = \max(i, j), ca′2i5j=ca′ 2k−i 5k−j10k,\frac{c_a'}{2^{i}5^{j}} = \frac{c_a'\,2^{k-i}\,5^{k-j}}{10^{k}}, so the exact result is the integer ca′2k−i5k−jc_a' 2^{k-i}5^{k-j} with exponent qa−qb−kq_a - q_b - k, handed to the finalizer like any exact result.
  3. Non-terminating quotient. The result is certainly inexact. Let ac=adj⁡(ca/cb)a_c = \operatorname{adj}(c_a/c_b), found exactly by comparing cac_a with cbc_b scaled by the right power of ten. Scaling the numerator (or the denominator) by 10p−1−ac10^{p-1-a_c} makes the integer quotient have exactly pp digits: N=QD+RN = Q D + R. The increment decision compares 2R2R with DD — the same midpoint test as above, now with the exact remainder as sticky information — so the quotient is rounded once.

The third route is used for normal results in extended contexts. For subnormal results and subset contexts the code computes p+digits⁡(cb)+2p + \operatorname{digits}(c_b) + 2 digits, rounds them in the context mode, and rounds again to the precision and to EtinyE_{\text{tiny}}. The context-free operator / uses that same guarded scheme with HalfEven. Rounding twice to nearest is not always correct: if the first rounding lands exactly on a midpoint of the second, the tie rule of the second rounding decides a case the exact value had already decided. The scheme is therefore exact in every case except those near-midpoints; the operator / shows it, for example, for 15/8329415/83294 at five digits (0.00018008 instead of 0.00018009). div_ctx on normal results does not have this weakness.

ZeroFiveUp exists precisely to make such two-step schemes safe. If xx is first rounded with ZeroFiveUp to p+kp + k digits (k≥1k \ge 1) and then with any mode ∘\circ to pp digits, the result equals ∘(x)\circ(x): an inexact ZeroFiveUp result ends in a digit other than 0 and 5, so it is never a pp-digit number nor a pp-digit midpoint, and it lies on the same side of every pp-digit number and midpoint as xx. The proof is in the attachment.33 Rounding proofs for decimal contains the full proofs of the double-rounding lemma, the overflow table, the fold-down bound, the certification lemma and the NTT bound.

Square root

Let the target exponent be t=max⁡(Etiny, ⌊adj⁡(x)/2⌋−p+1)t = \max(E_{\text{tiny}},\ \lfloor \operatorname{adj}(x)/2 \rfloor - p + 1) and scale the operand to the integer M=c 10q−2tM = c\,10^{q - 2t} (multiplying by 10 first if q−2tq - 2t is odd in the negative branch). The integer square root r=⌊M⌋r = \lfloor \sqrt{M} \rfloor is computed with Newton’s iteration on integers,

ak+1=⌊ak+⌊M/ak⌋2⌋,a0=10⌈(digits⁡(M)+1)/2⌉>M,a_{k+1} = \left\lfloor \frac{a_k + \lfloor M / a_k \rfloor}{2} \right\rfloor , \qquad a_0 = 10^{\lceil (\operatorname{digits}(M)+1)/2 \rceil} > \sqrt{M},

which decreases strictly while ak>⌊M⌋a_k > \lfloor\sqrt M\rfloor (by the AM–GM inequality 12(a+M/a)≥M\tfrac12(a + M/a) \ge \sqrt{M}, and ak+1<aka_{k+1} < a_k iff ak2>Ma_k^2 > M) and stops at ⌊M⌋\lfloor \sqrt M \rfloor. The remainder M−r2M - r^2 is the sticky information. The midpoint test is M≷r+12  ⟺  4M≷(2r+1)2\sqrt{M} \gtrless r + \tfrac12 \iff 4M \gtrless (2r+1)^{2}, and equality is impossible because 4M4M is even and (2r+1)2(2r+1)^2 is odd. A square root is never exactly halfway, so HalfEven, HalfUp and HalfDown agree on it. Rounding directly at the subnormal target exponent avoids rounding twice for tiny roots. Exact roots are detected first (the reduced coefficient is a perfect square after making the exponent even) and returned at the preferred exponent ⌊q/2⌋\lfloor q/2 \rfloor.

Exponent range

The finalizer works on the rounded coefficient C′C' with pp digits and exponent Q′Q', i.e. on the value rounded with an unbounded exponent range.

Overflow

If adj⁡(C′10Q′)>emax⁡\operatorname{adj}(C' 10^{Q'}) > e_{\max} the result overflows. IEEE 754 §7.4 defines the delivered result as the rounding of the exact value in a format with the same pp but unbounded exponent, then saturated: a direction that never increases magnitude cannot leave the finite range, a direction that may increase it goes to infinity. Hence, with Nmax⁡=(10p−1)10EtopN_{\max} = (10^{p}-1)10^{E_{\text{top}}}:

Modex>0x > 0x<0x < 0
HalfEven, HalfUp, HalfDown, Up+∞+\infty−∞-\infty
Down+Nmax⁡+N_{\max}−Nmax⁡-N_{\max}
Ceiling+∞+\infty−Nmax⁡-N_{\max}
Floor+Nmax⁡+N_{\max}−∞-\infty
ZeroFiveUp+Nmax⁡+N_{\max}−Nmax⁡-N_{\max}

The half modes go to infinity because an overflowing exact value is at least Nmax⁡+1210EtopN_{\max} + \tfrac12 10^{E_{\text{top}}} (anything smaller rounds to a finite value and does not overflow), which is at or beyond the midpoint between Nmax⁡N_{\max} and the next power of ten. ZeroFiveUp saturates because the last digit of Nmax⁡N_{\max} is 9. Every overflow raises overflow, inexact and rounded.

///|
test "design: overflow depends on the rounding direction" {
  let ctx = @decimal.DecimalContext::decimal64()
  let big = @decimal.Decimal::from_string("9E+384").unwrap()
  let ten = @decimal.Decimal::from_int(10)
  inspect(big.mul_ctx(ten, ctx).0, content="inf")
  let down = ctx.with_rounding(@def.RoundingMode::TowardZero)
  let (sat, flags) = big.mul_ctx(ten, down)
  inspect(sat, content="9.999999999999999E+384")
  inspect(flags.overflow && flags.inexact, content="true")
}

Subnormals, tininess and underflow

If the exact result needs an exponent below EtinyE_{\text{tiny}}, it is rounded to the subnormal grid: the shift becomes s=Etiny−Qs = E_{\text{tiny}} - Q and the rounding rule above applies with fewer than pp digits kept. The result is tiny when its adjusted exponent is below emin⁡e_{\min}, measured on the exact value (BeforeRounding) or on the value rounded to pp digits with unbounded exponent (AfterRounding, the default). The two rules differ only for values just below 10emin⁡10^{e_{\min}} that round up to it. A tiny result raises subnormal; a tiny and inexact result also raises underflow, as IEEE 754 §7.5 requires for default exception handling. A result that rounds to zero gets exponent EtinyE_{\text{tiny}} and clamped.

Clamping

With clamp (all interchange formats), exponents above EtopE_{\text{top}} are not representable, because the encoding has room only for Etop−Etiny+1E_{\text{top}} - E_{\text{tiny}} + 1 exponents. A result with Q′>EtopQ' > E_{\text{top}} that did not overflow is folded down: the coefficient is multiplied by 10Q′−Etop10^{Q' - E_{\text{top}}} and the exponent set to EtopE_{\text{top}}, raising clamped. This never needs more than pp digits:

digits⁡(C′)+(Q′−Etop)=(adj⁡−Q′+1)+Q′−(emax⁡−p+1)=adj⁡−emax⁡+p≤p,\operatorname{digits}(C') + (Q' - E_{\text{top}}) = \bigl(\operatorname{adj} - Q' + 1\bigr) + Q' - (e_{\max} - p + 1) = \operatorname{adj} - e_{\max} + p \le p ,

using adj⁡≤emax⁡\operatorname{adj} \le e_{\max}. The value is unchanged; only the cohort member changes. In decimal32, 1E+96 is stored as 1000000E+90.

///|
test "design: fold-down in decimal32" {
  let ctx = @decimal.DecimalContext::decimal32()
  let (x, flags) = @decimal.Decimal::from_string_ctx("1E+96", ctx)
  inspect(x.coefficient(), content="1000000")
  inspect(x.exponent10(), content="90")
  inspect(flags.clamped, content="true")
}

Zeros have no digits to pad, so their exponent is simply clamped into [Etiny,Etop][E_{\text{tiny}}, E_{\text{top}}] (or [Etiny,emax⁡][E_{\text{tiny}}, e_{\max}] without clamp), raising clamped when it changes.

Flags as returned values

Problem. IEEE 754 status flags are sticky process state in hardware; GDA adds traps. Hidden state conflicts with the Luna-Flow rule that semantics are explicit, and makes concurrent or compositional code fragile.

Choice. Each context operation returns its own DecimalFlags. combine is the field-wise OR, so flag sets form a commutative idempotent monoid with identity DecimalFlags::new(): accumulating them over a pipeline in any grouping gives the same set, which is exactly the sticky-flag semantics without the state. decimal_checked packages that accumulation; decimal_gda implements sticky status and traps as a separate model. The five IEEE exceptions map to invalid_operation, division_by_zero, overflow, underflow and inexact; the GDA conditions (rounded, subnormal, clamped, lost_digits, conversion_syntax, division_impossible, division_undefined, invalid_context) refine them.

Quantize and same-quantum

x.quantize(y) returns the value of xx with exponent exactly t=qyt = q_y:

quantize⁡(c 10q, t)={c 10q−t⋅10tq≥t (exact padding),∘ ⁣(c 10q−t)⋅10tq<t (rounding).\operatorname{quantize}(c\,10^{q},\, t) = \begin{cases} c\,10^{q-t} \cdot 10^{t} & q \ge t \text{ (exact padding)},\\ \circ\!\left(c\,10^{q - t}\right)\cdot 10^{t} & q < t \text{ (rounding)} . \end{cases}

The result must be representable at that exponent: the new coefficient must have at most pp digits, tt must lie in [Etiny,emax⁡][E_{\text{tiny}}, e_{\max}] and the result’s adjusted exponent must not exceed emax⁡e_{\max}. Otherwise the operation is invalid. It never substitutes another exponent, because the exponent is the contract (an amount quantized to cents must have two decimal places). Rounding a coefficient up can add a digit (9.99→10.09.99 \to 10.0 at two digits of precision), which is why the digit check comes after rounding. same_quantum is the predicate qx=qyq_x = q_y (true for two infinities or two NaNs); it is the test to use before combining values whose exponents must agree.

Interchange encodings

All three formats share one layout: a sign bit, a 5-bit combination field GG, ww exponent continuation bits and a trailing significand of 10J10J bits, where p=3J+1p = 3J + 1:

FormatwwJJ1+5+w+10J1 + 5 + w + 10J
decimal326232
decimal648564
decimal1281211128

The biased exponent E=q+biasE = q + \text{bias} has w+2w + 2 bits whose top two bits take only the values 00, 01, 10; this is the 3⋅2w3\cdot 2^{w} count derived above.

DPD. The combination field holds the top two exponent bits and the leading digit d0d_0: if G=ab cdeG = ab\,cde with ab≠11ab \ne 11, the exponent bits are abab and d0=cde∈[0,7]d_0 = cde \in [0,7]; if G=11 cd eG = 11\,cd\,e with cd≠11cd \ne 11, the exponent bits are cdcd and d0=8+ed_0 = 8 + e. G=11110G = 11110 is infinity and G=11111G = 11111 is NaN, the next bit distinguishing signaling from quiet. The other 3J3J digits are stored in JJ declets, 10 bits for 3 digits, by densely packed decimal.44 M. F. Cowlishaw, “Densely packed decimal encoding”, IEE Proceedings — Computers and Digital Techniques 149(3), 2002. IEEE 754-2019 §3.5.2 gives the encoding tables; the code implements them as Boolean formulas, and the conformance corpus checks all 1024 declets. Three digits have 1000 values and 10 bits 1024 codes, an efficiency of log⁡21000/10=99.66%\log_2 1000 / 10 = 99.66\%. Call a digit small if it is 0–7 (3 bits) and large if it is 8 or 9 (1 bit). The declet pqr stu v wxypqr\,stu\,v\,wxy uses v=0v = 0 for three small digits, which are then stored verbatim in pqrpqr, stustu, wxywxy; v=1v = 1 marks at least one large digit, and wxwx, then stst, say which. Counting by the number of large digits,

83⏟v=0=512,3⋅2⋅82⏟v=1, wx≠11=384,3⋅22⋅8⏟wx=11, st≠11=96,23⏟wx=11, st=11=8,\underbrace{8^3}_{v=0} = 512,\quad \underbrace{3\cdot 2\cdot 8^2}_{v=1,\ wx\ne 11} = 384,\quad \underbrace{3\cdot 2^2\cdot 8}_{wx=11,\ st\ne 11} = 96,\quad \underbrace{2^3}_{wx=11,\ st=11} = 8,

and 512+384+96+8=1000512 + 384 + 96 + 8 = 1000. The first three cases use exactly 512, 384 and 96 codes. The last case has 32 codes for 8 values: rr, uu, yy carry the low bits of the three digits and pp, qq are ignored, so 24 codes are redundant: each value with three large digits has four encodings, of which the one with pq=00pq = 00 is canonical. Decoding accepts all four, canonical() rewrites them. For example, 125 is the declet 0010100101 = 0x0A5 (three small digits) and 999 is 0011111111 = 0x0FF, also written 0x1FF, 0x2FF, 0x3FF.

BID. The coefficient is stored as a binary integer. If the two bits after the sign are not 11, the next w+2w+2 bits are the biased exponent and the remaining 10J+310J + 3 bits the coefficient. Otherwise the exponent follows the 11 and the coefficient is 210J+32^{10J+3} plus the remaining 10J+110J+1 bits (“100” implied). Since 107−1<22410^{7} - 1 < 2^{24}, 1016−1<25410^{16}-1 < 2^{54} and 1034−1<211410^{34}-1 < 2^{114}, every coefficient fits; encodings with a coefficient ≥10p\ge 10^{p} are non-canonical and decode to zero.

///|
test "design: redundant DPD declets decode and canonicalize" {
  let fmt = @decimal.DecimalInterchangeFormat::Decimal64
  let canonical = @decimal.DecimalInterchange::from_hex("#22300000000004FF", fmt).unwrap()
  let redundant = @decimal.DecimalInterchange::from_hex("#22300000000007FF", fmt).unwrap()
  inspect(canonical.to_decimal(), content="19.99")
  inspect(redundant.to_decimal(), content="19.99")
  inspect(redundant.is_canonical(), content="false")
  inspect(redundant.canonical().to_hex(), content="#22300000000004FF")
}

DecimalInterchange keeps the raw bits so that non-canonical input survives until the caller decides to canonicalize; arithmetic is always on Decimal.

Certified elementary functions

Problem. For transcendental ff, f(x)f(x) is irrational for almost every decimal xx, so it can only be approximated; a correctly rounded result needs an approximation good enough to decide the rounding (the table maker’s dilemma).

Options. A fixed-precision evaluation with an a-priori error bound (fast, but correctness then depends on the bound being right for every function and argument); Ziv’s adaptive strategy55 A. Ziv, “Fast evaluation of elementary mathematical functions with correctly rounded last bit”, ACM TOMS 17(3), 1991; J.-M. Muller et al., Handbook of Floating-Point Arithmetic, 2nd ed., Birkhäuser 2018, ch. 12; J. van der Hoeven, “Ball arithmetic”, 2009, for the enclosure model. with rigorous error bounds.

Choice. A Ziv loop over rigorous enclosures. For an operation ff and input xx:

  1. Convert xx exactly into a binary interval: x‾=∇w(x)\underline{x} = \nabla_w(x) and x‾=Δw(x)\overline{x} = \Delta_w(x), to_bin_float with TowardNegative and TowardPositive at ww bits, so x∈[x‾,x‾]x \in [\underline{x}, \overline{x}].
  2. Evaluate ff on that interval with ball_float, which returns an interval [L,U]∋f(x)[L, U] \ni f(x) for every point of the input interval.
  3. Convert LL and UU (dyadic, hence exact decimals) to Decimal exactly and round both with the target context. If both give the same representation (compare_total equal) and the same flags, return it.
  4. Otherwise increase w←w+max⁡(32,⌊w/2⌋)w \leftarrow w + \max(32, \lfloor w/2 \rfloor) and repeat, at most 12 times; then report a certification failure.

The acceptance test is sound because rounding is monotone: L≤f(x)≤UL \le f(x) \le U implies ∘(L)≤∘(f(x))≤∘(U)\circ(L) \le \circ(f(x)) \le \circ(U), and if the outer two are the same representation, so is the middle one. The flags transfer as well, provided f(x)f(x) is not itself representable: then one endpoint differs from the common result, so both are inexact, and overflow, subnormal and underflow are monotone in ∣f(x)∣|f(x)| on an interval that does not contain zero. Representable results are therefore handled before the loop (below).

The initial working precision is w0=max⁡(128, 4max⁡(D,p)+64)w_0 = \max(128,\ 4\max(D, p) + 64) bits, where DD is the number of input digits: one decimal digit needs log⁡210≈3.32<4\log_2 10 \approx 3.32 < 4 bits, so 4max⁡(D,p)4\max(D,p) bits represent the input and the target with margin, and 64 bits more cover the loss of the enclosure. The schedule grows roughly by a factor 3/23/2 per step; after 12 steps the budget is about w0⋅(3/2)12≈130 w0w_0 \cdot (3/2)^{12} \approx 130\,w_0 bits.

Two kinds of inputs never pass the agreement test and are decided before the loop:

  • Exact results. If f(x)f(x) is a representable decimal, L<f(x)<UL < f(x) < U round to different neighbours in a directed mode, forever. The code detects the exact cases (exp(0), ln(1), log⁡1010k\log_{10} 10^{k}, integer and half-integer arguments of sinpi/cospi/tanpi, integer arguments of exp2/exp10, x1/2x^{1/2}, integer powers, …); ball_float returns point intervals for exact binary results such as log⁡28=3\log_2 8 = 3, 83=2\sqrt[3]{8} = 2 and hypot⁡(3,4)=5\operatorname{hypot}(3, 4) = 5. An exact result that neither detects — for example 41.5=84^{1.5} = 8 through power_ctx — is not recognised and runs through the whole refinement budget.
  • Out-of-range results. An enclosure such as [0,tiny][0, \text{tiny}] has endpoints that round with different flags. Any value ≥10emax⁡+2\ge 10^{e_{\max}+2} overflows identically, and any non-zero value ≤10Etiny−2<1210Etiny\le 10^{E_{\text{tiny}}-2} < \tfrac12 10^{E_{\text{tiny}}} rounds identically (to zero or to the smallest subnormal, depending only on the mode and sign), so far endpoints are replaced by such representatives; the binary test uses 3.322>log⁡2103.322 > \log_2 10 so the replacement is conservative. For exp, x≥3(emax⁡+1)x \ge 3(e_{\max}+1) overflows because xlog⁡10e≥3⋅0.434 (emax⁡+1)>emax⁡+1x \log_{10} e \ge 3 \cdot 0.434\,(e_{\max}+1) > e_{\max}+1, and x≤−3 ∣Etiny−1∣x \le -3\,|E_{\text{tiny}} - 1| underflows below 10Etiny−110^{E_{\text{tiny}}-1} by the same estimate. power bounds ylog⁡2xy \log_2 x with directed 128-bit arithmetic for the same purpose.

The elementary functions refuse contexts with pp or ∣e∣|e| above 999,999 (invalid_context), which bounds the size of the exact endpoint conversions.

Coefficient kernels

Problem. Rounding at decimal positions needs fast division by powers of ten and fast digit counts; large precisions need sub-quadratic multiplication and division.

Choice. The package-private DecCoeff is either an inline UInt below 10910^{9} or a little-endian array of base-10910^{9} limbs with no leading zero limb and a cached digit count. Base 10910^9 is the largest power of ten below 2302^{30}, so a limb product is below 1018<26010^{18} < 2^{60}; shifting by a multiple of nine digits moves limbs, and digits10 is a limb count plus the digits of the top limb. BigInt appears only at the public boundary.

Multiplication dispatch:

ShapeAlgorithmCost
one limb eachinlineO(1)O(1)
many zero limbs (na′nb′⋅4<nanbn_a' n_b' \cdot 4 < n_a n_b non-zero limbs)sparse productsO(na′nb′)O(n_a' n_b')
nlong>2nshortn_{\text{long}} > 2 n_{\text{short}}balanced blocks of the short lengthnlongnshortM(nshort)\frac{n_{\text{long}}}{n_{\text{short}}} M(n_{\text{short}})
smallComba columns / schoolbookO(n2)O(n^2)
from the Karatsuba thresholdKaratsubaO(nlog⁡23)=O(n1.585)O(n^{\log_2 3}) = O(n^{1.585})
from the Toom-3 thresholdToom-3O(nlog⁡35)=O(n1.465)O(n^{\log_3 5}) = O(n^{1.465})
from the NTT thresholdtwo-prime NTTO(nlog⁡n)O(n \log n)

The Comba kernel accumulates a column of nn limb products in a UInt64 only when min⁡(na,nb)≤18\min(n_a,n_b) \le 18: a column sum plus incoming carry is at most 18(109−1)2+2⋅1010<1.8⋅1019<26418(10^{9}-1)^{2} + 2\cdot 10^{10} < 1.8\cdot 10^{19} < 2^{64}, while 19 products could exceed 264≈1.845⋅10192^{64} \approx 1.845\cdot 10^{19}.

The NTT splits the coefficients into base-10410^4 digits and convolves them modulo the primes p1=998 244 353=119⋅223+1p_1 = 998\,244\,353 = 119\cdot 2^{23} + 1 and p2=754 974 721=45⋅224+1p_2 = 754\,974\,721 = 45 \cdot 2^{24} + 1, both of which have 2232^{23}-th roots of unity, so transforms up to length 2232^{23} exist. A convolution coefficient is a sum of at most m=min⁡(na,nb)m = \min(n_a, n_b) products of digits below 10410^{4}, hence below m⋅99992m \cdot 9999^{2}; it is recovered exactly from its residues by the Chinese remainder theorem,

z=r1+p1((r2−r1) p1−1 mod p2),z = r_1 + p_1 \bigl((r_2 - r_1)\, p_1^{-1} \bmod p_2\bigr),

as long as m⋅99992<p1p2≈7.54⋅1017m \cdot 9999^{2} < p_1 p_2 \approx 7.54 \cdot 10^{17}, i.e. m<7.5⋅109m < 7.5 \cdot 10^{9} — always true below the transform limit. Base 10910^{9} digits would need m⋅1018<p1p2m\cdot 10^{18} < p_1p_2, impossible for any m≥1m \ge 1, which is why the NTT uses smaller digits. When the bounds do not hold the kernel falls back to Toom-3.

Division uses one-limb division, Knuth’s Algorithm D,66 D. E. Knuth, The Art of Computer Programming, vol. 2, 3rd ed., §4.3.1 (Algorithm D) and §4.3.3; R. Brent and P. Zimmermann, Modern Computer Arithmetic, Cambridge 2010, §1.3–1.4 and §2.4 (NTT); C. Burnikel and J. Ziegler, “Fast recursive division”, MPI-I-98-1-022, 1998. Burnikel–Ziegler recursive division, or Newton reciprocal division. Newton computes r≈S/dr \approx S/d for S=Bn+1S = B^{n+1} by rk+1=⌊rk(2S−drk)/S⌋r_{k+1} = \lfloor r_k (2S - d r_k)/S \rfloor: with rk=S/d−εkr_k = S/d - \varepsilon_k one gets S/d−rk+1≈d εk2/SS/d - r_{k+1} \approx d\,\varepsilon_k^{2}/S, so the number of correct limbs doubles per step. The iteration must increase monotonically from below; if it does not, or the final quotient correction takes more than 2n+82n+8 steps, the routine falls back to Burnikel–Ziegler, which itself falls back to Algorithm D on unsupported shapes.

The crossovers are measured per target and stored in target-specific files:

TargetKaratsuba mul / squareToom-3first NTT mul / squareBurnikel–ZieglerNewton
native96 / 481,1521,728 / 640from 2,816disabled
LLVM96 / 962,0484,096 / 2,0482,0484,096
Wasm / Wasm-GC / JS96 / 964,0968,192 / 4,0962,0484,096

(limbs of nine digits). On native the NTT threshold also depends on the transform length — 1,728, 2,816, 4,608, 7,680, then 8,192 limbs for multiplication and 640, 1,040, 1,824, 3,648, 7,296, then 8,192 for squaring — and the Burnikel–Ziegler entry moves to 5,120 and 10,240 limbs for larger block lengths. These are dispatch boundaries: they change cost, never results. The native Newton path is implemented and tested but disabled, because native measurements do not show a crossover.

Correctness / invariants

  • Representation. A finite Decimal has c≥0c \ge 0; DecCoeff limbs are canonical (no leading zero limb, exact digit count). A zero’s sign is kept in the sign bit; coefficient() never carries a sign.
  • Single rounding. Every context result of +, -, ×, fma, sqrt, quantize, the conversions and (for normal results) / is the exact result rounded once; elementary results are correctly rounded whenever they are returned, and a failure is reported, never approximated.
  • Error bound. For normal results of those operations, fl⁡(x)=x(1+δ)\operatorname{fl}(x) = x(1+\delta) with ∣δ∣≤12101−p|\delta| \le \tfrac12 10^{1-p} in the half modes and ∣δ∣<101−p|\delta| < 10^{1-p} in the directed modes.
  • Exactness is visible. inexact is raised if and only if the returned value differs from the exact result; rounded whenever digits were dropped.
  • Cohort preservation. An exact result that fits is returned at the preferred exponent; quantize either returns exponent qyq_y or fails.
  • Flags. combine is associative, commutative and idempotent with identity new().
  • Ordering. compare is a total preorder (NaNs equal to each other and above every number, −0=+0-0 = +0); compare_total is a total order on representations and refines compare on non-NaN values.
  • Encodings. Decoding then encoding canonical bits is the identity; encoding then decoding a value that fits the format is the identity, including cohort, sign of zero and NaN payload (DPD).
  • Complexity. Comparison, addition, shifts and one-limb division are O(n)O(n) in limbs; multiplication and division follow the dispatch table.

The proofs of the midpoint test, the ZeroFiveUp double-rounding lemma, the overflow table, the fold-down bound, the certification lemma and the kernel bounds are collected in the attachment:

Rounding proofs for decimal

The conformance page records the finite evidence (fixed IEEE corpus, exhaustive declet check, MPFR-certified elementary rows, four targets).

Alternatives rejected

  • A binary BigInt coefficient. Rounding at a decimal position needs a division by 10s10^{s} and a digit count at every operation; with a binary coefficient both are expensive, with base-10910^{9} limbs they are limb moves and one small division.
  • Ambient context and sticky flags. Rejected for explicitness and composability; the flags monoid gives the same information.
  • A compare that aborts on NaN. Sorting and generic Compare code would abort on data containing NaN. The current compare is a total preorder and IEEE semantics are available through compare_checked, compare_ctx, compare_signal_ctx and compare_total.
  • Fixed-precision transcendental kernels with an analytic error bound. Faster for small precisions, but a correctness proof per function and per precision; the enclosure loop is correct by construction and its failure mode is explicit.
  • One package for IEEE and GDA. Sticky status, traps and trap precedence change the type of every operation; they live in decimal_gda.
  • Normalizing every result. Losing the cohort would make 12.30 and 12.3 indistinguishable and break quantum-sensitive protocols.

Boundaries

decimal deliberately does not:

  • keep sticky status or traps (use decimal_checked or decimal_gda);
  • round the context-free operators to a context: * is exact, + and / round to the operand precision only, and none applies an exponent range;
  • guarantee that elementary-function certification succeeds: after the refinement budget (which, at its last steps, works with very wide numbers and can take a long time) it reports CertificationFailure (try_*_ctx) or NaN with invalid_operation (*_ctx);
  • evaluate elementary functions in contexts with precision or exponents above 999,999;
  • preserve NaN payloads through binary conversions, or keep a BID NaN payload whose value precision differs from the format precision;
  • expose its coefficient representation, kernel selection or thresholds;
  • claim conformance beyond the finite evidence on the conformance page.

Footnotes

  1. IEEE Std 754-2019, Standard for Floating-Point Arithmetic, clauses 3.3–3.5 (decimal formats and encodings), 4 (attributes and rounding), 5 (operations), 7 (exceptions) and 9 (recommended operations). The General Decimal Arithmetic specification by M. F. Cowlishaw (version 1.70) gives the same model in an arbitrary-precision form; its terms coefficient, adjusted exponent, Etiny and clamp are used here. ↩

  2. Goldberg, “What every computer scientist should know about floating-point arithmetic”, ACM Computing Surveys 23(1), 1991, §1.2; Higham, Accuracy and Stability of Numerical Algorithms, 2nd ed., SIAM 2002, §2.1–2.2. ↩

  3. Rounding proofs for decimal contains the full proofs of the double-rounding lemma, the overflow table, the fold-down bound, the certification lemma and the NTT bound. ↩

  4. M. F. Cowlishaw, “Densely packed decimal encoding”, IEE Proceedings — Computers and Digital Techniques 149(3), 2002. IEEE 754-2019 §3.5.2 gives the encoding tables; the code implements them as Boolean formulas, and the conformance corpus checks all 1024 declets. ↩

  5. A. Ziv, “Fast evaluation of elementary mathematical functions with correctly rounded last bit”, ACM TOMS 17(3), 1991; J.-M. Muller et al., Handbook of Floating-Point Arithmetic, 2nd ed., Birkhäuser 2018, ch. 12; J. van der Hoeven, “Ball arithmetic”, 2009, for the enclosure model. ↩

  6. D. E. Knuth, The Art of Computer Programming, vol. 2, 3rd ed., §4.3.1 (Algorithm D) and §4.3.3; R. Brent and P. Zimmermann, Modern Computer Arithmetic, Cambridge 2010, §1.3–1.4 and §2.4 (NTT); C. Burnikel and J. Ziegler, “Fast recursive division”, MPI-I-98-1-022, 1998. ↩