From 9ccee51149760071dc8fb54ce623825f0d390af5 Mon Sep 17 00:00:00 2001 From: "Pascal S. de Kloe" Date: Mon, 23 Mar 2026 15:52:24 +0100 Subject: [PATCH 1/6] flt2dec front with format_short and format_fixed * Fallback from Grisu to Dragon in flt2dec front * Together with name change to ensure all use checked * Drop of Grisu benchmarks which tested Dragon fallback * Documentation at functions instead of module level The values dropped in the benchmarks of Grisu are still covered in the Dragon benchmarks. All use of the format functions shows in this patch due to the name change. This ensures we don't leave any code behind which still uses strategy directly by accident. --- library/core/src/fmt/float.rs | 16 ++-- library/core/src/num/imp/flt2dec/mod.rs | 87 ++++++++++++++++--- .../src/num/imp/flt2dec/strategy/dragon.rs | 26 +++--- .../src/num/imp/flt2dec/strategy/grisu.rs | 67 ++++---------- .../benches/num/flt2dec/strategy/dragon.rs | 16 ++-- .../benches/num/flt2dec/strategy/grisu.rs | 57 ++---------- library/coretests/tests/num/flt2dec/mod.rs | 6 +- library/coretests/tests/num/flt2dec/random.rs | 34 ++++---- .../tests/num/flt2dec/strategy/dragon.rs | 22 ++--- .../tests/num/flt2dec/strategy/grisu.rs | 39 +++++---- 10 files changed, 175 insertions(+), 195 deletions(-) diff --git a/library/core/src/fmt/float.rs b/library/core/src/fmt/float.rs index bb70e59f960ed..1876998222275 100644 --- a/library/core/src/fmt/float.rs +++ b/library/core/src/fmt/float.rs @@ -45,14 +45,14 @@ where let mut buf: [MaybeUninit; 1024] = [MaybeUninit::uninit(); 1024]; // enough for f32 and f64 let mut parts: [MaybeUninit>; 4] = [MaybeUninit::uninit(); 4]; let formatted = flt2dec::to_exact_fixed_str( - flt2dec::strategy::grisu::format_exact, + flt2dec::format_fixed, *num, sign, precision.into(), &mut buf, &mut parts, ); - // SAFETY: `to_exact_fixed_str` and `format_exact` produce only ASCII characters. + // SAFETY: `to_exact_fixed_str` and `format_fixed` produce only ASCII characters. unsafe { fmt.pad_formatted_parts(&formatted) } } @@ -73,14 +73,14 @@ where [MaybeUninit::uninit(); flt2dec::MAX_SIG_DIGITS]; let mut parts: [MaybeUninit>; 4] = [MaybeUninit::uninit(); 4]; let formatted = flt2dec::to_shortest_str( - flt2dec::strategy::grisu::format_shortest, + flt2dec::format_short, *num, sign, precision.into(), &mut buf, &mut parts, ); - // SAFETY: `to_shortest_str` and `format_shortest` produce only ASCII characters. + // SAFETY: `to_shortest_str` and `format_short` produce only ASCII characters. unsafe { fmt.pad_formatted_parts(&formatted) } } @@ -118,7 +118,7 @@ where let mut buf: [MaybeUninit; 1024] = [MaybeUninit::uninit(); 1024]; // enough for f32 and f64 let mut parts: [MaybeUninit>; 6] = [MaybeUninit::uninit(); 6]; let formatted = flt2dec::to_exact_exp_str( - flt2dec::strategy::grisu::format_exact, + flt2dec::format_fixed, *num, sign, precision.into(), @@ -126,7 +126,7 @@ where &mut buf, &mut parts, ); - // SAFETY: `to_exact_exp_str` and `format_exact` produce only ASCII characters. + // SAFETY: `to_exact_exp_str` and `format_fixed` produce only ASCII characters. unsafe { fmt.pad_formatted_parts(&formatted) } } @@ -147,7 +147,7 @@ where [MaybeUninit::uninit(); flt2dec::MAX_SIG_DIGITS]; let mut parts: [MaybeUninit>; 6] = [MaybeUninit::uninit(); 6]; let formatted = flt2dec::to_shortest_exp_str( - flt2dec::strategy::grisu::format_shortest, + flt2dec::format_short, *num, sign, (0, 0), @@ -155,7 +155,7 @@ where &mut buf, &mut parts, ); - // SAFETY: `to_shortest_exp_str` and `format_shortest` produce only ASCII characters. + // SAFETY: `to_shortest_exp_str` and `format_short` produce only ASCII characters. unsafe { fmt.pad_formatted_parts(&formatted) } } diff --git a/library/core/src/num/imp/flt2dec/mod.rs b/library/core/src/num/imp/flt2dec/mod.rs index cf895b3e89e66..1efbb3c214c38 100644 --- a/library/core/src/num/imp/flt2dec/mod.rs +++ b/library/core/src/num/imp/flt2dec/mod.rs @@ -73,18 +73,6 @@ available in `strategy::dragon` and `strategy::grisu` respectively, extensively describes all necessary justifications and many proofs for them. (It is still difficult to follow though. You have been warned.) -Both implementations expose two public functions: - -- `format_shortest(decoded, buf)`, which always needs at least - `MAX_SIG_DIGITS` digits of buffer. Implements the shortest mode. - -- `format_exact(decoded, buf, limit)`, which accepts as small as - one digit of buffer. Implements exact and fixed modes. - -They try to fill the `u8` buffer with digits and returns the number of digits -written and the exponent `k`. They are total for all finite `f32` and `f64` -inputs (Grisu internally falls back to Dragon if necessary). - The rendered digits are formatted into the actual string form with four functions: @@ -142,6 +130,76 @@ pub mod strategy { /// The exact formula is `ceil(# bits in mantissa * log_10 2 + 1)`. pub const MAX_SIG_DIGITS: usize = 17; +/// Formats a finite, non-zero floating-point number in decimal form. +/// +/// The return pair `(digits, pow10)` represents: +/// +/// v = (0.d₀d₁…dₙ₋₁) × 10ᵏ +/// +/// where `digits = [d₀,…,dₙ₋₁]` and `pow10 = k`. The leading digit satisfies +/// `d₀ ≠ 0`, ensuring `0.1 ≤ mantissa < 1`. +/// +/// Given an input floating-point value `f`, the produced decimal `v` is the +/// closest such `n`-digit value satisfying: +/// +/// |f − v| ≤ ½ × 10^(k−n) +/// +/// If two `n`-digit decimals are equally close, a deterministic tie-breaking +/// rule is applied so that parsing the produced decimal recovers `f` exactly. +/// +/// In short mode, the minimal `n ≥ 1` satisfying the above property is chosen. +/// The result is therefore the shortest decimal that round-trips back to the +/// original floating-point value. This formatting matches common expectations; +/// for example `0.1f32` prints as `"0.1"`. +pub fn format_short<'a>(d: &Decoded, buf: &'a mut [MaybeUninit]) -> (&'a [u8], i16) { + // SAFETY: The borrow checker is not smart enough to let us use `buf` + // in the second branch, so we launder the lifetime here. But we only re-use + // `buf` if `format_short` returned `None` so this is okay. + match strategy::grisu::format_short(d, unsafe { &mut *(buf as *mut _) }) { + Some(ret) => ret, + None => strategy::dragon::format_short(d, buf), + } +} + +/// Disables `format_fixed` argument explicitly. +pub const UNLIMITED_RESOLUTION: i16 = i16::MIN; + +/// Formats a finite, non-zero floating-point number in decimal form. +/// +/// The return pair `(digits, pow10)` represents: +/// +/// v = (0.d₀d₁…dₙ₋₁) × 10ᵏ +/// +/// where `digits = [d₀,…,dₙ₋₁]` and `pow10 = k`. The leading digit satisfies +/// `d₀ ≠ 0`, ensuring `0.1 ≤ mantissa < 1`. +/// +/// Given an input floating-point value `f`, the produced decimal `v` is the +/// closest such `n`-digit value satisfying: +/// +/// |f − v| ≤ ½ × 10^(k−n) +/// +/// If two `n`-digit decimals are equally close, a deterministic tie-breaking +/// rule is applied so that parsing the produced decimal recovers `f` exactly. +/// +/// In fixed mode, the number of digits `n` is limited by the buffer size. The +/// `resolution` parameter may further restrict `n` by requiring `v` to be an +/// integer multiple of `10^resolution`. For example, a resolution of `-3` +/// causes rounding to three decimal places, i.e., values are multiples of +/// `0.001`. Use of [`UNLIMITED_RESOLUTION`] can get expensive. +pub fn format_fixed<'a>( + d: &Decoded, + buf: &'a mut [MaybeUninit], + resolution: i16, +) -> (&'a [u8], i16) { + // SAFETY: The borrow checker is not smart enough to let us use `buf` + // in the second branch, so we launder the lifetime here. But we only re-use + // `buf` if `format_exact_opt` returned `None` so this is okay. + match strategy::grisu::format_fixed(d, unsafe { &mut *(buf as *mut _) }, resolution) { + Some(ret) => ret, + None => strategy::dragon::format_fixed(d, buf, resolution), + } +} + /// When `d` contains decimal digits, increase the last digit and propagate carry. /// Returns a next digit when it causes the length to change. #[doc(hidden)] @@ -578,7 +636,7 @@ where let sig_digits = if frac_digits < maxlen { frac_digits + 1 } else { maxlen }; assert!(buf.len() >= sig_digits); - let (buf, exp) = format_exact(decoded, &mut buf[..sig_digits], i16::MIN); + let (buf, exp) = format_exact(decoded, &mut buf[..sig_digits], UNLIMITED_RESOLUTION); Formatted { sign, parts: digits_to_exp_str(buf, exp, frac_digits, upper, parts) } } } @@ -653,7 +711,8 @@ where // it *is* possible that `frac_digits` is ridiculously large. // `format_exact` will end rendering digits much earlier in this case, // because we are strictly limited by `maxlen`. - let limit = if frac_digits < 0x8000 { -(frac_digits as i16) } else { i16::MIN }; + let limit = + if frac_digits < 0x8000 { -(frac_digits as i16) } else { UNLIMITED_RESOLUTION }; let (buf, exp) = format_exact(decoded, &mut buf[..maxlen], limit); if exp <= limit { // the restriction couldn't been met, so this should render like zero no matter diff --git a/library/core/src/num/imp/flt2dec/strategy/dragon.rs b/library/core/src/num/imp/flt2dec/strategy/dragon.rs index b63b93034d57f..368e9f214f5b4 100644 --- a/library/core/src/num/imp/flt2dec/strategy/dragon.rs +++ b/library/core/src/num/imp/flt2dec/strategy/dragon.rs @@ -132,11 +132,11 @@ fn div_rem_upto_16<'a>( (d, x) } -/// The shortest mode implementation for Dragon. -pub fn format_shortest<'a>( +/// Provides a Dragon implementation for flt2dec::format_short. +pub fn format_short<'a>( d: &Decoded, buf: &'a mut [MaybeUninit], -) -> (/*digits*/ &'a [u8], /*exp*/ i16) { +) -> (/*digits*/ &'a [u8], /*pow10*/ i16) { // the number `v` to format is known to be: // - equal to `mant * 2^exp`; // - preceded by `(mant - 2 * minus) * 2^exp` in the original type; and @@ -292,12 +292,12 @@ pub fn format_shortest<'a>( (unsafe { buf[..i].assume_init_ref() }, k) } -/// The exact and fixed mode implementation for Dragon. -pub fn format_exact<'a>( +/// Provides a Dragon implementation for flt2dec::format_fixed. +pub fn format_fixed<'a>( d: &Decoded, buf: &'a mut [MaybeUninit], - limit: i16, -) -> (/*digits*/ &'a [u8], /*exp*/ i16) { + resolution: i16, +) -> (/*digits*/ &'a [u8], /*pow10*/ i16) { assert!(d.mant > 0); assert!(d.minus > 0); assert!(d.plus > 0); @@ -337,14 +337,14 @@ pub fn format_exact<'a>( // if we are working with the last-digit limitation, we need to shorten the buffer // before the actual rendering in order to avoid double rounding. // note that we have to enlarge the buffer again when rounding up happens! - let mut len = if k < limit { + let mut len = if k < resolution { // oops, we cannot even produce *one* digit. // this is possible when, say, we've got something like 9.5 and it's being rounded to 10. // we return an empty buffer, with an exception of the later rounding-up case - // which occurs when `k == limit` and has to produce exactly one digit. + // which occurs when `k == resolution` and has to produce exactly one digit. 0 - } else if ((k as i32 - limit as i32) as usize) < buf.len() { - (k - limit) as usize + } else if ((k as i32 - resolution as i32) as usize) < buf.len() { + (k - resolution) as usize } else { buf.len() }; @@ -409,9 +409,9 @@ pub fn format_exact<'a>( if let Some(c) = round_up(unsafe { buf[..len].assume_init_mut() }) { // ...unless we've been requested the fixed precision instead. // we also need to check that, if the original buffer was empty, - // the additional digit can only be added when `k == limit` (edge case). + // the additional digit can only be added when `k == resolution` (edge case). k += 1; - if k > limit && len < buf.len() { + if k > resolution && len < buf.len() { buf[len] = MaybeUninit::new(c); len += 1; } diff --git a/library/core/src/num/imp/flt2dec/strategy/grisu.rs b/library/core/src/num/imp/flt2dec/strategy/grisu.rs index f7ee465829340..a08294967121e 100644 --- a/library/core/src/num/imp/flt2dec/strategy/grisu.rs +++ b/library/core/src/num/imp/flt2dec/strategy/grisu.rs @@ -161,13 +161,13 @@ pub fn max_pow10_no_more_than(x: u32) -> (u8, u32) { } } -/// The shortest mode implementation for Grisu. +/// Provides a Grisu implementation for flt2dec::format_short. /// /// It returns `None` when it would return an inexact representation otherwise. -pub fn format_shortest_opt<'a>( +pub fn format_short<'a>( d: &Decoded, buf: &'a mut [MaybeUninit], -) -> Option<(/*digits*/ &'a [u8], /*exp*/ i16)> { +) -> Option<(/*digits*/ &'a [u8], /*pow10*/ i16)> { assert!(d.mant > 0); assert!(d.minus > 0); assert!(d.plus > 0); @@ -450,31 +450,14 @@ pub fn format_shortest_opt<'a>( } } -/// The shortest mode implementation for Grisu with Dragon fallback. -/// -/// This should be used for most cases. -pub fn format_shortest<'a>( - d: &Decoded, - buf: &'a mut [MaybeUninit], -) -> (/*digits*/ &'a [u8], /*exp*/ i16) { - use flt2dec::strategy::dragon::format_shortest as fallback; - // SAFETY: The borrow checker is not smart enough to let us use `buf` - // in the second branch, so we launder the lifetime here. But we only re-use - // `buf` if `format_shortest_opt` returned `None` so this is okay. - match format_shortest_opt(d, unsafe { &mut *(buf as *mut _) }) { - Some(ret) => ret, - None => fallback(d, buf), - } -} - -/// The exact and fixed mode implementation for Grisu. +/// Provides a Grisu implementation for flt2dec::format_fixed. /// /// It returns `None` when it would return an inexact representation otherwise. -pub fn format_exact_opt<'a>( +pub fn format_fixed<'a>( d: &Decoded, buf: &'a mut [MaybeUninit], - limit: i16, -) -> Option<(/*digits*/ &'a [u8], /*exp*/ i16)> { + resolution: i16, +) -> Option<(/*digits*/ &'a [u8], /*pow10*/ i16)> { assert!(d.mant > 0); assert!(d.mant < (1 << 61)); // we need at least three bits of additional precision assert!(!buf.is_empty()); @@ -528,7 +511,7 @@ pub fn format_exact_opt<'a>( // if we are working with the last-digit limitation, we need to shorten the buffer // before the actual rendering in order to avoid double rounding. // note that we have to enlarge the buffer again when rounding up happens! - let len = if exp <= limit { + let len = if exp <= resolution { // oops, we cannot even produce *one* digit. // this is possible when, say, we've got something like 9.5 and it's being rounded to 10. // @@ -540,10 +523,10 @@ pub fn format_exact_opt<'a>( // // SAFETY: `len=0`, so the obligation of having initialized this memory is trivial. return unsafe { - possibly_round(buf, 0, exp, limit, v.f / 10, (max_ten_kappa as u64) << e, err << e) + possibly_round(buf, 0, exp, resolution, v.f / 10, (max_ten_kappa as u64) << e, err << e) }; - } else if ((exp as i32 - limit as i32) as usize) < buf.len() { - (exp - limit) as usize + } else if ((exp as i32 - resolution as i32) as usize) < buf.len() { + (exp - resolution) as usize } else { buf.len() }; @@ -573,7 +556,7 @@ pub fn format_exact_opt<'a>( let vrem = ((r as u64) << e) + vfrac; // == (v % 10^kappa) * 2^e // SAFETY: we have initialized `len` many bytes. return unsafe { - possibly_round(buf, len, exp, limit, vrem, (ten_kappa as u64) << e, err << e) + possibly_round(buf, len, exp, resolution, vrem, (ten_kappa as u64) << e, err << e) }; } @@ -625,7 +608,7 @@ pub fn format_exact_opt<'a>( // is the buffer full? run the rounding pass with the remainder. if i == len { // SAFETY: we have initialized `len` many bytes. - return unsafe { possibly_round(buf, len, exp, limit, r, 1 << e, err) }; + return unsafe { possibly_round(buf, len, exp, resolution, r, 1 << e, err) }; } // restore invariants @@ -651,7 +634,7 @@ pub fn format_exact_opt<'a>( buf: &mut [MaybeUninit], mut len: usize, mut exp: i16, - limit: i16, + resolution: i16, remainder: u64, ten_kappa: u64, ulp: u64, @@ -742,9 +725,9 @@ pub fn format_exact_opt<'a>( { // only add an additional digit when we've been requested the fixed precision. // we also need to check that, if the original buffer was empty, - // the additional digit can only be added when `exp == limit` (edge case). + // the additional digit can only be added when `exp == resolution` (edge case). exp += 1; - if exp > limit && len < buf.len() { + if exp > resolution && len < buf.len() { buf[len] = MaybeUninit::new(c); len += 1; } @@ -758,21 +741,3 @@ pub fn format_exact_opt<'a>( None } } - -/// The exact and fixed mode implementation for Grisu with Dragon fallback. -/// -/// This should be used for most cases. -pub fn format_exact<'a>( - d: &Decoded, - buf: &'a mut [MaybeUninit], - limit: i16, -) -> (/*digits*/ &'a [u8], /*exp*/ i16) { - use flt2dec::strategy::dragon::format_exact as fallback; - // SAFETY: The borrow checker is not smart enough to let us use `buf` - // in the second branch, so we launder the lifetime here. But we only re-use - // `buf` if `format_exact_opt` returned `None` so this is okay. - match format_exact_opt(d, unsafe { &mut *(buf as *mut _) }, limit) { - Some(ret) => ret, - None => fallback(d, buf, limit), - } -} diff --git a/library/coretests/benches/num/flt2dec/strategy/dragon.rs b/library/coretests/benches/num/flt2dec/strategy/dragon.rs index d98b3df9b2f35..c79f0a4f38f7f 100644 --- a/library/coretests/benches/num/flt2dec/strategy/dragon.rs +++ b/library/coretests/benches/num/flt2dec/strategy/dragon.rs @@ -8,7 +8,7 @@ fn bench_small_shortest(b: &mut Bencher) { let decoded = decode_finite(3.141592f64); let mut buf = [MaybeUninit::new(0); MAX_SIG_DIGITS]; b.iter(|| { - format_shortest(black_box(&decoded), &mut buf); + format_short(black_box(&decoded), &mut buf); }); } @@ -17,7 +17,7 @@ fn bench_big_shortest(b: &mut Bencher) { let decoded = decode_finite(f64::MAX); let mut buf = [MaybeUninit::new(0); MAX_SIG_DIGITS]; b.iter(|| { - format_shortest(black_box(&decoded), &mut buf); + format_short(black_box(&decoded), &mut buf); }); } @@ -26,7 +26,7 @@ fn bench_small_exact_3(b: &mut Bencher) { let decoded = decode_finite(3.141592f64); let mut buf = [MaybeUninit::new(0); 3]; b.iter(|| { - format_exact(black_box(&decoded), &mut buf, i16::MIN); + format_fixed(black_box(&decoded), &mut buf, i16::MIN); }); } @@ -35,7 +35,7 @@ fn bench_big_exact_3(b: &mut Bencher) { let decoded = decode_finite(f64::MAX); let mut buf = [MaybeUninit::new(0); 3]; b.iter(|| { - format_exact(black_box(&decoded), &mut buf, i16::MIN); + format_fixed(black_box(&decoded), &mut buf, i16::MIN); }); } @@ -44,7 +44,7 @@ fn bench_small_exact_12(b: &mut Bencher) { let decoded = decode_finite(3.141592f64); let mut buf = [MaybeUninit::new(0); 12]; b.iter(|| { - format_exact(black_box(&decoded), &mut buf, i16::MIN); + format_fixed(black_box(&decoded), &mut buf, i16::MIN); }); } @@ -53,7 +53,7 @@ fn bench_big_exact_12(b: &mut Bencher) { let decoded = decode_finite(f64::MAX); let mut buf = [MaybeUninit::new(0); 12]; b.iter(|| { - format_exact(black_box(&decoded), &mut buf, i16::MIN); + format_fixed(black_box(&decoded), &mut buf, i16::MIN); }); } @@ -62,7 +62,7 @@ fn bench_small_exact_inf(b: &mut Bencher) { let decoded = decode_finite(3.141592f64); let mut buf = [MaybeUninit::new(0); 1024]; b.iter(|| { - format_exact(black_box(&decoded), &mut buf, i16::MIN); + format_fixed(black_box(&decoded), &mut buf, i16::MIN); }); } @@ -71,6 +71,6 @@ fn bench_big_exact_inf(b: &mut Bencher) { let decoded = decode_finite(f64::MAX); let mut buf = [MaybeUninit::new(0); 1024]; b.iter(|| { - format_exact(black_box(&decoded), &mut buf, i16::MIN); + format_fixed(black_box(&decoded), &mut buf, i16::MIN); }); } diff --git a/library/coretests/benches/num/flt2dec/strategy/grisu.rs b/library/coretests/benches/num/flt2dec/strategy/grisu.rs index 5f73fdebf5e77..bb3a979842f28 100644 --- a/library/coretests/benches/num/flt2dec/strategy/grisu.rs +++ b/library/coretests/benches/num/flt2dec/strategy/grisu.rs @@ -15,7 +15,7 @@ fn bench_small_shortest(b: &mut Bencher) { let decoded = decode_finite(3.141592f64); let mut buf = [MaybeUninit::new(0); MAX_SIG_DIGITS]; b.iter(|| { - format_shortest(black_box(&decoded), &mut buf); + format_short(black_box(&decoded), &mut buf).unwrap(); }); } @@ -24,7 +24,7 @@ fn bench_big_shortest(b: &mut Bencher) { let decoded = decode_finite(f64::MAX); let mut buf = [MaybeUninit::new(0); MAX_SIG_DIGITS]; b.iter(|| { - format_shortest(black_box(&decoded), &mut buf); + format_short(black_box(&decoded), &mut buf).unwrap(); }); } @@ -33,7 +33,7 @@ fn bench_small_exact_3(b: &mut Bencher) { let decoded = decode_finite(3.141592f64); let mut buf = [MaybeUninit::new(0); 3]; b.iter(|| { - format_exact(black_box(&decoded), &mut buf, i16::MIN); + format_fixed(black_box(&decoded), &mut buf, i16::MIN).unwrap(); }); } @@ -42,7 +42,7 @@ fn bench_big_exact_3(b: &mut Bencher) { let decoded = decode_finite(f64::MAX); let mut buf = [MaybeUninit::new(0); 3]; b.iter(|| { - format_exact(black_box(&decoded), &mut buf, i16::MIN); + format_fixed(black_box(&decoded), &mut buf, i16::MIN).unwrap(); }); } @@ -51,7 +51,7 @@ fn bench_small_exact_12(b: &mut Bencher) { let decoded = decode_finite(3.141592f64); let mut buf = [MaybeUninit::new(0); 12]; b.iter(|| { - format_exact(black_box(&decoded), &mut buf, i16::MIN); + format_fixed(black_box(&decoded), &mut buf, i16::MIN).unwrap(); }); } @@ -60,51 +60,6 @@ fn bench_big_exact_12(b: &mut Bencher) { let decoded = decode_finite(f64::MAX); let mut buf = [MaybeUninit::new(0); 12]; b.iter(|| { - format_exact(black_box(&decoded), &mut buf, i16::MIN); - }); -} - -#[bench] -fn bench_small_exact_inf(b: &mut Bencher) { - let decoded = decode_finite(3.141592f64); - let mut buf = [MaybeUninit::new(0); 1024]; - b.iter(|| { - format_exact(black_box(&decoded), &mut buf, i16::MIN); - }); -} - -#[bench] -fn bench_big_exact_inf(b: &mut Bencher) { - let decoded = decode_finite(f64::MAX); - let mut buf = [MaybeUninit::new(0); 1024]; - b.iter(|| { - format_exact(black_box(&decoded), &mut buf, i16::MIN); - }); -} - -#[bench] -fn bench_one_exact_inf(b: &mut Bencher) { - let decoded = decode_finite(1.0); - let mut buf = [MaybeUninit::new(0); 1024]; - b.iter(|| { - format_exact(black_box(&decoded), &mut buf, i16::MIN); - }); -} - -#[bench] -fn bench_trailing_zero_exact_inf(b: &mut Bencher) { - let decoded = decode_finite(250.000000000000000000000000); - let mut buf = [MaybeUninit::new(0); 1024]; - b.iter(|| { - format_exact(black_box(&decoded), &mut buf, i16::MIN); - }); -} - -#[bench] -fn bench_halfway_point_exact_inf(b: &mut Bencher) { - let decoded = decode_finite(1.00000000000000011102230246251565404236316680908203125); - let mut buf = [MaybeUninit::new(0); 1024]; - b.iter(|| { - format_exact(black_box(&decoded), &mut buf, i16::MIN); + format_fixed(black_box(&decoded), &mut buf, i16::MIN).unwrap(); }); } diff --git a/library/coretests/tests/num/flt2dec/mod.rs b/library/coretests/tests/num/flt2dec/mod.rs index 02a5d31553da1..17b99770c1f8c 100644 --- a/library/coretests/tests/num/flt2dec/mod.rs +++ b/library/coretests/tests/num/flt2dec/mod.rs @@ -1,6 +1,6 @@ use core::num::imp::flt2dec::{ - DecodableFloat, Decoded, FullDecoded, MAX_SIG_DIGITS, Sign, decode, round_up, to_exact_exp_str, - to_exact_fixed_str, to_shortest_exp_str, to_shortest_str, + DecodableFloat, Decoded, FullDecoded, MAX_SIG_DIGITS, Sign, UNLIMITED_RESOLUTION, decode, + round_up, to_exact_exp_str, to_exact_fixed_str, to_shortest_exp_str, to_shortest_str, }; use core::num::imp::fmt::{Formatted, Part}; use std::mem::MaybeUninit; @@ -60,7 +60,7 @@ macro_rules! check_shortest { macro_rules! try_exact { ($f:ident($decoded:expr) => $buf:expr, $expected:expr, $expectedk:expr; $fmt:expr, $($key:ident = $val:expr),*) => ({ - let (buf, k) = $f($decoded, &mut $buf[..$expected.len()], i16::MIN); + let (buf, k) = $f($decoded, &mut $buf[..$expected.len()], UNLIMITED_RESOLUTION); assert!((buf, k) == ($expected, $expectedk), $fmt, actual = (str::from_utf8(buf).unwrap(), k), expected = (str::from_utf8($expected).unwrap(), $expectedk), diff --git a/library/coretests/tests/num/flt2dec/random.rs b/library/coretests/tests/num/flt2dec/random.rs index b90386aee617c..579ff4ff126ab 100644 --- a/library/coretests/tests/num/flt2dec/random.rs +++ b/library/coretests/tests/num/flt2dec/random.rs @@ -4,7 +4,7 @@ use core::num::imp::flt2dec; use std::mem::MaybeUninit; use std::str; -use flt2dec::strategy::grisu::{format_exact_opt, format_shortest_opt}; +use flt2dec::strategy::{dragon, grisu}; use flt2dec::{DecodableFloat, Decoded, FullDecoded, MAX_SIG_DIGITS, decode}; use rand::distr::{Distribution, Uniform}; @@ -160,14 +160,14 @@ where #[test] fn shortest_random_equivalence_test() { - use flt2dec::strategy::dragon::format_shortest as fallback; + use dragon::format_short as fallback; // Miri is too slow let n = if cfg!(miri) { 10 } else { 10_000 }; - f64_random_equivalence_test(format_shortest_opt, fallback, MAX_SIG_DIGITS, n); - f32_random_equivalence_test(format_shortest_opt, fallback, MAX_SIG_DIGITS, n); + f64_random_equivalence_test(grisu::format_short, fallback, MAX_SIG_DIGITS, n); + f32_random_equivalence_test(grisu::format_short, fallback, MAX_SIG_DIGITS, n); #[cfg(target_has_reliable_f16)] - f16_random_equivalence_test(format_shortest_opt, fallback, MAX_SIG_DIGITS, n); + f16_random_equivalence_test(grisu::format_short, fallback, MAX_SIG_DIGITS, n); } #[test] @@ -175,8 +175,8 @@ fn shortest_random_equivalence_test() { #[cfg(target_has_reliable_f16)] fn shortest_f16_exhaustive_equivalence_test() { // see the f32 version - use flt2dec::strategy::dragon::format_shortest as fallback; - f16_exhaustive_equivalence_test(format_shortest_opt, fallback, MAX_SIG_DIGITS); + use dragon::format_short as fallback; + f16_exhaustive_equivalence_test(grisu::format_short, fallback, MAX_SIG_DIGITS); } #[test] @@ -189,8 +189,8 @@ fn shortest_f32_exhaustive_equivalence_test() { // with `--nocapture` (and plenty of time and appropriate rustc flags), this should print: // `done, ignored=17643158 passed=2121451881 failed=0`. - use flt2dec::strategy::dragon::format_shortest as fallback; - f32_exhaustive_equivalence_test(format_shortest_opt, fallback, MAX_SIG_DIGITS); + use dragon::format_short as fallback; + f32_exhaustive_equivalence_test(grisu::format_short, fallback, MAX_SIG_DIGITS); } #[test] @@ -198,20 +198,20 @@ fn shortest_f32_exhaustive_equivalence_test() { fn shortest_f64_hard_random_equivalence_test() { // this again probably has to use appropriate rustc flags. - use flt2dec::strategy::dragon::format_shortest as fallback; - f64_random_equivalence_test(format_shortest_opt, fallback, MAX_SIG_DIGITS, 100_000_000); + use dragon::format_short as fallback; + f64_random_equivalence_test(grisu::format_short, fallback, MAX_SIG_DIGITS, 100_000_000); } #[test] #[cfg(target_has_reliable_f16)] fn exact_f16_random_equivalence_test() { - use flt2dec::strategy::dragon::format_exact as fallback; + use dragon::format_fixed as fallback; // Miri is too slow let n = if cfg!(miri) { 3 } else { 1_000 }; for k in 1..21 { f16_random_equivalence_test( - |d, buf| format_exact_opt(d, buf, i16::MIN), + |d, buf| grisu::format_fixed(d, buf, i16::MIN), |d, buf| fallback(d, buf, i16::MIN), k, n, @@ -221,13 +221,13 @@ fn exact_f16_random_equivalence_test() { #[test] fn exact_f32_random_equivalence_test() { - use flt2dec::strategy::dragon::format_exact as fallback; + use dragon::format_fixed as fallback; // Miri is too slow let n = if cfg!(miri) { 3 } else { 1_000 }; for k in 1..21 { f32_random_equivalence_test( - |d, buf| format_exact_opt(d, buf, i16::MIN), + |d, buf| grisu::format_fixed(d, buf, i16::MIN), |d, buf| fallback(d, buf, i16::MIN), k, n, @@ -237,13 +237,13 @@ fn exact_f32_random_equivalence_test() { #[test] fn exact_f64_random_equivalence_test() { - use flt2dec::strategy::dragon::format_exact as fallback; + use dragon::format_fixed as fallback; // Miri is too slow let n = if cfg!(miri) { 2 } else { 1_000 }; for k in 1..21 { f64_random_equivalence_test( - |d, buf| format_exact_opt(d, buf, i16::MIN), + |d, buf| grisu::format_fixed(d, buf, i16::MIN), |d, buf| fallback(d, buf, i16::MIN), k, n, diff --git a/library/coretests/tests/num/flt2dec/strategy/dragon.rs b/library/coretests/tests/num/flt2dec/strategy/dragon.rs index 395f8e082e655..42e2fcfade181 100644 --- a/library/coretests/tests/num/flt2dec/strategy/dragon.rs +++ b/library/coretests/tests/num/flt2dec/strategy/dragon.rs @@ -16,11 +16,11 @@ fn test_mul_pow10() { #[test] fn shortest_sanity_test() { - f64_shortest_sanity_test(format_shortest); - f32_shortest_sanity_test(format_shortest); + f64_shortest_sanity_test(format_short); + f32_shortest_sanity_test(format_short); #[cfg(target_has_reliable_f16)] - f16_shortest_sanity_test(format_shortest); - more_shortest_sanity_test(format_shortest); + f16_shortest_sanity_test(format_short); + more_shortest_sanity_test(format_short); } #[test] @@ -40,30 +40,30 @@ fn exact_sanity_test() { // anyway and we're not super interested in testing each platform's exp2 // implementation. if !cfg!(target_env = "msvc") { - f64_exact_sanity_test(format_exact); + f64_exact_sanity_test(format_fixed); } - f32_exact_sanity_test(format_exact); + f32_exact_sanity_test(format_fixed); #[cfg(target_has_reliable_f16)] - f16_exact_sanity_test(format_exact); + f16_exact_sanity_test(format_fixed); } #[test] fn test_to_shortest_str() { - to_shortest_str_test(format_shortest); + to_shortest_str_test(format_short); } #[test] fn test_to_shortest_exp_str() { - to_shortest_exp_str_test(format_shortest); + to_shortest_exp_str_test(format_short); } #[test] fn test_to_exact_exp_str() { - to_exact_exp_str_test(format_exact); + to_exact_exp_str_test(format_fixed); } #[test] fn test_to_exact_fixed_str() { - to_exact_fixed_str_test(format_exact); + to_exact_fixed_str_test(format_fixed); } diff --git a/library/coretests/tests/num/flt2dec/strategy/grisu.rs b/library/coretests/tests/num/flt2dec/strategy/grisu.rs index 8350480333380..b65cd0da2b741 100644 --- a/library/coretests/tests/num/flt2dec/strategy/grisu.rs +++ b/library/coretests/tests/num/flt2dec/strategy/grisu.rs @@ -1,18 +1,19 @@ -use core::num::imp::flt2dec::strategy::grisu::*; +use core::num::imp::flt2dec; +use core::num::imp::flt2dec::strategy::grisu; use super::super::*; #[test] #[cfg_attr(miri, ignore)] // Miri is too slow fn test_cached_power() { - assert_eq!(CACHED_POW10.first().unwrap().1, CACHED_POW10_FIRST_E); - assert_eq!(CACHED_POW10.last().unwrap().1, CACHED_POW10_LAST_E); + assert_eq!(grisu::CACHED_POW10.first().unwrap().1, grisu::CACHED_POW10_FIRST_E); + assert_eq!(grisu::CACHED_POW10.last().unwrap().1, grisu::CACHED_POW10_LAST_E); for e in -1137..961 { // full range for f64 - let low = ALPHA - e - 64; - let high = GAMMA - e - 64; - let (_k, cached) = cached_power(low, high); + let low = grisu::ALPHA - e - 64; + let high = grisu::GAMMA - e - 64; + let (_k, cached) = grisu::cached_power(low, high); assert!( low <= cached.e && cached.e <= high, "cached_power({}, {}) = {:?} is incorrect", @@ -28,19 +29,19 @@ fn test_max_pow10_no_more_than() { let mut prevtenk = 1; for k in 1..10 { let tenk = prevtenk * 10; - assert_eq!(max_pow10_no_more_than(tenk - 1), (k - 1, prevtenk)); - assert_eq!(max_pow10_no_more_than(tenk), (k, tenk)); + assert_eq!(grisu::max_pow10_no_more_than(tenk - 1), (k - 1, prevtenk)); + assert_eq!(grisu::max_pow10_no_more_than(tenk), (k, tenk)); prevtenk = tenk; } } #[test] fn shortest_sanity_test() { - f64_shortest_sanity_test(format_shortest); - f32_shortest_sanity_test(format_shortest); + f64_shortest_sanity_test(flt2dec::format_short); + f32_shortest_sanity_test(flt2dec::format_short); #[cfg(target_has_reliable_f16)] - f16_shortest_sanity_test(format_shortest); - more_shortest_sanity_test(format_shortest); + f16_shortest_sanity_test(flt2dec::format_short); + more_shortest_sanity_test(flt2dec::format_short); } #[test] @@ -49,29 +50,29 @@ fn exact_sanity_test() { // See comments in dragon.rs's exact_sanity_test for why this test is // ignored on MSVC if !cfg!(target_env = "msvc") { - f64_exact_sanity_test(format_exact); + f64_exact_sanity_test(flt2dec::format_fixed); } - f32_exact_sanity_test(format_exact); + f32_exact_sanity_test(flt2dec::format_fixed); #[cfg(target_has_reliable_f16)] - f16_exact_sanity_test(format_exact); + f16_exact_sanity_test(flt2dec::format_fixed); } #[test] fn test_to_shortest_str() { - to_shortest_str_test(format_shortest); + to_shortest_str_test(flt2dec::format_short); } #[test] fn test_to_shortest_exp_str() { - to_shortest_exp_str_test(format_shortest); + to_shortest_exp_str_test(flt2dec::format_short); } #[test] fn test_to_exact_exp_str() { - to_exact_exp_str_test(format_exact); + to_exact_exp_str_test(flt2dec::format_fixed); } #[test] fn test_to_exact_fixed_str() { - to_exact_fixed_str_test(format_exact); + to_exact_fixed_str_test(flt2dec::format_fixed); } From 8fb070209f4beb8dfb8dd8c6e8b434000c08fa08 Mon Sep 17 00:00:00 2001 From: "Pascal S. de Kloe" Date: Mon, 23 Mar 2026 20:00:15 +0100 Subject: [PATCH 2/6] less abstraction in equivelance of flt2dec/random.rs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit "A little copying is better than a little dependency." — Rob Pike * Equivalence comparison explicit instead of generic * Bit patterns specified as ops::Range constants * No panic on invalid UTF-8 in equivalence test --- library/coretests/tests/num/flt2dec/random.rs | 317 ++++++++---------- 1 file changed, 142 insertions(+), 175 deletions(-) diff --git a/library/coretests/tests/num/flt2dec/random.rs b/library/coretests/tests/num/flt2dec/random.rs index 579ff4ff126ab..27748139b9d9e 100644 --- a/library/coretests/tests/num/flt2dec/random.rs +++ b/library/coretests/tests/num/flt2dec/random.rs @@ -2,251 +2,218 @@ use core::num::imp::flt2dec; use std::mem::MaybeUninit; -use std::str; +use std::string::String; use flt2dec::strategy::{dragon, grisu}; use flt2dec::{DecodableFloat, Decoded, FullDecoded, MAX_SIG_DIGITS, decode}; use rand::distr::{Distribution, Uniform}; -pub fn decode_finite(v: T) -> Decoded { +// Bits 0u16..0x7C00 cover the positive finite-range, +// with 1u16..0x7C00 for non-zero. +const F16_POS_FIN_RANGE: std::ops::Range = 1..f16::INFINITY.to_bits(); + +// Bits 0u32..0x7F80_0000 cover the positive finite-range, +// with 1u32..0x7F80_0000 for non-zero. +const F32_POS_FIN_RANGE: std::ops::Range = 1..f32::INFINITY.to_bits(); + +// Bits 0u64..0x7FF0_0000_0000_0000 cover the positive finite-range, +// with 1u64..0x7FF0_0000_0000_0000 for non-zero. +const F64_POS_FIN_RANGE: std::ops::Range = 1..f64::INFINITY.to_bits(); + +fn decode_finite(v: T) -> Decoded { match decode(v).1 { FullDecoded::Finite(decoded) => decoded, full_decoded => panic!("expected finite, got {full_decoded:?} instead"), } } -fn iterate(func: &str, k: usize, n: usize, mut f: F, mut g: G, mut v: V) -> (usize, usize) +/// Verifies whether `format_short` of Grisu and Dragon both have the same +/// outcome for n `Decoded` values from `v`. Values which get `None` from Grisu +/// are omitted. The return counts the amount omitted, range 0..n. +fn n_short_equiv(n: usize, mut v: V) -> usize where - F: for<'a> FnMut(&Decoded, &'a mut [MaybeUninit]) -> Option<(&'a [u8], i16)>, - G: for<'a> FnMut(&Decoded, &'a mut [MaybeUninit]) -> (&'a [u8], i16), V: FnMut(usize) -> Decoded, { - assert!(k <= 1024); - - let mut npassed = 0; // f(x) = Some(g(x)) - let mut nignored = 0; // f(x) = None - + let mut omitted = 0; + let mut failed = 0; for i in 0..n { - if (i & 0xfffff) == 0 { - println!( - "in progress, {:x}/{:x} (ignored={} passed={} failed={})", - i, - n, - nignored, - npassed, - i - nignored - npassed - ); + if (i & 0xf_ffff) == 0 { + println!("did {i} out of {n}, with {omitted} omitted"); } - - let decoded = v(i); - let mut buf1 = [MaybeUninit::new(0); 1024]; - if let Some((buf1, e1)) = f(&decoded, &mut buf1[..k]) { - let mut buf2 = [MaybeUninit::new(0); 1024]; - let (buf2, e2) = g(&decoded, &mut buf2[..k]); - if e1 == e2 && buf1 == buf2 { - npassed += 1; - } else { + let dec = v(i); + + let mut grisu_buf = [MaybeUninit::new(b'_'); MAX_SIG_DIGITS]; + let mut dragon_buf = [MaybeUninit::new(b'_'); MAX_SIG_DIGITS]; + if let Some(grisu_return) = grisu::format_short(&dec, &mut grisu_buf) { + let dragon_return = dragon::format_short(&dec, &mut dragon_buf); + if grisu_return != dragon_return { + failed += 1; println!( - "equivalence test failed, {:x}/{:x}: {:?} f(i)={}e{} g(i)={}e{}", - i, - n, - decoded, - str::from_utf8(buf1).unwrap(), - e1, - str::from_utf8(buf2).unwrap(), - e2 + "grisu got ({}, {}) while dragon got ({}, {}) for {:?}", + String::from_utf8_lossy(grisu_return.0), + grisu_return.1, + String::from_utf8_lossy(dragon_return.0), + dragon_return.1, + dec, ); } } else { - nignored += 1; + omitted += 1; } } - println!( - "{}({}): done, ignored={} passed={} failed={}", - func, - k, - nignored, - npassed, - n - nignored - npassed - ); - assert!( - nignored + npassed == n, - "{}({}): {} out of {} values returns an incorrect value!", - func, - k, - n - nignored - npassed, - n - ); - (npassed, nignored) + assert_eq!(failed, 0, "number of different outcomes"); + omitted } -#[cfg(target_has_reliable_f16)] -pub fn f16_random_equivalence_test(f: F, g: G, k: usize, n: usize) +/// Verifies whether `format_fixed` of Grisu and Dragon both have the same +/// outcome for n `Decoded` values from `v`. Values which get `None` from Grisu +/// are omitted. The return counts the amount omitted, range 0..n. +fn n_fixed_equiv(n: usize, bufn: usize, mut v: V) -> usize where - F: for<'a> FnMut(&Decoded, &'a mut [MaybeUninit]) -> Option<(&'a [u8], i16)>, - G: for<'a> FnMut(&Decoded, &'a mut [MaybeUninit]) -> (&'a [u8], i16), + V: FnMut(usize) -> Decoded, { - let mut rng = crate::test_rng(); - let f16_range = Uniform::new(0x0001u16, 0x7c00).unwrap(); - iterate("f16_random_equivalence_test", k, n, f, g, |_| { - let x = f16::from_bits(f16_range.sample(&mut rng)); - decode_finite(x) - }); + const BUF_CAP: usize = 64; + assert!(bufn <= BUF_CAP, "just increase capacity when needed"); + const RESOLUTION: i16 = i16::MIN; // unlimited + + let mut omitted = 0; + let mut failed = 0; + for i in 0..n { + if (i & 0xf_ffff) == 0 { + println!("did {i} out of {n}, with {omitted} omitted"); + } + let dec = v(i); + + let mut grisu_buf = [MaybeUninit::new(b'_'); BUF_CAP]; + let mut dragon_buf = [MaybeUninit::new(b'_'); BUF_CAP]; + if let Some(grisu_return) = grisu::format_fixed(&dec, &mut grisu_buf[..bufn], RESOLUTION) { + let dragon_return = dragon::format_fixed(&dec, &mut dragon_buf[..bufn], RESOLUTION); + if grisu_return != dragon_return { + failed += 1; + println!( + "grisu got ({}, {}) while dragon got ({}, {}) for {:?} with a {}-byte buffer", + String::from_utf8_lossy(grisu_return.0), + grisu_return.1, + String::from_utf8_lossy(dragon_return.0), + dragon_return.1, + dec, + bufn, + ); + } + } else { + omitted += 1; + } + } + assert_eq!(failed, 0, "number of different outcomes"); + omitted } -pub fn f32_random_equivalence_test(f: F, g: G, k: usize, n: usize) -where - F: for<'a> FnMut(&Decoded, &'a mut [MaybeUninit]) -> Option<(&'a [u8], i16)>, - G: for<'a> FnMut(&Decoded, &'a mut [MaybeUninit]) -> (&'a [u8], i16), -{ +#[test] +#[cfg(target_has_reliable_f16)] +fn test_short_f16_random_equiv() { + // Miri is too slow + let n = if cfg!(miri) { 10 } else { 10_000 }; + let mut rng = crate::test_rng(); - let f32_range = Uniform::new(0x0000_0001u32, 0x7f80_0000).unwrap(); - iterate("f32_random_equivalence_test", k, n, f, g, |_| { - let x = f32::from_bits(f32_range.sample(&mut rng)); + let u = Uniform::new(F16_POS_FIN_RANGE.start, F16_POS_FIN_RANGE.end).unwrap(); + n_short_equiv(n, |_| { + let x = f16::from_bits(u.sample(&mut rng)); decode_finite(x) }); } -pub fn f64_random_equivalence_test(f: F, g: G, k: usize, n: usize) -where - F: for<'a> FnMut(&Decoded, &'a mut [MaybeUninit]) -> Option<(&'a [u8], i16)>, - G: for<'a> FnMut(&Decoded, &'a mut [MaybeUninit]) -> (&'a [u8], i16), -{ +#[test] +fn test_short_f32_random_equiv() { + // Miri is too slow + let n = if cfg!(miri) { 10 } else { 10_000 }; + let mut rng = crate::test_rng(); - let f64_range = Uniform::new(0x0000_0000_0000_0001u64, 0x7ff0_0000_0000_0000).unwrap(); - iterate("f64_random_equivalence_test", k, n, f, g, |_| { - let x = f64::from_bits(f64_range.sample(&mut rng)); + let u = Uniform::new(F32_POS_FIN_RANGE.start, F32_POS_FIN_RANGE.end).unwrap(); + n_short_equiv(n, |_| { + let x = f32::from_bits(u.sample(&mut rng)); decode_finite(x) }); } -#[cfg(target_has_reliable_f16)] -pub fn f16_exhaustive_equivalence_test(f: F, g: G, k: usize) -where - F: for<'a> FnMut(&Decoded, &'a mut [MaybeUninit]) -> Option<(&'a [u8], i16)>, - G: for<'a> FnMut(&Decoded, &'a mut [MaybeUninit]) -> (&'a [u8], i16), -{ - // Unlike the other float types, `f16` is small enough that these exhaustive tests - // can run in less than a second so we don't need to ignore it. - - // iterate from 0x0001 to 0x7bff, i.e., all finite ranges - let (npassed, nignored) = - iterate("f16_exhaustive_equivalence_test", k, 0x7bff, f, g, |i: usize| { - let x = f16::from_bits(i as u16 + 1); - decode_finite(x) - }); - assert_eq!((npassed, nignored), (29735, 2008)); -} - -pub fn f32_exhaustive_equivalence_test(f: F, g: G, k: usize) -where - F: for<'a> FnMut(&Decoded, &'a mut [MaybeUninit]) -> Option<(&'a [u8], i16)>, - G: for<'a> FnMut(&Decoded, &'a mut [MaybeUninit]) -> (&'a [u8], i16), -{ - // we have only 2^23 * (2^8 - 1) - 1 = 2,139,095,039 positive finite f32 values, - // so why not simply testing all of them? - // - // this is of course very stressful (and thus should be behind an `#[ignore]` attribute), - // but with `-C opt-level=3 -C lto` this only takes about an hour or so. - - // iterate from 0x0000_0001 to 0x7f7f_ffff, i.e., all finite ranges - let (npassed, nignored) = - iterate("f32_exhaustive_equivalence_test", k, 0x7f7f_ffff, f, g, |i: usize| { - let x = f32::from_bits(i as u32 + 1); - decode_finite(x) - }); - assert_eq!((npassed, nignored), (2121451881, 17643158)); -} - #[test] -fn shortest_random_equivalence_test() { - use dragon::format_short as fallback; +fn test_short_f64_random_equiv() { // Miri is too slow let n = if cfg!(miri) { 10 } else { 10_000 }; - f64_random_equivalence_test(grisu::format_short, fallback, MAX_SIG_DIGITS, n); - f32_random_equivalence_test(grisu::format_short, fallback, MAX_SIG_DIGITS, n); - #[cfg(target_has_reliable_f16)] - f16_random_equivalence_test(grisu::format_short, fallback, MAX_SIG_DIGITS, n); + let mut rng = crate::test_rng(); + let u = Uniform::new(F64_POS_FIN_RANGE.start, F64_POS_FIN_RANGE.end).unwrap(); + n_short_equiv(n, |_| { + let x = f64::from_bits(u.sample(&mut rng)); + decode_finite(x) + }); } +/// Unlike the other float types, `f16` is small enough that these exhaustive tests +/// can run in less than a second so we don't need to ignore it. #[test] #[cfg_attr(miri, ignore)] // Miri is to slow #[cfg(target_has_reliable_f16)] -fn shortest_f16_exhaustive_equivalence_test() { - // see the f32 version - use dragon::format_short as fallback; - f16_exhaustive_equivalence_test(grisu::format_short, fallback, MAX_SIG_DIGITS); -} - -#[test] -#[ignore] // it is too expensive -fn shortest_f32_exhaustive_equivalence_test() { - // it is hard to directly test the optimality of the output, but we can at least test if - // two different algorithms agree to each other. - // - // this reports the progress and the number of f32 values returned `None`. - // with `--nocapture` (and plenty of time and appropriate rustc flags), this should print: - // `done, ignored=17643158 passed=2121451881 failed=0`. - - use dragon::format_short as fallback; - f32_exhaustive_equivalence_test(grisu::format_short, fallback, MAX_SIG_DIGITS); +fn test_short_f16_exhaustive_equiv() { + let omitted = n_short_equiv(F16_POS_FIN_RANGE.len(), |i: usize| { + let x = f16::from_bits(F16_POS_FIN_RANGE.start + i as u16); + decode_finite(x) + }); + assert_eq!(omitted, 2008, "number of inputs in {F16_POS_FIN_RANGE:?} not covered by Grisu"); } #[test] -#[ignore] // it is too expensive -fn shortest_f64_hard_random_equivalence_test() { - // this again probably has to use appropriate rustc flags. - - use dragon::format_short as fallback; - f64_random_equivalence_test(grisu::format_short, fallback, MAX_SIG_DIGITS, 100_000_000); +#[ignore] // takes about 40 seconds to run +#[cfg_attr(miri, ignore)] // Miri is to slow +fn test_short_f32_exhaustive_equiv() { + let omitted = n_short_equiv(F32_POS_FIN_RANGE.len(), |i: usize| { + let x = f32::from_bits(F32_POS_FIN_RANGE.start + i as u32); + decode_finite(x) + }); + assert_eq!(omitted, 17643158, "number of inputs in {F32_POS_FIN_RANGE:?} not covered by Grisu"); } #[test] #[cfg(target_has_reliable_f16)] -fn exact_f16_random_equivalence_test() { - use dragon::format_fixed as fallback; +fn test_fixed_f16_random_equiv() { // Miri is too slow let n = if cfg!(miri) { 3 } else { 1_000 }; - for k in 1..21 { - f16_random_equivalence_test( - |d, buf| grisu::format_fixed(d, buf, i16::MIN), - |d, buf| fallback(d, buf, i16::MIN), - k, - n, - ); + for bufn in 1..21 { + let mut rng = crate::test_rng(); + let f16_range = Uniform::new(F16_POS_FIN_RANGE.start, F16_POS_FIN_RANGE.end).unwrap(); + n_fixed_equiv(n, bufn, |_| { + let x = f16::from_bits(f16_range.sample(&mut rng)); + decode_finite(x) + }); } } #[test] -fn exact_f32_random_equivalence_test() { - use dragon::format_fixed as fallback; +fn test_fixed_f32_random_equiv() { // Miri is too slow let n = if cfg!(miri) { 3 } else { 1_000 }; - for k in 1..21 { - f32_random_equivalence_test( - |d, buf| grisu::format_fixed(d, buf, i16::MIN), - |d, buf| fallback(d, buf, i16::MIN), - k, - n, - ); + for bufn in 1..21 { + let mut rng = crate::test_rng(); + let f32_range = Uniform::new(F32_POS_FIN_RANGE.start, F32_POS_FIN_RANGE.end).unwrap(); + n_fixed_equiv(n, bufn, |_| { + let x = f32::from_bits(f32_range.sample(&mut rng)); + decode_finite(x) + }); } } #[test] -fn exact_f64_random_equivalence_test() { - use dragon::format_fixed as fallback; +fn test_fixed_f64_random_equiv() { // Miri is too slow let n = if cfg!(miri) { 2 } else { 1_000 }; - for k in 1..21 { - f64_random_equivalence_test( - |d, buf| grisu::format_fixed(d, buf, i16::MIN), - |d, buf| fallback(d, buf, i16::MIN), - k, - n, - ); + for bufn in 1..21 { + let mut rng = crate::test_rng(); + let f64_range = Uniform::new(F64_POS_FIN_RANGE.start, F64_POS_FIN_RANGE.end).unwrap(); + n_fixed_equiv(n, bufn, |_| { + let x = f64::from_bits(f64_range.sample(&mut rng)); + decode_finite(x) + }); } } From baa2a5b39f9f94b1cbea2e1b18768d2bf3a7a871 Mon Sep 17 00:00:00 2001 From: "Pascal S. de Kloe" Date: Thu, 26 Mar 2026 15:09:12 +0100 Subject: [PATCH 3/6] test of flt2dec::format_short & flt2dec::format_fixed normalized * No generics abstraction of Grisu and Dragon * The stringify! of input in assertion message * TestableFloat abstraction omitted with macro * MSVC omission direct on respective test * FIX test missed verification of the last digit * FIX test assertion value miss due the previous Unified signature plus description on assertion tools: * check_shortest! -> check_short! * try_exact! -> check_fixed_buf_match! * try_fixed! -> check_fixed_resolution! * check_exact & check_exact! -> check_fixed_mix! * check_exact_one -> check_coef_pow2! --- library/coretests/tests/num/flt2dec/mod.rs | 663 ++++++++---------- .../tests/num/flt2dec/strategy/dragon.rs | 34 - .../tests/num/flt2dec/strategy/grisu.rs | 22 - 3 files changed, 305 insertions(+), 414 deletions(-) diff --git a/library/coretests/tests/num/flt2dec/mod.rs b/library/coretests/tests/num/flt2dec/mod.rs index 17b99770c1f8c..4d5791fe2fc10 100644 --- a/library/coretests/tests/num/flt2dec/mod.rs +++ b/library/coretests/tests/num/flt2dec/mod.rs @@ -1,10 +1,11 @@ +use core::num::imp::flt2dec; +use core::num::imp::flt2dec::strategy::{dragon, grisu}; use core::num::imp::flt2dec::{ DecodableFloat, Decoded, FullDecoded, MAX_SIG_DIGITS, Sign, UNLIMITED_RESOLUTION, decode, round_up, to_exact_exp_str, to_exact_fixed_str, to_shortest_exp_str, to_shortest_str, }; use core::num::imp::fmt::{Formatted, Part}; use std::mem::MaybeUninit; -use std::{fmt, str}; use Sign::{Minus, MinusPlus}; @@ -17,203 +18,145 @@ mod strategy { } mod random; -pub fn decode_finite(v: T) -> Decoded { +/// Buffer that fits enough digits to format any `f64`. +const LARGE_TEST_BUF: [MaybeUninit; 1024] = [MaybeUninit::new(b'_'); 1024]; + +fn decode_finite(v: T) -> Decoded { match decode(v).1 { FullDecoded::Finite(decoded) => decoded, full_decoded => panic!("expected finite, got {full_decoded:?} instead for {v:?}"), } } -macro_rules! check_shortest { - ($f:ident($v:expr) => $buf:expr, $exp:expr) => ( - check_shortest!($f($v) => $buf, $exp; - "shortest mismatch for v={v}: actual {actual:?}, expected {expected:?}", - v = stringify!($v)) - ); +/// Assert format_short outcome. +macro_rules! check_short { + ($v:expr => $digits:expr, $pow10:expr) => { + let label = stringify!($v); + let dec = decode_finite($v); + let want: (&[u8], i16) = ($digits, $pow10); - ($f:ident{$($k:ident: $v:expr),+} => $buf:expr, $exp:expr) => ( - check_shortest!($f{$($k: $v),+} => $buf, $exp; - "shortest mismatch for {v:?}: actual {actual:?}, expected {expected:?}", - v = Decoded { $($k: $v),+ }) - ); + let mut bufd = [MaybeUninit::new(b'_'); MAX_SIG_DIGITS]; + let d = dragon::format_short(&dec, &mut bufd); + assert_eq!(d, want, "dragon short format of {label}"); - ($f:ident($v:expr) => $buf:expr, $exp:expr; $fmt:expr, $($key:ident = $val:expr),*) => ({ - let mut buf = [MaybeUninit::new(b'_'); MAX_SIG_DIGITS]; - let (buf, k) = $f(&decode_finite($v), &mut buf); - assert!((buf, k) == ($buf, $exp), - $fmt, actual = (str::from_utf8(buf).unwrap(), k), - expected = (str::from_utf8($buf).unwrap(), $exp), - $($key = $val),*); - }); - - ($f:ident{$($k:ident: $v:expr),+} => $buf:expr, $exp:expr; - $fmt:expr, $($key:ident = $val:expr),*) => ({ - let mut buf = [MaybeUninit::new(b'_'); MAX_SIG_DIGITS]; - let (buf, k) = $f(&Decoded { $($k: $v),+ }, &mut buf); - assert!((buf, k) == ($buf, $exp), - $fmt, actual = (str::from_utf8(buf).unwrap(), k), - expected = (str::from_utf8($buf).unwrap(), $exp), - $($key = $val),*); - }) + let mut bufg = [MaybeUninit::new(b'_'); MAX_SIG_DIGITS]; + if let Some(g) = grisu::format_short(&dec, &mut bufg) { + assert_eq!(g, want, "grisu short format of {label}"); + } + }; } -macro_rules! try_exact { - ($f:ident($decoded:expr) => $buf:expr, $expected:expr, $expectedk:expr; - $fmt:expr, $($key:ident = $val:expr),*) => ({ - let (buf, k) = $f($decoded, &mut $buf[..$expected.len()], UNLIMITED_RESOLUTION); - assert!((buf, k) == ($expected, $expectedk), - $fmt, actual = (str::from_utf8(buf).unwrap(), k), - expected = (str::from_utf8($expected).unwrap(), $expectedk), - $($key = $val),*); - }) +/// Assert format_fixed outcome with a specific resolution. +macro_rules! check_fixed_resolution { + ($v:expr, $resolution:expr => $digits:expr, $pow10:expr) => { + let label = stringify!($v); + let dec = decode_finite($v); + let resolution: i16 = $resolution; + let want: (&[u8], i16) = ($digits, $pow10); + + let mut bufd = LARGE_TEST_BUF; + let d = dragon::format_fixed(&dec, &mut bufd, resolution); + assert_eq!(d, want, "dragon short format of {label}"); + + let mut bufg = LARGE_TEST_BUF; + if let Some(g) = grisu::format_fixed(&dec, &mut bufg, resolution) { + assert_eq!(g, want, "dragon short format of {label}"); + } + }; } -macro_rules! try_fixed { - ($f:ident($decoded:expr) => $buf:expr, $request:expr, $expected:expr, $expectedk:expr; - $fmt:expr, $($key:ident = $val:expr),*) => ({ - let (buf, k) = $f($decoded, &mut $buf[..], $request); - assert!((buf, k) == ($expected, $expectedk), - $fmt, actual = (str::from_utf8(buf).unwrap(), k), - expected = (str::from_utf8($expected).unwrap(), $expectedk), - $($key = $val),*); - }) +/// Assert format_fixed outcome with a matching buffer size. +macro_rules! check_fixed_buf_match { + ($v:expr => $digits:expr, $pow10:expr) => { + let label = stringify!($v); + let dec = decode_finite($v); + let want: (&[u8], i16) = ($digits, $pow10); + + let mut bufd = LARGE_TEST_BUF; + let mut matchedd = &mut bufd[..want.0.len()]; + let d = dragon::format_fixed(&dec, &mut matchedd, UNLIMITED_RESOLUTION); + assert_eq!(d, want, "dragon fixed format of {label}"); + + let mut bufg = LARGE_TEST_BUF; + let mut matchedg = &mut bufg[..want.0.len()]; + if let Some(g) = grisu::format_fixed(&dec, &mut matchedg, UNLIMITED_RESOLUTION) { + assert_eq!(g, want, "grisu fixed format of {label}"); + } + }; } -fn check_exact(mut f: F, v: T, vstr: &str, expected: &[u8], expectedk: i16) -where - T: DecodableFloat, - F: for<'a> FnMut(&Decoded, &'a mut [MaybeUninit], i16) -> (&'a [u8], i16), -{ - // use a large enough buffer - let mut buf = [MaybeUninit::new(b'_'); 1024]; - let mut expected_ = [b'_'; 1024]; - - let decoded = decode_finite(v); - let cut = expected.iter().position(|&c| c == b' '); - - // check significant digits - for i in 1..cut.unwrap_or(expected.len() - 1) { - expected_[..i].copy_from_slice(&expected[..i]); - let mut expectedk_ = expectedk; - if expected[i] >= b'5' { - // check if this is a rounding-to-even case. - // we avoid rounding ...x5000... (with infinite zeroes) to ...(x+1) when x is even. - if !(i + 1 < expected.len() - && expected[i - 1] & 1 == 0 - && expected[i] == b'5' - && expected[i + 1] == b' ') - { - // if this returns true, expected_[..i] is all `9`s and being rounded up. - // we should always return `100..00` (`i` digits) instead, since that's - // what we can came up with `i` digits anyway. `round_up` assumes that - // the adjustment to the length is done by caller, which we simply ignore. - if let Some(_) = round_up(&mut expected_[..i]) { - expectedk_ += 1; +/// Assert format_fixed outcome including variants at a lower resolution. +/// An equals suffix ("=") on `$digits` triggers testing of infinite zeroes. +macro_rules! check_fixed_mix { + ($v:expr => $digits:expr, $pow10:expr) => ( + let (digits, inf_zero): (&[u8], bool) = { + let d: &[u8] = $digits; + match d.strip_suffix(b"=".as_slice()) { + Some(s) => (s, true), + None => (d, false), + } + }; + let pow10: i16 = $pow10; + + // Verify base assertion. + check_fixed_buf_match!($v => &digits, pow10); + check_fixed_resolution!($v, pow10 - digits.len() as i16 => &digits, pow10); + + let mut want_buf = [b'_'; 1024]; + + // Verify at a lower resolution with substrings of `$digits`. + for i in 1..(digits.len() - 1) { + want_buf[..i].copy_from_slice(&digits[..i]); + let mut want_pow10 = pow10; + if digits[i] >= b'5' { + // Include rounding-to-even case (with equals suffix). + if !(digits[i - 1] & 1 == 0 + && digits[i] == b'5' + && digits[i + 1] == b'=') + { + if let Some(_) = round_up(&mut want_buf[..i]) { + // All '9's got rounded up to an extra digit. + want_pow10 += 1; + } } } - } - try_exact!(f(&decoded) => &mut buf, &expected_[..i], expectedk_; - "exact sigdigit mismatch for v={v}, i={i}: \ - actual {actual:?}, expected {expected:?}", - v = vstr, i = i); - try_fixed!(f(&decoded) => &mut buf, expectedk_ - i as i16, &expected_[..i], expectedk_; - "fixed sigdigit mismatch for v={v}, i={i}: \ - actual {actual:?}, expected {expected:?}", - v = vstr, i = i); - } + check_fixed_buf_match!($v => &want_buf[..i], want_pow10); + check_fixed_resolution!($v, want_pow10 - i as i16 => &want_buf[..i], want_pow10); + } - // check exact rounding for zero- and negative-width cases - let start; - if expected[0] > b'5' { - try_fixed!(f(&decoded) => &mut buf, expectedk, b"1", expectedk + 1; - "zero-width rounding-up mismatch for v={v}: \ - actual {actual:?}, expected {expected:?}", - v = vstr); - start = 1; - } else { - start = 0; - } - for i in start..-10 { - try_fixed!(f(&decoded) => &mut buf, expectedk - i, b"", expectedk; - "rounding-down mismatch for v={v}, i={i}: \ - actual {actual:?}, expected {expected:?}", - v = vstr, i = -i); - } + // check exact rounding for zero- and negative-width cases + let start; + if digits[0] > b'5' { + check_fixed_resolution!($v, pow10 => b"1", pow10 + 1); + start = 1; + } else { + start = 0; + } + for i in start..-10 { + check_fixed_resolution!($v, pow10 - i => b"", pow10); + } - // check infinite zero digits - if let Some(cut) = cut { - for i in cut..expected.len() - 1 { - expected_[..cut].copy_from_slice(&expected[..cut]); - for c in &mut expected_[cut..i] { - *c = b'0'; + // Verify infinite zero digits. + if inf_zero { + let zero_trail = b"0000000"; + let trail_range = digits.len()..(digits.len() + zero_trail.len()); + want_buf[..trail_range.start].copy_from_slice(digits); + want_buf[trail_range.clone()].copy_from_slice(&zero_trail[..]); + for last in trail_range { + let end = last + 1; + check_fixed_buf_match!($v => &want_buf[..end], pow10); + check_fixed_resolution!($v, pow10 - end as i16 => &want_buf[..end], pow10); } - - try_exact!(f(&decoded) => &mut buf, &expected_[..i], expectedk; - "exact infzero mismatch for v={v}, i={i}: \ - actual {actual:?}, expected {expected:?}", - v = vstr, i = i); - try_fixed!(f(&decoded) => &mut buf, expectedk - i as i16, &expected_[..i], expectedk; - "fixed infzero mismatch for v={v}, i={i}: \ - actual {actual:?}, expected {expected:?}", - v = vstr, i = i); } - } -} - -trait TestableFloat: DecodableFloat + fmt::Display { - /// Returns `x * 2^exp`. Almost same to `std::{f32,f64}::ldexp`. - /// This is used for testing. - fn ldexpi(f: i64, exp: isize) -> Self; + ) } -#[cfg(target_has_reliable_f16)] -impl TestableFloat for f16 { - fn ldexpi(f: i64, exp: isize) -> Self { - f as Self * (exp as Self).exp2() - } -} - -impl TestableFloat for f32 { - fn ldexpi(f: i64, exp: isize) -> Self { - f as Self * (exp as Self).exp2() - } -} - -impl TestableFloat for f64 { - fn ldexpi(f: i64, exp: isize) -> Self { - f as Self * (exp as Self).exp2() - } -} - -fn check_exact_one(mut f: F, x: i64, e: isize, tstr: &str, expected: &[u8], expectedk: i16) -where - T: TestableFloat, - F: for<'a> FnMut(&Decoded, &'a mut [MaybeUninit], i16) -> (&'a [u8], i16), -{ - // use a large enough buffer - let mut buf = [MaybeUninit::new(b'_'); 1024]; - let v: T = TestableFloat::ldexpi(x, e); - let decoded = decode_finite(v); - - try_exact!(f(&decoded) => &mut buf, &expected, expectedk; - "exact mismatch for v={x}p{e}{t}: actual {actual:?}, expected {expected:?}", - x = x, e = e, t = tstr); - try_fixed!(f(&decoded) => &mut buf, expectedk - expected.len() as i16, &expected, expectedk; - "fixed mismatch for v={x}p{e}{t}: actual {actual:?}, expected {expected:?}", - x = x, e = e, t = tstr); -} - -macro_rules! check_exact { - ($f:ident($v:expr) => $buf:expr, $exp:expr) => { - check_exact(|d, b, k| $f(d, b, k), $v, stringify!($v), $buf, $exp) - }; -} - -macro_rules! check_exact_one { - ($f:ident($x:expr, $e:expr; $t:ty) => $buf:expr, $exp:expr) => { - check_exact_one::<_, $t>(|d, b, k| $f(d, b, k), $x, $e, stringify!($t), $buf, $exp) - }; +macro_rules! check_coef_pow2 { + ($coef:expr, $pow2:expr => $digits:expr, $pow10:expr) => ( + check_fixed_buf_match!($coef * $pow2.exp2() => $digits, $pow10); + check_fixed_resolution!($coef * $pow2.exp2(), $pow10 - $digits.len() as i16 => $digits, $pow10); + ) } // in the following comments, three numbers are spaced by 1 ulp apart, @@ -225,334 +168,338 @@ macro_rules! check_exact_one { // ftp://ftp.ee.lbl.gov/testbase-report.ps.Z // or https://www.icir.org/vern/papers/testbase-report.pdf +#[test] #[cfg(target_has_reliable_f16)] -pub fn f16_shortest_sanity_test(mut f: F) -where - F: for<'a> FnMut(&Decoded, &'a mut [MaybeUninit]) -> (&'a [u8], i16), -{ +fn f16_short_sanity_test() { // 0.0999145507813 // 0.0999755859375 // 0.100036621094 - check_shortest!(f(0.1f16) => b"1", 0); + check_short!(0.1f16 => b"1", 0); // 0.3330078125 // 0.333251953125 (1/3 in the default rounding) // 0.33349609375 - check_shortest!(f(1.0f16/3.0) => b"3333", 0); + check_short!(1.0f16/3.0 => b"3333", 0); // 10^1 * 0.3138671875 // 10^1 * 0.3140625 // 10^1 * 0.3142578125 - check_shortest!(f(3.14f16) => b"314", 1); + check_short!(3.14f16 => b"314", 1); // 10^18 * 0.31415916243714048 // 10^18 * 0.314159196796878848 // 10^18 * 0.314159231156617216 - check_shortest!(f(3.1415e4f16) => b"3141", 5); + check_short!(3.1415e4f16 => b"3141", 5); // regression test for decoders // 10^2 * 0.31984375 // 10^2 * 0.32 // 10^2 * 0.3203125 - check_shortest!(f(crate::num::ldexp_f16(1.0, 5)) => b"32", 2); + check_short!(crate::num::ldexp_f16(1.0, 5) => b"32", 2); // 10^5 * 0.65472 // 10^5 * 0.65504 // 10^5 * 0.65536 - check_shortest!(f(f16::MAX) => b"655", 5); + check_short!(f16::MAX => b"655", 5); // 10^-4 * 0.60975551605224609375 // 10^-4 * 0.6103515625 // 10^-4 * 0.61094760894775390625 - check_shortest!(f(f16::MIN_POSITIVE) => b"6104", -4); + check_short!(f16::MIN_POSITIVE => b"6104", -4); // 10^-9 * 0 // 10^-9 * 0.59604644775390625 // 10^-8 * 0.11920928955078125 - let minf16 = crate::num::ldexp_f16(1.0, -24); - check_shortest!(f(minf16) => b"6", -7); + check_short!(crate::num::ldexp_f16(1.0, -24) => b"6", -7); } +#[test] #[cfg(target_has_reliable_f16)] -pub fn f16_exact_sanity_test(mut f: F) -where - F: for<'a> FnMut(&Decoded, &'a mut [MaybeUninit], i16) -> (&'a [u8], i16), -{ +fn f16_fixed_sanity_test() { let minf16 = crate::num::ldexp_f16(1.0, -24); - check_exact!(f(0.1f16) => b"999755859375 ", -1); - check_exact!(f(0.5f16) => b"5 ", 0); - check_exact!(f(1.0f16/3.0) => b"333251953125 ", 0); - check_exact!(f(3.141f16) => b"3140625 ", 1); - check_exact!(f(3.141e4f16) => b"31408 ", 5); - check_exact!(f(f16::MAX) => b"65504 ", 5); - check_exact!(f(f16::MIN_POSITIVE) => b"6103515625 ", -4); - check_exact!(f(minf16) => b"59604644775390625", -7); + check_fixed_mix!(0.1f16 => b"999755859375=", -1); + check_fixed_mix!(0.5f16 => b"5=", 0); + check_fixed_mix!(1.0f16/3.0 => b"333251953125=", 0); + check_fixed_mix!(3.141f16 => b"3140625=", 1); + check_fixed_mix!(3.141e4f16 => b"31408=", 5); + check_fixed_mix!(f16::MAX => b"65504=", 5); + check_fixed_mix!(f16::MIN_POSITIVE => b"6103515625=", -4); + check_fixed_mix!(minf16 => b"59604644775390625", -7); - // FIXME(f16): these should gain the check_exact_one tests like `f32` and `f64` have, + // FIXME(f16): these should gain the check_coef_pow2 tests like `f32` and `f64` have, // but these values are not easy to generate. The algorithm from the Paxon paper [1] needs // to be adapted to binary16. } -pub fn f32_shortest_sanity_test(mut f: F) -where - F: for<'a> FnMut(&Decoded, &'a mut [MaybeUninit]) -> (&'a [u8], i16), -{ +#[test] +fn f32_short_sanity_test() { // 0.0999999940395355224609375 // 0.100000001490116119384765625 // 0.10000000894069671630859375 - check_shortest!(f(0.1f32) => b"1", 0); + check_short!(0.1f32 => b"1", 0); // 0.333333313465118408203125 // 0.3333333432674407958984375 (1/3 in the default rounding) // 0.33333337306976318359375 - check_shortest!(f(1.0f32/3.0) => b"33333334", 0); + check_short!(1.0f32/3.0 => b"33333334", 0); // 10^1 * 0.31415917873382568359375 // 10^1 * 0.31415920257568359375 // 10^1 * 0.31415922641754150390625 - check_shortest!(f(3.141592f32) => b"3141592", 1); + check_short!(3.141592f32 => b"3141592", 1); // 10^18 * 0.31415916243714048 // 10^18 * 0.314159196796878848 // 10^18 * 0.314159231156617216 - check_shortest!(f(3.141592e17f32) => b"3141592", 18); + check_short!(3.141592e17f32 => b"3141592", 18); // regression test for decoders // 10^8 * 0.3355443 // 10^8 * 0.33554432 // 10^8 * 0.33554436 - check_shortest!(f(ldexp_f32(1.0, 25)) => b"33554432", 8); + check_short!(ldexp_f32(1.0, 25) => b"33554432", 8); // 10^39 * 0.340282326356119256160033759537265639424 // 10^39 * 0.34028234663852885981170418348451692544 // 10^39 * 0.340282366920938463463374607431768211456 - check_shortest!(f(f32::MAX) => b"34028235", 39); + check_short!(f32::MAX => b"34028235", 39); // 10^-37 * 0.1175494210692441075487029444849287348827... // 10^-37 * 0.1175494350822287507968736537222245677818... // 10^-37 * 0.1175494490952133940450443629595204006810... - check_shortest!(f(f32::MIN_POSITIVE) => b"11754944", -37); + check_short!(f32::MIN_POSITIVE => b"11754944", -37); // 10^-44 * 0 // 10^-44 * 0.1401298464324817070923729583289916131280... // 10^-44 * 0.2802596928649634141847459166579832262560... - let minf32 = ldexp_f32(1.0, -149); - check_shortest!(f(minf32) => b"1", -44); + check_short!(ldexp_f32(1.0, -149) => b"1", -44); } -pub fn f32_exact_sanity_test(mut f: F) -where - F: for<'a> FnMut(&Decoded, &'a mut [MaybeUninit], i16) -> (&'a [u8], i16), -{ - let minf32 = ldexp_f32(1.0, -149); - - check_exact!(f(0.1f32) => b"100000001490116119384765625 ", 0); - check_exact!(f(0.5f32) => b"5 ", 0); - check_exact!(f(1.0f32/3.0) => b"3333333432674407958984375 ", 0); - check_exact!(f(3.141592f32) => b"31415920257568359375 ", 1); - check_exact!(f(3.141592e17f32) => b"314159196796878848 ", 18); - check_exact!(f(f32::MAX) => b"34028234663852885981170418348451692544 ", 39); - check_exact!(f(f32::MIN_POSITIVE) => b"1175494350822287507968736537222245677818", -37); - check_exact!(f(minf32) => b"1401298464324817070923729583289916131280", -44); +#[test] +fn f32_fixed_sanity_test() { + check_fixed_mix!(0.1f32 => b"100000001490116119384765625=", 0); + check_fixed_mix!(0.5f32 => b"5=", 0); + check_fixed_mix!(1.0f32/3.0 => b"3333333432674407958984375=", 0); + check_fixed_mix!(3.141592f32 => b"31415920257568359375=", 1); + check_fixed_mix!(3.141592e17f32 => b"314159196796878848=", 18); + check_fixed_mix!(f32::MAX => b"34028234663852885981170418348451692544=", 39); + check_fixed_mix!(f32::MIN_POSITIVE => b"1175494350822287507968736537222245677819", -37); + check_fixed_mix!(ldexp_f32(1.0, -149) => b"1401298464324817070923729583289916131280", -44); // [1], Table 16: Stress Inputs for Converting 24-bit Binary to Decimal, < 1/2 ULP - check_exact_one!(f(12676506, -102; f32) => b"2", -23); - check_exact_one!(f(12676506, -103; f32) => b"12", -23); - check_exact_one!(f(15445013, 86; f32) => b"119", 34); - check_exact_one!(f(13734123, -138; f32) => b"3941", -34); - check_exact_one!(f(12428269, -130; f32) => b"91308", -32); - check_exact_one!(f(15334037, -146; f32) => b"171900", -36); - check_exact_one!(f(11518287, -41; f32) => b"5237910", -5); - check_exact_one!(f(12584953, -145; f32) => b"28216440", -36); - check_exact_one!(f(15961084, -125; f32) => b"375243281", -30); - check_exact_one!(f(14915817, -146; f32) => b"1672120916", -36); - check_exact_one!(f(10845484, -102; f32) => b"21388945814", -23); - check_exact_one!(f(16431059, -61; f32) => b"712583594561", -11); + check_coef_pow2!(12676506_f32, -102_f32 => b"2", -23); + check_coef_pow2!(12676506_f32, -103_f32 => b"12", -23); + check_coef_pow2!(15445013_f32, 86_f32 => b"119", 34); + check_coef_pow2!(13734123_f32, -138_f32 => b"3941", -34); + check_coef_pow2!(12428269_f32, -130_f32 => b"91308", -32); + check_coef_pow2!(15334037_f32, -146_f32 => b"171900", -36); + check_coef_pow2!(11518287_f32, -41_f32 => b"5237910", -5); + check_coef_pow2!(12584953_f32, -145_f32 => b"28216440", -36); + check_coef_pow2!(15961084_f32, -125_f32 => b"375243281", -30); + check_coef_pow2!(14915817_f32, -146_f32 => b"1672120916", -36); + check_coef_pow2!(10845484_f32, -102_f32 => b"21388945814", -23); + check_coef_pow2!(16431059_f32, -61_f32 => b"712583594561", -11); // [1], Table 17: Stress Inputs for Converting 24-bit Binary to Decimal, > 1/2 ULP - check_exact_one!(f(16093626, 69; f32) => b"1", 29); - check_exact_one!(f( 9983778, 25; f32) => b"34", 15); - check_exact_one!(f(12745034, 104; f32) => b"259", 39); - check_exact_one!(f(12706553, 72; f32) => b"6001", 29); - check_exact_one!(f(11005028, 45; f32) => b"38721", 21); - check_exact_one!(f(15059547, 71; f32) => b"355584", 29); - check_exact_one!(f(16015691, -99; f32) => b"2526831", -22); - check_exact_one!(f( 8667859, 56; f32) => b"62458507", 24); - check_exact_one!(f(14855922, -82; f32) => b"307213267", -17); - check_exact_one!(f(14855922, -83; f32) => b"1536066333", -17); - check_exact_one!(f(10144164, -110; f32) => b"78147796834", -26); - check_exact_one!(f(13248074, 95; f32) => b"524810279937", 36); + check_coef_pow2!(16093626_f32, 69_f32 => b"1", 29); + check_coef_pow2!( 9983778_f32, 25_f32 => b"34", 15); + check_coef_pow2!(12745034_f32, 104_f32 => b"259", 39); + check_coef_pow2!(12706553_f32, 72_f32 => b"6001", 29); + check_coef_pow2!(11005028_f32, 45_f32 => b"38721", 21); + check_coef_pow2!(15059547_f32, 71_f32 => b"355584", 29); + check_coef_pow2!(16015691_f32, -99_f32 => b"2526831", -22); + check_coef_pow2!( 8667859_f32, 56_f32 => b"62458507", 24); + check_coef_pow2!(14855922_f32, -82_f32 => b"307213267", -17); + check_coef_pow2!(14855922_f32, -83_f32 => b"1536066333", -17); + check_coef_pow2!(10144164_f32, -110_f32 => b"78147796834", -26); + check_coef_pow2!(13248074_f32, 95_f32 => b"524810279937", 36); } -pub fn f64_shortest_sanity_test(mut f: F) -where - F: for<'a> FnMut(&Decoded, &'a mut [MaybeUninit]) -> (&'a [u8], i16), -{ +#[test] +fn f64_short_sanity_test() { // 0.0999999999999999777955395074968691915273... // 0.1000000000000000055511151231257827021181... // 0.1000000000000000333066907387546962127089... - check_shortest!(f(0.1f64) => b"1", 0); + check_short!(0.1f64 => b"1", 0); // this example is explicitly mentioned in the paper. // 10^3 * 0.0999999999999999857891452847979962825775... // 10^3 * 0.1 (exact) // 10^3 * 0.1000000000000000142108547152020037174224... - check_shortest!(f(100.0f64) => b"1", 3); + check_short!(100.0f64 => b"1", 3); // 0.3333333333333332593184650249895639717578... // 0.3333333333333333148296162562473909929394... (1/3 in the default rounding) // 0.3333333333333333703407674875052180141210... - check_shortest!(f(1.0f64/3.0) => b"3333333333333333", 0); + check_short!(1.0f64/3.0 => b"3333333333333333", 0); // explicit test case for equally closest representations. // Dragon has its own tie-breaking rule; Grisu should fall back. // 10^1 * 0.1000007629394531027955395074968691915273... // 10^1 * 0.100000762939453125 (exact) // 10^1 * 0.1000007629394531472044604925031308084726... - check_shortest!(f(1.00000762939453125f64) => b"10000076293945313", 1); + check_short!(1.00000762939453125f64 => b"10000076293945313", 1); // 10^1 * 0.3141591999999999718085064159822650253772... // 10^1 * 0.3141592000000000162174274009885266423225... // 10^1 * 0.3141592000000000606263483859947882592678... - check_shortest!(f(3.141592f64) => b"3141592", 1); + check_short!(3.141592f64 => b"3141592", 1); // 10^18 * 0.314159199999999936 // 10^18 * 0.3141592 (exact) // 10^18 * 0.314159200000000064 - check_shortest!(f(3.141592e17f64) => b"3141592", 18); + check_short!(3.141592e17f64 => b"3141592", 18); // regression test for decoders // 10^20 * 0.18446744073709549568 // 10^20 * 0.18446744073709551616 // 10^20 * 0.18446744073709555712 - check_shortest!(f(ldexp_f64(1.0, 64)) => b"18446744073709552", 20); + check_short!(ldexp_f64(1.0, 64) => b"18446744073709552", 20); // pathological case: high = 10^23 (exact). tie breaking should always prefer that. // 10^24 * 0.099999999999999974834176 // 10^24 * 0.099999999999999991611392 // 10^24 * 0.100000000000000008388608 - check_shortest!(f(1.0e23f64) => b"1", 24); + check_short!(1.0e23f64 => b"1", 24); // 10^309 * 0.1797693134862315508561243283845062402343... // 10^309 * 0.1797693134862315708145274237317043567980... // 10^309 * 0.1797693134862315907729305190789024733617... - check_shortest!(f(f64::MAX) => b"17976931348623157", 309); + check_short!(f64::MAX => b"17976931348623157", 309); // 10^-307 * 0.2225073858507200889024586876085859887650... // 10^-307 * 0.2225073858507201383090232717332404064219... // 10^-307 * 0.2225073858507201877155878558578948240788... - check_shortest!(f(f64::MIN_POSITIVE) => b"22250738585072014", -307); + check_short!(f64::MIN_POSITIVE => b"22250738585072014", -307); // 10^-323 * 0 // 10^-323 * 0.4940656458412465441765687928682213723650... // 10^-323 * 0.9881312916824930883531375857364427447301... - let minf64 = ldexp_f64(1.0, -1074); - check_shortest!(f(minf64) => b"5", -323); + check_short!(ldexp_f64(1.0, -1074) => b"5", -323); } -pub fn f64_exact_sanity_test(mut f: F) -where - F: for<'a> FnMut(&Decoded, &'a mut [MaybeUninit], i16) -> (&'a [u8], i16), -{ - let minf64 = ldexp_f64(1.0, -1074); - - check_exact!(f(0.1f64) => b"1000000000000000055511151231257827021181", 0); - check_exact!(f(0.45f64) => b"4500000000000000111022302462515654042363", 0); - check_exact!(f(0.5f64) => b"5 ", 0); - check_exact!(f(0.95f64) => b"9499999999999999555910790149937383830547", 0); - check_exact!(f(100.0f64) => b"1 ", 3); - check_exact!(f(999.5f64) => b"9995000000000000000000000000000000000000", 3); - check_exact!(f(1.0f64/3.0) => b"3333333333333333148296162562473909929394", 0); - check_exact!(f(3.141592f64) => b"3141592000000000162174274009885266423225", 1); - check_exact!(f(3.141592e17f64) => b"3141592 ", 18); - check_exact!(f(1.0e23f64) => b"99999999999999991611392 ", 23); - check_exact!(f(f64::MAX) => b"1797693134862315708145274237317043567980", 309); - check_exact!(f(f64::MIN_POSITIVE) => b"2225073858507201383090232717332404064219", -307); - check_exact!(f(minf64) => b"4940656458412465441765687928682213723650\ - 5980261432476442558568250067550727020875\ - 1865299836361635992379796564695445717730\ - 9266567103559397963987747960107818781263\ - 0071319031140452784581716784898210368871\ - 8636056998730723050006387409153564984387\ - 3124733972731696151400317153853980741262\ - 3856559117102665855668676818703956031062\ - 4931945271591492455329305456544401127480\ - 1297099995419319894090804165633245247571\ - 4786901472678015935523861155013480352649\ - 3472019379026810710749170333222684475333\ - 5720832431936092382893458368060106011506\ - 1698097530783422773183292479049825247307\ - 7637592724787465608477820373446969953364\ - 7017972677717585125660551199131504891101\ - 4510378627381672509558373897335989936648\ - 0994116420570263709027924276754456522908\ - 7538682506419718265533447265625 ", -323); +/// This test ends up running what I can only assume is some corner-ish case +/// of the `exp2` library function, defined in whatever C runtime we're +/// using. In VS 2013 this function apparently had a bug as this test fails +/// when linked, but with VS 2015 the bug appears fixed as the test runs just +/// fine. +/// +/// The bug seems to be a difference in return value of `exp2(-1057)`, where +/// in VS 2013 it returns a double with the bit pattern 0x2 and in VS 2015 it +/// returns 0x20000. +/// +/// For now just ignore this test entirely on MSVC as it's tested elsewhere +/// anyway and we're not super interested in testing each platform's exp2 +/// implementation. +#[test] +#[cfg_attr(all(not(target_env = "msvc"), miri), ignore)] // Miri is too slow +fn f64_fixed_sanity_test() { + check_fixed_mix!(0.1f64 => b"1000000000000000055511151231257827021182", 0); + check_fixed_mix!(0.45f64 => b"4500000000000000111022302462515654042363", 0); + check_fixed_mix!(0.5f64 => b"5=", 0); + check_fixed_mix!(0.95f64 => b"9499999999999999555910790149937383830547", 0); + check_fixed_mix!(100.0f64 => b"1=", 3); + check_fixed_mix!(999.5f64 => b"9995000000000000000000000000000000000000", 3); + check_fixed_mix!(1.0f64/3.0 => b"3333333333333333148296162562473909929395", 0); + check_fixed_mix!(3.141592f64 => b"3141592000000000162174274009885266423225", 1); + check_fixed_mix!(3.141592e17f64 => b"3141592=", 18); + check_fixed_mix!(1.0e23f64 => b"99999999999999991611392=", 23); + check_fixed_mix!(f64::MAX => b"1797693134862315708145274237317043567981", 309); + check_fixed_mix!(f64::MIN_POSITIVE => b"2225073858507201383090232717332404064219", -307); + check_fixed_mix!(ldexp_f64(1.0, -1074) => b"4940656458412465441765687928682213723650\ + 5980261432476442558568250067550727020875\ + 1865299836361635992379796564695445717730\ + 9266567103559397963987747960107818781263\ + 0071319031140452784581716784898210368871\ + 8636056998730723050006387409153564984387\ + 3124733972731696151400317153853980741262\ + 3856559117102665855668676818703956031062\ + 4931945271591492455329305456544401127480\ + 1297099995419319894090804165633245247571\ + 4786901472678015935523861155013480352649\ + 3472019379026810710749170333222684475333\ + 5720832431936092382893458368060106011506\ + 1698097530783422773183292479049825247307\ + 7637592724787465608477820373446969953364\ + 7017972677717585125660551199131504891101\ + 4510378627381672509558373897335989936648\ + 0994116420570263709027924276754456522908\ + 7538682506419718265533447265625=", -323); // [1], Table 3: Stress Inputs for Converting 53-bit Binary to Decimal, < 1/2 ULP - check_exact_one!(f(8511030020275656, -342; f64) => b"9", -87); - check_exact_one!(f(5201988407066741, -824; f64) => b"46", -232); - check_exact_one!(f(6406892948269899, 237; f64) => b"141", 88); - check_exact_one!(f(8431154198732492, 72; f64) => b"3981", 38); - check_exact_one!(f(6475049196144587, 99; f64) => b"41040", 46); - check_exact_one!(f(8274307542972842, 726; f64) => b"292084", 235); - check_exact_one!(f(5381065484265332, -456; f64) => b"2891946", -121); - check_exact_one!(f(6761728585499734, -1057; f64) => b"43787718", -302); - check_exact_one!(f(7976538478610756, 376; f64) => b"122770163", 130); - check_exact_one!(f(5982403858958067, 377; f64) => b"1841552452", 130); - check_exact_one!(f(5536995190630837, 93; f64) => b"54835744350", 44); - check_exact_one!(f(7225450889282194, 710; f64) => b"389190181146", 230); - check_exact_one!(f(7225450889282194, 709; f64) => b"1945950905732", 230); - check_exact_one!(f(8703372741147379, 117; f64) => b"14460958381605", 52); - check_exact_one!(f(8944262675275217, -1001; f64) => b"417367747458531", -285); - check_exact_one!(f(7459803696087692, -707; f64) => b"1107950772878888", -196); - check_exact_one!(f(6080469016670379, -381; f64) => b"12345501366327440", -98); - check_exact_one!(f(8385515147034757, 721; f64) => b"925031711960365024", 233); - check_exact_one!(f(7514216811389786, -828; f64) => b"4198047150284889840", -233); - check_exact_one!(f(8397297803260511, -345; f64) => b"11716315319786511046", -87); - check_exact_one!(f(6733459239310543, 202; f64) => b"432810072844612493629", 77); - check_exact_one!(f(8091450587292794, -473; f64) => b"3317710118160031081518", -126); + check_coef_pow2!(8511030020275656_f64, -342_f64 => b"9", -87); + check_coef_pow2!(5201988407066741_f64, -824_f64 => b"46", -232); + check_coef_pow2!(6406892948269899_f64, 237_f64 => b"141", 88); + check_coef_pow2!(8431154198732492_f64, 72_f64 => b"3981", 38); + check_coef_pow2!(6475049196144587_f64, 99_f64 => b"41040", 46); + check_coef_pow2!(8274307542972842_f64, 726_f64 => b"292084", 235); + check_coef_pow2!(5381065484265332_f64, -456_f64 => b"2891946", -121); + check_coef_pow2!(6761728585499734_f64, -1057_f64 => b"43787718", -302); + check_coef_pow2!(7976538478610756_f64, 376_f64 => b"122770163", 130); + check_coef_pow2!(5982403858958067_f64, 377_f64 => b"1841552452", 130); + check_coef_pow2!(5536995190630837_f64, 93_f64 => b"54835744350", 44); + check_coef_pow2!(7225450889282194_f64, 710_f64 => b"389190181146", 230); + check_coef_pow2!(7225450889282194_f64, 709_f64 => b"1945950905732", 230); + check_coef_pow2!(8703372741147379_f64, 117_f64 => b"14460958381605", 52); + check_coef_pow2!(8944262675275217_f64, -1001_f64 => b"417367747458531", -285); + check_coef_pow2!(7459803696087692_f64, -707_f64 => b"1107950772878888", -196); + check_coef_pow2!(6080469016670379_f64, -381_f64 => b"12345501366327440", -98); + check_coef_pow2!(8385515147034757_f64, 721_f64 => b"925031711960365024", 233); + check_coef_pow2!(7514216811389786_f64, -828_f64 => b"4198047150284889840", -233); + check_coef_pow2!(8397297803260511_f64, -345_f64 => b"11716315319786511046", -87); + check_coef_pow2!(6733459239310543_f64, 202_f64 => b"432810072844612493629", 77); + check_coef_pow2!(8091450587292794_f64, -473_f64 => b"3317710118160031081518", -126); // [1], Table 4: Stress Inputs for Converting 53-bit Binary to Decimal, > 1/2 ULP - check_exact_one!(f(6567258882077402, 952; f64) => b"3", 303); - check_exact_one!(f(6712731423444934, 535; f64) => b"76", 177); - check_exact_one!(f(6712731423444934, 534; f64) => b"378", 177); - check_exact_one!(f(5298405411573037, -957; f64) => b"4350", -272); - check_exact_one!(f(5137311167659507, -144; f64) => b"23037", -27); - check_exact_one!(f(6722280709661868, 363; f64) => b"126301", 126); - check_exact_one!(f(5344436398034927, -169; f64) => b"7142211", -35); - check_exact_one!(f(8369123604277281, -853; f64) => b"13934574", -240); - check_exact_one!(f(8995822108487663, -780; f64) => b"141463449", -218); - check_exact_one!(f(8942832835564782, -383; f64) => b"4539277920", -99); - check_exact_one!(f(8942832835564782, -384; f64) => b"22696389598", -99); - check_exact_one!(f(8942832835564782, -385; f64) => b"113481947988", -99); - check_exact_one!(f(6965949469487146, -249; f64) => b"7700366561890", -59); - check_exact_one!(f(6965949469487146, -250; f64) => b"38501832809448", -59); - check_exact_one!(f(6965949469487146, -251; f64) => b"192509164047238", -59); - check_exact_one!(f(7487252720986826, 548; f64) => b"6898586531774201", 181); - check_exact_one!(f(5592117679628511, 164; f64) => b"13076622631878654", 66); - check_exact_one!(f(8887055249355788, 665; f64) => b"136052020756121240", 217); - check_exact_one!(f(6994187472632449, 690; f64) => b"3592810217475959676", 224); - check_exact_one!(f(8797576579012143, 588; f64) => b"89125197712484551899", 193); - check_exact_one!(f(7363326733505337, 272; f64) => b"558769757362301140950", 98); - check_exact_one!(f(8549497411294502, -448; f64) => b"1176257830728540379990", -118); + check_coef_pow2!(6567258882077402_f64, 952_f64 => b"3", 303); + check_coef_pow2!(6712731423444934_f64, 535_f64 => b"76", 177); + check_coef_pow2!(6712731423444934_f64, 534_f64 => b"378", 177); + check_coef_pow2!(5298405411573037_f64, -957_f64 => b"4350", -272); + check_coef_pow2!(5137311167659507_f64, -144_f64 => b"23037", -27); + check_coef_pow2!(6722280709661868_f64, 363_f64 => b"126301", 126); + check_coef_pow2!(5344436398034927_f64, -169_f64 => b"7142211", -35); + check_coef_pow2!(8369123604277281_f64, -853_f64 => b"13934574", -240); + check_coef_pow2!(8995822108487663_f64, -780_f64 => b"141463449", -218); + check_coef_pow2!(8942832835564782_f64, -383_f64 => b"4539277920", -99); + check_coef_pow2!(8942832835564782_f64, -384_f64 => b"22696389598", -99); + check_coef_pow2!(8942832835564782_f64, -385_f64 => b"113481947988", -99); + check_coef_pow2!(6965949469487146_f64, -249_f64 => b"7700366561890", -59); + check_coef_pow2!(6965949469487146_f64, -250_f64 => b"38501832809448", -59); + check_coef_pow2!(6965949469487146_f64, -251_f64 => b"192509164047238", -59); + check_coef_pow2!(7487252720986826_f64, 548_f64 => b"6898586531774201", 181); + check_coef_pow2!(5592117679628511_f64, 164_f64 => b"13076622631878654", 66); + check_coef_pow2!(8887055249355788_f64, 665_f64 => b"136052020756121240", 217); + check_coef_pow2!(6994187472632449_f64, 690_f64 => b"3592810217475959676", 224); + check_coef_pow2!(8797576579012143_f64, 588_f64 => b"89125197712484551899", 193); + check_coef_pow2!(7363326733505337_f64, 272_f64 => b"558769757362301140950", 98); + check_coef_pow2!(8549497411294502_f64, -448_f64 => b"1176257830728540379990", -118); } -pub fn more_shortest_sanity_test(mut f: F) -where - F: for<'a> FnMut(&Decoded, &'a mut [MaybeUninit]) -> (&'a [u8], i16), -{ - check_shortest!(f{mant: 99_999_999_999_999_999, minus: 1, plus: 1, - exp: 0, inclusive: true} => b"1", 18); - check_shortest!(f{mant: 99_999_999_999_999_999, minus: 1, plus: 1, - exp: 0, inclusive: false} => b"99999999999999999", 17); +#[test] +fn more_short_sanity_test() { + let mut buf = [MaybeUninit::new(b'_'); MAX_SIG_DIGITS]; + assert_eq!( + flt2dec::format_short( + &Decoded { mant: 99_999_999_999_999_999, minus: 1, plus: 1, exp: 0, inclusive: true }, + &mut buf, + ), + ("1".as_bytes(), 18), + ); + assert_eq!( + flt2dec::format_short( + &Decoded { mant: 99_999_999_999_999_999, minus: 1, plus: 1, exp: 0, inclusive: false }, + &mut buf, + ), + ("99999999999999999".as_bytes(), 17), + ); } fn to_string_with_parts(mut f: F) -> String where F: for<'a> FnMut(&'a mut [MaybeUninit], &'a mut [MaybeUninit>]) -> Formatted<'a>, { - let mut buf = [MaybeUninit::new(0); 1024]; + let mut buf = LARGE_TEST_BUF; let mut parts = [MaybeUninit::new(Part::Zero(0)); 16]; let formatted = f(&mut buf, &mut parts); let mut ret = vec![0; formatted.len()]; diff --git a/library/coretests/tests/num/flt2dec/strategy/dragon.rs b/library/coretests/tests/num/flt2dec/strategy/dragon.rs index 42e2fcfade181..c335512a7f13b 100644 --- a/library/coretests/tests/num/flt2dec/strategy/dragon.rs +++ b/library/coretests/tests/num/flt2dec/strategy/dragon.rs @@ -14,40 +14,6 @@ fn test_mul_pow10() { } } -#[test] -fn shortest_sanity_test() { - f64_shortest_sanity_test(format_short); - f32_shortest_sanity_test(format_short); - #[cfg(target_has_reliable_f16)] - f16_shortest_sanity_test(format_short); - more_shortest_sanity_test(format_short); -} - -#[test] -#[cfg_attr(miri, ignore)] // Miri is too slow -fn exact_sanity_test() { - // This test ends up running what I can only assume is some corner-ish case - // of the `exp2` library function, defined in whatever C runtime we're - // using. In VS 2013 this function apparently had a bug as this test fails - // when linked, but with VS 2015 the bug appears fixed as the test runs just - // fine. - // - // The bug seems to be a difference in return value of `exp2(-1057)`, where - // in VS 2013 it returns a double with the bit pattern 0x2 and in VS 2015 it - // returns 0x20000. - // - // For now just ignore this test entirely on MSVC as it's tested elsewhere - // anyway and we're not super interested in testing each platform's exp2 - // implementation. - if !cfg!(target_env = "msvc") { - f64_exact_sanity_test(format_fixed); - } - f32_exact_sanity_test(format_fixed); - - #[cfg(target_has_reliable_f16)] - f16_exact_sanity_test(format_fixed); -} - #[test] fn test_to_shortest_str() { to_shortest_str_test(format_short); diff --git a/library/coretests/tests/num/flt2dec/strategy/grisu.rs b/library/coretests/tests/num/flt2dec/strategy/grisu.rs index b65cd0da2b741..aae45d6df11d8 100644 --- a/library/coretests/tests/num/flt2dec/strategy/grisu.rs +++ b/library/coretests/tests/num/flt2dec/strategy/grisu.rs @@ -35,28 +35,6 @@ fn test_max_pow10_no_more_than() { } } -#[test] -fn shortest_sanity_test() { - f64_shortest_sanity_test(flt2dec::format_short); - f32_shortest_sanity_test(flt2dec::format_short); - #[cfg(target_has_reliable_f16)] - f16_shortest_sanity_test(flt2dec::format_short); - more_shortest_sanity_test(flt2dec::format_short); -} - -#[test] -#[cfg_attr(miri, ignore)] // Miri is too slow -fn exact_sanity_test() { - // See comments in dragon.rs's exact_sanity_test for why this test is - // ignored on MSVC - if !cfg!(target_env = "msvc") { - f64_exact_sanity_test(flt2dec::format_fixed); - } - f32_exact_sanity_test(flt2dec::format_fixed); - #[cfg(target_has_reliable_f16)] - f16_exact_sanity_test(flt2dec::format_fixed); -} - #[test] fn test_to_shortest_str() { to_shortest_str_test(flt2dec::format_short); From 49d1c1c3bc1a642db8fde409e09c06bc2a02267f Mon Sep 17 00:00:00 2001 From: "Pascal S. de Kloe" Date: Fri, 27 Mar 2026 16:30:09 +0100 Subject: [PATCH 4/6] typo in test value from round-to-even in fmt Inverted assertion order. It's not the value of exp which steers the branche. The concern at hand is not having any digits (i.e, an empty buffer). The reason for not having digits can be asserted, as an optional double check, just to be more sure we ended up without digits for the correct reason. Both the current and the correct value are equivalent when rounded to three decimals. --- library/core/src/num/imp/flt2dec/mod.rs | 11 ++++++----- library/coretests/tests/fmt/float.rs | 12 ++++++++---- library/coretests/tests/num/flt2dec/mod.rs | 12 +++++------- 3 files changed, 19 insertions(+), 16 deletions(-) diff --git a/library/core/src/num/imp/flt2dec/mod.rs b/library/core/src/num/imp/flt2dec/mod.rs index 1efbb3c214c38..3a84d09071a78 100644 --- a/library/core/src/num/imp/flt2dec/mod.rs +++ b/library/core/src/num/imp/flt2dec/mod.rs @@ -186,6 +186,9 @@ pub const UNLIMITED_RESOLUTION: i16 = i16::MIN; /// integer multiple of `10^resolution`. For example, a resolution of `-3` /// causes rounding to three decimal places, i.e., values are multiples of /// `0.001`. Use of [`UNLIMITED_RESOLUTION`] can get expensive. +/// +/// Note: If the resolution causes the value to round to zero, then the returned +/// digit slice is empty. This preserves the invariant d₀ ≠ 0. pub fn format_fixed<'a>( d: &Decoded, buf: &'a mut [MaybeUninit], @@ -714,11 +717,9 @@ where let limit = if frac_digits < 0x8000 { -(frac_digits as i16) } else { UNLIMITED_RESOLUTION }; let (buf, exp) = format_exact(decoded, &mut buf[..maxlen], limit); - if exp <= limit { - // the restriction couldn't been met, so this should render like zero no matter - // `exp` was. this does not include the case that the restriction has been met - // only after the final rounding-up; it's a regular case with `exp = limit + 1`. - debug_assert_eq!(buf.len(), 0); + if buf.len() == 0 { + // The number rounds down to zero at the given resolution. + debug_assert!(exp <= limit); if frac_digits > 0 { // [0.][0000] parts[0] = MaybeUninit::new(Part::Copy(b"0.")); diff --git a/library/coretests/tests/fmt/float.rs b/library/coretests/tests/fmt/float.rs index 4f7357761744b..18225363617f1 100644 --- a/library/coretests/tests/fmt/float.rs +++ b/library/coretests/tests/fmt/float.rs @@ -28,6 +28,8 @@ fn test_format_f64() { #[test] fn test_format_f64_rounds_ties_to_even() { + // Use only values exactly representable in IEEE floating-point, i.e., + // multiples of 1/2^n. assert_eq!("0", format!("{:.0}", 0.5f64)); assert_eq!("2", format!("{:.0}", 1.5f64)); assert_eq!("2", format!("{:.0}", 2.5f64)); @@ -40,7 +42,7 @@ fn test_format_f64_rounds_ties_to_even() { assert_eq!("0.8", format!("{:.1}", 0.75f64)); assert_eq!("0.12", format!("{:.2}", 0.125f64)); assert_eq!("0.88", format!("{:.2}", 0.875f64)); - assert_eq!("0.062", format!("{:.3}", 0.062f64)); + assert_eq!("0.062", format!("{:.3}", 0.0625f64)); assert_eq!("-0", format!("{:.0}", -0.5f64)); assert_eq!("-2", format!("{:.0}", -1.5f64)); assert_eq!("-2", format!("{:.0}", -2.5f64)); @@ -53,7 +55,7 @@ fn test_format_f64_rounds_ties_to_even() { assert_eq!("-0.8", format!("{:.1}", -0.75f64)); assert_eq!("-0.12", format!("{:.2}", -0.125f64)); assert_eq!("-0.88", format!("{:.2}", -0.875f64)); - assert_eq!("-0.062", format!("{:.3}", -0.062f64)); + assert_eq!("-0.062", format!("{:.3}", -0.0625f64)); assert_eq!("2e0", format!("{:.0e}", 1.5f64)); assert_eq!("2e0", format!("{:.0e}", 2.5f64)); @@ -114,6 +116,8 @@ fn test_format_f32() { #[test] fn test_format_f32_rounds_ties_to_even() { + // Use only values exactly representable in IEEE floating-point, i.e., + // multiples of 1/2^n. assert_eq!("0", format!("{:.0}", 0.5f32)); assert_eq!("2", format!("{:.0}", 1.5f32)); assert_eq!("2", format!("{:.0}", 2.5f32)); @@ -126,7 +130,7 @@ fn test_format_f32_rounds_ties_to_even() { assert_eq!("0.8", format!("{:.1}", 0.75f32)); assert_eq!("0.12", format!("{:.2}", 0.125f32)); assert_eq!("0.88", format!("{:.2}", 0.875f32)); - assert_eq!("0.062", format!("{:.3}", 0.062f32)); + assert_eq!("0.062", format!("{:.3}", 0.0625f32)); assert_eq!("-0", format!("{:.0}", -0.5f32)); assert_eq!("-2", format!("{:.0}", -1.5f32)); assert_eq!("-2", format!("{:.0}", -2.5f32)); @@ -139,7 +143,7 @@ fn test_format_f32_rounds_ties_to_even() { assert_eq!("-0.8", format!("{:.1}", -0.75f32)); assert_eq!("-0.12", format!("{:.2}", -0.125f32)); assert_eq!("-0.88", format!("{:.2}", -0.875f32)); - assert_eq!("-0.062", format!("{:.3}", -0.062f32)); + assert_eq!("-0.062", format!("{:.3}", -0.0625f32)); assert_eq!("2e0", format!("{:.0e}", 1.5f32)); assert_eq!("2e0", format!("{:.0e}", 2.5f32)); diff --git a/library/coretests/tests/num/flt2dec/mod.rs b/library/coretests/tests/num/flt2dec/mod.rs index 4d5791fe2fc10..24475de6544f0 100644 --- a/library/coretests/tests/num/flt2dec/mod.rs +++ b/library/coretests/tests/num/flt2dec/mod.rs @@ -125,15 +125,13 @@ macro_rules! check_fixed_mix { check_fixed_resolution!($v, want_pow10 - i as i16 => &want_buf[..i], want_pow10); } - // check exact rounding for zero- and negative-width cases - let start; - if digits[0] > b'5' { + // Verify digit loss by resolution. + let skip_first = digits[0] > b'5'; + if skip_first { check_fixed_resolution!($v, pow10 => b"1", pow10 + 1); - start = 1; - } else { - start = 0; } - for i in start..-10 { + for i in i16::from(skip_first)..-10 { + // Digit loss must still maintain the exponent. check_fixed_resolution!($v, pow10 - i => b"", pow10); } From 99c4dff954e4ce745bfbe02698fd9158a641df44 Mon Sep 17 00:00:00 2001 From: "Pascal S. de Kloe" Date: Tue, 31 Mar 2026 12:07:43 +0200 Subject: [PATCH 5/6] flt2dec/random.rs renamed to flt2dec/equiv.rs --- library/coretests/tests/num/flt2dec/{random.rs => equiv.rs} | 0 library/coretests/tests/num/flt2dec/mod.rs | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename library/coretests/tests/num/flt2dec/{random.rs => equiv.rs} (100%) diff --git a/library/coretests/tests/num/flt2dec/random.rs b/library/coretests/tests/num/flt2dec/equiv.rs similarity index 100% rename from library/coretests/tests/num/flt2dec/random.rs rename to library/coretests/tests/num/flt2dec/equiv.rs diff --git a/library/coretests/tests/num/flt2dec/mod.rs b/library/coretests/tests/num/flt2dec/mod.rs index 24475de6544f0..e7a6b0efdf9c0 100644 --- a/library/coretests/tests/num/flt2dec/mod.rs +++ b/library/coretests/tests/num/flt2dec/mod.rs @@ -11,12 +11,12 @@ use Sign::{Minus, MinusPlus}; use crate::num::{ldexp_f32, ldexp_f64}; +mod equiv; mod estimator; mod strategy { mod dragon; mod grisu; } -mod random; /// Buffer that fits enough digits to format any `f64`. const LARGE_TEST_BUF: [MaybeUninit; 1024] = [MaybeUninit::new(b'_'); 1024]; From 59d1c07a773dc1f3333b65896e60cc4d4f74047b Mon Sep 17 00:00:00 2001 From: Trevor Gross Date: Mon, 19 Oct 2026 05:19:04 +0000 Subject: [PATCH 6/6] test: Gate `f16`-specific variables behind `target_has_reliable_f16` --- library/coretests/tests/num/flt2dec/equiv.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/library/coretests/tests/num/flt2dec/equiv.rs b/library/coretests/tests/num/flt2dec/equiv.rs index 27748139b9d9e..f5a5c6b24e113 100644 --- a/library/coretests/tests/num/flt2dec/equiv.rs +++ b/library/coretests/tests/num/flt2dec/equiv.rs @@ -10,6 +10,7 @@ use rand::distr::{Distribution, Uniform}; // Bits 0u16..0x7C00 cover the positive finite-range, // with 1u16..0x7C00 for non-zero. +#[cfg(target_has_reliable_f16)] const F16_POS_FIN_RANGE: std::ops::Range = 1..f16::INFINITY.to_bits(); // Bits 0u32..0x7F80_0000 cover the positive finite-range,