internal design
Design goal
The binary, decimal and interval cores all reduce their hard steps to exact
integer arithmetic: aligning exponents, removing trailing zeros, dividing with
a rounding mode, enclosing a ratio between two dyadics. internal implements
these steps once, as small functions with stated invariants, so that every
core rounds by the same rule and the consistency tests can check each helper
against a BigInt oracle. It is internal so the cores can change the helpers
together without a public compatibility promise.
Mathematical background
Integer division with remainder
For and there are unique integers with and (Euclidean division). Then , and is an integer exactly when . The fractional part is , and
Rounding to an integer
For a real and a rounding direction, is the integer chosen among and : toward zero, toward , toward , away from zero, or to the nearest with ties to the even candidate.11 IEEE 754-2019, clause 4.3, defines the rounding-direction attributes; the integer case is the same definition with as the set of representable numbers.
Dyadic enclosures
A dyadic number is with , . For a real and a scale , the dyadics of scale around are , an interval of width at most .
Design decisions
Magnitude and sign passed separately
round_positive_div(n, d, negative, mode) takes and the sign of the
true quotient as a flag. Sign-magnitude is how the cores store their
coefficients, and directed rounding of a magnitude depends on the sign:
rounding toward increases the magnitude. Keeping the sign as
a separate argument makes the rule a single table and avoids negative
remainders, whose convention differs between languages.
Powers of ten and five from a shared cache
Decimal scaling uses and the binary-decimal conversions use (because and the factor is a shift). The caches start with the 19 powers that fit in 64 bits and are extended on demand up to ; larger powers are computed directly on each call, so that an extreme exponent cannot make the cache grow without bound.
Digit count without strings
digits10 starts from the estimate
, where is the bit length of
, and corrects it by comparison with powers of ten. Since
, the true digit count satisfies
and because the two bounds differ by at most one. So at most
one downward correction is ever needed (the upward loop guards against
floating-point error in the estimate), and the cost is a constant number of
BigInt comparisons plus one power of ten.
Saturating exponent parsing
split_decimal_string caps the written exponent at
while reading digits. Any exponent beyond that is far
outside every supported exponent range, so the value already overflows or
underflows; capping keeps the arithmetic in Int without changing any
observable result.
Canonical rationals
ExactRat::new divides by and makes the denominator positive.
With a canonical form, derived structural equality is equality of rational
numbers, which is what semantic needs to compare values across binary and
decimal representations.
Ziv-style refinement budget
The certified elementary functions evaluate an enclosure at a working precision and accept the result when both ends round to the same target number (Ziv’s strategy22 A. Ziv, “Fast evaluation of elementary mathematical functions with correctly rounded last bit”, ACM TOMS 17(3), 1991. ); otherwise they retry at a higher precision. The budget fixes the schedule
with refinements by default. For this is , geometric growth with ratio , so (about for ). If one attempt at precision costs (at least linear), the total cost of all attempts is dominated by the last one:
which holds, up to the floors in the schedule, for
with in the geometric phase.
Exhaustion is reported with certified_failure, which records the target and
final working precision, instead of returning a value that is not certified.
Correctness / invariants
Rounding table. Let with and
. Then , with only if
, and round_positive_div returns :
Proof. lies in . Toward zero takes the smaller magnitude. Toward takes the larger magnitude for a positive non-integer and the smaller for a negative one; toward is the mirror image. Away from zero takes the larger magnitude for any non-integer. For nearest, the distance to is and to is , so is nearer iff , and is the tie, broken toward the even one of , which is iff is odd.
round_shift(m, s, …) is the case with and
, so the same table applies. The consistency tests check
both functions against these formulas on ties and directed cases.
Factor removal. remove_factor2(sig, e) returns with
, so
and the new significand is
odd. remove_factor10 and trim_trailing_decimal_zeros preserve
in the same way, one factor of 10 per step; the latter stops
after max_drop steps.
Enclosure. certified_dyadic_fraction(n, d, s) returns
and
; from
with we get
and , with equality iff
. The floor of a negative ratio is computed as
, so the enclosure is correct for both signs.
certified_dyadic_div rewrites
with the sign moved to the numerator,
so it inherits the same bound. round_down and round_up are the same floor
and ceiling at a smaller scale, so .
Budget. The precision sequence is strictly increasing (each step adds at
least 32 bits) and at most limit refinements are made, so every refinement
loop that checks available() terminates.
Abort contract. pow2, pow5, pow10 with a negative exponent,
round_positive_div with or , ExactRat::new with ,
and dyadic constructors with a negative scale abort: these are programmer
errors inside the cores, never reachable from user input.
Alternatives rejected
- Rounding via floating point. Converting to
Doubleto round a ratio loses exactness beyond 53 bits; all helpers stay inBigInt. - Signed division with remainder. Truncating and flooring division differ for negative operands; sign-magnitude avoids the ambiguity.
- A normalized
CertifiedDyadic. Normalizing after every operation costs a trailing-zero scan; enclosures are compared withcompare, which does not need canonical forms. - An unbounded power cache. Pathological exponents would retain huge
BigIntvalues for the life of the process.
Boundaries
- No floating-point formats, contexts or flags: the cores build them on these helpers.
- No decimal string formatting and no special values (
inf,nan) insplit_decimal_string. - No public stability: the package is importable only inside
Luna-Flow/floating. - The power caches are process-wide mutable state, the only state in the package; they never change a result.