From 7faff1a057538d6a29cd3430f11fbcfcfc9f19e4 Mon Sep 17 00:00:00 2001 From: Juho Kahala <57393910+quaternic@users.noreply.github.com> Date: Sun, 5 Apr 2026 20:31:01 +0300 Subject: [PATCH 1/2] libm-test: generator for varying specific bits --- libm-test/src/generate.rs | 1 + libm-test/src/generate/bitpattern.rs | 133 +++++++++++++++++++++++++++ 2 files changed, 134 insertions(+) create mode 100644 libm-test/src/generate/bitpattern.rs diff --git a/libm-test/src/generate.rs b/libm-test/src/generate.rs index da080d23f..509d6232f 100644 --- a/libm-test/src/generate.rs +++ b/libm-test/src/generate.rs @@ -1,5 +1,6 @@ //! Different generators that can create random or systematic bit patterns. +pub mod bitpattern; pub mod case_list; pub mod edge_cases; pub mod random; diff --git a/libm-test/src/generate/bitpattern.rs b/libm-test/src/generate/bitpattern.rs new file mode 100644 index 000000000..cefb711b9 --- /dev/null +++ b/libm-test/src/generate/bitpattern.rs @@ -0,0 +1,133 @@ +use libm::support::{Float, Int, MinInt}; + +/// An efficient equivalent to +/// `(0..=uN::MAX).filter(|x| x & !varying == 0).map(|x| x ^ preset)` +/// That is, "all integers that only differ from the preset in the varying bits" +pub struct BitConfig { + /// A bitmask of bits to list exhaustively + varying: I, + /// Other bits are set according to preset. + preset: I, +} + +impl> BitConfig { + fn into_iter(self) -> impl Iterator + Clone { + assert!( + self.varying != I::MAX, + "to optimize the implementation, varying every bit is not supported" + ); + let fixed = !self.varying; + let flip = self.preset ^ fixed; + let mut counter = fixed - I::ONE; + + // `(counter + 1) & !fixed` is initially 0, and increases after each item returned + std::iter::from_fn(move || { + counter = counter.checked_add(I::ONE)? | fixed; + Some(counter ^ flip) + }) + } +} + +/// Biased generator for floats. +/// +/// The returned iterator will produce `fillers.len() << bits_to_vary` items. +pub fn float_gen(bits_to_vary: u32, fillers: Vec) -> impl Iterator + Clone +where + F: Float, + F::Int: Int, +{ + assert!(bits_to_vary < F::Int::BITS); + + let mut bit_priority: Vec<_> = (0..F::BITS).rev().collect(); + // sign bit first, otherwise by least distance to any edge of a bitfield, + bit_priority[1..].sort_by_key(|&i| { + // avoid a fencepost error by mapping the bit indices to odd numbers, + // and compare them to the bitfield edges mapped to even integers + let i = 2 * i + 1; + i.min(i.abs_diff(F::SIG_BITS * 2)) + .min(i.abs_diff((F::BITS - 1) * 2)) + }); + + let varying = bit_priority[..bits_to_vary as usize] + .iter() + .map(|&i| F::Int::ONE << i) + .reduce(std::ops::BitOr::bitor) + .unwrap_or(F::Int::ZERO); + + fillers + .into_iter() + .map(move |preset| BitConfig { + preset: preset & !varying, + varying, + }) + .flat_map(BitConfig::into_iter) + .map(F::from_bits) +} + +#[cfg(test)] +mod test { + use super::{BitConfig, float_gen}; + #[test] + fn equivalence() { + // with a small integer type, we can easily verify that behaviour matches for all arguments + for varying in 0..u8::MAX { + for preset in 0..=u8::MAX { + let expect = (0..=u8::MAX) + .filter(|x| x & !varying == 0) + .map(|x| x ^ preset); + let iter = BitConfig { varying, preset }.into_iter(); + assert!(iter.eq(expect)); + } + } + } + #[test] + fn gen_includes_specials() { + let v: Vec<_> = float_gen(5, vec![0, 0x7fffff, !0x7fffff, !0]) + .map(f32::to_bits) + .collect(); + for x in &[ + 0.0, + f32::from_bits(1), + f32::MIN_POSITIVE, + f32::MAX, + f32::INFINITY, + f32::NAN, + ] { + assert!(v.contains(&x.to_bits()), "{x} not found"); + assert!(v.contains(&(-x).to_bits()), "-{x} not found"); + } + } + #[test] + fn count() { + for k in 0..10 { + assert!(float_gen::(k, vec![0]).count() == 1 << k); + assert!(float_gen::(k, vec![0, !0]).count() == 2 << k); + assert!(float_gen::(k, vec![0, 1, 2]).count() == 3 << k); + } + } + #[test] + fn specific() { + let iter = float_gen::(1, vec![0]).map(f32::to_bits); + assert!(iter.eq([0.0_f32.to_bits(), (-0.0_f32).to_bits()])); + let iter = float_gen::(1, vec![0]).map(f64::to_bits); + assert!(iter.eq([0.0_f64.to_bits(), (-0.0_f64).to_bits()])); + + let mut v: Vec<_> = float_gen::(5, vec![0]).map(f32::to_bits).collect(); + assert!(v.len() == 32); + assert!(v.is_sorted()); + v.dedup(); + assert!(v.len() == 32); + for bits in v { + assert!(bits & 0xc0c0_0001 == bits); + } + + let mut v: Vec<_> = float_gen::(5, vec![0]).map(f64::to_bits).collect(); + assert!(v.len() == 32); + assert!(v.is_sorted()); + v.dedup(); + assert!(v.len() == 32); + for bits in v { + assert!(bits & 0xc018_0000_0000_0001 == bits); + } + } +} From eca4c85885b9b2525b64429e2e171c70071a74db Mon Sep 17 00:00:00 2001 From: Juho Kahala <57393910+quaternic@users.noreply.github.com> Date: Thu, 9 Apr 2026 20:23:35 +0300 Subject: [PATCH 2/2] refactoring --- libm-test/src/generate/bitpattern.rs | 170 ++++++++++++++++++--------- libm/src/math/support/int_traits.rs | 5 + 2 files changed, 120 insertions(+), 55 deletions(-) diff --git a/libm-test/src/generate/bitpattern.rs b/libm-test/src/generate/bitpattern.rs index cefb711b9..d59912568 100644 --- a/libm-test/src/generate/bitpattern.rs +++ b/libm-test/src/generate/bitpattern.rs @@ -1,83 +1,143 @@ use libm::support::{Float, Int, MinInt}; -/// An efficient equivalent to -/// `(0..=uN::MAX).filter(|x| x & !varying == 0).map(|x| x ^ preset)` -/// That is, "all integers that only differ from the preset in the varying bits" -pub struct BitConfig { - /// A bitmask of bits to list exhaustively - varying: I, - /// Other bits are set according to preset. - preset: I, -} +/// Iterate all the bitwise subsets of the given mask. +/// +/// Produces the same sequence as `(0..=uN::MAX).filter(|x| x & !mask == 0)`, +/// but each item is generated in O(1) time. +/// +/// # Panics +/// +/// Panics if the mask has all bits set. +/// +/// # Example +/// +/// ```ignore +/// assert!(bitwise_subsets(0b1001).eq([0b0000, 0b0001, 0b1000, 0b1001])); +/// ``` +fn bitwise_subsets(mask: I) -> impl Iterator + Clone +where + I: Int, +{ + assert!( + mask != I::MAX, + "to optimize the implementation, varying every bit is not supported" + ); + let fixed = !mask; + let mut counter = fixed - I::ONE; -impl> BitConfig { - fn into_iter(self) -> impl Iterator + Clone { - assert!( - self.varying != I::MAX, - "to optimize the implementation, varying every bit is not supported" - ); - let fixed = !self.varying; - let flip = self.preset ^ fixed; - let mut counter = fixed - I::ONE; + // `(counter + 1) & !fixed` is initially 0, and increases after each item returned + std::iter::from_fn(move || { + counter = counter.checked_add(I::ONE)? | fixed; + Some(counter ^ fixed) + }) +} - // `(counter + 1) & !fixed` is initially 0, and increases after each item returned - std::iter::from_fn(move || { - counter = counter.checked_add(I::ONE)? | fixed; - Some(counter ^ flip) - }) +fn low_exp_bits(count: u32) -> F::Int { + F::EXP_MASK & (F::EXP_MASK >> (F::EXP_BITS.saturating_sub(count))) +} +fn high_exp_bits(count: u32) -> F::Int { + F::EXP_MASK & (F::EXP_MASK << (F::EXP_BITS.saturating_sub(count))) +} +fn low_sig_bits(count: u32) -> F::Int { + F::SIG_MASK & (F::SIG_MASK >> (F::SIG_BITS.saturating_sub(count))) +} +fn high_sig_bits(count: u32) -> F::Int { + F::SIG_MASK & (F::SIG_MASK << (F::SIG_BITS.saturating_sub(count))) +} +fn most_wanted_bitmask(count: u32) -> F::Int { + if count == 0 { + return F::Int::ZERO; } + let mut mask = F::SIGN_MASK; + let n = (count - 1) / 4; + mask |= low_exp_bits::(n); + mask |= high_exp_bits::(n); + mask |= high_sig_bits::(n); + // spend the remaining budget on the least significant bits + mask |= low_sig_bits::(count - mask.count_ones()); + mask } /// Biased generator for floats. /// /// The returned iterator will produce `fillers.len() << bits_to_vary` items. -pub fn float_gen(bits_to_vary: u32, fillers: Vec) -> impl Iterator + Clone +#[cfg_attr(not(test), expect(dead_code))] +fn float_gen( + bits_to_vary: u32, + fillers: impl IntoIterator, +) -> impl Iterator where F: Float, F::Int: Int, { - assert!(bits_to_vary < F::Int::BITS); - - let mut bit_priority: Vec<_> = (0..F::BITS).rev().collect(); - // sign bit first, otherwise by least distance to any edge of a bitfield, - bit_priority[1..].sort_by_key(|&i| { - // avoid a fencepost error by mapping the bit indices to odd numbers, - // and compare them to the bitfield edges mapped to even integers - let i = 2 * i + 1; - i.min(i.abs_diff(F::SIG_BITS * 2)) - .min(i.abs_diff((F::BITS - 1) * 2)) - }); - - let varying = bit_priority[..bits_to_vary as usize] - .iter() - .map(|&i| F::Int::ONE << i) - .reduce(std::ops::BitOr::bitor) - .unwrap_or(F::Int::ZERO); + let varying = most_wanted_bitmask::(bits_to_vary); + let patterns = bitwise_subsets(varying); fillers .into_iter() - .map(move |preset| BitConfig { - preset: preset & !varying, - varying, - }) - .flat_map(BitConfig::into_iter) + .flat_map(move |preset| patterns.clone().map(move |x| x ^ preset)) .map(F::from_bits) } #[cfg(test)] mod test { - use super::{BitConfig, float_gen}; + use super::{bitwise_subsets, float_gen}; #[test] fn equivalence() { // with a small integer type, we can easily verify that behaviour matches for all arguments - for varying in 0..u8::MAX { - for preset in 0..=u8::MAX { - let expect = (0..=u8::MAX) - .filter(|x| x & !varying == 0) - .map(|x| x ^ preset); - let iter = BitConfig { varying, preset }.into_iter(); - assert!(iter.eq(expect)); - } + for mask in 0..u8::MAX { + let expect = (0..=u8::MAX).filter(|x| x & !mask == 0); + assert!(bitwise_subsets(mask).eq(expect)); + } + } + + #[test] + fn most_wanted() { + let expected = [ + 0b0_00000000_00000000000000000000000, + // + 0b1_00000000_00000000000000000000000, + 0b1_00000000_00000000000000000000001, + 0b1_00000000_00000000000000000000011, + 0b1_00000000_00000000000000000000111, + // + 0b1_10000001_10000000000000000000001, + 0b1_10000001_10000000000000000000011, + 0b1_10000001_10000000000000000000111, + 0b1_10000001_10000000000000000001111, + // + 0b1_11000011_11000000000000000000011, + 0b1_11000011_11000000000000000000111, + 0b1_11000011_11000000000000000001111, + 0b1_11000011_11000000000000000011111, + // + 0b1_11100111_11100000000000000000111, + 0b1_11100111_11100000000000000001111, + 0b1_11100111_11100000000000000011111, + 0b1_11100111_11100000000000000111111, + // + 0b1_11111111_11110000000000000001111, + 0b1_11111111_11110000000000000011111, + 0b1_11111111_11110000000000000111111, + 0b1_11111111_11110000000000001111111, + // + 0b1_11111111_11111000000000001111111, + 0b1_11111111_11111000000000011111111, + 0b1_11111111_11111000000000111111111, + 0b1_11111111_11111000000001111111111, + // + 0b1_11111111_11111100000001111111111, + 0b1_11111111_11111100000011111111111, + 0b1_11111111_11111100000111111111111, + 0b1_11111111_11111100001111111111111, + // + 0b1_11111111_11111110001111111111111, + 0b1_11111111_11111110011111111111111, + 0b1_11111111_11111110111111111111111, + 0b1_11111111_11111111111111111111111, + ]; + for k in 0..=32 { + assert_eq!(super::most_wanted_bitmask::(k), expected[k as usize]); } } #[test] diff --git a/libm/src/math/support/int_traits.rs b/libm/src/math/support/int_traits.rs index f113f9d62..48c0e0438 100644 --- a/libm/src/math/support/int_traits.rs +++ b/libm/src/math/support/int_traits.rs @@ -110,6 +110,7 @@ pub trait Int: fn borrowing_sub(self, other: Self, borrow: bool) -> (Self, bool); fn leading_zeros(self) -> u32; fn trailing_zeros(self) -> u32; + fn count_ones(self) -> u32; fn ilog2(self) -> u32; } @@ -179,6 +180,10 @@ macro_rules! int_impl_common { ::trailing_zeros(self) } + fn count_ones(self) -> u32 { + ::count_ones(self) + } + fn ilog2(self) -> u32 { // On our older MSRV, this resolves to the trait method. Which won't actually work, // but this is only called behind other gates.