Skip to content
This repository was archived by the owner on Apr 28, 2025. It is now read-only.

Commit f258331

Browse files
authored
Merge pull request #483 from tgross35/hex-print
Add support for printing hex float syntax
2 parents a89add3 + 151cd29 commit f258331

File tree

5 files changed

+250
-17
lines changed

5 files changed

+250
-17
lines changed

crates/libm-test/src/f8_impl.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
use std::cmp::{self, Ordering};
44
use std::{fmt, ops};
55

6+
use libm::support::hex_float::parse_any;
7+
68
use crate::Float;
79

810
/// Sometimes verifying float logic is easiest when all values can quickly be checked exhaustively
@@ -490,3 +492,7 @@ impl fmt::LowerHex for f8 {
490492
self.0.fmt(f)
491493
}
492494
}
495+
496+
pub const fn hf8(s: &str) -> f8 {
497+
f8(parse_any(s, 8, 3) as u8)
498+
}

crates/libm-test/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ use std::path::PathBuf;
2020
use std::sync::LazyLock;
2121
use std::time::SystemTime;
2222

23-
pub use f8_impl::f8;
23+
pub use f8_impl::{f8, hf8};
2424
pub use libm::support::{Float, Int, IntTy, MinInt};
2525
pub use num::{FloatExt, linear_ints, logspace};
2626
pub use op::{

crates/util/src/main.rs

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ use std::env;
88
use std::num::ParseIntError;
99
use std::str::FromStr;
1010

11-
use libm::support::{hf32, hf64};
11+
use libm::support::{Hexf, hf32, hf64};
1212
#[cfg(feature = "build-mpfr")]
1313
use libm_test::mpfloat::MpOp;
1414
use libm_test::{MathOp, TupleCall};
@@ -73,7 +73,7 @@ macro_rules! handle_call {
7373
}
7474
_ => panic!("unrecognized or disabled basis '{}'", $basis),
7575
};
76-
println!("{output:?}");
76+
println!("{output:?} {:x}", Hexf(output));
7777
return;
7878
}
7979
};
@@ -303,6 +303,10 @@ impl FromStrRadix for i32 {
303303
#[cfg(f16_enabled)]
304304
impl FromStrRadix for f16 {
305305
fn from_str_radix(s: &str, radix: u32) -> Result<Self, ParseIntError> {
306+
if radix == 16 && s.contains("p") {
307+
return Ok(libm::support::hf16(s));
308+
}
309+
306310
let s = strip_radix_prefix(s, radix);
307311
u16::from_str_radix(s, radix).map(Self::from_bits)
308312
}
@@ -334,6 +338,9 @@ impl FromStrRadix for f64 {
334338
#[cfg(f128_enabled)]
335339
impl FromStrRadix for f128 {
336340
fn from_str_radix(s: &str, radix: u32) -> Result<Self, ParseIntError> {
341+
if radix == 16 && s.contains("p") {
342+
return Ok(libm::support::hf128(s));
343+
}
337344
let s = strip_radix_prefix(s, radix);
338345
u128::from_str_radix(s, radix).map(Self::from_bits)
339346
}

src/math/support/hex_float.rs

Lines changed: 232 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@
22
33
#![allow(dead_code)] // FIXME: remove once this gets used
44

5-
use super::{f32_from_bits, f64_from_bits};
5+
use core::fmt;
6+
7+
use super::{Float, f32_from_bits, f64_from_bits};
68

79
/// Construct a 16-bit float from hex float representation (C-style)
810
#[cfg(f16_enabled)]
@@ -26,17 +28,25 @@ pub const fn hf128(s: &str) -> f128 {
2628
f128::from_bits(parse_any(s, 128, 112))
2729
}
2830

29-
const fn parse_any(s: &str, bits: u32, sig_bits: u32) -> u128 {
31+
/// Parse any float from hex to its bitwise representation.
32+
///
33+
/// `nan_repr` is passed rather than constructed so the platform-specific NaN is returned.
34+
pub const fn parse_any(s: &str, bits: u32, sig_bits: u32) -> u128 {
3035
let exp_bits: u32 = bits - sig_bits - 1;
3136
let max_msb: i32 = (1 << (exp_bits - 1)) - 1;
3237
// The exponent of one ULP in the subnormals
3338
let min_lsb: i32 = 1 - max_msb - sig_bits as i32;
3439

35-
let (neg, mut sig, exp) = parse_hex(s.as_bytes());
40+
let exp_mask = ((1 << exp_bits) - 1) << sig_bits;
3641

37-
if sig == 0 {
38-
return (neg as u128) << (bits - 1);
39-
}
42+
let (neg, mut sig, exp) = match parse_hex(s.as_bytes()) {
43+
Parsed::Finite { neg, sig: 0, .. } => return (neg as u128) << (bits - 1),
44+
Parsed::Finite { neg, sig, exp } => (neg, sig, exp),
45+
Parsed::Infinite { neg } => return ((neg as u128) << (bits - 1)) | exp_mask,
46+
Parsed::Nan { neg } => {
47+
return ((neg as u128) << (bits - 1)) | exp_mask | (1 << (sig_bits - 1));
48+
}
49+
};
4050

4151
// exponents of the least and most significant bits in the value
4252
let lsb = sig.trailing_zeros() as i32;
@@ -76,11 +86,24 @@ const fn parse_any(s: &str, bits: u32, sig_bits: u32) -> u128 {
7686
sig | ((neg as u128) << (bits - 1))
7787
}
7888

89+
/// A parsed floating point number.
90+
enum Parsed {
91+
/// Absolute value sig * 2^e
92+
Finite {
93+
neg: bool,
94+
sig: u128,
95+
exp: i32,
96+
},
97+
Infinite {
98+
neg: bool,
99+
},
100+
Nan {
101+
neg: bool,
102+
},
103+
}
104+
79105
/// Parse a hexadecimal float x
80-
/// returns (s,n,e):
81-
/// s == x.is_sign_negative()
82-
/// n * 2^e == x.abs()
83-
const fn parse_hex(mut b: &[u8]) -> (bool, u128, i32) {
106+
const fn parse_hex(mut b: &[u8]) -> Parsed {
84107
let mut neg = false;
85108
let mut sig: u128 = 0;
86109
let mut exp: i32 = 0;
@@ -90,6 +113,12 @@ const fn parse_hex(mut b: &[u8]) -> (bool, u128, i32) {
90113
neg = c == b'-';
91114
}
92115

116+
match *b {
117+
[b'i' | b'I', b'n' | b'N', b'f' | b'F'] => return Parsed::Infinite { neg },
118+
[b'n' | b'N', b'a' | b'A', b'n' | b'N'] => return Parsed::Nan { neg },
119+
_ => (),
120+
}
121+
93122
if let &[b'0', b'x' | b'X', ref rest @ ..] = b {
94123
b = rest;
95124
} else {
@@ -152,7 +181,7 @@ const fn parse_hex(mut b: &[u8]) -> (bool, u128, i32) {
152181
exp += pexp;
153182
}
154183

155-
(neg, sig, exp)
184+
Parsed::Finite { neg, sig, exp }
156185
}
157186

158187
const fn dec_digit(c: u8) -> u8 {
@@ -179,8 +208,107 @@ const fn u128_ilog2(v: u128) -> u32 {
179208
u128::BITS - 1 - v.leading_zeros()
180209
}
181210

211+
/// Format a floating point number as its IEEE hex (`%a`) representation.
212+
pub struct Hexf<F>(pub F);
213+
214+
// Adapted from https://github.com/ericseppanen/hexfloat2/blob/a5c27932f0ff/src/format.rs
215+
fn fmt_any_hex<F: Float>(x: &F, f: &mut fmt::Formatter<'_>) -> fmt::Result {
216+
if x.is_sign_negative() {
217+
write!(f, "-")?;
218+
}
219+
220+
if x.is_nan() {
221+
return write!(f, "NaN");
222+
} else if x.is_infinite() {
223+
return write!(f, "inf");
224+
} else if *x == F::ZERO {
225+
return write!(f, "0x0p+0");
226+
}
227+
228+
let mut exponent = x.exp_unbiased();
229+
let sig = x.to_bits() & F::SIG_MASK;
230+
231+
let bias = F::EXP_BIAS as i32;
232+
// The mantissa MSB needs to be shifted up to the nearest nibble.
233+
let mshift = (4 - (F::SIG_BITS % 4)) % 4;
234+
let sig = sig << mshift;
235+
// The width is rounded up to the nearest char (4 bits)
236+
let mwidth = (F::SIG_BITS as usize + 3) / 4;
237+
let leading = if exponent == -bias {
238+
// subnormal number means we shift our output by 1 bit.
239+
exponent += 1;
240+
"0."
241+
} else {
242+
"1."
243+
};
244+
245+
write!(f, "0x{leading}{sig:0mwidth$x}p{exponent:+}")
246+
}
247+
248+
#[cfg(f16_enabled)]
249+
impl fmt::LowerHex for Hexf<f16> {
250+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
251+
fmt_any_hex(&self.0, f)
252+
}
253+
}
254+
255+
impl fmt::LowerHex for Hexf<f32> {
256+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
257+
fmt_any_hex(&self.0, f)
258+
}
259+
}
260+
261+
impl fmt::LowerHex for Hexf<f64> {
262+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
263+
fmt_any_hex(&self.0, f)
264+
}
265+
}
266+
267+
#[cfg(f128_enabled)]
268+
impl fmt::LowerHex for Hexf<f128> {
269+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
270+
fmt_any_hex(&self.0, f)
271+
}
272+
}
273+
274+
impl fmt::LowerHex for Hexf<i32> {
275+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
276+
fmt::LowerHex::fmt(&self.0, f)
277+
}
278+
}
279+
280+
impl<T1, T2> fmt::LowerHex for Hexf<(T1, T2)>
281+
where
282+
T1: Copy,
283+
T2: Copy,
284+
Hexf<T1>: fmt::LowerHex,
285+
Hexf<T2>: fmt::LowerHex,
286+
{
287+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
288+
write!(f, "({:x}, {:x})", Hexf(self.0.0), Hexf(self.0.1))
289+
}
290+
}
291+
292+
impl<T> fmt::Debug for Hexf<T>
293+
where
294+
Hexf<T>: fmt::LowerHex,
295+
{
296+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
297+
fmt::LowerHex::fmt(self, f)
298+
}
299+
}
300+
301+
impl<T> fmt::Display for Hexf<T>
302+
where
303+
Hexf<T>: fmt::LowerHex,
304+
{
305+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
306+
fmt::LowerHex::fmt(self, f)
307+
}
308+
}
309+
182310
#[cfg(test)]
183-
mod tests {
311+
mod parse_tests {
184312
extern crate std;
185313
use std::{format, println};
186314

@@ -272,6 +400,10 @@ mod tests {
272400
("-0x1.998p-4", (-0.1f16).to_bits()),
273401
("0x0.123p-12", 0x0123),
274402
("0x1p-24", 0x0001),
403+
("nan", f16::NAN.to_bits()),
404+
("-nan", (-f16::NAN).to_bits()),
405+
("inf", f16::INFINITY.to_bits()),
406+
("-inf", f16::NEG_INFINITY.to_bits()),
275407
];
276408
for (s, exp) in checks {
277409
println!("parsing {s}");
@@ -322,6 +454,10 @@ mod tests {
322454
("0x1.111114p-127", 0x00444445),
323455
("0x1.23456p-130", 0x00091a2b),
324456
("0x1p-149", 0x00000001),
457+
("nan", f32::NAN.to_bits()),
458+
("-nan", (-f32::NAN).to_bits()),
459+
("inf", f32::INFINITY.to_bits()),
460+
("-inf", f32::NEG_INFINITY.to_bits()),
325461
];
326462
for (s, exp) in checks {
327463
println!("parsing {s}");
@@ -360,6 +496,10 @@ mod tests {
360496
("0x0.8000000000001p-1022", 0x0008000000000001),
361497
("0x0.123456789abcdp-1022", 0x000123456789abcd),
362498
("0x0.0000000000002p-1022", 0x0000000000000002),
499+
("nan", f64::NAN.to_bits()),
500+
("-nan", (-f64::NAN).to_bits()),
501+
("inf", f64::INFINITY.to_bits()),
502+
("-inf", f64::NEG_INFINITY.to_bits()),
363503
];
364504
for (s, exp) in checks {
365505
println!("parsing {s}");
@@ -401,6 +541,10 @@ mod tests {
401541
("-0x1.999999999999999999999999999ap-4", (-0.1f128).to_bits()),
402542
("0x0.abcdef0123456789abcdef012345p-16382", 0x0000abcdef0123456789abcdef012345),
403543
("0x1p-16494", 0x00000000000000000000000000000001),
544+
("nan", f128::NAN.to_bits()),
545+
("-nan", (-f128::NAN).to_bits()),
546+
("inf", f128::INFINITY.to_bits()),
547+
("-inf", f128::NEG_INFINITY.to_bits()),
404548
];
405549
for (s, exp) in checks {
406550
println!("parsing {s}");
@@ -623,3 +767,79 @@ mod tests_panicking {
623767
#[cfg(f128_enabled)]
624768
f128_tests!();
625769
}
770+
771+
#[cfg(test)]
772+
mod print_tests {
773+
extern crate std;
774+
use std::string::ToString;
775+
776+
use super::*;
777+
778+
#[test]
779+
#[cfg(f16_enabled)]
780+
fn test_f16() {
781+
use std::format;
782+
// Exhaustively check that `f16` roundtrips.
783+
for x in 0..=u16::MAX {
784+
let f = f16::from_bits(x);
785+
let s = format!("{}", Hexf(f));
786+
let from_s = hf16(&s);
787+
788+
if f.is_nan() && from_s.is_nan() {
789+
continue;
790+
}
791+
792+
assert_eq!(
793+
f.to_bits(),
794+
from_s.to_bits(),
795+
"{f:?} formatted as {s} but parsed as {from_s:?}"
796+
);
797+
}
798+
}
799+
800+
#[test]
801+
fn spot_checks() {
802+
assert_eq!(Hexf(f32::MAX).to_string(), "0x1.fffffep+127");
803+
assert_eq!(Hexf(f64::MAX).to_string(), "0x1.fffffffffffffp+1023");
804+
805+
assert_eq!(Hexf(f32::MIN).to_string(), "-0x1.fffffep+127");
806+
assert_eq!(Hexf(f64::MIN).to_string(), "-0x1.fffffffffffffp+1023");
807+
808+
assert_eq!(Hexf(f32::ZERO).to_string(), "0x0p+0");
809+
assert_eq!(Hexf(f64::ZERO).to_string(), "0x0p+0");
810+
811+
assert_eq!(Hexf(f32::NEG_ZERO).to_string(), "-0x0p+0");
812+
assert_eq!(Hexf(f64::NEG_ZERO).to_string(), "-0x0p+0");
813+
814+
assert_eq!(Hexf(f32::NAN).to_string(), "NaN");
815+
assert_eq!(Hexf(f64::NAN).to_string(), "NaN");
816+
817+
assert_eq!(Hexf(f32::INFINITY).to_string(), "inf");
818+
assert_eq!(Hexf(f64::INFINITY).to_string(), "inf");
819+
820+
assert_eq!(Hexf(f32::NEG_INFINITY).to_string(), "-inf");
821+
assert_eq!(Hexf(f64::NEG_INFINITY).to_string(), "-inf");
822+
823+
#[cfg(f16_enabled)]
824+
{
825+
assert_eq!(Hexf(f16::MAX).to_string(), "0x1.ffcp+15");
826+
assert_eq!(Hexf(f16::MIN).to_string(), "-0x1.ffcp+15");
827+
assert_eq!(Hexf(f16::ZERO).to_string(), "0x0p+0");
828+
assert_eq!(Hexf(f16::NEG_ZERO).to_string(), "-0x0p+0");
829+
assert_eq!(Hexf(f16::NAN).to_string(), "NaN");
830+
assert_eq!(Hexf(f16::INFINITY).to_string(), "inf");
831+
assert_eq!(Hexf(f16::NEG_INFINITY).to_string(), "-inf");
832+
}
833+
834+
#[cfg(f128_enabled)]
835+
{
836+
assert_eq!(Hexf(f128::MAX).to_string(), "0x1.ffffffffffffffffffffffffffffp+16383");
837+
assert_eq!(Hexf(f128::MIN).to_string(), "-0x1.ffffffffffffffffffffffffffffp+16383");
838+
assert_eq!(Hexf(f128::ZERO).to_string(), "0x0p+0");
839+
assert_eq!(Hexf(f128::NEG_ZERO).to_string(), "-0x0p+0");
840+
assert_eq!(Hexf(f128::NAN).to_string(), "NaN");
841+
assert_eq!(Hexf(f128::INFINITY).to_string(), "inf");
842+
assert_eq!(Hexf(f128::NEG_INFINITY).to_string(), "-inf");
843+
}
844+
}
845+
}

0 commit comments

Comments
 (0)