#![expect(
clippy::cast_sign_loss,
reason = "exponent arithmetic uses intentional i64->u64 casts for bit manipulation"
)]
#![expect(
clippy::cast_possible_truncation,
reason = "exponent/mantissa arithmetic for exact floating point — bounded by construction"
)]
#![expect(
clippy::cast_possible_wrap,
reason = "u64 mantissa bits reinterpreted as i64 for exponent arithmetic — values bounded"
)]
use num_bigint::{BigInt, Sign};
use num_traits::ToPrimitive;
use std::cmp::Ordering;
use std::fmt;
#[derive(Clone, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct ExactFloat {
mantissa: BigInt,
exp: i64,
}
impl ExactFloat {
#[inline]
pub fn zero() -> Self {
ExactFloat {
mantissa: BigInt::from(0i64),
exp: 0,
}
}
#[inline]
pub fn one() -> Self {
ExactFloat {
mantissa: BigInt::from(1i64),
exp: 0,
}
}
#[inline]
pub fn is_zero(&self) -> bool {
self.mantissa.sign() == Sign::NoSign
}
#[inline]
pub fn signum(&self) -> i32 {
match self.mantissa.sign() {
Sign::Minus => -1,
Sign::NoSign => 0,
Sign::Plus => 1,
}
}
pub fn abs(&self) -> Self {
ExactFloat {
mantissa: BigInt::from(self.mantissa.magnitude().clone()),
exp: self.exp,
}
}
pub fn neg(&self) -> Self {
ExactFloat {
mantissa: -&self.mantissa,
exp: self.exp,
}
}
pub fn add(&self, other: &Self) -> Self {
if self.is_zero() {
return other.clone();
}
if other.is_zero() {
return self.clone();
}
let mut result = if self.exp <= other.exp {
let shift = (other.exp - self.exp) as usize;
ExactFloat {
mantissa: &self.mantissa + (&other.mantissa << shift),
exp: self.exp,
}
} else {
let shift = (self.exp - other.exp) as usize;
ExactFloat {
mantissa: (&self.mantissa << shift) + &other.mantissa,
exp: other.exp,
}
};
result.normalize();
result
}
pub fn sub(&self, other: &Self) -> Self {
self.add(&other.neg())
}
pub fn mul(&self, other: &Self) -> Self {
if self.is_zero() || other.is_zero() {
return Self::zero();
}
let mut result = ExactFloat {
mantissa: &self.mantissa * &other.mantissa,
exp: self.exp + other.exp,
};
result.normalize();
result
}
pub fn exp(&self) -> i64 {
if self.is_zero() {
return i64::MIN;
}
self.exp + self.mantissa.bits() as i64
}
pub fn to_f64_shifted(&self, shift: i64) -> f64 {
if self.is_zero() {
return 0.0;
}
let bits = self.mantissa.bits() as i64;
if bits <= 53 {
let m = self.mantissa.to_f64().unwrap_or(0.0);
ldexp_f64(m, self.exp - shift)
} else {
let reduce = (bits - 53) as usize;
let reduced = &self.mantissa >> reduce;
let m = reduced.to_f64().unwrap_or(0.0);
ldexp_f64(m, self.exp + reduce as i64 - shift)
}
}
pub fn to_f64(&self) -> f64 {
if self.is_zero() {
return 0.0;
}
let bits = self.mantissa.bits() as i64;
if bits <= 53 {
let m = self.mantissa.to_f64().unwrap_or(0.0);
ldexp_f64(m, self.exp)
} else {
let shift = (bits - 53) as usize;
let reduced = &self.mantissa >> shift;
let m = reduced.to_f64().unwrap_or(0.0);
ldexp_f64(m, self.exp + shift as i64)
}
}
fn normalize(&mut self) {
if self.is_zero() {
self.mantissa = BigInt::from(0i64);
self.exp = 0;
return;
}
if let Some(tz) = self.mantissa.magnitude().trailing_zeros()
&& tz > 0
{
let m = std::mem::replace(&mut self.mantissa, BigInt::from(0i64));
self.mantissa = m >> (tz as usize);
self.exp += tz as i64;
}
}
}
fn ldexp_f64(m: f64, exp: i64) -> f64 {
if exp == 0 || m == 0.0 {
return m;
}
let exp = exp.clamp(-2100, 2100) as i32;
if exp.abs() <= 1023 {
m * (2.0f64).powi(exp)
} else {
let half = exp / 2;
let other = exp - half;
m * (2.0f64).powi(half) * (2.0f64).powi(other)
}
}
impl From<f64> for ExactFloat {
fn from(v: f64) -> Self {
if v == 0.0 {
return Self::zero();
}
let bits = v.to_bits();
let negative = (bits >> 63) != 0;
let biased_exp = ((bits >> 52) & 0x7FF) as i64;
let fraction = bits & 0x000F_FFFF_FFFF_FFFFu64;
assert!(
biased_exp != 0x7FF,
"ExactFloat does not support infinity or NaN"
);
let (mantissa_val, exp) = if biased_exp == 0 {
(fraction, -1074i64)
} else {
((1u64 << 52) | fraction, biased_exp - 1023 - 52)
};
let mut mantissa = BigInt::from(mantissa_val);
if negative {
mantissa = -mantissa;
}
let mut result = ExactFloat { mantissa, exp };
result.normalize();
result
}
}
impl From<i64> for ExactFloat {
fn from(v: i64) -> Self {
if v == 0 {
return Self::zero();
}
let mut result = ExactFloat {
mantissa: BigInt::from(v),
exp: 0,
};
result.normalize();
result
}
}
impl Default for ExactFloat {
fn default() -> Self {
ExactFloat::zero()
}
}
impl PartialEq for ExactFloat {
fn eq(&self, other: &Self) -> bool {
self.exp == other.exp && self.mantissa == other.mantissa
}
}
impl Eq for ExactFloat {}
impl Ord for ExactFloat {
fn cmp(&self, other: &Self) -> Ordering {
let s1 = self.signum();
let s2 = other.signum();
if s1 != s2 {
return s1.cmp(&s2);
}
if s1 == 0 {
return Ordering::Equal;
}
if self.exp == other.exp {
return self.mantissa.cmp(&other.mantissa);
}
let diff = self.sub(other);
match diff.signum() {
-1 => Ordering::Less,
0 => Ordering::Equal,
1 => Ordering::Greater,
_ => unreachable!("signum returns -1, 0, or 1"),
}
}
}
impl PartialOrd for ExactFloat {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl fmt::Display for ExactFloat {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.to_f64())
}
}
#[cfg(test)]
mod tests {
use super::*;
fn is_send_sync<T: Sized + Send + Sync + Unpin>() {}
#[test]
fn exact_float_is_send_sync() {
is_send_sync::<ExactFloat>();
}
#[test]
fn test_from_f64_zero() {
let z = ExactFloat::from(0.0);
assert!(z.is_zero());
assert_eq!(z.signum(), 0);
}
#[test]
fn test_from_f64_integers() {
assert_eq!(ExactFloat::from(1.0), ExactFloat::one());
assert_eq!(ExactFloat::from(1.0).to_f64(), 1.0);
assert_eq!(ExactFloat::from(-1.0).to_f64(), -1.0);
assert_eq!(ExactFloat::from(42.0).to_f64(), 42.0);
}
const MIRI_TOL: f64 = 1e-14;
#[test]
fn test_from_f64_fractions() {
assert!((ExactFloat::from(0.5).to_f64() - 0.5).abs() < MIRI_TOL);
assert!((ExactFloat::from(0.25).to_f64() - 0.25).abs() < MIRI_TOL);
assert!((ExactFloat::from(1.5).to_f64() - 1.5).abs() < MIRI_TOL);
}
#[test]
fn test_add() {
let a = ExactFloat::from(1.0);
let b = ExactFloat::from(2.0);
assert_eq!(a.add(&b).to_f64(), 3.0);
let c = ExactFloat::from(-1.0);
assert_eq!(a.add(&c).to_f64(), 0.0);
assert!(a.add(&c).is_zero());
}
#[test]
fn test_sub() {
let a = ExactFloat::from(3.0);
let b = ExactFloat::from(1.0);
assert!((a.sub(&b).to_f64() - 2.0).abs() < MIRI_TOL);
assert!(a.sub(&a).is_zero());
}
#[test]
fn test_mul() {
let a = ExactFloat::from(3.0);
let b = ExactFloat::from(4.0);
assert!((a.mul(&b).to_f64() - 12.0).abs() < MIRI_TOL);
let c = ExactFloat::from(-2.0);
assert!((a.mul(&c).to_f64() - (-6.0)).abs() < MIRI_TOL);
}
#[test]
fn test_comparison() {
let a = ExactFloat::from(1.0);
let b = ExactFloat::from(2.0);
let c = ExactFloat::from(1.0);
assert!(a < b);
assert!(b > a);
assert_eq!(a, c);
}
#[test]
fn test_exact_cancellation() {
let big = ExactFloat::from(1e20);
let one = ExactFloat::from(1.0);
let result = big.add(&one).sub(&big);
assert_eq!(result, one);
}
#[test]
fn test_abs_neg() {
let a = ExactFloat::from(-3.0);
assert_eq!(a.abs().to_f64(), 3.0);
assert_eq!(a.neg().to_f64(), 3.0);
assert_eq!(a.neg().neg().to_f64(), -3.0);
}
#[test]
fn test_signum() {
assert_eq!(ExactFloat::from(5.0).signum(), 1);
assert_eq!(ExactFloat::from(-5.0).signum(), -1);
assert_eq!(ExactFloat::zero().signum(), 0);
}
#[test]
fn test_to_f64_zero() {
assert_eq!(ExactFloat::from(0.0).to_f64(), 0.0);
}
#[test]
fn test_to_f64_max() {
assert_eq!(ExactFloat::from(f64::MAX).to_f64(), f64::MAX);
}
#[test]
fn test_to_f64_neg_min_positive() {
assert_eq!(
ExactFloat::from(-f64::MIN_POSITIVE).to_f64(),
-f64::MIN_POSITIVE
);
}
#[test]
fn test_to_f64_denorm_min() {
let denorm_min = f64::from_bits(1); assert_eq!(ExactFloat::from(denorm_min).to_f64(), denorm_min);
}
#[test]
fn test_to_f64_negative_fraction() {
assert_eq!(ExactFloat::from(-12.7).to_f64(), -12.7);
}
#[test]
fn test_to_f64_pi() {
assert_eq!(
ExactFloat::from(std::f64::consts::PI).to_f64(),
std::f64::consts::PI
);
}
#[test]
fn test_to_f64_large_mantissa_roundtrip() {
let a = ExactFloat::from(1e15);
let b = ExactFloat::from(1e15);
let product = a.mul(&b);
let result = product.to_f64();
assert!(
(result - 1e30).abs() / 1e30 < 1e-15,
"expected ~1e30, got {result}"
);
}
#[test]
fn test_exp() {
assert_eq!(ExactFloat::from(1.0).exp(), 1);
assert_eq!(ExactFloat::from(2.0).exp(), 2);
assert_eq!(ExactFloat::from(0.5).exp(), 0);
assert_eq!(ExactFloat::zero().exp(), i64::MIN);
assert_eq!(ExactFloat::from(-4.0).exp(), 3);
}
#[test]
fn test_to_f64_shifted() {
assert_eq!(ExactFloat::from(8.0).to_f64_shifted(0), 8.0);
assert_eq!(ExactFloat::from(8.0).to_f64_shifted(3), 1.0);
assert_eq!(ExactFloat::from(8.0).to_f64_shifted(-1), 16.0);
assert_eq!(ExactFloat::zero().to_f64_shifted(100), 0.0);
let tiny = ExactFloat::from(5e-324);
let exp = tiny.exp();
let shifted = tiny.to_f64_shifted(exp);
assert!((0.25..=1.0).contains(&shifted), "shifted = {shifted}");
}
#[cfg(feature = "serde")]
#[test]
fn test_serde_roundtrip() {
let cases = [
0.0,
1.0,
-1.0,
0.5,
1e20,
-std::f64::consts::PI,
f64::MIN_POSITIVE,
];
for &v in &cases {
let ef = ExactFloat::from(v);
let json = serde_json::to_string(&ef).unwrap();
let back: ExactFloat = serde_json::from_str(&json).unwrap();
assert_eq!(back, ef, "roundtrip failed for {v}");
}
}
}