the-q
Bounded rational arithmetic for Rust, with a rounding contract that is machine-checked in Verus.
A value is two i64 words. Arithmetic is exact whenever the exact result fits
that budget, and when it does not, the result is rounded to a proven error
bound in a direction you choose. Nothing overflows, nothing allocates, and
every operation on the total type Q returns a value, never a panic.
use ;
let price = from_decimal.unwrap; // 19.99, exactly
let rate = from_decimal.unwrap; // 0.0825, exactly
let tax = mul; // 1.649175, exactly
assert_eq!;
let third = new.unwrap;
assert_eq!; // no drift
// Division by zero is not a panic on `Q`; it is a value.
assert_eq!;
assert_eq!;
Why
Every representation of a real number in a machine makes a compromise, and the usual ones make it silently.
f64rounds on almost every operation, to a bound that is relative, so0.1 + 0.2 != 0.3and a long sum drifts by an amount no one has written down.Ratio<i64>is exact until the moment it overflows, and then it panics, or wraps, depending on the build profile.- Arbitrary-precision rationals are exact forever, and their denominators grow without bound: a chain of a few thousand operations holds megabytes and each step gets slower than the last.
the-q takes the fourth position. A Rat is a canonical fraction whose
numerator and denominator are each bounded by 2^62 − 1, chosen so that every
cross-multiplied i128 intermediate provably fits. When an exact result would
leave that budget, it is rounded, once, to a dyadic grid, and the rounding
satisfies four properties (R1–R4 below) that are theorems about the code, not
documentation of it. A step rounds only when its reduced result no longer fits
2^62 − 1: sums and products of a handful of small fractions are exact, while
in a long chain the denominators grow until every step rounds, each time by a
proven amount, in the direction you asked for.
The proofs are written in Verus inside the source files. cargo build erases
them and compiles ordinary Rust; cargo verus verify checks them. The verified
count in CI is 1219 verified, 0 errors, with no assume, no admit, and
three trusted functions, all at the f64 boundary or a panic message, none on
an arithmetic path.
This is the right tool when memory must be flat, chains are long or unbounded,
results must be bit-identical across machines, or the per-step error must be a
proven number. For short exact computations use an exact rational; for raw
speed use f64.
Install
[]
= "0.2.1"
# Optional features, all off by default:
# the-q = { version = "0.2.1", features = ["serde", "rust_decimal"] }
| feature | adds |
|---|---|
serde |
Serialize/Deserialize for Rat and Q |
rust_decimal |
conversions from rust_decimal::Decimal |
fixed |
conversions from every 128-bit fixed type |
num-rational |
conversions from Ratio<i64> and BigRational |
num-bigint |
conversions from BigInt |
bigdecimal |
conversions from BigDecimal |
all-integrations |
the five library conversions at once |
MSRV is Rust 1.85, edition 2024. Verus is not needed to build or use the crate; it is needed only to re-check the proofs.
The types
| type | what it is | failure mode |
|---|---|---|
Rat |
a canonical bounded fraction, Copy, 128 bits |
partial: None on a bad constructor argument, panic on a zero divisor, saturation on overflow |
Q |
Rat plus explicit ±Sat, ±Inf, Nan |
total: every operation returns a Q |
QI |
an interval [lo, hi] of Rat with proven enclosure |
panic on lo > hi in new; checked_new |
Exact |
a Rat whose operations refuse to round |
Result<Exact, ExactError> |
Dir |
Down, Up, Nearest: the rounding direction |
|
Sign |
Negative, Zero, Positive: what Q::signum returns |
|
ExactError, ParseQError |
the error enums of Exact and Q: FromStr |
|
MAX_MAG, MAX_DEC_PLACES |
2^62 − 1, and 18, the largest decimal exponent from_decimal accepts |
|
MAX_DECIMAL_SCALE, MAX_DECIMAL_MANTISSA |
28 and 2^96 − 1: the domain of a rust_decimal::Decimal, which from_decimal128_dir accepts in full |
Plus the module nary (reproducible folds over Rat), the
roots and transcendentals on Q, and the
conversions: from_f64_dir, q_from_f64,
to_f64, the i128 cores from_decimal128_* and from_ratio128_*, the
feature-gated library adapters, Display, FromStr and serde.
Everything named in this section follows semver. Modules such as gcd,
model, round, lipschitz, fx, denote, laws_q, soundness* and
helpers such as q::add_n_exec are public only because Verus's visibility
rules demand it, and may change shape in a patch release.
Rat
Rat is num / den in two private i64 fields. It is always canonical
(den > 0, gcd(|num|, den) == 1, and 0 is 0/1) and always bounded
(|num|, den ≤ 2^62 − 1). Because the fields are private, every Rat a
caller can hold came from a constructor and satisfies the invariant, and
because the form is canonical, the derived Eq and Hash are value equality.
It is Copy, Send + Sync, and does not touch the heap.
Constructing
use ;
// Every constructor canonicalises: sign on the numerator, gcd removed.
assert_eq!;
assert_eq!;
assert_eq!;
// `None` for a zero denominator, or a pair that does not fit the budget.
assert_eq!;
assert_eq!;
// Decimal literals are exact; `(1999, 2)` is 19.99.
assert_eq!;
assert_eq!;
// `new_rounded` is total in the numerator: it rounds instead of refusing.
let big = new_rounded.unwrap;
assert_eq!;
assert_eq!;
assert_eq!;
Arithmetic
The plain operations round to nearest, ties to even. The *_dir variants take
a Dir. The checked_* variants return None when the result saturates (and
checked_div also when the divisor is zero). neg, abs, recip, min,
max and clamp are exact. pow_u32 is a fold of mul.
use Rat;
let a = new.unwrap;
let b = new.unwrap;
// Exact when the exact result fits, which is nearly always.
assert_eq!;
assert_eq!; // operators delegate
assert_eq!;
assert_eq!;
assert_eq!;
assert_eq!;
// No `/` operator on `Rat`: `div` has a precondition the operator cannot
// express. Use `Rat::div`, `Rat::checked_div`, or move to `Q`.
assert_eq!;
assert_eq!;
assert_eq!;
assert_eq!;
When rounding happens
Most sums and products of everyday fractions are exact. Rounding occurs only
when the reduced result has a numerator or denominator above 2^62 − 1. Here
is one that does:
use ;
// Reduced denominator of the sum is about 9.2e18, past the 2^62 budget.
let a = new.unwrap;
let b = new.unwrap;
let nearest = add; // rounded, ties to even
let down = add_dir;
let up = add_dir;
assert!; // R2: the exact sum lies in [down, up]
assert!; // so rounding did happen
// `checked_*` on `Rat` reports saturation only, not rounding.
assert!;
let m = new.unwrap;
assert_eq!; // over the budget
assert_eq!; // the plain op saturates
Rat is deliberately partial: Rat::new(_, 0) is None; Rat::div(x, 0)
and Rat::zero().recip() panic (a precondition under Verus, a runtime check
with a message for everyone else); an exact result above the budget saturates
to ±(2^62 − 1), which the checked_* variants report as None and the
plain operations do not. Q turns each of those into a value.
Comparing
use BTreeMap;
use Rat;
let a = new.unwrap;
let b = new.unwrap;
// Exact and total: cross-multiplication, never a float.
assert!;
assert_eq!;
assert_eq!;
assert_eq!;
// `Eq` and `Hash` are structural, and canonical form makes that value
// equality, so `Rat` is a map key.
let mut table = new;
table.insert;
assert_eq!;
assert_eq!;
assert!;
assert!;
Reading it out
use ;
let x = new.unwrap;
assert_eq!; // always `num/den`, always canonical
assert_eq!;
assert_eq!;
assert!; // display only
Q
Q is the total layer. It is a wrapper around Rat, not a rewrite: the
kernel's invariant and proofs are untouched, and Q adds five payload-free
states for the values a Rat cannot hold.
Every operation on Q, including arithmetic, comparison, the folds, and every
root and transcendental, returns a Q and never panics.
use ;
// Every `Rat` failure mode is a `Q` value.
assert_eq!;
assert_eq!;
assert_eq!;
assert_eq!;
assert_eq!;
assert_eq!; // `Q` has a `/` operator
assert_eq!;
// Overflow is reported as saturation, distinct from infinity.
let m = Number;
let over = m + m;
assert_eq!;
assert!;
assert_eq!;
// `checked_*` on `Q` returns `Option<Rat>`: `Some` only for a number.
assert_eq!;
assert_eq!;
assert_eq!;
Three choices differ deliberately from IEEE 754, and each is proven:
- Saturation is finite.
PosSatdenotes the reals above2^62 − 1, not infinity. So0 * PosSat == 0, and there is nois_finite(), because a saturated value is finite. - The order is total.
Nan == Nan, andNansorts last, soQisEq,Hash,Ord, and a valid map key. OutsideNumberthe order is on representations, not values. - Selection propagates
Nan.Q::min,Q::maxandQ::clampreturnNanif either input isNan.Ord::mincannot do that, so a fold ofQ::minis notiter().min().
use ;
// Saturation denotes a finite real, so this is exact where `0 * inf` is not.
assert_eq!;
assert_eq!;
assert_eq!;
assert_eq!;
assert_eq!;
// The order is total and `Nan == Nan`, so `Q` is a map key too.
let mut v = vec!;
v.sort;
assert_eq!;
// Selection propagates `Nan`; `Ord::min` cannot, so do not fold with it.
assert_eq!;
assert_eq!;
// Every non-`Nan` value has a sign.
assert_eq!;
assert_eq!;
assert_eq!;
Folds
Q::sum, Q::product and Q::weighted_mean are fixed left folds. The order
is part of the contract: with rounding, addition is not associative, so fixing
the order is what makes the result bit-identical across machines and threads.
use Q;
let third = new;
assert_eq!;
assert_eq!;
// `(weight, value)` pairs. Total: a zero weight sum is `Nan` or `±Inf`.
let mean = weighted_mean;
assert_eq!;
assert_eq!;
// A `Nan` anywhere in the fold is a `Nan` result.
assert_eq!;
Text
use FromStr;
use ;
// `Display` and `FromStr` round-trip every state.
for q in
assert_eq!;
assert_eq!;
// Input is canonicalised; a bare integer is accepted; a zero denominator
// in *text* is a malformed numeral, not a computation.
assert_eq!;
assert_eq!;
assert_eq!;
assert_eq!;
Which laws survive
Q::add and Q::mul are commutative for every input. Associativity,
distributivity and monotonicity hold only while every operand and every
intermediate is a Number on the exact path; that is the same scope as
Rat's laws, not a wider one. Once a saturation is reachable all three fail,
and Q::div(a, b) differs from Q::mul(a, Q::recip(b)) in the six cells
{NegInf, PosInf, Number(0)} × {PosSat, NegSat}. Each failure has an
executable counterexample in tests/q_laws.rs; the proofs (V11 below) cover
the laws where they hold, and, separately, that every special-value cell of
the propagation tables contains the true result.
use ;
let m = Number;
let neg_m = Number;
// Commutative always.
assert_eq!;
// Associative only while no operand saturates: (m + m) + (-m) hits `PosSat`
// first and `PosSat + NegSat` is indeterminate, but m + (m + -m) is exact.
assert_eq!;
assert_eq!;
// `div` is not `mul` by `recip` once saturation is involved.
assert_eq!;
assert_eq!;
QI
QI is a closed interval [lo, hi] of Rat. Its lower endpoint is always
computed with Dir::Down and its upper with Dir::Up, so enclosure follows
from R2 with no new rounding proofs. It is proven for add, sub, neg and
mul across every sign pattern.
use ;
let a = QInew; // [1/3, 1/2]
let b = QIexact; // [3, 3]
// Endpoints round outward (`lo` down, `hi` up), so enclosure is proven:
// if `x ∈ a` and `y ∈ b` then `x ∘ y ∈ a ∘ b` for `+`, `-`, `*`.
let s = QIadd;
assert_eq!;
assert_eq!;
assert!;
let p = QImul; // sign patterns are all handled
assert_eq!;
assert_eq!;
// `width` is rounded up and returned as a `Q`: `PosSat` if too wide.
assert_eq!;
assert_eq!;
let whole = QInew;
assert_eq!;
assert_eq!;
// `hull` is the smallest interval containing both.
assert_eq!;
// `lo > hi` is a precondition: `new` panics, `checked_new` is `None`.
assert_eq!;
Exact
Exact is a Rat wrapper for callers who would rather fail than round. Its
add, sub, mul and div succeed only when the result needs no rounding,
and return Result<Exact, ExactError> (Inexact, or DivisionByZero for
div) the moment they would leave the exact path. The checked_* variants
return Option instead.
The difference from Rat::checked_* matters: Rat::checked_add is None
only on saturation, and silently rounds otherwise. Exact::checked_add is
None on any rounding.
use ;
let half = new;
assert_eq!;
assert_eq!;
// The same two operands `Rat::add` rounds above: here they are an error.
let a = new;
let b = new;
assert_eq!;
assert_eq!;
assert!; // `Rat` only refuses saturation
// Division by zero is a separate error, not a panic.
assert_eq!;
assert_eq!;
// `Exact` has no `Ord`; compare with `Exact::le`.
assert!;
assert!;
What you buy with the refusal: associativity, distributivity and monotonicity,
absent in general once rounding happens (see Limits), hold for any
chain of Exact operations in which every step succeeded. That is sufficient,
not claimed necessary, and it is proven (theorem_exact_add_associative,
theorem_exact_mul_associative, theorem_exact_distributive,
theorem_exact_add_monotone, theorem_exact_mul_monotone_nonneg).
Roots and transcendentals
sqrt, cbrt, hypot; exp, exp2, powf, pow_i32, ln, log2,
log10, log; sin, cos, tan, asin, acos, atan, atan2; sinh,
cosh, tanh; and the constants pi, e, ln2, ln10, half_pi in the
transcendental module. They live on Q because none of them is
rational-closed. All are total, and all are fixed-length series, so termination
is structural and cost is constant.
use transcendental;
use ;
// Exact where the answer is rational.
assert_eq!;
assert_eq!;
assert_eq!;
assert_eq!;
assert_eq!; // a fold of `mul`
// Otherwise a bounded rational near the true value; accuracy is measured,
// not proven (see the table below).
let Number = pi else ;
assert!;
let Number = one.exp else ;
assert!;
// `cbrt(8)` goes through `exp(ln)` and is a hair off 2, not 2. The
// logarithms at powers of their base are exact by a proven postcondition.
let Number = new.cbrt else ;
assert_ne!;
assert!;
assert_eq!;
assert_eq!;
// Total: every domain edge is a value, never a panic.
assert_eq!;
assert_eq!;
assert_eq!; // above the budget
assert_eq!; // below the grid
assert_eq!; // the image reaches back inside
assert_eq!;
assert_eq!;
Domains: exp returns PosSat above x = 43.67 and 0 below −44; sin,
cos, tan return Nan for |x| > 2^20; sqrt(PosSat) and ln(PosSat) are
Nan because the image of the saturated region reaches back inside the budget.
Accuracy is measured, not proven. Totality and termination are theorems;
the error figures are observations. Two independent oracles were used, mpmath
at 120 decimal digits and exact rational series in Common Lisp carried to
2^-160, and they agree with each other to better than 2^-98 on every
function. Worst observed error over the sweeps (tests/transcendental.rs and
scripts/accuracy/), in R3's own metric |got − true| / max(1, |true|):
| function | worst error | note |
|---|---|---|
exp, cosh, sinh, tanh |
2^-61 |
|
e |
2^-63 |
|
sqrt |
2^-61 |
|
hypot |
2^-62 |
|
cbrt, powf |
2^-60 |
via exp(ln) |
pi, ln2, atan2 |
2^-60 |
|
ln, log2 |
2^-59 |
|
log10 |
2^-61 |
|
atan, asin, acos |
2^-58 |
|
sin, cos, tan |
2^-59 at |x| ≤ 1, 2^-57 at ≤ 8, 2^-50 at ≤ 2^10, 2^-40 at ≤ 2^20 |
reduction error grows as |x| · 2^-60 |
exp2 |
|x| · 2^-61 (2^-55 at x = 40) |
argument scaled by a rounded ln 2 |
Read the table with the R3 caveat below: these are absolute bounds below 1.
asin(0.002) is 2^-53 relative; exp(−40) is 2^-3; exp(−44) is 0.
tan near a pole inherits cos's absolute error divided by cos², so it is
unbounded there. ln within 2^-8 of 1 keeps a small-denominator rational and
is often far better than 2^-61 absolute, but not uniformly: 2^-52 relative
at 1 ± 2^-24, 2^-124 at 1 ± 2^-60.
pi, e, ln2, ln10 are literals; each has a *_series derivation and a
test that the two are bit-identical.
N-ary folds
The nary module has the same three folds as Q for callers who stay on
Rat. sum and product are total. weighted_mean takes (weight, value)
pairs and is None when the rounded weight sum is zero, whether the weights
cancel or their sum is below the grid.
use Rat;
use nary;
let third = new.unwrap;
assert_eq!;
assert_eq!;
assert_eq!;
// `(weight, value)` pairs; `None` when the rounded weight sum is zero.
let pairs = ;
assert_eq!;
assert_eq!;
The accumulation bounds are proven (V8 below): sum and product are within
k · m · 2^-61 after k steps, with |factor| ≤ 1 for product, and
weighted_mean is within 8k · 2^-61 / δ when weights and values lie in
[0, 1] and the exact weight sum is at least δ.
Conversions and serialisation
f64 in, by from_f64_dir (partial, directed) or q_from_f64 (total).
f64 out, by to_f64, which is for display: it makes three roundings and is
one of the three trusted functions (TRUSTED.md). There is deliberately no
Q → f64, because no float honestly denotes PosSat.
use ;
// `0.1f64` is not one tenth; the conversion is exact about the double.
let tenth = from_f64_dir.unwrap;
assert_eq!;
assert_ne!;
// Directed conversion brackets the double when it does not fit exactly.
let lo = from_f64_dir.unwrap;
let hi = from_f64_dir.unwrap;
assert!;
// `None` for NaN, infinity, and magnitudes above 2^61.
assert_eq!;
assert_eq!;
// `q_from_f64` is total. There is no `Q -> f64`: nothing denotes `PosSat`.
assert_eq!;
assert_eq!;
assert_eq!;
assert_eq!;
// `to_f64` is for display; three roundings, do not feed it back in.
assert_eq!;
Wider inputs: i128 pairs and decimals
Two always-available cores take anything an i128 can express. Both have an
exact variant that returns None the moment ingestion itself would have to
round, so an Exact built from it denotes the original value, not merely a
Rat that needs no further rounding.
use ;
use ;
// `mantissa · 10^-scale` with an `i128` mantissa and a scale up to 28:
// the domain of a `rust_decimal::Decimal`, exact whenever it fits.
assert_eq!;
assert_eq!;
assert_eq!;
// A 96-bit mantissa at scale 28 does not reduce into the budget: the
// directed conversion rounds, the exact one refuses.
let lo = from_decimal128_dir.unwrap;
let hi = from_decimal128_dir.unwrap;
assert!;
assert_eq!;
// Any `i128` pair, sign-normalised: the core every library adapter uses.
assert_eq!;
assert_eq!;
assert_eq!;
assert!;
Other numeric libraries
Each feature-gated adapter reduces its library's representation to an i128
pair and hands it to the cores above. Each comes in the same three tiers: a
directed Rat function, a total Q function (also impl From) that rounds to
nearest and saturates by sign, and an Exact function (also impl TryFrom,
except for fixed) that refuses rather than rounds.
use Decimal;
use ;
use ;
let d = new; // 0.85
assert_eq!;
assert_eq!;
assert_eq!;
// Past the budget: `Q` saturates by sign, `Exact` refuses.
assert_eq!;
assert_eq!;
use I64F64;
use ;
use ;
// One generic function for every 128-bit-backed `fixed` type.
let v = I64F64from_num;
assert_eq!;
assert_eq!;
assert_eq!;
assert_eq!;
fixed gets no From/TryFrom impls: a blanket impl over every Fixed type
cannot coexist with the concrete impl for Decimal.
use BigInt;
use ;
use ;
use ;
// `Ratio<i64>` is always exact: its invariant is the one `Rat` needs.
assert_eq!;
assert_eq!;
// `BigRational` needs an `i128` extraction first; when its reduced terms
// are too wide, the `Q` conversion falls back to `to_f64` and the `Rat`
// conversion is `None`.
let v = new;
assert_eq!;
let wide = new;
assert_eq!;
assert_eq!; // via f64, lossy
The to_f64 fallback exists because Ratio keeps its terms in lowest form and
nothing bounds those by the value's magnitude: (10^40 + 1) / 10^40 is a
value near 1 whose terms fit no i128. Guessing a saturation sign would be
wrong, so the Q conversion goes through a double instead and says so in its
docs.
use BigInt;
use ;
use ;
assert_eq!;
// An integer's magnitude is its value, so "too wide" always means saturation.
let huge = from.pow;
assert_eq!;
assert_eq!;
use BigDecimal;
use FromStr;
use ;
use ;
let v = from_str.unwrap;
assert_eq!;
assert_eq!;
// A negative scale is a large integer, still exact.
assert_eq!;
assert_eq!;
With the serde feature, Rat encodes as the exact [num, den] pair and Q
as that pair or the special's string. The encoding is untagged, so it decodes
only in self-describing formats. Decoding re-canonicalises through Rat::new
and rejects a malformed payload rather than constructing an invalid value.
use ;
// `Rat` is the exact `[num, den]` pair; `Q` specials are strings.
let x = new.unwrap;
assert_eq!;
assert_eq!;
assert_eq!;
assert_eq!;
// Decoding re-canonicalises and rejects a malformed pair.
assert_eq!;
assert!;
The rounding contract
When an exact result does not fit, it snaps to a dyadic grid chosen per
magnitude: with k = bitlen(floor(|x|)), the grid step is 2^-(62-k), capped
at 2^-61. Ties go to even. Proven, for every operation:
| rule | statement |
|---|---|
| R1 | a representable exact result is returned unchanged |
| R2 | Dir::Down ≤ exact ≤ Dir::Up |
| R3 | error ≤ 2^-61 · max(1, |exact|) in every direction; 2^-62 for Dir::Nearest, which add/sub/mul/div use |
| R4 | rounding is monotone on a fixed grid |
R3 is absolute below 1. A result near 1 carries about 61 significant bits;
a result near 2^-40 carries about 21; below 2^-62 a value rounds to zero.
This is the single most important thing to know before using the crate for
small quantities, and it governs every accuracy figure above. If small values
matter, scale the problem.
Limits
Not associative. add and mul are commutative with rounding. They are
associative and distributive only on the exact path (no intermediate rounds),
and that is proven. The general failure is bounded:
|((a+b)+c) − (a+(b+c))| ≤ 4 · 2^-61 · m for any bound m ≥ 1 on the partial
sums, and |((a·b)·c) − (a·(b·c))| ≤ 6 · 2^-61 on [0, 1]. Exact gives the
laws back in exchange for refusing to round.
Not globally monotone. R4 holds on each grid, not across the
representable/rounded boundary. Counterexample (tests/adversarial.rs):
u = 2 / (2^61 + 1) representable, returned exactly
v = u · (1 + 1/(2^62 − 2)) a hair larger, not representable
round_down(v) = 2^-61 < u = round_down(u) though u < v
The cause is that round has no single codomain: a representable input is
returned from the whole bounded-denominator set, an unrepresentable one is
snapped to the far coarser dyadic subgrid. Projection onto any one fixed set
would be monotone. Fixing it means either always snapping, which discards
exactness, or a Stern–Brocot floor onto the full set, which is the harder proof.
The crate does neither; if you iterate a rounded projection to a fixed point,
know this.
Saturation is a choice. Some exact results above 2^62 − 1 do have a
Rat within 2^-61 (saturation::lemma_saturation_is_a_choice exhibits one).
R3 is scoped below the ceiling to keep one clean boundary, and results above it
saturate. Rat::new also returns None for an i64 pair that is already
reduced but over budget; Rat::new_rounded is total in the numerator.
Q is not a semiring. Its add and mul are commutative
unconditionally, but associative, distributive and monotone only on the
all-Number exact path, exactly as for Rat. Once a PosSat/NegSat is
reachable all three fail; mul additionally fails to associate on values that
never touch saturation, because rounding a product to zero can manufacture a
0 · ∞ that exact arithmetic never would. Q::div(a, b) == Q::mul(a, Q::recip(b)) fails in six cells. No defect bound is stated at this level:
Nan and the unbounded saturation intervals carry no metric to bound against.
Every failure has a test in tests/q_laws.rs.
Transcendentals are exact at rational points only where a proof says so.
cbrt(8) is 2305843009213693951 / 2^60, within 2^-60 of 2 but not 2, and
exp2(3) is likewise a hair under 8, because both go through exp(ln) and
those are series. sqrt of a perfect square, exp(0), ln(1), log2 at a
power of two and log10 at a power of ten return the integer, and for the two
logarithms that is a proven postcondition (log2(2^k) == k for k ≤ 61,
log10(10^k) == k for k ≤ 18, both signs, every power a Rat holds). Everything else lands near the
true value by the measured margins above, not on it.
What is proven
1219 verified, 0 errors in CI; no assume, no admit; three
external_body functions, enumerated in TRUSTED.md.
- V1 Every public operation preserves canonical form and the budget.
- V2 No panic, no overflow: every
i128intermediate is bounded. The2^62budget is exactly what makes cross-multiplied products fit. - V3 Every result equals the exact rational model, stated division-free.
- V4 R1–R4 above, with the tighter nearest bound.
- V5 GCD correctness and termination.
- V6 Commutativity; exact-path associativity and distributivity; identity and involution laws; a total order agreeing with the value order.
- V7 Lipschitz bounds for
add,mul,recip,div. - V8 Accumulation bounds:
sumandproductwithink · m · 2^-61afterksteps (productneeds|factor| ≤ 1);weighted_mean's returned value within8k · 2^-61 / δwhen weights and values lie in[0, 1]and the exact weight sum is at leastδ. - V9
Q: totality, classification, total order,Nanabsorption. - V10 Transcendentals: totality and termination.
isqrtis exactly the integer square root;log2returns exactlykon2^kand1/2^k, andlog10exactlykon10^kand1/10^k, for every such power aRatholds. - V11
Q's algebraic laws (commutative unconditionally; associative, distributive and monotone on the all-Numberexact path and nowhere further), and the containment obligation{x ⊕ y : x ∈ ⟦a⟧, y ∈ ⟦b⟧} ⊆ ⟦op(a, b)⟧for the special-value propagation ofadd,sub,mulanddiv, proven against an independent ghost model of true values (denote.rs) rather than tested against the table alone. Soundness, and honesty (Nanis returned whenever a genuine indeterminate such as∞ − ∞or0 · ∞can be witnessed), for all four; necessity (Nanis never returned where a smaller state would also be sound) for one cell as the pattern. - The associativity bounds under Limits, interval enclosure, the
Exactlaws, thei128conversion cores, and the value pinning of every constructor including thef64decomposition's integer core.
Not proven: the f64 decode/encode (trusted, tested); transcendental
accuracy (measured, above); pow_u32's value (only its well-formedness);
mul associativity outside [0, 1]; the Q folds against the containment
model (a set-valued induction, tracked as follow-on work); Nan necessity for
every mul/div cell; the feature-gated library adapters, which call into
foreign types Verus has no model of and sit outside verus! as thin wrappers
over the verified cores.
Testing
malachite-q is the oracle, as a dev-dependency only (scripts/check-no-lgpl.sh
enforces this). tests/oracle.rs runs 20,000 random cases per operation per
direction against R1–R3 plus every p/q with |p|, q ≤ 12; tests/bounds.rs
checks R3's nearest bound, the weighted-mean bound and interval enclosure on
random operands; tests/props.rs covers the invariant, the laws, and
byte-identical results across eight threads; tests/adversarial.rs holds the
budget edges and both counterexamples; tests/transcendental.rs carries the
accuracy oracles; tests/q_laws.rs the counterexamples to Q's laws;
tests/ratio_core.rs and one suite per library integration the conversion
boundary; tests/readme_examples.rs compiles and runs every code block on this
page and fails if one drifts from this file. Every public function has a
doctest. Overflow checks stay on in release.
Performance
cargo bench --bench arith: minimum of seven runs, overflow-checks = true.
Linux 6.16 x86-64, rustc 1.96. The benchmark prints its own quality block; on
this run control drift was 5.8% and the widest spread 137%, so treat the second
digit as noise.
| op | the-q | num-rational Ratio<i64> |
malachite-q (exact) |
f64 |
|---|---|---|---|---|
add |
56 ns | 127 ns | 110 ns | 1.3 ns |
mul |
62 ns | 150 ns | 147 ns | 1.3 ns |
div |
62 ns | 160 ns | 131 ns | 1.3 ns |
| compare | 2.6 ns | 28 ns | 68 ns | 1.4 ns |
Those are exact-path costs. Rounding is the expensive path, and it is where the exact backend wins:
Rat::add path |
the-q | exact |
|---|---|---|
| no rounding | 14 ns | 49 ns |
| rounded | 349 ns | 148 ns |
| saturated | 137 ns | 46 ns |
chain acc = (acc + x) · y, per step |
the-q | exact | exact digits |
|---|---|---|---|
k = 4 |
195 ns | 326 ns | 29 |
k = 64 |
1400 ns | 443 ns | 311 |
k = 1024 |
1503 ns | 1093 ns | 2 979 |
k = 4096 |
1619 ns | 2609 ns | 8 806 |
the-q plateaus once operands fill the budget; the exact backend grows without
bound and is cheaper between roughly k = 8 and k = 2000. An 8-pair
weighted_mean is 8.0 µs against 5.3 µs exact. Transcendentals: exp 0.6 µs,
ln 1.5 µs, sqrt 11 µs, sin/cos 17–19 µs, atan 32 µs.
Verification
Specifications and proofs live in the source inside verus! blocks. cargo build compiles them with plain rustc; cargo verus verify checks them.
VERIFICATION.md has the obligation map and reproduction steps; TRUSTED.md
the three trusted functions; docs/SPEC.md the original specification with its
six recorded departures.
Licence
Dual-licensed. Free under AGPL-3.0-or-later: use, modify, and redistribute
freely, including as a network service, as long as you release your source
under the same terms (AGPL-3.0-or-later section 13, the network-use
clause, applies to any service built on this crate, not only to
redistributed binaries). If that obligation does not work for your use, which
covers most commercial and closed-source use, a separate commercial licence is
available; contact the author for terms.
The LGPL-3.0-only oracle malachite-q is a dev-dependency only and never
ships, so its terms do not apply to any release of this crate.