Merge pull request #337 from fiddlerwoaroof/adr005/numtower

feat(numtower): ADR 005 step 1, the numeric tower crate (improvise-59nw.1)
This commit is contained in:
Ed Langley
2026-10-09 20:47:05 -07:00
committed by GitHub
19 changed files with 4605 additions and 3 deletions
+25
View File
@@ -0,0 +1,25 @@
[package]
name = "numtower"
version = "0.1.0"
edition = "2024"
license = "Apache-2.0"
description = "A three-level numeric tower: exact decimals, bounded exact fractions and inexact f64, with Scheme's contagion rules and exact, order-free sums."
keywords = ["decimal", "rational", "exact", "numeric-tower"]
categories = ["mathematics"]
# ADR 005 clause 14: rust_decimal (std only), num-rational without its default
# features (for Ratio<i128>), and num-bigint always. No serde.
[dependencies]
rust_decimal = { version = "1.43", default-features = false, features = ["std"] }
num-rational = { version = "0.4.2", default-features = false }
num-bigint = { version = "0.4", default-features = false, features = ["std"] }
num-integer = { version = "0.1", default-features = false }
num-traits = { version = "0.2", default-features = false, features = ["std"] }
[dev-dependencies]
num-rational = { version = "0.4.2", features = ["num-bigint-std"] }
proptest = "1"
[[example]]
name = "bench"
test = false
+187
View File
@@ -0,0 +1,187 @@
//! ADR 005 migration step 1's benchmark: `cargo run --release -p numtower --example bench`.
//! A microbenchmark, not the evaluator; what it does and does not show is in `context/code/numtower.md`.
use std::hint::black_box;
use std::time::Instant;
use numtower::{Decimal, ExactSum, Num, NumSum, Rounding, Typing, parse_number};
struct SplitMix64(u64);
impl SplitMix64 {
fn next(&mut self) -> u64 {
self.0 = self.0.wrapping_add(0x9E37_79B9_7F4A_7C15);
let mut z = self.0;
z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
z ^ (z >> 31)
}
}
/// Typed two-place values from 0.01 to 999,999.99, as the ADR's appendix draws them.
fn typed_texts(seed: u64, n: usize) -> Vec<String> {
let mut rng = SplitMix64(seed);
(0..n)
.map(|_| {
let cents = 1 + rng.next() % 99_999_999;
format!("{}.{:02}", cents / 100, cents % 100)
})
.collect()
}
fn nums(texts: &[String]) -> Vec<Num> {
texts
.iter()
.map(|t| parse_number(t, Typing::Inferred).unwrap())
.collect()
}
/// The best of `runs`, in nanoseconds per item.
fn best(runs: usize, items: usize, mut f: impl FnMut()) -> f64 {
(0..runs)
.map(|_| {
let t = Instant::now();
f();
t.elapsed().as_nanos() as f64 / items as f64
})
.fold(f64::INFINITY, f64::min)
}
fn sums() {
println!("SUM over typed two-place values, ns per element, best of 20");
for n in [18_000, 90_000] {
let texts = typed_texts(1, n);
let floats: Vec<f64> = texts.iter().map(|t| t.parse().unwrap()).collect();
let decimals: Vec<Decimal> = texts.iter().map(|t| t.parse().unwrap()).collect();
let values = nums(&texts);
let f64_fold = best(20, n, || {
black_box(black_box(&floats).iter().sum::<f64>());
});
let decimal_fold = best(20, n, || {
let s = black_box(&decimals)
.iter()
.fold(Decimal::ZERO, |a, b| a.checked_add(*b).unwrap());
black_box(s);
});
let num_fold = best(20, n, || {
let zero = Num::exact(Decimal::ZERO);
black_box(black_box(&values).iter().fold(zero, |a, b| &a + b));
});
let accumulator = best(20, n, || {
let mut s = NumSum::new();
black_box(&values).iter().for_each(|v| s.insert(v));
black_box(s.read());
});
println!(
" {n:>6} cells: f64 {f64_fold:.2}, rust_decimal {decimal_fold:.2}, Num + fold {num_fold:.2}, NumSum {accumulator:.2}"
);
}
}
fn operations() {
println!("Per operation over 18,000 typed pairs, ns, best of 20");
let (a, b) = (
nums(&typed_texts(21, 18_000)),
nums(&typed_texts(22, 18_000)),
);
let decimal =
|v: &[Num]| -> Vec<Decimal> { v.iter().map(|n| n.to_string().parse().unwrap()).collect() };
let (da, db) = (decimal(&a), decimal(&b));
let pairs = a.len();
let rd_add = best(20, pairs, || {
da.iter().zip(&db).for_each(|(x, y)| {
black_box(x.checked_add(*y));
})
});
let rd_mul = best(20, pairs, || {
da.iter().zip(&db).for_each(|(x, y)| {
black_box(x.checked_mul(*y));
})
});
let rd_div = best(20, pairs, || {
da.iter().zip(&db).for_each(|(x, y)| {
black_box(x.checked_div(*y));
})
});
let add = best(20, pairs, || {
a.iter().zip(&b).for_each(|(x, y)| drop(black_box(x + y)))
});
let mul = best(20, pairs, || {
a.iter().zip(&b).for_each(|(x, y)| drop(black_box(x * y)))
});
let div = best(20, pairs, || {
a.iter()
.zip(&b)
.for_each(|(x, y)| drop(black_box(x.checked_div(y))))
});
let margin = best(20, pairs, || {
a.iter()
.zip(&b)
.for_each(|(rev, cost)| drop(black_box((rev - cost).checked_div(rev))));
});
let to_f64 = best(20, pairs, || {
a.iter().for_each(|x| {
black_box(x.to_f64());
})
});
let places = best(20, pairs, || {
a.iter()
.for_each(|x| drop(black_box(x.round_to_places(2, Rounding::HalfAwayFromZero))));
});
println!(" rust_decimal checked: add {rd_add:.1}, mul {rd_mul:.1}, div {rd_div:.1}");
println!(" Num: add {add:.1}, mul {mul:.1}, div {div:.1}");
println!(
" Num (Rev - Cost) / Rev {margin:.1}, to_f64 {to_f64:.1}, round_to_places(2) {places:.1}"
);
}
/// K groups of near-`i128` denominators: the ADR's worst case for the exact read.
fn grouped_read() {
println!("NumSum read over K distinct 126-bit denominators, us per read, best of 200");
for k in [2usize, 8] {
let mut rng = SplitMix64(0x5EED);
let mut sum = NumSum::new();
for _ in 0..k {
let den = ((rng.next() as i128) << 62 | rng.next() as i128 >> 2) | 1 | (1 << 125);
let num = (rng.next() as i128) << 60 ^ rng.next() as i128;
let term = Num::ratio(num, den).unwrap();
sum.add(&term, i64::MAX);
sum.add(&term, i64::MAX);
}
assert_eq!(sum.groups(), k);
let read = best(200, 1, || drop(black_box(sum.read())));
println!(
" K = {k}: {:.1} us (exact: {})",
read / 1000.0,
sum.read().is_exact()
);
}
}
fn exact_sum() {
println!("ExactSum (f64 superaccumulator), best of 20");
let floats: Vec<f64> = typed_texts(1, 1_000_000)
.iter()
.map(|t| t.parse().unwrap())
.collect();
let mut acc = ExactSum::new();
let insert = best(20, floats.len(), || {
acc = ExactSum::new();
floats.iter().for_each(|x| acc.add(*x, 1));
});
let read = best(20, 1, || {
black_box(acc.value());
});
println!(
" {insert:.2} ns per insert, {:.2} us per read",
read / 1000.0
);
}
fn main() {
println!("numtower bench (release); name the machine when you quote these");
sums();
operations();
grouped_read();
exact_sum();
}
+445
View File
@@ -0,0 +1,445 @@
//! Arithmetic and contagion (ADR 005 clauses 4 and 5, question 13 (b)).
//! The reasoning for each rule is in `context/code/numtower.md`, "Arithmetic".
use std::ops::{Add, Mul, Neg, Sub};
use num_bigint::{BigInt, BigUint};
use num_integer::Integer;
use num_traits::{One, Signed, ToPrimitive, Zero};
use rust_decimal::Decimal;
use crate::fixed::I256;
use crate::num::{MANTISSA_LIMIT, MAX_SCALE, Num, NumRef, decimal, pow10, small_ratio};
use crate::ratio::BigQ;
/// The widest an exact intermediate may grow, in bits of numerator or denominator.
pub const CEILING_BITS: u64 = 8192;
/// A value inside one evaluation: exact intermediates are unbounded up to the ceiling.
#[derive(Clone, Debug)]
pub struct Interim(Inner);
#[derive(Clone, Debug)]
enum Inner {
Stored(Num),
Wide(Box<BigQ>),
}
/// The divisor was exactly zero, exact or inexact (clause 5).
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct DivisionByZero;
impl From<Num> for Interim {
fn from(n: Num) -> Interim {
Interim(Inner::Stored(n))
}
}
impl Interim {
fn wide(q: BigQ) -> Interim {
Interim(Inner::Wide(Box::new(q)))
}
/// The stored bound of question 13 (b): a wide value is the nearest f64.
pub fn store(self) -> Num {
match self.0 {
Inner::Stored(n) => n,
Inner::Wide(q) => Num::inexact(q.to_f64()),
}
}
pub fn is_exact(&self) -> bool {
match &self.0 {
Inner::Stored(n) => n.is_exact(),
Inner::Wide(_) => true,
}
}
fn operand(&self) -> Operand<'_> {
match &self.0 {
Inner::Stored(n) => Operand::of(n),
Inner::Wide(q) => Operand::Big(q),
}
}
pub fn checked_div(&self, o: &Interim) -> Result<Interim, DivisionByZero> {
divide(self.operand(), o.operand())
}
pub fn pow(&self, o: &Interim) -> Result<Interim, DivisionByZero> {
power(self.operand(), o.operand())
}
/// Exactly zero; a wide value never is, because zero fits a stored arm.
pub fn is_zero(&self) -> bool {
match &self.0 {
Inner::Stored(n) => n.is_zero(),
Inner::Wide(_) => false,
}
}
/// Clause 6's exact order, before the stored bound; `None` only when either is NaN.
pub fn cmp_exact(&self, o: &Interim) -> Option<std::cmp::Ordering> {
match (&self.0, &o.0) {
(Inner::Stored(a), Inner::Stored(b)) => a.cmp_exact(b),
_ => match (self.operand().big(), o.operand().big()) {
(Some(a), Some(b)) => Some(a.cmp_value(&b)),
(Some(a), None) => crate::cmp::exact_against_f64(&a, o.operand().float()),
(None, Some(b)) => {
crate::cmp::exact_against_f64(&b, self.operand().float()).map(|c| c.reverse())
}
(None, None) => self.operand().float().partial_cmp(&o.operand().float()),
},
}
}
}
impl Num {
pub fn checked_div(&self, o: &Num) -> Result<Num, DivisionByZero> {
divide(Operand::of(self), Operand::of(o)).map(Interim::store)
}
pub fn pow(&self, o: &Num) -> Result<Num, DivisionByZero> {
power(Operand::of(self), Operand::of(o)).map(Interim::store)
}
}
macro_rules! binary_ops {
($($trait:ident $method:ident $f:ident;)*) => {$(
impl $trait<&Interim> for &Interim {
type Output = Interim;
fn $method(self, o: &Interim) -> Interim {
$f(self.operand(), o.operand())
}
}
impl $trait<&Num> for &Num {
type Output = Num;
fn $method(self, o: &Num) -> Num {
$f(Operand::of(self), Operand::of(o)).store()
}
}
)*};
}
binary_ops! {
Add add plus;
Sub sub minus;
Mul mul times;
}
impl Neg for &Interim {
type Output = Interim;
fn neg(self) -> Interim {
negate(self.operand())
}
}
impl Neg for &Num {
type Output = Num;
fn neg(self) -> Num {
negate(Operand::of(self)).store()
}
}
#[derive(Clone, Copy)]
enum Operand<'a> {
Dec(&'a Decimal),
Small(i128, i128),
Big(&'a BigQ),
Float(f64),
}
impl<'a> Operand<'a> {
fn of(n: &'a Num) -> Operand<'a> {
match n.kind() {
NumRef::Exact(d) => Operand::Dec(d),
NumRef::Fraction(r) => Operand::Small(*r.numer(), *r.denom()),
NumRef::Inexact(x) => Operand::Float(x),
}
}
/// The `i128` fast path's operands; `i128::MIN` cannot be negated, so it takes the bigint path.
fn parts(self) -> Option<(i128, i128)> {
match self {
Operand::Dec(d) => Some((d.mantissa(), pow10(d.scale()))),
Operand::Small(n, d) => (n != i128::MIN).then_some((n, d)),
Operand::Big(_) | Operand::Float(_) => None,
}
}
fn big(self) -> Option<BigQ> {
match self {
Operand::Dec(d) => Some(BigQ::from_i128(d.mantissa(), pow10(d.scale()))),
Operand::Small(n, d) => Some(BigQ::from_i128(n, d)),
Operand::Big(q) => Some(q.clone()),
Operand::Float(_) => None,
}
}
fn float(self) -> f64 {
match self {
Operand::Dec(d) => crate::num::decimal_to_f64(d),
Operand::Small(n, d) => crate::ratio::ratio_to_f64(&BigInt::from(n), &BigInt::from(d)),
Operand::Big(q) => q.to_f64(),
Operand::Float(x) => x,
}
}
fn is_float(self) -> bool {
matches!(self, Operand::Float(_))
}
fn is_zero(self) -> bool {
match self {
Operand::Dec(d) => d.is_zero(),
Operand::Small(..) | Operand::Big(_) => false,
Operand::Float(x) => x == 0.0,
}
}
}
fn inexact(x: f64) -> Interim {
Interim::from(Num::inexact(x))
}
/// An exact result in the first arm it fits; wide while under the ceiling, else demoted.
fn settle_wide(q: BigQ) -> Interim {
match Num::fit(&q) {
Some(n) => Interim::from(n),
None if q.bits() > CEILING_BITS => inexact(q.to_f64()),
None => Interim::wide(q),
}
}
fn exact_or_wide(
a: Operand,
b: Operand,
small: impl Fn(i128, i128, i128, i128) -> Option<(i128, i128)>,
big: impl Fn(&BigQ, &BigQ) -> BigQ,
) -> Interim {
let fast = a
.parts()
.zip(b.parts())
.and_then(|((n1, d1), (n2, d2))| small(n1, d1, n2, d2));
fast.and_then(|(n, d)| small_ratio(n, d))
.map(Interim::from)
.unwrap_or_else(|| match a.big().zip(b.big()) {
Some((x, y)) => settle_wide(big(&x, &y)),
None => unreachable!("both operands are exact here"),
})
}
fn plus(a: Operand, b: Operand) -> Interim {
match (a, b) {
(x, y) if x.is_float() || y.is_float() => inexact(x.float() + y.float()),
(Operand::Dec(x), Operand::Dec(y)) => decimal_sum(x, y),
_ => exact_or_wide(
a,
b,
|n1, d1, n2, d2| {
let n = n1.checked_mul(d2)?.checked_add(n2.checked_mul(d1)?)?;
Some((n, d1.checked_mul(d2)?))
},
BigQ::add,
),
}
}
fn minus(a: Operand, b: Operand) -> Interim {
plus(a, negate(b).operand())
}
fn times(a: Operand, b: Operand) -> Interim {
match (a, b) {
(x, y) if x.is_float() || y.is_float() => inexact(x.float() * y.float()),
(Operand::Dec(x), Operand::Dec(y)) => decimal_product(x, y),
_ => exact_or_wide(
a,
b,
|n1, d1, n2, d2| {
let (g1, g2) = (n1.gcd(&d2).max(1), n2.gcd(&d1).max(1));
Some((
(n1 / g1).checked_mul(n2 / g2)?,
(d1 / g2).checked_mul(d2 / g1)?,
))
},
BigQ::mul,
),
}
}
fn negate(a: Operand) -> Interim {
match a {
Operand::Dec(d) => Interim::from(Num::exact(-*d)),
Operand::Float(x) => inexact(-x),
Operand::Small(n, d) => n
.checked_neg()
.and_then(|m| small_ratio(m, d))
.map(Interim::from)
.unwrap_or_else(|| settle_wide(BigQ::from_i128(n, d).neg())),
Operand::Big(q) => settle_wide(q.neg()),
}
}
fn divide(a: Operand, b: Operand) -> Result<Interim, DivisionByZero> {
match (a, b) {
(_, y) if y.is_zero() => Err(DivisionByZero),
(x, y) if x.is_float() || y.is_float() => Ok(inexact(x.float() / y.float())),
_ => Ok(exact_or_wide(
a,
b,
|n1, d1, n2, d2| {
let (g1, g2) = (n1.gcd(&n2).max(1), d1.gcd(&d2).max(1));
Some((
(n1 / g1).checked_mul(d2 / g2)?,
(d1 / g2).checked_mul(n2 / g1)?,
))
},
|x, y| {
x.div(y)
.unwrap_or_else(|| unreachable!("the divisor is not zero"))
},
)),
}
}
/// Clause 4's hint: a returned scale below the nominal one may mean rounding.
fn decimal_sum(x: &Decimal, y: &Decimal) -> Interim {
let nominal = x.scale().max(y.scale());
match x.checked_add(*y).filter(|r| r.scale() == nominal) {
Some(r) => Interim::from(Num::exact(r)),
None => {
let align = |d: &Decimal| {
I256::from_i128(d.mantissa())
.checked_mul_u128(pow10(nominal - d.scale()) as u128)
.expect("an aligned mantissa is below 2^190")
};
let sum = align(x).checked_add(&align(y)).expect("below 2^191");
settle_decimal_integer(sum, nominal, nominal)
}
}
}
fn decimal_product(x: &Decimal, y: &Decimal) -> Interim {
let nominal = x.scale() + y.scale();
match x.checked_mul(*y).filter(|r| r.scale() == nominal) {
Some(r) => Interim::from(Num::exact(r)),
None => {
settle_decimal_integer(I256::mul_i128(x.mantissa(), y.mantissa()), nominal, nominal)
}
}
}
/// The recompute of clause 4: strip trailing zeros, fit 96 bits at scale <= 28, else a fraction.
fn settle_decimal_integer(m: I256, scale: u32, nominal: u32) -> Interim {
let (mut m, mut s) = (m, scale);
while s > 0 {
let (q, r) = m.div_rem_u64(10);
if r != 0 {
break;
}
m = q;
s -= 1;
}
let fits = m
.to_i128()
.filter(|v| v.unsigned_abs() < MANTISSA_LIMIT as u128);
match fits {
Some(v) if s <= MAX_SCALE => Interim::from(padded(v, s, nominal.min(MAX_SCALE))),
_ => settle_wide(BigQ::new(m.to_bigint(), BigInt::from(10).pow(s))),
}
}
/// The stored scale of clause 4: pad toward `target` while the mantissa fits 96 bits.
pub(crate) fn padded(mantissa: i128, scale: u32, target: u32) -> Num {
let (mut m, mut s) = (mantissa, scale);
while s < target && (m * 10).unsigned_abs() < MANTISSA_LIMIT as u128 {
m *= 10;
s += 1;
}
decimal(m, s).expect("a padded mantissa still fits")
}
/// The exponent of an exact integer-valued operand.
fn integer_value(b: Operand) -> Option<BigInt> {
match b {
Operand::Dec(d) if d.fract().is_zero() => {
Some(BigInt::from(d.mantissa() / pow10(d.scale())))
}
Operand::Small(n, 1) => Some(BigInt::from(n)),
Operand::Big(q) if q.den.is_one() => Some(q.num.clone()),
_ => None,
}
}
fn power(a: Operand, b: Operand) -> Result<Interim, DivisionByZero> {
match (a.is_float(), integer_value(b)) {
(false, Some(n)) => exact_power(a, &n),
_ => Ok(inexact(a.float().powf(b.float()))),
}
}
/// Clause 5's `POW`: the size is checked before anything is computed.
fn exact_power(a: Operand, n: &BigInt) -> Result<Interim, DivisionByZero> {
let q = a.big().expect("an exact base");
let base_scale = match a {
Operand::Dec(d) => d.scale(),
_ => 0,
};
let k = n.magnitude();
let target = match n.is_negative() {
true => 0,
false => k
.to_u32()
.and_then(|k| k.checked_mul(base_scale))
.map_or(MAX_SCALE, |s| s.min(MAX_SCALE)),
};
let unit = q.den.is_one() && q.num.magnitude().is_one();
match (q.num.is_zero(), n.is_negative(), unit) {
(_, _, _) if n.is_zero() => Ok(Interim::from(padded(1, 0, 0))),
(true, true, _) => Err(DivisionByZero),
(true, false, _) => Ok(Interim::from(padded(0, 0, target))),
(false, _, true) => {
let negative = q.num.is_negative() && k.is_odd();
Ok(Interim::from(padded(
if negative { -1 } else { 1 },
0,
target,
)))
}
(false, negative, false) => {
let lower = |x: &BigInt| BigUint::from(x.bits().saturating_sub(1)) * k;
let too_big = lower(&q.num).max(lower(&q.den)) > BigUint::from(CEILING_BITS);
let odd_negative = q.num.is_negative() && k.is_odd();
match (too_big, k.to_u32()) {
(false, Some(k)) => Ok(exact_integer_power(&q, k, negative, target)),
_ => Ok(inexact(signed_powf(a.float(), n, odd_negative))),
}
}
}
}
/// `powf` on the magnitude, the sign taken from the exact exponent's parity.
fn signed_powf(base: f64, n: &BigInt, odd_negative: bool) -> f64 {
let magnitude = base.abs().powf(n.to_f64().unwrap_or(f64::INFINITY));
match odd_negative {
true => -magnitude,
false => magnitude,
}
}
fn exact_integer_power(q: &BigQ, k: u32, negative: bool, target: u32) -> Interim {
let (num, den) = (q.num.pow(k), q.den.pow(k));
let value = match negative {
true => BigQ::new(den, num),
false => BigQ { num, den },
};
let settled = settle_wide(value);
match (&settled.0, negative) {
(Inner::Stored(n), false) => match n.kind() {
NumRef::Exact(d) => Interim::from(padded(d.mantissa(), d.scale(), target)),
_ => settled,
},
_ => settled,
}
}
+107
View File
@@ -0,0 +1,107 @@
//! Exact comparison at every pairing of arms (clause 6), and the `MIN`/`MAX`
//! tie-breaks (clause 8). Why: `context/code/numtower.md`, "Comparison".
use std::cmp::Ordering;
use crate::fixed::I256;
use crate::num::{Num, NumRef};
use crate::ratio::BigQ;
impl Num {
/// The order of the two values, exactly; `None` only when either is NaN.
pub fn cmp_exact(&self, o: &Num) -> Option<Ordering> {
match (self.exact_parts(), o.exact_parts()) {
(Some(a), Some(b)) => Some(cross(a, b)),
(Some(_), None) => mixed(self, o.to_f64()),
(None, Some(_)) => mixed(o, self.to_f64()).map(Ordering::reverse),
(None, None) => self.to_f64().partial_cmp(&o.to_f64()),
}
}
/// The smaller, NaN skipped; ties keep the most information (clause 8, question 11).
pub fn min_of<'a>(&'a self, o: &'a Num) -> &'a Num {
pick(self, o, Ordering::Less)
}
pub fn max_of<'a>(&'a self, o: &'a Num) -> &'a Num {
pick(self, o, Ordering::Greater)
}
}
impl PartialEq for Num {
fn eq(&self, o: &Num) -> bool {
self.cmp_exact(o) == Some(Ordering::Equal)
}
}
impl PartialOrd for Num {
fn partial_cmp(&self, o: &Num) -> Option<Ordering> {
self.cmp_exact(o)
}
}
/// `a.0/a.1` against `b.0/b.1`, denominators positive, in 256 bits.
fn cross(a: (i128, i128), b: (i128, i128)) -> Ordering {
I256::mul_i128(a.0, b.1).cmp(&I256::mul_i128(b.0, a.1))
}
/// An exact value against an f64: the rounded value decides unless it ties.
fn mixed(exact: &Num, x: f64) -> Option<Ordering> {
match (x.is_nan(), x.is_infinite()) {
(true, _) => None,
(false, true) => Some(if x > 0.0 {
Ordering::Less
} else {
Ordering::Greater
}),
(false, false) => match exact.to_f64().partial_cmp(&x)? {
Ordering::Equal => {
let q = exact.exact_q().expect("an exact operand");
Some(q.cmp_value(&BigQ::from_f64(x)))
}
decided => Some(decided),
},
}
}
/// A wide exact value against an f64, with no fast path: the slow, total form of `mixed`.
pub(crate) fn exact_against_f64(q: &BigQ, x: f64) -> Option<Ordering> {
match (x.is_nan(), x.is_infinite(), x > 0.0) {
(true, _, _) => None,
(false, true, true) => Some(Ordering::Less),
(false, true, false) => Some(Ordering::Greater),
(false, false, _) => Some(q.cmp_value(&BigQ::from_f64(x))),
}
}
fn pick<'a>(a: &'a Num, b: &'a Num, want: Ordering) -> &'a Num {
match (a.cmp_exact(b), is_nan(a), is_nan(b)) {
(_, true, _) => b,
(_, false, true) => a,
(Some(o), _, _) if o == want => a,
(Some(Ordering::Equal), _, _) => tie(a, b, want),
_ => b,
}
}
fn is_nan(n: &Num) -> bool {
matches!(n.kind(), NumRef::Inexact(x) if x.is_nan())
}
/// Equal values: exact over inexact, the larger scale, and IEEE's signed zero.
fn tie<'a>(a: &'a Num, b: &'a Num, want: Ordering) -> &'a Num {
match (a.kind(), b.kind()) {
(NumRef::Inexact(_), NumRef::Exact(_) | NumRef::Fraction(_)) => b,
(NumRef::Exact(_) | NumRef::Fraction(_), NumRef::Inexact(_)) => a,
(NumRef::Exact(x), NumRef::Exact(y)) if y.scale() > x.scale() => b,
(NumRef::Inexact(x), NumRef::Inexact(y)) => {
let negative_wanted = want == Ordering::Less;
match y.is_sign_negative() == negative_wanted && x.is_sign_negative() != negative_wanted
{
true => b,
false => a,
}
}
_ => a,
}
}
+234
View File
@@ -0,0 +1,234 @@
//! Fixed-width two's-complement integers for the hot paths: no allocation.
//! Why they are hand-rolled: `context/code/numtower.md`, "The wide integers".
use std::cmp::Ordering;
use num_bigint::{BigInt, Sign};
/// A signed integer of `64 * N` bits, little-endian limbs, two's complement.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub(crate) struct Fixed<const N: usize> {
limbs: [u64; N],
}
/// The 256-bit integer clauses 4–6 use, and the 320-bit decimal partition of clause 8.
pub(crate) type I256 = Fixed<4>;
pub(crate) type I320 = Fixed<5>;
impl<const N: usize> Fixed<N> {
pub(crate) const ZERO: Self = Fixed { limbs: [0; N] };
pub(crate) fn from_i128(x: i128) -> Self {
let fill = if x < 0 { u64::MAX } else { 0 };
let mut limbs = [fill; N];
limbs[0] = x as u64;
limbs[1] = (x >> 64) as u64;
Fixed { limbs }
}
pub(crate) fn is_negative(&self) -> bool {
self.limbs[N - 1] >> 63 == 1
}
pub(crate) fn is_zero(&self) -> bool {
self.limbs.iter().all(|&l| l == 0)
}
fn wrapping_neg(&self) -> Self {
let mut out = [0u64; N];
let mut carry = 1u64;
for (o, &l) in out.iter_mut().zip(&self.limbs) {
let (v, c) = (!l).overflowing_add(carry);
*o = v;
carry = c as u64;
}
Fixed { limbs: out }
}
fn wrapping_add(&self, other: &Self) -> Self {
let mut out = [0u64; N];
let mut carry = false;
for (o, (a, b)) in out.iter_mut().zip(self.limbs.iter().zip(&other.limbs)) {
let (s, c1) = a.overflowing_add(*b);
let (s, c2) = s.overflowing_add(carry as u64);
*o = s;
carry = c1 || c2;
}
Fixed { limbs: out }
}
pub(crate) fn checked_add(&self, other: &Self) -> Option<Self> {
let sum = self.wrapping_add(other);
let same = self.is_negative() == other.is_negative();
(!same || sum.is_negative() == self.is_negative()).then_some(sum)
}
pub(crate) fn checked_neg(&self) -> Option<Self> {
let neg = self.wrapping_neg();
(self.is_zero() || neg.is_negative() != self.is_negative()).then_some(neg)
}
/// The magnitude as unsigned limbs, with the sign split out.
fn magnitude(&self) -> (bool, [u64; N]) {
match self.is_negative() {
true => (true, self.wrapping_neg().limbs),
false => (false, self.limbs),
}
}
fn from_magnitude(negative: bool, limbs: [u64; N]) -> Option<Self> {
let unsigned = Fixed { limbs };
match (negative, unsigned.is_negative()) {
(false, false) => Some(unsigned),
(true, _) => {
let neg = unsigned.wrapping_neg();
(neg.is_negative() || neg.is_zero()).then_some(neg)
}
(false, true) => None,
}
}
pub(crate) fn checked_mul_u128(&self, k: u128) -> Option<Self> {
let (negative, mag) = self.magnitude();
let mut out = [0u64; N];
for (j, kk) in [k as u64, (k >> 64) as u64].into_iter().enumerate() {
if kk == 0 {
continue;
}
let mut carry = 0u128;
for (i, &limb) in mag.iter().enumerate() {
let idx = i + j;
let prod = limb as u128 * kk as u128 + carry;
match idx < N {
true => {
let s = out[idx] as u128 + (prod as u64) as u128;
out[idx] = s as u64;
carry = (prod >> 64) + (s >> 64);
}
false if prod != 0 => return None,
false => carry = 0,
}
}
if carry != 0 {
return None;
}
}
Self::from_magnitude(negative, out)
}
/// Divides the magnitude by `k`, returning the quotient (sign kept) and the remainder.
pub(crate) fn div_rem_u64(&self, k: u64) -> (Self, u64) {
let (negative, mag) = self.magnitude();
let mut out = [0u64; N];
let mut rem = 0u128;
for i in (0..N).rev() {
let cur = (rem << 64) | mag[i] as u128;
out[i] = (cur / k as u128) as u64;
rem = cur % k as u128;
}
let q = Self::from_magnitude(negative, out).unwrap_or(Self::ZERO);
(q, rem as u64)
}
pub(crate) fn to_i128(self) -> Option<i128> {
let fill = if self.is_negative() { u64::MAX } else { 0 };
let hi_ok = self.limbs[2..].iter().all(|&l| l == fill);
let v = (self.limbs[0] as u128 | ((self.limbs[1] as u128) << 64)) as i128;
(hi_ok && (v < 0) == self.is_negative()).then_some(v)
}
pub(crate) fn to_bigint(self) -> BigInt {
let (negative, mag) = self.magnitude();
let digits: Vec<u32> = mag
.iter()
.flat_map(|&l| [l as u32, (l >> 32) as u32])
.collect();
let sign = if negative { Sign::Minus } else { Sign::Plus };
BigInt::from_slice(sign, &digits)
}
/// The product of two `i128`s; needs `N >= 4`.
pub(crate) fn mul_i128(a: i128, b: i128) -> Self {
let negative = (a < 0) != (b < 0);
let (x, y) = (a.unsigned_abs(), b.unsigned_abs());
let (x0, x1) = (x as u64 as u128, x >> 64);
let (y0, y1) = (y as u64 as u128, y >> 64);
let p00 = x0 * y0;
let p01 = x0 * y1;
let p10 = x1 * y0;
let p11 = x1 * y1;
let mid = (p00 >> 64) + (p01 as u64 as u128) + (p10 as u64 as u128);
let mut limbs = [0u64; N];
limbs[0] = p00 as u64;
limbs[1] = mid as u64;
let hi = (mid >> 64) + (p01 >> 64) + (p10 >> 64) + p11;
limbs[2] = hi as u64;
limbs[3] = (hi >> 64) as u64;
let unsigned = Fixed { limbs };
match negative {
true => unsigned.wrapping_neg(),
false => unsigned,
}
}
}
impl<const N: usize> Default for Fixed<N> {
fn default() -> Self {
Self::ZERO
}
}
impl<const N: usize> Ord for Fixed<N> {
fn cmp(&self, other: &Self) -> Ordering {
match (self.is_negative(), other.is_negative()) {
(true, false) => Ordering::Less,
(false, true) => Ordering::Greater,
_ => self.limbs.iter().rev().cmp(other.limbs.iter().rev()),
}
}
}
impl<const N: usize> PartialOrd for Fixed<N> {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_256_bit_product_of_two_i128s_matches_bigint() {
let cases = [
(i128::MAX, i128::MAX),
(i128::MAX, -i128::MAX),
(-1, 1),
(0, i128::MIN),
(123_456_789_012_345_678_901_234, -98_765_432_109_876_543_210),
];
for (a, b) in cases {
let want = BigInt::from(a) * BigInt::from(b);
assert_eq!(I256::mul_i128(a, b).to_bigint(), want, "{a} * {b}");
}
}
#[test]
fn checked_arithmetic_refuses_what_overflows_and_keeps_what_fits() {
let max = I256::from_i128(i128::MAX)
.checked_mul_u128(u128::MAX)
.unwrap();
assert_eq!(
max.to_bigint(),
BigInt::from(i128::MAX) * BigInt::from(u128::MAX)
);
assert!(max.checked_mul_u128(u128::MAX).is_none());
let neg = max.checked_neg().unwrap();
assert_eq!(neg.to_bigint(), -max.to_bigint());
let (q, r) = neg.div_rem_u64(10);
assert_eq!(q.to_bigint() * 10 - BigInt::from(r), neg.to_bigint());
assert_eq!(I256::from_i128(-5).to_i128(), Some(-5));
assert_eq!(max.to_i128(), None);
assert!(I256::from_i128(-1) < I256::ZERO);
}
}
+192
View File
@@ -0,0 +1,192 @@
//! Clause 3's one number language: the gate, the exact parse, the forced `n/d` arm,
//! and the canonical text. Why each rule: `context/code/numtower.md`, "The number language".
use std::fmt;
use num_bigint::BigInt;
use num_traits::{One, Zero};
use crate::arith::padded;
use crate::num::{MAX_SCALE, Num, NumRef};
use crate::ratio::BigQ;
/// Whether the type of the text is being guessed or was forced (question 12).
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Typing {
Inferred,
Forced,
}
/// The gate: the strings `f64::from_str` accepts and reads as a finite value.
pub fn is_number(text: &str, typing: Typing) -> bool {
gate(text) || (typing == Typing::Forced && forced_fraction(text).is_some())
}
/// A number if the text is one, read exactly; `None` means the text is text.
pub fn parse_number(text: &str, typing: Typing) -> Option<Num> {
match (gate(text), typing) {
(true, _) => Lexeme::read(text).map(|lx| lx.value(text)),
(false, Typing::Forced) => forced_fraction(text),
(false, Typing::Inferred) => None,
}
}
/// A forced `n/d`; one that rounds to infinity is text, as a finite lexeme never reads as one.
fn forced_fraction(text: &str) -> Option<Num> {
let n = Num::settle(&fraction_lexeme(text)?);
n.to_f64().is_finite().then_some(n)
}
fn gate(text: &str) -> bool {
text.parse::<f64>().is_ok_and(f64::is_finite)
}
/// `[+-]digits/digits` with a nonzero denominator, as an exact value.
fn fraction_lexeme(text: &str) -> Option<BigQ> {
let (n, d) = text.split_once('/')?;
let digits = |s: &str| !s.is_empty() && s.bytes().all(|b| b.is_ascii_digit());
let unsigned = n.strip_prefix(['+', '-']).unwrap_or(n);
let ok = digits(unsigned) && digits(d);
let num: BigInt = ok.then(|| n.trim_start_matches('+').parse().ok())??;
let den: BigInt = d.parse().ok()?;
(!den.is_zero()).then(|| BigQ::new(num, den))
}
/// A lexeme of the gate's language: sign, significant digits, and the power of ten.
#[derive(Debug, PartialEq, Eq)]
pub(crate) struct Lexeme {
pub(crate) negative: bool,
pub(crate) digits: String,
pub(crate) exp10: i64,
}
/// Exponents beyond this cannot reach either exact arm, and saturate.
const EXP_SATURATE: i64 = 1 << 40;
impl Lexeme {
/// `[+-]? (d+ (. d*)? | . d+) ([eE] [+-]? d+)?`, and nothing else.
pub(crate) fn read(text: &str) -> Option<Lexeme> {
let (negative, rest) = match text.as_bytes().first()? {
b'-' => (true, &text[1..]),
b'+' => (false, &text[1..]),
_ => (false, text),
};
let (mantissa, exponent) = match rest.find(['e', 'E']) {
Some(i) => (&rest[..i], Some(&rest[i + 1..])),
None => (rest, None),
};
let (int, frac) = mantissa.split_once('.').unwrap_or((mantissa, ""));
let all_digits = |s: &str| s.bytes().all(|b| b.is_ascii_digit());
let mantissa_ok =
all_digits(int) && all_digits(frac) && !(int.is_empty() && frac.is_empty());
let exp = match exponent {
Some(e) => read_exponent(e)?,
None => 0,
};
let digits = format!("{int}{frac}");
mantissa_ok.then(|| Lexeme {
negative,
digits,
exp10: exp.saturating_sub(frac.len() as i64),
})
}
/// Clause 3's order: a decimal at the lexeme's scale, then a fraction, then the nearest f64.
fn value(&self, text: &str) -> Num {
let significant = self.digits.trim_start_matches('0');
let stripped = significant.trim_end_matches('0');
let implied_scale = (-self.exp10).clamp(0, MAX_SCALE as i64) as u32;
match stripped.is_empty() {
true => padded(0, 0, implied_scale),
false => {
let shift = self
.exp10
.saturating_add((significant.len() - stripped.len()) as i64);
self.exact(stripped, shift, implied_scale)
.unwrap_or_else(|| Num::inexact(text.parse().unwrap_or(f64::NAN)))
}
}
}
fn exact(&self, stripped: &str, shift: i64, implied_scale: u32) -> Option<Num> {
let leading = stripped.len() as i64 + shift;
let reachable = stripped.len() <= 180 && (-130..=40).contains(&leading) && shift >= -400;
let q = reachable.then(|| self.rational(stripped, shift))?;
Num::fit(&q).map(|n| match n.kind() {
NumRef::Exact(d) => padded(d.mantissa(), d.scale(), implied_scale),
_ => n,
})
}
pub(crate) fn rational(&self, stripped: &str, shift: i64) -> BigQ {
let m: BigInt = stripped.parse().unwrap_or_default();
let m = if self.negative { -m } else { m };
let ten = BigInt::from(10);
match shift >= 0 {
true => BigQ::new(m * ten.pow(shift as u32), BigInt::one()),
false => BigQ::new(m, ten.pow((-shift) as u32)),
}
}
/// The exact value, whatever its size: for display rounding.
pub(crate) fn exact_value(&self) -> BigQ {
let stripped = self.digits.trim_start_matches('0');
match stripped.is_empty() {
true => BigQ::new(BigInt::zero(), BigInt::one()),
false => self.rational(stripped, self.exp10),
}
}
}
fn read_exponent(e: &str) -> Option<i64> {
let (negative, digits) = match e.as_bytes().first()? {
b'-' => (true, &e[1..]),
b'+' => (false, &e[1..]),
_ => (false, e),
};
let ok = !digits.is_empty() && digits.bytes().all(|b| b.is_ascii_digit());
let significant = digits.trim_start_matches('0');
let magnitude = match significant.len() > 12 {
true => EXP_SATURATE,
false => significant.parse::<i64>().unwrap_or(0).min(EXP_SATURATE),
};
ok.then_some(if negative { -magnitude } else { magnitude })
}
/// The shortest text that reads back as `x`; `None` for a non-finite value.
pub fn shortest_round_trip(x: f64) -> Option<String> {
x.is_finite().then(|| format!("{x:?}"))
}
const INEXACT_MARK: &str = "#i";
impl Num {
/// Reads every spelling `Display` writes: a decimal, `n/d`, `#i…`, `inf`, `-inf`, `nan`.
pub fn from_canonical(text: &str) -> Option<Num> {
match text {
"inf" => Some(Num::inexact(f64::INFINITY)),
"-inf" => Some(Num::inexact(f64::NEG_INFINITY)),
"nan" => Some(Num::inexact(f64::NAN)),
_ => match text.strip_prefix(INEXACT_MARK) {
Some(rest) if gate(rest) => rest.parse().ok().map(Num::inexact),
Some(_) => None,
None => parse_number(text, Typing::Forced).filter(Num::is_exact),
},
}
}
}
/// The canonical text: a decimal with its scale, `n/d`, `#i` and the shortest spelling, or a token.
impl fmt::Display for Num {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self.kind() {
NumRef::Exact(d) => write!(f, "{d}"),
NumRef::Fraction(r) => write!(f, "{}/{}", r.numer(), r.denom()),
NumRef::Inexact(x) if x.is_nan() => f.write_str("nan"),
NumRef::Inexact(x) if x.is_infinite() => {
f.write_str(if x > 0.0 { "inf" } else { "-inf" })
}
NumRef::Inexact(x) => write!(f, "{INEXACT_MARK}{x:?}"),
}
}
}
+20
View File
@@ -0,0 +1,20 @@
//! A three-level numeric tower: exact decimal, exact bounded fraction, inexact f64.
//! The decisions are ADR 005; the reasoning is `context/code/numtower.md`.
mod arith;
mod cmp;
mod fixed;
mod lang;
mod num;
mod ratio;
mod round;
mod sum;
pub use arith::{CEILING_BITS, DivisionByZero, Interim};
pub use lang::{Typing, is_number, parse_number, shortest_round_trip};
pub use num::{Num, NumRef};
pub use round::Rounding;
pub use sum::{ExactSum, K, NumSum};
pub use num_rational::Ratio;
pub use rust_decimal::Decimal;
+198
View File
@@ -0,0 +1,198 @@
//! The stored number: an exact decimal, an exact bounded fraction, or an f64.
//! The arms are private so a value is always normalized: `context/code/numtower.md`.
use num_bigint::BigInt;
use num_integer::Integer;
use num_rational::Ratio;
use num_traits::{One, ToPrimitive};
use rust_decimal::Decimal;
use crate::ratio::{BigQ, ratio_to_f64};
/// The most significant digits a `Decimal` holds, and its mantissa bound.
pub(crate) const MAX_SCALE: u32 = 28;
pub(crate) const MANTISSA_LIMIT: i128 = 1 << 96;
/// A number of the tower (ADR 005 clause 2), always in its one normal form.
#[derive(Clone, Debug)]
pub struct Num(Repr);
#[derive(Clone, Debug)]
enum Repr {
Exact(Decimal),
Fraction(Box<Ratio<i128>>),
Inexact(f64),
}
/// A borrowed view of a `Num`'s arm, for exhaustive matching by callers.
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum NumRef<'a> {
Exact(&'a Decimal),
Fraction(&'a Ratio<i128>),
Inexact(f64),
}
impl Num {
/// An exact decimal. Zero loses its sign: the sign of zero is an f64 artefact.
pub fn exact(d: Decimal) -> Num {
let mut d = d;
if d.is_zero() {
d.set_sign_positive(true);
}
Num(Repr::Exact(d))
}
pub fn inexact(x: f64) -> Num {
Num(Repr::Inexact(x))
}
/// The exact value `num / den`, normalized; `None` when `den` is zero.
pub fn ratio(num: i128, den: i128) -> Option<Num> {
(den != 0).then(|| Num::settle(&BigQ::from_i128(num, den)))
}
fn fraction(num: i128, den: i128) -> Num {
Num(Repr::Fraction(Box::new(Ratio::new_raw(num, den))))
}
pub fn kind(&self) -> NumRef<'_> {
match &self.0 {
Repr::Exact(d) => NumRef::Exact(d),
Repr::Fraction(r) => NumRef::Fraction(r),
Repr::Inexact(x) => NumRef::Inexact(*x),
}
}
pub fn is_exact(&self) -> bool {
!matches!(self.0, Repr::Inexact(_))
}
/// Exactly zero, at either level; the one zero predicate of clause 6.
pub fn is_zero(&self) -> bool {
match &self.0 {
Repr::Exact(d) => d.is_zero(),
Repr::Fraction(_) => false,
Repr::Inexact(x) => *x == 0.0,
}
}
/// The correctly rounded f64 (clause 4): a decimal through its text, a fraction exactly.
pub fn to_f64(&self) -> f64 {
match &self.0 {
Repr::Exact(d) => decimal_to_f64(d),
Repr::Fraction(r) => ratio_to_f64(&BigInt::from(*r.numer()), &BigInt::from(*r.denom())),
Repr::Inexact(x) => *x,
}
}
/// The exact value as a rational; `None` for an inexact number.
pub(crate) fn exact_parts(&self) -> Option<(i128, i128)> {
match &self.0 {
Repr::Exact(d) => Some((d.mantissa(), pow10(d.scale()))),
Repr::Fraction(r) => Some((*r.numer(), *r.denom())),
Repr::Inexact(_) => None,
}
}
pub(crate) fn exact_q(&self) -> Option<BigQ> {
self.exact_parts().map(|(n, d)| BigQ::from_i128(n, d))
}
/// The stored bound: a decimal if it fits, else an `i128` fraction, else the nearest f64.
pub(crate) fn settle(q: &BigQ) -> Num {
Num::fit(q).unwrap_or_else(|| Num::inexact(q.to_f64()))
}
/// `q` in an exact arm, at its minimal scale, if it fits one.
pub(crate) fn fit(q: &BigQ) -> Option<Num> {
decimal_of(q).or_else(|| {
let num = q.num.to_i128()?;
let den = q.den.to_i128()?;
Some(Num::fraction(num, den))
})
}
}
/// `num / den` normalized without allocating; `None` hands the case to the bigint path.
pub(crate) fn small_ratio(num: i128, den: i128) -> Option<Num> {
let representable = num != i128::MIN && den != i128::MIN && den != 0;
representable.then(|| {
let g = num.gcd(&den);
let (n, d) = match den < 0 {
true => (-num / g, -den / g),
false => (num / g, den / g),
};
let p = d.trailing_zeros();
let mut rest = d >> p;
let mut q5 = 0;
while rest % 5 == 0 {
rest /= 5;
q5 += 1;
}
let k = p.max(q5);
let as_decimal = (rest == 1 && k <= MAX_SCALE)
.then(|| 2i128.pow(k - p) * 5i128.pow(k - q5))
.and_then(|pad| n.checked_mul(pad))
.and_then(|m| decimal(m, k));
as_decimal.unwrap_or_else(|| Num::fraction(n, d))
})
}
pub(crate) fn pow10(k: u32) -> i128 {
10i128.pow(k)
}
pub(crate) fn decimal_to_f64(d: &Decimal) -> f64 {
d.to_string().parse().unwrap_or(f64::NAN)
}
/// A decimal from a mantissa and scale, if the mantissa fits 96 bits.
pub(crate) fn decimal(mantissa: i128, scale: u32) -> Option<Num> {
let fits = mantissa.unsigned_abs() < MANTISSA_LIMIT as u128 && scale <= MAX_SCALE;
fits.then(|| Num::exact(Decimal::from_i128_with_scale(mantissa, scale)))
}
/// The direct test of clause 5: a reduced `n / (2^p 5^q)` is a decimal iff it fits.
fn decimal_of(q: &BigQ) -> Option<Num> {
let p = q.den.trailing_zeros().unwrap_or(0);
let mut rest = &q.den >> p as usize;
let five = BigInt::from(5);
let mut q5 = 0u64;
while rest.is_multiple_of(&five) && q5 <= MAX_SCALE as u64 {
rest /= &five;
q5 += 1;
}
let k = p.max(q5);
if !rest.is_one() || k > MAX_SCALE as u64 {
return None;
}
let pad = (BigInt::one() << (k - p) as usize) * five.pow((k - q5) as u32);
let mantissa = (&q.num * pad).to_i128()?;
decimal(mantissa, k as u32)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_num_is_24_bytes() {
assert_eq!(std::mem::size_of::<Num>(), 24);
}
#[test]
fn a_fraction_that_is_a_fitting_decimal_is_stored_as_the_decimal() {
let half = Num::ratio(1, 2).unwrap();
assert_eq!(half.kind(), NumRef::Exact(&Decimal::new(5, 1)));
let third = Num::ratio(-10, 3).unwrap();
assert_eq!(third.kind(), NumRef::Fraction(&Ratio::new_raw(-10, 3)));
let tiny = Num::ratio(1, 1 << 40).unwrap();
assert!(matches!(tiny.kind(), NumRef::Fraction(_)), "{tiny:?}");
let mut minus_zero = Decimal::new(0, 2);
minus_zero.set_sign_negative(true);
assert!(minus_zero.is_sign_negative());
let zero = Num::exact(minus_zero);
assert!(!zero.to_f64().is_sign_negative());
assert!(Num::ratio(1, 0).is_none());
}
}
+172
View File
@@ -0,0 +1,172 @@
//! Exact rationals in bigints: the widened intermediate of question 13 (b), and
//! the one correctly rounded conversion of a rational to f64 (clauses 4 and 5).
use std::cmp::Ordering;
use num_bigint::{BigInt, BigUint, Sign};
use num_integer::Integer;
use num_traits::{One, Signed, ToPrimitive, Zero};
/// A reduced fraction with a positive denominator.
#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) struct BigQ {
pub(crate) num: BigInt,
pub(crate) den: BigInt,
}
impl BigQ {
pub(crate) fn new(num: BigInt, den: BigInt) -> BigQ {
let (num, den) = match den.is_negative() {
true => (-num, -den),
false => (num, den),
};
let g = num.gcd(&den);
match g.is_zero() || g.is_one() {
true => BigQ { num, den },
false => BigQ {
num: num / &g,
den: den / &g,
},
}
}
pub(crate) fn from_i128(num: i128, den: i128) -> BigQ {
BigQ::new(BigInt::from(num), BigInt::from(den))
}
pub(crate) fn add(&self, o: &BigQ) -> BigQ {
BigQ::new(&self.num * &o.den + &o.num * &self.den, &self.den * &o.den)
}
pub(crate) fn neg(&self) -> BigQ {
BigQ {
num: -&self.num,
den: self.den.clone(),
}
}
pub(crate) fn mul(&self, o: &BigQ) -> BigQ {
BigQ::new(&self.num * &o.num, &self.den * &o.den)
}
/// `None` when `o` is zero.
pub(crate) fn div(&self, o: &BigQ) -> Option<BigQ> {
(!o.num.is_zero()).then(|| BigQ::new(&self.num * &o.den, &self.den * &o.num))
}
pub(crate) fn cmp_value(&self, o: &BigQ) -> Ordering {
(&self.num * &o.den).cmp(&(&o.num * &self.den))
}
pub(crate) fn bits(&self) -> u64 {
self.num.bits().max(self.den.bits())
}
pub(crate) fn to_f64(&self) -> f64 {
ratio_to_f64(&self.num, &self.den)
}
/// The exact value of a finite f64.
pub(crate) fn from_f64(x: f64) -> BigQ {
let bits = x.to_bits();
let biased = ((bits >> 52) & 0x7ff) as i64;
let fraction = bits & ((1u64 << 52) - 1);
let (mant, exp) = match biased {
0 => (fraction, -1074),
e => (fraction | (1 << 52), e - 1075),
};
let signed = match x.is_sign_negative() {
true => -BigInt::from(mant),
false => BigInt::from(mant),
};
match exp >= 0 {
true => BigQ::new(signed << exp as usize, BigInt::one()),
false => BigQ::new(signed, BigInt::one() << (-exp) as usize),
}
}
}
/// `num / den` rounded to the nearest f64, ties to even, subnormals and overflow included.
pub(crate) fn ratio_to_f64(num: &BigInt, den: &BigInt) -> f64 {
let negative = (num.sign() == Sign::Minus) != (den.sign() == Sign::Minus);
let a = num.magnitude();
let b = den.magnitude();
let magnitude = match a.is_zero() {
true => 0.0,
false => magnitude_to_f64(a, b),
};
match negative {
true => -magnitude,
false => magnitude,
}
}
fn magnitude_to_f64(a: &BigUint, b: &BigUint) -> f64 {
let shift = 54 - (a.bits() as i64 - b.bits() as i64);
let (n, d) = match shift >= 0 {
true => (a << shift as usize, b.clone()),
false => (a.clone(), b << (-shift) as usize),
};
let (q, r) = n.div_rem(&d);
let qbits = q.bits() as i64;
let drop = (qbits - 53).max(shift - 1074);
if drop > qbits + 1 {
return 0.0;
}
let kept = (&q >> drop as usize).to_u64().unwrap_or(0);
let round = q.bit(drop as u64 - 1);
let sticky = !r.is_zero() || q.trailing_zeros().is_some_and(|t| (t as i64) < drop - 1);
let up = round && (sticky || kept & 1 == 1);
ldexp(kept + up as u64, drop - shift)
}
/// `m * 2^e` for `m <= 2^53`, exact unless it overflows to infinity.
fn ldexp(m: u64, e: i64) -> f64 {
let top = e + 63 - m.leading_zeros() as i64;
match (m == 0, top > 1023) {
(true, _) => 0.0,
(false, true) => f64::INFINITY,
(false, false) => {
let half = e / 2;
m as f64 * pow2(half) * pow2(e - half)
}
}
}
fn pow2(e: i64) -> f64 {
match e {
e if e >= -1022 => f64::from_bits(((e + 1023) as u64) << 52),
e => f64::from_bits(1u64 << (e + 1074)),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn rounding_to_f64_reaches_subnormals_ties_and_overflow() {
let one = BigInt::one();
let min_sub = f64::from_bits(1);
assert_eq!(ratio_to_f64(&one, &(BigInt::one() << 1074)), min_sub);
assert_eq!(ratio_to_f64(&one, &(BigInt::one() << 1075)), 0.0);
let three = BigInt::from(3);
assert_eq!(ratio_to_f64(&three, &(BigInt::one() << 1076)), min_sub);
assert_eq!(ratio_to_f64(&one, &BigInt::from(3)), 1.0 / 3.0);
let tie = (BigInt::one() << 53) + 1;
assert_eq!(ratio_to_f64(&tie, &one), 9007199254740992.0);
let big = BigInt::one() << 1024;
assert_eq!(ratio_to_f64(&big, &one), f64::INFINITY);
assert_eq!(ratio_to_f64(&-big, &one), f64::NEG_INFINITY);
let max = BigQ::from_f64(f64::MAX);
assert_eq!(max.to_f64(), f64::MAX);
}
#[test]
fn every_f64_converts_to_its_exact_value_and_back() {
for x in [0.1, -2.5e-310, 1e300, f64::MIN_POSITIVE, 123456.789, -7.0] {
assert_eq!(BigQ::from_f64(x).to_f64().to_bits(), x.to_bits(), "{x}");
}
assert!(BigQ::from_f64(-0.0).num.is_zero());
}
}
+107
View File
@@ -0,0 +1,107 @@
//! Rounding any value to `places` for display, exactly (clause 12, "Format").
//! An inexact value is rounded over its shortest round-trip spelling (improvise-felo.9).
use num_bigint::BigInt;
use num_integer::Integer;
use num_traits::Signed;
use crate::lang::{Lexeme, shortest_round_trip};
use crate::num::Num;
use crate::ratio::BigQ;
/// The rounding mode is the caller's policy, not the crate's.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Rounding {
HalfAwayFromZero,
HalfEven,
}
impl Num {
/// Plain decimal text with exactly `places` fractional digits; `None` for a non-finite value.
pub fn round_to_places(&self, places: u8, mode: Rounding) -> Option<String> {
let q = match self.exact_q() {
Some(q) => q,
None => Lexeme::read(&shortest_round_trip(self.to_f64())?)?.exact_value(),
};
Some(render(round(&q, places, mode), places))
}
}
/// `q * 10^places`, rounded to an integer by `mode`.
fn round(q: &BigQ, places: u8, mode: Rounding) -> BigInt {
let scaled = q.num.abs() * BigInt::from(10).pow(places as u32);
let (whole, rest) = scaled.div_rem(&q.den);
let twice: BigInt = rest * 2;
let up = match twice.cmp(&q.den) {
std::cmp::Ordering::Greater => true,
std::cmp::Ordering::Equal => mode == Rounding::HalfAwayFromZero || whole.is_odd(),
std::cmp::Ordering::Less => false,
};
let magnitude = whole + BigInt::from(up as u8);
match q.num.is_negative() {
true => -magnitude,
false => magnitude,
}
}
fn render(units: BigInt, places: u8) -> String {
let digits = units.abs().to_string();
let width = places as usize + 1;
let padded = format!("{digits:0>width$}");
let (int, frac) = padded.split_at(padded.len() - places as usize);
let sign = if units.is_negative() { "-" } else { "" };
match places {
0 => format!("{sign}{int}"),
_ => format!("{sign}{int}.{frac}"),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::lang::{Typing, parse_number};
fn typed(s: &str) -> Num {
parse_number(s, Typing::Inferred).unwrap()
}
#[test]
fn a_typed_value_rounds_as_it_was_typed() {
let at2 = |n: &Num| n.round_to_places(2, Rounding::HalfAwayFromZero).unwrap();
for (text, want) in [
("1.005", "1.01"),
("1.015", "1.02"),
("0.285", "0.29"),
("-2.675", "-2.68"),
] {
assert_eq!(at2(&typed(text)), want, "{text}");
assert_eq!(
at2(&Num::inexact(text.parse().unwrap())),
want,
"inexact {text}"
);
}
assert_eq!(at2(&Num::ratio(10, 3).unwrap()), "3.33");
assert_eq!(at2(&Num::ratio(2, 3).unwrap()), "0.67");
assert_eq!(at2(&typed("-0.001")), "0.00");
let even = typed("0.125")
.round_to_places(2, Rounding::HalfEven)
.unwrap();
assert_eq!(even, "0.12");
assert_eq!(
typed("12.5")
.round_to_places(0, Rounding::HalfEven)
.unwrap(),
"12"
);
assert_eq!(
Num::inexact(f64::NAN).round_to_places(2, Rounding::HalfEven),
None
);
let wide = Num::ratio(1, 3)
.unwrap()
.round_to_places(40, Rounding::HalfEven)
.unwrap();
assert_eq!(wide, format!("0.{}", "3".repeat(40)));
}
}
+396
View File
@@ -0,0 +1,396 @@
//! Order-free, retractable sums (clause 8): ADR 009's f64 `ExactSum`, and `NumSum`,
//! which adds the exact partitions. The design: `context/code/numtower.md`, "Sums".
use std::collections::BTreeMap;
use num_bigint::{BigInt, BigUint};
use num_integer::Integer;
use num_traits::{One, Zero};
use crate::fixed::{I256, I320};
use crate::num::{MAX_SCALE, Num, NumRef, pow10};
use crate::ratio::{BigQ, ratio_to_f64};
/// The most non-decimal denominator groups a sum reads exactly (question 16: K = 8).
pub const K: usize = 8;
const LIMB_BITS: u32 = 32;
/// A finite f64 is `mant * 2^(shift - 1074)`, `shift <= 2045`; weights add 60 bits.
const LIMBS: usize = 70;
const CHUNK: u64 = 1 << 30;
/// An exact sum of weighted f64s, rounded once when read (ADR 009).
#[derive(Clone, Debug)]
pub struct ExactSum {
limbs: [i64; LIMBS],
pending: u64,
nan: i128,
pos_inf: i128,
neg_inf: i128,
}
/// Equal sums, not equal representations: carries propagated, `pending` ignored.
impl PartialEq for ExactSum {
fn eq(&self, o: &ExactSum) -> bool {
let (mut mine, mut theirs) = (self.limbs, o.limbs);
propagate(&mut mine);
propagate(&mut theirs);
let counts = |s: &ExactSum| (s.nan, s.pos_inf, s.neg_inf);
mine == theirs && counts(self) == counts(o)
}
}
impl Eq for ExactSum {}
impl Default for ExactSum {
fn default() -> Self {
ExactSum {
limbs: [0; LIMBS],
pending: 0,
nan: 0,
pos_inf: 0,
neg_inf: 0,
}
}
}
impl ExactSum {
pub fn new() -> ExactSum {
ExactSum::default()
}
/// Adds `x` with a Z-set `weight`; the weight is split into base-2^30 digits.
pub fn add(&mut self, x: f64, weight: i64) {
match (x.is_nan(), x.is_infinite(), x > 0.0) {
(true, _, _) => self.nan += weight as i128,
(false, true, true) => self.pos_inf += weight as i128,
(false, true, false) => self.neg_inf += weight as i128,
(false, false, _) => {
let magnitude = weight.unsigned_abs();
for j in 0..3 {
let digit = (magnitude >> (30 * j)) & (CHUNK - 1);
if digit != 0 {
let signed = if weight < 0 {
-(digit as i64)
} else {
digit as i64
};
self.add_digit(x, signed, 30 * j);
}
}
}
}
}
fn add_digit(&mut self, x: f64, weight: i64, extra: u32) {
if self.pending + weight.unsigned_abs() > CHUNK {
propagate(&mut self.limbs);
self.pending = 0;
}
let bits = x.to_bits();
let biased = ((bits >> 52) & 0x7ff) as u32;
let fraction = bits & ((1 << 52) - 1);
let (mant, shift) = match biased {
0 => (fraction, 0),
e => (fraction | (1 << 52), e - 1),
};
let sign = if x.is_sign_negative() {
-weight
} else {
weight
};
let at = shift + extra;
let wide = u128::from(mant) << (at % LIMB_BITS);
let k = (at / LIMB_BITS) as usize;
for i in 0..3 {
let part = ((wide >> (LIMB_BITS * i as u32)) & 0xffff_ffff) as i64;
self.limbs[k + i] += sign * part;
}
self.pending += weight.unsigned_abs();
}
/// Adds another accumulator's contents.
pub fn absorb(&mut self, o: &ExactSum) {
let mut theirs = o.limbs;
propagate(&mut theirs);
propagate(&mut self.limbs);
for (mine, t) in self.limbs.iter_mut().zip(theirs) {
*mine += t;
}
propagate(&mut self.limbs);
self.pending = 0;
self.nan += o.nan;
self.pos_inf += o.pos_inf;
self.neg_inf += o.neg_inf;
}
/// The exact sum rounded to the nearest f64, ties to even.
pub fn value(&self) -> f64 {
match (self.nan != 0, self.pos_inf != 0, self.neg_inf != 0) {
(true, _, _) | (false, true, true) => f64::NAN,
(false, true, false) => f64::INFINITY,
(false, false, true) => f64::NEG_INFINITY,
(false, false, false) => ratio_to_f64(&self.total(), &(BigInt::one() << 1074)),
}
}
/// The finite part as an integer in units of 2^-1074.
fn total(&self) -> BigInt {
let mut limbs = self.limbs;
propagate(&mut limbs);
let digits: Vec<u32> = limbs[..LIMBS - 1].iter().map(|&l| l as u32).collect();
let low = BigInt::from(BigUint::from_slice(&digits));
(BigInt::from(limbs[LIMBS - 1]) << (LIMB_BITS as usize * (LIMBS - 1))) + low
}
}
/// Carry each limb into the next, leaving every limb but the top in [0, 2^32).
fn propagate(limbs: &mut [i64; LIMBS]) {
for k in 0..LIMBS - 1 {
let carry = limbs[k] >> LIMB_BITS;
limbs[k] -= carry << LIMB_BITS;
limbs[k + 1] += carry;
}
}
/// A `SUM` over any `Num`s: exact when every term is exact and at most K groups remain.
#[derive(Clone, Debug, Default)]
pub struct NumSum {
decimals: I320,
groups: BTreeMap<i128, I256>,
group_floats: ExactSum,
inexact: ExactSum,
/// Net weight per f64 bit pattern: a read is inexact while any term is present.
inexact_terms: BTreeMap<u64, i128>,
}
impl NumSum {
pub fn new() -> NumSum {
NumSum::default()
}
pub fn insert(&mut self, x: &Num) {
self.add(x, 1);
}
pub fn retract(&mut self, x: &Num) {
self.add(x, -1);
}
/// Adds `x` with a Z-set `weight`. Exact and associative: the order cannot matter.
pub fn add(&mut self, x: &Num, weight: i64) {
match x.kind() {
NumRef::Inexact(f) => {
self.inexact.add(f, weight);
let net = self.inexact_terms.entry(f.to_bits()).or_insert(0);
*net += weight as i128;
if *net == 0 {
self.inexact_terms.remove(&f.to_bits());
}
}
NumRef::Exact(d) => {
let unit = pow10(MAX_SCALE - d.scale()) as u128;
self.add_scaled(d.mantissa(), unit, weight);
}
NumRef::Fraction(r) => match at_scale_28(*r.denom()) {
Some(unit) => self.add_scaled(*r.numer(), unit, weight),
None => self.add_to_group(*r.numer(), *r.denom(), weight),
},
}
}
fn add_scaled(&mut self, n: i128, unit: u128, weight: i64) {
let magnitude = weight.unsigned_abs() as u128;
let scaled = match unit.checked_mul(magnitude) {
Some(both) => I320::from_i128(n).checked_mul_u128(both),
None => I320::from_i128(n)
.checked_mul_u128(unit)
.and_then(|t| t.checked_mul_u128(magnitude)),
};
let term = scaled
.and_then(|t| if weight < 0 { t.checked_neg() } else { Some(t) })
.expect("a weighted term is below 2^284");
self.decimals = self
.decimals
.checked_add(&term)
.expect("320 bits holds 2^36 terms");
}
fn add_to_group(&mut self, n: i128, d: i128, weight: i64) {
let old = self.groups.get(&d).copied().unwrap_or(I256::ZERO);
let new = old
.checked_add(&I256::mul_i128(n, weight as i128))
.expect("256 bits holds 2^64 terms");
let value = |num: I256| ratio_to_f64(&num.to_bigint(), &BigInt::from(d));
if !old.is_zero() {
self.group_floats.add(value(old), -1);
}
match new.is_zero() {
true => {
self.groups.remove(&d);
}
false => {
self.group_floats.add(value(new), 1);
self.groups.insert(d, new);
}
}
}
/// The current non-decimal denominator groups.
pub fn groups(&self) -> usize {
self.groups.len()
}
/// One read: exact over at most K groups, else the f64 accumulators (clause 8).
pub fn read(&self) -> Num {
match (!self.inexact_terms.is_empty(), self.groups.len() > K) {
(false, false) => Num::settle(&self.exact_total()),
(_, true) => {
let mut acc = self.inexact.clone();
acc.absorb(&self.group_floats);
acc.add(self.decimal_total().to_f64(), 1);
Num::inexact(acc.value())
}
(true, false) => {
let mut acc = self.inexact.clone();
acc.add(self.exact_total().to_f64(), 1);
Num::inexact(acc.value())
}
}
}
fn decimal_total(&self) -> BigQ {
BigQ::new(self.decimals.to_bigint(), BigInt::from(pow10(MAX_SCALE)))
}
fn exact_total(&self) -> BigQ {
let decimal = (!self.decimals.is_zero())
.then(|| (self.decimals.to_bigint(), BigInt::from(pow10(MAX_SCALE))));
let terms: Vec<(BigInt, BigInt)> = decimal
.into_iter()
.chain(
self.groups
.iter()
.map(|(d, n)| (n.to_bigint(), BigInt::from(*d))),
)
.collect();
let (num, den) = combine_over_lcm(&terms);
BigQ::new(num, den)
}
}
/// `Σ n/d` over the lcm of the denominators, not yet reduced: the read reduces once.
pub(crate) fn combine_over_lcm(terms: &[(BigInt, BigInt)]) -> (BigInt, BigInt) {
let lcm = terms.iter().fold(BigInt::one(), |l, (_, d)| l.lcm(d));
let num = terms
.iter()
.fold(BigInt::zero(), |s, (n, d)| s + n * (&lcm / d));
(num, lcm)
}
/// `10^28 / d` when `d = 2^p 5^q` with `max(p, q) <= 28`: the term joins the decimal partition.
fn at_scale_28(d: i128) -> Option<u128> {
let p = d.trailing_zeros();
let mut rest = d >> p;
let mut q = 0;
while rest % 5 == 0 {
rest /= 5;
q += 1;
}
(rest == 1 && p.max(q) <= MAX_SCALE).then(|| (pow10(MAX_SCALE) / d) as u128)
}
#[cfg(test)]
mod tests {
use super::*;
use num_rational::BigRational;
use proptest::prelude::*;
use proptest::test_runner::{Config, RngAlgorithm, TestRng, TestRunner};
type Combine = fn(&[(BigInt, BigInt)]) -> (BigInt, BigInt);
/// Up to K groups of wide numerators whose denominators share factors.
fn groups() -> impl Strategy<Value = Vec<(BigInt, BigInt)>> {
let shared = prop::sample::select(vec![1i128, 3, 7 * 11, 1 << 20, 3i128.pow(40)]);
let group =
(any::<i128>(), any::<i64>(), shared, 1i128..(1 << 60)).prop_map(|(n, w, s, d)| {
(
BigInt::from(n) * BigInt::from(w),
BigInt::from(s) * BigInt::from(d),
)
});
prop::collection::vec(group, 1..=K)
}
fn check_grouped_read(combine: Combine, deterministic: bool) -> Result<(), String> {
let config = Config {
cases: 256,
failure_persistence: None,
..Config::default()
};
let mut runner = match deterministic {
true => {
TestRunner::new_with_rng(config, TestRng::deterministic_rng(RngAlgorithm::ChaCha))
}
false => TestRunner::new(config),
};
let result = runner.run(&groups(), |terms| {
let (num, den) = combine(&terms);
let lcm = terms.iter().fold(BigInt::one(), |l, (_, d)| l.lcm(d));
prop_assert_eq!(&den, &lcm, "the read did not combine over the lcm");
let oracle: BigRational = terms
.iter()
.map(|(n, d)| BigRational::new(n.clone(), d.clone()))
.sum();
let read = BigQ::new(num, den);
let got = BigRational::new(read.num.clone(), read.den.clone());
prop_assert_eq!(got, oracle, "the read is not the sum");
prop_assert!(read.num.gcd(&read.den).is_one(), "the read is not reduced");
Ok(())
});
result.map_err(|e| e.to_string())
}
#[test]
fn the_grouped_read_combines_over_the_lcm_and_reduces_once() {
check_grouped_read(combine_over_lcm, false).unwrap();
}
fn over_the_product(terms: &[(BigInt, BigInt)]) -> (BigInt, BigInt) {
let den = terms.iter().fold(BigInt::one(), |p, (_, d)| p * d);
let num = terms
.iter()
.fold(BigInt::zero(), |s, (n, d)| s + n * (&den / d));
(num, den)
}
#[test]
fn the_grouped_read_combines_over_the_lcm_and_reduces_once_catches_the_product() {
let err = check_grouped_read(over_the_product, true).expect_err("the rival passed");
assert!(
err.contains("the read did not combine over the lcm"),
"{err}"
);
}
#[test]
fn a_fraction_with_a_decimal_denominator_joins_the_decimal_partition() {
let mut s = NumSum::new();
s.insert(&Num::ratio(10i128.pow(35), 1).unwrap());
s.insert(&Num::ratio(3, 1 << 20).unwrap());
assert_eq!(s.groups(), 0);
s.insert(&Num::ratio(1, 1i128 << 40).unwrap());
s.insert(&Num::ratio(1, 3).unwrap());
assert_eq!(
s.groups(),
2,
"2^40 has scale 40, past 28: a group of its own"
);
s.retract(&Num::ratio(1, 3).unwrap());
assert_eq!(
s.groups(),
1,
"a group whose numerator returns to zero is dropped"
);
}
}
+189
View File
@@ -0,0 +1,189 @@
//! The `BigRational` oracle and the generators the property tests share.
//! Each property is a checker taking the implementation, so a rival can be run through it.
#![allow(dead_code)]
use std::cmp::Ordering;
use num_bigint::BigInt;
use num_rational::BigRational;
use num_traits::{One, Signed, ToPrimitive, Zero};
use numtower::{Decimal, Num, NumRef};
use proptest::prelude::*;
use proptest::test_runner::{Config, RngAlgorithm, TestCaseError, TestRng, TestRunner};
pub fn q(n: &Num) -> Option<BigRational> {
match n.kind() {
NumRef::Exact(d) => Some(BigRational::new(
d.mantissa().into(),
BigInt::from(10).pow(d.scale()),
)),
NumRef::Fraction(r) => Some(BigRational::new((*r.numer()).into(), (*r.denom()).into())),
NumRef::Inexact(x) => BigRational::from_float(x),
}
}
/// Whether `v` fits a stored exact arm: a decimal at scale <= 28, or an `i128` fraction.
pub fn fits(v: &BigRational) -> bool {
let limit = BigInt::one() << 96;
let decimal = (0..=28u32).any(|s| {
let scaled = v * BigRational::from_integer(BigInt::from(10).pow(s));
scaled.is_integer() && scaled.to_integer().abs() < limit
});
let (min, max) = (BigInt::from(i128::MIN), BigInt::from(i128::MAX));
decimal || (&min <= v.numer() && v.numer() <= &max && v.denom() <= &max)
}
/// Whether `f` is the round-to-nearest-even f64 of `v`: the review's interval form.
pub fn is_nearest(f: f64, v: &BigRational) -> bool {
let exact = |x: f64| BigRational::from_float(x).expect("finite");
let two = BigRational::from_integer(BigInt::from(2));
let half_ulp_past = |x: f64, up: bool| {
let (next, inward) = match up {
true => (x.next_up(), x.next_down()),
false => (x.next_down(), x.next_up()),
};
match next.is_finite() {
true => (exact(x) + exact(next)) / &two,
false => exact(x) + (exact(x) - exact(inward)) / &two,
}
};
match f.is_finite() {
false => {
let edge = half_ulp_past(f64::MAX.copysign(f), f > 0.0);
(f > 0.0 && v >= &edge) || (f < 0.0 && v <= &edge)
}
true => {
let lo = half_ulp_past(f, false);
let hi = half_ulp_past(f, true);
let even = f.to_bits() & 1 == 0;
match even {
true => &lo <= v && v <= &hi,
false => &lo < v && v < &hi,
}
}
}
}
/// A decimal's text built digit by digit, without `rust_decimal`'s `Display`.
pub fn decimal_text(mantissa: i128, scale: u32) -> String {
let digits = format!(
"{:0>width$}",
mantissa.unsigned_abs(),
width = scale as usize + 1
);
let (int, frac) = digits.split_at(digits.len() - scale as usize);
let sign = if mantissa < 0 { "-" } else { "" };
format!("{sign}{int}.{frac}0")
}
/// The correctly rounded f64 of an exact value, independent of `numtower`'s own.
pub fn independent_f64(n: &Num) -> f64 {
match n.kind() {
NumRef::Exact(d) => decimal_text(d.mantissa(), d.scale()).parse().unwrap(),
NumRef::Fraction(_) => q(n).unwrap().to_f64().unwrap(),
NumRef::Inexact(x) => x,
}
}
/// Same arm, same value and same scale: what a fingerprint would see.
pub fn same(a: &Num, b: &Num) -> bool {
match (a.kind(), b.kind()) {
(NumRef::Exact(x), NumRef::Exact(y)) => {
x.mantissa() == y.mantissa() && x.scale() == y.scale()
}
(NumRef::Fraction(x), NumRef::Fraction(y)) => x == y,
(NumRef::Inexact(x), NumRef::Inexact(y)) => x.to_bits() == y.to_bits(),
_ => false,
}
}
pub fn arm(n: &Num) -> &'static str {
match n.kind() {
NumRef::Exact(_) => "exact",
NumRef::Fraction(_) => "fraction",
NumRef::Inexact(_) => "inexact",
}
}
pub fn mantissa96() -> impl Strategy<Value = i128> {
let max = (1i128 << 96) - 1;
prop_oneof![
-max..=max,
-1_000_000i128..1_000_000,
-(10i128.pow(15))..10i128.pow(15),
]
}
pub fn decimal() -> impl Strategy<Value = Num> {
(mantissa96(), 0u32..=28).prop_map(|(m, s)| Num::exact(Decimal::from_i128_with_scale(m, s)))
}
fn of_width(bits: u32) -> impl Strategy<Value = i128> {
(any::<u128>(), any::<bool>()).prop_map(move |(r, neg)| {
let v = ((r >> (128 - bits)) | (1u128 << (bits - 1))) as i128;
if neg { -v } else { v }
})
}
pub fn fraction() -> impl Strategy<Value = Num> {
let widths = (1u32..=126, 1u32..=126)
.prop_flat_map(|(a, b)| (of_width(a), of_width(b)))
.prop_map(|(n, d)| Num::ratio(n, d).unwrap());
let edge = (1i128..i128::MAX).prop_map(|d| Num::ratio(i128::MIN, d | 1).unwrap());
prop_oneof![8 => widths, 1 => edge]
}
pub fn finite_f64() -> impl Strategy<Value = f64> {
use proptest::num::f64::{NEGATIVE, NORMAL, POSITIVE, SUBNORMAL, ZERO};
prop_oneof![
(POSITIVE | NEGATIVE | NORMAL | SUBNORMAL | ZERO),
-1e6f64..1e6,
(-1000i64..1000).prop_map(|k| k as f64 / 8.0),
]
}
pub fn exact() -> impl Strategy<Value = Num> {
prop_oneof![decimal(), fraction()]
}
pub fn any_num() -> impl Strategy<Value = Num> {
prop_oneof![
3 => exact(),
1 => finite_f64().prop_map(Num::inexact),
1 => prop_oneof![Just(f64::NAN), Just(f64::INFINITY), Just(f64::NEG_INFINITY)].prop_map(Num::inexact),
]
}
/// Runs a property; `deterministic` for a rival, so its failure is not luck.
pub fn run<S: Strategy>(
strategy: S,
cases: u32,
deterministic: bool,
test: impl Fn(S::Value) -> Result<(), TestCaseError>,
) -> Result<(), String> {
let config = Config {
cases,
failure_persistence: None,
..Config::default()
};
let mut runner = match deterministic {
true => TestRunner::new_with_rng(config, TestRng::deterministic_rng(RngAlgorithm::ChaCha)),
false => TestRunner::new(config),
};
runner.run(&strategy, test).map_err(|e| e.to_string())
}
/// A rival must fail on the property's own assertion, never on something else.
pub fn assert_caught(result: Result<(), String>, marker: &str) {
let err = result.expect_err("the rival passed: the property cannot see it");
assert!(err.contains(marker), "failed for another reason: {err}");
}
pub fn oracle_cmp(a: &Num, b: &Num) -> Option<Ordering> {
Some(q(a)?.cmp(&q(b)?))
}
pub fn integer(v: &BigRational) -> bool {
v.is_integer() && !v.numer().is_zero()
}
File diff suppressed because it is too large Load Diff