From b46eb8a07cab13f90d68eefb1996361024ee7857 Mon Sep 17 00:00:00 2001 From: quaternic <57393910+quaternic@users.noreply.github.com> Date: Mon, 3 Mar 2025 02:00:26 +0200 Subject: [PATCH 01/10] implement rounding for the hex float parsing, and prepare to improve error handling --- crates/libm-test/src/f8_impl.rs | 3 +- src/math/support/env.rs | 20 +- src/math/support/hex_float.rs | 447 +++++++++++++++++++++++++------- 3 files changed, 367 insertions(+), 103 deletions(-) diff --git a/crates/libm-test/src/f8_impl.rs b/crates/libm-test/src/f8_impl.rs index 0683d8392..8009b0fbe 100644 --- a/crates/libm-test/src/f8_impl.rs +++ b/crates/libm-test/src/f8_impl.rs @@ -499,5 +499,6 @@ impl fmt::LowerHex for f8 { } pub const fn hf8(s: &str) -> f8 { - f8(parse_any(s, 8, 3) as u8) + let (bits, libm::support::Status::OK) = parse_any(s, 8, 3, libm::support::Round::Nearest) else { panic!() }; + f8(bits as u8) } diff --git a/src/math/support/env.rs b/src/math/support/env.rs index c05890d98..cd1040d77 100644 --- a/src/math/support/env.rs +++ b/src/math/support/env.rs @@ -90,24 +90,34 @@ impl Status { /// True if `UNDERFLOW` is set. #[cfg_attr(not(feature = "unstable-public-internals"), allow(dead_code))] - pub fn underflow(self) -> bool { + pub const fn underflow(self) -> bool { self.0 & Self::UNDERFLOW.0 != 0 } - pub fn set_underflow(&mut self, val: bool) { + /// True if `OVERFLOW` is set. + #[cfg_attr(not(feature = "unstable-public-internals"), allow(dead_code))] + pub const fn overflow(self) -> bool { + self.0 & Self::OVERFLOW.0 != 0 + } + + pub const fn set_underflow(&mut self, val: bool) { self.set_flag(val, Self::UNDERFLOW); } + pub const fn set_overflow(&mut self, val: bool) { + self.set_flag(val, Self::OVERFLOW); + } + /// True if `INEXACT` is set. - pub fn inexact(self) -> bool { + pub const fn inexact(self) -> bool { self.0 & Self::INEXACT.0 != 0 } - pub fn set_inexact(&mut self, val: bool) { + pub const fn set_inexact(&mut self, val: bool) { self.set_flag(val, Self::INEXACT); } - fn set_flag(&mut self, val: bool, mask: Self) { + const fn set_flag(&mut self, val: bool, mask: Self) { if val { self.0 |= mask.0; } else { diff --git a/src/math/support/hex_float.rs b/src/math/support/hex_float.rs index be7d7607f..585fed9aa 100644 --- a/src/math/support/hex_float.rs +++ b/src/math/support/hex_float.rs @@ -2,149 +2,240 @@ use core::fmt; -use super::{Float, f32_from_bits, f64_from_bits}; +use super::{f32_from_bits, f64_from_bits, Float, Round, Status}; /// Construct a 16-bit float from hex float representation (C-style) #[cfg(f16_enabled)] pub const fn hf16(s: &str) -> f16 { - f16::from_bits(parse_any(s, 16, 10) as u16) + match parse_any(s, 16, 10, Round::Nearest) { + (bits, Status::OK) => f16::from_bits(bits as u16), + (_, status) => status_panic(status), + } } /// Construct a 32-bit float from hex float representation (C-style) #[allow(unused)] pub const fn hf32(s: &str) -> f32 { - f32_from_bits(parse_any(s, 32, 23) as u32) + match parse_any(s, 32, 23, Round::Nearest) { + (bits, Status::OK) => f32_from_bits(bits as u32), + (_, status) => status_panic(status), + } } /// Construct a 64-bit float from hex float representation (C-style) pub const fn hf64(s: &str) -> f64 { - f64_from_bits(parse_any(s, 64, 52) as u64) + match parse_any(s, 64, 52, Round::Nearest) { + (bits, Status::OK) => f64_from_bits(bits as u64), + (_, status) => status_panic(status), + } } /// Construct a 128-bit float from hex float representation (C-style) #[cfg(f128_enabled)] pub const fn hf128(s: &str) -> f128 { - f128::from_bits(parse_any(s, 128, 112)) + match parse_any(s, 128, 112, Round::Nearest) { + (bits, Status::OK) => f128::from_bits(bits), + (_, status) => status_panic(status), + } +} + +const fn status_panic(status: Status) -> ! { + if status.underflow() { + assert!(status.inexact()); + panic!("the value is too tiny") + } + if status.overflow() { + assert!(status.inexact()); + panic!("the value is too huge") + } + if status.inexact() { panic!("the value is too precise") } + panic!("unknown issue") + } /// Parse any float from hex to its bitwise representation. -/// -/// `nan_repr` is passed rather than constructed so the platform-specific NaN is returned. -pub const fn parse_any(s: &str, bits: u32, sig_bits: u32) -> u128 { +pub const fn parse_any(s: &str, bits: u32, sig_bits: u32, round: Round) -> (u128, Status) { + let mut b = s.as_bytes(); + + assert!(sig_bits <= 119); + assert!(bits <= 128); + assert!(bits >= sig_bits + 3); + assert!(bits <= sig_bits + 30); + + let mut neg = false; + if let &[c @ (b'-' | b'+'), ref rest @ ..] = b { + b = rest; + neg = c == b'-'; + } + + let sign_bit = 1 << (bits - 1); + let quiet_bit = 1 << (sig_bits - 1); + let nan = sign_bit - quiet_bit; + let inf = nan - quiet_bit; + + let (mut x, status) = match b { + &[b'i' | b'I', b'n' | b'N', b'f' | b'F'] => (inf, Status::OK), + &[b'n' | b'N', b'a' | b'A', b'n' | b'N'] => (nan, Status::OK), + &[b'0', b'x' | b'X', ref rest @ ..] => { + let round = if neg && matches!(round, Round::Positive) { + Round::Negative + } else if neg && matches!(round, Round::Negative) { + Round::Positive + } else { + round + }; + parse_finite(rest, bits, sig_bits, round) + }, + _ => panic!("no hex indicator"), + }; + + if neg { + x ^= sign_bit; + } + + (x, status) +} + +const fn parse_finite(b: &[u8], bits: u32, sig_bits: u32, rounding_mode: Round) -> (u128, Status) { let exp_bits: u32 = bits - sig_bits - 1; let max_msb: i32 = (1 << (exp_bits - 1)) - 1; // The exponent of one ULP in the subnormals let min_lsb: i32 = 1 - max_msb - sig_bits as i32; - let exp_mask = ((1 << exp_bits) - 1) << sig_bits; + let (mut sig, mut exp) = match parse_hex(b) { + Err(e) => panic!("{}", e.0), + Ok(Parsed { sig: 0, .. }) => return (0, Status::OK), + Ok(Parsed { sig, exp }) => (sig, exp), + }; + + let mut round_bits = u128_ilog2(sig) as i32 - sig_bits as i32; + + // Round at least up to min_lsb + if exp < min_lsb - round_bits { + round_bits = min_lsb - exp; + } - let (neg, mut sig, exp) = match parse_hex(s.as_bytes()) { - Parsed::Finite { neg, sig: 0, .. } => return (neg as u128) << (bits - 1), - Parsed::Finite { neg, sig, exp } => (neg, sig, exp), - Parsed::Infinite { neg } => return ((neg as u128) << (bits - 1)) | exp_mask, - Parsed::Nan { neg } => { - return ((neg as u128) << (bits - 1)) | exp_mask | (1 << (sig_bits - 1)); + let mut status = Status::OK; + + exp += round_bits; + + if round_bits > 0 { + // first, prepare for rounding exactly two bits + if round_bits == 1 { + sig <<= 1; + } else if round_bits > 2 { + sig = shr_odd_rounding(sig, (round_bits - 2) as u32); } - }; - // exponents of the least and most significant bits in the value - let lsb = sig.trailing_zeros() as i32; - let msb = u128_ilog2(sig) as i32; - let sig_bits = sig_bits as i32; + if sig & 0b11 != 0 { + status = Status::INEXACT; + } - assert!(msb - lsb <= sig_bits, "the value is too precise"); - assert!(msb + exp <= max_msb, "the value is too huge"); - assert!(lsb + exp >= min_lsb, "the value is too tiny"); + sig = shr2_round(sig, rounding_mode); + } else if round_bits < 0 { + sig <<= -round_bits; + } // The parsed value is X = sig * 2^exp // Expressed as a multiple U of the smallest subnormal value: // X = U * 2^min_lsb, so U = sig * 2^(exp-min_lsb) - let mut uexp = exp - min_lsb; + let uexp = (exp - min_lsb) as u128; + let uexp = uexp << sig_bits; - let shift = if uexp + msb >= sig_bits { - // normal, shift msb to position sig_bits - sig_bits - msb - } else { - // subnormal, shift so that uexp becomes 0 - uexp - }; + debug_assert!(sig <= 2 << sig_bits); - if shift >= 0 { - sig <<= shift; - } else { - sig >>= -shift; + let inf = ((1 << exp_bits) - 1) << sig_bits; + + match sig.checked_add(uexp) { + Some(bits) if bits < inf => { + if status.inexact() { + if bits < (1 << sig_bits) { + status.set_underflow(true); + } + } + (bits, status) + } + _ => { + // overflow to infinity + status.set_inexact(true); + status.set_overflow(true); + match rounding_mode { + Round::Positive + | Round::Nearest => (inf, status), + Round::Negative + | Round::Zero => (inf - 1, status), + } + } } - uexp -= shift; +} - // the most significant bit is like having 1 in the exponent bits - // add any leftover exponent to that - assert!(uexp >= 0 && uexp < (1 << exp_bits) - 2); - sig += (uexp as u128) << sig_bits; +/// Shift right, rounding all inexact divisions to the nearest odd number +/// E.g. (0 >> 4) -> 0, (1..=31 >> 4) -> 1, (32 >> 4) -> 2, ... +/// +/// Useful for reducing a number before rounding the last two bits, since +/// the result of the final rounding is preserved for all rounding modes. +const fn shr_odd_rounding(x: u128, k: u32) -> u128 { + if k < 128 { + let inexact = x.trailing_zeros() < k; + (x >> k) | (inexact as u128) + } else { + (x != 0) as u128 + } +} - // finally, set the sign bit if necessary - sig | ((neg as u128) << (bits - 1)) +/// Divide by 4, rounding accor +const fn shr2_round(x: u128, round: Round) -> u128 { + let d = x % 8; + (x / 4) + match round { + Round::Nearest => (1 & (0b11001000_u8 >> d)) as u128, + Round::Negative | Round::Zero => 0, + Round::Positive => ((x % 4) != 0) as u128, + } } -/// A parsed floating point number. -enum Parsed { - /// Absolute value sig * 2^e - Finite { - neg: bool, - sig: u128, - exp: i32, - }, - Infinite { - neg: bool, - }, - Nan { - neg: bool, - }, +/// A parsed finite and unsigned floating point number. +struct Parsed { + /// Absolute value sig * 2^exp + sig: u128, + exp: i32, } + +struct HexFloatParseError(&'static str); + /// Parse a hexadecimal float x -const fn parse_hex(mut b: &[u8]) -> Parsed { - let mut neg = false; +const fn parse_hex(mut b: &[u8]) -> Result { let mut sig: u128 = 0; let mut exp: i32 = 0; - if let &[c @ (b'-' | b'+'), ref rest @ ..] = b { - b = rest; - neg = c == b'-'; - } - - match *b { - [b'i' | b'I', b'n' | b'N', b'f' | b'F'] => return Parsed::Infinite { neg }, - [b'n' | b'N', b'a' | b'A', b'n' | b'N'] => return Parsed::Nan { neg }, - _ => (), - } - - if let &[b'0', b'x' | b'X', ref rest @ ..] = b { - b = rest; - } else { - panic!("no hex indicator"); - } - let mut seen_point = false; let mut some_digits = false; + let mut inexact = false; while let &[c, ref rest @ ..] = b { b = rest; match c { b'.' => { - assert!(!seen_point); + if seen_point { return Err(HexFloatParseError("unexpected '.' parsing fractional digits")) } seen_point = true; continue; } b'p' | b'P' => break, c => { - let digit = hex_digit(c); + let Some(digit) = hex_digit(c) else { + return Err(HexFloatParseError("expected hexadecimal digit")); + }; some_digits = true; - let of; - (sig, of) = sig.overflowing_mul(16); - assert!(!of, "too many digits"); - sig |= digit as u128; - // up until the fractional point, the value grows + + if (sig >> 124) == 0 { + sig <<= 4; + sig |= digit as u128; + } else { + exp += 4; + inexact |= digit != 0; + } + // Up until the fractional point, the value grows // with more digits, but after it the exponent is // compensated to match. if seen_point { @@ -153,7 +244,16 @@ const fn parse_hex(mut b: &[u8]) -> Parsed { } } } - assert!(some_digits, "at least one digit is required"); + // If we've set inexact, the exact value has more than 125 + // significant bits, and lies somewhere between sig and sig + 1. + // Because we'll round off at least two of the trailing bits, + // setting the last bit gives correct rounding for inexact values. + sig |= inexact as u128; + + if !some_digits { + return Err(HexFloatParseError("at least one digit is required")); + }; + some_digits = false; let mut negate_exp = false; @@ -165,14 +265,21 @@ const fn parse_hex(mut b: &[u8]) -> Parsed { let mut pexp: i32 = 0; while let &[c, ref rest @ ..] = b { b = rest; - let digit = dec_digit(c); + let Some(digit) = dec_digit(c) else { + return Err(HexFloatParseError("expected decimal digit")); + }; some_digits = true; let of; (pexp, of) = pexp.overflowing_mul(10); - assert!(!of, "too many exponent digits"); + if of { + return Err(HexFloatParseError("too many exponent digits")); + } pexp += digit as i32; } - assert!(some_digits, "at least one exponent digit is required"); + + if !some_digits { + return Err(HexFloatParseError("at least one exponent digit is required")); + }; if negate_exp { exp -= pexp; @@ -180,22 +287,22 @@ const fn parse_hex(mut b: &[u8]) -> Parsed { exp += pexp; } - Parsed::Finite { neg, sig, exp } + Ok(Parsed { sig, exp }) } -const fn dec_digit(c: u8) -> u8 { +const fn dec_digit(c: u8) -> Option { match c { - b'0'..=b'9' => c - b'0', - _ => panic!("bad char"), + b'0'..=b'9' => Some(c - b'0'), + _ => None, } } -const fn hex_digit(c: u8) -> u8 { +const fn hex_digit(c: u8) -> Option { match c { - b'0'..=b'9' => c - b'0', - b'a'..=b'f' => c - b'a' + 10, - b'A'..=b'F' => c - b'A' + 10, - _ => panic!("bad char"), + b'0'..=b'9' => Some(c - b'0'), + b'a'..=b'f' => Some(c - b'a' + 10), + b'A'..=b'F' => Some(c - b'A' + 10), + _ => None, } } @@ -341,6 +448,58 @@ mod parse_tests { use super::*; + fn rounding_properties(s: &str) { + let (xd, s0) = parse_any(s, 16, 10, Round::Negative); + let (xu, s1) = parse_any(s, 16, 10, Round::Positive); + let (xz, s2) = parse_any(s, 16, 10, Round::Zero); + let (xn, s3) = parse_any(s, 16, 10, Round::Nearest); + + // FIXME: A value between the least normal and largest subnormal + // could have underflow status depend on rounding mode. + + if let Status::OK = s0 { + // an exact result is the same for all rounding modes + assert_eq!(s0, s1); + assert_eq!(s0, s2); + assert_eq!(s0, s3); + + assert_eq!(xd, xu); + assert_eq!(xd, xz); + assert_eq!(xd, xn); + } else { + assert!([s0,s1,s2,s3].into_iter().all(Status::inexact)); + + let xd = f16::from_bits(xd as u16); + let xu = f16::from_bits(xu as u16); + let xz = f16::from_bits(xz as u16); + let xn = f16::from_bits(xn as u16); + + assert_biteq!(xd.next_up(), xu, "s={s}, xd={xd:?}, xu={xu:?}"); + + let signs = [xd,xu,xz,xn].map(f16::is_sign_negative); + + if signs == [true; 4] { + assert_biteq!(xz, xu); + } else { + assert_eq!(signs, [false; 4]); + assert_biteq!(xz, xd); + } + + if xn.to_bits() != xd.to_bits() { + assert_biteq!(xn, xu); + } + } + } + #[test] + fn test_rounding() { + let n = 1_i32 << 14; + for i in -n..n { + let u = i.rotate_right(11) as u32; + let s = format!("{}",Hexf(f32::from_bits(u))); + rounding_properties(&s); + } + } + #[test] fn test_parse_any() { for k in -149..=127 { @@ -396,7 +555,53 @@ mod parse_tests { } } } - + #[test] + fn rounding() { + let pi = std::f128::consts::PI; + let s = format!("{}", Hexf(pi)); + + for k in 0..=111 { + let (bits, status) = parse_any(&s, 128-k, 112-k, Round::Nearest); + let scale = (1u128 << (112-k-1)) as f128; + let expected = (pi * scale).round_ties_even() / scale; + assert_eq!( + f128::from_bits(bits << k), + expected, + ); + assert_eq!( + expected != pi, + status.inexact(), + ); + } + } + #[test] + fn rounding_extreme_underflow() { + for k in 1..1000 { + let s = format!("0x1p{}", -149 - k); + let (bits, status) = parse_any(&s, 32, 23, Round::Nearest); + assert_eq!(bits, 0, "{s} should round to zero, got bits={bits}"); + assert!(status.underflow(), "should indicate underflow when parsing {s}"); + assert!(status.inexact(), "should indicate inexact when parsing {s}"); + } + } + #[test] + fn long_tail() { + for k in 1..1000 { + let s = format!("0x1.{}p0", "0".repeat(k)); + let (bits, Status::OK) = parse_any(&s, 32, 23, Round::Nearest) else { + panic!("parsing {s} failed") + }; + assert_eq!(f32::from_bits(bits as u32), 1.0); + + let s = format!("0x1.{}1p0", "0".repeat(k)); + let (bits, status) = parse_any(&s, 32, 23, Round::Nearest); + if status.inexact() { + assert!(1.0 == f32::from_bits(bits as u32)); + } else { + assert!(1.0 < f32::from_bits(bits as u32)); + } + } + } // HACK(msrv): 1.63 rejects unknown width float literals at an AST level, so use a macro to // hide them from the AST. #[cfg(f16_enabled)] @@ -434,6 +639,7 @@ mod parse_tests { ]; for (s, exp) in checks { println!("parsing {s}"); + rounding_properties(s); let act = hf16(s).to_bits(); assert_eq!( act, exp, @@ -749,7 +955,13 @@ mod tests_panicking { #[test] #[should_panic(expected = "the value is too precise")] fn test_f128_extra_precision() { - // One bit more than the above. + // Just below the maximum finite. + hf128("0x1.fffffffffffffffffffffffffffe8p+16383"); + } + #[test] + #[should_panic(expected = "the value is too huge")] + fn test_f128_extra_precision_overflow() { + // One bit more than the above. Should overflow. hf128("0x1.ffffffffffffffffffffffffffff8p+16383"); } @@ -822,6 +1034,47 @@ mod print_tests { } } + #[test] + #[cfg(f16_enabled)] + fn test_f16_to_f32() { + use std::format; + // Exhaustively check that these are equivalent for all `f16`: + // - `f16 -> f32` + // - `f16 -> str -> f32` + // - `f16 -> f32 -> str -> f32` + // - `f16 -> f32 -> str -> f16 -> f32` + for x in 0..=u16::MAX { + let f16 = f16::from_bits(x); + let s16 = format!("{}", Hexf(f16)); + let f32 = f16 as f32; + let s32 = format!("{}", Hexf(f32)); + + + let a = hf32(&s16); + let b = hf32(&s32); + let c = hf16(&s32); + + if f32.is_nan() && a.is_nan() && b.is_nan() && c.is_nan() { + continue; + } + + assert_eq!( + f32.to_bits(), + (a as f32).to_bits(), + "{f16:?} : f16 formatted as {s16} which parsed as {a:?} : f16" + ); + assert_eq!( + f32.to_bits(), + b.to_bits(), + "{f32:?} : f32 formatted as {s32} which parsed as {b:?} : f32" + ); + assert_eq!( + f32.to_bits(), + (c as f32).to_bits(), + "{f32:?} : f32 formatted as {s32} which parsed as {c:?} : f16" + ); + } + } #[test] fn spot_checks() { assert_eq!(Hexf(f32::MAX).to_string(), "0x1.fffffep+127"); From bba23d47daf68c428709a5d7af00574e3a34b131 Mon Sep 17 00:00:00 2001 From: quaternic <57393910+quaternic@users.noreply.github.com> Date: Mon, 3 Mar 2025 02:03:41 +0200 Subject: [PATCH 02/10] cargo fmt --- crates/libm-test/src/f8_impl.rs | 5 +- crates/libm-test/src/gen/case_list.rs | 80 ++++++++++++--------------- src/math/support/big/tests.rs | 9 ++- src/math/support/hex_float.rs | 60 +++++++++----------- 4 files changed, 69 insertions(+), 85 deletions(-) diff --git a/crates/libm-test/src/f8_impl.rs b/crates/libm-test/src/f8_impl.rs index 8009b0fbe..047d81569 100644 --- a/crates/libm-test/src/f8_impl.rs +++ b/crates/libm-test/src/f8_impl.rs @@ -499,6 +499,9 @@ impl fmt::LowerHex for f8 { } pub const fn hf8(s: &str) -> f8 { - let (bits, libm::support::Status::OK) = parse_any(s, 8, 3, libm::support::Round::Nearest) else { panic!() }; + let (bits, libm::support::Status::OK) = parse_any(s, 8, 3, libm::support::Round::Nearest) + else { + panic!() + }; f8(bits as u8) } diff --git a/crates/libm-test/src/gen/case_list.rs b/crates/libm-test/src/gen/case_list.rs index 7cb9897d8..9b81633ed 100644 --- a/crates/libm-test/src/gen/case_list.rs +++ b/crates/libm-test/src/gen/case_list.rs @@ -251,13 +251,10 @@ fn floorf16_cases() -> Vec> { fn fma_cases() -> Vec> { let mut v = vec![]; - TestCase::append_pairs( - &mut v, - &[ - // Previous failure with incorrect sign - ((5e-324, -5e-324, 0.0), Some(-0.0)), - ], - ); + TestCase::append_pairs(&mut v, &[ + // Previous failure with incorrect sign + ((5e-324, -5e-324, 0.0), Some(-0.0)), + ]); v } @@ -268,29 +265,26 @@ fn fmaf_cases() -> Vec> { #[cfg(f128_enabled)] fn fmaf128_cases() -> Vec> { let mut v = vec![]; - TestCase::append_pairs( - &mut v, - &[ + TestCase::append_pairs(&mut v, &[ + ( + // Tricky rounding case that previously failed in extensive tests ( - // Tricky rounding case that previously failed in extensive tests - ( - hf128!("-0x1.1966cc01966cc01966cc01966f06p-25"), - hf128!("-0x1.669933fe69933fe69933fe6997c9p-16358"), - hf128!("-0x0.000000000000000000000000048ap-16382"), - ), - Some(hf128!("0x0.c5171470a3ff5e0f68d751491b18p-16382")), + hf128!("-0x1.1966cc01966cc01966cc01966f06p-25"), + hf128!("-0x1.669933fe69933fe69933fe6997c9p-16358"), + hf128!("-0x0.000000000000000000000000048ap-16382"), ), + Some(hf128!("0x0.c5171470a3ff5e0f68d751491b18p-16382")), + ), + ( + // Subnormal edge case that caused a failure ( - // Subnormal edge case that caused a failure - ( - hf128!("0x0.7ffffffffffffffffffffffffff7p-16382"), - hf128!("0x1.ffffffffffffffffffffffffffffp-1"), - hf128!("0x0.8000000000000000000000000009p-16382"), - ), - Some(hf128!("0x1.0000000000000000000000000000p-16382")), + hf128!("0x0.7ffffffffffffffffffffffffff7p-16382"), + hf128!("0x1.ffffffffffffffffffffffffffffp-1"), + hf128!("0x0.8000000000000000000000000009p-16382"), ), - ], - ); + Some(hf128!("0x1.0000000000000000000000000000p-16382")), + ), + ]); v } @@ -576,16 +570,13 @@ fn remquof_cases() -> Vec> { fn rint_cases() -> Vec> { let mut v = vec![]; - TestCase::append_pairs( - &mut v, - &[ - // Known failure on i586 - #[cfg(not(x86_no_sse))] - ((hf64!("-0x1.e3f13ff995ffcp+38"),), Some(hf64!("-0x1.e3f13ff994000p+38"))), - #[cfg(x86_no_sse)] - ((hf64!("-0x1.e3f13ff995ffcp+38"),), Some(hf64!("-0x1.e3f13ff998000p+38"))), - ], - ); + TestCase::append_pairs(&mut v, &[ + // Known failure on i586 + #[cfg(not(x86_no_sse))] + ((hf64!("-0x1.e3f13ff995ffcp+38"),), Some(hf64!("-0x1.e3f13ff994000p+38"))), + #[cfg(x86_no_sse)] + ((hf64!("-0x1.e3f13ff995ffcp+38"),), Some(hf64!("-0x1.e3f13ff998000p+38"))), + ]); v } @@ -628,16 +619,13 @@ fn roundevenf16_cases() -> Vec> { fn roundeven_cases() -> Vec> { let mut v = vec![]; - TestCase::append_pairs( - &mut v, - &[ - // Known failure on i586 - #[cfg(not(x86_no_sse))] - ((hf64!("-0x1.e3f13ff995ffcp+38"),), Some(hf64!("-0x1.e3f13ff994000p+38"))), - #[cfg(x86_no_sse)] - ((hf64!("-0x1.e3f13ff995ffcp+38"),), Some(hf64!("-0x1.e3f13ff998000p+38"))), - ], - ); + TestCase::append_pairs(&mut v, &[ + // Known failure on i586 + #[cfg(not(x86_no_sse))] + ((hf64!("-0x1.e3f13ff995ffcp+38"),), Some(hf64!("-0x1.e3f13ff994000p+38"))), + #[cfg(x86_no_sse)] + ((hf64!("-0x1.e3f13ff995ffcp+38"),), Some(hf64!("-0x1.e3f13ff998000p+38"))), + ]); v } diff --git a/src/math/support/big/tests.rs b/src/math/support/big/tests.rs index 2c71191ba..c464e448f 100644 --- a/src/math/support/big/tests.rs +++ b/src/math/support/big/tests.rs @@ -112,11 +112,10 @@ fn shr_u256() { (u256::MAX, 193, u256 { lo: u64::MAX as u128 >> 1, hi: 0 }), (u256::MAX, 254, u256 { lo: 0b11, hi: 0 }), (u256::MAX, 255, u256 { lo: 1, hi: 0 }), - ( - u256 { hi: LOHI_SPLIT, lo: 0 }, - 64, - u256 { lo: 0xffffffffffffffff0000000000000000, hi: 0xaaaaaaaaaaaaaaaa }, - ), + (u256 { hi: LOHI_SPLIT, lo: 0 }, 64, u256 { + lo: 0xffffffffffffffff0000000000000000, + hi: 0xaaaaaaaaaaaaaaaa, + }), ]; for (input, shift, expected) in check { diff --git a/src/math/support/hex_float.rs b/src/math/support/hex_float.rs index 585fed9aa..24f688a1c 100644 --- a/src/math/support/hex_float.rs +++ b/src/math/support/hex_float.rs @@ -2,7 +2,7 @@ use core::fmt; -use super::{f32_from_bits, f64_from_bits, Float, Round, Status}; +use super::{Float, Round, Status, f32_from_bits, f64_from_bits}; /// Construct a 16-bit float from hex float representation (C-style) #[cfg(f16_enabled)] @@ -48,9 +48,10 @@ const fn status_panic(status: Status) -> ! { assert!(status.inexact()); panic!("the value is too huge") } - if status.inexact() { panic!("the value is too precise") } + if status.inexact() { + panic!("the value is too precise") + } panic!("unknown issue") - } /// Parse any float from hex to its bitwise representation. @@ -85,7 +86,7 @@ pub const fn parse_any(s: &str, bits: u32, sig_bits: u32, round: Round) -> (u128 round }; parse_finite(rest, bits, sig_bits, round) - }, + } _ => panic!("no hex indicator"), }; @@ -160,10 +161,8 @@ const fn parse_finite(b: &[u8], bits: u32, sig_bits: u32, rounding_mode: Round) status.set_inexact(true); status.set_overflow(true); match rounding_mode { - Round::Positive - | Round::Nearest => (inf, status), - Round::Negative - | Round::Zero => (inf - 1, status), + Round::Positive | Round::Nearest => (inf, status), + Round::Negative | Round::Zero => (inf - 1, status), } } } @@ -171,7 +170,7 @@ const fn parse_finite(b: &[u8], bits: u32, sig_bits: u32, rounding_mode: Round) /// Shift right, rounding all inexact divisions to the nearest odd number /// E.g. (0 >> 4) -> 0, (1..=31 >> 4) -> 1, (32 >> 4) -> 2, ... -/// +/// /// Useful for reducing a number before rounding the last two bits, since /// the result of the final rounding is preserved for all rounding modes. const fn shr_odd_rounding(x: u128, k: u32) -> u128 { @@ -186,11 +185,12 @@ const fn shr_odd_rounding(x: u128, k: u32) -> u128 { /// Divide by 4, rounding accor const fn shr2_round(x: u128, round: Round) -> u128 { let d = x % 8; - (x / 4) + match round { - Round::Nearest => (1 & (0b11001000_u8 >> d)) as u128, - Round::Negative | Round::Zero => 0, - Round::Positive => ((x % 4) != 0) as u128, - } + (x / 4) + + match round { + Round::Nearest => (1 & (0b11001000_u8 >> d)) as u128, + Round::Negative | Round::Zero => 0, + Round::Positive => ((x % 4) != 0) as u128, + } } /// A parsed finite and unsigned floating point number. @@ -200,11 +200,10 @@ struct Parsed { exp: i32, } - struct HexFloatParseError(&'static str); /// Parse a hexadecimal float x -const fn parse_hex(mut b: &[u8]) -> Result { +const fn parse_hex(mut b: &[u8]) -> Result { let mut sig: u128 = 0; let mut exp: i32 = 0; @@ -217,14 +216,16 @@ const fn parse_hex(mut b: &[u8]) -> Result { match c { b'.' => { - if seen_point { return Err(HexFloatParseError("unexpected '.' parsing fractional digits")) } + if seen_point { + return Err(HexFloatParseError("unexpected '.' parsing fractional digits")); + } seen_point = true; continue; } b'p' | b'P' => break, c => { let Some(digit) = hex_digit(c) else { - return Err(HexFloatParseError("expected hexadecimal digit")); + return Err(HexFloatParseError("expected hexadecimal digit")); }; some_digits = true; @@ -266,7 +267,7 @@ const fn parse_hex(mut b: &[u8]) -> Result { while let &[c, ref rest @ ..] = b { b = rest; let Some(digit) = dec_digit(c) else { - return Err(HexFloatParseError("expected decimal digit")); + return Err(HexFloatParseError("expected decimal digit")); }; some_digits = true; let of; @@ -467,7 +468,7 @@ mod parse_tests { assert_eq!(xd, xz); assert_eq!(xd, xn); } else { - assert!([s0,s1,s2,s3].into_iter().all(Status::inexact)); + assert!([s0, s1, s2, s3].into_iter().all(Status::inexact)); let xd = f16::from_bits(xd as u16); let xu = f16::from_bits(xu as u16); @@ -476,7 +477,7 @@ mod parse_tests { assert_biteq!(xd.next_up(), xu, "s={s}, xd={xd:?}, xu={xu:?}"); - let signs = [xd,xu,xz,xn].map(f16::is_sign_negative); + let signs = [xd, xu, xz, xn].map(f16::is_sign_negative); if signs == [true; 4] { assert_biteq!(xz, xu); @@ -495,7 +496,7 @@ mod parse_tests { let n = 1_i32 << 14; for i in -n..n { let u = i.rotate_right(11) as u32; - let s = format!("{}",Hexf(f32::from_bits(u))); + let s = format!("{}", Hexf(f32::from_bits(u))); rounding_properties(&s); } } @@ -561,17 +562,11 @@ mod parse_tests { let s = format!("{}", Hexf(pi)); for k in 0..=111 { - let (bits, status) = parse_any(&s, 128-k, 112-k, Round::Nearest); - let scale = (1u128 << (112-k-1)) as f128; + let (bits, status) = parse_any(&s, 128 - k, 112 - k, Round::Nearest); + let scale = (1u128 << (112 - k - 1)) as f128; let expected = (pi * scale).round_ties_even() / scale; - assert_eq!( - f128::from_bits(bits << k), - expected, - ); - assert_eq!( - expected != pi, - status.inexact(), - ); + assert_eq!(f128::from_bits(bits << k), expected,); + assert_eq!(expected != pi, status.inexact(),); } } #[test] @@ -1049,7 +1044,6 @@ mod print_tests { let f32 = f16 as f32; let s32 = format!("{}", Hexf(f32)); - let a = hf32(&s16); let b = hf32(&s32); let c = hf16(&s32); From 603c8401044786b1a733a75912611050688d1d92 Mon Sep 17 00:00:00 2001 From: quaternic <57393910+quaternic@users.noreply.github.com> Date: Tue, 4 Mar 2025 19:11:21 +0200 Subject: [PATCH 03/10] more parsing fixes --- crates/libm-test/src/f8_impl.rs | 7 +- crates/libm-test/src/gen/case_list.rs | 80 +++++---- src/math/support/big/tests.rs | 9 +- src/math/support/hex_float.rs | 226 +++++++++++++++----------- src/math/support/mod.rs | 1 - 5 files changed, 181 insertions(+), 142 deletions(-) diff --git a/crates/libm-test/src/f8_impl.rs b/crates/libm-test/src/f8_impl.rs index 047d81569..6772e092c 100644 --- a/crates/libm-test/src/f8_impl.rs +++ b/crates/libm-test/src/f8_impl.rs @@ -3,8 +3,6 @@ use std::cmp::{self, Ordering}; use std::{fmt, ops}; -use libm::support::hex_float::parse_any; - use crate::Float; /// Sometimes verifying float logic is easiest when all values can quickly be checked exhaustively @@ -499,9 +497,6 @@ impl fmt::LowerHex for f8 { } pub const fn hf8(s: &str) -> f8 { - let (bits, libm::support::Status::OK) = parse_any(s, 8, 3, libm::support::Round::Nearest) - else { - panic!() - }; + let Ok(bits) = libm::support::hex_float::parse_hex_exact(s, 8, 3) else { panic!() }; f8(bits as u8) } diff --git a/crates/libm-test/src/gen/case_list.rs b/crates/libm-test/src/gen/case_list.rs index 9b81633ed..7cb9897d8 100644 --- a/crates/libm-test/src/gen/case_list.rs +++ b/crates/libm-test/src/gen/case_list.rs @@ -251,10 +251,13 @@ fn floorf16_cases() -> Vec> { fn fma_cases() -> Vec> { let mut v = vec![]; - TestCase::append_pairs(&mut v, &[ - // Previous failure with incorrect sign - ((5e-324, -5e-324, 0.0), Some(-0.0)), - ]); + TestCase::append_pairs( + &mut v, + &[ + // Previous failure with incorrect sign + ((5e-324, -5e-324, 0.0), Some(-0.0)), + ], + ); v } @@ -265,26 +268,29 @@ fn fmaf_cases() -> Vec> { #[cfg(f128_enabled)] fn fmaf128_cases() -> Vec> { let mut v = vec![]; - TestCase::append_pairs(&mut v, &[ - ( - // Tricky rounding case that previously failed in extensive tests + TestCase::append_pairs( + &mut v, + &[ ( - hf128!("-0x1.1966cc01966cc01966cc01966f06p-25"), - hf128!("-0x1.669933fe69933fe69933fe6997c9p-16358"), - hf128!("-0x0.000000000000000000000000048ap-16382"), + // Tricky rounding case that previously failed in extensive tests + ( + hf128!("-0x1.1966cc01966cc01966cc01966f06p-25"), + hf128!("-0x1.669933fe69933fe69933fe6997c9p-16358"), + hf128!("-0x0.000000000000000000000000048ap-16382"), + ), + Some(hf128!("0x0.c5171470a3ff5e0f68d751491b18p-16382")), ), - Some(hf128!("0x0.c5171470a3ff5e0f68d751491b18p-16382")), - ), - ( - // Subnormal edge case that caused a failure ( - hf128!("0x0.7ffffffffffffffffffffffffff7p-16382"), - hf128!("0x1.ffffffffffffffffffffffffffffp-1"), - hf128!("0x0.8000000000000000000000000009p-16382"), + // Subnormal edge case that caused a failure + ( + hf128!("0x0.7ffffffffffffffffffffffffff7p-16382"), + hf128!("0x1.ffffffffffffffffffffffffffffp-1"), + hf128!("0x0.8000000000000000000000000009p-16382"), + ), + Some(hf128!("0x1.0000000000000000000000000000p-16382")), ), - Some(hf128!("0x1.0000000000000000000000000000p-16382")), - ), - ]); + ], + ); v } @@ -570,13 +576,16 @@ fn remquof_cases() -> Vec> { fn rint_cases() -> Vec> { let mut v = vec![]; - TestCase::append_pairs(&mut v, &[ - // Known failure on i586 - #[cfg(not(x86_no_sse))] - ((hf64!("-0x1.e3f13ff995ffcp+38"),), Some(hf64!("-0x1.e3f13ff994000p+38"))), - #[cfg(x86_no_sse)] - ((hf64!("-0x1.e3f13ff995ffcp+38"),), Some(hf64!("-0x1.e3f13ff998000p+38"))), - ]); + TestCase::append_pairs( + &mut v, + &[ + // Known failure on i586 + #[cfg(not(x86_no_sse))] + ((hf64!("-0x1.e3f13ff995ffcp+38"),), Some(hf64!("-0x1.e3f13ff994000p+38"))), + #[cfg(x86_no_sse)] + ((hf64!("-0x1.e3f13ff995ffcp+38"),), Some(hf64!("-0x1.e3f13ff998000p+38"))), + ], + ); v } @@ -619,13 +628,16 @@ fn roundevenf16_cases() -> Vec> { fn roundeven_cases() -> Vec> { let mut v = vec![]; - TestCase::append_pairs(&mut v, &[ - // Known failure on i586 - #[cfg(not(x86_no_sse))] - ((hf64!("-0x1.e3f13ff995ffcp+38"),), Some(hf64!("-0x1.e3f13ff994000p+38"))), - #[cfg(x86_no_sse)] - ((hf64!("-0x1.e3f13ff995ffcp+38"),), Some(hf64!("-0x1.e3f13ff998000p+38"))), - ]); + TestCase::append_pairs( + &mut v, + &[ + // Known failure on i586 + #[cfg(not(x86_no_sse))] + ((hf64!("-0x1.e3f13ff995ffcp+38"),), Some(hf64!("-0x1.e3f13ff994000p+38"))), + #[cfg(x86_no_sse)] + ((hf64!("-0x1.e3f13ff995ffcp+38"),), Some(hf64!("-0x1.e3f13ff998000p+38"))), + ], + ); v } diff --git a/src/math/support/big/tests.rs b/src/math/support/big/tests.rs index c464e448f..2c71191ba 100644 --- a/src/math/support/big/tests.rs +++ b/src/math/support/big/tests.rs @@ -112,10 +112,11 @@ fn shr_u256() { (u256::MAX, 193, u256 { lo: u64::MAX as u128 >> 1, hi: 0 }), (u256::MAX, 254, u256 { lo: 0b11, hi: 0 }), (u256::MAX, 255, u256 { lo: 1, hi: 0 }), - (u256 { hi: LOHI_SPLIT, lo: 0 }, 64, u256 { - lo: 0xffffffffffffffff0000000000000000, - hi: 0xaaaaaaaaaaaaaaaa, - }), + ( + u256 { hi: LOHI_SPLIT, lo: 0 }, + 64, + u256 { lo: 0xffffffffffffffff0000000000000000, hi: 0xaaaaaaaaaaaaaaaa }, + ), ]; for (input, shift, expected) in check { diff --git a/src/math/support/hex_float.rs b/src/math/support/hex_float.rs index 24f688a1c..737200384 100644 --- a/src/math/support/hex_float.rs +++ b/src/math/support/hex_float.rs @@ -2,71 +2,77 @@ use core::fmt; -use super::{Float, Round, Status, f32_from_bits, f64_from_bits}; +use super::{Float, Round, Status}; /// Construct a 16-bit float from hex float representation (C-style) #[cfg(f16_enabled)] pub const fn hf16(s: &str) -> f16 { - match parse_any(s, 16, 10, Round::Nearest) { - (bits, Status::OK) => f16::from_bits(bits as u16), - (_, status) => status_panic(status), + match parse_hex_exact(s, 16, 10) { + Ok(bits) => f16::from_bits(bits as u16), + Err(HexFloatParseError(s)) => panic!("{}", s), } } /// Construct a 32-bit float from hex float representation (C-style) #[allow(unused)] pub const fn hf32(s: &str) -> f32 { - match parse_any(s, 32, 23, Round::Nearest) { - (bits, Status::OK) => f32_from_bits(bits as u32), - (_, status) => status_panic(status), + match parse_hex_exact(s, 32, 23) { + Ok(bits) => f32::from_bits(bits as u32), + Err(HexFloatParseError(s)) => panic!("{}", s), } } /// Construct a 64-bit float from hex float representation (C-style) pub const fn hf64(s: &str) -> f64 { - match parse_any(s, 64, 52, Round::Nearest) { - (bits, Status::OK) => f64_from_bits(bits as u64), - (_, status) => status_panic(status), + match parse_hex_exact(s, 64, 52) { + Ok(bits) => f64::from_bits(bits as u64), + Err(HexFloatParseError(s)) => panic!("{}", s), } } /// Construct a 128-bit float from hex float representation (C-style) #[cfg(f128_enabled)] pub const fn hf128(s: &str) -> f128 { - match parse_any(s, 128, 112, Round::Nearest) { - (bits, Status::OK) => f128::from_bits(bits), - (_, status) => status_panic(status), + match parse_hex_exact(s, 128, 112) { + Ok(bits) => f128::from_bits(bits), + Err(HexFloatParseError(s)) => panic!("{}", s), } } - -const fn status_panic(status: Status) -> ! { - if status.underflow() { - assert!(status.inexact()); - panic!("the value is too tiny") - } - if status.overflow() { - assert!(status.inexact()); - panic!("the value is too huge") +#[derive(Copy, Clone, Debug)] +pub struct HexFloatParseError(&'static str); + +/// Parses any float to its bitwise representation, returning an error if it cannot be represented exactly +pub const fn parse_hex_exact( + s: &str, + bits: u32, + sig_bits: u32, +) -> Result { + match parse_any(s, bits, sig_bits, Round::Nearest) { + Err(e) => Err(e), + Ok((bits, Status::OK)) => Ok(bits), + Ok((_, status)) if status.overflow() => Err(HexFloatParseError("the value is too huge")), + Ok((_, status)) if status.underflow() => Err(HexFloatParseError("the value is too tiny")), + Ok((_, status)) if status.inexact() => Err(HexFloatParseError("the value is too precise")), + Ok(_) => unreachable!(), } - if status.inexact() { - panic!("the value is too precise") - } - panic!("unknown issue") } /// Parse any float from hex to its bitwise representation. -pub const fn parse_any(s: &str, bits: u32, sig_bits: u32, round: Round) -> (u128, Status) { +pub const fn parse_any( + s: &str, + bits: u32, + sig_bits: u32, + round: Round, +) -> Result<(u128, Status), HexFloatParseError> { let mut b = s.as_bytes(); - assert!(sig_bits <= 119); - assert!(bits <= 128); - assert!(bits >= sig_bits + 3); - assert!(bits <= sig_bits + 30); + if sig_bits > 119 || bits > 128 || bits < sig_bits + 3 || bits > sig_bits + 30 { + return Err(HexFloatParseError("unsupported target float configuration")); + } - let mut neg = false; - if let &[c @ (b'-' | b'+'), ref rest @ ..] = b { + let neg = matches!(b, [b'-', ..]); + if let &[b'-' | b'+', ref rest @ ..] = b { b = rest; - neg = c == b'-'; } let sign_bit = 1 << (bits - 1); @@ -74,38 +80,46 @@ pub const fn parse_any(s: &str, bits: u32, sig_bits: u32, round: Round) -> (u128 let nan = sign_bit - quiet_bit; let inf = nan - quiet_bit; - let (mut x, status) = match b { - &[b'i' | b'I', b'n' | b'N', b'f' | b'F'] => (inf, Status::OK), - &[b'n' | b'N', b'a' | b'A', b'n' | b'N'] => (nan, Status::OK), - &[b'0', b'x' | b'X', ref rest @ ..] => { - let round = if neg && matches!(round, Round::Positive) { - Round::Negative - } else if neg && matches!(round, Round::Negative) { - Round::Positive - } else { - round + let (mut x, status) = match *b { + [b'i' | b'I', b'n' | b'N', b'f' | b'F'] => (inf, Status::OK), + [b'n' | b'N', b'a' | b'A', b'n' | b'N'] => (nan, Status::OK), + [b'0', b'x' | b'X', ref rest @ ..] => { + let round = match (neg, round) { + // parse("-x", Round::Positive) == -parse("x", Round::Negative) + (true, Round::Positive) => Round::Negative, + (true, Round::Negative) => Round::Positive, + // rounding toward nearest or zero are symmetric + (true, Round::Nearest | Round::Zero) | (false, _) => round, }; - parse_finite(rest, bits, sig_bits, round) + match parse_finite(rest, bits, sig_bits, round) { + Err(e) => return Err(e), + Ok(res) => res, + } } - _ => panic!("no hex indicator"), + _ => return Err(HexFloatParseError("no hex indicator")), }; if neg { x ^= sign_bit; } - (x, status) + Ok((x, status)) } -const fn parse_finite(b: &[u8], bits: u32, sig_bits: u32, rounding_mode: Round) -> (u128, Status) { +const fn parse_finite( + b: &[u8], + bits: u32, + sig_bits: u32, + rounding_mode: Round, +) -> Result<(u128, Status), HexFloatParseError> { let exp_bits: u32 = bits - sig_bits - 1; let max_msb: i32 = (1 << (exp_bits - 1)) - 1; // The exponent of one ULP in the subnormals let min_lsb: i32 = 1 - max_msb - sig_bits as i32; let (mut sig, mut exp) = match parse_hex(b) { - Err(e) => panic!("{}", e.0), - Ok(Parsed { sig: 0, .. }) => return (0, Status::OK), + Err(e) => return Err(e), + Ok(Parsed { sig: 0, .. }) => return Ok((0, Status::OK)), Ok(Parsed { sig, exp }) => (sig, exp), }; @@ -143,29 +157,32 @@ const fn parse_finite(b: &[u8], bits: u32, sig_bits: u32, rounding_mode: Round) let uexp = (exp - min_lsb) as u128; let uexp = uexp << sig_bits; + // Note that it is possible for the exponent bits to equal 2 here + // if the value rounded up, but that means the mantissa is all zeroes + // so the value is still correct debug_assert!(sig <= 2 << sig_bits); let inf = ((1 << exp_bits) - 1) << sig_bits; - match sig.checked_add(uexp) { + let bits = match sig.checked_add(uexp) { Some(bits) if bits < inf => { - if status.inexact() { - if bits < (1 << sig_bits) { - status.set_underflow(true); - } + // inexact subnormal or zero? + if status.inexact() && bits < (1 << sig_bits) { + status.set_underflow(true); } - (bits, status) + bits } _ => { // overflow to infinity status.set_inexact(true); status.set_overflow(true); match rounding_mode { - Round::Positive | Round::Nearest => (inf, status), - Round::Negative | Round::Zero => (inf - 1, status), + Round::Positive | Round::Nearest => inf, + Round::Negative | Round::Zero => inf - 1, } } - } + }; + Ok((bits, status)) } /// Shift right, rounding all inexact divisions to the nearest odd number @@ -183,14 +200,15 @@ const fn shr_odd_rounding(x: u128, k: u32) -> u128 { } /// Divide by 4, rounding accor -const fn shr2_round(x: u128, round: Round) -> u128 { - let d = x % 8; - (x / 4) - + match round { - Round::Nearest => (1 & (0b11001000_u8 >> d)) as u128, - Round::Negative | Round::Zero => 0, - Round::Positive => ((x % 4) != 0) as u128, - } +const fn shr2_round(mut x: u128, round: Round) -> u128 { + let t = (x as u32) & 0b111; + x >>= 2; + match round { + Round::Nearest => x + ((0b11001000_u8 >> t) & 1) as u128, + Round::Negative => x, + Round::Zero => x, + Round::Positive => x + (t & 0b11 != 0) as u128, + } } /// A parsed finite and unsigned floating point number. @@ -200,8 +218,6 @@ struct Parsed { exp: i32, } -struct HexFloatParseError(&'static str); - /// Parse a hexadecimal float x const fn parse_hex(mut b: &[u8]) -> Result { let mut sig: u128 = 0; @@ -233,6 +249,7 @@ const fn parse_hex(mut b: &[u8]) -> Result { sig <<= 4; sig |= digit as u128; } else { + // FIXME: it is technically possible for exp to overflow if parsing a string with >500M digits exp += 4; inexact |= digit != 0; } @@ -257,36 +274,49 @@ const fn parse_hex(mut b: &[u8]) -> Result { some_digits = false; - let mut negate_exp = false; - if let &[c @ (b'-' | b'+'), ref rest @ ..] = b { + let negate_exp = matches!(b, [b'-', ..]); + if let &[b'-' | b'+', ref rest @ ..] = b { b = rest; - negate_exp = c == b'-'; } - let mut pexp: i32 = 0; + let mut pexp: u32 = 0; while let &[c, ref rest @ ..] = b { b = rest; let Some(digit) = dec_digit(c) else { return Err(HexFloatParseError("expected decimal digit")); }; some_digits = true; - let of; - (pexp, of) = pexp.overflowing_mul(10); - if of { - return Err(HexFloatParseError("too many exponent digits")); - } - pexp += digit as i32; + pexp = pexp.saturating_mul(10); + pexp += digit as u32; } if !some_digits { return Err(HexFloatParseError("at least one exponent digit is required")); }; + { + let e; + if negate_exp { + e = (exp as i64) - (pexp as i64); + } else { + e = (exp as i64) + (pexp as i64); + }; + + exp = if e < i32::MIN as i64 { + i32::MIN + } else if e > i32::MAX as i64 { + i32::MAX + } else { + e as i32 + }; + } + /* FIXME(msrv): once MSRV >= 1.66, replace the above workaround block with: if negate_exp { - exp -= pexp; + exp = exp.saturating_sub_unsigned(pexp); } else { - exp += pexp; - } + exp = exp.saturating_add_unsigned(pexp); + }; + */ Ok(Parsed { sig, exp }) } @@ -449,11 +479,12 @@ mod parse_tests { use super::*; - fn rounding_properties(s: &str) { - let (xd, s0) = parse_any(s, 16, 10, Round::Negative); - let (xu, s1) = parse_any(s, 16, 10, Round::Positive); - let (xz, s2) = parse_any(s, 16, 10, Round::Zero); - let (xn, s3) = parse_any(s, 16, 10, Round::Nearest); + #[cfg(f16_enabled)] + fn rounding_properties(s: &str) -> Result<(), HexFloatParseError> { + let (xd, s0) = parse_any(s, 16, 10, Round::Negative)?; + let (xu, s1) = parse_any(s, 16, 10, Round::Positive)?; + let (xz, s2) = parse_any(s, 16, 10, Round::Zero)?; + let (xn, s3) = parse_any(s, 16, 10, Round::Nearest)?; // FIXME: A value between the least normal and largest subnormal // could have underflow status depend on rounding mode. @@ -490,14 +521,16 @@ mod parse_tests { assert_biteq!(xn, xu); } } + Ok(()) } #[test] + #[cfg(f16_enabled)] fn test_rounding() { let n = 1_i32 << 14; for i in -n..n { let u = i.rotate_right(11) as u32; let s = format!("{}", Hexf(f32::from_bits(u))); - rounding_properties(&s); + assert!(rounding_properties(&s).is_ok()); } } @@ -557,23 +590,24 @@ mod parse_tests { } } #[test] + #[cfg(f128_enabled)] fn rounding() { let pi = std::f128::consts::PI; let s = format!("{}", Hexf(pi)); for k in 0..=111 { - let (bits, status) = parse_any(&s, 128 - k, 112 - k, Round::Nearest); + let (bits, status) = parse_any(&s, 128 - k, 112 - k, Round::Nearest).unwrap(); let scale = (1u128 << (112 - k - 1)) as f128; let expected = (pi * scale).round_ties_even() / scale; - assert_eq!(f128::from_bits(bits << k), expected,); - assert_eq!(expected != pi, status.inexact(),); + assert_eq!(bits << k, expected.to_bits()); + assert_eq!(expected != pi, status.inexact()); } } #[test] fn rounding_extreme_underflow() { for k in 1..1000 { let s = format!("0x1p{}", -149 - k); - let (bits, status) = parse_any(&s, 32, 23, Round::Nearest); + let Ok((bits, status)) = parse_any(&s, 32, 23, Round::Nearest) else { unreachable!() }; assert_eq!(bits, 0, "{s} should round to zero, got bits={bits}"); assert!(status.underflow(), "should indicate underflow when parsing {s}"); assert!(status.inexact(), "should indicate inexact when parsing {s}"); @@ -583,13 +617,11 @@ mod parse_tests { fn long_tail() { for k in 1..1000 { let s = format!("0x1.{}p0", "0".repeat(k)); - let (bits, Status::OK) = parse_any(&s, 32, 23, Round::Nearest) else { - panic!("parsing {s} failed") - }; + let Ok(bits) = parse_hex_exact(&s, 32, 23) else { panic!("parsing {s} failed") }; assert_eq!(f32::from_bits(bits as u32), 1.0); let s = format!("0x1.{}1p0", "0".repeat(k)); - let (bits, status) = parse_any(&s, 32, 23, Round::Nearest); + let Ok((bits, status)) = parse_any(&s, 32, 23, Round::Nearest) else { unreachable!() }; if status.inexact() { assert!(1.0 == f32::from_bits(bits as u32)); } else { @@ -634,7 +666,7 @@ mod parse_tests { ]; for (s, exp) in checks { println!("parsing {s}"); - rounding_properties(s); + assert!(rounding_properties(s).is_ok()); let act = hf16(s).to_bits(); assert_eq!( act, exp, diff --git a/src/math/support/mod.rs b/src/math/support/mod.rs index ee3f2bbdf..e776d466c 100644 --- a/src/math/support/mod.rs +++ b/src/math/support/mod.rs @@ -11,7 +11,6 @@ pub use big::{i256, u256}; pub use env::{FpResult, Round, Status}; #[allow(unused_imports)] pub use float_traits::{DFloat, Float, HFloat, IntTy}; -pub(crate) use float_traits::{f32_from_bits, f64_from_bits}; #[cfg(f16_enabled)] #[allow(unused_imports)] pub use hex_float::hf16; From b85e1ac7fd0461bfc654958a7c745c48ade22e5d Mon Sep 17 00:00:00 2001 From: quaternic <57393910+quaternic@users.noreply.github.com> Date: Tue, 4 Mar 2025 19:33:44 +0200 Subject: [PATCH 04/10] fix msrv, clippy --- src/math/support/hex_float.rs | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/math/support/hex_float.rs b/src/math/support/hex_float.rs index 737200384..19b3984df 100644 --- a/src/math/support/hex_float.rs +++ b/src/math/support/hex_float.rs @@ -240,8 +240,9 @@ const fn parse_hex(mut b: &[u8]) -> Result { } b'p' | b'P' => break, c => { - let Some(digit) = hex_digit(c) else { - return Err(HexFloatParseError("expected hexadecimal digit")); + let digit = match hex_digit(c) { + Some(d) => d, + None => return Err(HexFloatParseError("expected hexadecimal digit")), }; some_digits = true; @@ -282,8 +283,9 @@ const fn parse_hex(mut b: &[u8]) -> Result { let mut pexp: u32 = 0; while let &[c, ref rest @ ..] = b { b = rest; - let Some(digit) = dec_digit(c) else { - return Err(HexFloatParseError("expected decimal digit")); + let digit = match dec_digit(c) { + Some(d) => d, + None => return Err(HexFloatParseError("expected decimal digit")), }; some_digits = true; pexp = pexp.saturating_mul(10); @@ -599,7 +601,7 @@ mod parse_tests { let (bits, status) = parse_any(&s, 128 - k, 112 - k, Round::Nearest).unwrap(); let scale = (1u128 << (112 - k - 1)) as f128; let expected = (pi * scale).round_ties_even() / scale; - assert_eq!(bits << k, expected.to_bits()); + assert_eq!(bits << k, expected.to_bits(), "k = {k}, s = {s}"); assert_eq!(expected != pi, status.inexact()); } } @@ -1086,7 +1088,7 @@ mod print_tests { assert_eq!( f32.to_bits(), - (a as f32).to_bits(), + a.to_bits(), "{f16:?} : f16 formatted as {s16} which parsed as {a:?} : f16" ); assert_eq!( From 8d87c198b52eb83e482e5ce9505963866f448554 Mon Sep 17 00:00:00 2001 From: quaternic <57393910+quaternic@users.noreply.github.com> Date: Tue, 4 Mar 2025 20:03:08 +0200 Subject: [PATCH 05/10] more mrsv fixes --- src/math/support/env.rs | 14 +++++++++----- src/math/support/hex_float.rs | 11 +++++------ src/math/support/mod.rs | 1 + 3 files changed, 15 insertions(+), 11 deletions(-) diff --git a/src/math/support/env.rs b/src/math/support/env.rs index cd1040d77..b22aba53d 100644 --- a/src/math/support/env.rs +++ b/src/math/support/env.rs @@ -46,7 +46,7 @@ pub enum Round { } /// IEEE 754 exception status flags. -#[derive(Clone, Copy, Debug, PartialEq)] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct Status(u8); impl Status { @@ -100,11 +100,11 @@ impl Status { self.0 & Self::OVERFLOW.0 != 0 } - pub const fn set_underflow(&mut self, val: bool) { + pub fn set_underflow(&mut self, val: bool) { self.set_flag(val, Self::UNDERFLOW); } - pub const fn set_overflow(&mut self, val: bool) { + pub fn set_overflow(&mut self, val: bool) { self.set_flag(val, Self::OVERFLOW); } @@ -113,15 +113,19 @@ impl Status { self.0 & Self::INEXACT.0 != 0 } - pub const fn set_inexact(&mut self, val: bool) { + pub fn set_inexact(&mut self, val: bool) { self.set_flag(val, Self::INEXACT); } - const fn set_flag(&mut self, val: bool, mask: Self) { + fn set_flag(&mut self, val: bool, mask: Self) { if val { self.0 |= mask.0; } else { self.0 &= !mask.0; } } + + pub(crate) const fn with(self, rhs: Self) -> Self { + Self(self.0 | rhs.0) + } } diff --git a/src/math/support/hex_float.rs b/src/math/support/hex_float.rs index 19b3984df..094839f50 100644 --- a/src/math/support/hex_float.rs +++ b/src/math/support/hex_float.rs @@ -2,7 +2,7 @@ use core::fmt; -use super::{Float, Round, Status}; +use super::{Float, Round, Status, f32_from_bits, f64_from_bits}; /// Construct a 16-bit float from hex float representation (C-style) #[cfg(f16_enabled)] @@ -17,7 +17,7 @@ pub const fn hf16(s: &str) -> f16 { #[allow(unused)] pub const fn hf32(s: &str) -> f32 { match parse_hex_exact(s, 32, 23) { - Ok(bits) => f32::from_bits(bits as u32), + Ok(bits) => f32_from_bits(bits as u32), Err(HexFloatParseError(s)) => panic!("{}", s), } } @@ -25,7 +25,7 @@ pub const fn hf32(s: &str) -> f32 { /// Construct a 64-bit float from hex float representation (C-style) pub const fn hf64(s: &str) -> f64 { match parse_hex_exact(s, 64, 52) { - Ok(bits) => f64::from_bits(bits as u64), + Ok(bits) => f64_from_bits(bits as u64), Err(HexFloatParseError(s)) => panic!("{}", s), } } @@ -168,14 +168,13 @@ const fn parse_finite( Some(bits) if bits < inf => { // inexact subnormal or zero? if status.inexact() && bits < (1 << sig_bits) { - status.set_underflow(true); + status = status.with(Status::UNDERFLOW); } bits } _ => { // overflow to infinity - status.set_inexact(true); - status.set_overflow(true); + status = status.with(Status::OVERFLOW).with(Status::INEXACT); match rounding_mode { Round::Positive | Round::Nearest => inf, Round::Negative | Round::Zero => inf - 1, diff --git a/src/math/support/mod.rs b/src/math/support/mod.rs index e776d466c..ee3f2bbdf 100644 --- a/src/math/support/mod.rs +++ b/src/math/support/mod.rs @@ -11,6 +11,7 @@ pub use big::{i256, u256}; pub use env::{FpResult, Round, Status}; #[allow(unused_imports)] pub use float_traits::{DFloat, Float, HFloat, IntTy}; +pub(crate) use float_traits::{f32_from_bits, f64_from_bits}; #[cfg(f16_enabled)] #[allow(unused_imports)] pub use hex_float::hf16; From 5a2786b19231d269f68e659be85cd5dd2cdf1161 Mon Sep 17 00:00:00 2001 From: quaternic <57393910+quaternic@users.noreply.github.com> Date: Tue, 4 Mar 2025 20:08:20 +0200 Subject: [PATCH 06/10] snip dead code --- src/math/support/env.rs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/math/support/env.rs b/src/math/support/env.rs index b22aba53d..796309372 100644 --- a/src/math/support/env.rs +++ b/src/math/support/env.rs @@ -104,10 +104,6 @@ impl Status { self.set_flag(val, Self::UNDERFLOW); } - pub fn set_overflow(&mut self, val: bool) { - self.set_flag(val, Self::OVERFLOW); - } - /// True if `INEXACT` is set. pub const fn inexact(self) -> bool { self.0 & Self::INEXACT.0 != 0 From bab43ef1e53b7841db4b70ef5ba8ac5eacb9fa32 Mon Sep 17 00:00:00 2001 From: quaternic <57393910+quaternic@users.noreply.github.com> Date: Tue, 4 Mar 2025 21:01:13 +0200 Subject: [PATCH 07/10] disable problematic test for most targets --- src/math/support/hex_float.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/math/support/hex_float.rs b/src/math/support/hex_float.rs index 094839f50..70a32ef08 100644 --- a/src/math/support/hex_float.rs +++ b/src/math/support/hex_float.rs @@ -590,6 +590,9 @@ mod parse_tests { } } } + + // FIXME: this test is causing failures that are likely UB on various platforms + #[cfg(all(target_arch = "x86_64", target_os = "linux"))] #[test] #[cfg(f128_enabled)] fn rounding() { From d551777eff7f7675a35b1281bf7f27ec7513fabf Mon Sep 17 00:00:00 2001 From: quaternic <57393910+quaternic@users.noreply.github.com> Date: Sun, 13 Apr 2025 14:24:37 +0300 Subject: [PATCH 08/10] Fix broken comment --- src/math/support/hex_float.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/math/support/hex_float.rs b/src/math/support/hex_float.rs index 70a32ef08..8943d4bda 100644 --- a/src/math/support/hex_float.rs +++ b/src/math/support/hex_float.rs @@ -198,7 +198,7 @@ const fn shr_odd_rounding(x: u128, k: u32) -> u128 { } } -/// Divide by 4, rounding accor +/// Divide by 4, rounding with the given mode const fn shr2_round(mut x: u128, round: Round) -> u128 { let t = (x as u32) & 0b111; x >>= 2; From e21cb075b3ab1e16ac6003b8f62d942c199b6120 Mon Sep 17 00:00:00 2001 From: Trevor Gross Date: Mon, 14 Apr 2025 20:10:24 -0400 Subject: [PATCH 09/10] Update src/math/support/hex_float.rs Co-authored-by: quaternic <57393910+quaternic@users.noreply.github.com> --- src/math/support/hex_float.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/math/support/hex_float.rs b/src/math/support/hex_float.rs index 8943d4bda..cffcdabc3 100644 --- a/src/math/support/hex_float.rs +++ b/src/math/support/hex_float.rs @@ -203,7 +203,9 @@ const fn shr2_round(mut x: u128, round: Round) -> u128 { let t = (x as u32) & 0b111; x >>= 2; match round { + // Look-up-table on the last three bits for when to round up Round::Nearest => x + ((0b11001000_u8 >> t) & 1) as u128, + Round::Negative => x, Round::Zero => x, Round::Positive => x + (t & 0b11 != 0) as u128, From 26fcd19114aae190e197142c4113918cc82ffbc3 Mon Sep 17 00:00:00 2001 From: Trevor Gross Date: Mon, 14 Apr 2025 20:12:41 -0400 Subject: [PATCH 10/10] Update src/math/support/hex_float.rs --- src/math/support/hex_float.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/math/support/hex_float.rs b/src/math/support/hex_float.rs index cffcdabc3..819e2f56e 100644 --- a/src/math/support/hex_float.rs +++ b/src/math/support/hex_float.rs @@ -203,9 +203,9 @@ const fn shr2_round(mut x: u128, round: Round) -> u128 { let t = (x as u32) & 0b111; x >>= 2; match round { - // Look-up-table on the last three bits for when to round up + // Look-up-table on the last three bits for when to round up Round::Nearest => x + ((0b11001000_u8 >> t) & 1) as u128, - + Round::Negative => x, Round::Zero => x, Round::Positive => x + (t & 0b11 != 0) as u128,