| //! Everything related to verifying that parsed outputs are correct. | |
| use std::any::{Any, type_name}; | |
| use std::collections::BTreeMap; | |
| use std::ops::RangeInclusive; | |
| use std::str::FromStr; | |
| use std::sync::LazyLock; | |
| use num::bigint::ToBigInt; | |
| use num::{BigInt, BigRational, FromPrimitive, Signed, ToPrimitive}; | |
| use crate::{CheckFailure, Float, Int}; | |
| /// Powers of two that we store for constants. Account for binary128 which has a 15-bit exponent. | |
| const POWERS_OF_TWO_RANGE: RangeInclusive<i32> = (-(2 << 15))..=(2 << 15); | |
| /// Powers of ten that we cache. Account for binary128, which can fit +4932/-4931 | |
| const POWERS_OF_TEN_RANGE: RangeInclusive<i32> = -5_000..=5_000; | |
| /// Cached powers of 10 so we can look them up rather than recreating. | |
| static POWERS_OF_TEN: LazyLock<BTreeMap<i32, BigRational>> = LazyLock::new(|| { | |
| POWERS_OF_TEN_RANGE.map(|exp| (exp, BigRational::from_u32(10).unwrap().pow(exp))).collect() | |
| }); | |
| /// Rational property-related constants for a specific float type. | |
| #[allow(dead_code)] | |
| #[derive(Debug)] | |
| pub struct Constants { | |
| /// The minimum positive value (a subnormal). | |
| min_subnormal: BigRational, | |
| /// The maximum possible finite value. | |
| max: BigRational, | |
| /// Cutoff between rounding to zero and rounding to the minimum value (min subnormal). | |
| zero_cutoff: BigRational, | |
| /// Cutoff between rounding to the max value and rounding to infinity. | |
| inf_cutoff: BigRational, | |
| /// Opposite of `inf_cutoff` | |
| neg_inf_cutoff: BigRational, | |
| /// The powers of two for all relevant integers. | |
| powers_of_two: BTreeMap<i32, BigRational>, | |
| /// Half of each power of two. ULP = "unit in last position". | |
| /// | |
| /// This is a mapping from integers to half the precision available at that exponent. In other | |
| /// words, `0.5 * 2^n` = `2^(n-1)`, which is half the distance between `m * 2^n` and | |
| /// `(m + 1) * 2^n`, m ∈ ℤ. | |
| /// | |
| /// So, this is the maximum error from a real number to its floating point representation, | |
| /// assuming the float type can represent the exponent. | |
| half_ulp: BTreeMap<i32, BigRational>, | |
| /// Handy to have around so we don't need to reallocate for it | |
| two: BigInt, | |
| } | |
| impl Constants { | |
| pub fn new<F: Float>() -> Self { | |
| let two_int = &BigInt::from_u32(2).unwrap(); | |
| let two = &BigRational::from_integer(2.into()); | |
| // The minimum subnormal (aka minimum positive) value. Most negative power of two is the | |
| // minimum exponent (bias - 1) plus the extra from shifting within the mantissa bits. | |
| let min_subnormal = two.pow(-(F::EXP_BIAS + F::MAN_BITS - 1).to_signed()); | |
| // The maximum value is the maximum exponent with a fully saturated mantissa. This | |
| // is easiest to calculate by evaluating what the next value up would be if representable | |
| // (zeroed mantissa, exponent increments by one, i.e. `2^(bias + 1)`), and subtracting | |
| // a single LSB (`2 ^ (-mantissa_bits)`). | |
| let max = (two - two.pow(-F::MAN_BITS.to_signed())) * (two.pow(F::EXP_BIAS.to_signed())); | |
| let zero_cutoff = &min_subnormal / two_int; | |
| let inf_cutoff = &max + two_int.pow(F::EXP_BIAS - F::MAN_BITS - 1); | |
| let neg_inf_cutoff = -&inf_cutoff; | |
| let powers_of_two: BTreeMap<i32, _> = | |
| (POWERS_OF_TWO_RANGE).map(|n| (n, two.pow(n))).collect(); | |
| let mut half_ulp = powers_of_two.clone(); | |
| half_ulp.iter_mut().for_each(|(_k, v)| *v = &*v / two_int); | |
| Self { | |
| min_subnormal, | |
| max, | |
| zero_cutoff, | |
| inf_cutoff, | |
| neg_inf_cutoff, | |
| powers_of_two, | |
| half_ulp, | |
| two: two_int.clone(), | |
| } | |
| } | |
| } | |
| /// Validate that a string parses correctly | |
| pub fn validate<F: Float>(input: &str) -> Result<(), CheckError> { | |
| // Catch panics in case debug assertions within `std` fail. | |
| let parsed = std::panic::catch_unwind(|| { | |
| input.parse::<F>().map_err(|e| CheckError { | |
| fail: CheckFailure::ParsingFailed(e.to_string().into()), | |
| input: input.into(), | |
| float_res: "none".into(), | |
| }) | |
| }) | |
| .map_err(|e| convert_panic_error(&e, input))??; | |
| // Parsed float, decoded into significand and exponent | |
| let decoded = decode(parsed); | |
| // Float parsed separately into a rational | |
| let rational = Rational::parse(input); | |
| // Verify that the values match | |
| decoded.check(rational, input) | |
| } | |
| /// Turn panics into concrete error types. | |
| fn convert_panic_error(e: &dyn Any, input: &str) -> CheckError { | |
| let msg = e | |
| .downcast_ref::<String>() | |
| .map(|s| s.as_str()) | |
| .or_else(|| e.downcast_ref::<&str>().copied()) | |
| .unwrap_or("(no contents)"); | |
| CheckError { | |
| fail: CheckFailure::Panic(msg.into()), | |
| input: input.into(), | |
| float_res: "none".into(), | |
| } | |
| } | |
| /// The result of parsing a string to a float type. | |
| #[derive(Clone, Copy, Debug, PartialEq)] | |
| pub enum FloatRes<F: Float> { | |
| Inf, | |
| NegInf, | |
| Zero, | |
| Nan, | |
| /// A real number with significand and exponent. Value is `sig * 2 ^ exp`. | |
| Real { | |
| sig: F::SInt, | |
| exp: i32, | |
| }, | |
| } | |
| #[derive(Clone, Debug)] | |
| pub struct CheckError { | |
| pub fail: CheckFailure, | |
| /// String for which parsing was attempted. | |
| pub input: Box<str>, | |
| /// The parsed & decomposed `FloatRes`, already stringified so we don't need generics here. | |
| pub float_res: Box<str>, | |
| } | |
| impl<F: Float> FloatRes<F> { | |
| /// Given a known exact rational, check that this representation is accurate within the | |
| /// limits of the float representation. If not, construct a failure `Update` to send. | |
| fn check(self, expected: Rational, input: &str) -> Result<(), CheckError> { | |
| let consts = F::constants(); | |
| // let bool_helper = |cond: bool, err| cond.then_some(()).ok_or(err); | |
| let res = match (expected, self) { | |
| // Easy correct cases | |
| (Rational::Inf, FloatRes::Inf) | |
| | (Rational::NegInf, FloatRes::NegInf) | |
| | (Rational::Nan, FloatRes::Nan) => Ok(()), | |
| // Easy incorrect cases | |
| ( | |
| Rational::Inf, | |
| FloatRes::NegInf | FloatRes::Zero | FloatRes::Nan | FloatRes::Real { .. }, | |
| ) => Err(CheckFailure::ExpectedInf), | |
| ( | |
| Rational::NegInf, | |
| FloatRes::Inf | FloatRes::Zero | FloatRes::Nan | FloatRes::Real { .. }, | |
| ) => Err(CheckFailure::ExpectedNegInf), | |
| ( | |
| Rational::Nan, | |
| FloatRes::Inf | FloatRes::NegInf | FloatRes::Zero | FloatRes::Real { .. }, | |
| ) => Err(CheckFailure::ExpectedNan), | |
| (Rational::Finite(_), FloatRes::Nan) => Err(CheckFailure::UnexpectedNan), | |
| // Cases near limits | |
| (Rational::Finite(r), FloatRes::Zero) => { | |
| if r <= consts.zero_cutoff { | |
| Ok(()) | |
| } else { | |
| Err(CheckFailure::UnexpectedZero) | |
| } | |
| } | |
| (Rational::Finite(r), FloatRes::Inf) => { | |
| if r >= consts.inf_cutoff { | |
| Ok(()) | |
| } else { | |
| Err(CheckFailure::UnexpectedInf) | |
| } | |
| } | |
| (Rational::Finite(r), FloatRes::NegInf) => { | |
| if r <= consts.neg_inf_cutoff { | |
| Ok(()) | |
| } else { | |
| Err(CheckFailure::UnexpectedNegInf) | |
| } | |
| } | |
| // Actual numbers | |
| (Rational::Finite(r), FloatRes::Real { sig, exp }) => Self::validate_real(r, sig, exp), | |
| }; | |
| res.map_err(|fail| CheckError { | |
| fail, | |
| input: input.into(), | |
| float_res: format!("{self:?}").into(), | |
| }) | |
| } | |
| /// Check that `sig * 2^exp` is the same as `rational`, within the float's error margin. | |
| fn validate_real(rational: BigRational, sig: F::SInt, exp: i32) -> Result<(), CheckFailure> { | |
| let consts = F::constants(); | |
| // `2^exp`. Use cached powers of two to be faster. | |
| let two_exp = consts | |
| .powers_of_two | |
| .get(&exp) | |
| .unwrap_or_else(|| panic!("missing exponent {exp} for {}", type_name::<F>())); | |
| // Rational from the parsed value, `sig * 2^exp` | |
| let parsed_rational = two_exp * sig.to_bigint().unwrap(); | |
| let error = (parsed_rational - &rational).abs(); | |
| // Determine acceptable error at this exponent, which is halfway between this value | |
| // (`sig * 2^exp`) and the next value up (`(sig+1) * 2^exp`). | |
| let half_ulp = consts.half_ulp.get(&exp).unwrap(); | |
| // If we are within one error value (but not equal) then we rounded correctly. | |
| if &error < half_ulp { | |
| return Ok(()); | |
| } | |
| // For values where we are exactly between two representable values, meaning that the error | |
| // is exactly one half of the precision at that exponent, we need to round to an even | |
| // binary value (i.e. mantissa ends in 0). | |
| let incorrect_midpoint_rounding = if &error == half_ulp { | |
| if sig & F::SInt::ONE == F::SInt::ZERO { | |
| return Ok(()); | |
| } | |
| // We rounded to odd rather than even; failing based on midpoint rounding. | |
| true | |
| } else { | |
| // We are out of spec for some other reason. | |
| false | |
| }; | |
| let one_ulp = consts.half_ulp.get(&(exp + 1)).unwrap(); | |
| assert_eq!(one_ulp, &(half_ulp * &consts.two), "ULP values are incorrect"); | |
| let relative_error = error / one_ulp; | |
| Err(CheckFailure::InvalidReal { | |
| error_float: relative_error.to_f64(), | |
| error_str: relative_error.to_string().into(), | |
| incorrect_midpoint_rounding, | |
| }) | |
| } | |
| /// Remove trailing zeros in the significand and adjust the exponent | |
| #[cfg(test)] | |
| fn normalize(self) -> Self { | |
| use std::cmp::min; | |
| match self { | |
| Self::Real { sig, exp } => { | |
| // If there are trailing zeroes, remove them and increment the exponent instead | |
| let shift = min(sig.trailing_zeros(), exp.wrapping_neg().try_into().unwrap()); | |
| Self::Real { sig: sig >> shift, exp: exp + i32::try_from(shift).unwrap() } | |
| } | |
| _ => self, | |
| } | |
| } | |
| } | |
| /// Decompose a float into its integral components. This includes the implicit bit. | |
| /// | |
| /// If `allow_nan` is `false`, panic if `NaN` values are reached. | |
| fn decode<F: Float>(f: F) -> FloatRes<F> { | |
| let ione = F::SInt::ONE; | |
| let izero = F::SInt::ZERO; | |
| let mut exponent_biased = f.exponent(); | |
| let mut mantissa = f.mantissa().to_signed(); | |
| if exponent_biased == 0 { | |
| if mantissa == izero { | |
| return FloatRes::Zero; | |
| } | |
| exponent_biased += 1; | |
| } else if exponent_biased == F::EXP_SAT { | |
| if mantissa != izero { | |
| return FloatRes::Nan; | |
| } | |
| if f.is_sign_negative() { | |
| return FloatRes::NegInf; | |
| } | |
| return FloatRes::Inf; | |
| } else { | |
| // Set implicit bit | |
| mantissa |= ione << F::MAN_BITS; | |
| } | |
| let mut exponent = i32::try_from(exponent_biased).unwrap(); | |
| // Adjust for bias and the rnage of the mantissa | |
| exponent -= i32::try_from(F::EXP_BIAS + F::MAN_BITS).unwrap(); | |
| if f.is_sign_negative() { | |
| mantissa = mantissa.wrapping_neg(); | |
| } | |
| FloatRes::Real { sig: mantissa, exp: exponent } | |
| } | |
| /// A rational or its unrepresentable values. | |
| #[derive(Clone, Debug, PartialEq)] | |
| enum Rational { | |
| Inf, | |
| NegInf, | |
| Nan, | |
| Finite(BigRational), | |
| } | |
| impl Rational { | |
| /// Turn a string into a rational. `None` if `NaN`. | |
| fn parse(s: &str) -> Rational { | |
| let mut s = s; // lifetime rules | |
| if s.strip_prefix('+').unwrap_or(s).eq_ignore_ascii_case("nan") | |
| || s.eq_ignore_ascii_case("-nan") | |
| { | |
| return Rational::Nan; | |
| } | |
| if s.strip_prefix('+').unwrap_or(s).eq_ignore_ascii_case("inf") { | |
| return Rational::Inf; | |
| } | |
| if s.eq_ignore_ascii_case("-inf") { | |
| return Rational::NegInf; | |
| } | |
| // Fast path; no decimals or exponents ot parse | |
| if s.bytes().all(|b| b.is_ascii_digit() || b == b'-') { | |
| return Rational::Finite(BigRational::from_str(s).unwrap()); | |
| } | |
| let mut ten_exp: i32 = 0; | |
| // Remove and handle e.g. `e-4`, `e+10`, `e5` suffixes | |
| if let Some(pos) = s.bytes().position(|b| b == b'e' || b == b'E') { | |
| let (dec, exp) = s.split_at(pos); | |
| s = dec; | |
| ten_exp = exp[1..].parse().unwrap(); | |
| } | |
| // Remove the decimal and instead change our exponent | |
| // E.g. "12.3456" becomes "123456 * 10^-4" | |
| let mut s_owned; | |
| if let Some(pos) = s.bytes().position(|b| b == b'.') { | |
| ten_exp = ten_exp.checked_sub((s.len() - pos - 1).try_into().unwrap()).unwrap(); | |
| s_owned = s.to_owned(); | |
| s_owned.remove(pos); | |
| s = &s_owned; | |
| } | |
| // let pow = BigRational::from_u32(10).unwrap().pow(ten_exp); | |
| let pow = | |
| POWERS_OF_TEN.get(&ten_exp).unwrap_or_else(|| panic!("missing power of ten {ten_exp}")); | |
| let r = pow | |
| * BigInt::from_str(s) | |
| .unwrap_or_else(|e| panic!("`BigInt::from_str(\"{s}\")` failed with {e}")); | |
| Rational::Finite(r) | |
| } | |
| #[cfg(test)] | |
| fn expect_finite(self) -> BigRational { | |
| let Self::Finite(r) = self else { | |
| panic!("got non rational: {self:?}"); | |
| }; | |
| r | |
| } | |
| } | |
| #[cfg(test)] | |
| mod tests; |