From 6466b504430c29de673d2283c70b672cce44b77e Mon Sep 17 00:00:00 2001 From: juncheng Date: Sat, 27 Jun 2026 03:45:38 +0000 Subject: [PATCH 1/7] feat: add leading_zeros to BitStr with SIMD-accelerated dispatch BitStr::leading_zeros() counts consecutive false bits from the start of a view. Internally delegates to BitsArith::leading_zero_words() on [u64], which uses SIMD (AVX2/SSE2/NEON) to skip over zero words in batches of 4 or 2, falling back to scalar for short inputs. Backend equivalence tests validate all SIMD paths against the scalar oracle. Co-authored-by: Claude Co-authored-by: DeepSeek AI --- src/bit_str/impls_for_bit_arith.rs | 1 + .../impls_for_leading_zeros.rs | 77 ++++++ .../tests_for_leading_zeros.rs | 149 +++++++++++ src/traits/bits_arith.rs | 7 + .../funcs_for_leading_zero_words.rs | 237 ++++++++++++++++++ .../tests_for_backend_equivalence.rs | 100 ++++++++ src/traits/bits_arith/impls_for_u64_slice.rs | 6 + 7 files changed, 577 insertions(+) create mode 100644 src/bit_str/impls_for_bit_arith/impls_for_leading_zeros.rs create mode 100644 src/bit_str/impls_for_bit_arith/impls_for_leading_zeros/tests_for_leading_zeros.rs create mode 100644 src/traits/bits_arith/funcs_for_leading_zero_words.rs create mode 100644 src/traits/bits_arith/funcs_for_leading_zero_words/tests_for_backend_equivalence.rs diff --git a/src/bit_str/impls_for_bit_arith.rs b/src/bit_str/impls_for_bit_arith.rs index e8f484b..0393966 100644 --- a/src/bit_str/impls_for_bit_arith.rs +++ b/src/bit_str/impls_for_bit_arith.rs @@ -1 +1,2 @@ mod impls_for_count_ones; +mod impls_for_leading_zeros; diff --git a/src/bit_str/impls_for_bit_arith/impls_for_leading_zeros.rs b/src/bit_str/impls_for_bit_arith/impls_for_leading_zeros.rs new file mode 100644 index 0000000..bb68f1b --- /dev/null +++ b/src/bit_str/impls_for_bit_arith/impls_for_leading_zeros.rs @@ -0,0 +1,77 @@ +use crate::traits::*; +use crate::{WORD_BITS, low_mask}; + +use crate::BitStr; + +impl<'bs> BitStr<'bs> { + /// Returns the number of consecutive `false` bits from the start of this + /// view. + /// + /// # Complexity + /// + /// O(n / 64) scalar worst case, SIMD-accelerated for long zero runs. + /// Returns early at the first non-zero word. + /// + /// # Examples + /// + /// ``` + /// use bit_string::BitString; + /// + /// let bits = BitString::try_from("00101").unwrap(); + /// let v = bits.as_bit_str(); + /// assert_eq!(v.leading_zeros(), 2); + /// ``` + #[inline] + pub fn leading_zeros(&self) -> usize { + if self.bit_len == 0 { + return 0; + } + + let words = self.source.words(); + let end = self.start + self.bit_len; + let start_offset = self.start % WORD_BITS; + let end_rem = end % WORD_BITS; + let last_wi = (end - 1) / WORD_BITS; + + let mut scanned = 0usize; + let mut wi = self.start / WORD_BITS; + + // First word — only bits from start_offset upward are in view. + let first_val = words[wi] >> start_offset; + let first_limit = (WORD_BITS - start_offset).min(self.bit_len); + let first_z = (first_val.trailing_zeros() as usize).min(first_limit); + if first_z < first_limit { + return first_z; + } + scanned += first_limit; + wi += 1; + + // Full middle words — SIMD-accelerated zero-word scan. + let mid_end = if end_rem == 0 { last_wi + 1 } else { last_wi }; + if wi < mid_end { + let zero_words = words[wi..mid_end].leading_zero_words(); + let zero_bits = zero_words * WORD_BITS; + scanned += zero_bits; + wi += zero_words; + + // If there's a non-zero word left in the middle range, count its + // trailing zeros and return early. + if wi < mid_end { + let z = (words[wi].trailing_zeros() as usize).min(WORD_BITS); + return scanned + z; + } + } + + // Last partial word (only when end_rem != 0). + if end_rem != 0 && wi == last_wi { + let last_val = words[wi] & low_mask(end_rem); + let last_z = (last_val.trailing_zeros() as usize).min(end_rem); + scanned += last_z; + } + + scanned.min(self.bit_len) + } +} + +#[cfg(test)] +mod tests_for_leading_zeros; diff --git a/src/bit_str/impls_for_bit_arith/impls_for_leading_zeros/tests_for_leading_zeros.rs b/src/bit_str/impls_for_bit_arith/impls_for_leading_zeros/tests_for_leading_zeros.rs new file mode 100644 index 0000000..f3dd482 --- /dev/null +++ b/src/bit_str/impls_for_bit_arith/impls_for_leading_zeros/tests_for_leading_zeros.rs @@ -0,0 +1,149 @@ +use int_interval::UsizeCO; + +use crate::BitString; + +/// An empty view has no leading zeros. +#[test] +fn empty_view_returns_zero() { + let bits = BitString::try_from("10110").unwrap(); + let v = bits.as_bit_str().slice(UsizeCO::try_new(10, 20).unwrap()); + assert_eq!(v.leading_zeros(), 0); +} + +/// Starts with a 1 → zero leading zeros. +#[test] +fn starts_with_one() { + let bits = BitString::try_from("10011").unwrap(); + assert_eq!(bits.as_bit_str().leading_zeros(), 0); +} + +/// Starts with several zeros. +#[test] +fn leading_zero_run() { + let bits = BitString::try_from("00101").unwrap(); + assert_eq!(bits.as_bit_str().leading_zeros(), 2); +} + +/// Single zero bit. +#[test] +fn single_zero() { + let bits = BitString::zeros(1); + assert_eq!(bits.as_bit_str().leading_zeros(), 1); +} + +/// Single one bit. +#[test] +fn single_one() { + let bits = BitString::ones(1); + assert_eq!(bits.as_bit_str().leading_zeros(), 0); +} + +/// All zeros at various lengths including word-boundary values. +#[test] +fn all_zeros_at_various_lengths() { + for len in [1, 63, 64, 65, 127, 128, 129, 130] { + let bits = BitString::zeros(len); + assert_eq!(bits.as_bit_str().leading_zeros(), len, "len={len}"); + } +} + +/// All ones at various lengths. +#[test] +fn all_ones_at_various_lengths() { + for len in [1, 63, 64, 65, 127, 128, 129, 130] { + let bits = BitString::ones(len); + assert_eq!(bits.as_bit_str().leading_zeros(), 0, "len={len}"); + } +} + +/// First 1 is deep into the second word (bit 64+). +#[test] +fn first_one_in_second_word() { + let mut bits = BitString::zeros(130); + bits.set(100, true); + assert_eq!(bits.as_bit_str().leading_zeros(), 100); +} + +/// First 1 spans across the first two words (index 64). +#[test] +fn first_one_at_word_boundary() { + let mut bits = BitString::zeros(130); + bits.set(64, true); + assert_eq!(bits.as_bit_str().leading_zeros(), 64); +} + +/// Unaligned view: start is not word-aligned. +#[test] +fn unaligned_start() { + let mut bits = BitString::zeros(130); + // Set bit 10 to 1. View starts at bit 3, so leading zeros = 7. + bits.set(10, true); + let v = bits.as_bit_str().slice(UsizeCO::try_new(3, 130).unwrap()); + assert_eq!(v.leading_zeros(), 7); +} + +/// Unaligned view that is all zeros. +#[test] +fn unaligned_all_zeros() { + let bits = BitString::zeros(200); + let v = bits.as_bit_str().slice(UsizeCO::try_new(3, 130).unwrap()); + assert_eq!(v.leading_zeros(), 127); // bit_len = 130 - 3 +} + +/// Unaligned single word: both start and end within the same word. +#[test] +fn unaligned_single_word() { + let bits = BitString::try_from("11111000").unwrap(); + // bits: 1 1 1 1 1 0 0 0 + // View bits 2..6 → "1110" = bits 2,3,4,5 (all ones) + let v = bits.as_bit_str().slice(UsizeCO::try_new(2, 6).unwrap()); + assert_eq!(v.leading_zeros(), 0); + + // View bits 5..8 → "000" + let v = bits.as_bit_str().slice(UsizeCO::try_new(5, 8).unwrap()); + assert_eq!(v.leading_zeros(), 3); +} + +/// A long view with a 1 exactly at the last bit. +#[test] +fn last_bit_is_one() { + let mut bits = BitString::zeros(130); + bits.set(129, true); + assert_eq!(bits.as_bit_str().leading_zeros(), 129); +} + +/// Alternating pattern starting with 0. +#[test] +fn alternating_starting_with_zero() { + // "01010101" — 8 bits + let bits = BitString::try_from("01010101").unwrap(); + assert_eq!(bits.as_bit_str().leading_zeros(), 1); +} + +/// Invariant: leading_zeros ≤ bit_len and leading_zeros == bit_len iff all zeros. +#[test] +fn invariant_leading_zeros_bounds() { + let mut bits = BitString::zeros(200); + for i in (0..200).step_by(7) { + bits.set(i, true); + } + let full = bits.as_bit_str(); + + for start in [0, 1, 5, 63, 64, 65, 127, 128] { + for len in [10, 63, 64, 65, 128, 129] { + let end = (start + len).min(full.bit_len()); + if start == end { + continue; + } + let v = full.slice(UsizeCO::try_new(start, end).unwrap()); + let lz = v.leading_zeros(); + assert!(lz <= v.bit_len(), "start={start} end={end} lz={lz}"); + if v.is_all_zeros() { + assert_eq!(lz, v.bit_len(), "all zeros: start={start} end={end}"); + } else { + // If not all zeros, the bit at position `lz` must be 1. + assert_eq!(v.get(lz), Some(true), "start={start} end={end} lz={lz}"); + } + } + } +} diff --git a/src/traits/bits_arith.rs b/src/traits/bits_arith.rs index 7814c40..515daee 100644 --- a/src/traits/bits_arith.rs +++ b/src/traits/bits_arith.rs @@ -48,10 +48,17 @@ pub(crate) trait BitsArith { /// Bits beyond `bit_len` are assumed to already be zero (masked by /// prior calls to [`BitsEdit::mask_unused_bits`]). fn count_ones(&self, bit_len: usize) -> usize; + + /// Returns the number of consecutive zero words at the start of `self`. + /// + /// All words up to (but not including) the returned index are zero. If + /// the return value equals `self.len()`, every word is zero. + fn leading_zero_words(&self) -> usize; } pub(crate) mod funcs_for_binary_core; pub(crate) mod funcs_for_count_ones; +pub(crate) mod funcs_for_leading_zero_words; pub(crate) mod funcs_for_not_core; pub(crate) mod funcs_for_shl_core; pub(crate) mod funcs_for_shr_core; diff --git a/src/traits/bits_arith/funcs_for_leading_zero_words.rs b/src/traits/bits_arith/funcs_for_leading_zero_words.rs new file mode 100644 index 0000000..8bae1c6 --- /dev/null +++ b/src/traits/bits_arith/funcs_for_leading_zero_words.rs @@ -0,0 +1,237 @@ +//! SIMD-accelerated leading-zero-word scan. +//! +//! Counts consecutive zero-valued `u64` words from the start of a slice. + +use crate::SMALL_WORDS; + +/// Returns the number of consecutive zero words at the start of `words`. +/// +/// All words up to (but not including) the returned index are zero. If the +/// return value equals `words.len()`, every word is zero. +#[inline] +pub(crate) fn leading_zero_words(words: &[u64]) -> usize { + if words.len() < SMALL_WORDS { + return scalar::scan(words); + } + + #[cfg(all( + any(target_arch = "x86", target_arch = "x86_64"), + target_feature = "avx2" + ))] + { + // SAFETY: AVX2 is available (compiled with target_feature check). + unsafe { + return avx2::scan(words); + } + } + + #[cfg(all( + any(target_arch = "x86", target_arch = "x86_64"), + target_feature = "sse2", + not(target_feature = "avx2") + ))] + { + // SAFETY: SSE2 is available. + unsafe { + return sse2::scan(words); + } + } + + #[cfg(all(target_arch = "aarch64", target_feature = "neon"))] + { + // SAFETY: NEON is available. + unsafe { + return neon::scan(words); + } + } + + #[allow(unused)] + scalar::scan(words) +} + +// --------------------------------------------------------------------------- +// Scalar +// --------------------------------------------------------------------------- + +mod scalar { + #[inline] + pub(super) fn scan(words: &[u64]) -> usize { + for (i, &w) in words.iter().enumerate() { + if w != 0 { + return i; + } + } + words.len() + } +} + +// --------------------------------------------------------------------------- +// AVX2 — 4 words at a time +// --------------------------------------------------------------------------- + +#[allow(unused)] +#[cfg(any(target_arch = "x86", target_arch = "x86_64"))] +mod avx2 { + #[cfg(target_arch = "x86")] + use core::arch::x86::{ + __m256i, _mm256_cmpeq_epi64, _mm256_loadu_si256, _mm256_movemask_epi8, _mm256_setzero_si256, + }; + #[cfg(target_arch = "x86_64")] + use core::arch::x86_64::{ + __m256i, _mm256_cmpeq_epi64, _mm256_loadu_si256, _mm256_movemask_epi8, _mm256_setzero_si256, + }; + + const LANES: usize = 4; + + /// AVX2 backend. + /// + /// # Safety + /// + /// Caller must ensure AVX2 is available. + #[target_feature(enable = "avx2")] + pub(super) unsafe fn scan(words: &[u64]) -> usize { + // SAFETY: all operations inside require AVX2, which is guaranteed by + // the `target_feature` annotation on this function. + unsafe { + let zero = _mm256_setzero_si256(); + let chunks = words.len() / LANES; + let mut count = 0usize; + + for chunk in 0..chunks { + let offset = chunk * LANES; + // SAFETY: offset + LANES ≤ words.len(); _mm256_loadu_si256 + // supports unaligned reads. + let data = _mm256_loadu_si256(words.as_ptr().add(offset).cast::<__m256i>()); + // Compare each u64 lane to zero → -1 for zero, 0 for non-zero. + let cmp = _mm256_cmpeq_epi64(data, zero); + // movemask extracts the MSB of each byte → 32-bit mask. + // A zero-word lane (all bytes 0xFF after cmpeq) contributes + // 0xFF (8 bits of 1). A non-zero lane contributes 0x00 + // (8 bits of 0). + let mask = _mm256_movemask_epi8(cmp) as u32; + if mask != 0xFFFF_FFFF { + // At least one word is non-zero. Find the first one. + for k in 0..LANES { + if (mask >> (k * 8)) & 0xFF != 0xFF { + return count + k; + } + } + // SAFETY: mask != 0xFFFF_FFFF guarantees at least one + // lane is non-zero, so the loop above always returns. + core::hint::unreachable_unchecked(); + } + count += LANES; + } + + let done = chunks * LANES; + count + super::scalar::scan(&words[done..]) + } + } +} + +// --------------------------------------------------------------------------- +// SSE2 — 2 words at a time +// --------------------------------------------------------------------------- + +#[allow(unused)] +#[cfg(any(target_arch = "x86", target_arch = "x86_64"))] +mod sse2 { + #[cfg(target_arch = "x86")] + use core::arch::x86::{ + __m128i, _mm_cmpeq_epi64, _mm_loadu_si128, _mm_movemask_epi8, _mm_setzero_si128, + }; + #[cfg(target_arch = "x86_64")] + use core::arch::x86_64::{ + __m128i, _mm_cmpeq_epi64, _mm_loadu_si128, _mm_movemask_epi8, _mm_setzero_si128, + }; + + const LANES: usize = 2; + + /// SSE2 backend. + /// + /// # Safety + /// + /// Caller must ensure SSE2 is available. + #[target_feature(enable = "sse2")] + pub(super) unsafe fn scan(words: &[u64]) -> usize { + // SAFETY: all operations inside require SSE2, which is guaranteed by + // the `target_feature` annotation. + unsafe { + let zero = _mm_setzero_si128(); + let chunks = words.len() / LANES; + let mut count = 0usize; + + for chunk in 0..chunks { + let offset = chunk * LANES; + let data = _mm_loadu_si128(words.as_ptr().add(offset).cast::<__m128i>()); + let cmp = _mm_cmpeq_epi64(data, zero); + let mask = _mm_movemask_epi8(cmp) as u32; + // 16-bit mask from 2 u64 lanes: 2 × 8 bytes = 16 bits. + if mask != 0xFFFF { + for k in 0..LANES { + if (mask >> (k * 8)) & 0xFF != 0xFF { + return count + k; + } + } + // SAFETY: mask != 0xFFFF guarantees at least one lane is + // non-zero, so the loop above always returns. + core::hint::unreachable_unchecked(); + } + count += LANES; + } + + let done = chunks * LANES; + count + super::scalar::scan(&words[done..]) + } + } +} + +// --------------------------------------------------------------------------- +// NEON — 2 words at a time +// --------------------------------------------------------------------------- + +#[allow(unused)] +#[cfg(target_arch = "aarch64")] +mod neon { + use core::arch::aarch64::{uint64x2_t, vceqq_u64, vdupq_n_u64, vgetq_lane_u64, vld1q_u64}; + + const LANES: usize = 2; + + /// NEON backend. + /// + /// # Safety + /// + /// Caller must ensure NEON is available. + #[target_feature(enable = "neon")] + pub(super) unsafe fn scan(words: &[u64]) -> usize { + // SAFETY: all operations inside require NEON, which is guaranteed by + // the `target_feature` annotation. + unsafe { + let zero = vdupq_n_u64(0); + let chunks = words.len() / LANES; + let mut count = 0usize; + + for chunk in 0..chunks { + let offset = chunk * LANES; + let data = vld1q_u64(words.as_ptr().add(offset)); + let cmp = vceqq_u64(data, zero); + // vceqq returns all-1s (-1) for equal, all-0s for not equal. + if vgetq_lane_u64(cmp, 0) == 0 { + // First word is non-zero. + return count; + } + if vgetq_lane_u64(cmp, 1) == 0 { + // Second word is non-zero. + return count + 1; + } + count += LANES; + } + + let done = chunks * LANES; + count + super::scalar::scan(&words[done..]) + } + } +} + +#[cfg(test)] +mod tests_for_backend_equivalence; diff --git a/src/traits/bits_arith/funcs_for_leading_zero_words/tests_for_backend_equivalence.rs b/src/traits/bits_arith/funcs_for_leading_zero_words/tests_for_backend_equivalence.rs new file mode 100644 index 0000000..7107bfa --- /dev/null +++ b/src/traits/bits_arith/funcs_for_leading_zero_words/tests_for_backend_equivalence.rs @@ -0,0 +1,100 @@ +use super::*; + +const CASES: &[&[u64]] = &[ + &[], + &[0], + &[u64::MAX], + &[1], + &[0, u64::MAX], + &[0, 0, u64::MAX], + &[0, 0, 0, 0, u64::MAX], + &[0, 0, 0, 0, 0, u64::MAX], + &[0, 0, 0, 0, 0, 0, u64::MAX], + &[0, 0, 0, 0, 0, 0, 0, u64::MAX], + &[0, 0, 0, 0, 0, 0, 0, 0, u64::MAX], + &[0x5555_5555_5555_5555, 0xAAAA_AAAA_AAAA_AAAA], + &[0, 1, u64::MAX, 0x0123_4567_89AB_CDEF, 0xFEDC_BA98_7654_3210], + &[ + 0, 0, 0, 0, 0, 0, 0, 0, // + 0, 0, 0, 0, 0, 0, 0, 0, + ], + // All zeros long run. + &[0u64; 32], + // All ones. + &[u64::MAX; 16], + // Mixed: 16 zeros then 8 MAX. + &[ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, // + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, // + u64::MAX, + u64::MAX, + u64::MAX, + u64::MAX, + u64::MAX, + u64::MAX, + u64::MAX, + u64::MAX, + ], +]; + +fn assert_backend_matches_scalar(backend: unsafe fn(&[u64]) -> usize) { + for &src in CASES { + let expected = scalar::scan(src); + // SAFETY: the backend requires the corresponding target feature, but + // the test is only compiled when that feature is enabled. + let actual = unsafe { backend(src) }; + + assert_eq!(actual, expected, "src = {src:?}"); + } +} + +// --------------------------------------------------------------------------- +// AVX2 +// --------------------------------------------------------------------------- + +#[cfg(all( + any(target_arch = "x86", target_arch = "x86_64"), + target_feature = "avx2" +))] +#[test] +fn avx2_matches_scalar() { + assert_backend_matches_scalar(avx2::scan); +} + +// --------------------------------------------------------------------------- +// SSE2 +// --------------------------------------------------------------------------- + +#[cfg(all( + any(target_arch = "x86", target_arch = "x86_64"), + target_feature = "sse2", + not(target_feature = "avx2") +))] +#[test] +fn sse2_matches_scalar() { + assert_backend_matches_scalar(sse2::scan); +} + +// --------------------------------------------------------------------------- +// NEON +// --------------------------------------------------------------------------- + +#[cfg(all(target_arch = "aarch64", target_feature = "neon"))] +#[test] +fn neon_matches_scalar() { + assert_backend_matches_scalar(neon::scan); +} diff --git a/src/traits/bits_arith/impls_for_u64_slice.rs b/src/traits/bits_arith/impls_for_u64_slice.rs index df08c72..773ab6f 100644 --- a/src/traits/bits_arith/impls_for_u64_slice.rs +++ b/src/traits/bits_arith/impls_for_u64_slice.rs @@ -3,6 +3,7 @@ use alloc::vec::Vec; use super::BitsArith; use super::funcs_for_binary_core::{OP_AND, OP_OR, OP_XOR, assign, owned}; use super::funcs_for_count_ones; +use super::funcs_for_leading_zero_words; use super::funcs_for_not_core; use super::funcs_for_shl_core; use super::funcs_for_shr_core; @@ -72,4 +73,9 @@ impl BitsArith for [u64] { fn count_ones(&self, bit_len: usize) -> usize { funcs_for_count_ones::count_ones(self, bit_len) } + + #[inline] + fn leading_zero_words(&self) -> usize { + funcs_for_leading_zero_words::leading_zero_words(self) + } } From a5ab44acd66ec10de5ce272d1520bff6614582db Mon Sep 17 00:00:00 2001 From: juncheng Date: Sat, 27 Jun 2026 04:26:09 +0000 Subject: [PATCH 2/7] feat: add leading_ones, parameterize SIMD scan, fix SSE4.1 gates MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - BitStr::leading_zeros() and leading_ones() share a parameterised internal helper (leading_value_count) that dispatches fill=0 or !0 - BitsArith gains leading_one_words() alongside leading_zero_words() Both delegate to leading_value_words(words, fill) which uses AVX2/SSE4.1/NEON backends with a scalar fallback - Rename funcs_for_leading_zero_words → funcs_for_leading_value_words Module parameterised by fill value, zero and one tests unified - Fix _mm_cmpeq_epi64 requires SSE4.1, not SSE2 Rename sse2→sse41 in 8 module files that use the intrinsic: leading_value_words, eq_words_aligned, eq_words_unaligned, contains, find, rfind, cmp_aligned, cmp_unaligned - CI: linux-x64-ssse3 → linux-x64-sse4.1 so SSE4.1 backend equivalence tests are covered Co-authored-by: Claude Co-authored-by: DeepSeek AI --- .github/workflows/gate.yml | 4 +- .../impls_for_leading_zeros.rs | 137 +++++++++++------ .../tests_for_leading_ones.rs | 140 ++++++++++++++++++ src/traits/bits_arith.rs | 9 +- ...ds.rs => funcs_for_leading_value_words.rs} | 90 +++++------ .../tests_for_backend_equivalence.rs | 136 +++++++++++++++++ .../tests_for_backend_equivalence.rs | 100 ------------- src/traits/bits_arith/impls_for_u64_slice.rs | 9 +- .../funcs_for_eq_words_aligned_core.rs | 8 +- .../funcs_for_eq_words_unaligned_core.rs | 8 +- .../bits_find/funcs_for_contains_core.rs | 10 +- src/traits/bits_find/funcs_for_find_core.rs | 8 +- src/traits/bits_find/funcs_for_rfind_core.rs | 8 +- .../bits_ord/funcs_for_cmp_aligned_core.rs | 8 +- .../bits_ord/funcs_for_cmp_unaligned_core.rs | 8 +- 15 files changed, 460 insertions(+), 223 deletions(-) create mode 100644 src/bit_str/impls_for_bit_arith/impls_for_leading_zeros/tests_for_leading_ones.rs rename src/traits/bits_arith/{funcs_for_leading_zero_words.rs => funcs_for_leading_value_words.rs} (70%) create mode 100644 src/traits/bits_arith/funcs_for_leading_value_words/tests_for_backend_equivalence.rs delete mode 100644 src/traits/bits_arith/funcs_for_leading_zero_words/tests_for_backend_equivalence.rs diff --git a/.github/workflows/gate.yml b/.github/workflows/gate.yml index 98242a7..ef0abe6 100644 --- a/.github/workflows/gate.yml +++ b/.github/workflows/gate.yml @@ -23,9 +23,9 @@ jobs: os: ubuntu-latest rustflags: "" - - name: linux-x64-ssse3 + - name: linux-x64-sse4.1 os: ubuntu-latest - rustflags: "-C target-feature=+ssse3" + rustflags: "-C target-feature=+sse4.1" - name: linux-x64-avx2 os: ubuntu-latest diff --git a/src/bit_str/impls_for_bit_arith/impls_for_leading_zeros.rs b/src/bit_str/impls_for_bit_arith/impls_for_leading_zeros.rs index bb68f1b..8c819a6 100644 --- a/src/bit_str/impls_for_bit_arith/impls_for_leading_zeros.rs +++ b/src/bit_str/impls_for_bit_arith/impls_for_leading_zeros.rs @@ -3,14 +3,78 @@ use crate::{WORD_BITS, low_mask}; use crate::BitStr; +// --------------------------------------------------------------------------- +// Shared helper — parameterised by `fill` (0 → zeros, !0 → ones) +// --------------------------------------------------------------------------- + +/// Counts consecutive bits equal to `fill` from the start of a view. +/// +/// `fill` must be `0` (for `leading_zeros`) or `!0` (for `leading_ones`). +#[inline] +fn leading_value_count(words: &[u64], start: usize, bit_len: usize, fill: u64) -> usize { + let end = start + bit_len; + let start_offset = start % WORD_BITS; + let end_rem = end % WORD_BITS; + let last_wi = (end - 1) / WORD_BITS; + + let mut scanned = 0usize; + let mut wi = start / WORD_BITS; + + // First word — only bits from start_offset upward are in view. + let first_val = words[wi] >> start_offset; + let first_limit = (WORD_BITS - start_offset).min(bit_len); + let first_count = count_trailing(first_val, fill).min(first_limit); + if first_count < first_limit { + return first_count; + } + scanned += first_limit; + wi += 1; + + // Full middle words — SIMD-accelerated value-word scan. + let mid_end = if end_rem == 0 { last_wi + 1 } else { last_wi }; + if wi < mid_end { + let value_words = if fill == 0 { + words[wi..mid_end].leading_zero_words() + } else { + words[wi..mid_end].leading_one_words() + }; + scanned += value_words * WORD_BITS; + wi += value_words; + + if wi < mid_end { + return scanned + count_trailing(words[wi], fill).min(WORD_BITS); + } + } + + // Last partial word (only when end_rem != 0). + if end_rem != 0 && wi == last_wi { + let last_val = words[wi] & low_mask(end_rem); + scanned += count_trailing(last_val, fill).min(end_rem); + } + + scanned.min(bit_len) +} + +/// Counts trailing bits of a given value within a single u64 word. +/// +/// `fill = 0` → `trailing_zeros`, `fill = !0` → `trailing_ones`. +#[inline] +fn count_trailing(val: u64, fill: u64) -> usize { + if fill == 0 { + val.trailing_zeros() as usize + } else { + (!val).trailing_zeros() as usize + } +} + impl<'bs> BitStr<'bs> { /// Returns the number of consecutive `false` bits from the start of this /// view. /// /// # Complexity /// - /// O(n / 64) scalar worst case, SIMD-accelerated for long zero runs. - /// Returns early at the first non-zero word. + /// O(n / 64) worst case, SIMD-accelerated for long runs. + /// Returns early at the first bit that differs from the expected value. /// /// # Examples /// @@ -26,52 +90,37 @@ impl<'bs> BitStr<'bs> { if self.bit_len == 0 { return 0; } + leading_value_count(self.source.words(), self.start, self.bit_len, 0) + } - let words = self.source.words(); - let end = self.start + self.bit_len; - let start_offset = self.start % WORD_BITS; - let end_rem = end % WORD_BITS; - let last_wi = (end - 1) / WORD_BITS; - - let mut scanned = 0usize; - let mut wi = self.start / WORD_BITS; - - // First word — only bits from start_offset upward are in view. - let first_val = words[wi] >> start_offset; - let first_limit = (WORD_BITS - start_offset).min(self.bit_len); - let first_z = (first_val.trailing_zeros() as usize).min(first_limit); - if first_z < first_limit { - return first_z; - } - scanned += first_limit; - wi += 1; - - // Full middle words — SIMD-accelerated zero-word scan. - let mid_end = if end_rem == 0 { last_wi + 1 } else { last_wi }; - if wi < mid_end { - let zero_words = words[wi..mid_end].leading_zero_words(); - let zero_bits = zero_words * WORD_BITS; - scanned += zero_bits; - wi += zero_words; - - // If there's a non-zero word left in the middle range, count its - // trailing zeros and return early. - if wi < mid_end { - let z = (words[wi].trailing_zeros() as usize).min(WORD_BITS); - return scanned + z; - } - } - - // Last partial word (only when end_rem != 0). - if end_rem != 0 && wi == last_wi { - let last_val = words[wi] & low_mask(end_rem); - let last_z = (last_val.trailing_zeros() as usize).min(end_rem); - scanned += last_z; + /// Returns the number of consecutive `true` bits from the start of this + /// view. + /// + /// # Complexity + /// + /// O(n / 64) worst case, SIMD-accelerated for long runs. + /// Returns early at the first bit that differs from the expected value. + /// + /// # Examples + /// + /// ``` + /// use bit_string::BitString; + /// + /// let bits = BitString::try_from("11010").unwrap(); + /// let v = bits.as_bit_str(); + /// assert_eq!(v.leading_ones(), 2); + /// ``` + #[inline] + pub fn leading_ones(&self) -> usize { + if self.bit_len == 0 { + return 0; } - - scanned.min(self.bit_len) + leading_value_count(self.source.words(), self.start, self.bit_len, !0) } } #[cfg(test)] mod tests_for_leading_zeros; + +#[cfg(test)] +mod tests_for_leading_ones; diff --git a/src/bit_str/impls_for_bit_arith/impls_for_leading_zeros/tests_for_leading_ones.rs b/src/bit_str/impls_for_bit_arith/impls_for_leading_zeros/tests_for_leading_ones.rs new file mode 100644 index 0000000..98a86eb --- /dev/null +++ b/src/bit_str/impls_for_bit_arith/impls_for_leading_zeros/tests_for_leading_ones.rs @@ -0,0 +1,140 @@ +use int_interval::UsizeCO; + +use crate::BitString; + +/// An empty view has no leading ones. +#[test] +fn empty_view_returns_zero() { + let bits = BitString::try_from("01010").unwrap(); + let v = bits.as_bit_str().slice(UsizeCO::try_new(10, 20).unwrap()); + assert_eq!(v.leading_ones(), 0); +} + +/// Starts with a 0 → zero leading ones. +#[test] +fn starts_with_zero() { + let bits = BitString::try_from("01101").unwrap(); + assert_eq!(bits.as_bit_str().leading_ones(), 0); +} + +/// Starts with several ones. +#[test] +fn leading_one_run() { + let bits = BitString::try_from("11010").unwrap(); + assert_eq!(bits.as_bit_str().leading_ones(), 2); +} + +/// Single one bit. +#[test] +fn single_one() { + let bits = BitString::ones(1); + assert_eq!(bits.as_bit_str().leading_ones(), 1); +} + +/// Single zero bit. +#[test] +fn single_zero() { + let bits = BitString::zeros(1); + assert_eq!(bits.as_bit_str().leading_ones(), 0); +} + +/// All ones at various lengths including word-boundary values. +#[test] +fn all_ones_at_various_lengths() { + for len in [1, 63, 64, 65, 127, 128, 129, 130] { + let bits = BitString::ones(len); + assert_eq!(bits.as_bit_str().leading_ones(), len, "len={len}"); + } +} + +/// All zeros at various lengths. +#[test] +fn all_zeros_at_various_lengths() { + for len in [1, 63, 64, 65, 127, 128, 129, 130] { + let bits = BitString::zeros(len); + assert_eq!(bits.as_bit_str().leading_ones(), 0, "len={len}"); + } +} + +/// First 0 across word boundary. +#[test] +fn first_zero_in_second_word() { + let mut bits = BitString::ones(130); + bits.set(100, false); + assert_eq!(bits.as_bit_str().leading_ones(), 100); +} + +/// First 0 exactly at word boundary. +#[test] +fn first_zero_at_word_boundary() { + let mut bits = BitString::ones(130); + bits.set(64, false); + assert_eq!(bits.as_bit_str().leading_ones(), 64); +} + +/// Unaligned view. +#[test] +fn unaligned_start() { + let mut bits = BitString::ones(130); + bits.set(10, false); // first zero at bit 10 + let v = bits.as_bit_str().slice(UsizeCO::try_new(3, 130).unwrap()); + assert_eq!(v.leading_ones(), 7); // from bit 3, ones at positions 3..=9, then 0 at 10 +} + +/// Unaligned view all ones. +#[test] +fn unaligned_all_ones() { + let bits = BitString::ones(200); + let v = bits.as_bit_str().slice(UsizeCO::try_new(3, 130).unwrap()); + assert_eq!(v.leading_ones(), 127); +} + +/// Unaligned single word. +#[test] +fn unaligned_single_word() { + let bits = BitString::try_from("00000111").unwrap(); + // bits: 0 0 0 0 0 1 1 1 + + // View bits 4..8 → "0111" + let v = bits.as_bit_str().slice(UsizeCO::try_new(4, 8).unwrap()); + assert_eq!(v.leading_ones(), 0); + + // View bits 5..8 → "111" + let v = bits.as_bit_str().slice(UsizeCO::try_new(5, 8).unwrap()); + assert_eq!(v.leading_ones(), 3); +} + +/// Last bit is zero. +#[test] +fn last_bit_is_zero() { + let mut bits = BitString::ones(130); + bits.set(129, false); + assert_eq!(bits.as_bit_str().leading_ones(), 129); +} + +/// Invariant: leading_ones ≤ bit_len and leading_ones == bit_len iff all ones. +#[test] +fn invariant_leading_ones_bounds() { + let mut bits = BitString::ones(200); + for i in (1..200).step_by(7) { + bits.set(i, false); + } + let full = bits.as_bit_str(); + + for start in [0, 1, 5, 63, 64, 65, 127, 128] { + for len in [10, 63, 64, 65, 128, 129] { + let end = (start + len).min(full.bit_len()); + if start == end { + continue; + } + let v = full.slice(UsizeCO::try_new(start, end).unwrap()); + let lo = v.leading_ones(); + assert!(lo <= v.bit_len(), "start={start} end={end} lo={lo}"); + if v.is_all_ones() { + assert_eq!(lo, v.bit_len(), "all ones: start={start} end={end}"); + } else { + assert_eq!(v.get(lo), Some(false), "start={start} end={end} lo={lo}"); + } + } + } +} diff --git a/src/traits/bits_arith.rs b/src/traits/bits_arith.rs index 515daee..f081e27 100644 --- a/src/traits/bits_arith.rs +++ b/src/traits/bits_arith.rs @@ -54,11 +54,18 @@ pub(crate) trait BitsArith { /// All words up to (but not including) the returned index are zero. If /// the return value equals `self.len()`, every word is zero. fn leading_zero_words(&self) -> usize; + + /// Returns the number of consecutive all-ones words at the start of + /// `self`. + /// + /// All words up to (but not including) the returned index are all-ones. + /// If the return value equals `self.len()`, every word is all-ones. + fn leading_one_words(&self) -> usize; } pub(crate) mod funcs_for_binary_core; pub(crate) mod funcs_for_count_ones; -pub(crate) mod funcs_for_leading_zero_words; +pub(crate) mod funcs_for_leading_value_words; pub(crate) mod funcs_for_not_core; pub(crate) mod funcs_for_shl_core; pub(crate) mod funcs_for_shr_core; diff --git a/src/traits/bits_arith/funcs_for_leading_zero_words.rs b/src/traits/bits_arith/funcs_for_leading_value_words.rs similarity index 70% rename from src/traits/bits_arith/funcs_for_leading_zero_words.rs rename to src/traits/bits_arith/funcs_for_leading_value_words.rs index 8bae1c6..4158643 100644 --- a/src/traits/bits_arith/funcs_for_leading_zero_words.rs +++ b/src/traits/bits_arith/funcs_for_leading_value_words.rs @@ -1,17 +1,20 @@ -//! SIMD-accelerated leading-zero-word scan. +//! SIMD-accelerated leading-value-word scan. //! -//! Counts consecutive zero-valued `u64` words from the start of a slice. +//! Counts consecutive words equal to `fill` from the start of a slice. +//! Used to implement both `leading_zero_words` (`fill = 0`) and +//! `leading_one_words` (`fill = !0`). use crate::SMALL_WORDS; -/// Returns the number of consecutive zero words at the start of `words`. +/// Returns the number of consecutive words equal to `fill` at the start of +/// `words`. /// -/// All words up to (but not including) the returned index are zero. If the -/// return value equals `words.len()`, every word is zero. +/// All words up to (but not including) the returned index equal `fill`. If +/// the return value equals `words.len()`, every word equals `fill`. #[inline] -pub(crate) fn leading_zero_words(words: &[u64]) -> usize { +pub(crate) fn leading_value_words(words: &[u64], fill: u64) -> usize { if words.len() < SMALL_WORDS { - return scalar::scan(words); + return scalar::scan(words, fill); } #[cfg(all( @@ -21,19 +24,19 @@ pub(crate) fn leading_zero_words(words: &[u64]) -> usize { { // SAFETY: AVX2 is available (compiled with target_feature check). unsafe { - return avx2::scan(words); + return avx2::scan(words, fill); } } #[cfg(all( any(target_arch = "x86", target_arch = "x86_64"), - target_feature = "sse2", + target_feature = "sse4.1", not(target_feature = "avx2") ))] { - // SAFETY: SSE2 is available. + // SAFETY: SSE4.1 is available. unsafe { - return sse2::scan(words); + return sse41::scan(words, fill); } } @@ -41,12 +44,12 @@ pub(crate) fn leading_zero_words(words: &[u64]) -> usize { { // SAFETY: NEON is available. unsafe { - return neon::scan(words); + return neon::scan(words, fill); } } #[allow(unused)] - scalar::scan(words) + scalar::scan(words, fill) } // --------------------------------------------------------------------------- @@ -55,9 +58,9 @@ pub(crate) fn leading_zero_words(words: &[u64]) -> usize { mod scalar { #[inline] - pub(super) fn scan(words: &[u64]) -> usize { + pub(super) fn scan(words: &[u64], fill: u64) -> usize { for (i, &w) in words.iter().enumerate() { - if w != 0 { + if w != fill { return i; } } @@ -74,11 +77,11 @@ mod scalar { mod avx2 { #[cfg(target_arch = "x86")] use core::arch::x86::{ - __m256i, _mm256_cmpeq_epi64, _mm256_loadu_si256, _mm256_movemask_epi8, _mm256_setzero_si256, + __m256i, _mm256_cmpeq_epi64, _mm256_loadu_si256, _mm256_movemask_epi8, _mm256_set1_epi64x, }; #[cfg(target_arch = "x86_64")] use core::arch::x86_64::{ - __m256i, _mm256_cmpeq_epi64, _mm256_loadu_si256, _mm256_movemask_epi8, _mm256_setzero_si256, + __m256i, _mm256_cmpeq_epi64, _mm256_loadu_si256, _mm256_movemask_epi8, _mm256_set1_epi64x, }; const LANES: usize = 4; @@ -89,11 +92,11 @@ mod avx2 { /// /// Caller must ensure AVX2 is available. #[target_feature(enable = "avx2")] - pub(super) unsafe fn scan(words: &[u64]) -> usize { + pub(super) unsafe fn scan(words: &[u64], fill: u64) -> usize { // SAFETY: all operations inside require AVX2, which is guaranteed by // the `target_feature` annotation on this function. unsafe { - let zero = _mm256_setzero_si256(); + let fill_vec = _mm256_set1_epi64x(fill as i64); let chunks = words.len() / LANES; let mut count = 0usize; @@ -102,69 +105,68 @@ mod avx2 { // SAFETY: offset + LANES ≤ words.len(); _mm256_loadu_si256 // supports unaligned reads. let data = _mm256_loadu_si256(words.as_ptr().add(offset).cast::<__m256i>()); - // Compare each u64 lane to zero → -1 for zero, 0 for non-zero. - let cmp = _mm256_cmpeq_epi64(data, zero); + // Compare each u64 lane to fill → -1 for equal, 0 for not. + let cmp = _mm256_cmpeq_epi64(data, fill_vec); // movemask extracts the MSB of each byte → 32-bit mask. - // A zero-word lane (all bytes 0xFF after cmpeq) contributes - // 0xFF (8 bits of 1). A non-zero lane contributes 0x00 + // An equal-word lane (all bytes 0xFF after cmpeq) contributes + // 0xFF (8 bits of 1). A non-equal lane contributes 0x00 // (8 bits of 0). let mask = _mm256_movemask_epi8(cmp) as u32; if mask != 0xFFFF_FFFF { - // At least one word is non-zero. Find the first one. for k in 0..LANES { if (mask >> (k * 8)) & 0xFF != 0xFF { return count + k; } } // SAFETY: mask != 0xFFFF_FFFF guarantees at least one - // lane is non-zero, so the loop above always returns. + // lane is not equal to fill, so the loop always returns. core::hint::unreachable_unchecked(); } count += LANES; } let done = chunks * LANES; - count + super::scalar::scan(&words[done..]) + count + super::scalar::scan(&words[done..], fill) } } } // --------------------------------------------------------------------------- -// SSE2 — 2 words at a time +// SSE4.1 — 2 words at a time // --------------------------------------------------------------------------- #[allow(unused)] #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] -mod sse2 { +mod sse41 { #[cfg(target_arch = "x86")] use core::arch::x86::{ - __m128i, _mm_cmpeq_epi64, _mm_loadu_si128, _mm_movemask_epi8, _mm_setzero_si128, + __m128i, _mm_cmpeq_epi64, _mm_loadu_si128, _mm_movemask_epi8, _mm_set1_epi64x, }; #[cfg(target_arch = "x86_64")] use core::arch::x86_64::{ - __m128i, _mm_cmpeq_epi64, _mm_loadu_si128, _mm_movemask_epi8, _mm_setzero_si128, + __m128i, _mm_cmpeq_epi64, _mm_loadu_si128, _mm_movemask_epi8, _mm_set1_epi64x, }; const LANES: usize = 2; - /// SSE2 backend. + /// SSE4.1 backend. /// /// # Safety /// - /// Caller must ensure SSE2 is available. - #[target_feature(enable = "sse2")] - pub(super) unsafe fn scan(words: &[u64]) -> usize { - // SAFETY: all operations inside require SSE2, which is guaranteed by + /// Caller must ensure SSE4.1 is available. + #[target_feature(enable = "sse4.1")] + pub(super) unsafe fn scan(words: &[u64], fill: u64) -> usize { + // SAFETY: all operations inside require SSE4.1, which is guaranteed by // the `target_feature` annotation. unsafe { - let zero = _mm_setzero_si128(); + let fill_vec = _mm_set1_epi64x(fill as i64); let chunks = words.len() / LANES; let mut count = 0usize; for chunk in 0..chunks { let offset = chunk * LANES; let data = _mm_loadu_si128(words.as_ptr().add(offset).cast::<__m128i>()); - let cmp = _mm_cmpeq_epi64(data, zero); + let cmp = _mm_cmpeq_epi64(data, fill_vec); let mask = _mm_movemask_epi8(cmp) as u32; // 16-bit mask from 2 u64 lanes: 2 × 8 bytes = 16 bits. if mask != 0xFFFF { @@ -174,14 +176,14 @@ mod sse2 { } } // SAFETY: mask != 0xFFFF guarantees at least one lane is - // non-zero, so the loop above always returns. + // not equal to fill, so the loop always returns. core::hint::unreachable_unchecked(); } count += LANES; } let done = chunks * LANES; - count + super::scalar::scan(&words[done..]) + count + super::scalar::scan(&words[done..], fill) } } } @@ -203,32 +205,30 @@ mod neon { /// /// Caller must ensure NEON is available. #[target_feature(enable = "neon")] - pub(super) unsafe fn scan(words: &[u64]) -> usize { + pub(super) unsafe fn scan(words: &[u64], fill: u64) -> usize { // SAFETY: all operations inside require NEON, which is guaranteed by // the `target_feature` annotation. unsafe { - let zero = vdupq_n_u64(0); + let fill_vec = vdupq_n_u64(fill); let chunks = words.len() / LANES; let mut count = 0usize; for chunk in 0..chunks { let offset = chunk * LANES; let data = vld1q_u64(words.as_ptr().add(offset)); - let cmp = vceqq_u64(data, zero); + let cmp = vceqq_u64(data, fill_vec); // vceqq returns all-1s (-1) for equal, all-0s for not equal. if vgetq_lane_u64(cmp, 0) == 0 { - // First word is non-zero. return count; } if vgetq_lane_u64(cmp, 1) == 0 { - // Second word is non-zero. return count + 1; } count += LANES; } let done = chunks * LANES; - count + super::scalar::scan(&words[done..]) + count + super::scalar::scan(&words[done..], fill) } } } diff --git a/src/traits/bits_arith/funcs_for_leading_value_words/tests_for_backend_equivalence.rs b/src/traits/bits_arith/funcs_for_leading_value_words/tests_for_backend_equivalence.rs new file mode 100644 index 0000000..fdd08e9 --- /dev/null +++ b/src/traits/bits_arith/funcs_for_leading_value_words/tests_for_backend_equivalence.rs @@ -0,0 +1,136 @@ +use alloc::vec; + +use super::*; + +const CASES_ZERO: &[&[u64]] = &[ + &[], + &[0], + &[u64::MAX], + &[1], + &[0, u64::MAX], + &[0, 0, u64::MAX], + &[0, 0, 0, 0, u64::MAX], + &[0, 0, 0, 0, 0, 0, 0, u64::MAX], + &[0x5555_5555_5555_5555, 0xAAAA_AAAA_AAAA_AAAA], + &[0, 1, u64::MAX, 0x0123_4567_89AB_CDEF], + &[0u64; 32], + &[u64::MAX; 16], +]; + +const CASES_ONE: &[&[u64]] = &[ + &[], + &[u64::MAX], + &[0], + &[u64::MAX - 1], + &[u64::MAX, 0], + &[u64::MAX, u64::MAX, 0], + &[u64::MAX, u64::MAX, u64::MAX, u64::MAX, 0], + &[ + u64::MAX, + u64::MAX, + u64::MAX, + u64::MAX, + u64::MAX, + u64::MAX, + u64::MAX, + 0, + ], + &[0x5555_5555_5555_5555, 0xAAAA_AAAA_AAAA_AAAA], + &[u64::MAX, 1, 0, 0x0123_4567_89AB_CDEF], + &[u64::MAX; 32], + &[0u64; 16], +]; + +fn assert_backend_matches_scalar(backend: unsafe fn(&[u64], u64) -> usize, fill: u64) { + let cases: &[&[u64]] = if fill == 0 { CASES_ZERO } else { CASES_ONE }; + + for &src in cases { + let expected = scalar::scan(src, fill); + // SAFETY: the backend requires the corresponding target feature, but + // the test is only compiled when that feature is enabled. + let actual = unsafe { backend(src, fill) }; + + assert_eq!(actual, expected, "fill=0x{fill:x} src={src:?}"); + } +} + +// Also test with random-looking data. +fn run_random(backend: unsafe fn(&[u64], u64) -> usize, fill: u64) { + for run in [0, 1, 3, 5, 7, 9, 15, 16, 17, 31] { + let mut v = vec![fill; run]; + for _ in 0..16 { + v.push(if fill == 0 { u64::MAX } else { 0 }); + } + let expected = scalar::scan(&v, fill); + let actual = unsafe { backend(&v, fill) }; + assert_eq!(actual, expected, "fill=0x{fill:x} run={run}"); + } +} + +// --------------------------------------------------------------------------- +// AVX2 +// --------------------------------------------------------------------------- + +#[cfg(all( + any(target_arch = "x86", target_arch = "x86_64"), + target_feature = "avx2" +))] +#[test] +fn avx2_matches_scalar_zeros() { + assert_backend_matches_scalar(avx2::scan, 0); + run_random(avx2::scan, 0); +} + +#[cfg(all( + any(target_arch = "x86", target_arch = "x86_64"), + target_feature = "avx2" +))] +#[test] +fn avx2_matches_scalar_ones() { + assert_backend_matches_scalar(avx2::scan, !0); + run_random(avx2::scan, !0); +} + +// --------------------------------------------------------------------------- +// SSE4.1 +// --------------------------------------------------------------------------- + +#[cfg(all( + any(target_arch = "x86", target_arch = "x86_64"), + target_feature = "sse4.1", + not(target_feature = "avx2") +))] +#[test] +fn sse41_matches_scalar_zeros() { + assert_backend_matches_scalar(sse41::scan, 0); + run_random(sse41::scan, 0); +} + +#[cfg(all( + any(target_arch = "x86", target_arch = "x86_64"), + target_feature = "sse4.1", + not(target_feature = "avx2") +))] +#[test] +fn sse41_matches_scalar_ones() { + assert_backend_matches_scalar(sse41::scan, !0); + run_random(sse41::scan, !0); +} + +// --------------------------------------------------------------------------- +// NEON +// --------------------------------------------------------------------------- + +#[cfg(all(target_arch = "aarch64", target_feature = "neon"))] +#[test] +fn neon_matches_scalar_zeros() { + assert_backend_matches_scalar(neon::scan, 0); + run_random(neon::scan, 0); +} + +#[cfg(all(target_arch = "aarch64", target_feature = "neon"))] +#[test] +fn neon_matches_scalar_ones() { + assert_backend_matches_scalar(neon::scan, !0); + run_random(neon::scan, !0); +} diff --git a/src/traits/bits_arith/funcs_for_leading_zero_words/tests_for_backend_equivalence.rs b/src/traits/bits_arith/funcs_for_leading_zero_words/tests_for_backend_equivalence.rs deleted file mode 100644 index 7107bfa..0000000 --- a/src/traits/bits_arith/funcs_for_leading_zero_words/tests_for_backend_equivalence.rs +++ /dev/null @@ -1,100 +0,0 @@ -use super::*; - -const CASES: &[&[u64]] = &[ - &[], - &[0], - &[u64::MAX], - &[1], - &[0, u64::MAX], - &[0, 0, u64::MAX], - &[0, 0, 0, 0, u64::MAX], - &[0, 0, 0, 0, 0, u64::MAX], - &[0, 0, 0, 0, 0, 0, u64::MAX], - &[0, 0, 0, 0, 0, 0, 0, u64::MAX], - &[0, 0, 0, 0, 0, 0, 0, 0, u64::MAX], - &[0x5555_5555_5555_5555, 0xAAAA_AAAA_AAAA_AAAA], - &[0, 1, u64::MAX, 0x0123_4567_89AB_CDEF, 0xFEDC_BA98_7654_3210], - &[ - 0, 0, 0, 0, 0, 0, 0, 0, // - 0, 0, 0, 0, 0, 0, 0, 0, - ], - // All zeros long run. - &[0u64; 32], - // All ones. - &[u64::MAX; 16], - // Mixed: 16 zeros then 8 MAX. - &[ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, // - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, // - u64::MAX, - u64::MAX, - u64::MAX, - u64::MAX, - u64::MAX, - u64::MAX, - u64::MAX, - u64::MAX, - ], -]; - -fn assert_backend_matches_scalar(backend: unsafe fn(&[u64]) -> usize) { - for &src in CASES { - let expected = scalar::scan(src); - // SAFETY: the backend requires the corresponding target feature, but - // the test is only compiled when that feature is enabled. - let actual = unsafe { backend(src) }; - - assert_eq!(actual, expected, "src = {src:?}"); - } -} - -// --------------------------------------------------------------------------- -// AVX2 -// --------------------------------------------------------------------------- - -#[cfg(all( - any(target_arch = "x86", target_arch = "x86_64"), - target_feature = "avx2" -))] -#[test] -fn avx2_matches_scalar() { - assert_backend_matches_scalar(avx2::scan); -} - -// --------------------------------------------------------------------------- -// SSE2 -// --------------------------------------------------------------------------- - -#[cfg(all( - any(target_arch = "x86", target_arch = "x86_64"), - target_feature = "sse2", - not(target_feature = "avx2") -))] -#[test] -fn sse2_matches_scalar() { - assert_backend_matches_scalar(sse2::scan); -} - -// --------------------------------------------------------------------------- -// NEON -// --------------------------------------------------------------------------- - -#[cfg(all(target_arch = "aarch64", target_feature = "neon"))] -#[test] -fn neon_matches_scalar() { - assert_backend_matches_scalar(neon::scan); -} diff --git a/src/traits/bits_arith/impls_for_u64_slice.rs b/src/traits/bits_arith/impls_for_u64_slice.rs index 773ab6f..bcc8fad 100644 --- a/src/traits/bits_arith/impls_for_u64_slice.rs +++ b/src/traits/bits_arith/impls_for_u64_slice.rs @@ -3,7 +3,7 @@ use alloc::vec::Vec; use super::BitsArith; use super::funcs_for_binary_core::{OP_AND, OP_OR, OP_XOR, assign, owned}; use super::funcs_for_count_ones; -use super::funcs_for_leading_zero_words; +use super::funcs_for_leading_value_words; use super::funcs_for_not_core; use super::funcs_for_shl_core; use super::funcs_for_shr_core; @@ -76,6 +76,11 @@ impl BitsArith for [u64] { #[inline] fn leading_zero_words(&self) -> usize { - funcs_for_leading_zero_words::leading_zero_words(self) + funcs_for_leading_value_words::leading_value_words(self, 0) + } + + #[inline] + fn leading_one_words(&self) -> usize { + funcs_for_leading_value_words::leading_value_words(self, !0) } } diff --git a/src/traits/bits_eq/funcs_for_eq_words_aligned_core.rs b/src/traits/bits_eq/funcs_for_eq_words_aligned_core.rs index 0675603..80b7fd0 100644 --- a/src/traits/bits_eq/funcs_for_eq_words_aligned_core.rs +++ b/src/traits/bits_eq/funcs_for_eq_words_aligned_core.rs @@ -32,11 +32,11 @@ pub(super) fn eq_words_aligned(src: &[u64], other: &[u64], count: usize) -> bool #[cfg(all( any(target_arch = "x86", target_arch = "x86_64"), - target_feature = "sse2", + target_feature = "sse4.1", not(target_feature = "avx2") ))] { - return unsafe { sse2::eq_words(src, other, count) }; + return unsafe { sse41::eq_words(src, other, count) }; } #[cfg(all(target_arch = "aarch64", target_feature = "neon"))] @@ -87,13 +87,13 @@ mod avx2 { #[allow(unused)] #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] -mod sse2 { +mod sse41 { #[cfg(target_arch = "x86")] use core::arch::x86::{__m128i, _mm_cmpeq_epi64, _mm_loadu_si128, _mm_movemask_epi8}; #[cfg(target_arch = "x86_64")] use core::arch::x86_64::{__m128i, _mm_cmpeq_epi64, _mm_loadu_si128, _mm_movemask_epi8}; - #[target_feature(enable = "sse2")] + #[target_feature(enable = "sse4.1")] pub(super) unsafe fn eq_words(src: &[u64], other: &[u64], len: usize) -> bool { let mut i = 0; while i + 2 <= len { diff --git a/src/traits/bits_eq/funcs_for_eq_words_unaligned_core.rs b/src/traits/bits_eq/funcs_for_eq_words_unaligned_core.rs index cd306ba..3ac73b6 100644 --- a/src/traits/bits_eq/funcs_for_eq_words_unaligned_core.rs +++ b/src/traits/bits_eq/funcs_for_eq_words_unaligned_core.rs @@ -36,11 +36,11 @@ pub(super) fn eq_words_unaligned(src: &[u64], other: &[u64], count: usize, shift #[cfg(all( any(target_arch = "x86", target_arch = "x86_64"), - target_feature = "sse2", + target_feature = "sse4.1", not(target_feature = "avx2") ))] { - return unsafe { sse2::eq_words_unaligned(src, other, count, shift) }; + return unsafe { sse41::eq_words_unaligned(src, other, count, shift) }; } #[cfg(all(target_arch = "aarch64", target_feature = "neon"))] @@ -114,7 +114,7 @@ mod avx2 { #[allow(unused)] #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] -mod sse2 { +mod sse41 { use crate::WORD_BITS; #[cfg(target_arch = "x86")] @@ -128,7 +128,7 @@ mod sse2 { _mm_set1_epi64x, _mm_sll_epi64, _mm_srl_epi64, }; - #[target_feature(enable = "sse2")] + #[target_feature(enable = "sse4.1")] pub(super) unsafe fn eq_words_unaligned( src: &[u64], other: &[u64], diff --git a/src/traits/bits_find/funcs_for_contains_core.rs b/src/traits/bits_find/funcs_for_contains_core.rs index 8273bfc..941bda9 100644 --- a/src/traits/bits_find/funcs_for_contains_core.rs +++ b/src/traits/bits_find/funcs_for_contains_core.rs @@ -69,12 +69,12 @@ where #[cfg(all( any(target_arch = "x86", target_arch = "x86_64"), - target_feature = "sse2", + target_feature = "sse4.1", not(target_feature = "avx2") ))] { unsafe { - return sse2::find_any( + return sse41::find_any( haystack, needle_first, needle_mask, @@ -154,7 +154,7 @@ where #[allow(unused)] #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] -mod sse2 { +mod sse41 { use super::*; #[cfg(target_arch = "x86")] @@ -170,10 +170,10 @@ mod sse2 { const LANES: usize = 2; - /// SSE2 backend: loads 2 consecutive words, computes a sliding + /// SSE4.1 backend: loads 2 consecutive words, computes a sliding /// window for the current shift, and compares against the broadcast /// needle word. `movemask` extracts match lanes. - #[target_feature(enable = "sse2")] + #[target_feature(enable = "sse4.1")] pub(super) unsafe fn find_any( haystack: &[u64], needle_first: u64, diff --git a/src/traits/bits_find/funcs_for_find_core.rs b/src/traits/bits_find/funcs_for_find_core.rs index 4d6967c..f4d9933 100644 --- a/src/traits/bits_find/funcs_for_find_core.rs +++ b/src/traits/bits_find/funcs_for_find_core.rs @@ -41,12 +41,12 @@ where #[cfg(all( any(target_arch = "x86", target_arch = "x86_64"), - target_feature = "sse2", + target_feature = "sse4.1", not(target_feature = "avx2") ))] { unsafe { - return sse2::find(haystack, needle_first, needle_mask, last_start, verify); + return sse41::find(haystack, needle_first, needle_mask, last_start, verify); } } @@ -102,7 +102,7 @@ where #[allow(unused)] #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] -mod sse2 { +mod sse41 { use super::*; #[cfg(target_arch = "x86")] @@ -116,7 +116,7 @@ mod sse2 { _mm_set1_epi64x, }; - #[target_feature(enable = "sse2")] + #[target_feature(enable = "sse4.1")] pub(super) unsafe fn find( haystack: &[u64], needle_first: u64, diff --git a/src/traits/bits_find/funcs_for_rfind_core.rs b/src/traits/bits_find/funcs_for_rfind_core.rs index d8a562f..838db2b 100644 --- a/src/traits/bits_find/funcs_for_rfind_core.rs +++ b/src/traits/bits_find/funcs_for_rfind_core.rs @@ -43,12 +43,12 @@ where #[cfg(all( any(target_arch = "x86", target_arch = "x86_64"), - target_feature = "sse2", + target_feature = "sse4.1", not(target_feature = "avx2") ))] { unsafe { - return sse2::rfind(haystack, needle_key, needle_mask, last_start, verify); + return sse41::rfind(haystack, needle_key, needle_mask, last_start, verify); } } @@ -108,7 +108,7 @@ where #[allow(unused)] #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] -mod sse2 { +mod sse41 { use super::*; #[cfg(target_arch = "x86")] @@ -122,7 +122,7 @@ mod sse2 { _mm_set1_epi64x, }; - #[target_feature(enable = "sse2")] + #[target_feature(enable = "sse4.1")] pub(super) unsafe fn rfind( haystack: &[u64], needle_key: u64, diff --git a/src/traits/bits_ord/funcs_for_cmp_aligned_core.rs b/src/traits/bits_ord/funcs_for_cmp_aligned_core.rs index ba16e56..677eede 100644 --- a/src/traits/bits_ord/funcs_for_cmp_aligned_core.rs +++ b/src/traits/bits_ord/funcs_for_cmp_aligned_core.rs @@ -24,11 +24,11 @@ pub(super) fn cmp_aligned_words(src: &[u64], other: &[u64], count: usize) -> Opt #[cfg(all( any(target_arch = "x86", target_arch = "x86_64"), - target_feature = "sse2", + target_feature = "sse4.1", not(target_feature = "avx2") ))] { - return unsafe { sse2::cmp_aligned(src, other, count) }; + return unsafe { sse41::cmp_aligned(src, other, count) }; } #[cfg(all(target_arch = "aarch64", target_feature = "neon"))] @@ -96,7 +96,7 @@ mod avx2 { #[allow(unused)] #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] -mod sse2 { +mod sse41 { use core::cmp::Ordering; use crate::traits::BitOrd; @@ -106,7 +106,7 @@ mod sse2 { #[cfg(target_arch = "x86_64")] use core::arch::x86_64::{__m128i, _mm_cmpeq_epi64, _mm_loadu_si128, _mm_movemask_epi8}; - #[target_feature(enable = "sse2")] + #[target_feature(enable = "sse4.1")] pub(super) unsafe fn cmp_aligned(src: &[u64], other: &[u64], len: usize) -> Option { let mut i = 0; while i + 2 <= len { diff --git a/src/traits/bits_ord/funcs_for_cmp_unaligned_core.rs b/src/traits/bits_ord/funcs_for_cmp_unaligned_core.rs index dc4ee6c..639fc18 100644 --- a/src/traits/bits_ord/funcs_for_cmp_unaligned_core.rs +++ b/src/traits/bits_ord/funcs_for_cmp_unaligned_core.rs @@ -34,11 +34,11 @@ pub(super) fn cmp_unaligned_words( #[cfg(all( any(target_arch = "x86", target_arch = "x86_64"), - target_feature = "sse2", + target_feature = "sse4.1", not(target_feature = "avx2") ))] { - return unsafe { sse2::cmp_unaligned(src, other, count, shift) }; + return unsafe { sse41::cmp_unaligned(src, other, count, shift) }; } #[cfg(all(target_arch = "aarch64", target_feature = "neon"))] @@ -135,7 +135,7 @@ mod avx2 { #[allow(unused)] #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] -mod sse2 { +mod sse41 { use core::cmp::Ordering; use crate::WORD_BITS; @@ -152,7 +152,7 @@ mod sse2 { _mm_set1_epi64x, _mm_sll_epi64, _mm_srl_epi64, }; - #[target_feature(enable = "sse2")] + #[target_feature(enable = "sse4.1")] pub(super) unsafe fn cmp_unaligned( src: &[u64], other: &[u64], From fc57ef81dc82e0360622dba7ac944d6e8f0cda4e Mon Sep 17 00:00:00 2001 From: juncheng Date: Sat, 27 Jun 2026 06:01:24 +0000 Subject: [PATCH 3/7] feat: add BitString::leading_zeros() and leading_ones() delegating to BitStr Co-authored-by: Claude Co-authored-by: DeepSeek AI --- .../impls_for_bit_arith/impls_for_count_ones.rs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/bit_string/impls_for_bit_arith/impls_for_count_ones.rs b/src/bit_string/impls_for_bit_arith/impls_for_count_ones.rs index 05031b5..08e164c 100644 --- a/src/bit_string/impls_for_bit_arith/impls_for_count_ones.rs +++ b/src/bit_string/impls_for_bit_arith/impls_for_count_ones.rs @@ -14,6 +14,22 @@ impl BitString { pub fn count_zeros(&self) -> usize { self.bit_len - self.count_ones() } + + /// Returns the number of consecutive `false` bits from the start. + /// + /// Delegates to [`BitStr::leading_zeros`](crate::BitStr::leading_zeros). + #[inline] + pub fn leading_zeros(&self) -> usize { + self.as_bit_str().leading_zeros() + } + + /// Returns the number of consecutive `true` bits from the start. + /// + /// Delegates to [`BitStr::leading_ones`](crate::BitStr::leading_ones). + #[inline] + pub fn leading_ones(&self) -> usize { + self.as_bit_str().leading_ones() + } } #[cfg(test)] From 13d232c61fb643ebc5862d8c8f61970f149ea765 Mon Sep 17 00:00:00 2001 From: juncheng Date: Sat, 27 Jun 2026 06:13:37 +0000 Subject: [PATCH 4/7] refactor: separate BitString::{leading_zeros,leading_ones} into own file MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Move the two delegation methods from impls_for_count_ones.rs into a new impls_for_leading_zeros.rs — mirroring the BitStr side. No separate tests needed; coverage is on the BitStr implementation. Co-authored-by: Claude Co-authored-by: DeepSeek AI --- src/bit_string/impls_for_bit_arith.rs | 1 + .../impls_for_count_ones.rs | 16 ---------------- .../impls_for_leading_zeros.rs | 19 +++++++++++++++++++ 3 files changed, 20 insertions(+), 16 deletions(-) create mode 100644 src/bit_string/impls_for_bit_arith/impls_for_leading_zeros.rs diff --git a/src/bit_string/impls_for_bit_arith.rs b/src/bit_string/impls_for_bit_arith.rs index 4f0ee32..5e26ab7 100644 --- a/src/bit_string/impls_for_bit_arith.rs +++ b/src/bit_string/impls_for_bit_arith.rs @@ -1,5 +1,6 @@ mod impls_for_and; mod impls_for_count_ones; +mod impls_for_leading_zeros; mod impls_for_not; mod impls_for_or; mod impls_for_shl; diff --git a/src/bit_string/impls_for_bit_arith/impls_for_count_ones.rs b/src/bit_string/impls_for_bit_arith/impls_for_count_ones.rs index 08e164c..05031b5 100644 --- a/src/bit_string/impls_for_bit_arith/impls_for_count_ones.rs +++ b/src/bit_string/impls_for_bit_arith/impls_for_count_ones.rs @@ -14,22 +14,6 @@ impl BitString { pub fn count_zeros(&self) -> usize { self.bit_len - self.count_ones() } - - /// Returns the number of consecutive `false` bits from the start. - /// - /// Delegates to [`BitStr::leading_zeros`](crate::BitStr::leading_zeros). - #[inline] - pub fn leading_zeros(&self) -> usize { - self.as_bit_str().leading_zeros() - } - - /// Returns the number of consecutive `true` bits from the start. - /// - /// Delegates to [`BitStr::leading_ones`](crate::BitStr::leading_ones). - #[inline] - pub fn leading_ones(&self) -> usize { - self.as_bit_str().leading_ones() - } } #[cfg(test)] diff --git a/src/bit_string/impls_for_bit_arith/impls_for_leading_zeros.rs b/src/bit_string/impls_for_bit_arith/impls_for_leading_zeros.rs new file mode 100644 index 0000000..f4b6fbe --- /dev/null +++ b/src/bit_string/impls_for_bit_arith/impls_for_leading_zeros.rs @@ -0,0 +1,19 @@ +use super::BitString; + +impl BitString { + /// Returns the number of consecutive `false` bits from the start. + /// + /// Delegates to [`BitStr::leading_zeros`](crate::BitStr::leading_zeros). + #[inline] + pub fn leading_zeros(&self) -> usize { + self.as_bit_str().leading_zeros() + } + + /// Returns the number of consecutive `true` bits from the start. + /// + /// Delegates to [`BitStr::leading_ones`](crate::BitStr::leading_ones). + #[inline] + pub fn leading_ones(&self) -> usize { + self.as_bit_str().leading_ones() + } +} From 5517888b9c7ac9cd5aa6f62baa16679a8510914d Mon Sep 17 00:00:00 2001 From: juncheng Date: Sat, 27 Jun 2026 06:27:19 +0000 Subject: [PATCH 5/7] =?UTF-8?q?refactor:=20remove=20redundant=20BitString?= =?UTF-8?q?=20tests=20=E2=80=94=20coverage=20on=20BitStr=20side?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit BitString methods that delegate one-to-one to BitStr (matches_at, starts_with, ends_with, contains, find, rfind, strip_prefix, strip_suffix, cmp) no longer carry separate test modules. The logic is already exercised by the BitStr test suite. Co-authored-by: Claude Co-authored-by: DeepSeek AI --- Cargo.toml | 2 +- .../impls_for_matching/impls_for_find.rs | 9 -- .../impls_for_find/tests_for_contains.rs | 72 ------------ .../impls_for_find/tests_for_find.rs | 69 ------------ .../impls_for_find/tests_for_rfind.rs | 74 ------------ .../impls_for_matches_at.rs | 9 -- .../tests_for_ends_with.rs | 41 ------- .../tests_for_matches_at.rs | 26 ----- .../tests_for_starts_with.rs | 63 ----------- .../impls_for_matching/impls_for_strip.rs | 6 - .../impls_for_strip/tests_for_strip_prefix.rs | 50 --------- .../impls_for_strip/tests_for_strip_suffix.rs | 105 ------------------ src/bit_string/impls_for_ord.rs | 3 - src/bit_string/impls_for_ord/tests_for_ord.rs | 105 ------------------ 14 files changed, 1 insertion(+), 633 deletions(-) delete mode 100644 src/bit_string/impls_for_matching/impls_for_find/tests_for_contains.rs delete mode 100644 src/bit_string/impls_for_matching/impls_for_find/tests_for_find.rs delete mode 100644 src/bit_string/impls_for_matching/impls_for_find/tests_for_rfind.rs delete mode 100644 src/bit_string/impls_for_matching/impls_for_matches_at/tests_for_ends_with.rs delete mode 100644 src/bit_string/impls_for_matching/impls_for_matches_at/tests_for_matches_at.rs delete mode 100644 src/bit_string/impls_for_matching/impls_for_matches_at/tests_for_starts_with.rs delete mode 100644 src/bit_string/impls_for_matching/impls_for_strip/tests_for_strip_prefix.rs delete mode 100644 src/bit_string/impls_for_matching/impls_for_strip/tests_for_strip_suffix.rs delete mode 100644 src/bit_string/impls_for_ord/tests_for_ord.rs diff --git a/Cargo.toml b/Cargo.toml index b22c123..63f6567 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,7 +8,7 @@ repository = "https://github.com/jcfangc/bit-string" license = "MIT OR Apache-2.0" keywords = ["bitstring", "bits", "bitset", "sequence"] categories = ["data-structures", "no-std"] -exclude = ["/benches", "/src/**/tests_for_*"] +exclude = ["/benches", "/src/**/tests_for_*", ".github/"] [dependencies] diff --git a/src/bit_string/impls_for_matching/impls_for_find.rs b/src/bit_string/impls_for_matching/impls_for_find.rs index 7d3298e..012cf57 100644 --- a/src/bit_string/impls_for_matching/impls_for_find.rs +++ b/src/bit_string/impls_for_matching/impls_for_find.rs @@ -16,12 +16,3 @@ impl BitString { self.as_bit_str().rfind(needle) } } - -#[cfg(test)] -mod tests_for_contains; - -#[cfg(test)] -mod tests_for_find; - -#[cfg(test)] -mod tests_for_rfind; diff --git a/src/bit_string/impls_for_matching/impls_for_find/tests_for_contains.rs b/src/bit_string/impls_for_matching/impls_for_find/tests_for_contains.rs deleted file mode 100644 index 21eaa73..0000000 --- a/src/bit_string/impls_for_matching/impls_for_find/tests_for_contains.rs +++ /dev/null @@ -1,72 +0,0 @@ -use crate::BitString; - -#[test] -fn returns_true_for_empty_needle() { - let bits = BitString::try_from("101001").unwrap(); - let needle = BitString::new(); - - assert!(bits.contains(needle.as_bit_str())); -} - -#[test] -fn returns_true_when_needle_is_found_at_start() { - let bits = BitString::try_from("101001").unwrap(); - let needle = BitString::try_from("101").unwrap(); - - assert!(bits.contains(needle.as_bit_str())); -} - -#[test] -fn returns_true_when_needle_is_found_in_middle() { - let bits = BitString::try_from("00110110").unwrap(); - let needle = BitString::try_from("110").unwrap(); - - assert!(bits.contains(needle.as_bit_str())); -} - -#[test] -fn returns_true_when_needle_is_found_at_end() { - let bits = BitString::try_from("101001").unwrap(); - let needle = BitString::try_from("001").unwrap(); - - assert!(bits.contains(needle.as_bit_str())); -} - -#[test] -fn returns_true_when_needle_equals_self() { - let bits = BitString::try_from("101001").unwrap(); - let needle = BitString::try_from("101001").unwrap(); - - assert!(bits.contains(needle.as_bit_str())); -} - -#[test] -fn returns_false_when_needle_is_absent() { - let bits = BitString::try_from("101001").unwrap(); - let needle = BitString::try_from("111").unwrap(); - - assert!(!bits.contains(needle.as_bit_str())); -} - -#[test] -fn returns_false_when_needle_is_longer_than_self() { - let bits = BitString::try_from("101").unwrap(); - let needle = BitString::try_from("1010").unwrap(); - - assert!(!bits.contains(needle.as_bit_str())); -} - -#[test] -fn works_across_word_boundaries() { - let mut bits = BitString::zeros(130); - - bits.set(63, true); - bits.set(64, true); - bits.set(65, true); - - let present = BitString::try_from("01110").unwrap(); - let absent = BitString::try_from("11110").unwrap(); - - assert!(bits.contains(present.as_bit_str())); - assert!(!bits.contains(absent.as_bit_str())); -} diff --git a/src/bit_string/impls_for_matching/impls_for_find/tests_for_find.rs b/src/bit_string/impls_for_matching/impls_for_find/tests_for_find.rs deleted file mode 100644 index 2554a68..0000000 --- a/src/bit_string/impls_for_matching/impls_for_find/tests_for_find.rs +++ /dev/null @@ -1,69 +0,0 @@ -use crate::BitString; - -#[test] -fn empty_needle_matches_at_zero() { - let bits = BitString::try_from("101001").unwrap(); - let needle = BitString::new(); - - assert_eq!(bits.find(needle.as_bit_str()), Some(0)); -} - -#[test] -fn returns_none_when_needle_is_longer() { - let bits = BitString::try_from("101").unwrap(); - let needle = BitString::try_from("0101").unwrap(); - - assert_eq!(bits.find(needle.as_bit_str()), None); -} - -#[test] -fn finds_needle_at_start() { - let bits = BitString::try_from("101001").unwrap(); - let needle = BitString::try_from("101").unwrap(); - - assert_eq!(bits.find(needle.as_bit_str()), Some(0)); -} - -#[test] -fn finds_needle_in_middle() { - let bits = BitString::try_from("101001").unwrap(); - let needle = BitString::try_from("100").unwrap(); - - assert_eq!(bits.find(needle.as_bit_str()), Some(2)); -} - -#[test] -fn finds_needle_at_end() { - let bits = BitString::try_from("101001").unwrap(); - let needle = BitString::try_from("001").unwrap(); - - assert_eq!(bits.find(needle.as_bit_str()), Some(3)); -} - -#[test] -fn returns_none_when_needle_is_absent() { - let bits = BitString::try_from("101001").unwrap(); - let needle = BitString::try_from("111").unwrap(); - - assert_eq!(bits.find(needle.as_bit_str()), None); -} - -#[test] -fn returns_first_match_when_needle_occurs_multiple_times() { - let bits = BitString::try_from("101101101").unwrap(); - let needle = BitString::try_from("101").unwrap(); - - assert_eq!(bits.find(needle.as_bit_str()), Some(0)); -} - -#[test] -fn finds_match_across_word_boundary() { - let mut bits = BitString::zeros(130); - let needle = BitString::try_from("111").unwrap(); - - bits.set(63, true); - bits.set(64, true); - bits.set(65, true); - - assert_eq!(bits.find(needle.as_bit_str()), Some(63)); -} diff --git a/src/bit_string/impls_for_matching/impls_for_find/tests_for_rfind.rs b/src/bit_string/impls_for_matching/impls_for_find/tests_for_rfind.rs deleted file mode 100644 index 5188f89..0000000 --- a/src/bit_string/impls_for_matching/impls_for_find/tests_for_rfind.rs +++ /dev/null @@ -1,74 +0,0 @@ -use crate::BitString; - -#[test] -fn returns_len_for_empty_needle() { - let bits = BitString::try_from("101001").unwrap(); - let needle = BitString::new(); - - assert_eq!(bits.rfind(needle.as_bit_str()), Some(bits.bit_len())); -} - -#[test] -fn returns_none_when_needle_is_longer_than_self() { - let bits = BitString::try_from("101").unwrap(); - let needle = BitString::try_from("1010").unwrap(); - - assert_eq!(bits.rfind(needle.as_bit_str()), None); -} - -#[test] -fn returns_last_match_index() { - let bits = BitString::try_from("00110110").unwrap(); - let needle = BitString::try_from("110").unwrap(); - - assert_eq!(bits.rfind(needle.as_bit_str()), Some(5)); -} - -#[test] -fn returns_start_when_only_match_is_at_start() { - let bits = BitString::try_from("101001").unwrap(); - let needle = BitString::try_from("101").unwrap(); - - assert_eq!(bits.rfind(needle.as_bit_str()), Some(0)); -} - -#[test] -fn returns_end_match_when_match_is_at_end() { - let bits = BitString::try_from("101001").unwrap(); - let needle = BitString::try_from("001").unwrap(); - - assert_eq!(bits.rfind(needle.as_bit_str()), Some(3)); -} - -#[test] -fn returns_none_when_needle_is_absent() { - let bits = BitString::try_from("101001").unwrap(); - let needle = BitString::try_from("111").unwrap(); - - assert_eq!(bits.rfind(needle.as_bit_str()), None); -} - -#[test] -fn returns_zero_when_needle_equals_self() { - let bits = BitString::try_from("101001").unwrap(); - let needle = BitString::try_from("101001").unwrap(); - - assert_eq!(bits.rfind(needle.as_bit_str()), Some(0)); -} - -#[test] -fn works_across_word_boundaries() { - let mut bits = BitString::zeros(132); - - bits.set(63, true); - bits.set(64, true); - bits.set(65, true); - - bits.set(127, true); - bits.set(128, true); - bits.set(129, true); - - let needle = BitString::try_from("01110").unwrap(); - - assert_eq!(bits.rfind(needle.as_bit_str()), Some(126)); -} diff --git a/src/bit_string/impls_for_matching/impls_for_matches_at.rs b/src/bit_string/impls_for_matching/impls_for_matches_at.rs index 7dfc156..6d96876 100644 --- a/src/bit_string/impls_for_matching/impls_for_matches_at.rs +++ b/src/bit_string/impls_for_matching/impls_for_matches_at.rs @@ -19,12 +19,3 @@ impl BitString { self.as_bit_str().ends_with(suffix) } } - -#[cfg(test)] -mod tests_for_matches_at; - -#[cfg(test)] -mod tests_for_starts_with; - -#[cfg(test)] -mod tests_for_ends_with; diff --git a/src/bit_string/impls_for_matching/impls_for_matches_at/tests_for_ends_with.rs b/src/bit_string/impls_for_matching/impls_for_matches_at/tests_for_ends_with.rs deleted file mode 100644 index 2cbe30b..0000000 --- a/src/bit_string/impls_for_matching/impls_for_matches_at/tests_for_ends_with.rs +++ /dev/null @@ -1,41 +0,0 @@ -use crate::BitString; - -#[test] -fn returns_false_when_suffix_is_longer() { - let bits = BitString::try_from("101").unwrap(); - let suffix = BitString::try_from("0101").unwrap(); - - assert!(!bits.ends_with(suffix.as_bit_str())); -} - -#[test] -fn empty_suffix_always_matches() { - let bits = BitString::try_from("101").unwrap(); - let suffix = BitString::new(); - - assert!(bits.ends_with(suffix.as_bit_str())); -} - -#[test] -fn matches_suffix() { - let bits = BitString::try_from("101001").unwrap(); - let suffix = BitString::try_from("001").unwrap(); - - assert!(bits.ends_with(suffix.as_bit_str())); -} - -#[test] -fn rejects_non_suffix() { - let bits = BitString::try_from("101001").unwrap(); - let suffix = BitString::try_from("101").unwrap(); - - assert!(!bits.ends_with(suffix.as_bit_str())); -} - -#[test] -fn equal_bit_strings_match_as_suffix() { - let bits = BitString::try_from("101001").unwrap(); - let suffix = BitString::try_from("101001").unwrap(); - - assert!(bits.ends_with(suffix.as_bit_str())); -} diff --git a/src/bit_string/impls_for_matching/impls_for_matches_at/tests_for_matches_at.rs b/src/bit_string/impls_for_matching/impls_for_matches_at/tests_for_matches_at.rs deleted file mode 100644 index 88a93c7..0000000 --- a/src/bit_string/impls_for_matching/impls_for_matches_at/tests_for_matches_at.rs +++ /dev/null @@ -1,26 +0,0 @@ -use crate::BitString; - -#[test] -fn returns_false_when_index_is_past_len() { - let bits = BitString::try_from("1010").unwrap(); - let pattern = BitString::try_from("1").unwrap(); - - assert!(!bits.matches_at(bits.bit_len() + 1, pattern.as_bit_str())); -} - -#[test] -fn returns_false_when_pattern_does_not_fit_at_index() { - let bits = BitString::try_from("1010").unwrap(); - let pattern = BitString::try_from("10").unwrap(); - - assert!(!bits.matches_at(3, pattern.as_bit_str())); - assert!(!bits.matches_at(bits.bit_len(), pattern.as_bit_str())); -} - -#[test] -fn allows_empty_pattern_at_len() { - let bits = BitString::try_from("1010").unwrap(); - let pattern = BitString::new(); - - assert!(bits.matches_at(bits.bit_len(), pattern.as_bit_str())); -} diff --git a/src/bit_string/impls_for_matching/impls_for_matches_at/tests_for_starts_with.rs b/src/bit_string/impls_for_matching/impls_for_matches_at/tests_for_starts_with.rs deleted file mode 100644 index 6dec2de..0000000 --- a/src/bit_string/impls_for_matching/impls_for_matches_at/tests_for_starts_with.rs +++ /dev/null @@ -1,63 +0,0 @@ -use crate::BitString; - -#[test] -fn returns_true_for_empty_prefix() { - let bits = BitString::try_from("101001").unwrap(); - let prefix = BitString::new(); - - assert!(bits.starts_with(prefix.as_bit_str())); -} - -#[test] -fn returns_true_for_matching_prefix() { - let bits = BitString::try_from("101001").unwrap(); - let prefix = BitString::try_from("101").unwrap(); - - assert!(bits.starts_with(prefix.as_bit_str())); -} - -#[test] -fn returns_true_for_full_self_prefix() { - let bits = BitString::try_from("101001").unwrap(); - let prefix = BitString::try_from("101001").unwrap(); - - assert!(bits.starts_with(prefix.as_bit_str())); -} - -#[test] -fn returns_false_for_non_matching_prefix() { - let bits = BitString::try_from("101001").unwrap(); - let prefix = BitString::try_from("100").unwrap(); - - assert!(!bits.starts_with(prefix.as_bit_str())); -} - -#[test] -fn returns_false_when_prefix_is_longer_than_self() { - let bits = BitString::try_from("101").unwrap(); - let prefix = BitString::try_from("1010").unwrap(); - - assert!(!bits.starts_with(prefix.as_bit_str())); -} - -#[test] -fn works_across_word_boundaries() { - let mut bits = BitString::zeros(130); - - bits.set(0, true); - bits.set(63, true); - bits.set(64, true); - bits.set(65, true); - - let mut prefix = BitString::zeros(66); - prefix.set(0, true); - prefix.set(63, true); - prefix.set(64, true); - prefix.set(65, true); - - assert!(bits.starts_with(prefix.as_bit_str())); - - prefix.set(62, true); - - assert!(!bits.starts_with(prefix.as_bit_str())); -} diff --git a/src/bit_string/impls_for_matching/impls_for_strip.rs b/src/bit_string/impls_for_matching/impls_for_strip.rs index 0a7a29d..e28d350 100644 --- a/src/bit_string/impls_for_matching/impls_for_strip.rs +++ b/src/bit_string/impls_for_matching/impls_for_strip.rs @@ -15,9 +15,3 @@ impl BitString { .then(|| self.slice_until(self.bit_len - suffix.bit_len)) } } - -#[cfg(test)] -mod tests_for_strip_prefix; - -#[cfg(test)] -mod tests_for_strip_suffix; diff --git a/src/bit_string/impls_for_matching/impls_for_strip/tests_for_strip_prefix.rs b/src/bit_string/impls_for_matching/impls_for_strip/tests_for_strip_prefix.rs deleted file mode 100644 index d1b8af3..0000000 --- a/src/bit_string/impls_for_matching/impls_for_strip/tests_for_strip_prefix.rs +++ /dev/null @@ -1,50 +0,0 @@ -use alloc::string::ToString; - -use crate::BitString; - -#[test] -fn empty_prefix_returns_original_copy() { - let bits = BitString::try_from("101001").unwrap(); - let prefix = BitString::new(); - - let stripped = bits.strip_prefix(prefix.as_bit_str()).unwrap(); - - assert_eq!(stripped.to_string(), "101001"); -} - -#[test] -fn strips_matching_prefix() { - let bits = BitString::try_from("101001").unwrap(); - let prefix = BitString::try_from("101").unwrap(); - - let stripped = bits.strip_prefix(prefix.as_bit_str()).unwrap(); - - assert_eq!(stripped.to_string(), "001"); -} - -#[test] -fn equal_prefix_returns_empty_bit_string() { - let bits = BitString::try_from("101001").unwrap(); - let prefix = BitString::try_from("101001").unwrap(); - - let stripped = bits.strip_prefix(prefix.as_bit_str()).unwrap(); - - assert!(stripped.is_empty()); - assert_eq!(stripped.to_string(), ""); -} - -#[test] -fn returns_none_when_prefix_does_not_match() { - let bits = BitString::try_from("101001").unwrap(); - let prefix = BitString::try_from("100").unwrap(); - - assert_eq!(bits.strip_prefix(prefix.as_bit_str()), None); -} - -#[test] -fn returns_none_when_prefix_is_longer() { - let bits = BitString::try_from("101").unwrap(); - let prefix = BitString::try_from("1010").unwrap(); - - assert_eq!(bits.strip_prefix(prefix.as_bit_str()), None); -} diff --git a/src/bit_string/impls_for_matching/impls_for_strip/tests_for_strip_suffix.rs b/src/bit_string/impls_for_matching/impls_for_strip/tests_for_strip_suffix.rs deleted file mode 100644 index b893d8d..0000000 --- a/src/bit_string/impls_for_matching/impls_for_strip/tests_for_strip_suffix.rs +++ /dev/null @@ -1,105 +0,0 @@ -use alloc::string::ToString; - -use crate::BitString; - -#[test] -fn strips_empty_suffix_by_returning_self_copy() { - let bits = BitString::try_from("101001").unwrap(); - let suffix = BitString::new(); - - let stripped = bits.strip_suffix(suffix.as_bit_str()).unwrap(); - - assert_eq!(stripped.to_string(), "101001"); -} - -#[test] -fn strips_matching_suffix() { - let bits = BitString::try_from("101001").unwrap(); - let suffix = BitString::try_from("001").unwrap(); - - let stripped = bits.strip_suffix(suffix.as_bit_str()).unwrap(); - - assert_eq!(stripped.to_string(), "101"); -} - -#[test] -fn strips_full_self_suffix_to_empty() { - let bits = BitString::try_from("101001").unwrap(); - let suffix = BitString::try_from("101001").unwrap(); - - let stripped = bits.strip_suffix(suffix.as_bit_str()).unwrap(); - - assert!(stripped.is_empty()); - assert_eq!(stripped.to_string(), ""); -} - -#[test] -fn returns_none_for_non_matching_suffix() { - let bits = BitString::try_from("101001").unwrap(); - let suffix = BitString::try_from("101").unwrap(); - - assert_eq!(bits.strip_suffix(suffix.as_bit_str()), None); -} - -#[test] -fn returns_none_when_suffix_is_longer_than_self() { - let bits = BitString::try_from("101").unwrap(); - let suffix = BitString::try_from("0101").unwrap(); - - assert_eq!(bits.strip_suffix(suffix.as_bit_str()), None); -} - -#[test] -fn result_is_independent_from_original() { - let mut bits = BitString::try_from("101001").unwrap(); - let suffix = BitString::try_from("001").unwrap(); - - let mut stripped = bits.strip_suffix(suffix.as_bit_str()).unwrap(); - - bits.set(0, false); - stripped.set(0, false); - - assert_eq!(bits.to_string(), "001001"); - assert_eq!(stripped.to_string(), "001"); -} - -#[test] -fn works_across_word_boundaries() { - let mut bits = BitString::zeros(130); - - bits.set(0, true); - bits.set(63, true); - bits.set(64, true); - bits.set(65, true); - bits.set(129, true); - - let mut suffix = BitString::zeros(67); - suffix.set(0, true); - suffix.set(1, true); - suffix.set(2, true); - suffix.set(66, true); - - let stripped = bits.strip_suffix(suffix.as_bit_str()).unwrap(); - - assert_eq!(stripped.bit_len(), 63); - assert_eq!(stripped.get(0), Some(true)); - assert_eq!(stripped.get(62), Some(false)); -} - -#[test] -fn returns_none_for_almost_matching_cross_word_suffix() { - let mut bits = BitString::zeros(130); - - bits.set(63, true); - bits.set(64, true); - bits.set(65, true); - bits.set(129, true); - - let mut suffix = BitString::zeros(67); - suffix.set(0, true); - suffix.set(1, true); - suffix.set(2, true); - suffix.set(65, true); - - assert_eq!(bits.strip_suffix(suffix.as_bit_str()), None); -} diff --git a/src/bit_string/impls_for_ord.rs b/src/bit_string/impls_for_ord.rs index ccfeb6f..1ca6698 100644 --- a/src/bit_string/impls_for_ord.rs +++ b/src/bit_string/impls_for_ord.rs @@ -15,6 +15,3 @@ impl Ord for BitString { self.as_bit_str().cmp(&other.as_bit_str()) } } - -#[cfg(test)] -mod tests_for_ord; diff --git a/src/bit_string/impls_for_ord/tests_for_ord.rs b/src/bit_string/impls_for_ord/tests_for_ord.rs deleted file mode 100644 index efa3611..0000000 --- a/src/bit_string/impls_for_ord/tests_for_ord.rs +++ /dev/null @@ -1,105 +0,0 @@ -use alloc::string::{String, ToString}; -use alloc::vec; -use alloc::vec::Vec; -use core::cmp::Ordering; - -use crate::BitString; - -// --------------------------------------------------------------------------- -// BitString Ord — delegates to as_bit_str().cmp() -// --------------------------------------------------------------------------- - -#[test] -fn equal_bit_strings_are_ordered_equal() { - let a = BitString::try_from("101001").unwrap(); - let b = BitString::try_from("101001").unwrap(); - assert_eq!(a, b); - assert_eq!(a.cmp(&b), Ordering::Equal); -} - -#[test] -fn less_greater() { - let a = BitString::try_from("100").unwrap(); - let b = BitString::try_from("101").unwrap(); - assert!(a < b); - assert!(b > a); -} - -#[test] -fn prefix_shorter_is_less() { - let a = BitString::try_from("101").unwrap(); - let b = BitString::try_from("1010").unwrap(); - assert!(a < b); - assert!(b > a); -} - -#[test] -fn empty_is_less_than_nonempty() { - let a = BitString::new(); - let b = BitString::try_from("0").unwrap(); - assert!(a < b); - assert!(a <= b); -} - -#[test] -fn empty_equals_empty() { - let a = BitString::new(); - let b = BitString::new(); - assert_eq!(a.cmp(&b), Ordering::Equal); -} - -#[test] -fn sort_bit_strings() { - let mut strings: Vec = ["101", "001", "111", "000", "010"] - .into_iter() - .map(|s| BitString::try_from(s).unwrap()) - .collect(); - strings.sort(); - - let sorted: Vec = strings.iter().map(|bs| bs.to_string()).collect(); - assert_eq!(sorted, vec!["000", "001", "010", "101", "111"]); -} - -#[test] -fn min_max() { - let a = BitString::try_from("0011").unwrap(); - let b = BitString::try_from("1100").unwrap(); - let a2 = a.clone(); - let b2 = b.clone(); - let b3 = b.clone(); - assert_eq!(a.min(b), a2); - assert_eq!(a2.max(b2), b3); -} - -#[test] -fn cmp_consistent_with_bit_str() { - for bits in ["", "0", "1", "10", "01", "111", "000", "101010", "1100"] { - let x = BitString::try_from(bits).unwrap(); - let y = BitString::try_from(bits).unwrap(); - assert_eq!( - x.cmp(&y), - x.as_bit_str().cmp(&y.as_bit_str()), - "mismatch for {bits:?}" - ); - } -} - -#[test] -fn cross_word_different() { - let mut a = BitString::zeros(128); - let mut b = BitString::zeros(128); - a.set(70, true); - b.set(71, true); - assert_eq!(a.as_bit_str().cmp(&b.as_bit_str()), a.cmp(&b)); -} - -// --------------------------------------------------------------------------- -// BitString PartialOrd — delegates to cmp() -// --------------------------------------------------------------------------- - -#[test] -fn partial_cmp_is_some() { - let a = BitString::try_from("101").unwrap(); - let b = BitString::try_from("100").unwrap(); - assert_eq!(a.partial_cmp(&b), Some(Ordering::Greater)); -} From 202c4efb4fe573ab248a8cd460c177abc59d5a0c Mon Sep 17 00:00:00 2001 From: juncheng Date: Sat, 27 Jun 2026 06:46:22 +0000 Subject: [PATCH 6/7] feat: add trailing_zeros/trailing_ones with SIMD reverse scan MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - BitStr::trailing_zeros() / trailing_ones() count consecutive bits from the end of a view — mirroring leading_* but scanning from the last bit backwards (uses u64::leading_zeros for within-word scan) - BitsArith::trailing_zero_words() / trailing_one_words() with trailing_value_words(words, fill) dispatch — AVX2/SSE4.1/NEON backends process chunks in reverse order - BitString delegates to BitStr - 27 unit tests + 2 AVX2 backend equivalence tests for trailing scan Co-authored-by: Claude Co-authored-by: DeepSeek AI --- .../impls_for_leading_zeros.rs | 147 +++++++++++++++++ .../tests_for_trailing_ones.rs | 115 +++++++++++++ .../tests_for_trailing_zeros.rs | 148 +++++++++++++++++ .../impls_for_leading_zeros.rs | 16 ++ src/traits/bits_arith.rs | 11 ++ .../funcs_for_leading_value_words.rs | 154 ++++++++++++++++++ .../tests_for_backend_equivalence.rs | 82 ++++++++++ src/traits/bits_arith/impls_for_u64_slice.rs | 10 ++ 8 files changed, 683 insertions(+) create mode 100644 src/bit_str/impls_for_bit_arith/impls_for_leading_zeros/tests_for_trailing_ones.rs create mode 100644 src/bit_str/impls_for_bit_arith/impls_for_leading_zeros/tests_for_trailing_zeros.rs diff --git a/src/bit_str/impls_for_bit_arith/impls_for_leading_zeros.rs b/src/bit_str/impls_for_bit_arith/impls_for_leading_zeros.rs index 8c819a6..c8c0737 100644 --- a/src/bit_str/impls_for_bit_arith/impls_for_leading_zeros.rs +++ b/src/bit_str/impls_for_bit_arith/impls_for_leading_zeros.rs @@ -117,6 +117,147 @@ impl<'bs> BitStr<'bs> { } leading_value_count(self.source.words(), self.start, self.bit_len, !0) } + + /// Returns the number of consecutive `false` bits from the **end** of this + /// view. + /// + /// # Examples + /// + /// ``` + /// use bit_string::BitString; + /// + /// let bits = BitString::try_from("10000").unwrap(); + /// let v = bits.as_bit_str(); + /// assert_eq!(v.trailing_zeros(), 4); + /// ``` + #[inline] + pub fn trailing_zeros(&self) -> usize { + if self.bit_len == 0 { + return 0; + } + trailing_value_count(self.source.words(), self.start, self.bit_len, 0) + } + + /// Returns the number of consecutive `true` bits from the **end** of this + /// view. + /// + /// # Examples + /// + /// ``` + /// use bit_string::BitString; + /// + /// let bits = BitString::try_from("01111").unwrap(); + /// let v = bits.as_bit_str(); + /// assert_eq!(v.trailing_ones(), 4); + /// ``` + #[inline] + pub fn trailing_ones(&self) -> usize { + if self.bit_len == 0 { + return 0; + } + trailing_value_count(self.source.words(), self.start, self.bit_len, !0) + } +} + +// --------------------------------------------------------------------------- +// Trailing helper — scans from the end backwards +// --------------------------------------------------------------------------- + +/// Counts consecutive bits equal to `fill` from the end of a view. +#[inline] +fn trailing_value_count(words: &[u64], start: usize, bit_len: usize, fill: u64) -> usize { + let end = start + bit_len; + let start_offset = start % WORD_BITS; + let end_rem = end % WORD_BITS; + let last_wi = (end - 1) / WORD_BITS; + let start_wi = start / WORD_BITS; + + let mut scanned = 0usize; + + // Last word (partial, if end_rem != 0). + if end_rem != 0 { + let last_limit = if last_wi == start_wi { + end_rem - start_offset + } else { + end_rem + }; + let last_val = if last_wi == start_wi { + words[last_wi] >> start_offset + } else { + words[last_wi] & low_mask(end_rem) + }; + let last_count = count_leading_within(last_val, last_limit, fill); + if last_count < last_limit { + return last_count; + } + scanned += last_limit; + + // Entire view was within a single partial word. + if last_wi == start_wi { + return scanned.min(bit_len); + } + } + + // Full middle words — SIMD-accelerated, from right to left. + let wi_end = if end_rem != 0 { last_wi - 1 } else { last_wi }; + let mid_first = if start_offset > 0 { + start_wi + 1 + } else { + start_wi + }; + if wi_end >= mid_first { + let nr_words = wi_end + 1 - mid_first; + let trailing_w = if fill == 0 { + words[mid_first..=wi_end].trailing_zero_words() + } else { + words[mid_first..=wi_end].trailing_one_words() + }; + if trailing_w < nr_words { + let hit_wi = wi_end - trailing_w; + scanned += trailing_w * WORD_BITS; + return scanned + count_leading(words[hit_wi], fill).min(WORD_BITS); + } + scanned += trailing_w * WORD_BITS; + } + + // First word (partial, if start_offset != 0 and not already handled). + if start_offset > 0 { + let first_limit = WORD_BITS - start_offset; + let first_val = words[start_wi] >> start_offset; + let first_count = count_leading_within(first_val, first_limit, fill); + scanned += first_count.min(first_limit); + } + + scanned.min(bit_len) +} + +/// Counts leading bits of a given value within a full u64 word. +/// +/// `fill = 0` → `leading_zeros`, `fill = !0` → `leading_ones`. +#[inline] +fn count_leading(val: u64, fill: u64) -> usize { + if fill == 0 { + val.leading_zeros() as usize + } else { + (!val).leading_zeros() as usize + } +} + +/// Counts leading bits of a value within its highest `limit` bits. +/// +/// Shifts valid bits to the top of the u64 so that [`u64::leading_zeros`] +/// counts from the highest valid bit downwards. +#[inline] +fn count_leading_within(val: u64, limit: usize, fill: u64) -> usize { + if limit == 0 { + return 0; + } + let shifted = val << (WORD_BITS - limit); + if fill == 0 { + (shifted.leading_zeros() as usize).min(limit) + } else { + ((!shifted).leading_zeros() as usize).min(limit) + } } #[cfg(test)] @@ -124,3 +265,9 @@ mod tests_for_leading_zeros; #[cfg(test)] mod tests_for_leading_ones; + +#[cfg(test)] +mod tests_for_trailing_zeros; + +#[cfg(test)] +mod tests_for_trailing_ones; diff --git a/src/bit_str/impls_for_bit_arith/impls_for_leading_zeros/tests_for_trailing_ones.rs b/src/bit_str/impls_for_bit_arith/impls_for_leading_zeros/tests_for_trailing_ones.rs new file mode 100644 index 0000000..21eea89 --- /dev/null +++ b/src/bit_str/impls_for_bit_arith/impls_for_leading_zeros/tests_for_trailing_ones.rs @@ -0,0 +1,115 @@ +use int_interval::UsizeCO; + +use crate::BitString; + +#[test] +fn empty_view_returns_zero() { + let bits = BitString::try_from("01010").unwrap(); + let v = bits.as_bit_str().slice(UsizeCO::try_new(10, 20).unwrap()); + assert_eq!(v.trailing_ones(), 0); +} + +#[test] +fn ends_with_zero() { + let bits = BitString::try_from("10110").unwrap(); + assert_eq!(bits.as_bit_str().trailing_ones(), 0); +} + +#[test] +fn trailing_one_run() { + let bits = BitString::try_from("01011").unwrap(); + assert_eq!(bits.as_bit_str().trailing_ones(), 2); +} + +#[test] +fn single_one() { + let bits = BitString::ones(1); + assert_eq!(bits.as_bit_str().trailing_ones(), 1); +} + +#[test] +fn single_zero() { + let bits = BitString::zeros(1); + assert_eq!(bits.as_bit_str().trailing_ones(), 0); +} + +#[test] +fn all_ones_at_various_lengths() { + for len in [1, 63, 64, 65, 127, 128, 129, 130] { + let bits = BitString::ones(len); + assert_eq!(bits.as_bit_str().trailing_ones(), len, "len={len}"); + } +} + +#[test] +fn all_zeros_at_various_lengths() { + for len in [1, 63, 64, 65, 127, 128, 129, 130] { + let bits = BitString::zeros(len); + assert_eq!(bits.as_bit_str().trailing_ones(), 0, "len={len}"); + } +} + +#[test] +fn last_zero_cross_word() { + let mut bits = BitString::ones(130); + bits.set(30, false); + assert_eq!(bits.as_bit_str().trailing_ones(), 99); +} + +#[test] +fn unaligned_view() { + let mut bits = BitString::ones(130); + bits.set(120, false); + let v = bits.as_bit_str().slice(UsizeCO::try_new(3, 130).unwrap()); + assert_eq!(v.trailing_ones(), 9); +} + +#[test] +fn unaligned_all_ones() { + let bits = BitString::ones(200); + let v = bits.as_bit_str().slice(UsizeCO::try_new(3, 130).unwrap()); + assert_eq!(v.trailing_ones(), 127); +} + +#[test] +fn unaligned_single_word() { + let bits = BitString::try_from("11100000").unwrap(); + // bits: 1 1 1 0 0 0 0 0 + let v = bits.as_bit_str().slice(UsizeCO::try_new(0, 5).unwrap()); + // "11100" → trailing ones = 0 + assert_eq!(v.trailing_ones(), 0); + + let v = bits.as_bit_str().slice(UsizeCO::try_new(0, 3).unwrap()); + // "111" → trailing ones = 3 + assert_eq!(v.trailing_ones(), 3); +} + +#[test] +fn invariant_trailing_ones_bounds() { + let mut bits = BitString::ones(200); + for i in (1..200).step_by(7) { + bits.set(i, false); + } + let full = bits.as_bit_str(); + + for start in [0, 1, 5, 63, 64, 65, 127, 128] { + for len in [10, 63, 64, 65, 128, 129] { + let end = (start + len).min(full.bit_len()); + if start == end { + continue; + } + let v = full.slice(UsizeCO::try_new(start, end).unwrap()); + let to = v.trailing_ones(); + assert!(to <= v.bit_len(), "start={start} end={end} to={to}"); + if v.is_all_ones() { + assert_eq!(to, v.bit_len(), "all ones: start={start} end={end}"); + } else { + assert_eq!( + v.get(v.bit_len() - 1 - to), + Some(false), + "start={start} end={end} to={to}" + ); + } + } + } +} diff --git a/src/bit_str/impls_for_bit_arith/impls_for_leading_zeros/tests_for_trailing_zeros.rs b/src/bit_str/impls_for_bit_arith/impls_for_leading_zeros/tests_for_trailing_zeros.rs new file mode 100644 index 0000000..ae8d5a1 --- /dev/null +++ b/src/bit_str/impls_for_bit_arith/impls_for_leading_zeros/tests_for_trailing_zeros.rs @@ -0,0 +1,148 @@ +use int_interval::UsizeCO; + +use crate::BitString; + +/// An empty view has no trailing zeros. +#[test] +fn empty_view_returns_zero() { + let bits = BitString::try_from("10110").unwrap(); + let v = bits.as_bit_str().slice(UsizeCO::try_new(10, 20).unwrap()); + assert_eq!(v.trailing_zeros(), 0); +} + +/// Ends with a 1 → zero trailing zeros. +#[test] +fn ends_with_one() { + let bits = BitString::try_from("11001").unwrap(); + assert_eq!(bits.as_bit_str().trailing_zeros(), 0); +} + +/// Ends with several zeros. +#[test] +fn trailing_zero_run() { + let bits = BitString::try_from("10100").unwrap(); + assert_eq!(bits.as_bit_str().trailing_zeros(), 2); +} + +/// Single zero bit. +#[test] +fn single_zero() { + let bits = BitString::zeros(1); + assert_eq!(bits.as_bit_str().trailing_zeros(), 1); +} + +/// Single one bit. +#[test] +fn single_one() { + let bits = BitString::ones(1); + assert_eq!(bits.as_bit_str().trailing_zeros(), 0); +} + +/// All zeros at various lengths. +#[test] +fn all_zeros_at_various_lengths() { + for len in [1, 63, 64, 65, 127, 128, 129, 130] { + let bits = BitString::zeros(len); + assert_eq!(bits.as_bit_str().trailing_zeros(), len, "len={len}"); + } +} + +/// All ones at various lengths. +#[test] +fn all_ones_at_various_lengths() { + for len in [1, 63, 64, 65, 127, 128, 129, 130] { + let bits = BitString::ones(len); + assert_eq!(bits.as_bit_str().trailing_zeros(), 0, "len={len}"); + } +} + +/// Last 1 is deep in the first word (left side). +#[test] +fn last_one_in_first_word() { + let mut bits = BitString::zeros(130); + bits.set(30, true); + assert_eq!(bits.as_bit_str().trailing_zeros(), 99); // bits 31..129 are zero +} + +/// Last 1 at word boundary. +#[test] +fn last_one_at_word_boundary() { + let mut bits = BitString::zeros(130); + bits.set(63, true); + assert_eq!(bits.as_bit_str().trailing_zeros(), 66); // bits 64..129 +} + +/// Unaligned view. +#[test] +fn unaligned_start() { + let mut bits = BitString::zeros(130); + bits.set(120, true); // last 1 at bit 120 + // View bits 3..130 → bit_len 127 + let v = bits.as_bit_str().slice(UsizeCO::try_new(3, 130).unwrap()); + assert_eq!(v.trailing_zeros(), 9); // bits 121..129 in view +} + +/// Unaligned all zeros. +#[test] +fn unaligned_all_zeros() { + let bits = BitString::zeros(200); + let v = bits.as_bit_str().slice(UsizeCO::try_new(3, 130).unwrap()); + assert_eq!(v.trailing_zeros(), 127); +} + +/// Unaligned single word. +#[test] +fn unaligned_single_word() { + let bits = BitString::try_from("00011111").unwrap(); + // bits: 0 0 0 1 1 1 1 1 + + // View bits 0..5 → "00011" + let v = bits.as_bit_str().slice(UsizeCO::try_new(0, 5).unwrap()); + assert_eq!(v.trailing_zeros(), 0); // ends with 1 + + // View bits 0..3 → "000" + let v = bits.as_bit_str().slice(UsizeCO::try_new(0, 3).unwrap()); + assert_eq!(v.trailing_zeros(), 3); +} + +/// First bit is one (all other zeros). +#[test] +fn first_bit_is_one() { + let mut bits = BitString::zeros(130); + // Set the leftmost bit. + bits.set(0, true); + // Bits 1..129 are all zeros, so trailing from the right = 129. + assert_eq!(bits.as_bit_str().trailing_zeros(), 129); +} + +/// Invariant: trailing_zeros ≤ bit_len and trailing_zeros == bit_len iff all zeros. +#[test] +fn invariant_trailing_zeros_bounds() { + let mut bits = BitString::zeros(200); + for i in (0..200).step_by(7) { + bits.set(i, true); + } + let full = bits.as_bit_str(); + + for start in [0, 1, 5, 63, 64, 65, 127, 128] { + for len in [10, 63, 64, 65, 128, 129] { + let end = (start + len).min(full.bit_len()); + if start == end { + continue; + } + let v = full.slice(UsizeCO::try_new(start, end).unwrap()); + let tz = v.trailing_zeros(); + assert!(tz <= v.bit_len(), "start={start} end={end} tz={tz}"); + if v.is_all_zeros() { + assert_eq!(tz, v.bit_len(), "all zeros: start={start} end={end}"); + } else { + // The bit at `bit_len - 1 - tz` must be 1. + assert_eq!( + v.get(v.bit_len() - 1 - tz), + Some(true), + "start={start} end={end} tz={tz}" + ); + } + } + } +} diff --git a/src/bit_string/impls_for_bit_arith/impls_for_leading_zeros.rs b/src/bit_string/impls_for_bit_arith/impls_for_leading_zeros.rs index f4b6fbe..cc15022 100644 --- a/src/bit_string/impls_for_bit_arith/impls_for_leading_zeros.rs +++ b/src/bit_string/impls_for_bit_arith/impls_for_leading_zeros.rs @@ -16,4 +16,20 @@ impl BitString { pub fn leading_ones(&self) -> usize { self.as_bit_str().leading_ones() } + + /// Returns the number of consecutive `false` bits from the end. + /// + /// Delegates to [`BitStr::trailing_zeros`](crate::BitStr::trailing_zeros). + #[inline] + pub fn trailing_zeros(&self) -> usize { + self.as_bit_str().trailing_zeros() + } + + /// Returns the number of consecutive `true` bits from the end. + /// + /// Delegates to [`BitStr::trailing_ones`](crate::BitStr::trailing_ones). + #[inline] + pub fn trailing_ones(&self) -> usize { + self.as_bit_str().trailing_ones() + } } diff --git a/src/traits/bits_arith.rs b/src/traits/bits_arith.rs index f081e27..bb1e6e5 100644 --- a/src/traits/bits_arith.rs +++ b/src/traits/bits_arith.rs @@ -61,6 +61,17 @@ pub(crate) trait BitsArith { /// All words up to (but not including) the returned index are all-ones. /// If the return value equals `self.len()`, every word is all-ones. fn leading_one_words(&self) -> usize; + + /// Returns the number of consecutive zero words at the **end** of `self`. + /// + /// All words from `self.len() - count` onwards are zero. + fn trailing_zero_words(&self) -> usize; + + /// Returns the number of consecutive all-ones words at the **end** of + /// `self`. + /// + /// All words from `self.len() - count` onwards are all-ones. + fn trailing_one_words(&self) -> usize; } pub(crate) mod funcs_for_binary_core; diff --git a/src/traits/bits_arith/funcs_for_leading_value_words.rs b/src/traits/bits_arith/funcs_for_leading_value_words.rs index 4158643..a0a2600 100644 --- a/src/traits/bits_arith/funcs_for_leading_value_words.rs +++ b/src/traits/bits_arith/funcs_for_leading_value_words.rs @@ -52,6 +52,49 @@ pub(crate) fn leading_value_words(words: &[u64], fill: u64) -> usize { scalar::scan(words, fill) } +/// Returns the number of consecutive words equal to `fill` at the **end** of +/// `words`. +/// +/// All words from `words.len() - count` onwards equal `fill`. If the return +/// value equals `words.len()`, every word equals `fill`. +#[inline] +pub(crate) fn trailing_value_words(words: &[u64], fill: u64) -> usize { + if words.len() < SMALL_WORDS { + return scalar::scan_rev(words, fill); + } + + #[cfg(all( + any(target_arch = "x86", target_arch = "x86_64"), + target_feature = "avx2" + ))] + { + unsafe { + return avx2::scan_rev(words, fill); + } + } + + #[cfg(all( + any(target_arch = "x86", target_arch = "x86_64"), + target_feature = "sse4.1", + not(target_feature = "avx2") + ))] + { + unsafe { + return sse41::scan_rev(words, fill); + } + } + + #[cfg(all(target_arch = "aarch64", target_feature = "neon"))] + { + unsafe { + return neon::scan_rev(words, fill); + } + } + + #[allow(unused)] + scalar::scan_rev(words, fill) +} + // --------------------------------------------------------------------------- // Scalar // --------------------------------------------------------------------------- @@ -66,6 +109,16 @@ mod scalar { } words.len() } + + #[inline] + pub(super) fn scan_rev(words: &[u64], fill: u64) -> usize { + for i in 0..words.len() { + if words[words.len() - 1 - i] != fill { + return i; + } + } + words.len() + } } // --------------------------------------------------------------------------- @@ -129,6 +182,42 @@ mod avx2 { count + super::scalar::scan(&words[done..], fill) } } + + /// AVX2 backend — reverse scan. + #[target_feature(enable = "avx2")] + pub(super) unsafe fn scan_rev(words: &[u64], fill: u64) -> usize { + unsafe { + let fill_vec = _mm256_set1_epi64x(fill as i64); + let full_chunks = words.len() / LANES; + let done = full_chunks * LANES; + + // Process the partial tail first. + let tail_count = super::scalar::scan_rev(&words[done..], fill); + if tail_count < words.len() - done { + return tail_count; + } + let mut count = tail_count; + + // Full chunks from right to left. + for chunk in (0..full_chunks).rev() { + let offset = chunk * LANES; + let data = _mm256_loadu_si256(words.as_ptr().add(offset).cast::<__m256i>()); + let cmp = _mm256_cmpeq_epi64(data, fill_vec); + let mask = _mm256_movemask_epi8(cmp) as u32; + if mask != 0xFFFF_FFFF { + for k in (0..LANES).rev() { + if (mask >> (k * 8)) & 0xFF != 0xFF { + return count + (LANES - 1 - k); + } + } + core::hint::unreachable_unchecked(); + } + count += LANES; + } + + count + } + } } // --------------------------------------------------------------------------- @@ -186,6 +275,40 @@ mod sse41 { count + super::scalar::scan(&words[done..], fill) } } + + /// SSE4.1 backend — reverse scan. + #[target_feature(enable = "sse4.1")] + pub(super) unsafe fn scan_rev(words: &[u64], fill: u64) -> usize { + unsafe { + let fill_vec = _mm_set1_epi64x(fill as i64); + let full_chunks = words.len() / LANES; + let done = full_chunks * LANES; + + let tail_count = super::scalar::scan_rev(&words[done..], fill); + if tail_count < words.len() - done { + return tail_count; + } + let mut count = tail_count; + + for chunk in (0..full_chunks).rev() { + let offset = chunk * LANES; + let data = _mm_loadu_si128(words.as_ptr().add(offset).cast::<__m128i>()); + let cmp = _mm_cmpeq_epi64(data, fill_vec); + let mask = _mm_movemask_epi8(cmp) as u32; + if mask != 0xFFFF { + for k in (0..LANES).rev() { + if (mask >> (k * 8)) & 0xFF != 0xFF { + return count + (LANES - 1 - k); + } + } + core::hint::unreachable_unchecked(); + } + count += LANES; + } + + count + } + } } // --------------------------------------------------------------------------- @@ -231,6 +354,37 @@ mod neon { count + super::scalar::scan(&words[done..], fill) } } + + /// NEON backend — reverse scan. + #[target_feature(enable = "neon")] + pub(super) unsafe fn scan_rev(words: &[u64], fill: u64) -> usize { + unsafe { + let fill_vec = vdupq_n_u64(fill); + let full_chunks = words.len() / LANES; + let done = full_chunks * LANES; + + let tail_count = super::scalar::scan_rev(&words[done..], fill); + if tail_count < words.len() - done { + return tail_count; + } + let mut count = tail_count; + + for chunk in (0..full_chunks).rev() { + let offset = chunk * LANES; + let data = vld1q_u64(words.as_ptr().add(offset)); + let cmp = vceqq_u64(data, fill_vec); + if vgetq_lane_u64(cmp, 1) == 0 { + return count; + } + if vgetq_lane_u64(cmp, 0) == 0 { + return count + 1; + } + count += LANES; + } + + count + } + } } #[cfg(test)] diff --git a/src/traits/bits_arith/funcs_for_leading_value_words/tests_for_backend_equivalence.rs b/src/traits/bits_arith/funcs_for_leading_value_words/tests_for_backend_equivalence.rs index fdd08e9..f1fcd20 100644 --- a/src/traits/bits_arith/funcs_for_leading_value_words/tests_for_backend_equivalence.rs +++ b/src/traits/bits_arith/funcs_for_leading_value_words/tests_for_backend_equivalence.rs @@ -134,3 +134,85 @@ fn neon_matches_scalar_ones() { assert_backend_matches_scalar(neon::scan, !0); run_random(neon::scan, !0); } + +// --------------------------------------------------------------------------- +// Trailing scan (reverse) — AVX2 / SSE4.1 / NEON +// --------------------------------------------------------------------------- + +fn assert_trailing_backend_matches_scalar(backend: unsafe fn(&[u64], u64) -> usize, fill: u64) { + let cases: &[&[u64]] = if fill == 0 { CASES_ZERO } else { CASES_ONE }; + + for &src in cases { + let expected = scalar::scan_rev(src, fill); + let actual = unsafe { backend(src, fill) }; + assert_eq!(actual, expected, "fill=0x{fill:x} src={src:?}"); + } +} + +fn run_random_trailing(backend: unsafe fn(&[u64], u64) -> usize, fill: u64) { + for run in [0, 1, 3, 5, 7, 9, 15, 16, 17, 31] { + let mut v = vec![fill; run]; + for _ in 0..16 { + v.push(if fill == 0 { u64::MAX } else { 0 }); + } + let expected = scalar::scan_rev(&v, fill); + let actual = unsafe { backend(&v, fill) }; + assert_eq!(actual, expected, "fill=0x{fill:x} run={run}"); + } +} + +#[cfg(all( + any(target_arch = "x86", target_arch = "x86_64"), + target_feature = "avx2" +))] +#[test] +fn avx2_trailing_matches_scalar_zeros() { + assert_trailing_backend_matches_scalar(avx2::scan_rev, 0); + run_random_trailing(avx2::scan_rev, 0); +} + +#[cfg(all( + any(target_arch = "x86", target_arch = "x86_64"), + target_feature = "avx2" +))] +#[test] +fn avx2_trailing_matches_scalar_ones() { + assert_trailing_backend_matches_scalar(avx2::scan_rev, !0); + run_random_trailing(avx2::scan_rev, !0); +} + +#[cfg(all( + any(target_arch = "x86", target_arch = "x86_64"), + target_feature = "sse4.1", + not(target_feature = "avx2") +))] +#[test] +fn sse41_trailing_matches_scalar_zeros() { + assert_trailing_backend_matches_scalar(sse41::scan_rev, 0); + run_random_trailing(sse41::scan_rev, 0); +} + +#[cfg(all( + any(target_arch = "x86", target_arch = "x86_64"), + target_feature = "sse4.1", + not(target_feature = "avx2") +))] +#[test] +fn sse41_trailing_matches_scalar_ones() { + assert_trailing_backend_matches_scalar(sse41::scan_rev, !0); + run_random_trailing(sse41::scan_rev, !0); +} + +#[cfg(all(target_arch = "aarch64", target_feature = "neon"))] +#[test] +fn neon_trailing_matches_scalar_zeros() { + assert_trailing_backend_matches_scalar(neon::scan_rev, 0); + run_random_trailing(neon::scan_rev, 0); +} + +#[cfg(all(target_arch = "aarch64", target_feature = "neon"))] +#[test] +fn neon_trailing_matches_scalar_ones() { + assert_trailing_backend_matches_scalar(neon::scan_rev, !0); + run_random_trailing(neon::scan_rev, !0); +} diff --git a/src/traits/bits_arith/impls_for_u64_slice.rs b/src/traits/bits_arith/impls_for_u64_slice.rs index bcc8fad..90bcfda 100644 --- a/src/traits/bits_arith/impls_for_u64_slice.rs +++ b/src/traits/bits_arith/impls_for_u64_slice.rs @@ -83,4 +83,14 @@ impl BitsArith for [u64] { fn leading_one_words(&self) -> usize { funcs_for_leading_value_words::leading_value_words(self, !0) } + + #[inline] + fn trailing_zero_words(&self) -> usize { + funcs_for_leading_value_words::trailing_value_words(self, 0) + } + + #[inline] + fn trailing_one_words(&self) -> usize { + funcs_for_leading_value_words::trailing_value_words(self, !0) + } } From 280c17e788dacc7cc2c93dc549c407302a2be030 Mon Sep 17 00:00:00 2001 From: juncheng Date: Sat, 27 Jun 2026 07:29:15 +0000 Subject: [PATCH 7/7] feat: add BitString::reverse_bits() and reverse_bits_assign() Three-step word-level algorithm: 1. Reverse bits within each u64 word via u64::reverse_bits() 2. Reverse word order 3. Right-shift by unused bits to align to LSB reverse_bits_assign() delegates to reverse_bits() via reassignment. Co-authored-by: Claude Co-authored-by: DeepSeek AI --- src/bit_string/impls_for_editing.rs | 1 + .../impls_for_reverse_bits.rs | 67 +++++++++++ .../tests_for_reverse_bits.rs | 112 ++++++++++++++++++ 3 files changed, 180 insertions(+) create mode 100644 src/bit_string/impls_for_editing/impls_for_reverse_bits.rs create mode 100644 src/bit_string/impls_for_editing/impls_for_reverse_bits/tests_for_reverse_bits.rs diff --git a/src/bit_string/impls_for_editing.rs b/src/bit_string/impls_for_editing.rs index 0648522..3610b41 100644 --- a/src/bit_string/impls_for_editing.rs +++ b/src/bit_string/impls_for_editing.rs @@ -19,6 +19,7 @@ mod impls_for_insert_remove; mod impls_for_push_pop; mod impls_for_replace; mod impls_for_retain; +mod impls_for_reverse_bits; mod impls_for_set; mod impls_for_slice; mod impls_for_truncate; diff --git a/src/bit_string/impls_for_editing/impls_for_reverse_bits.rs b/src/bit_string/impls_for_editing/impls_for_reverse_bits.rs new file mode 100644 index 0000000..2f7d53c --- /dev/null +++ b/src/bit_string/impls_for_editing/impls_for_reverse_bits.rs @@ -0,0 +1,67 @@ +use alloc::vec::Vec; + +use crate::traits::*; +use crate::{WORD_BITS, funcs_for_bits::word_len}; + +use super::BitString; + +impl BitString { + /// Returns a new `BitString` with the bits in reverse order. + /// + /// Bit at position `i` moves to position `bit_len - 1 - i`. + /// + /// # Examples + /// + /// ``` + /// use bit_string::BitString; + /// + /// let bits = BitString::try_from("10011").unwrap(); + /// let rev = bits.reverse_bits(); + /// assert_eq!(rev, BitString::try_from("11001").unwrap()); + /// ``` + #[inline] + pub fn reverse_bits(&self) -> Self { + if self.bit_len <= 1 { + return self.clone(); + } + + let n_words = word_len(self.bit_len); + let unused = n_words * WORD_BITS - self.bit_len; + + // Step 1: Reverse bits within each word. + let mut words: Vec = self.words.iter().map(|w| w.reverse_bits()).collect(); + words.resize(n_words, 0); + + // Step 2: Reverse word order. + words[..n_words].reverse(); + + // Step 3: Right-shift by `unused` to align bits to LSB. + if unused > 0 { + words = words.shr(n_words * WORD_BITS, unused); + } + + BitString::from_words(&words, self.bit_len) + .expect("reverse_bits: from_words should always succeed for valid inputs") + } + + /// Reverses the bit order in place. + /// + /// Bit at position `i` moves to position `bit_len - 1 - i`. + /// + /// # Examples + /// + /// ``` + /// use bit_string::BitString; + /// + /// let mut bits = BitString::try_from("10011").unwrap(); + /// bits.reverse_bits_assign(); + /// assert_eq!(bits, BitString::try_from("11001").unwrap()); + /// ``` + #[inline] + pub fn reverse_bits_assign(&mut self) { + *self = self.reverse_bits(); + } +} + +#[cfg(test)] +mod tests_for_reverse_bits; diff --git a/src/bit_string/impls_for_editing/impls_for_reverse_bits/tests_for_reverse_bits.rs b/src/bit_string/impls_for_editing/impls_for_reverse_bits/tests_for_reverse_bits.rs new file mode 100644 index 0000000..4eba736 --- /dev/null +++ b/src/bit_string/impls_for_editing/impls_for_reverse_bits/tests_for_reverse_bits.rs @@ -0,0 +1,112 @@ +use crate::BitString; + +/// Reversing the empty string produces the empty string. +#[test] +fn reverse_empty() { + let bits = BitString::new(); + assert_eq!(bits.reverse_bits(), bits); +} + +/// Reversing a single bit is a no-op. +#[test] +fn reverse_single_bit() { + for val in [false, true] { + let bits = BitString::repeat(val, 1); + let rev = bits.reverse_bits(); + assert_eq!(rev, bits, "val={val}"); + } +} + +/// Reversing a known pattern. +#[test] +fn reverse_known_pattern() { + let bits = BitString::try_from("10011").unwrap(); + let rev = bits.reverse_bits(); + let expected = BitString::try_from("11001").unwrap(); + assert_eq!(rev, expected); + + // Reverse twice should produce original. + assert_eq!(rev.reverse_bits(), bits); +} + +/// Reversing "1010" → "0101" +#[test] +fn reverse_alternating() { + let bits = BitString::try_from("1010").unwrap(); + let rev = bits.reverse_bits(); + assert_eq!(rev, BitString::try_from("0101").unwrap()); + assert_eq!(rev.reverse_bits(), bits); +} + +/// Reverse a palindrome → same. +#[test] +fn reverse_palindrome() { + let bits = BitString::try_from("1001").unwrap(); + assert_eq!(bits.reverse_bits(), bits); +} + +/// All zeros. +#[test] +fn reverse_all_zeros() { + for len in [1, 63, 64, 65, 127, 128, 129, 130] { + let bits = BitString::zeros(len); + assert_eq!(bits.reverse_bits(), bits, "len={len}"); + } +} + +/// All ones. +#[test] +fn reverse_all_ones() { + for len in [1, 63, 64, 65, 127, 128, 129, 130] { + let bits = BitString::ones(len); + assert_eq!(bits.reverse_bits(), bits, "len={len}"); + } +} + +/// Reverse a string with a single 1 at various positions. +#[test] +fn reverse_single_one_at_various_positions() { + for pos in [0, 1, 31, 63, 64, 65, 100, 127, 128, 129] { + let len = 130; + let mut bits = BitString::zeros(len); + bits.set(pos, true); + let rev = bits.reverse_bits(); + // The reversed bit should be at position len - 1 - pos. + assert!(rev.get(len - 1 - pos).unwrap(), "pos={pos}"); + // All other positions should be 0. + assert_eq!(rev.count_ones(), 1, "pos={pos}"); + } +} + +/// Reverse twice → identity (invariant). +#[test] +fn reverse_twice_is_identity() { + for len in [1, 2, 5, 63, 64, 65, 127, 128, 129, 130] { + let mut bits = BitString::zeros(len); + for i in (0..len).step_by(3) { + bits.set(i, true); + } + assert_eq!(bits.reverse_bits().reverse_bits(), bits, "len={len}"); + } +} + +/// reverse_bits_assign modifies in place. +#[test] +fn reverse_bits_assign_modifies_in_place() { + let mut bits = BitString::try_from("10011").unwrap(); + let expected = bits.reverse_bits(); + bits.reverse_bits_assign(); + assert_eq!(bits, expected); +} + +/// Reverse preserves bit_len. +#[test] +fn reverse_preserves_bit_len() { + for len in [0, 1, 2, 63, 64, 65, 128, 129] { + let mut bits = BitString::zeros(len); + for i in (0..len).step_by(5) { + bits.set(i, true); + } + assert_eq!(bits.reverse_bits().bit_len(), len, "len={len}"); + } +}