decimal Design
decimal is the IEEE-oriented arbitrary-precision decimal core of floating 0.7.1. It combines a quantum-preserving decimal value, explicit contextual rounding and flags, decimal32/64/128 interchange, and certified elementary functions. The separate decimal_gda package owns sticky General Decimal Arithmetic status and traps; the two value types are intentionally not aliases.
Design Contract
The implementation is organized around one semantic rule: coefficient kernels compute exact integer facts, while a shared finalization step alone decides the bounded decimal result. This gives the following pipeline:
Decimal operands + immutable DecimalContext
-> classify special values and operation domain
-> form exact sign/coefficient/preferred exponent
-> round once to the requested precision and mode
-> apply exponent, subnormal, tininess, and clamp policy
-> return Decimal + DecimalFlagsConsequently, a faster multiplication or division algorithm cannot alter rounding, quantum, signed zero, NaN payloads, or flags. These observable states are owned by the decimal layer, not by the coefficient representation.
Representation And Cohorts
A finite Decimal stores an independent sign, non-negative coefficient, base-10 exponent, precision, and special-value state. Its mathematical value is
(-1)^negative × coefficient × 10^exponent10.The same number may have several cohort representations. For example, 1.2300 has quantum -4, whereas 1.23 has quantum -2. Parsing preserves the input exponent when it fits the requested precision; normalized() or reduce_ctx() removes removable powers of ten only when the caller explicitly requests canonical cohort form. Numeric comparison and compare_total are therefore different relations.
The private DecCoeff uses an inline representation for small magnitudes and canonical little-endian base-10^9 UInt limbs for larger values. Limb arrays have no leading zero and carry an exact decimal digit count. BigInt remains at public conversion/serialization boundaries and in test oracles, but decimal hot paths operate on DecCoeff.
IEEE 754 Alignment
DecimalContext::ieee754() and decimal32/64/128 presets make precision, rounding, exponent limits, clamp, and before/after-rounding tininess explicit. Context operations return (Decimal, DecimalFlags); there is no ambient rounding mode or process-global flag register. FMA preserves the exact product through aligned addition and performs one final rounding.
DecimalInterchange implements both DPD and BID encodings for decimal32, decimal64, and decimal128. Decode and encode preserve signed zero, infinity, quiet/signaling NaN, and payload state. Non-canonical finite encodings are handled at this encoding boundary rather than leaking bit-layout rules into ordinary arithmetic.
The checked IEEE matrix covers the operation/encoding rows declared in testdata/decimal/ieee/conformance_matrix.json. GDA-compatible operations that remain on Decimal are compatibility surface, not evidence that this package threads GDA traps. Applications requiring sticky status, fixed trap precedence, or .decTest behavior must use decimal_gda.
Coefficient Algorithm Selection
The selector considers limb count, density, balance ratio, square shape, transform length, and target. Sparse coefficients use compressed nonzero products; strongly unbalanced coefficients are divided into balanced blocks. Dense balanced multiplication follows schoolbook/Comba, Karatsuba, Toom-3, and dual-modulus NTT stages.
| Target | Karatsuba mul / square | Toom-3 mul | first NTT mul / square | BZ division | Newton division |
|---|---|---|---|---|---|
| native | 96 / 48 | 1,152 | 1,728 / 640 | adaptive from 2,816 | disabled |
| LLVM | 96 / 96 | 2,048 | 4,096 / 2,048 | 2,048 | 4,096 |
| Wasm / Wasm-GC / JS | 96 / 96 | 4,096 | 8,192 / 4,096 | 2,048 | 4,096 |
The native NTT boundary is further adjusted by transform size: multiplication uses 1,728, 2,816, 4,608, 7,680, then 8,192 limbs as the transform grows; squaring uses 640, 1,040, 1,824, 3,648, 7,296, then 8,192. Native Burnikel-Ziegler entry likewise moves from 2,816 to 5,120 and 10,240 limbs for larger block lengths. These piecewise boundaries encode measured non-monotonic costs that a single global cutoff would hide.
Algorithm-specific preconditions remain mandatory:
- Karatsuba and Toom-3 stop at bounded recursion depth and use scratch arenas.
- Toom-3 keeps signed interpolation values private and checks every exact small division.
- NTT converts to smaller working digits, checks transform and coefficient bounds, reconstructs with two-prime CRT, and falls back if exact reconstruction cannot be proved.
- Division checks
n = qd + rand0 <= r < d; Newton/Burnikel-Ziegler may fall back to normalized Knuth Algorithm D. - Scratch buffers are rewound only after all returned values have detached from temporary storage.
The native Newton selector is intentionally disabled in 0.7.1 even though the algorithm is implemented and tested: current native measurements do not justify its production crossover. LLVM, Wasm-family, and JS retain it from 4,096 limbs, where the target cost model differs.
Rounding, Exponents, And Quantum
Context finalization receives a signed exact coefficient and preferred exponent. It computes guard/sticky information, applies one of the supported decimal rounding modes, then decides overflow, underflow, subnormal, rounded, inexact, and clamped status. quantize fixes the target exponent and reports invalid_operation when the coefficient cannot fit the context; it never silently changes the requested quantum.
Parsing follows the same model. Decimal::parse/from_string preserve an input quantum that fits the chosen precision; a longer coefficient is rounded once. from_string_ctx additionally returns conversion/rounding flags and applies context exponent policy. This distinction is important for accounting, protocol, and test-vector data where trailing zeros are meaningful.
Certified Elementary Functions
Decimal elementary functions use a proof-producing bridge rather than a host floating-point approximation. Each decimal input is converted twice to a dyadic interval: once toward negative infinity and once toward positive infinity. ball_float evaluates that enclosure, and both exact dyadic endpoints are converted back through integer arithmetic. The result is accepted only when both endpoints produce the same target Decimal and identical DecimalFlags.
The initial working precision is at least 128 bits and otherwise scales from the input digits and target precision. A shared 12-step refinement budget grows precision by max(32, work / 2). try_*_ctx returns a structured certification failure when no unique target rounding cell can be proved; convenience methods use the same certified path and do not substitute an unchecked approximation.
The committed MPFR 4.2.2 oracle independently forms 768-bit directed bounds and contains 2,784 rows across 29 operations, decimal32/64/128, and all eight DecimalRoundingMode values. libmpdec allcr=1 separately checks the GDA-compatible exp, ln, and log10 subset. These are finite evidence sets, not a claim over all real inputs.
Optimization And Boundary Tuning
Maremark measurements separate dense, sparse, balanced, unbalanced, square, kernel-only, and full contextual paths. Candidate and baseline order is rotated to limit warmup/order bias, and threshold tests exercise the exact limb below, at, and above every selected boundary. A candidate path enters production only after exact differential tests and a measured practical advantage both hold.
This produces two classes of boundary:
- semantic boundaries, such as precision, exponent, rounding, and quantum, which are public and must remain stable; and
- dispatch boundaries, such as 1,152 or 1,728 limbs, which are private, target-specific, and may be retuned without changing any result.
Complexity And Trade-offs
For n base-10^9 limbs, comparison, addition, normalization, shifts, and single-limb division are O(n). Schoolbook multiplication and Knuth division are quadratic; Karatsuba, Toom-3, NTT, Burnikel-Ziegler, and Newton reduce large asymptotic cost at the price of setup, temporary storage, and more restrictive preconditions. The selector prefers the simplest proven algorithm until the measured crossover pays for that complexity.
The algorithmic basis follows Knuth Algorithm D, Karatsuba multiplication, Bodrato-style Toom-Cook interpolation, Burnikel-Ziegler division, Brent-Zimmermann multiple-precision methods, Cowlishaw's decimal arithmetic model, and IEEE 754-2019 / ISO 60559 semantics. Repository conformance data and current code determine the exact 0.7.1 claim.
0.7.1 Semantic Preservation Proof
The exact decimal division path reduces the coefficient fraction by its GCD. A finite decimal expansion exists exactly when the reduced denominator is 2^i * 5^j; the implementation checks this factor condition before entering the path. It then constructs an exact coefficient and preferred exponent and passes that value to the same finalizer used by the generic route. The bounded inexact path is similarly guarded and falls back when its bound cannot prove a unique result. The optimization can therefore change allocation and division work, but not quantum, rounding, overflow/underflow, or flags.
The acceptance condition is equality of the complete IEEE observation tuple: class, sign, coefficient, exponent, precision, flags, and interchange value. The fixed IEEE corpus, four-target public API matrix, division/remainder regressions, and exact-path differential tests provide the release evidence; the selected threshold remains a private performance policy.
Evidence Map
- API reference inventories the public surface.
- Tutorial demonstrates quantum, contexts, interchange, and certified operations.
- IEEE conformance defines the finite IEEE evidence claim.
- Performance records the dispatch and benchmark protocol.
decimal_gdaconformance records the separate GDA claim and prevents the two state models from being conflated.