From 2cfc6abc3dea6680793249f5830f979003ac1b9e Mon Sep 17 00:00:00 2001 From: juncheng Date: Sun, 21 Jun 2026 10:19:35 +0000 Subject: [PATCH 01/22] refactor: promote traits to crate-level, add BitStr zero-copy view MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Move src/bit_string/traits/ to src/traits/ so BitsEdit, BitsArith, and BitsEq are accessible at the crate root — [u64] operations are not specific to BitString and other types can reuse them. Add src/bit_str.rs — a zero-copy BitStr<'a> view type: - 24 bytes: &BitString + start offset + bit_len - slice, expand_left, expand_right (all zero-copy) - get, starts_with, ends_with, find, rfind, contains - strip_prefix, strip_suffix - iter, display, debug, PartialEq - to_bit_string() for materialization Co-authored-by: Claude Co-authored-by: DeepSeek AI --- src/bit_str.rs | 46 +++++++++++++ src/bit_string.rs | 18 ++++- src/bit_string/impls_for_access.rs | 67 +++++-------------- .../impls_for_bit_arith/impls_for_and.rs | 2 +- .../impls_for_count_ones.rs | 2 +- .../impls_for_bit_arith/impls_for_not.rs | 2 +- .../impls_for_bit_arith/impls_for_or.rs | 2 +- .../impls_for_bit_arith/impls_for_shl.rs | 2 +- .../impls_for_bit_arith/impls_for_shr.rs | 2 +- .../impls_for_bit_arith/impls_for_xor.rs | 2 +- .../funcs_for_pack_bools_core.rs | 2 +- .../tests_for_from_bool_iter.rs | 2 +- .../funcs_for_pack_str_core.rs | 2 +- .../impls_for_from_words.rs | 2 +- .../impls_for_repeat.rs | 2 +- .../impls_for_editing/impls_for_concat.rs | 2 +- .../impls_for_editing/impls_for_drain.rs | 2 +- .../tests_for_drain_interval.rs | 2 +- .../impls_for_insert_remove.rs | 2 +- .../impls_for_editing/impls_for_push_pop.rs | 2 +- .../impls_for_push_pop/tests_for_pop.rs | 6 +- .../impls_for_editing/impls_for_replace.rs | 2 +- .../impls_for_editing/impls_for_retain.rs | 2 +- .../impls_for_editing/impls_for_set.rs | 2 +- .../impls_for_editing/impls_for_slice.rs | 2 +- .../impls_for_editing/impls_for_truncate.rs | 2 +- .../impls_for_editing/tests_for_invariants.rs | 4 +- src/bit_string/impls_for_iter.rs | 5 ++ src/bit_string/impls_for_matching.rs | 2 +- .../impls_for_matching/impls_for_find.rs | 10 +-- .../tests_for_backend_equivalence.rs | 8 +-- .../tests_for_backend_equivalence.rs | 4 +- .../tests_for_backend_equivalence.rs | 8 +-- .../impls_for_matches_at.rs | 6 +- src/bit_string/tests_for_proptest.rs | 4 +- src/lib.rs | 3 + src/{bit_string => }/traits.rs | 0 src/{bit_string => }/traits/bits_arith.rs | 0 .../bits_arith/funcs_for_binary_core.rs | 0 .../tests_for_backend_equivalence.rs | 0 .../traits/bits_arith/funcs_for_count_ones.rs | 0 .../tests_for_backend_equivalence.rs | 0 .../traits/bits_arith/funcs_for_not_core.rs | 2 +- .../tests_for_backend_equivalence.rs | 0 .../traits/bits_arith/funcs_for_shl_core.rs | 2 +- .../tests_for_backend_equivalence.rs | 0 .../traits/bits_arith/funcs_for_shr_core.rs | 2 +- .../tests_for_backend_equivalence.rs | 0 .../traits/bits_arith/impls_for_u64_slice.rs | 0 src/{bit_string => }/traits/bits_edit.rs | 0 .../traits/bits_edit/bits_copied.rs | 0 .../bits_copied/funcs_for_copy_words_core.rs | 0 .../tests_for_backend_equivalence.rs | 0 .../funcs_for_copy_words_shifted_core.rs | 0 .../tests_for_backend_equivalence.rs | 0 .../bits_edit/bits_copied/tests_for_copy.rs | 0 .../traits/bits_edit/impls_for_u64_slice.rs | 0 .../impls_for_u64_slice/tests_for_bit_at.rs | 0 .../tests_for_mask_unused.rs | 0 .../tests_for_read_chunk.rs | 0 .../impls_for_u64_slice/tests_for_set_bit.rs | 0 .../tests_for_write_chunk.rs | 0 src/{bit_string => }/traits/bits_eq.rs | 0 .../funcs_for_eq_words_aligned_core.rs | 0 .../tests_for_backend_equivalence.rs | 0 .../funcs_for_eq_words_unaligned_core.rs | 0 .../tests_for_backend_equivalence.rs | 0 .../traits/bits_eq/impls_for_u64_slice.rs | 0 68 files changed, 139 insertions(+), 102 deletions(-) create mode 100644 src/bit_str.rs rename src/{bit_string => }/traits.rs (100%) rename src/{bit_string => }/traits/bits_arith.rs (100%) rename src/{bit_string => }/traits/bits_arith/funcs_for_binary_core.rs (100%) rename src/{bit_string => }/traits/bits_arith/funcs_for_binary_core/tests_for_backend_equivalence.rs (100%) rename src/{bit_string => }/traits/bits_arith/funcs_for_count_ones.rs (100%) rename src/{bit_string => }/traits/bits_arith/funcs_for_count_ones/tests_for_backend_equivalence.rs (100%) rename src/{bit_string => }/traits/bits_arith/funcs_for_not_core.rs (99%) rename src/{bit_string => }/traits/bits_arith/funcs_for_not_core/tests_for_backend_equivalence.rs (100%) rename src/{bit_string => }/traits/bits_arith/funcs_for_shl_core.rs (99%) rename src/{bit_string => }/traits/bits_arith/funcs_for_shl_core/tests_for_backend_equivalence.rs (100%) rename src/{bit_string => }/traits/bits_arith/funcs_for_shr_core.rs (99%) rename src/{bit_string => }/traits/bits_arith/funcs_for_shr_core/tests_for_backend_equivalence.rs (100%) rename src/{bit_string => }/traits/bits_arith/impls_for_u64_slice.rs (100%) rename src/{bit_string => }/traits/bits_edit.rs (100%) rename src/{bit_string => }/traits/bits_edit/bits_copied.rs (100%) rename src/{bit_string => }/traits/bits_edit/bits_copied/funcs_for_copy_words_core.rs (100%) rename src/{bit_string => }/traits/bits_edit/bits_copied/funcs_for_copy_words_core/tests_for_backend_equivalence.rs (100%) rename src/{bit_string => }/traits/bits_edit/bits_copied/funcs_for_copy_words_shifted_core.rs (100%) rename src/{bit_string => }/traits/bits_edit/bits_copied/funcs_for_copy_words_shifted_core/tests_for_backend_equivalence.rs (100%) rename src/{bit_string => }/traits/bits_edit/bits_copied/tests_for_copy.rs (100%) rename src/{bit_string => }/traits/bits_edit/impls_for_u64_slice.rs (100%) rename src/{bit_string => }/traits/bits_edit/impls_for_u64_slice/tests_for_bit_at.rs (100%) rename src/{bit_string => }/traits/bits_edit/impls_for_u64_slice/tests_for_mask_unused.rs (100%) rename src/{bit_string => }/traits/bits_edit/impls_for_u64_slice/tests_for_read_chunk.rs (100%) rename src/{bit_string => }/traits/bits_edit/impls_for_u64_slice/tests_for_set_bit.rs (100%) rename src/{bit_string => }/traits/bits_edit/impls_for_u64_slice/tests_for_write_chunk.rs (100%) rename src/{bit_string => }/traits/bits_eq.rs (100%) rename src/{bit_string => }/traits/bits_eq/funcs_for_eq_words_aligned_core.rs (100%) rename src/{bit_string => }/traits/bits_eq/funcs_for_eq_words_aligned_core/tests_for_backend_equivalence.rs (100%) rename src/{bit_string => }/traits/bits_eq/funcs_for_eq_words_unaligned_core.rs (100%) rename src/{bit_string => }/traits/bits_eq/funcs_for_eq_words_unaligned_core/tests_for_backend_equivalence.rs (100%) rename src/{bit_string => }/traits/bits_eq/impls_for_u64_slice.rs (100%) diff --git a/src/bit_str.rs b/src/bit_str.rs new file mode 100644 index 0000000..3bc7502 --- /dev/null +++ b/src/bit_str.rs @@ -0,0 +1,46 @@ +use crate::BitString; + +/// A zero-copy borrowed view of a [`BitString`] or subrange thereof. +/// +/// `BitStr` is to `BitString` as `&str` is to `String` — it references the +/// underlying data without allocation, and carries a start offset plus length +/// for bit-level slicing. +/// +/// # Size +/// +/// 24 bytes on 64-bit targets (1 pointer + 2 usizes). +/// +/// # Lifetime +/// +/// The lifetime `'a` is tied to the source [`BitString`]. While any `BitStr` +/// is live, Rust's borrow checker prevents mutation of the source. +#[derive(Clone, Copy)] +pub struct BitStr<'bs> { + source: &'bs BitString, + start: usize, + /// Number of bits in this view. + bit_len: usize, +} + +// --------------------------------------------------------------------------- +// Getters +// --------------------------------------------------------------------------- + +impl<'bs> BitStr<'bs> { + /// The bit offset of this view within its source [`BitString`]. + #[inline] + pub fn start(&self) -> usize { + self.start + } + + /// The number of bits in this view. + #[inline] + pub fn bit_len(&self) -> usize { + self.bit_len + } + + #[inline] + pub fn source(&self) -> &BitString { + self.source + } +} diff --git a/src/bit_string.rs b/src/bit_string.rs index 82eea95..d452c44 100644 --- a/src/bit_string.rs +++ b/src/bit_string.rs @@ -8,6 +8,22 @@ pub struct BitString { bit_len: usize, } +impl BitString { + #[inline] + pub fn bit_len(&self) -> usize { + self.bit_len + } + + /// Returns the internal little-endian words. + /// + /// Bit index `i` is stored in word `i / 64`, bit offset `i % 64`. + /// Unused high bits in the last word are guaranteed to be zero. + #[inline] + pub fn words(&self) -> &[u64] { + &self.words + } +} + pub mod errors; mod impls_for_access; mod impls_for_bit_arith; @@ -17,7 +33,5 @@ mod impls_for_fmt; mod impls_for_iter; mod impls_for_matching; -pub(crate) mod traits; - #[cfg(test)] mod tests_for_proptest; diff --git a/src/bit_string/impls_for_access.rs b/src/bit_string/impls_for_access.rs index e7faaac..4ac4ae9 100644 --- a/src/bit_string/impls_for_access.rs +++ b/src/bit_string/impls_for_access.rs @@ -1,28 +1,7 @@ -use alloc::vec::Vec; +use crate::traits::*; use super::*; -impl BitString { - #[inline] - pub fn bit_len(&self) -> usize { - self.bit_len - } - - #[inline] - pub fn is_empty(&self) -> bool { - self.bit_len == 0 - } - - #[inline] - pub fn get(&self, index: usize) -> Option { - (index < self.bit_len).then(|| { - let word = self.words[index / 64]; - let mask = 1u64 << (index % 64); - word & mask != 0 - }) - } -} - impl BitString { #[inline] pub fn any(&self) -> bool { @@ -43,16 +22,21 @@ impl BitString { pub fn is_all_ones(&self) -> bool { self.all() } + + #[inline] + pub fn is_empty(&self) -> bool { + self.bit_len == 0 + } } impl BitString { - /// Returns the internal little-endian words. - /// - /// Bit index `i` is stored in word `i / 64`, bit offset `i % 64`. - /// Unused high bits in the last word are guaranteed to be zero. #[inline] - pub fn as_words(&self) -> &[u64] { - &self.words + pub fn get(&self, index: usize) -> Option { + (index < self.bit_len).then(|| { + let word = self.words[index / 64]; + let mask = 1u64 << (index % 64); + word & mask != 0 + }) } #[inline] @@ -60,25 +44,6 @@ impl BitString { self.get(0) } - /// Reads up to 64 bits starting at `bit_start`, returning them in the - /// low bits of a `u64`. - /// - /// Bits beyond `self.len()` are treated as zero. - #[inline] - pub fn get_chunk(&self, bit_start: usize) -> u64 { - let word = bit_start / WORD_BITS; - let shift = bit_start % WORD_BITS; - - let lo = self.words.get(word).copied().unwrap_or(0) >> shift; - - if shift == 0 { - lo - } else { - let hi = self.words.get(word + 1).copied().unwrap_or(0); - lo | (hi << (WORD_BITS - shift)) - } - } - #[inline] pub fn last(&self) -> Option { self.bit_len @@ -86,9 +51,13 @@ impl BitString { .and_then(|index| self.get(index)) } + /// Reads up to 64 bits starting at `bit_start`, returning them in the + /// low bits of a `u64`. + /// + /// Bits beyond `self.len()` are treated as zero. #[inline] - pub fn to_bool_vec(&self) -> Vec { - self.iter().collect() + pub fn get_chunk(&self, bit_start: usize) -> u64 { + self.words.read_word_at(bit_start) } } diff --git a/src/bit_string/impls_for_bit_arith/impls_for_and.rs b/src/bit_string/impls_for_bit_arith/impls_for_and.rs index 4864194..9a42d56 100644 --- a/src/bit_string/impls_for_bit_arith/impls_for_and.rs +++ b/src/bit_string/impls_for_bit_arith/impls_for_and.rs @@ -1,5 +1,5 @@ use crate::bit_string::errors::BitStringLenMismatch; -use crate::bit_string::traits::*; +use crate::traits::*; use super::BitString; 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 58560b5..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 @@ -1,4 +1,4 @@ -use crate::bit_string::traits::*; +use crate::traits::*; use super::BitString; diff --git a/src/bit_string/impls_for_bit_arith/impls_for_not.rs b/src/bit_string/impls_for_bit_arith/impls_for_not.rs index 6a48e40..333c319 100644 --- a/src/bit_string/impls_for_bit_arith/impls_for_not.rs +++ b/src/bit_string/impls_for_bit_arith/impls_for_not.rs @@ -1,4 +1,4 @@ -use crate::bit_string::traits::*; +use crate::traits::*; use super::BitString; diff --git a/src/bit_string/impls_for_bit_arith/impls_for_or.rs b/src/bit_string/impls_for_bit_arith/impls_for_or.rs index 88be160..b8549c4 100644 --- a/src/bit_string/impls_for_bit_arith/impls_for_or.rs +++ b/src/bit_string/impls_for_bit_arith/impls_for_or.rs @@ -1,5 +1,5 @@ use crate::bit_string::errors::BitStringLenMismatch; -use crate::bit_string::traits::*; +use crate::traits::*; use super::BitString; diff --git a/src/bit_string/impls_for_bit_arith/impls_for_shl.rs b/src/bit_string/impls_for_bit_arith/impls_for_shl.rs index 6bc6c42..cad9765 100644 --- a/src/bit_string/impls_for_bit_arith/impls_for_shl.rs +++ b/src/bit_string/impls_for_bit_arith/impls_for_shl.rs @@ -1,4 +1,4 @@ -use crate::bit_string::traits::*; +use crate::traits::*; use super::BitString; diff --git a/src/bit_string/impls_for_bit_arith/impls_for_shr.rs b/src/bit_string/impls_for_bit_arith/impls_for_shr.rs index 6447949..18b852a 100644 --- a/src/bit_string/impls_for_bit_arith/impls_for_shr.rs +++ b/src/bit_string/impls_for_bit_arith/impls_for_shr.rs @@ -1,4 +1,4 @@ -use crate::bit_string::traits::*; +use crate::traits::*; use super::BitString; diff --git a/src/bit_string/impls_for_bit_arith/impls_for_xor.rs b/src/bit_string/impls_for_bit_arith/impls_for_xor.rs index 2bdb4a8..de4d6b3 100644 --- a/src/bit_string/impls_for_bit_arith/impls_for_xor.rs +++ b/src/bit_string/impls_for_bit_arith/impls_for_xor.rs @@ -1,5 +1,5 @@ use crate::bit_string::errors::BitStringLenMismatch; -use crate::bit_string::traits::*; +use crate::traits::*; use super::BitString; diff --git a/src/bit_string/impls_for_construction/funcs_for_pack_bools_core.rs b/src/bit_string/impls_for_construction/funcs_for_pack_bools_core.rs index 9bf3a6d..34035de 100644 --- a/src/bit_string/impls_for_construction/funcs_for_pack_bools_core.rs +++ b/src/bit_string/impls_for_construction/funcs_for_pack_bools_core.rs @@ -1,6 +1,6 @@ use alloc::vec::Vec; -use crate::bit_string::traits::*; +use crate::traits::*; use crate::word_len; /// Pack `bit_len` LSBs from `src` into a `Vec`. diff --git a/src/bit_string/impls_for_construction/funcs_for_pack_bools_core/impls_for_from_bool_iter/tests_for_from_bool_iter.rs b/src/bit_string/impls_for_construction/funcs_for_pack_bools_core/impls_for_from_bool_iter/tests_for_from_bool_iter.rs index e7d0430..0ec356a 100644 --- a/src/bit_string/impls_for_construction/funcs_for_pack_bools_core/impls_for_from_bool_iter/tests_for_from_bool_iter.rs +++ b/src/bit_string/impls_for_construction/funcs_for_pack_bools_core/impls_for_from_bool_iter/tests_for_from_bool_iter.rs @@ -79,5 +79,5 @@ fn leaves_unused_tail_bits_zero() { let bits = BitString::from_bool_iter([true]); assert_eq!(bits.bit_len(), 1); - assert_eq!(bits.as_words(), &[1]); + assert_eq!(bits.words(), &[1]); } diff --git a/src/bit_string/impls_for_construction/impls_for_from_str/funcs_for_pack_str_core.rs b/src/bit_string/impls_for_construction/impls_for_from_str/funcs_for_pack_str_core.rs index bf9faa2..4038ed4 100644 --- a/src/bit_string/impls_for_construction/impls_for_from_str/funcs_for_pack_str_core.rs +++ b/src/bit_string/impls_for_construction/impls_for_from_str/funcs_for_pack_str_core.rs @@ -1,6 +1,6 @@ use alloc::vec::Vec; -use crate::bit_string::traits::*; +use crate::traits::*; use crate::word_len; /// Pack `bit_len` ASCII '0'/'1' bytes from `src` into a `Vec`. diff --git a/src/bit_string/impls_for_construction/impls_for_from_words.rs b/src/bit_string/impls_for_construction/impls_for_from_words.rs index 5e0b1e3..9ecb30d 100644 --- a/src/bit_string/impls_for_construction/impls_for_from_words.rs +++ b/src/bit_string/impls_for_construction/impls_for_from_words.rs @@ -1,6 +1,6 @@ use super::*; -use crate::bit_string::traits::*; use crate::funcs_for_bits::*; +use crate::traits::*; impl BitString { /// Constructs a bit string from packed little-endian words. diff --git a/src/bit_string/impls_for_construction/impls_for_repeat.rs b/src/bit_string/impls_for_construction/impls_for_repeat.rs index 4b39820..f01d79c 100644 --- a/src/bit_string/impls_for_construction/impls_for_repeat.rs +++ b/src/bit_string/impls_for_construction/impls_for_repeat.rs @@ -1,6 +1,6 @@ use super::*; -use crate::bit_string::traits::*; use crate::funcs_for_bits::*; +use crate::traits::*; use alloc::vec::Vec; impl BitString { diff --git a/src/bit_string/impls_for_editing/impls_for_concat.rs b/src/bit_string/impls_for_editing/impls_for_concat.rs index c3a01dc..3cd2fc8 100644 --- a/src/bit_string/impls_for_editing/impls_for_concat.rs +++ b/src/bit_string/impls_for_editing/impls_for_concat.rs @@ -1,5 +1,5 @@ -use crate::bit_string::traits::*; use crate::funcs_for_bits::*; +use crate::traits::*; use super::*; diff --git a/src/bit_string/impls_for_editing/impls_for_drain.rs b/src/bit_string/impls_for_editing/impls_for_drain.rs index bf1c6d4..a8c7cf9 100644 --- a/src/bit_string/impls_for_editing/impls_for_drain.rs +++ b/src/bit_string/impls_for_editing/impls_for_drain.rs @@ -1,7 +1,7 @@ use int_interval::UsizeCO; -use crate::bit_string::traits::*; use crate::funcs_for_bits::*; +use crate::traits::*; use super::*; diff --git a/src/bit_string/impls_for_editing/impls_for_drain/tests_for_drain_interval.rs b/src/bit_string/impls_for_editing/impls_for_drain/tests_for_drain_interval.rs index d2284b5..39b7272 100644 --- a/src/bit_string/impls_for_editing/impls_for_drain/tests_for_drain_interval.rs +++ b/src/bit_string/impls_for_editing/impls_for_drain/tests_for_drain_interval.rs @@ -88,7 +88,7 @@ fn assign_drains_entire_bit_string() { assert_eq!(removed.to_string(), "101001"); assert_eq!(bits.bit_len(), 0); assert_eq!(bits.to_string(), ""); - assert_eq!(bits.as_words().len(), 0); + assert_eq!(bits.words().len(), 0); } #[test] diff --git a/src/bit_string/impls_for_editing/impls_for_insert_remove.rs b/src/bit_string/impls_for_editing/impls_for_insert_remove.rs index 60183ff..01eddcb 100644 --- a/src/bit_string/impls_for_editing/impls_for_insert_remove.rs +++ b/src/bit_string/impls_for_editing/impls_for_insert_remove.rs @@ -1,5 +1,5 @@ -use crate::bit_string::traits::*; use crate::funcs_for_bits::*; +use crate::traits::*; use super::*; diff --git a/src/bit_string/impls_for_editing/impls_for_push_pop.rs b/src/bit_string/impls_for_editing/impls_for_push_pop.rs index b343ee3..7745e27 100644 --- a/src/bit_string/impls_for_editing/impls_for_push_pop.rs +++ b/src/bit_string/impls_for_editing/impls_for_push_pop.rs @@ -1,5 +1,5 @@ -use crate::bit_string::traits::*; use crate::funcs_for_bits::*; +use crate::traits::*; use super::*; diff --git a/src/bit_string/impls_for_editing/impls_for_push_pop/tests_for_pop.rs b/src/bit_string/impls_for_editing/impls_for_push_pop/tests_for_pop.rs index 05cbd77..2d315ff 100644 --- a/src/bit_string/impls_for_editing/impls_for_push_pop/tests_for_pop.rs +++ b/src/bit_string/impls_for_editing/impls_for_push_pop/tests_for_pop.rs @@ -39,11 +39,11 @@ fn pops_bits_from_back_in_order() { fn shrinks_when_crossing_word_boundary() { let mut bits = BitString::ones(65); - assert_eq!(bits.as_words().len(), 2); + assert_eq!(bits.words().len(), 2); assert_eq!(bits.pop(), Some(true)); assert_eq!(bits.bit_len(), 64); - assert_eq!(bits.as_words().len(), 1); + assert_eq!(bits.words().len(), 1); assert_eq!(bits.count_ones(), 64); assert_eq!(bits.to_string(), "1".repeat(64)); } @@ -82,6 +82,6 @@ fn repeated_pop_eventually_clears_storage() { assert_eq!(bits.pop(), None); assert_eq!(bits.bit_len(), 0); - assert_eq!(bits.as_words().len(), 0); + assert_eq!(bits.words().len(), 0); assert_eq!(bits.to_string(), ""); } diff --git a/src/bit_string/impls_for_editing/impls_for_replace.rs b/src/bit_string/impls_for_editing/impls_for_replace.rs index 980e1f7..cc5e184 100644 --- a/src/bit_string/impls_for_editing/impls_for_replace.rs +++ b/src/bit_string/impls_for_editing/impls_for_replace.rs @@ -1,8 +1,8 @@ use int_interval::UsizeCO; use witnessed::{WitnessExt, Witnessed}; -use crate::bit_string::traits::*; use crate::funcs_for_bits::*; +use crate::traits::*; use super::*; diff --git a/src/bit_string/impls_for_editing/impls_for_retain.rs b/src/bit_string/impls_for_editing/impls_for_retain.rs index d27d16b..f19a7cb 100644 --- a/src/bit_string/impls_for_editing/impls_for_retain.rs +++ b/src/bit_string/impls_for_editing/impls_for_retain.rs @@ -1,4 +1,4 @@ -use crate::bit_string::traits::*; +use crate::traits::*; use super::*; diff --git a/src/bit_string/impls_for_editing/impls_for_set.rs b/src/bit_string/impls_for_editing/impls_for_set.rs index 7751f98..aefb9a6 100644 --- a/src/bit_string/impls_for_editing/impls_for_set.rs +++ b/src/bit_string/impls_for_editing/impls_for_set.rs @@ -1,5 +1,5 @@ -use crate::bit_string::traits::*; use crate::funcs_for_bits::*; +use crate::traits::*; use super::*; diff --git a/src/bit_string/impls_for_editing/impls_for_slice.rs b/src/bit_string/impls_for_editing/impls_for_slice.rs index efda725..d474f1a 100644 --- a/src/bit_string/impls_for_editing/impls_for_slice.rs +++ b/src/bit_string/impls_for_editing/impls_for_slice.rs @@ -1,7 +1,7 @@ use int_interval::UsizeCO; -use crate::bit_string::traits::*; use crate::funcs_for_bits::*; +use crate::traits::*; use super::*; diff --git a/src/bit_string/impls_for_editing/impls_for_truncate.rs b/src/bit_string/impls_for_editing/impls_for_truncate.rs index 0a5b77d..4aef3ca 100644 --- a/src/bit_string/impls_for_editing/impls_for_truncate.rs +++ b/src/bit_string/impls_for_editing/impls_for_truncate.rs @@ -1,5 +1,5 @@ -use crate::bit_string::traits::*; use crate::funcs_for_bits::*; +use crate::traits::*; use super::*; diff --git a/src/bit_string/impls_for_editing/tests_for_invariants.rs b/src/bit_string/impls_for_editing/tests_for_invariants.rs index 2dfebaf..c6002d8 100644 --- a/src/bit_string/impls_for_editing/tests_for_invariants.rs +++ b/src/bit_string/impls_for_editing/tests_for_invariants.rs @@ -11,7 +11,7 @@ use int_interval::UsizeCO; fn assert_invariants(bits: &BitString) { let bit_len = bits.bit_len(); let expected_words = word_len(bit_len); - let actual_words = bits.as_words().len(); + let actual_words = bits.words().len(); assert_eq!( actual_words, expected_words, "word count mismatch: bit_len={bit_len}, words={actual_words}, expected_words={expected_words}", @@ -54,7 +54,7 @@ fn clear_on_already_empty_is_idempotent() { bits.clear(); assert!(bits.is_empty()); assert_eq!(bits.bit_len(), 0); - assert_eq!(bits.as_words().len(), 0); + assert_eq!(bits.words().len(), 0); } // --------------------------------------------------------------------------- diff --git a/src/bit_string/impls_for_iter.rs b/src/bit_string/impls_for_iter.rs index 1eed2f8..5ec4c2f 100644 --- a/src/bit_string/impls_for_iter.rs +++ b/src/bit_string/impls_for_iter.rs @@ -9,6 +9,11 @@ impl BitString { back: self.bit_len, } } + + #[inline] + pub fn to_bool_vec(&self) -> Vec { + self.iter().collect() + } } pub struct Iter<'a> { diff --git a/src/bit_string/impls_for_matching.rs b/src/bit_string/impls_for_matching.rs index f9e74d4..7262507 100644 --- a/src/bit_string/impls_for_matching.rs +++ b/src/bit_string/impls_for_matching.rs @@ -1,4 +1,4 @@ -use crate::bit_string::traits::*; +use crate::traits::*; use super::*; 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 81a2828..e3e6fd5 100644 --- a/src/bit_string/impls_for_matching/impls_for_find.rs +++ b/src/bit_string/impls_for_matching/impls_for_find.rs @@ -19,7 +19,7 @@ impl BitString { funcs_for_contains_core::find_any_candidate( &self.words, self.bit_len, - needle.as_words(), + needle.words(), needle.bit_len, &mut |pos| self.bits_equal_at(pos, needle), ) @@ -37,7 +37,7 @@ impl BitString { && !funcs_for_contains_core::find_any_candidate( &self.words, self.bit_len, - needle.as_words(), + needle.words(), needle.bit_len, &mut |pos| self.bits_equal_at(pos, needle), ) @@ -49,7 +49,7 @@ impl BitString { funcs_for_find_core::find_first_word( &self.words, self.bit_len, - needle.as_words(), + needle.words(), needle.bit_len, &mut |pos| self.bits_equal_at(pos, needle), ) @@ -66,7 +66,7 @@ impl BitString { && !funcs_for_contains_core::find_any_candidate( &self.words, self.bit_len, - needle.as_words(), + needle.words(), needle.bit_len, &mut |pos| self.bits_equal_at(pos, needle), ) @@ -78,7 +78,7 @@ impl BitString { funcs_for_rfind_core::find_last_word( &self.words, self.bit_len, - needle.as_words(), + needle.words(), needle.bit_len, &mut |pos| self.bits_equal_at(pos, needle), ) diff --git a/src/bit_string/impls_for_matching/impls_for_find/funcs_for_contains_core/tests_for_backend_equivalence.rs b/src/bit_string/impls_for_matching/impls_for_find/funcs_for_contains_core/tests_for_backend_equivalence.rs index 420e551..ed9ff03 100644 --- a/src/bit_string/impls_for_matching/impls_for_find/funcs_for_contains_core/tests_for_backend_equivalence.rs +++ b/src/bit_string/impls_for_matching/impls_for_find/funcs_for_contains_core/tests_for_backend_equivalence.rs @@ -27,9 +27,9 @@ proptest! { let needle = BitString::from_bool_iter(n_bools); let result = super::find_any_candidate( - haystack.as_words(), + haystack.words(), haystack.bit_len(), - needle.as_words(), + needle.words(), needle.bit_len(), &mut |pos| haystack.bits_equal_at(pos, &needle), ); @@ -70,9 +70,9 @@ proptest! { let mut any_found = false; let _ = super::find_any_candidate( - haystack.as_words(), + haystack.words(), haystack_len, - needle.as_words(), + needle.words(), needle_len, &mut |pos| { let ok = haystack.bits_equal_at(pos, &needle); diff --git a/src/bit_string/impls_for_matching/impls_for_find/funcs_for_find_core/tests_for_backend_equivalence.rs b/src/bit_string/impls_for_matching/impls_for_find/funcs_for_find_core/tests_for_backend_equivalence.rs index 624d7fd..75d3b90 100644 --- a/src/bit_string/impls_for_matching/impls_for_find/funcs_for_find_core/tests_for_backend_equivalence.rs +++ b/src/bit_string/impls_for_matching/impls_for_find/funcs_for_find_core/tests_for_backend_equivalence.rs @@ -66,8 +66,8 @@ proptest! { let haystack_len = haystack.bit_len(); let needle_len = needle.bit_len(); - let haystack_words = haystack.as_words(); - let needle_words = needle.as_words(); + let haystack_words = haystack.words(); + let needle_words = needle.words(); let needle_first = needle_words[0]; let needle_mask = low_mask(needle_len.min(WORD_BITS)); let last_start = haystack_len.saturating_sub(needle_len); diff --git a/src/bit_string/impls_for_matching/impls_for_find/funcs_for_rfind_core/tests_for_backend_equivalence.rs b/src/bit_string/impls_for_matching/impls_for_find/funcs_for_rfind_core/tests_for_backend_equivalence.rs index 96d6eaa..8284e33 100644 --- a/src/bit_string/impls_for_matching/impls_for_find/funcs_for_rfind_core/tests_for_backend_equivalence.rs +++ b/src/bit_string/impls_for_matching/impls_for_find/funcs_for_rfind_core/tests_for_backend_equivalence.rs @@ -27,9 +27,9 @@ proptest! { let needle = BitString::from_bool_iter(n_bools); let result = super::find_last_word( - haystack.as_words(), + haystack.words(), haystack.bit_len(), - needle.as_words(), + needle.words(), needle.bit_len(), &mut |pos| haystack.bits_equal_at(pos, &needle), ); @@ -66,9 +66,9 @@ proptest! { let needle = BitString::from_bool_iter(n_bools); let result = super::find_last_word( - haystack.as_words(), + haystack.words(), haystack.bit_len(), - needle.as_words(), + needle.words(), needle.bit_len(), &mut |pos| haystack.bits_equal_at(pos, &needle), ); 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 1cd0da4..b7da80b 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 @@ -15,7 +15,7 @@ impl BitString { if needle_bits == 0 { return true; } - let needle_words = needle.as_words(); + let needle_words = needle.words(); // Sub-word fast path: the entire pattern fits in one u64. if needle_bits <= WORD_BITS { @@ -72,7 +72,7 @@ impl BitString { return false; } - let pw = prefix.as_words(); + let pw = prefix.words(); let sw: &[u64] = &self.words; // Sub-word fast path: one u64 read + mask. @@ -112,7 +112,7 @@ impl BitString { let shift = start % WORD_BITS; let base_word = start / WORD_BITS; let sw: &[u64] = &self.words[base_word..]; - let pw = suffix.as_words(); + let pw = suffix.words(); // Sub-word fast path: one 64-bit window + mask. if suffix.bit_len <= WORD_BITS { diff --git a/src/bit_string/tests_for_proptest.rs b/src/bit_string/tests_for_proptest.rs index c253e4c..1307891 100644 --- a/src/bit_string/tests_for_proptest.rs +++ b/src/bit_string/tests_for_proptest.rs @@ -57,7 +57,7 @@ fn bs_with_index() -> impl Strategy { fn assert_all_invariants(bits: &BitString) { let bit_len = bits.bit_len(); let expected_words = crate::word_len(bit_len); - let actual_words = bits.as_words().len(); + let actual_words = bits.words().len(); assert_eq!( actual_words, expected_words, "word-count invariant: bit_len={bit_len}, words={actual_words}, expected={expected_words}", @@ -66,7 +66,7 @@ fn assert_all_invariants(bits: &BitString) { // Unused high bits in last word must be zero. let rem = bit_len % crate::WORD_BITS; if rem != 0 { - if let Some(&last) = bits.as_words().last() { + if let Some(&last) = bits.words().last() { assert_eq!( last >> rem, 0, diff --git a/src/lib.rs b/src/lib.rs index 42f9fed..e22442a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -5,11 +5,14 @@ extern crate alloc; mod consts_for_bits; mod funcs_for_bits; +pub(crate) mod traits; pub(crate) use consts_for_bits::*; pub(crate) use funcs_for_bits::*; +mod bit_str; mod bit_string; +pub use bit_str::BitStr; pub use bit_string::BitString; pub use bit_string::errors::*; diff --git a/src/bit_string/traits.rs b/src/traits.rs similarity index 100% rename from src/bit_string/traits.rs rename to src/traits.rs diff --git a/src/bit_string/traits/bits_arith.rs b/src/traits/bits_arith.rs similarity index 100% rename from src/bit_string/traits/bits_arith.rs rename to src/traits/bits_arith.rs diff --git a/src/bit_string/traits/bits_arith/funcs_for_binary_core.rs b/src/traits/bits_arith/funcs_for_binary_core.rs similarity index 100% rename from src/bit_string/traits/bits_arith/funcs_for_binary_core.rs rename to src/traits/bits_arith/funcs_for_binary_core.rs diff --git a/src/bit_string/traits/bits_arith/funcs_for_binary_core/tests_for_backend_equivalence.rs b/src/traits/bits_arith/funcs_for_binary_core/tests_for_backend_equivalence.rs similarity index 100% rename from src/bit_string/traits/bits_arith/funcs_for_binary_core/tests_for_backend_equivalence.rs rename to src/traits/bits_arith/funcs_for_binary_core/tests_for_backend_equivalence.rs diff --git a/src/bit_string/traits/bits_arith/funcs_for_count_ones.rs b/src/traits/bits_arith/funcs_for_count_ones.rs similarity index 100% rename from src/bit_string/traits/bits_arith/funcs_for_count_ones.rs rename to src/traits/bits_arith/funcs_for_count_ones.rs diff --git a/src/bit_string/traits/bits_arith/funcs_for_count_ones/tests_for_backend_equivalence.rs b/src/traits/bits_arith/funcs_for_count_ones/tests_for_backend_equivalence.rs similarity index 100% rename from src/bit_string/traits/bits_arith/funcs_for_count_ones/tests_for_backend_equivalence.rs rename to src/traits/bits_arith/funcs_for_count_ones/tests_for_backend_equivalence.rs diff --git a/src/bit_string/traits/bits_arith/funcs_for_not_core.rs b/src/traits/bits_arith/funcs_for_not_core.rs similarity index 99% rename from src/bit_string/traits/bits_arith/funcs_for_not_core.rs rename to src/traits/bits_arith/funcs_for_not_core.rs index 1b2fe7e..a720e92 100644 --- a/src/bit_string/traits/bits_arith/funcs_for_not_core.rs +++ b/src/traits/bits_arith/funcs_for_not_core.rs @@ -1,6 +1,6 @@ use alloc::vec::Vec; -use crate::bit_string::traits::*; +use crate::traits::*; #[inline] pub(super) fn owned(src: &[u64], bit_len: usize) -> Vec { diff --git a/src/bit_string/traits/bits_arith/funcs_for_not_core/tests_for_backend_equivalence.rs b/src/traits/bits_arith/funcs_for_not_core/tests_for_backend_equivalence.rs similarity index 100% rename from src/bit_string/traits/bits_arith/funcs_for_not_core/tests_for_backend_equivalence.rs rename to src/traits/bits_arith/funcs_for_not_core/tests_for_backend_equivalence.rs diff --git a/src/bit_string/traits/bits_arith/funcs_for_shl_core.rs b/src/traits/bits_arith/funcs_for_shl_core.rs similarity index 99% rename from src/bit_string/traits/bits_arith/funcs_for_shl_core.rs rename to src/traits/bits_arith/funcs_for_shl_core.rs index 7ed1cc9..9f492c3 100644 --- a/src/bit_string/traits/bits_arith/funcs_for_shl_core.rs +++ b/src/traits/bits_arith/funcs_for_shl_core.rs @@ -2,7 +2,7 @@ use alloc::vec::Vec; use crate::WORD_BITS; -use crate::bit_string::traits::*; +use crate::traits::*; #[inline] pub(super) fn owned(src: &[u64], bit_len: usize, amount: usize) -> Vec { diff --git a/src/bit_string/traits/bits_arith/funcs_for_shl_core/tests_for_backend_equivalence.rs b/src/traits/bits_arith/funcs_for_shl_core/tests_for_backend_equivalence.rs similarity index 100% rename from src/bit_string/traits/bits_arith/funcs_for_shl_core/tests_for_backend_equivalence.rs rename to src/traits/bits_arith/funcs_for_shl_core/tests_for_backend_equivalence.rs diff --git a/src/bit_string/traits/bits_arith/funcs_for_shr_core.rs b/src/traits/bits_arith/funcs_for_shr_core.rs similarity index 99% rename from src/bit_string/traits/bits_arith/funcs_for_shr_core.rs rename to src/traits/bits_arith/funcs_for_shr_core.rs index f76a2bc..ad8f59e 100644 --- a/src/bit_string/traits/bits_arith/funcs_for_shr_core.rs +++ b/src/traits/bits_arith/funcs_for_shr_core.rs @@ -2,7 +2,7 @@ use alloc::vec::Vec; use crate::WORD_BITS; -use crate::bit_string::traits::*; +use crate::traits::*; #[inline] pub(super) fn owned(src: &[u64], bit_len: usize, amount: usize) -> Vec { diff --git a/src/bit_string/traits/bits_arith/funcs_for_shr_core/tests_for_backend_equivalence.rs b/src/traits/bits_arith/funcs_for_shr_core/tests_for_backend_equivalence.rs similarity index 100% rename from src/bit_string/traits/bits_arith/funcs_for_shr_core/tests_for_backend_equivalence.rs rename to src/traits/bits_arith/funcs_for_shr_core/tests_for_backend_equivalence.rs diff --git a/src/bit_string/traits/bits_arith/impls_for_u64_slice.rs b/src/traits/bits_arith/impls_for_u64_slice.rs similarity index 100% rename from src/bit_string/traits/bits_arith/impls_for_u64_slice.rs rename to src/traits/bits_arith/impls_for_u64_slice.rs diff --git a/src/bit_string/traits/bits_edit.rs b/src/traits/bits_edit.rs similarity index 100% rename from src/bit_string/traits/bits_edit.rs rename to src/traits/bits_edit.rs diff --git a/src/bit_string/traits/bits_edit/bits_copied.rs b/src/traits/bits_edit/bits_copied.rs similarity index 100% rename from src/bit_string/traits/bits_edit/bits_copied.rs rename to src/traits/bits_edit/bits_copied.rs diff --git a/src/bit_string/traits/bits_edit/bits_copied/funcs_for_copy_words_core.rs b/src/traits/bits_edit/bits_copied/funcs_for_copy_words_core.rs similarity index 100% rename from src/bit_string/traits/bits_edit/bits_copied/funcs_for_copy_words_core.rs rename to src/traits/bits_edit/bits_copied/funcs_for_copy_words_core.rs diff --git a/src/bit_string/traits/bits_edit/bits_copied/funcs_for_copy_words_core/tests_for_backend_equivalence.rs b/src/traits/bits_edit/bits_copied/funcs_for_copy_words_core/tests_for_backend_equivalence.rs similarity index 100% rename from src/bit_string/traits/bits_edit/bits_copied/funcs_for_copy_words_core/tests_for_backend_equivalence.rs rename to src/traits/bits_edit/bits_copied/funcs_for_copy_words_core/tests_for_backend_equivalence.rs diff --git a/src/bit_string/traits/bits_edit/bits_copied/funcs_for_copy_words_shifted_core.rs b/src/traits/bits_edit/bits_copied/funcs_for_copy_words_shifted_core.rs similarity index 100% rename from src/bit_string/traits/bits_edit/bits_copied/funcs_for_copy_words_shifted_core.rs rename to src/traits/bits_edit/bits_copied/funcs_for_copy_words_shifted_core.rs diff --git a/src/bit_string/traits/bits_edit/bits_copied/funcs_for_copy_words_shifted_core/tests_for_backend_equivalence.rs b/src/traits/bits_edit/bits_copied/funcs_for_copy_words_shifted_core/tests_for_backend_equivalence.rs similarity index 100% rename from src/bit_string/traits/bits_edit/bits_copied/funcs_for_copy_words_shifted_core/tests_for_backend_equivalence.rs rename to src/traits/bits_edit/bits_copied/funcs_for_copy_words_shifted_core/tests_for_backend_equivalence.rs diff --git a/src/bit_string/traits/bits_edit/bits_copied/tests_for_copy.rs b/src/traits/bits_edit/bits_copied/tests_for_copy.rs similarity index 100% rename from src/bit_string/traits/bits_edit/bits_copied/tests_for_copy.rs rename to src/traits/bits_edit/bits_copied/tests_for_copy.rs diff --git a/src/bit_string/traits/bits_edit/impls_for_u64_slice.rs b/src/traits/bits_edit/impls_for_u64_slice.rs similarity index 100% rename from src/bit_string/traits/bits_edit/impls_for_u64_slice.rs rename to src/traits/bits_edit/impls_for_u64_slice.rs diff --git a/src/bit_string/traits/bits_edit/impls_for_u64_slice/tests_for_bit_at.rs b/src/traits/bits_edit/impls_for_u64_slice/tests_for_bit_at.rs similarity index 100% rename from src/bit_string/traits/bits_edit/impls_for_u64_slice/tests_for_bit_at.rs rename to src/traits/bits_edit/impls_for_u64_slice/tests_for_bit_at.rs diff --git a/src/bit_string/traits/bits_edit/impls_for_u64_slice/tests_for_mask_unused.rs b/src/traits/bits_edit/impls_for_u64_slice/tests_for_mask_unused.rs similarity index 100% rename from src/bit_string/traits/bits_edit/impls_for_u64_slice/tests_for_mask_unused.rs rename to src/traits/bits_edit/impls_for_u64_slice/tests_for_mask_unused.rs diff --git a/src/bit_string/traits/bits_edit/impls_for_u64_slice/tests_for_read_chunk.rs b/src/traits/bits_edit/impls_for_u64_slice/tests_for_read_chunk.rs similarity index 100% rename from src/bit_string/traits/bits_edit/impls_for_u64_slice/tests_for_read_chunk.rs rename to src/traits/bits_edit/impls_for_u64_slice/tests_for_read_chunk.rs diff --git a/src/bit_string/traits/bits_edit/impls_for_u64_slice/tests_for_set_bit.rs b/src/traits/bits_edit/impls_for_u64_slice/tests_for_set_bit.rs similarity index 100% rename from src/bit_string/traits/bits_edit/impls_for_u64_slice/tests_for_set_bit.rs rename to src/traits/bits_edit/impls_for_u64_slice/tests_for_set_bit.rs diff --git a/src/bit_string/traits/bits_edit/impls_for_u64_slice/tests_for_write_chunk.rs b/src/traits/bits_edit/impls_for_u64_slice/tests_for_write_chunk.rs similarity index 100% rename from src/bit_string/traits/bits_edit/impls_for_u64_slice/tests_for_write_chunk.rs rename to src/traits/bits_edit/impls_for_u64_slice/tests_for_write_chunk.rs diff --git a/src/bit_string/traits/bits_eq.rs b/src/traits/bits_eq.rs similarity index 100% rename from src/bit_string/traits/bits_eq.rs rename to src/traits/bits_eq.rs diff --git a/src/bit_string/traits/bits_eq/funcs_for_eq_words_aligned_core.rs b/src/traits/bits_eq/funcs_for_eq_words_aligned_core.rs similarity index 100% rename from src/bit_string/traits/bits_eq/funcs_for_eq_words_aligned_core.rs rename to src/traits/bits_eq/funcs_for_eq_words_aligned_core.rs diff --git a/src/bit_string/traits/bits_eq/funcs_for_eq_words_aligned_core/tests_for_backend_equivalence.rs b/src/traits/bits_eq/funcs_for_eq_words_aligned_core/tests_for_backend_equivalence.rs similarity index 100% rename from src/bit_string/traits/bits_eq/funcs_for_eq_words_aligned_core/tests_for_backend_equivalence.rs rename to src/traits/bits_eq/funcs_for_eq_words_aligned_core/tests_for_backend_equivalence.rs diff --git a/src/bit_string/traits/bits_eq/funcs_for_eq_words_unaligned_core.rs b/src/traits/bits_eq/funcs_for_eq_words_unaligned_core.rs similarity index 100% rename from src/bit_string/traits/bits_eq/funcs_for_eq_words_unaligned_core.rs rename to src/traits/bits_eq/funcs_for_eq_words_unaligned_core.rs diff --git a/src/bit_string/traits/bits_eq/funcs_for_eq_words_unaligned_core/tests_for_backend_equivalence.rs b/src/traits/bits_eq/funcs_for_eq_words_unaligned_core/tests_for_backend_equivalence.rs similarity index 100% rename from src/bit_string/traits/bits_eq/funcs_for_eq_words_unaligned_core/tests_for_backend_equivalence.rs rename to src/traits/bits_eq/funcs_for_eq_words_unaligned_core/tests_for_backend_equivalence.rs diff --git a/src/bit_string/traits/bits_eq/impls_for_u64_slice.rs b/src/traits/bits_eq/impls_for_u64_slice.rs similarity index 100% rename from src/bit_string/traits/bits_eq/impls_for_u64_slice.rs rename to src/traits/bits_eq/impls_for_u64_slice.rs From f8be3ea6735fa2e11bd7140a107b585a09747f4b Mon Sep 17 00:00:00 2001 From: juncheng Date: Sun, 21 Jun 2026 10:46:18 +0000 Subject: [PATCH 02/22] feat: BitStr access, predicates, and slice with tests Implement BitStr access methods (get, first, last, get_chunk) with boundary masking to prevent leaking source bits beyond the view. Add BitStr predicates (is_empty implemented, rest stubbed) and zero-copy slice via UsizeCO (non-empty invariant encoded in type). Split BitString access into impls_for_access (get/get_chunk) and impls_for_predicates (any/all/is_empty) to match BitStr structure. Add 17 tests covering empty views, offset views, cross-word boundaries, unaligned reads, and chunk non-leakage. Co-authored-by: Claude Co-authored-by: DeepSeek AI --- src/bit_str.rs | 13 +- src/bit_str/errors.rs | 19 ++ src/bit_str/impls_for_access.rs | 46 +++++ src/bit_str/impls_for_access/tests_for_get.rs | 165 ++++++++++++++++++ .../impls_for_access/tests_for_get_chunk.rs | 102 +++++++++++ src/bit_str/impls_for_predicates.rs | 28 +++ src/bit_str/impls_for_slice.rs | 30 ++++ src/bit_string.rs | 11 ++ src/bit_string/impls_for_access.rs | 27 --- src/bit_string/impls_for_predicates.rs | 28 +++ src/lib.rs | 1 + 11 files changed, 439 insertions(+), 31 deletions(-) create mode 100644 src/bit_str/errors.rs create mode 100644 src/bit_str/impls_for_access.rs create mode 100644 src/bit_str/impls_for_access/tests_for_get.rs create mode 100644 src/bit_str/impls_for_access/tests_for_get_chunk.rs create mode 100644 src/bit_str/impls_for_predicates.rs create mode 100644 src/bit_str/impls_for_slice.rs create mode 100644 src/bit_string/impls_for_predicates.rs diff --git a/src/bit_str.rs b/src/bit_str.rs index 3bc7502..c767ed9 100644 --- a/src/bit_str.rs +++ b/src/bit_str.rs @@ -12,16 +12,21 @@ use crate::BitString; /// /// # Lifetime /// -/// The lifetime `'a` is tied to the source [`BitString`]. While any `BitStr` +/// The lifetime `'bs` is tied to the source [`BitString`]. While any `BitStr` /// is live, Rust's borrow checker prevents mutation of the source. #[derive(Clone, Copy)] pub struct BitStr<'bs> { - source: &'bs BitString, - start: usize, + pub(crate) source: &'bs BitString, + pub(crate) start: usize, /// Number of bits in this view. - bit_len: usize, + pub(crate) bit_len: usize, } +pub mod errors; +mod impls_for_access; +mod impls_for_predicates; +mod impls_for_slice; + // --------------------------------------------------------------------------- // Getters // --------------------------------------------------------------------------- diff --git a/src/bit_str/errors.rs b/src/bit_str/errors.rs new file mode 100644 index 0000000..d87c168 --- /dev/null +++ b/src/bit_str/errors.rs @@ -0,0 +1,19 @@ +use core::fmt; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct BitStrRangeError { + pub source_len: usize, + pub requested_start: usize, + pub requested_len: usize, +} + +impl fmt::Display for BitStrRangeError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "BitStr range out of bounds: source has {} bits, \ + requested start={}, len={}", + self.source_len, self.requested_start, self.requested_len, + ) + } +} diff --git a/src/bit_str/impls_for_access.rs b/src/bit_str/impls_for_access.rs new file mode 100644 index 0000000..a249247 --- /dev/null +++ b/src/bit_str/impls_for_access.rs @@ -0,0 +1,46 @@ +use crate::low_mask; +use crate::traits::*; + +use super::*; + +impl<'bs> BitStr<'bs> { + /// Returns the bit at `index`, or `None` when `index >= self.bit_len()`. + #[inline] + pub fn get(&self, index: usize) -> Option { + if index >= self.bit_len { + return None; + } + Some(self.source.words().read_bit_at(self.start + index)) + } + + /// Returns the first bit, or `None` when the view is empty. + #[inline] + pub fn first(&self) -> Option { + self.get(0) + } + + /// Returns the last bit, or `None` when the view is empty. + #[inline] + pub fn last(&self) -> Option { + self.bit_len.checked_sub(1).and_then(|i| self.get(i)) + } + + /// Reads up to 64 bits starting at `bit_start`, returning them in the + /// low bits of a `u64`. + /// + /// Bits beyond `self.bit_len()` are treated as zero. + #[inline] + pub fn get_chunk(&self, bit_start: usize) -> u64 { + let valid_bits = self.bit_len.saturating_sub(bit_start); + if valid_bits == 0 { + return 0; + } + let raw = self.source.words().read_word_at(self.start + bit_start); + raw & low_mask(valid_bits) + } +} + +#[cfg(test)] +mod tests_for_get; +#[cfg(test)] +mod tests_for_get_chunk; diff --git a/src/bit_str/impls_for_access/tests_for_get.rs b/src/bit_str/impls_for_access/tests_for_get.rs new file mode 100644 index 0000000..371e344 --- /dev/null +++ b/src/bit_str/impls_for_access/tests_for_get.rs @@ -0,0 +1,165 @@ +use int_interval::UsizeCO; + +use crate::BitStr; +use crate::BitString; + +// --------------------------------------------------------------------------- +// Empty views (from empty source or direct construct) +// --------------------------------------------------------------------------- + +#[test] +fn empty_source_returns_none() { + let s = BitString::new(); + let v = s.as_bitstr(); + assert!(v.is_empty()); + assert_eq!(v.get(0), None); + assert_eq!(v.get(usize::MAX), None); +} + +#[test] +fn empty_view_via_direct_construct_returns_none() { + let s = BitString::try_from("110").unwrap(); + let v = BitStr { + source: &s, + start: 1, + bit_len: 0, + }; + assert!(v.is_empty()); + assert_eq!(v.get(0), None); +} + +// --------------------------------------------------------------------------- +// In-bounds reads (full source view) +// --------------------------------------------------------------------------- + +#[test] +fn full_view_reads_all_bits() { + let s = BitString::try_from("101001").unwrap(); + let v = s.as_bitstr(); + + assert_eq!(v.get(0), Some(true)); + assert_eq!(v.get(1), Some(false)); + assert_eq!(v.get(2), Some(true)); + assert_eq!(v.get(3), Some(false)); + assert_eq!(v.get(4), Some(false)); + assert_eq!(v.get(5), Some(true)); +} + +#[test] +fn returns_none_at_bit_len_and_beyond() { + let s = BitString::try_from("101001").unwrap(); + let v = s.as_bitstr(); + + assert_eq!(v.get(6), None); // exactly bit_len + assert_eq!(v.get(7), None); + assert_eq!(v.get(100), None); + assert_eq!(v.get(usize::MAX), None); +} + +#[test] +fn first_and_last_on_full_view() { + let s = BitString::try_from("101001").unwrap(); + let v = s.as_bitstr(); + + assert_eq!(v.first(), Some(true)); + assert_eq!(v.last(), Some(true)); +} + +// --------------------------------------------------------------------------- +// Offset views via slice (UsizeCO guarantees non-empty) +// --------------------------------------------------------------------------- + +#[test] +fn offset_view_reads_correct_bits() { + let s = BitString::try_from("00110110").unwrap(); // 0,0,1,1,0,1,1,0 + let v = s.as_bitstr().slice(UsizeCO::try_new(2, 6).unwrap()); + + assert_eq!(v.bit_len(), 4); + assert_eq!(v.start(), 2); + assert_eq!(v.get(0), Some(true)); // src bit 2 + assert_eq!(v.get(1), Some(true)); // src bit 3 + assert_eq!(v.get(2), Some(false)); // src bit 4 + assert_eq!(v.get(3), Some(true)); // src bit 5 + assert_eq!(v.get(4), None); // beyond view +} + +#[test] +fn offset_view_first_and_last() { + let s = BitString::try_from("00011000").unwrap(); + let v = s.as_bitstr().slice(UsizeCO::try_new(3, 5).unwrap()); + + assert_eq!(v.first(), Some(true)); + assert_eq!(v.last(), Some(true)); +} + +// --------------------------------------------------------------------------- +// Cross-word boundary views +// --------------------------------------------------------------------------- + +#[test] +fn cross_word_boundary_in_offset_view() { + let mut s = BitString::zeros(130); + s.set(62, true); + s.set(63, true); + s.set(64, true); + s.set(65, true); + s.set(129, true); + + let v = s.as_bitstr().slice(UsizeCO::try_new(62, 130).unwrap()); // 68 bits + + assert_eq!(v.bit_len(), 68); + assert_eq!(v.get(0), Some(true)); // src bit 62 + assert_eq!(v.get(1), Some(true)); // src bit 63 + assert_eq!(v.get(2), Some(true)); // src bit 64 + assert_eq!(v.get(3), Some(true)); // src bit 65 + assert_eq!(v.get(4), Some(false)); // src bit 66 + assert_eq!(v.get(66), Some(false)); // src bit 128 + assert_eq!(v.get(67), Some(true)); // src bit 129 + assert_eq!(v.get(68), None); +} + +#[test] +fn view_starting_unaligned_and_crossing_word() { + let mut s = BitString::zeros(130); + s.set(60, true); + s.set(63, true); + s.set(64, true); + s.set(70, true); + + let v = s.as_bitstr().slice(UsizeCO::try_new(60, 72).unwrap()); // 12 bits + + assert_eq!(v.get(0), Some(true)); // src bit 60 + assert_eq!(v.get(1), Some(false)); + assert_eq!(v.get(2), Some(false)); + assert_eq!(v.get(3), Some(true)); // src bit 63 + assert_eq!(v.get(4), Some(true)); // src bit 64 + assert_eq!(v.get(10), Some(true)); // src bit 70 + assert_eq!(v.get(11), Some(false)); + assert_eq!(v.get(12), None); +} + +// --------------------------------------------------------------------------- +// Single-bit views +// --------------------------------------------------------------------------- + +#[test] +fn single_bit_view() { + let s = BitString::try_from("101").unwrap(); + let v = s.as_bitstr().slice(UsizeCO::try_new(1, 2).unwrap()); + + assert_eq!(v.bit_len(), 1); + assert_eq!(v.get(0), Some(false)); + assert_eq!(v.get(1), None); + assert_eq!(v.first(), Some(false)); + assert_eq!(v.last(), Some(false)); +} + +#[test] +fn single_bit_view_at_last_position() { + let s = BitString::try_from("101").unwrap(); + let v = s.as_bitstr().slice(UsizeCO::try_new(2, 3).unwrap()); + + assert_eq!(v.bit_len(), 1); + assert_eq!(v.get(0), Some(true)); + assert_eq!(v.get(1), None); +} diff --git a/src/bit_str/impls_for_access/tests_for_get_chunk.rs b/src/bit_str/impls_for_access/tests_for_get_chunk.rs new file mode 100644 index 0000000..33c77df --- /dev/null +++ b/src/bit_str/impls_for_access/tests_for_get_chunk.rs @@ -0,0 +1,102 @@ +use int_interval::UsizeCO; + +use crate::BitStr; +use crate::BitString; +use crate::low_mask; + +// --------------------------------------------------------------------------- +// Full source view +// --------------------------------------------------------------------------- + +#[test] +fn aligned_chunk_from_start() { + let s = BitString::try_from("1").unwrap(); + let v = s.as_bitstr(); + assert_eq!(v.get_chunk(0), 1); +} + +#[test] +fn aligned_chunk_spanning_two_words() { + let mut s = BitString::zeros(130); + s.set(63, true); // last bit of word 0 + s.set(64, true); // first bit of word 1 + + let v = s.as_bitstr(); + + let chunk = v.get_chunk(63); + assert_eq!(chunk & 1, 1); // bit 63 → LSB of chunk + assert_eq!((chunk >> 1) & 1, 1); // bit 64 + assert_eq!(chunk >> 2, 0); // nothing beyond valid bits +} + +// --------------------------------------------------------------------------- +// Unaligned offset views +// --------------------------------------------------------------------------- + +#[test] +fn unaligned_chunk_in_offset_view() { + let mut s = BitString::zeros(130); + s.set(61, true); + s.set(62, true); + s.set(63, true); + s.set(64, true); // crosses into word 1 + + let v = s.as_bitstr().slice(UsizeCO::try_new(61, 66).unwrap()); // 5 bits + + // Bits 61..65: 1,1,1,1,0 → 0b01111 = 15 + assert_eq!(v.get_chunk(0), 0b01111); + // Remaining 3 bits: 1,1,0 → 0b011 = 3 + assert_eq!(v.get_chunk(2), 0b011); +} + +// --------------------------------------------------------------------------- +// Edge: reading at or beyond view boundary +// --------------------------------------------------------------------------- + +#[test] +fn chunk_at_bit_len_returns_zero() { + let s = BitString::try_from("11111111").unwrap(); + let v = s.as_bitstr().slice(UsizeCO::try_new(2, 5).unwrap()); // 3 bits + + assert_eq!(v.get_chunk(3), 0); // exactly bit_len + assert_eq!(v.get_chunk(4), 0); // beyond + assert_eq!(v.get_chunk(100), 0); + assert_eq!(v.get_chunk(usize::MAX), 0); +} + +#[test] +fn chunk_does_not_leak_bits_beyond_view() { + let s = BitString::ones(130); + let v = s.as_bitstr().slice(UsizeCO::try_new(60, 70).unwrap()); // 10 bits + + let chunk = v.get_chunk(5); // 5 remaining valid bits + assert_eq!(chunk, low_mask(5)); // exactly 5 ones, nothing leaked + assert_eq!(chunk >> 5, 0); +} + +#[test] +fn chunk_at_last_bit() { + let mut s = BitString::zeros(130); + s.set(129, true); + let v = s.as_bitstr().slice(UsizeCO::try_new(128, 130).unwrap()); // 2 bits + + assert_eq!(v.get_chunk(1), 1); // just bit 129 + assert_eq!(v.get_chunk(2), 0); // beyond view +} + +// --------------------------------------------------------------------------- +// Empty view +// --------------------------------------------------------------------------- + +#[test] +fn empty_view_chunk_always_zero() { + let s = BitString::ones(64); + let v = BitStr { + source: &s, + start: 10, + bit_len: 0, + }; + + assert_eq!(v.get_chunk(0), 0); + assert_eq!(v.get_chunk(1), 0); +} diff --git a/src/bit_str/impls_for_predicates.rs b/src/bit_str/impls_for_predicates.rs new file mode 100644 index 0000000..a22fd5f --- /dev/null +++ b/src/bit_str/impls_for_predicates.rs @@ -0,0 +1,28 @@ +use super::*; + +impl<'bs> BitStr<'bs> { + #[inline] + pub fn is_empty(&self) -> bool { + self.bit_len == 0 + } + + #[inline] + pub fn any(&self) -> bool { + todo!() + } + + #[inline] + pub fn all(&self) -> bool { + todo!() + } + + #[inline] + pub fn is_all_zeros(&self) -> bool { + todo!() + } + + #[inline] + pub fn is_all_ones(&self) -> bool { + todo!() + } +} diff --git a/src/bit_str/impls_for_slice.rs b/src/bit_str/impls_for_slice.rs new file mode 100644 index 0000000..090fe27 --- /dev/null +++ b/src/bit_str/impls_for_slice.rs @@ -0,0 +1,30 @@ +use int_interval::UsizeCO; + +use super::*; + +impl<'bs> BitStr<'bs> { + /// Returns a sub-view of the bits in `interval`. + /// + /// The interval is clamped to `[0, self.bit_len()]`. An interval beyond the + /// view returns an empty result. + #[inline] + pub fn slice(&self, interval: UsizeCO) -> Self { + let s = interval.start().min(self.bit_len); + let e = interval.end_excl().min(self.bit_len).max(s); + Self { + source: self.source, + start: self.start + s, + bit_len: e - s, + } + } + + #[inline] + pub fn slice_from(&self, _at: usize) -> Self { + todo!() + } + + #[inline] + pub fn slice_until(&self, _to: usize) -> Self { + todo!() + } +} diff --git a/src/bit_string.rs b/src/bit_string.rs index d452c44..d740285 100644 --- a/src/bit_string.rs +++ b/src/bit_string.rs @@ -22,6 +22,16 @@ impl BitString { pub fn words(&self) -> &[u64] { &self.words } + + /// Returns a zero-copy [`BitStr`] view of the entire bit string. + #[inline] + pub fn as_bitstr(&self) -> crate::BitStr<'_> { + crate::BitStr { + source: self, + start: 0, + bit_len: self.bit_len, + } + } } pub mod errors; @@ -32,6 +42,7 @@ mod impls_for_editing; mod impls_for_fmt; mod impls_for_iter; mod impls_for_matching; +mod impls_for_predicates; #[cfg(test)] mod tests_for_proptest; diff --git a/src/bit_string/impls_for_access.rs b/src/bit_string/impls_for_access.rs index 4ac4ae9..5ded899 100644 --- a/src/bit_string/impls_for_access.rs +++ b/src/bit_string/impls_for_access.rs @@ -2,33 +2,6 @@ use crate::traits::*; use super::*; -impl BitString { - #[inline] - pub fn any(&self) -> bool { - self.count_ones() != 0 - } - - #[inline] - pub fn all(&self) -> bool { - self.count_ones() == self.bit_len - } - - #[inline] - pub fn is_all_zeros(&self) -> bool { - !self.any() - } - - #[inline] - pub fn is_all_ones(&self) -> bool { - self.all() - } - - #[inline] - pub fn is_empty(&self) -> bool { - self.bit_len == 0 - } -} - impl BitString { #[inline] pub fn get(&self, index: usize) -> Option { diff --git a/src/bit_string/impls_for_predicates.rs b/src/bit_string/impls_for_predicates.rs new file mode 100644 index 0000000..892fdfb --- /dev/null +++ b/src/bit_string/impls_for_predicates.rs @@ -0,0 +1,28 @@ +use super::*; + +impl BitString { + #[inline] + pub fn any(&self) -> bool { + self.count_ones() != 0 + } + + #[inline] + pub fn all(&self) -> bool { + self.count_ones() == self.bit_len + } + + #[inline] + pub fn is_all_zeros(&self) -> bool { + !self.any() + } + + #[inline] + pub fn is_all_ones(&self) -> bool { + self.all() + } + + #[inline] + pub fn is_empty(&self) -> bool { + self.bit_len == 0 + } +} diff --git a/src/lib.rs b/src/lib.rs index e22442a..e2864ca 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -15,4 +15,5 @@ mod bit_string; pub use bit_str::BitStr; pub use bit_string::BitString; +pub use bit_str::errors::*; pub use bit_string::errors::*; From 665670726c1bbe631d39ade57d31c5df1e305605 Mon Sep 17 00:00:00 2001 From: juncheng Date: Tue, 23 Jun 2026 12:31:32 +0000 Subject: [PATCH 03/22] feat: BitStr count_ones and count_zeros Add count_ones() and count_zeros() to BitStr, mirroring the BitString API. Word-aligned views delegate directly to the SIMD-accelerated [64]::count_ones. Unaligned views handle the first partial word via scalar popcnt, dispatch middle full words through the SIMD path, then handle the last partial word. Co-Authored-By: Claude Co-Authored-By: DeepSeek AI --- src/bit_str.rs | 1 + src/bit_str/impls_for_bit_arith.rs | 1 + .../impls_for_count_ones.rs | 72 ++++++++ .../tests_for_count_ones.rs | 155 ++++++++++++++++++ 4 files changed, 229 insertions(+) create mode 100644 src/bit_str/impls_for_bit_arith.rs create mode 100644 src/bit_str/impls_for_bit_arith/impls_for_count_ones.rs create mode 100644 src/bit_str/impls_for_bit_arith/impls_for_count_ones/tests_for_count_ones.rs diff --git a/src/bit_str.rs b/src/bit_str.rs index c767ed9..8c3cc8b 100644 --- a/src/bit_str.rs +++ b/src/bit_str.rs @@ -24,6 +24,7 @@ pub struct BitStr<'bs> { pub mod errors; mod impls_for_access; +mod impls_for_bit_arith; mod impls_for_predicates; mod impls_for_slice; diff --git a/src/bit_str/impls_for_bit_arith.rs b/src/bit_str/impls_for_bit_arith.rs new file mode 100644 index 0000000..e8f484b --- /dev/null +++ b/src/bit_str/impls_for_bit_arith.rs @@ -0,0 +1 @@ +mod impls_for_count_ones; diff --git a/src/bit_str/impls_for_bit_arith/impls_for_count_ones.rs b/src/bit_str/impls_for_bit_arith/impls_for_count_ones.rs new file mode 100644 index 0000000..bdc3129 --- /dev/null +++ b/src/bit_str/impls_for_bit_arith/impls_for_count_ones.rs @@ -0,0 +1,72 @@ +use crate::traits::*; +use crate::{WORD_BITS, low_mask}; + +use crate::BitStr; + +impl<'bs> BitStr<'bs> { + /// Returns the number of bits set to 1. + #[inline] + pub fn count_ones(&self) -> usize { + if self.bit_len == 0 { + return 0; + } + + let words = self.source.words(); + + // Fast path: when `start` is word-aligned we can delegate directly to + // the SIMD-accelerated `[u64]::count_ones` on the relevant suffix. + if self.start % WORD_BITS == 0 { + let word_start = self.start / WORD_BITS; + return words[word_start..].count_ones(self.bit_len); + } + + // Unaligned start: count across word boundaries. + let start_word = self.start / WORD_BITS; + let start_offset = self.start % WORD_BITS; + let end = self.start + self.bit_len; + let last_word = (end - 1) / WORD_BITS; + + // All bits lie within a single word. + if start_word == last_word { + let mask = low_mask(self.bit_len) << start_offset; + return (words[start_word] & mask).count_ones() as usize; + } + + let mut count = 0usize; + + // First word: bits from start_offset upward. + count += (words[start_word] >> start_offset).count_ones() as usize; + + let end_rem = end % WORD_BITS; + + // Middle words: full u64 words, SIMD-accelerated via + // `[u64]::count_ones`. The last word is included in the SIMD path + // when it is full (end_rem == 0), otherwise handled separately. + let mid_start = start_word + 1; + let mid_end = if end_rem == 0 { + last_word + 1 + } else { + last_word + }; + let full_word_count = mid_end.saturating_sub(mid_start); + if full_word_count > 0 { + count += words[mid_start..mid_end].count_ones(full_word_count * WORD_BITS); + } + + // Last word: partial. + if end_rem != 0 { + count += (words[last_word] & low_mask(end_rem)).count_ones() as usize; + } + + count + } + + /// Returns the number of bits set to 0. + #[inline] + pub fn count_zeros(&self) -> usize { + self.bit_len - self.count_ones() + } +} + +#[cfg(test)] +mod tests_for_count_ones; diff --git a/src/bit_str/impls_for_bit_arith/impls_for_count_ones/tests_for_count_ones.rs b/src/bit_str/impls_for_bit_arith/impls_for_count_ones/tests_for_count_ones.rs new file mode 100644 index 0000000..76306db --- /dev/null +++ b/src/bit_str/impls_for_bit_arith/impls_for_count_ones/tests_for_count_ones.rs @@ -0,0 +1,155 @@ +use int_interval::UsizeCO; + +use crate::BitString; + +/// An empty view always has zero ones and zeros. +#[test] +fn counts_empty_view() { + let bits = BitString::try_from("10110").unwrap(); + // Create an empty view by slicing beyond the source length. + let v = bits.as_bitstr().slice(UsizeCO::try_new(10, 20).unwrap()); + + assert_eq!(v.count_ones(), 0); + assert_eq!(v.count_zeros(), 0); +} + +/// Word-aligned views take the SIMD fast path. +#[test] +fn word_aligned_fast_path() { + let mut bits = BitString::zeros(130); + for i in [0, 63, 64, 65, 127, 128, 129] { + bits.set(i, true); + } + // Full view: bit_len=130, aligned, 7 ones. + let v = bits.as_bitstr(); + assert_eq!(v.count_ones(), 7); + assert_eq!(v.count_zeros(), 123); + + // Slice from word-aligned boundary (64..130), 6 ones. + let v = bits.as_bitstr().slice(UsizeCO::try_new(64, 130).unwrap()); + assert_eq!(v.count_ones(), 5); +} + +/// Unaligned start: the first word is partial. +#[test] +fn unaligned_start() { + let mut bits = BitString::zeros(130); + // Set only bit 1 and bit 66 (word 0 offset 1, word 1 offset 2). + bits.set(1, true); + bits.set(66, true); + + // View from bit 1 to bit 130 → len 129, unaligned start. + let v = bits.as_bitstr().slice(UsizeCO::try_new(1, 130).unwrap()); + assert_eq!(v.count_ones(), 2); + assert_eq!(v.count_zeros(), 127); +} + +/// All-ones string of 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); + let v = bits.as_bitstr(); + assert_eq!(v.count_ones(), len, "len={len}"); + assert_eq!(v.count_zeros(), 0, "len={len}"); + } +} + +/// All-zeros string of 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); + let v = bits.as_bitstr(); + assert_eq!(v.count_ones(), 0, "len={len}"); + assert_eq!(v.count_zeros(), len, "len={len}"); + } +} + +/// Mixed bits from a string pattern. +#[test] +fn counts_mixed_bits_from_string() { + let bits = BitString::try_from("1010011100").unwrap(); + let v = bits.as_bitstr(); + + assert_eq!(v.count_ones(), 5); + assert_eq!(v.count_zeros(), 5); +} + +/// Unaligned subrange within a single word. +#[test] +fn unaligned_single_word() { + let bits = BitString::try_from("11110000").unwrap(); + // bits: 1 1 1 1 0 0 0 0 + + // View bits 1..7 → "111000" + let v = bits.as_bitstr().slice(UsizeCO::try_new(1, 7).unwrap()); + assert_eq!(v.count_ones(), 3); + assert_eq!(v.count_zeros(), 3); + + // View bits 2..6 → "1100" + let v = bits.as_bitstr().slice(UsizeCO::try_new(2, 6).unwrap()); + assert_eq!(v.count_ones(), 2); + assert_eq!(v.count_zeros(), 2); +} + +/// Invariant: count_ones + count_zeros == bit_len for every view. +#[test] +fn invariant_ones_plus_zeros_equals_bit_len() { + let mut bits = BitString::zeros(200); + for i in (0..200).step_by(7) { + bits.set(i, true); + } + let full = bits.as_bitstr(); + + 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()); + // Skip degenerate cases where start == end (empty interval not + // constructible via try_new). + if start == end { + continue; + } + let v = full.slice(UsizeCO::try_new(start, end).unwrap()); + assert_eq!( + v.count_ones() + v.count_zeros(), + v.bit_len(), + "start={start} end={end}" + ); + } + } +} + +/// Unaligned view spanning many words exercises the SIMD middle-word path. +#[test] +fn unaligned_many_words() { + let mut bits = BitString::zeros(300); + // Set bits at known positions across multiple words. + for i in [1, 63, 64, 100, 127, 128, 129, 200, 255, 256, 299] { + bits.set(i, true); + } + + // View from bit 1 (unaligned) covering bits 1..300 → 299 bits length. + // Bit 1 is included; all 11 bits are in range [1, 300). + let v = bits.as_bitstr().slice(UsizeCO::try_new(1, 300).unwrap()); + assert_eq!(v.count_ones(), 11); + assert_eq!(v.count_zeros(), 288); +} + +/// Word-aligned view exactly one word long. +#[test] +fn aligned_one_word() { + let mut bits = BitString::zeros(128); + bits.set(0, true); + bits.set(63, true); + + // View word 0 (bits 0..64) + let v = bits.as_bitstr().slice(UsizeCO::try_new(0, 64).unwrap()); + assert_eq!(v.count_ones(), 2); + assert_eq!(v.count_zeros(), 62); + + // View word 1 (bits 64..128) + let v = bits.as_bitstr().slice(UsizeCO::try_new(64, 128).unwrap()); + assert_eq!(v.count_ones(), 0); + assert_eq!(v.count_zeros(), 64); +} From ff633ef2b10bb8cfea55ac70b3e916aa554230c5 Mon Sep 17 00:00:00 2001 From: juncheng Date: Tue, 23 Jun 2026 12:41:01 +0000 Subject: [PATCH 04/22] feat: BitStr predicates and slice_from/slice_until Implement any/all/is_all_zeros/is_all_ones via count_ones, matching the BitString pattern. Complete slice_from and slice_until with clamping semantics, plus 22 tests covering empty views, chaining, offset views, and mixed usage. Co-Authored-By: Claude Co-Authored-By: DeepSeek AI --- src/bit_str/impls_for_predicates.rs | 8 +- src/bit_str/impls_for_slice.rs | 28 +- .../impls_for_slice/tests_for_slice.rs | 294 ++++++++++++++++++ 3 files changed, 322 insertions(+), 8 deletions(-) create mode 100644 src/bit_str/impls_for_slice/tests_for_slice.rs diff --git a/src/bit_str/impls_for_predicates.rs b/src/bit_str/impls_for_predicates.rs index a22fd5f..b574661 100644 --- a/src/bit_str/impls_for_predicates.rs +++ b/src/bit_str/impls_for_predicates.rs @@ -8,21 +8,21 @@ impl<'bs> BitStr<'bs> { #[inline] pub fn any(&self) -> bool { - todo!() + self.count_ones() != 0 } #[inline] pub fn all(&self) -> bool { - todo!() + self.count_ones() == self.bit_len } #[inline] pub fn is_all_zeros(&self) -> bool { - todo!() + !self.any() } #[inline] pub fn is_all_ones(&self) -> bool { - todo!() + self.all() } } diff --git a/src/bit_str/impls_for_slice.rs b/src/bit_str/impls_for_slice.rs index 090fe27..9b89e99 100644 --- a/src/bit_str/impls_for_slice.rs +++ b/src/bit_str/impls_for_slice.rs @@ -18,13 +18,33 @@ impl<'bs> BitStr<'bs> { } } + /// Returns a sub-view from `at` to the end. + /// + /// The start is clamped to `self.bit_len()`. When `at >= self.bit_len()` + /// the result is empty. #[inline] - pub fn slice_from(&self, _at: usize) -> Self { - todo!() + pub fn slice_from(&self, at: usize) -> Self { + let s = at.min(self.bit_len); + Self { + source: self.source, + start: self.start + s, + bit_len: self.bit_len - s, + } } + /// Returns a sub-view from the start to `to`. + /// + /// `to` is clamped to `self.bit_len()`. When `to == 0` the result is empty. #[inline] - pub fn slice_until(&self, _to: usize) -> Self { - todo!() + pub fn slice_until(&self, to: usize) -> Self { + let e = to.min(self.bit_len); + Self { + source: self.source, + start: self.start, + bit_len: e, + } } } + +#[cfg(test)] +mod tests_for_slice; diff --git a/src/bit_str/impls_for_slice/tests_for_slice.rs b/src/bit_str/impls_for_slice/tests_for_slice.rs new file mode 100644 index 0000000..47c3797 --- /dev/null +++ b/src/bit_str/impls_for_slice/tests_for_slice.rs @@ -0,0 +1,294 @@ +use int_interval::UsizeCO; + +use crate::BitString; + +// --------------------------------------------------------------------------- +// slice (UsizeCO) +// --------------------------------------------------------------------------- + +#[test] +fn slice_full_range_returns_identical_view() { + let bits = BitString::try_from("1010011100").unwrap(); + let v = bits.as_bitstr(); + let s = v.slice(UsizeCO::try_new(0, 10).unwrap()); + + assert_eq!(s.bit_len(), v.bit_len()); + assert_eq!(s.start(), v.start()); + // Bits are identical. + for i in 0..v.bit_len() { + assert_eq!(s.get(i), v.get(i), "bit {i}"); + } +} + +#[test] +fn slice_subrange_within_bounds() { + let bits = BitString::try_from("11110000").unwrap(); + let v = bits.as_bitstr(); + + // Slice bits 2..6 → "1100" + let s = v.slice(UsizeCO::try_new(2, 6).unwrap()); + assert_eq!(s.bit_len(), 4); + assert_eq!(s.get(0), Some(true)); + assert_eq!(s.get(1), Some(true)); + assert_eq!(s.get(2), Some(false)); + assert_eq!(s.get(3), Some(false)); +} + +#[test] +fn slice_end_beyond_view_is_clamped() { + let bits = BitString::try_from("10110").unwrap(); + let v = bits.as_bitstr(); + + // [2, 100) → clamped to [2, 5) + let s = v.slice(UsizeCO::try_new(2, 100).unwrap()); + assert_eq!(s.bit_len(), 3); + assert_eq!(s.get(0), v.get(2)); + assert_eq!(s.get(1), v.get(3)); + assert_eq!(s.get(2), v.get(4)); +} + +#[test] +fn slice_start_beyond_view_is_empty() { + let bits = BitString::try_from("10110").unwrap(); + let v = bits.as_bitstr(); + + // [10, 20) → start clamped to 5, end clamped to 5 → empty + let s = v.slice(UsizeCO::try_new(10, 20).unwrap()); + assert_eq!(s.bit_len(), 0); +} + +#[test] +fn slice_single_bit() { + let bits = BitString::try_from("10110").unwrap(); + let v = bits.as_bitstr(); + + let s = v.slice(UsizeCO::try_new(0, 1).unwrap()); + assert_eq!(s.bit_len(), 1); + assert_eq!(s.get(0), Some(true)); +} + +#[test] +fn slice_last_bit() { + let bits = BitString::try_from("10110").unwrap(); + let v = bits.as_bitstr(); + + let s = v.slice(UsizeCO::try_new(4, 5).unwrap()); + assert_eq!(s.bit_len(), 1); + assert_eq!(s.get(0), Some(false)); // last bit is 0 +} + +#[test] +fn slice_chaining() { + let bits = BitString::try_from("11110000").unwrap(); + let v = bits.as_bitstr(); + + // First slice bits 2..7 → "11000" + let s1 = v.slice(UsizeCO::try_new(2, 7).unwrap()); + assert_eq!(s1.bit_len(), 5); + + // Then slice bits 1..4 of s1 → bits (2+1)..(2+4) of original → "100" + let s2 = s1.slice(UsizeCO::try_new(1, 4).unwrap()); + assert_eq!(s2.bit_len(), 3); + assert_eq!(s2.get(0), v.get(3)); + assert_eq!(s2.get(1), v.get(4)); + assert_eq!(s2.get(2), v.get(5)); +} + +// --------------------------------------------------------------------------- +// slice_from +// --------------------------------------------------------------------------- + +#[test] +fn slice_from_zero_returns_identical_view() { + let bits = BitString::try_from("1010011100").unwrap(); + let v = bits.as_bitstr(); + let s = v.slice_from(0); + + assert_eq!(s.bit_len(), v.bit_len()); + assert_eq!(s.start(), v.start()); +} + +#[test] +fn slice_from_mid() { + let bits = BitString::try_from("11110000").unwrap(); + let v = bits.as_bitstr(); + + let s = v.slice_from(4); + assert_eq!(s.bit_len(), 4); + // Original bits 4..8 → "0000" + for i in 0..4 { + assert_eq!(s.get(i), Some(false)); + } +} + +#[test] +fn slice_from_returns_empty_at_boundary() { + let bits = BitString::try_from("10110").unwrap(); + let v = bits.as_bitstr(); + + let s = v.slice_from(5); + assert_eq!(s.bit_len(), 0); +} + +#[test] +fn slice_from_beyond_boundary_returns_empty() { + let bits = BitString::try_from("10110").unwrap(); + let v = bits.as_bitstr(); + + let s = v.slice_from(100); + assert_eq!(s.bit_len(), 0); +} + +#[test] +fn slice_from_chaining() { + let bits = BitString::try_from("11110000").unwrap(); + let v = bits.as_bitstr(); + + // slice_from(2) → bits 2..8 → "110000" + let s1 = v.slice_from(2); + assert_eq!(s1.bit_len(), 6); + + // slice_from(3) of s1 → bits 5..8 → "000" + let s2 = s1.slice_from(3); + assert_eq!(s2.bit_len(), 3); + assert_eq!(s2.get(0), v.get(5)); + assert_eq!(s2.get(1), v.get(6)); + assert_eq!(s2.get(2), v.get(7)); +} + +// --------------------------------------------------------------------------- +// slice_until +// --------------------------------------------------------------------------- + +#[test] +fn slice_until_full_returns_identical_view() { + let bits = BitString::try_from("1010011100").unwrap(); + let v = bits.as_bitstr(); + let s = v.slice_until(10); + + assert_eq!(s.bit_len(), v.bit_len()); + assert_eq!(s.start(), v.start()); +} + +#[test] +fn slice_until_mid() { + let bits = BitString::try_from("11110000").unwrap(); + let v = bits.as_bitstr(); + + let s = v.slice_until(4); + assert_eq!(s.bit_len(), 4); + assert_eq!(s.get(0), Some(true)); + assert_eq!(s.get(1), Some(true)); + assert_eq!(s.get(2), Some(true)); + assert_eq!(s.get(3), Some(true)); +} + +#[test] +fn slice_until_zero_returns_empty() { + let bits = BitString::try_from("10110").unwrap(); + let v = bits.as_bitstr(); + + let s = v.slice_until(0); + assert_eq!(s.bit_len(), 0); +} + +#[test] +fn slice_until_beyond_boundary_is_clamped() { + let bits = BitString::try_from("10110").unwrap(); + let v = bits.as_bitstr(); + + let s = v.slice_until(100); + assert_eq!(s.bit_len(), 5); +} + +#[test] +fn slice_until_chaining() { + let bits = BitString::try_from("11110000").unwrap(); + let v = bits.as_bitstr(); + + // slice_until(6) → bits 0..6 → "111100" + let s1 = v.slice_until(6); + assert_eq!(s1.bit_len(), 6); + + // slice_until(3) of s1 → bits 0..3 → "111" + let s2 = s1.slice_until(3); + assert_eq!(s2.bit_len(), 3); + assert_eq!(s2.get(0), Some(true)); + assert_eq!(s2.get(1), Some(true)); + assert_eq!(s2.get(2), Some(true)); +} + +// --------------------------------------------------------------------------- +// Mixed slice / slice_from / slice_until +// --------------------------------------------------------------------------- + +#[test] +fn mixed_slice_and_slice_from() { + let bits = BitString::try_from("1011010011").unwrap(); + let v = bits.as_bitstr(); + + // slice(2..8) → bits 2..8 → "110100" + let s1 = v.slice(UsizeCO::try_new(2, 8).unwrap()); + assert_eq!(s1.bit_len(), 6); + + // slice_from(2) of s1 → bits 4..8 → "0100" + let s2 = s1.slice_from(2); + assert_eq!(s2.bit_len(), 4); +} + +#[test] +fn mixed_slice_and_slice_until() { + let bits = BitString::try_from("1011010011").unwrap(); + let v = bits.as_bitstr(); + + // slice(2..8) → bits 2..8 → "110100" + let s1 = v.slice(UsizeCO::try_new(2, 8).unwrap()); + + // slice_until(4) of s1 → bits 2..6 → "1101" + let s2 = s1.slice_until(4); + assert_eq!(s2.bit_len(), 4); +} + +// --------------------------------------------------------------------------- +// Invariants on offset views +// --------------------------------------------------------------------------- + +#[test] +fn slice_on_already_offset_view() { + let bits = BitString::try_from("111100001010").unwrap(); + let v = bits.as_bitstr(); + + // Offset view from bit 3 + let off = v.slice(UsizeCO::try_new(3, 12).unwrap()); + assert_eq!(off.bit_len(), 9); + assert_eq!(off.get(0), v.get(3)); + assert_eq!(off.get(8), v.get(11)); + + // Slice again within the offset view + let s = off.slice(UsizeCO::try_new(2, 7).unwrap()); + assert_eq!(s.bit_len(), 5); + // Should map back to original bits 5..10 + for i in 0..5 { + assert_eq!(s.get(i), v.get(5 + i), "bit {i}"); + } +} + +#[test] +fn slice_from_on_already_offset_view() { + let bits = BitString::try_from("111100001010").unwrap(); + let v = bits.as_bitstr(); + + let off = v.slice(UsizeCO::try_new(3, 12).unwrap()); + let s = off.slice_from(4); + assert_eq!(s.get(0), v.get(7)); +} + +#[test] +fn slice_until_on_already_offset_view() { + let bits = BitString::try_from("111100001010").unwrap(); + let v = bits.as_bitstr(); + + let off = v.slice(UsizeCO::try_new(3, 12).unwrap()); + let s = off.slice_until(4); + assert_eq!(s.get(3), v.get(6)); +} From 72ac70e3de45f4aa4a0be5ea6f788a7b42257d7b Mon Sep 17 00:00:00 2001 From: juncheng Date: Tue, 23 Jun 2026 12:43:12 +0000 Subject: [PATCH 05/22] feat: BitStr double-ended exact-size iterator Add Iter struct implementing Iterator, DoubleEndedIterator, ExactSizeIterator, and FusedIterator for BitStr. Also provides IntoIterator on &BitStr for for-loop ergonomics and to_bool_vec(). 14 tests covering full views, offset views, empty views, cross-word boundaries, and fused semantics. Co-Authored-By: Claude Co-Authored-By: DeepSeek AI --- src/bit_str.rs | 1 + src/bit_str/impls_for_iter.rs | 73 +++++++ src/bit_str/impls_for_iter/tests_for_iter.rs | 190 +++++++++++++++++++ 3 files changed, 264 insertions(+) create mode 100644 src/bit_str/impls_for_iter.rs create mode 100644 src/bit_str/impls_for_iter/tests_for_iter.rs diff --git a/src/bit_str.rs b/src/bit_str.rs index 8c3cc8b..e139931 100644 --- a/src/bit_str.rs +++ b/src/bit_str.rs @@ -25,6 +25,7 @@ pub struct BitStr<'bs> { pub mod errors; mod impls_for_access; mod impls_for_bit_arith; +mod impls_for_iter; mod impls_for_predicates; mod impls_for_slice; diff --git a/src/bit_str/impls_for_iter.rs b/src/bit_str/impls_for_iter.rs new file mode 100644 index 0000000..2b20ef2 --- /dev/null +++ b/src/bit_str/impls_for_iter.rs @@ -0,0 +1,73 @@ +use super::*; + +impl<'bs> BitStr<'bs> { + /// Returns a double-ended iterator over the bits of this view. + #[inline] + pub fn iter(&self) -> Iter<'bs> { + Iter { + bitstr: *self, + front: 0, + back: self.bit_len, + } + } + + /// Collects the bits into a `Vec`. + #[inline] + pub fn to_bool_vec(&self) -> alloc::vec::Vec { + self.iter().collect() + } +} + +/// A double-ended, exact-size iterator over the bits of a [`BitStr`]. +pub struct Iter<'bs> { + bitstr: BitStr<'bs>, + front: usize, + back: usize, +} + +impl Iterator for Iter<'_> { + type Item = bool; + + fn next(&mut self) -> Option { + if self.front == self.back { + return None; + } + // SAFETY: `front < back <= bit_len`, so `get(front)` is Some. + let value = self.bitstr.get(self.front).unwrap(); + self.front += 1; + Some(value) + } + + #[inline] + fn size_hint(&self) -> (usize, Option) { + let len = self.back - self.front; + (len, Some(len)) + } +} + +impl DoubleEndedIterator for Iter<'_> { + fn next_back(&mut self) -> Option { + if self.front == self.back { + return None; + } + self.back -= 1; + // SAFETY: `back >= front` after decrement, so `get(back)` is Some. + Some(self.bitstr.get(self.back).unwrap()) + } +} + +impl ExactSizeIterator for Iter<'_> {} +impl core::iter::FusedIterator for Iter<'_> {} + +impl<'bs> IntoIterator for &'bs BitStr<'bs> { + type Item = bool; + type IntoIter = Iter<'bs>; + + #[inline] + fn into_iter(self) -> Self::IntoIter { + self.iter() + } +} + +#[cfg(test)] +mod tests_for_iter; diff --git a/src/bit_str/impls_for_iter/tests_for_iter.rs b/src/bit_str/impls_for_iter/tests_for_iter.rs new file mode 100644 index 0000000..2d7d07d --- /dev/null +++ b/src/bit_str/impls_for_iter/tests_for_iter.rs @@ -0,0 +1,190 @@ +use alloc::vec::Vec; +use int_interval::UsizeCO; + +use crate::BitString; + +#[test] +fn iterates_bits_in_order() { + let bits = BitString::try_from("101001").unwrap(); + let v = bits.as_bitstr(); + + let values: Vec<_> = v.iter().collect(); + + assert_eq!(values, [true, false, true, false, false, true]); +} + +#[test] +fn iterates_empty_view() { + let bits = BitString::try_from("10110").unwrap(); + // Out-of-bounds slice clamps to empty. + let v = bits.as_bitstr().slice(UsizeCO::try_new(10, 20).unwrap()); + let mut iter = v.iter(); + + assert_eq!(iter.next(), None); + assert_eq!(iter.next_back(), None); + assert_eq!(iter.size_hint(), (0, Some(0))); + assert_eq!(iter.len(), 0); +} + +#[test] +fn supports_double_ended_iteration() { + let bits = BitString::try_from("101001").unwrap(); + let v = bits.as_bitstr(); + let mut iter = v.iter(); + + assert_eq!(iter.next(), Some(true)); + assert_eq!(iter.next_back(), Some(true)); + assert_eq!(iter.next(), Some(false)); + assert_eq!(iter.next_back(), Some(false)); + assert_eq!(iter.next(), Some(true)); + assert_eq!(iter.next_back(), Some(false)); + assert_eq!(iter.next(), None); + assert_eq!(iter.next_back(), None); +} + +#[test] +fn size_hint_tracks_remaining_bits() { + let bits = BitString::try_from("101001").unwrap(); + let v = bits.as_bitstr(); + let mut iter = v.iter(); + + assert_eq!(iter.size_hint(), (6, Some(6))); + assert_eq!(iter.len(), 6); + + assert_eq!(iter.next(), Some(true)); + assert_eq!(iter.size_hint(), (5, Some(5))); + assert_eq!(iter.len(), 5); + + assert_eq!(iter.next_back(), Some(true)); + assert_eq!(iter.size_hint(), (4, Some(4))); + assert_eq!(iter.len(), 4); +} + +#[test] +fn remains_fused_after_exhaustion() { + let bits = BitString::try_from("10").unwrap(); + let v = bits.as_bitstr(); + let mut iter = v.iter(); + + assert_eq!(iter.next(), Some(true)); + assert_eq!(iter.next(), Some(false)); + + assert_eq!(iter.next(), None); + assert_eq!(iter.next(), None); + assert_eq!(iter.next_back(), None); +} + +#[test] +fn borrowed_into_iter_uses_bit_iterator() { + let bits = BitString::try_from("1001").unwrap(); + let v = bits.as_bitstr(); + + let values: Vec<_> = (&v).into_iter().collect(); + + assert_eq!(values, [true, false, false, true]); +} + +#[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 v = bits.as_bitstr(); + let values: Vec<_> = v.iter().enumerate().map(|(i, b)| (i, b)).collect(); + + assert_eq!(values.len(), 130); + assert!(values[0].1); + assert!(values[63].1); + assert!(values[64].1); + assert!(values[65].1); + assert!(values[129].1); + + assert!(!values[1].1); + assert!(!values[62].1); + assert!(!values[66].1); + assert!(!values[128].1); +} + +#[test] +fn iterates_offset_view() { + let bits = BitString::try_from("101100").unwrap(); + // bits: 1 0 1 1 0 0 + // view bits 1..5 → 0 1 1 0 + let v = bits.as_bitstr().slice(UsizeCO::try_new(1, 5).unwrap()); + + let values: Vec<_> = v.iter().collect(); + assert_eq!(values, [false, true, true, false]); +} + +#[test] +fn double_ended_on_offset_view() { + let bits = BitString::try_from("101100").unwrap(); + // view bits 2..6 → 1 1 0 0 + let v = bits.as_bitstr().slice(UsizeCO::try_new(2, 6).unwrap()); + let mut iter = v.iter(); + + assert_eq!(iter.next(), Some(true)); + assert_eq!(iter.next_back(), Some(false)); + assert_eq!(iter.next(), Some(true)); + assert_eq!(iter.next_back(), Some(false)); + assert_eq!(iter.next(), None); +} + +#[test] +fn into_iter_on_offset_view() { + let bits = BitString::try_from("101100").unwrap(); + let v = bits.as_bitstr().slice(UsizeCO::try_new(1, 4).unwrap()); + + let values: Vec<_> = (&v).into_iter().collect(); + assert_eq!(values, [false, true, true]); +} + +#[test] +fn to_bool_vec_on_full_view() { + let bits = BitString::try_from("11010").unwrap(); + let v = bits.as_bitstr(); + + assert_eq!(v.to_bool_vec(), [true, true, false, true, false]); +} + +#[test] +fn to_bool_vec_on_offset_view() { + let bits = BitString::try_from("11010").unwrap(); + // view bits 1..4 → 1 0 1 + let v = bits.as_bitstr().slice(UsizeCO::try_new(1, 4).unwrap()); + + assert_eq!(v.to_bool_vec(), [true, false, true]); +} + +#[test] +fn for_loop_over_view() { + let bits = BitString::try_from("101").unwrap(); + let v = bits.as_bitstr(); + + let mut acc = Vec::new(); + for b in &v { + acc.push(b); + } + assert_eq!(acc, [true, false, true]); +} + +#[test] +fn exact_size_is_correct() { + let bits = BitString::try_from("1010011100").unwrap(); + let v = bits.as_bitstr(); + + let mut iter = v.iter(); + assert_eq!(iter.len(), 10); + + for _ in 0..10 { + let prev = iter.len(); + assert!(iter.next().is_some()); + assert_eq!(iter.len(), prev - 1); + } + assert_eq!(iter.len(), 0); +} From 7fa61a2e4ec10c19213a5a36d097d195965b6414 Mon Sep 17 00:00:00 2001 From: juncheng Date: Tue, 23 Jun 2026 12:52:25 +0000 Subject: [PATCH 06/22] =?UTF-8?q?feat:=20BitStr=20matching=20operations=20?= =?UTF-8?q?=E2=80=94=20matches=5Fat,=20starts=5Fwith,=20ends=5Fwith,=20con?= =?UTF-8?q?tains,=20find,=20rfind,=20strip?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implement the full matching suite for BitStr by delegating to BitString::bits_equal_at with the view start offset added. Needle type is &BitString, matching the BitString API convention. - matches_at / starts_with / ends_with: O(word_len) via SIMD word equality - contains / find / rfind: position-scanning with bits_equal_at verification - strip_prefix / strip_suffix: return Option sub-view 38 tests covering basic matching, empty needle, too-long needle, offset views, cross-word boundaries, and chained views. Co-Authored-By: Claude Co-Authored-By: DeepSeek AI --- src/bit_str.rs | 1 + src/bit_str/impls_for_matching.rs | 50 ++++++ .../impls_for_matching/impls_for_find.rs | 62 +++++++ .../impls_for_find/tests_for_find.rs | 136 ++++++++++++++++ .../impls_for_matching/impls_for_strip.rs | 22 +++ .../impls_for_strip/tests_for_strip.rs | 127 +++++++++++++++ .../tests_for_matches_at.rs | 151 ++++++++++++++++++ 7 files changed, 549 insertions(+) create mode 100644 src/bit_str/impls_for_matching.rs create mode 100644 src/bit_str/impls_for_matching/impls_for_find.rs create mode 100644 src/bit_str/impls_for_matching/impls_for_find/tests_for_find.rs create mode 100644 src/bit_str/impls_for_matching/impls_for_strip.rs create mode 100644 src/bit_str/impls_for_matching/impls_for_strip/tests_for_strip.rs create mode 100644 src/bit_str/impls_for_matching/tests_for_matches_at.rs diff --git a/src/bit_str.rs b/src/bit_str.rs index e139931..b884098 100644 --- a/src/bit_str.rs +++ b/src/bit_str.rs @@ -26,6 +26,7 @@ pub mod errors; mod impls_for_access; mod impls_for_bit_arith; mod impls_for_iter; +mod impls_for_matching; mod impls_for_predicates; mod impls_for_slice; diff --git a/src/bit_str/impls_for_matching.rs b/src/bit_str/impls_for_matching.rs new file mode 100644 index 0000000..d96ca6b --- /dev/null +++ b/src/bit_str/impls_for_matching.rs @@ -0,0 +1,50 @@ +use crate::BitString; + +use super::*; + +impl<'bs> BitStr<'bs> { + /// Compare `needle` bits against `self` starting at `offset`. + /// + /// Delegates to [`BitString::bits_equal_at`] with the view's start + /// offset added. + #[inline] + pub(crate) fn bits_equal_at(&self, offset: usize, needle: &BitString) -> bool { + self.source.bits_equal_at(self.start + offset, needle) + } + + /// Returns `true` if `pattern` matches the bits starting at `index`. + #[inline] + pub fn matches_at(&self, index: usize, pattern: &BitString) -> bool { + if index > self.bit_len { + return false; + } + if pattern.bit_len() > self.bit_len - index { + return false; + } + self.bits_equal_at(index, pattern) + } + + /// Returns `true` if `prefix` is a prefix of `self`. + #[inline] + pub fn starts_with(&self, prefix: &BitString) -> bool { + self.matches_at(0, prefix) + } + + /// Returns `true` if `suffix` is a suffix of `self`. + #[inline] + pub fn ends_with(&self, suffix: &BitString) -> bool { + if suffix.bit_len() == 0 { + return true; + } + if suffix.bit_len() > self.bit_len { + return false; + } + self.bits_equal_at(self.bit_len - suffix.bit_len(), suffix) + } +} + +mod impls_for_find; +mod impls_for_strip; + +#[cfg(test)] +mod tests_for_matches_at; diff --git a/src/bit_str/impls_for_matching/impls_for_find.rs b/src/bit_str/impls_for_matching/impls_for_find.rs new file mode 100644 index 0000000..9a1071e --- /dev/null +++ b/src/bit_str/impls_for_matching/impls_for_find.rs @@ -0,0 +1,62 @@ +use crate::BitString; + +use super::*; + +impl<'bs> BitStr<'bs> { + /// Returns `true` if `needle` is contained within `self`. + #[inline] + pub fn contains(&self, needle: &BitString) -> bool { + if needle.bit_len() == 0 { + return true; + } + if needle.bit_len() > self.bit_len { + return false; + } + let max_pos = self.bit_len - needle.bit_len(); + for pos in 0..=max_pos { + if self.bits_equal_at(pos, needle) { + return true; + } + } + false + } + + /// Returns the index of the first occurrence of `needle`, or `None`. + #[inline] + pub fn find(&self, needle: &BitString) -> Option { + if needle.bit_len() == 0 { + return Some(0); + } + if needle.bit_len() > self.bit_len { + return None; + } + let max_pos = self.bit_len - needle.bit_len(); + for pos in 0..=max_pos { + if self.bits_equal_at(pos, needle) { + return Some(pos); + } + } + None + } + + /// Returns the index of the last occurrence of `needle`, or `None`. + #[inline] + pub fn rfind(&self, needle: &BitString) -> Option { + if needle.bit_len() == 0 { + return Some(self.bit_len); + } + if needle.bit_len() > self.bit_len { + return None; + } + let max_pos = self.bit_len - needle.bit_len(); + for pos in (0..=max_pos).rev() { + if self.bits_equal_at(pos, needle) { + return Some(pos); + } + } + None + } +} + +#[cfg(test)] +mod tests_for_find; diff --git a/src/bit_str/impls_for_matching/impls_for_find/tests_for_find.rs b/src/bit_str/impls_for_matching/impls_for_find/tests_for_find.rs new file mode 100644 index 0000000..f99b507 --- /dev/null +++ b/src/bit_str/impls_for_matching/impls_for_find/tests_for_find.rs @@ -0,0 +1,136 @@ +use crate::BitString; + +// --------------------------------------------------------------------------- +// contains +// --------------------------------------------------------------------------- + +#[test] +fn contains_basic() { + let bits = BitString::try_from("101100").unwrap(); + let v = bits.as_bitstr(); + + assert!(v.contains(&BitString::try_from("01").unwrap())); + assert!(!v.contains(&BitString::try_from("111").unwrap())); +} + +#[test] +fn contains_empty_needle() { + let bits = BitString::try_from("10110").unwrap(); + let v = bits.as_bitstr(); + assert!(v.contains(&BitString::new())); +} + +#[test] +fn contains_longer_than_view_returns_false() { + let bits = BitString::try_from("10").unwrap(); + let v = bits.as_bitstr(); + assert!(!v.contains(&BitString::try_from("101").unwrap())); +} + +#[test] +fn contains_on_offset_view() { + let bits = BitString::try_from("110010").unwrap(); + // bits: 1 1 0 0 1 0 + // view bits 1..5 → 1 0 0 1 + let v = bits.as_bitstr().slice_from(1).slice_until(5); + + assert!(v.contains(&BitString::try_from("001").unwrap())); + assert!(!v.contains(&BitString::try_from("11").unwrap())); +} + +// --------------------------------------------------------------------------- +// find +// --------------------------------------------------------------------------- + +#[test] +fn find_first_occurrence() { + let bits = BitString::try_from("10110010").unwrap(); + let v = bits.as_bitstr(); + + assert_eq!(v.find(&BitString::try_from("10").unwrap()), Some(0)); + assert_eq!(v.find(&BitString::try_from("01").unwrap()), Some(1)); + assert_eq!(v.find(&BitString::try_from("111").unwrap()), None); +} + +#[test] +fn find_empty_needle_returns_zero() { + let bits = BitString::try_from("10110").unwrap(); + let v = bits.as_bitstr(); + assert_eq!(v.find(&BitString::new()), Some(0)); +} + +#[test] +fn find_longer_than_view_returns_none() { + let bits = BitString::try_from("10").unwrap(); + let v = bits.as_bitstr(); + assert_eq!(v.find(&BitString::try_from("101").unwrap()), None); +} + +#[test] +fn find_on_offset_view() { + let bits = BitString::try_from("11001001").unwrap(); + // bits: 1 1 0 0 1 0 0 1 + // slice_from(2).slice_until(7) → bits 2..8 → 0 0 1 0 0 1 (len 6) + let v = bits.as_bitstr().slice_from(2).slice_until(7); + + // First "10" is at view position 2 (original bits 4-5) + assert_eq!(v.find(&BitString::try_from("10").unwrap()), Some(2)); + // "00" at view position 3 (original bits 5-6) + assert_eq!(v.find(&BitString::try_from("00").unwrap()), Some(0)); +} + +#[test] +fn find_at_end_of_view() { + let bits = BitString::try_from("11100").unwrap(); + let v = bits.as_bitstr(); + + assert_eq!(v.find(&BitString::try_from("00").unwrap()), Some(3)); +} + +// --------------------------------------------------------------------------- +// rfind +// --------------------------------------------------------------------------- + +#[test] +fn rfind_last_occurrence() { + let bits = BitString::try_from("10110010").unwrap(); + let v = bits.as_bitstr(); + + // "10" at positions 0, 5, 6 → last is 6 + assert_eq!(v.rfind(&BitString::try_from("10").unwrap()), Some(6)); +} + +#[test] +fn rfind_empty_needle_returns_bit_len() { + let bits = BitString::try_from("10110").unwrap(); + let v = bits.as_bitstr(); + assert_eq!(v.rfind(&BitString::new()), Some(5)); +} + +#[test] +fn rfind_longer_than_view_returns_none() { + let bits = BitString::try_from("10").unwrap(); + let v = bits.as_bitstr(); + assert_eq!(v.rfind(&BitString::try_from("101").unwrap()), None); +} + +#[test] +fn rfind_on_offset_view() { + let bits = BitString::try_from("11001001").unwrap(); + // view bits 2..7 → 0 0 1 0 0 + let v = bits.as_bitstr().slice_from(2).slice_until(7); + + // Last "00" is at view position 3 + assert_eq!(v.rfind(&BitString::try_from("00").unwrap()), Some(3)); +} + +#[test] +fn find_and_rfind_needle_appears_once() { + let bits = BitString::try_from("11010").unwrap(); + let v = bits.as_bitstr(); + + let f = v.find(&BitString::try_from("101").unwrap()); + let r = v.rfind(&BitString::try_from("101").unwrap()); + assert_eq!(f, r); + assert_eq!(f, Some(1)); +} diff --git a/src/bit_str/impls_for_matching/impls_for_strip.rs b/src/bit_str/impls_for_matching/impls_for_strip.rs new file mode 100644 index 0000000..40d260e --- /dev/null +++ b/src/bit_str/impls_for_matching/impls_for_strip.rs @@ -0,0 +1,22 @@ +use crate::BitString; + +use super::*; + +impl<'bs> BitStr<'bs> { + /// Strips `prefix` from the start, returning the remaining sub-view. + #[inline] + pub fn strip_prefix(&self, prefix: &BitString) -> Option { + self.starts_with(prefix) + .then(|| self.slice_from(prefix.bit_len())) + } + + /// Strips `suffix` from the end, returning the remaining sub-view. + #[inline] + pub fn strip_suffix(&self, suffix: &BitString) -> Option { + self.ends_with(suffix) + .then(|| self.slice_until(self.bit_len - suffix.bit_len())) + } +} + +#[cfg(test)] +mod tests_for_strip; diff --git a/src/bit_str/impls_for_matching/impls_for_strip/tests_for_strip.rs b/src/bit_str/impls_for_matching/impls_for_strip/tests_for_strip.rs new file mode 100644 index 0000000..20494fe --- /dev/null +++ b/src/bit_str/impls_for_matching/impls_for_strip/tests_for_strip.rs @@ -0,0 +1,127 @@ +use crate::BitString; + +// --------------------------------------------------------------------------- +// strip_prefix +// --------------------------------------------------------------------------- + +#[test] +fn strip_prefix_removes_matching_prefix() { + let bits = BitString::try_from("101100").unwrap(); + let v = bits.as_bitstr(); + + let rest = v + .strip_prefix(&BitString::try_from("101").unwrap()) + .unwrap(); + assert_eq!(rest.bit_len(), 3); + assert_eq!(rest.get(0), Some(true)); // original bit 3 + assert_eq!(rest.get(1), Some(false)); // original bit 4 + assert_eq!(rest.get(2), Some(false)); // original bit 5 +} + +#[test] +fn strip_prefix_non_matching_returns_none() { + let bits = BitString::try_from("101100").unwrap(); + let v = bits.as_bitstr(); + + assert!( + v.strip_prefix(&BitString::try_from("11").unwrap()) + .is_none() + ); +} + +#[test] +fn strip_prefix_empty() { + let bits = BitString::try_from("10110").unwrap(); + let v = bits.as_bitstr(); + + let rest = v.strip_prefix(&BitString::new()).unwrap(); + assert_eq!(rest.bit_len(), v.bit_len()); +} + +#[test] +fn strip_prefix_entire_view() { + let bits = BitString::try_from("101").unwrap(); + let v = bits.as_bitstr(); + + let rest = v + .strip_prefix(&BitString::try_from("101").unwrap()) + .unwrap(); + assert_eq!(rest.bit_len(), 0); +} + +#[test] +fn strip_prefix_on_offset_view() { + let bits = BitString::try_from("110101").unwrap(); + // view bits 1..6 → 1 0 1 0 1 + let v = bits.as_bitstr().slice_from(1).slice_until(5); + + let rest = v.strip_prefix(&BitString::try_from("10").unwrap()).unwrap(); + // remaining: "101" (3 bits) + assert_eq!(rest.bit_len(), 3); + assert_eq!(rest.get(0), Some(true)); + assert_eq!(rest.get(1), Some(false)); + assert_eq!(rest.get(2), Some(true)); +} + +// --------------------------------------------------------------------------- +// strip_suffix +// --------------------------------------------------------------------------- + +#[test] +fn strip_suffix_removes_matching_suffix() { + let bits = BitString::try_from("101100").unwrap(); + let v = bits.as_bitstr(); + + let rest = v + .strip_suffix(&BitString::try_from("100").unwrap()) + .unwrap(); + assert_eq!(rest.bit_len(), 3); + assert_eq!(rest.get(0), Some(true)); // bit 0 + assert_eq!(rest.get(1), Some(false)); // bit 1 + assert_eq!(rest.get(2), Some(true)); // bit 2 +} + +#[test] +fn strip_suffix_non_matching_returns_none() { + let bits = BitString::try_from("101100").unwrap(); + let v = bits.as_bitstr(); + + assert!( + v.strip_suffix(&BitString::try_from("10").unwrap()) + .is_none() + ); +} + +#[test] +fn strip_suffix_empty() { + let bits = BitString::try_from("10110").unwrap(); + let v = bits.as_bitstr(); + + let rest = v.strip_suffix(&BitString::new()).unwrap(); + assert_eq!(rest.bit_len(), v.bit_len()); +} + +#[test] +fn strip_suffix_entire_view() { + let bits = BitString::try_from("101").unwrap(); + let v = bits.as_bitstr(); + + let rest = v + .strip_suffix(&BitString::try_from("101").unwrap()) + .unwrap(); + assert_eq!(rest.bit_len(), 0); +} + +#[test] +fn strip_suffix_on_offset_view() { + let bits = BitString::try_from("110101").unwrap(); + // view bits 1..6 → 1 0 1 0 1 + let v = bits.as_bitstr().slice_from(1).slice_until(5); + + let rest = v.strip_suffix(&BitString::try_from("01").unwrap()).unwrap(); + // remaining: "101" (3 bits) + assert_eq!(rest.bit_len(), 3); + assert_eq!(rest.get(0), Some(true)); + assert_eq!(rest.get(1), Some(false)); + assert_eq!(rest.get(2), Some(true)); +} diff --git a/src/bit_str/impls_for_matching/tests_for_matches_at.rs b/src/bit_str/impls_for_matching/tests_for_matches_at.rs new file mode 100644 index 0000000..3995280 --- /dev/null +++ b/src/bit_str/impls_for_matching/tests_for_matches_at.rs @@ -0,0 +1,151 @@ +use crate::BitString; + +// --------------------------------------------------------------------------- +// matches_at +// --------------------------------------------------------------------------- + +#[test] +fn matches_at_exact_match() { + let bits = BitString::try_from("101100").unwrap(); + let v = bits.as_bitstr(); + let p = BitString::try_from("10").unwrap(); + + assert!(v.matches_at(0, &p)); + assert!(!v.matches_at(1, &p)); +} + +#[test] +fn matches_at_beyond_view_returns_false() { + let bits = BitString::try_from("10110").unwrap(); + let v = bits.as_bitstr().slice_from(2); // "110" + let p = BitString::try_from("10").unwrap(); + + assert!(!v.matches_at(2, &p)); // index out of bounds +} + +#[test] +fn matches_at_pattern_too_long_returns_false() { + let bits = BitString::try_from("10").unwrap(); + let v = bits.as_bitstr(); + let p = BitString::try_from("100").unwrap(); + + assert!(!v.matches_at(0, &p)); +} + +#[test] +fn matches_at_empty_pattern_always_true() { + let bits = BitString::try_from("10110").unwrap(); + let v = bits.as_bitstr(); + let p = BitString::new(); + + assert!(v.matches_at(0, &p)); + assert!(v.matches_at(3, &p)); +} + +#[test] +fn matches_at_on_offset_view() { + let bits = BitString::try_from("11100011").unwrap(); + // bits: 1 1 1 0 0 0 1 1 + // view bits 2..7 → 1 0 0 0 1 + let v = bits.as_bitstr().slice_from(2).slice_until(7); + let p = BitString::try_from("0001").unwrap(); + + assert!(v.matches_at(1, &p)); +} + +// --------------------------------------------------------------------------- +// starts_with +// --------------------------------------------------------------------------- + +#[test] +fn starts_with_basic() { + let bits = BitString::try_from("101100").unwrap(); + let v = bits.as_bitstr(); + + assert!(v.starts_with(&BitString::try_from("101").unwrap())); + assert!(!v.starts_with(&BitString::try_from("11").unwrap())); + assert!(v.starts_with(&BitString::try_from("101100").unwrap())); +} + +#[test] +fn starts_with_empty_prefix() { + let bits = BitString::try_from("10110").unwrap(); + let v = bits.as_bitstr(); + + assert!(v.starts_with(&BitString::new())); +} + +#[test] +fn starts_with_longer_prefix_returns_false() { + let bits = BitString::try_from("101").unwrap(); + let v = bits.as_bitstr(); + + assert!(!v.starts_with(&BitString::try_from("1011").unwrap())); +} + +#[test] +fn starts_with_on_offset_view() { + let bits = BitString::try_from("110101").unwrap(); + // bits: 1 1 0 1 0 1 + // view bits 1..5 → 1 0 1 0 + let v = bits.as_bitstr().slice_from(1).slice_until(5); + + assert!(v.starts_with(&BitString::try_from("10").unwrap())); + assert!(!v.starts_with(&BitString::try_from("11").unwrap())); +} + +// --------------------------------------------------------------------------- +// ends_with +// --------------------------------------------------------------------------- + +#[test] +fn ends_with_basic() { + let bits = BitString::try_from("101100").unwrap(); + let v = bits.as_bitstr(); + + assert!(v.ends_with(&BitString::try_from("100").unwrap())); + assert!(!v.ends_with(&BitString::try_from("10").unwrap())); + assert!(v.ends_with(&BitString::try_from("101100").unwrap())); +} + +#[test] +fn ends_with_empty_suffix() { + let bits = BitString::try_from("10110").unwrap(); + let v = bits.as_bitstr(); + + assert!(v.ends_with(&BitString::new())); +} + +#[test] +fn ends_with_longer_suffix_returns_false() { + let bits = BitString::try_from("101").unwrap(); + let v = bits.as_bitstr(); + + assert!(!v.ends_with(&BitString::try_from("1101").unwrap())); +} + +#[test] +fn ends_with_on_offset_view() { + let bits = BitString::try_from("110101").unwrap(); + // bits: 1 1 0 1 0 1 + // slice_from(1).slice_until(5) → bits 1..6 → 1 0 1 0 1 + let v = bits.as_bitstr().slice_from(1).slice_until(5); + + assert!(v.ends_with(&BitString::try_from("01").unwrap())); + assert!(!v.ends_with(&BitString::try_from("10").unwrap())); +} + +#[test] +fn matches_and_ends_with_across_word_boundaries() { + let mut bits = BitString::zeros(130); + bits.set(62, true); + bits.set(63, true); + bits.set(64, true); + bits.set(65, true); + + let v = bits.as_bitstr(); + let p = BitString::try_from("1111").unwrap(); + + // "1111" at bits 62-65, crosses word boundary 63/64 + assert!(v.matches_at(62, &p)); +} From 6d3cc5e95518b01afbc9503f8e6a786712e2a872 Mon Sep 17 00:00:00 2001 From: juncheng Date: Tue, 23 Jun 2026 12:57:04 +0000 Subject: [PATCH 07/22] refactor: extract BitStr matches_at into impls_for_matches_at module Move the matches_at/starts_with/ends_with impl block and its tests from impls_for_matching.rs into a dedicated impls_for_matches_at.rs, matching the BitString module structure. Co-Authored-By: Claude Co-Authored-By: DeepSeek AI --- src/bit_str/impls_for_matching.rs | 49 +------------------ .../impls_for_matching/impls_for_find.rs | 2 +- .../impls_for_matches_at.rs | 47 ++++++++++++++++++ .../tests_for_matches_at.rs | 0 .../impls_for_matching/impls_for_strip.rs | 2 +- 5 files changed, 50 insertions(+), 50 deletions(-) create mode 100644 src/bit_str/impls_for_matching/impls_for_matches_at.rs rename src/bit_str/impls_for_matching/{ => impls_for_matches_at}/tests_for_matches_at.rs (100%) diff --git a/src/bit_str/impls_for_matching.rs b/src/bit_str/impls_for_matching.rs index d96ca6b..569ef14 100644 --- a/src/bit_str/impls_for_matching.rs +++ b/src/bit_str/impls_for_matching.rs @@ -1,50 +1,3 @@ -use crate::BitString; - -use super::*; - -impl<'bs> BitStr<'bs> { - /// Compare `needle` bits against `self` starting at `offset`. - /// - /// Delegates to [`BitString::bits_equal_at`] with the view's start - /// offset added. - #[inline] - pub(crate) fn bits_equal_at(&self, offset: usize, needle: &BitString) -> bool { - self.source.bits_equal_at(self.start + offset, needle) - } - - /// Returns `true` if `pattern` matches the bits starting at `index`. - #[inline] - pub fn matches_at(&self, index: usize, pattern: &BitString) -> bool { - if index > self.bit_len { - return false; - } - if pattern.bit_len() > self.bit_len - index { - return false; - } - self.bits_equal_at(index, pattern) - } - - /// Returns `true` if `prefix` is a prefix of `self`. - #[inline] - pub fn starts_with(&self, prefix: &BitString) -> bool { - self.matches_at(0, prefix) - } - - /// Returns `true` if `suffix` is a suffix of `self`. - #[inline] - pub fn ends_with(&self, suffix: &BitString) -> bool { - if suffix.bit_len() == 0 { - return true; - } - if suffix.bit_len() > self.bit_len { - return false; - } - self.bits_equal_at(self.bit_len - suffix.bit_len(), suffix) - } -} - mod impls_for_find; +mod impls_for_matches_at; mod impls_for_strip; - -#[cfg(test)] -mod tests_for_matches_at; diff --git a/src/bit_str/impls_for_matching/impls_for_find.rs b/src/bit_str/impls_for_matching/impls_for_find.rs index 9a1071e..320c387 100644 --- a/src/bit_str/impls_for_matching/impls_for_find.rs +++ b/src/bit_str/impls_for_matching/impls_for_find.rs @@ -1,6 +1,6 @@ use crate::BitString; -use super::*; +use crate::BitStr; impl<'bs> BitStr<'bs> { /// Returns `true` if `needle` is contained within `self`. diff --git a/src/bit_str/impls_for_matching/impls_for_matches_at.rs b/src/bit_str/impls_for_matching/impls_for_matches_at.rs new file mode 100644 index 0000000..e9f64c3 --- /dev/null +++ b/src/bit_str/impls_for_matching/impls_for_matches_at.rs @@ -0,0 +1,47 @@ +use crate::BitString; + +use crate::BitStr; + +impl<'bs> BitStr<'bs> { + /// Compare `needle` bits against `self` starting at `offset`. + /// + /// Delegates to [`BitString::bits_equal_at`] with the view's start + /// offset added. + #[inline] + pub(crate) fn bits_equal_at(&self, offset: usize, needle: &BitString) -> bool { + self.source.bits_equal_at(self.start + offset, needle) + } + + /// Returns `true` if `pattern` matches the bits starting at `index`. + #[inline] + pub fn matches_at(&self, index: usize, pattern: &BitString) -> bool { + if index > self.bit_len { + return false; + } + if pattern.bit_len() > self.bit_len - index { + return false; + } + self.bits_equal_at(index, pattern) + } + + /// Returns `true` if `prefix` is a prefix of `self`. + #[inline] + pub fn starts_with(&self, prefix: &BitString) -> bool { + self.matches_at(0, prefix) + } + + /// Returns `true` if `suffix` is a suffix of `self`. + #[inline] + pub fn ends_with(&self, suffix: &BitString) -> bool { + if suffix.bit_len() == 0 { + return true; + } + if suffix.bit_len() > self.bit_len { + return false; + } + self.bits_equal_at(self.bit_len - suffix.bit_len(), suffix) + } +} + +#[cfg(test)] +mod tests_for_matches_at; diff --git a/src/bit_str/impls_for_matching/tests_for_matches_at.rs b/src/bit_str/impls_for_matching/impls_for_matches_at/tests_for_matches_at.rs similarity index 100% rename from src/bit_str/impls_for_matching/tests_for_matches_at.rs rename to src/bit_str/impls_for_matching/impls_for_matches_at/tests_for_matches_at.rs diff --git a/src/bit_str/impls_for_matching/impls_for_strip.rs b/src/bit_str/impls_for_matching/impls_for_strip.rs index 40d260e..08e2d35 100644 --- a/src/bit_str/impls_for_matching/impls_for_strip.rs +++ b/src/bit_str/impls_for_matching/impls_for_strip.rs @@ -1,6 +1,6 @@ use crate::BitString; -use super::*; +use crate::BitStr; impl<'bs> BitStr<'bs> { /// Strips `prefix` from the start, returning the remaining sub-view. From 33c0ab5df0ac03d2188124491b78e09292946870 Mon Sep 17 00:00:00 2001 From: juncheng Date: Tue, 23 Jun 2026 13:03:09 +0000 Subject: [PATCH 08/22] refactor: extract BitsFind trait, SIMD-accelerate BitStr find MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Move three SIMD find-core functions from bit_string impls_for_find to traits/bits_find, and define a BitsFind trait on [u64] with find_any_candidate, find_first_word, and find_last_word. BitStr::find/rfind now use the SIMD candidate pre-filter: slice the source words from the view's start word, pass start_offset+bit_len as the effective length, then shift candidate positions back. This matches the BitString optimization path. Both BitString and BitStr now share the same SIMD-accelerated search via the trait — no code duplication. Co-Authored-By: Claude Co-Authored-By: DeepSeek AI --- .../impls_for_matching/impls_for_find.rs | 78 ++++++++++++++----- .../impls_for_matching/impls_for_find.rs | 68 +++++++--------- src/traits.rs | 2 + src/traits/bits_find.rs | 56 +++++++++++++ .../bits_find}/funcs_for_contains_core.rs | 0 .../tests_for_backend_equivalence.rs | 0 .../bits_find}/funcs_for_find_core.rs | 0 .../tests_for_backend_equivalence.rs | 0 .../bits_find}/funcs_for_rfind_core.rs | 0 .../tests_for_backend_equivalence.rs | 0 src/traits/bits_find/impls_for_u64_slice.rs | 66 ++++++++++++++++ 11 files changed, 209 insertions(+), 61 deletions(-) create mode 100644 src/traits/bits_find.rs rename src/{bit_string/impls_for_matching/impls_for_find => traits/bits_find}/funcs_for_contains_core.rs (100%) rename src/{bit_string/impls_for_matching/impls_for_find => traits/bits_find}/funcs_for_contains_core/tests_for_backend_equivalence.rs (100%) rename src/{bit_string/impls_for_matching/impls_for_find => traits/bits_find}/funcs_for_find_core.rs (100%) rename src/{bit_string/impls_for_matching/impls_for_find => traits/bits_find}/funcs_for_find_core/tests_for_backend_equivalence.rs (100%) rename src/{bit_string/impls_for_matching/impls_for_find => traits/bits_find}/funcs_for_rfind_core.rs (100%) rename src/{bit_string/impls_for_matching/impls_for_find => traits/bits_find}/funcs_for_rfind_core/tests_for_backend_equivalence.rs (100%) create mode 100644 src/traits/bits_find/impls_for_u64_slice.rs diff --git a/src/bit_str/impls_for_matching/impls_for_find.rs b/src/bit_str/impls_for_matching/impls_for_find.rs index 320c387..d483cfc 100644 --- a/src/bit_str/impls_for_matching/impls_for_find.rs +++ b/src/bit_str/impls_for_matching/impls_for_find.rs @@ -1,4 +1,6 @@ use crate::BitString; +use crate::SMALL_WORDS; +use crate::traits::*; use crate::BitStr; @@ -12,13 +14,7 @@ impl<'bs> BitStr<'bs> { if needle.bit_len() > self.bit_len { return false; } - let max_pos = self.bit_len - needle.bit_len(); - for pos in 0..=max_pos { - if self.bits_equal_at(pos, needle) { - return true; - } - } - false + self.find(needle).is_some() } /// Returns the index of the first occurrence of `needle`, or `None`. @@ -30,13 +26,35 @@ impl<'bs> BitStr<'bs> { if needle.bit_len() > self.bit_len { return None; } - let max_pos = self.bit_len - needle.bit_len(); - for pos in 0..=max_pos { - if self.bits_equal_at(pos, needle) { - return Some(pos); - } + + let start_word = self.start / crate::WORD_BITS; + let start_offset = self.start % crate::WORD_BITS; + let view_bits = start_offset + self.bit_len; + let words = &self.source.words()[start_word..]; + let needle_words = needle.words(); + let needle_len = needle.bit_len(); + + // Quick rejection: no candidate at all. + if words.len() >= SMALL_WORDS + && !words + .find_any_candidate(view_bits, needle_words, needle_len, &mut |pos| { + pos >= start_offset + && pos + needle_len <= view_bits + && self.bits_equal_at(pos - start_offset, needle) + }) + .is_some() + { + return None; } - None + + // Fine-grained word-outer search for the earliest match. + words + .find_first_word(view_bits, needle_words, needle_len, &mut |pos| { + pos >= start_offset + && pos + needle_len <= view_bits + && self.bits_equal_at(pos - start_offset, needle) + }) + .map(|pos| pos - start_offset) } /// Returns the index of the last occurrence of `needle`, or `None`. @@ -48,13 +66,35 @@ impl<'bs> BitStr<'bs> { if needle.bit_len() > self.bit_len { return None; } - let max_pos = self.bit_len - needle.bit_len(); - for pos in (0..=max_pos).rev() { - if self.bits_equal_at(pos, needle) { - return Some(pos); - } + + let start_word = self.start / crate::WORD_BITS; + let start_offset = self.start % crate::WORD_BITS; + let view_bits = start_offset + self.bit_len; + let words = &self.source.words()[start_word..]; + let needle_words = needle.words(); + let needle_len = needle.bit_len(); + + // Quick rejection. + if words.len() >= SMALL_WORDS + && !words + .find_any_candidate(view_bits, needle_words, needle_len, &mut |pos| { + pos >= start_offset + && pos + needle_len <= view_bits + && self.bits_equal_at(pos - start_offset, needle) + }) + .is_some() + { + return None; } - None + + // Reverse search for the rightmost match. + words + .find_last_word(view_bits, needle_words, needle_len, &mut |pos| { + pos >= start_offset + && pos + needle_len <= view_bits + && self.bits_equal_at(pos - start_offset, needle) + }) + .map(|pos| pos - start_offset) } } 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 e3e6fd5..a506efd 100644 --- a/src/bit_string/impls_for_matching/impls_for_find.rs +++ b/src/bit_string/impls_for_matching/impls_for_find.rs @@ -1,11 +1,8 @@ use crate::SMALL_WORDS; +use crate::traits::*; use super::*; -mod funcs_for_contains_core; -mod funcs_for_find_core; -mod funcs_for_rfind_core; - impl BitString { #[inline] pub fn contains(&self, needle: &Self) -> bool { @@ -16,14 +13,11 @@ impl BitString { return false; } - funcs_for_contains_core::find_any_candidate( - &self.words, - self.bit_len, - needle.words(), - needle.bit_len, - &mut |pos| self.bits_equal_at(pos, needle), - ) - .is_some() + self.words + .find_any_candidate(self.bit_len, needle.words(), needle.bit_len, &mut |pos| { + self.bits_equal_at(pos, needle) + }) + .is_some() } pub fn find(&self, needle: &Self) -> Option { @@ -34,25 +28,20 @@ impl BitString { return None; } if self.words.len() >= SMALL_WORDS - && !funcs_for_contains_core::find_any_candidate( - &self.words, - self.bit_len, - needle.words(), - needle.bit_len, - &mut |pos| self.bits_equal_at(pos, needle), - ) - .is_some() + && !self + .words + .find_any_candidate(self.bit_len, needle.words(), needle.bit_len, &mut |pos| { + self.bits_equal_at(pos, needle) + }) + .is_some() { return None; } - funcs_for_find_core::find_first_word( - &self.words, - self.bit_len, - needle.words(), - needle.bit_len, - &mut |pos| self.bits_equal_at(pos, needle), - ) + self.words + .find_first_word(self.bit_len, needle.words(), needle.bit_len, &mut |pos| { + self.bits_equal_at(pos, needle) + }) } pub fn rfind(&self, needle: &Self) -> Option { @@ -63,25 +52,20 @@ impl BitString { return None; } if self.words.len() >= SMALL_WORDS - && !funcs_for_contains_core::find_any_candidate( - &self.words, - self.bit_len, - needle.words(), - needle.bit_len, - &mut |pos| self.bits_equal_at(pos, needle), - ) - .is_some() + && !self + .words + .find_any_candidate(self.bit_len, needle.words(), needle.bit_len, &mut |pos| { + self.bits_equal_at(pos, needle) + }) + .is_some() { return None; } - funcs_for_rfind_core::find_last_word( - &self.words, - self.bit_len, - needle.words(), - needle.bit_len, - &mut |pos| self.bits_equal_at(pos, needle), - ) + self.words + .find_last_word(self.bit_len, needle.words(), needle.bit_len, &mut |pos| { + self.bits_equal_at(pos, needle) + }) } } diff --git a/src/traits.rs b/src/traits.rs index 497117a..f3c8388 100644 --- a/src/traits.rs +++ b/src/traits.rs @@ -1,7 +1,9 @@ pub(crate) mod bits_arith; pub(crate) mod bits_edit; pub(crate) mod bits_eq; +pub(crate) mod bits_find; pub(crate) use bits_arith::*; pub(crate) use bits_edit::*; pub(crate) use bits_eq::*; +pub(crate) use bits_find::*; diff --git a/src/traits/bits_find.rs b/src/traits/bits_find.rs new file mode 100644 index 0000000..2a90ba3 --- /dev/null +++ b/src/traits/bits_find.rs @@ -0,0 +1,56 @@ +/// SIMD-accelerated pattern-search operations on `[u64]` backing storage. +/// +/// All positions returned are relative to the start of `self`. Callers +/// with an offset view (e.g. [`BitStr`](crate::BitStr)) must shift +/// positions accordingly. +pub(crate) trait BitsFind { + /// Returns `Some(pos)` if any 64-bit window in the haystack matches + /// the first word of `needle_words` AND `verify(pos)` succeeds. + /// + /// Uses **shift-outer, word-inner** ordering — does **not** + /// guarantee the returned position is the earliest match. + fn find_any_candidate( + &self, + haystack_bit_len: usize, + needle_words: &[u64], + needle_bit_len: usize, + verify: &mut F, + ) -> Option + where + F: FnMut(usize) -> bool; + + /// Returns `Some(pos)` for the **earliest** position where the + /// first word of `needle_words` matches AND `verify(pos)` succeeds. + /// + /// Uses **word-outer, shift-inner** ordering so positions are + /// visited in increasing order. + fn find_first_word( + &self, + haystack_bit_len: usize, + needle_words: &[u64], + needle_bit_len: usize, + verify: &mut F, + ) -> Option + where + F: FnMut(usize) -> bool; + + /// Returns `Some(pos)` for the **rightmost** position where the + /// first word of `needle_words` matches AND `verify(pos)` succeeds. + /// + /// Uses **word-outer reverse, shift-inner reverse** ordering so + /// positions are visited in decreasing order. + fn find_last_word( + &self, + haystack_bit_len: usize, + needle_words: &[u64], + needle_bit_len: usize, + verify: &mut F, + ) -> Option + where + F: FnMut(usize) -> bool; +} + +pub(crate) mod funcs_for_contains_core; +pub(crate) mod funcs_for_find_core; +pub(crate) mod funcs_for_rfind_core; +pub(crate) mod impls_for_u64_slice; diff --git a/src/bit_string/impls_for_matching/impls_for_find/funcs_for_contains_core.rs b/src/traits/bits_find/funcs_for_contains_core.rs similarity index 100% rename from src/bit_string/impls_for_matching/impls_for_find/funcs_for_contains_core.rs rename to src/traits/bits_find/funcs_for_contains_core.rs diff --git a/src/bit_string/impls_for_matching/impls_for_find/funcs_for_contains_core/tests_for_backend_equivalence.rs b/src/traits/bits_find/funcs_for_contains_core/tests_for_backend_equivalence.rs similarity index 100% rename from src/bit_string/impls_for_matching/impls_for_find/funcs_for_contains_core/tests_for_backend_equivalence.rs rename to src/traits/bits_find/funcs_for_contains_core/tests_for_backend_equivalence.rs diff --git a/src/bit_string/impls_for_matching/impls_for_find/funcs_for_find_core.rs b/src/traits/bits_find/funcs_for_find_core.rs similarity index 100% rename from src/bit_string/impls_for_matching/impls_for_find/funcs_for_find_core.rs rename to src/traits/bits_find/funcs_for_find_core.rs diff --git a/src/bit_string/impls_for_matching/impls_for_find/funcs_for_find_core/tests_for_backend_equivalence.rs b/src/traits/bits_find/funcs_for_find_core/tests_for_backend_equivalence.rs similarity index 100% rename from src/bit_string/impls_for_matching/impls_for_find/funcs_for_find_core/tests_for_backend_equivalence.rs rename to src/traits/bits_find/funcs_for_find_core/tests_for_backend_equivalence.rs diff --git a/src/bit_string/impls_for_matching/impls_for_find/funcs_for_rfind_core.rs b/src/traits/bits_find/funcs_for_rfind_core.rs similarity index 100% rename from src/bit_string/impls_for_matching/impls_for_find/funcs_for_rfind_core.rs rename to src/traits/bits_find/funcs_for_rfind_core.rs diff --git a/src/bit_string/impls_for_matching/impls_for_find/funcs_for_rfind_core/tests_for_backend_equivalence.rs b/src/traits/bits_find/funcs_for_rfind_core/tests_for_backend_equivalence.rs similarity index 100% rename from src/bit_string/impls_for_matching/impls_for_find/funcs_for_rfind_core/tests_for_backend_equivalence.rs rename to src/traits/bits_find/funcs_for_rfind_core/tests_for_backend_equivalence.rs diff --git a/src/traits/bits_find/impls_for_u64_slice.rs b/src/traits/bits_find/impls_for_u64_slice.rs new file mode 100644 index 0000000..23616fd --- /dev/null +++ b/src/traits/bits_find/impls_for_u64_slice.rs @@ -0,0 +1,66 @@ +use super::BitsFind; +use super::funcs_for_contains_core; +use super::funcs_for_find_core; +use super::funcs_for_rfind_core; + +impl BitsFind for [u64] { + #[inline] + fn find_any_candidate( + &self, + haystack_bit_len: usize, + needle_words: &[u64], + needle_bit_len: usize, + verify: &mut F, + ) -> Option + where + F: FnMut(usize) -> bool, + { + funcs_for_contains_core::find_any_candidate( + self, + haystack_bit_len, + needle_words, + needle_bit_len, + verify, + ) + } + + #[inline] + fn find_first_word( + &self, + haystack_bit_len: usize, + needle_words: &[u64], + needle_bit_len: usize, + verify: &mut F, + ) -> Option + where + F: FnMut(usize) -> bool, + { + funcs_for_find_core::find_first_word( + self, + haystack_bit_len, + needle_words, + needle_bit_len, + verify, + ) + } + + #[inline] + fn find_last_word( + &self, + haystack_bit_len: usize, + needle_words: &[u64], + needle_bit_len: usize, + verify: &mut F, + ) -> Option + where + F: FnMut(usize) -> bool, + { + funcs_for_rfind_core::find_last_word( + self, + haystack_bit_len, + needle_words, + needle_bit_len, + verify, + ) + } +} From a53af6f6d95e2dbae52e0999613023f508299325 Mon Sep 17 00:00:00 2001 From: juncheng Date: Tue, 23 Jun 2026 13:15:49 +0000 Subject: [PATCH 09/22] =?UTF-8?q?perf:=20BitStr=20find=20=E2=80=94=20isola?= =?UTF-8?q?te=20first=20partial=20word,=20SIMD=20only=20aligned=20interior?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previously the unaligned path passed the whole words[start_word..] to SIMD with start_offset in the bit_len. The SIMD scan would test positions 0..start_offset that lie outside the view, requiring per-candidate filtering. Now the unaligned path does: 1. Scalar scan of the first partial word (at most 63 positions) 2. SIMD on aligned words[start_word+1..] with bit_len=remaining, zero filtering — every candidate is in-bounds. 3. rfind does SIMD first (reverse), then scalar fallback. Co-Authored-By: Claude Co-Authored-By: DeepSeek AI --- .../impls_for_matching/impls_for_find.rs | 160 +++++++++++++----- 1 file changed, 118 insertions(+), 42 deletions(-) diff --git a/src/bit_str/impls_for_matching/impls_for_find.rs b/src/bit_str/impls_for_matching/impls_for_find.rs index d483cfc..5d93ee6 100644 --- a/src/bit_str/impls_for_matching/impls_for_find.rs +++ b/src/bit_str/impls_for_matching/impls_for_find.rs @@ -1,5 +1,6 @@ use crate::BitString; use crate::SMALL_WORDS; +use crate::WORD_BITS; use crate::traits::*; use crate::BitStr; @@ -14,7 +15,40 @@ impl<'bs> BitStr<'bs> { if needle.bit_len() > self.bit_len { return false; } - self.find(needle).is_some() + + let words = self.source.words(); + let sw = self.start / WORD_BITS; + let so = self.start % WORD_BITS; + + // Unaligned start: check the first partial word before delegating to + // SIMD on the aligned remainder. + if so != 0 { + let first_bits = (WORD_BITS - so).min(self.bit_len); + let max = first_bits.saturating_sub(needle.bit_len()); + for p in 0..=max { + if self.bits_equal_at(p, needle) { + return true; + } + } + let remaining = self.bit_len - first_bits; + if remaining == 0 { + return false; + } + // Aligned remainder — SIMD shift-outer candidate scan. + let aligned = &words[sw + 1..]; + return aligned + .find_any_candidate(remaining, needle.words(), needle.bit_len(), &mut |pos| { + self.bits_equal_at(pos + first_bits, needle) + }) + .is_some(); + } + + // Word-aligned: full SIMD on the relevant suffix. + words[sw..] + .find_any_candidate(self.bit_len, needle.words(), needle.bit_len(), &mut |pos| { + self.bits_equal_at(pos, needle) + }) + .is_some() } /// Returns the index of the first occurrence of `needle`, or `None`. @@ -27,34 +61,54 @@ impl<'bs> BitStr<'bs> { return None; } - let start_word = self.start / crate::WORD_BITS; - let start_offset = self.start % crate::WORD_BITS; - let view_bits = start_offset + self.bit_len; - let words = &self.source.words()[start_word..]; + let words = self.source.words(); + let sw = self.start / WORD_BITS; + let so = self.start % WORD_BITS; let needle_words = needle.words(); let needle_len = needle.bit_len(); - // Quick rejection: no candidate at all. - if words.len() >= SMALL_WORDS - && !words - .find_any_candidate(view_bits, needle_words, needle_len, &mut |pos| { - pos >= start_offset - && pos + needle_len <= view_bits - && self.bits_equal_at(pos - start_offset, needle) + // Word-aligned fast path: the entire view is SIMD-friendly. + if so == 0 { + return words[sw..].find_first_word( + self.bit_len, + needle_words, + needle_len, + &mut |pos| self.bits_equal_at(pos, needle), + ); + } + + // Unaligned: scan the first partial word, then SIMD for the rest. + let first_bits = (WORD_BITS - so).min(self.bit_len); + let max = first_bits.saturating_sub(needle_len); + for p in 0..=max { + if self.bits_equal_at(p, needle) { + return Some(p); + } + } + + let remaining = self.bit_len - first_bits; + if remaining == 0 { + return None; + } + + let aligned = &words[sw + 1..]; + + // Quick rejection before the more expensive word-outer scan. + if aligned.len() >= SMALL_WORDS + && !aligned + .find_any_candidate(remaining, needle_words, needle_len, &mut |pos| { + self.bits_equal_at(pos + first_bits, needle) }) .is_some() { return None; } - // Fine-grained word-outer search for the earliest match. - words - .find_first_word(view_bits, needle_words, needle_len, &mut |pos| { - pos >= start_offset - && pos + needle_len <= view_bits - && self.bits_equal_at(pos - start_offset, needle) + aligned + .find_first_word(remaining, needle_words, needle_len, &mut |pos| { + self.bits_equal_at(pos + first_bits, needle) }) - .map(|pos| pos - start_offset) + .map(|pos| pos + first_bits) } /// Returns the index of the last occurrence of `needle`, or `None`. @@ -67,34 +121,56 @@ impl<'bs> BitStr<'bs> { return None; } - let start_word = self.start / crate::WORD_BITS; - let start_offset = self.start % crate::WORD_BITS; - let view_bits = start_offset + self.bit_len; - let words = &self.source.words()[start_word..]; + let words = self.source.words(); + let sw = self.start / WORD_BITS; + let so = self.start % WORD_BITS; let needle_words = needle.words(); let needle_len = needle.bit_len(); - // Quick rejection. - if words.len() >= SMALL_WORDS - && !words - .find_any_candidate(view_bits, needle_words, needle_len, &mut |pos| { - pos >= start_offset - && pos + needle_len <= view_bits - && self.bits_equal_at(pos - start_offset, needle) - }) - .is_some() - { - return None; + // Word-aligned fast path. + if so == 0 { + return words[sw..].find_last_word( + self.bit_len, + needle_words, + needle_len, + &mut |pos| self.bits_equal_at(pos, needle), + ); } - // Reverse search for the rightmost match. - words - .find_last_word(view_bits, needle_words, needle_len, &mut |pos| { - pos >= start_offset - && pos + needle_len <= view_bits - && self.bits_equal_at(pos - start_offset, needle) - }) - .map(|pos| pos - start_offset) + // Unaligned: SIMD on the aligned remainder first (reverse order), + // then fall back to the first partial word. + let first_bits = (WORD_BITS - so).min(self.bit_len); + let remaining = self.bit_len - first_bits; + + if remaining > 0 { + let aligned = &words[sw + 1..]; + + if aligned.len() >= SMALL_WORDS + && aligned + .find_any_candidate(remaining, needle_words, needle_len, &mut |pos| { + self.bits_equal_at(pos + first_bits, needle) + }) + .is_some() + { + if let Some(pos) = + aligned.find_last_word(remaining, needle_words, needle_len, &mut |pos| { + self.bits_equal_at(pos + first_bits, needle) + }) + { + return Some(pos + first_bits); + } + } + } + + // Check the first partial word. + let max = first_bits.saturating_sub(needle_len); + for p in (0..=max).rev() { + if self.bits_equal_at(p, needle) { + return Some(p); + } + } + + None } } From 7cf1fc0365f74f2969befd4df358bab2cb80209c Mon Sep 17 00:00:00 2001 From: juncheng Date: Tue, 23 Jun 2026 13:57:29 +0000 Subject: [PATCH 10/22] refactor: BitStr matching API accepts &BitStr instead of &BitString Change all read-only matching methods to take &BitStr needles: - matches_at / starts_with / ends_with - contains / find / rfind - strip_prefix / strip_suffix bits_equal_at now handles dual-offset comparison: - Sub-word (<= 64 bits): read_word_at on both sides + mask - Multi-word, needle aligned: SIMD eq_words on haystack - Multi-word, both misaligned: scalar word-at-a-time (rare) This eliminates allocations when using BitStr views as needles, following the &str pattern from std. BitString users pass &bs.as_bitstr() for zero-cost conversion. Co-Authored-By: Claude Co-Authored-By: DeepSeek AI --- .../impls_for_matching/impls_for_find.rs | 43 +++++---- .../impls_for_find/tests_for_find.rs | 78 ++++++++--------- .../impls_for_matches_at.rs | 87 ++++++++++++++++--- .../tests_for_matches_at.rs | 84 +++++++++--------- .../impls_for_matching/impls_for_strip.rs | 10 +-- .../impls_for_strip/tests_for_strip.rs | 68 ++++++--------- 6 files changed, 204 insertions(+), 166 deletions(-) diff --git a/src/bit_str/impls_for_matching/impls_for_find.rs b/src/bit_str/impls_for_matching/impls_for_find.rs index 5d93ee6..3033688 100644 --- a/src/bit_str/impls_for_matching/impls_for_find.rs +++ b/src/bit_str/impls_for_matching/impls_for_find.rs @@ -1,30 +1,30 @@ -use crate::BitString; -use crate::SMALL_WORDS; -use crate::WORD_BITS; use crate::traits::*; +use crate::{SMALL_WORDS, WORD_BITS}; use crate::BitStr; impl<'bs> BitStr<'bs> { /// Returns `true` if `needle` is contained within `self`. #[inline] - pub fn contains(&self, needle: &BitString) -> bool { - if needle.bit_len() == 0 { + pub fn contains(&self, needle: &BitStr<'_>) -> bool { + if needle.bit_len == 0 { return true; } - if needle.bit_len() > self.bit_len { + if needle.bit_len > self.bit_len { return false; } let words = self.source.words(); let sw = self.start / WORD_BITS; let so = self.start % WORD_BITS; + let needle_words = needle.source.words(); + let needle_len = needle.bit_len; // Unaligned start: check the first partial word before delegating to // SIMD on the aligned remainder. if so != 0 { let first_bits = (WORD_BITS - so).min(self.bit_len); - let max = first_bits.saturating_sub(needle.bit_len()); + let max = first_bits.saturating_sub(needle_len); for p in 0..=max { if self.bits_equal_at(p, needle) { return true; @@ -34,10 +34,9 @@ impl<'bs> BitStr<'bs> { if remaining == 0 { return false; } - // Aligned remainder — SIMD shift-outer candidate scan. let aligned = &words[sw + 1..]; return aligned - .find_any_candidate(remaining, needle.words(), needle.bit_len(), &mut |pos| { + .find_any_candidate(remaining, needle_words, needle_len, &mut |pos| { self.bits_equal_at(pos + first_bits, needle) }) .is_some(); @@ -45,7 +44,7 @@ impl<'bs> BitStr<'bs> { // Word-aligned: full SIMD on the relevant suffix. words[sw..] - .find_any_candidate(self.bit_len, needle.words(), needle.bit_len(), &mut |pos| { + .find_any_candidate(self.bit_len, needle_words, needle_len, &mut |pos| { self.bits_equal_at(pos, needle) }) .is_some() @@ -53,21 +52,21 @@ impl<'bs> BitStr<'bs> { /// Returns the index of the first occurrence of `needle`, or `None`. #[inline] - pub fn find(&self, needle: &BitString) -> Option { - if needle.bit_len() == 0 { + pub fn find(&self, needle: &BitStr<'_>) -> Option { + if needle.bit_len == 0 { return Some(0); } - if needle.bit_len() > self.bit_len { + if needle.bit_len > self.bit_len { return None; } let words = self.source.words(); let sw = self.start / WORD_BITS; let so = self.start % WORD_BITS; - let needle_words = needle.words(); - let needle_len = needle.bit_len(); + let needle_words = needle.source.words(); + let needle_len = needle.bit_len; - // Word-aligned fast path: the entire view is SIMD-friendly. + // Word-aligned fast path. if so == 0 { return words[sw..].find_first_word( self.bit_len, @@ -113,19 +112,19 @@ impl<'bs> BitStr<'bs> { /// Returns the index of the last occurrence of `needle`, or `None`. #[inline] - pub fn rfind(&self, needle: &BitString) -> Option { - if needle.bit_len() == 0 { + pub fn rfind(&self, needle: &BitStr<'_>) -> Option { + if needle.bit_len == 0 { return Some(self.bit_len); } - if needle.bit_len() > self.bit_len { + if needle.bit_len > self.bit_len { return None; } let words = self.source.words(); let sw = self.start / WORD_BITS; let so = self.start % WORD_BITS; - let needle_words = needle.words(); - let needle_len = needle.bit_len(); + let needle_words = needle.source.words(); + let needle_len = needle.bit_len; // Word-aligned fast path. if so == 0 { @@ -137,7 +136,7 @@ impl<'bs> BitStr<'bs> { ); } - // Unaligned: SIMD on the aligned remainder first (reverse order), + // Unaligned: SIMD on the aligned remainder first (reverse), // then fall back to the first partial word. let first_bits = (WORD_BITS - so).min(self.bit_len); let remaining = self.bit_len - first_bits; diff --git a/src/bit_str/impls_for_matching/impls_for_find/tests_for_find.rs b/src/bit_str/impls_for_matching/impls_for_find/tests_for_find.rs index f99b507..eef91e0 100644 --- a/src/bit_str/impls_for_matching/impls_for_find/tests_for_find.rs +++ b/src/bit_str/impls_for_matching/impls_for_find/tests_for_find.rs @@ -8,34 +8,36 @@ use crate::BitString; fn contains_basic() { let bits = BitString::try_from("101100").unwrap(); let v = bits.as_bitstr(); - - assert!(v.contains(&BitString::try_from("01").unwrap())); - assert!(!v.contains(&BitString::try_from("111").unwrap())); + let bs = BitString::try_from("01").unwrap(); + assert!(v.contains(&bs.as_bitstr())); + let bs = BitString::try_from("111").unwrap(); + assert!(!v.contains(&bs.as_bitstr())); } #[test] fn contains_empty_needle() { let bits = BitString::try_from("10110").unwrap(); let v = bits.as_bitstr(); - assert!(v.contains(&BitString::new())); + let empty = BitString::new(); + assert!(v.contains(&empty.as_bitstr())); } #[test] fn contains_longer_than_view_returns_false() { let bits = BitString::try_from("10").unwrap(); let v = bits.as_bitstr(); - assert!(!v.contains(&BitString::try_from("101").unwrap())); + let bs = BitString::try_from("101").unwrap(); + assert!(!v.contains(&bs.as_bitstr())); } #[test] fn contains_on_offset_view() { let bits = BitString::try_from("110010").unwrap(); - // bits: 1 1 0 0 1 0 - // view bits 1..5 → 1 0 0 1 let v = bits.as_bitstr().slice_from(1).slice_until(5); - - assert!(v.contains(&BitString::try_from("001").unwrap())); - assert!(!v.contains(&BitString::try_from("11").unwrap())); + let bs = BitString::try_from("001").unwrap(); + assert!(v.contains(&bs.as_bitstr())); + let bs = BitString::try_from("11").unwrap(); + assert!(!v.contains(&bs.as_bitstr())); } // --------------------------------------------------------------------------- @@ -46,45 +48,46 @@ fn contains_on_offset_view() { fn find_first_occurrence() { let bits = BitString::try_from("10110010").unwrap(); let v = bits.as_bitstr(); - - assert_eq!(v.find(&BitString::try_from("10").unwrap()), Some(0)); - assert_eq!(v.find(&BitString::try_from("01").unwrap()), Some(1)); - assert_eq!(v.find(&BitString::try_from("111").unwrap()), None); + let bs = BitString::try_from("10").unwrap(); + assert_eq!(v.find(&bs.as_bitstr()), Some(0)); + let bs = BitString::try_from("01").unwrap(); + assert_eq!(v.find(&bs.as_bitstr()), Some(1)); + let bs = BitString::try_from("111").unwrap(); + assert_eq!(v.find(&bs.as_bitstr()), None); } #[test] fn find_empty_needle_returns_zero() { let bits = BitString::try_from("10110").unwrap(); let v = bits.as_bitstr(); - assert_eq!(v.find(&BitString::new()), Some(0)); + let empty = BitString::new(); + assert_eq!(v.find(&empty.as_bitstr()), Some(0)); } #[test] fn find_longer_than_view_returns_none() { let bits = BitString::try_from("10").unwrap(); let v = bits.as_bitstr(); - assert_eq!(v.find(&BitString::try_from("101").unwrap()), None); + let bs = BitString::try_from("101").unwrap(); + assert_eq!(v.find(&bs.as_bitstr()), None); } #[test] fn find_on_offset_view() { let bits = BitString::try_from("11001001").unwrap(); - // bits: 1 1 0 0 1 0 0 1 - // slice_from(2).slice_until(7) → bits 2..8 → 0 0 1 0 0 1 (len 6) let v = bits.as_bitstr().slice_from(2).slice_until(7); - - // First "10" is at view position 2 (original bits 4-5) - assert_eq!(v.find(&BitString::try_from("10").unwrap()), Some(2)); - // "00" at view position 3 (original bits 5-6) - assert_eq!(v.find(&BitString::try_from("00").unwrap()), Some(0)); + let bs = BitString::try_from("10").unwrap(); + assert_eq!(v.find(&bs.as_bitstr()), Some(2)); + let bs = BitString::try_from("00").unwrap(); + assert_eq!(v.find(&bs.as_bitstr()), Some(0)); } #[test] fn find_at_end_of_view() { let bits = BitString::try_from("11100").unwrap(); let v = bits.as_bitstr(); - - assert_eq!(v.find(&BitString::try_from("00").unwrap()), Some(3)); + let bs = BitString::try_from("00").unwrap(); + assert_eq!(v.find(&bs.as_bitstr()), Some(3)); } // --------------------------------------------------------------------------- @@ -95,42 +98,39 @@ fn find_at_end_of_view() { fn rfind_last_occurrence() { let bits = BitString::try_from("10110010").unwrap(); let v = bits.as_bitstr(); - - // "10" at positions 0, 5, 6 → last is 6 - assert_eq!(v.rfind(&BitString::try_from("10").unwrap()), Some(6)); + let bs = BitString::try_from("10").unwrap(); + assert_eq!(v.rfind(&bs.as_bitstr()), Some(6)); } #[test] fn rfind_empty_needle_returns_bit_len() { let bits = BitString::try_from("10110").unwrap(); let v = bits.as_bitstr(); - assert_eq!(v.rfind(&BitString::new()), Some(5)); + let empty = BitString::new(); + assert_eq!(v.rfind(&empty.as_bitstr()), Some(5)); } #[test] fn rfind_longer_than_view_returns_none() { let bits = BitString::try_from("10").unwrap(); let v = bits.as_bitstr(); - assert_eq!(v.rfind(&BitString::try_from("101").unwrap()), None); + let bs = BitString::try_from("101").unwrap(); + assert_eq!(v.rfind(&bs.as_bitstr()), None); } #[test] fn rfind_on_offset_view() { let bits = BitString::try_from("11001001").unwrap(); - // view bits 2..7 → 0 0 1 0 0 let v = bits.as_bitstr().slice_from(2).slice_until(7); - - // Last "00" is at view position 3 - assert_eq!(v.rfind(&BitString::try_from("00").unwrap()), Some(3)); + let bs = BitString::try_from("00").unwrap(); + assert_eq!(v.rfind(&bs.as_bitstr()), Some(3)); } #[test] fn find_and_rfind_needle_appears_once() { let bits = BitString::try_from("11010").unwrap(); let v = bits.as_bitstr(); - - let f = v.find(&BitString::try_from("101").unwrap()); - let r = v.rfind(&BitString::try_from("101").unwrap()); - assert_eq!(f, r); - assert_eq!(f, Some(1)); + let bs = BitString::try_from("101").unwrap(); + assert_eq!(v.find(&bs.as_bitstr()), Some(1)); + assert_eq!(v.rfind(&bs.as_bitstr()), Some(1)); } diff --git a/src/bit_str/impls_for_matching/impls_for_matches_at.rs b/src/bit_str/impls_for_matching/impls_for_matches_at.rs index e9f64c3..7791f41 100644 --- a/src/bit_str/impls_for_matching/impls_for_matches_at.rs +++ b/src/bit_str/impls_for_matching/impls_for_matches_at.rs @@ -1,24 +1,87 @@ -use crate::BitString; +use crate::traits::*; +use crate::{WORD_BITS, low_mask}; use crate::BitStr; impl<'bs> BitStr<'bs> { /// Compare `needle` bits against `self` starting at `offset`. /// - /// Delegates to [`BitString::bits_equal_at`] with the view's start - /// offset added. + /// When `needle.start` is word-aligned, the fast path delegates to + /// SIMD word-equality via [`BitsEq::eq_words`]. Otherwise falls + /// back to a scalar word-at-a-time comparison. #[inline] - pub(crate) fn bits_equal_at(&self, offset: usize, needle: &BitString) -> bool { - self.source.bits_equal_at(self.start + offset, needle) + pub(crate) fn bits_equal_at(&self, offset: usize, needle: &BitStr<'_>) -> bool { + let n = needle.bit_len; + if n == 0 { + return true; + } + + let hs_base = self.start + offset; + let nd_base = needle.start; + let hs_words = self.source.words(); + let nd_words = needle.source.words(); + + // Sub-word fast path — a single 64-bit read on each side. + if n <= WORD_BITS { + let mask = low_mask(n); + let h = hs_words.read_word_at(hs_base); + let nd = nd_words.read_word_at(nd_base); + return (h & mask) == (nd & mask); + } + + // Multi-word, needle word-aligned — SIMD eq_words on haystack. + if nd_base % WORD_BITS == 0 { + let full_words = n / WORD_BITS; + let nd_aligned = &nd_words[nd_base / WORD_BITS..]; + + if !hs_words.eq_words(nd_aligned, full_words, hs_base) { + return false; + } + + let rem = n % WORD_BITS; + if rem > 0 { + let mask = low_mask(rem); + let h = hs_words.read_word_at(hs_base + full_words * WORD_BITS); + if (h & mask) != (nd_aligned[full_words] & mask) { + return false; + } + } + + return true; + } + + // Both sides misaligned — scalar word-at-a-time (rare). + let full_words = n / WORD_BITS; + for i in 0..full_words { + let pos = i * WORD_BITS; + let h = hs_words.read_word_at(hs_base + pos); + let nd = nd_words.read_word_at(nd_base + pos); + if h != nd { + return false; + } + } + + let rem = n % WORD_BITS; + if rem > 0 { + let mask = low_mask(rem); + let pos = full_words * WORD_BITS; + let h = hs_words.read_word_at(hs_base + pos); + let nd = nd_words.read_word_at(nd_base + pos); + if (h & mask) != (nd & mask) { + return false; + } + } + + true } /// Returns `true` if `pattern` matches the bits starting at `index`. #[inline] - pub fn matches_at(&self, index: usize, pattern: &BitString) -> bool { + pub fn matches_at(&self, index: usize, pattern: &BitStr<'_>) -> bool { if index > self.bit_len { return false; } - if pattern.bit_len() > self.bit_len - index { + if pattern.bit_len > self.bit_len - index { return false; } self.bits_equal_at(index, pattern) @@ -26,20 +89,20 @@ impl<'bs> BitStr<'bs> { /// Returns `true` if `prefix` is a prefix of `self`. #[inline] - pub fn starts_with(&self, prefix: &BitString) -> bool { + pub fn starts_with(&self, prefix: &BitStr<'_>) -> bool { self.matches_at(0, prefix) } /// Returns `true` if `suffix` is a suffix of `self`. #[inline] - pub fn ends_with(&self, suffix: &BitString) -> bool { - if suffix.bit_len() == 0 { + pub fn ends_with(&self, suffix: &BitStr<'_>) -> bool { + if suffix.bit_len == 0 { return true; } - if suffix.bit_len() > self.bit_len { + if suffix.bit_len > self.bit_len { return false; } - self.bits_equal_at(self.bit_len - suffix.bit_len(), suffix) + self.bits_equal_at(self.bit_len - suffix.bit_len, suffix) } } diff --git a/src/bit_str/impls_for_matching/impls_for_matches_at/tests_for_matches_at.rs b/src/bit_str/impls_for_matching/impls_for_matches_at/tests_for_matches_at.rs index 3995280..b5e6e7c 100644 --- a/src/bit_str/impls_for_matching/impls_for_matches_at/tests_for_matches_at.rs +++ b/src/bit_str/impls_for_matching/impls_for_matches_at/tests_for_matches_at.rs @@ -9,18 +9,16 @@ fn matches_at_exact_match() { let bits = BitString::try_from("101100").unwrap(); let v = bits.as_bitstr(); let p = BitString::try_from("10").unwrap(); - - assert!(v.matches_at(0, &p)); - assert!(!v.matches_at(1, &p)); + assert!(v.matches_at(0, &p.as_bitstr())); + assert!(!v.matches_at(1, &p.as_bitstr())); } #[test] fn matches_at_beyond_view_returns_false() { let bits = BitString::try_from("10110").unwrap(); - let v = bits.as_bitstr().slice_from(2); // "110" + let v = bits.as_bitstr().slice_from(2); let p = BitString::try_from("10").unwrap(); - - assert!(!v.matches_at(2, &p)); // index out of bounds + assert!(!v.matches_at(2, &p.as_bitstr())); } #[test] @@ -28,8 +26,7 @@ fn matches_at_pattern_too_long_returns_false() { let bits = BitString::try_from("10").unwrap(); let v = bits.as_bitstr(); let p = BitString::try_from("100").unwrap(); - - assert!(!v.matches_at(0, &p)); + assert!(!v.matches_at(0, &p.as_bitstr())); } #[test] @@ -37,20 +34,17 @@ fn matches_at_empty_pattern_always_true() { let bits = BitString::try_from("10110").unwrap(); let v = bits.as_bitstr(); let p = BitString::new(); - - assert!(v.matches_at(0, &p)); - assert!(v.matches_at(3, &p)); + assert!(v.matches_at(0, &p.as_bitstr())); + assert!(v.matches_at(3, &p.as_bitstr())); } #[test] fn matches_at_on_offset_view() { let bits = BitString::try_from("11100011").unwrap(); - // bits: 1 1 1 0 0 0 1 1 - // view bits 2..7 → 1 0 0 0 1 + // bits: 1 1 1 0 0 0 1 1, view 2..7 → 1 0 0 0 1 let v = bits.as_bitstr().slice_from(2).slice_until(7); let p = BitString::try_from("0001").unwrap(); - - assert!(v.matches_at(1, &p)); + assert!(v.matches_at(1, &p.as_bitstr())); } // --------------------------------------------------------------------------- @@ -61,37 +55,38 @@ fn matches_at_on_offset_view() { fn starts_with_basic() { let bits = BitString::try_from("101100").unwrap(); let v = bits.as_bitstr(); - - assert!(v.starts_with(&BitString::try_from("101").unwrap())); - assert!(!v.starts_with(&BitString::try_from("11").unwrap())); - assert!(v.starts_with(&BitString::try_from("101100").unwrap())); + let p = BitString::try_from("101").unwrap(); + assert!(v.starts_with(&p.as_bitstr())); + let p = BitString::try_from("11").unwrap(); + assert!(!v.starts_with(&p.as_bitstr())); + let p = BitString::try_from("101100").unwrap(); + assert!(v.starts_with(&p.as_bitstr())); } #[test] fn starts_with_empty_prefix() { let bits = BitString::try_from("10110").unwrap(); let v = bits.as_bitstr(); - - assert!(v.starts_with(&BitString::new())); + let p = BitString::new(); + assert!(v.starts_with(&p.as_bitstr())); } #[test] fn starts_with_longer_prefix_returns_false() { let bits = BitString::try_from("101").unwrap(); let v = bits.as_bitstr(); - - assert!(!v.starts_with(&BitString::try_from("1011").unwrap())); + let p = BitString::try_from("1011").unwrap(); + assert!(!v.starts_with(&p.as_bitstr())); } #[test] fn starts_with_on_offset_view() { let bits = BitString::try_from("110101").unwrap(); - // bits: 1 1 0 1 0 1 - // view bits 1..5 → 1 0 1 0 let v = bits.as_bitstr().slice_from(1).slice_until(5); - - assert!(v.starts_with(&BitString::try_from("10").unwrap())); - assert!(!v.starts_with(&BitString::try_from("11").unwrap())); + let p = BitString::try_from("10").unwrap(); + assert!(v.starts_with(&p.as_bitstr())); + let p = BitString::try_from("11").unwrap(); + assert!(!v.starts_with(&p.as_bitstr())); } // --------------------------------------------------------------------------- @@ -102,37 +97,39 @@ fn starts_with_on_offset_view() { fn ends_with_basic() { let bits = BitString::try_from("101100").unwrap(); let v = bits.as_bitstr(); - - assert!(v.ends_with(&BitString::try_from("100").unwrap())); - assert!(!v.ends_with(&BitString::try_from("10").unwrap())); - assert!(v.ends_with(&BitString::try_from("101100").unwrap())); + let s = BitString::try_from("100").unwrap(); + assert!(v.ends_with(&s.as_bitstr())); + let s = BitString::try_from("10").unwrap(); + assert!(!v.ends_with(&s.as_bitstr())); + let s = BitString::try_from("101100").unwrap(); + assert!(v.ends_with(&s.as_bitstr())); } #[test] fn ends_with_empty_suffix() { let bits = BitString::try_from("10110").unwrap(); let v = bits.as_bitstr(); - - assert!(v.ends_with(&BitString::new())); + let s = BitString::new(); + assert!(v.ends_with(&s.as_bitstr())); } #[test] fn ends_with_longer_suffix_returns_false() { let bits = BitString::try_from("101").unwrap(); let v = bits.as_bitstr(); - - assert!(!v.ends_with(&BitString::try_from("1101").unwrap())); + let s = BitString::try_from("1101").unwrap(); + assert!(!v.ends_with(&s.as_bitstr())); } #[test] fn ends_with_on_offset_view() { let bits = BitString::try_from("110101").unwrap(); - // bits: 1 1 0 1 0 1 - // slice_from(1).slice_until(5) → bits 1..6 → 1 0 1 0 1 + // bits 1..6 → 1 0 1 0 1 let v = bits.as_bitstr().slice_from(1).slice_until(5); - - assert!(v.ends_with(&BitString::try_from("01").unwrap())); - assert!(!v.ends_with(&BitString::try_from("10").unwrap())); + let s = BitString::try_from("01").unwrap(); + assert!(v.ends_with(&s.as_bitstr())); + let s = BitString::try_from("10").unwrap(); + assert!(!v.ends_with(&s.as_bitstr())); } #[test] @@ -142,10 +139,7 @@ fn matches_and_ends_with_across_word_boundaries() { bits.set(63, true); bits.set(64, true); bits.set(65, true); - let v = bits.as_bitstr(); let p = BitString::try_from("1111").unwrap(); - - // "1111" at bits 62-65, crosses word boundary 63/64 - assert!(v.matches_at(62, &p)); + assert!(v.matches_at(62, &p.as_bitstr())); } diff --git a/src/bit_str/impls_for_matching/impls_for_strip.rs b/src/bit_str/impls_for_matching/impls_for_strip.rs index 08e2d35..45ac3ef 100644 --- a/src/bit_str/impls_for_matching/impls_for_strip.rs +++ b/src/bit_str/impls_for_matching/impls_for_strip.rs @@ -1,20 +1,18 @@ -use crate::BitString; - use crate::BitStr; impl<'bs> BitStr<'bs> { /// Strips `prefix` from the start, returning the remaining sub-view. #[inline] - pub fn strip_prefix(&self, prefix: &BitString) -> Option { + pub fn strip_prefix(&self, prefix: &BitStr<'_>) -> Option { self.starts_with(prefix) - .then(|| self.slice_from(prefix.bit_len())) + .then(|| self.slice_from(prefix.bit_len)) } /// Strips `suffix` from the end, returning the remaining sub-view. #[inline] - pub fn strip_suffix(&self, suffix: &BitString) -> Option { + pub fn strip_suffix(&self, suffix: &BitStr<'_>) -> Option { self.ends_with(suffix) - .then(|| self.slice_until(self.bit_len - suffix.bit_len())) + .then(|| self.slice_until(self.bit_len - suffix.bit_len)) } } diff --git a/src/bit_str/impls_for_matching/impls_for_strip/tests_for_strip.rs b/src/bit_str/impls_for_matching/impls_for_strip/tests_for_strip.rs index 20494fe..8f29663 100644 --- a/src/bit_str/impls_for_matching/impls_for_strip/tests_for_strip.rs +++ b/src/bit_str/impls_for_matching/impls_for_strip/tests_for_strip.rs @@ -8,33 +8,28 @@ use crate::BitString; fn strip_prefix_removes_matching_prefix() { let bits = BitString::try_from("101100").unwrap(); let v = bits.as_bitstr(); - - let rest = v - .strip_prefix(&BitString::try_from("101").unwrap()) - .unwrap(); + let p = BitString::try_from("101").unwrap(); + let rest = v.strip_prefix(&p.as_bitstr()).unwrap(); assert_eq!(rest.bit_len(), 3); - assert_eq!(rest.get(0), Some(true)); // original bit 3 - assert_eq!(rest.get(1), Some(false)); // original bit 4 - assert_eq!(rest.get(2), Some(false)); // original bit 5 + assert_eq!(rest.get(0), Some(true)); + assert_eq!(rest.get(1), Some(false)); + assert_eq!(rest.get(2), Some(false)); } #[test] fn strip_prefix_non_matching_returns_none() { let bits = BitString::try_from("101100").unwrap(); let v = bits.as_bitstr(); - - assert!( - v.strip_prefix(&BitString::try_from("11").unwrap()) - .is_none() - ); + let p = BitString::try_from("11").unwrap(); + assert!(v.strip_prefix(&p.as_bitstr()).is_none()); } #[test] fn strip_prefix_empty() { let bits = BitString::try_from("10110").unwrap(); let v = bits.as_bitstr(); - - let rest = v.strip_prefix(&BitString::new()).unwrap(); + let p = BitString::new(); + let rest = v.strip_prefix(&p.as_bitstr()).unwrap(); assert_eq!(rest.bit_len(), v.bit_len()); } @@ -42,10 +37,8 @@ fn strip_prefix_empty() { fn strip_prefix_entire_view() { let bits = BitString::try_from("101").unwrap(); let v = bits.as_bitstr(); - - let rest = v - .strip_prefix(&BitString::try_from("101").unwrap()) - .unwrap(); + let p = BitString::try_from("101").unwrap(); + let rest = v.strip_prefix(&p.as_bitstr()).unwrap(); assert_eq!(rest.bit_len(), 0); } @@ -54,9 +47,8 @@ fn strip_prefix_on_offset_view() { let bits = BitString::try_from("110101").unwrap(); // view bits 1..6 → 1 0 1 0 1 let v = bits.as_bitstr().slice_from(1).slice_until(5); - - let rest = v.strip_prefix(&BitString::try_from("10").unwrap()).unwrap(); - // remaining: "101" (3 bits) + let p = BitString::try_from("10").unwrap(); + let rest = v.strip_prefix(&p.as_bitstr()).unwrap(); assert_eq!(rest.bit_len(), 3); assert_eq!(rest.get(0), Some(true)); assert_eq!(rest.get(1), Some(false)); @@ -71,33 +63,28 @@ fn strip_prefix_on_offset_view() { fn strip_suffix_removes_matching_suffix() { let bits = BitString::try_from("101100").unwrap(); let v = bits.as_bitstr(); - - let rest = v - .strip_suffix(&BitString::try_from("100").unwrap()) - .unwrap(); + let s = BitString::try_from("100").unwrap(); + let rest = v.strip_suffix(&s.as_bitstr()).unwrap(); assert_eq!(rest.bit_len(), 3); - assert_eq!(rest.get(0), Some(true)); // bit 0 - assert_eq!(rest.get(1), Some(false)); // bit 1 - assert_eq!(rest.get(2), Some(true)); // bit 2 + assert_eq!(rest.get(0), Some(true)); + assert_eq!(rest.get(1), Some(false)); + assert_eq!(rest.get(2), Some(true)); } #[test] fn strip_suffix_non_matching_returns_none() { let bits = BitString::try_from("101100").unwrap(); let v = bits.as_bitstr(); - - assert!( - v.strip_suffix(&BitString::try_from("10").unwrap()) - .is_none() - ); + let s = BitString::try_from("10").unwrap(); + assert!(v.strip_suffix(&s.as_bitstr()).is_none()); } #[test] fn strip_suffix_empty() { let bits = BitString::try_from("10110").unwrap(); let v = bits.as_bitstr(); - - let rest = v.strip_suffix(&BitString::new()).unwrap(); + let s = BitString::new(); + let rest = v.strip_suffix(&s.as_bitstr()).unwrap(); assert_eq!(rest.bit_len(), v.bit_len()); } @@ -105,10 +92,8 @@ fn strip_suffix_empty() { fn strip_suffix_entire_view() { let bits = BitString::try_from("101").unwrap(); let v = bits.as_bitstr(); - - let rest = v - .strip_suffix(&BitString::try_from("101").unwrap()) - .unwrap(); + let s = BitString::try_from("101").unwrap(); + let rest = v.strip_suffix(&s.as_bitstr()).unwrap(); assert_eq!(rest.bit_len(), 0); } @@ -117,9 +102,8 @@ fn strip_suffix_on_offset_view() { let bits = BitString::try_from("110101").unwrap(); // view bits 1..6 → 1 0 1 0 1 let v = bits.as_bitstr().slice_from(1).slice_until(5); - - let rest = v.strip_suffix(&BitString::try_from("01").unwrap()).unwrap(); - // remaining: "101" (3 bits) + let s = BitString::try_from("01").unwrap(); + let rest = v.strip_suffix(&s.as_bitstr()).unwrap(); assert_eq!(rest.bit_len(), 3); assert_eq!(rest.get(0), Some(true)); assert_eq!(rest.get(1), Some(false)); From 9a0ffbbf84a90d734ce746f91874ffc09814df6e Mon Sep 17 00:00:00 2001 From: juncheng Date: Wed, 24 Jun 2026 13:30:47 +0000 Subject: [PATCH 11/22] refactor: BitStr matching API uses value semantics, BitString delegates to as_bit_str() BitStr is Copy (24 bytes), so passing by value is cleaner and costs nothing after inlining. All matching methods now take BitStr<'_> instead of &BitStr<'_>. BitString's matching methods (matches_at, starts_with, ends_with, contains, find, rfind, strip_prefix, strip_suffix) now delegate entirely to self.as_bit_str(), removing ~130 lines of duplicated SIMD logic. Tests: moved tests_for_bits_equal_at to BitStr side; split combined tests_for_matches_at into separate files per method. Benchmarks updated. Co-authored-by: Claude Co-authored-by: DeepSeek AI --- benches/matching_ends_with.rs | 2 +- benches/matching_find.rs | 2 +- benches/matching_matches_at.rs | 4 +- benches/matching_rfind.rs | 3 +- benches/matching_starts_with.rs | 2 +- benches/matching_strip_prefix.rs | 4 +- src/bit_str/impls_for_access/tests_for_get.rs | 20 +-- .../impls_for_access/tests_for_get_chunk.rs | 12 +- .../tests_for_count_ones.rs | 26 ++-- src/bit_str/impls_for_iter/tests_for_iter.rs | 28 ++-- .../impls_for_matching/impls_for_find.rs | 6 +- .../impls_for_find/tests_for_find.rs | 68 ++++----- .../impls_for_matches_at.rs | 17 ++- .../tests_for_bits_equal_at.rs | 56 +++++++ .../tests_for_ends_with.rs | 56 +++++++ .../tests_for_matches_at.rs | 121 ++------------- .../tests_for_starts_with.rs | 43 ++++++ .../impls_for_matching/impls_for_strip.rs | 4 +- .../impls_for_strip/tests_for_strip.rs | 40 ++--- .../impls_for_slice/tests_for_slice.rs | 44 +++--- src/bit_string.rs | 2 +- src/bit_string/impls_for_matching.rs | 5 - .../impls_for_matching/impls_for_find.rs | 68 +-------- .../impls_for_find/tests_for_contains.rs | 18 +-- .../impls_for_find/tests_for_find.rs | 16 +- .../impls_for_find/tests_for_rfind.rs | 16 +- .../impls_for_matches_at.rs | 144 +----------------- .../tests_for_ends_with.rs | 10 +- .../tests_for_matches_at.rs | 8 +- .../tests_for_starts_with.rs | 14 +- .../impls_for_matching/impls_for_strip.rs | 12 +- .../impls_for_strip/tests_for_strip_prefix.rs | 10 +- .../impls_for_strip/tests_for_strip_suffix.rs | 16 +- .../tests_for_bits_equal_at.rs | 52 ------- .../tests_for_backend_equivalence.rs | 4 +- .../tests_for_backend_equivalence.rs | 4 +- .../tests_for_backend_equivalence.rs | 4 +- .../tests_for_backend_equivalence.rs | 2 +- .../tests_for_backend_equivalence.rs | 4 +- 39 files changed, 401 insertions(+), 566 deletions(-) create mode 100644 src/bit_str/impls_for_matching/impls_for_matches_at/tests_for_bits_equal_at.rs create mode 100644 src/bit_str/impls_for_matching/impls_for_matches_at/tests_for_ends_with.rs create mode 100644 src/bit_str/impls_for_matching/impls_for_matches_at/tests_for_starts_with.rs delete mode 100644 src/bit_string/impls_for_matching/tests_for_bits_equal_at.rs diff --git a/benches/matching_ends_with.rs b/benches/matching_ends_with.rs index 427deca..234d7d7 100644 --- a/benches/matching_ends_with.rs +++ b/benches/matching_ends_with.rs @@ -79,7 +79,7 @@ fn e6ns(b: Bencher) { } fn b_bit(b: Bencher, c: Case) { - b.bench(|| black_box(&c.haystack_bits).ends_with(black_box(&c.suffix_bits))); + b.bench(|| black_box(&c.haystack_bits).ends_with(black_box(c.suffix_bits.as_bit_str()))); } fn b_str(b: Bencher, c: Case) { b.bench(|| black_box(&c.haystack_string).ends_with(black_box(&c.suffix_string))); diff --git a/benches/matching_find.rs b/benches/matching_find.rs index 33d1938..c6bee89 100644 --- a/benches/matching_find.rs +++ b/benches/matching_find.rs @@ -79,7 +79,7 @@ fn f65536xs(bencher: Bencher) { } fn bench_bit_string(bencher: Bencher, case: NeedleCase) { - bencher.bench(|| black_box(&case.haystack_bits).find(black_box(&case.needle_bits))); + bencher.bench(|| black_box(&case.haystack_bits).find(black_box(case.needle_bits.as_bit_str()))); } fn bench_string(bencher: Bencher, case: NeedleCase) { diff --git a/benches/matching_matches_at.rs b/benches/matching_matches_at.rs index af6344f..09f3dfc 100644 --- a/benches/matching_matches_at.rs +++ b/benches/matching_matches_at.rs @@ -132,7 +132,9 @@ fn m6nus(b: Bencher) { // --------------------------------------------------------------------------- fn b_bit(b: Bencher, c: Case) { - b.bench(|| black_box(&c.haystack_bits).matches_at(c.index, black_box(&c.pattern_bits))); + b.bench(|| { + black_box(&c.haystack_bits).matches_at(c.index, black_box(c.pattern_bits.as_bit_str())) + }); } fn b_str(b: Bencher, c: Case) { diff --git a/benches/matching_rfind.rs b/benches/matching_rfind.rs index 191e96a..f3f7c17 100644 --- a/benches/matching_rfind.rs +++ b/benches/matching_rfind.rs @@ -93,7 +93,8 @@ fn rfind_len_65536_miss_string(bencher: Bencher) { } fn bench_bit_string(bencher: Bencher, case: NeedleCase) { - bencher.bench(|| black_box(&case.haystack_bits).rfind(black_box(&case.needle_bits))); + bencher + .bench(|| black_box(&case.haystack_bits).rfind(black_box(case.needle_bits.as_bit_str()))); } fn bench_string(bencher: Bencher, case: NeedleCase) { diff --git a/benches/matching_starts_with.rs b/benches/matching_starts_with.rs index ecd65b1..d53f93d 100644 --- a/benches/matching_starts_with.rs +++ b/benches/matching_starts_with.rs @@ -78,7 +78,7 @@ fn s6ns(b: Bencher) { } fn b_bit(b: Bencher, c: Case) { - b.bench(|| black_box(&c.haystack_bits).starts_with(black_box(&c.prefix_bits))); + b.bench(|| black_box(&c.haystack_bits).starts_with(black_box(c.prefix_bits.as_bit_str()))); } fn b_str(b: Bencher, c: Case) { b.bench(|| black_box(&c.haystack_string).starts_with(black_box(&c.prefix_string))); diff --git a/benches/matching_strip_prefix.rs b/benches/matching_strip_prefix.rs index b1442a5..a6631a4 100644 --- a/benches/matching_strip_prefix.rs +++ b/benches/matching_strip_prefix.rs @@ -53,7 +53,9 @@ fn strip_prefix_len_65536_miss_string(bencher: Bencher) { } fn bench_bit_string(bencher: Bencher, case: NeedleCase) { - bencher.bench(|| black_box(&case.haystack_bits).strip_prefix(black_box(&case.needle_bits))); + bencher.bench(|| { + black_box(&case.haystack_bits).strip_prefix(black_box(case.needle_bits.as_bit_str())) + }); } fn bench_string(bencher: Bencher, case: NeedleCase) { diff --git a/src/bit_str/impls_for_access/tests_for_get.rs b/src/bit_str/impls_for_access/tests_for_get.rs index 371e344..fcb165a 100644 --- a/src/bit_str/impls_for_access/tests_for_get.rs +++ b/src/bit_str/impls_for_access/tests_for_get.rs @@ -10,7 +10,7 @@ use crate::BitString; #[test] fn empty_source_returns_none() { let s = BitString::new(); - let v = s.as_bitstr(); + let v = s.as_bit_str(); assert!(v.is_empty()); assert_eq!(v.get(0), None); assert_eq!(v.get(usize::MAX), None); @@ -35,7 +35,7 @@ fn empty_view_via_direct_construct_returns_none() { #[test] fn full_view_reads_all_bits() { let s = BitString::try_from("101001").unwrap(); - let v = s.as_bitstr(); + let v = s.as_bit_str(); assert_eq!(v.get(0), Some(true)); assert_eq!(v.get(1), Some(false)); @@ -48,7 +48,7 @@ fn full_view_reads_all_bits() { #[test] fn returns_none_at_bit_len_and_beyond() { let s = BitString::try_from("101001").unwrap(); - let v = s.as_bitstr(); + let v = s.as_bit_str(); assert_eq!(v.get(6), None); // exactly bit_len assert_eq!(v.get(7), None); @@ -59,7 +59,7 @@ fn returns_none_at_bit_len_and_beyond() { #[test] fn first_and_last_on_full_view() { let s = BitString::try_from("101001").unwrap(); - let v = s.as_bitstr(); + let v = s.as_bit_str(); assert_eq!(v.first(), Some(true)); assert_eq!(v.last(), Some(true)); @@ -72,7 +72,7 @@ fn first_and_last_on_full_view() { #[test] fn offset_view_reads_correct_bits() { let s = BitString::try_from("00110110").unwrap(); // 0,0,1,1,0,1,1,0 - let v = s.as_bitstr().slice(UsizeCO::try_new(2, 6).unwrap()); + let v = s.as_bit_str().slice(UsizeCO::try_new(2, 6).unwrap()); assert_eq!(v.bit_len(), 4); assert_eq!(v.start(), 2); @@ -86,7 +86,7 @@ fn offset_view_reads_correct_bits() { #[test] fn offset_view_first_and_last() { let s = BitString::try_from("00011000").unwrap(); - let v = s.as_bitstr().slice(UsizeCO::try_new(3, 5).unwrap()); + let v = s.as_bit_str().slice(UsizeCO::try_new(3, 5).unwrap()); assert_eq!(v.first(), Some(true)); assert_eq!(v.last(), Some(true)); @@ -105,7 +105,7 @@ fn cross_word_boundary_in_offset_view() { s.set(65, true); s.set(129, true); - let v = s.as_bitstr().slice(UsizeCO::try_new(62, 130).unwrap()); // 68 bits + let v = s.as_bit_str().slice(UsizeCO::try_new(62, 130).unwrap()); // 68 bits assert_eq!(v.bit_len(), 68); assert_eq!(v.get(0), Some(true)); // src bit 62 @@ -126,7 +126,7 @@ fn view_starting_unaligned_and_crossing_word() { s.set(64, true); s.set(70, true); - let v = s.as_bitstr().slice(UsizeCO::try_new(60, 72).unwrap()); // 12 bits + let v = s.as_bit_str().slice(UsizeCO::try_new(60, 72).unwrap()); // 12 bits assert_eq!(v.get(0), Some(true)); // src bit 60 assert_eq!(v.get(1), Some(false)); @@ -145,7 +145,7 @@ fn view_starting_unaligned_and_crossing_word() { #[test] fn single_bit_view() { let s = BitString::try_from("101").unwrap(); - let v = s.as_bitstr().slice(UsizeCO::try_new(1, 2).unwrap()); + let v = s.as_bit_str().slice(UsizeCO::try_new(1, 2).unwrap()); assert_eq!(v.bit_len(), 1); assert_eq!(v.get(0), Some(false)); @@ -157,7 +157,7 @@ fn single_bit_view() { #[test] fn single_bit_view_at_last_position() { let s = BitString::try_from("101").unwrap(); - let v = s.as_bitstr().slice(UsizeCO::try_new(2, 3).unwrap()); + let v = s.as_bit_str().slice(UsizeCO::try_new(2, 3).unwrap()); assert_eq!(v.bit_len(), 1); assert_eq!(v.get(0), Some(true)); diff --git a/src/bit_str/impls_for_access/tests_for_get_chunk.rs b/src/bit_str/impls_for_access/tests_for_get_chunk.rs index 33c77df..c0360b8 100644 --- a/src/bit_str/impls_for_access/tests_for_get_chunk.rs +++ b/src/bit_str/impls_for_access/tests_for_get_chunk.rs @@ -11,7 +11,7 @@ use crate::low_mask; #[test] fn aligned_chunk_from_start() { let s = BitString::try_from("1").unwrap(); - let v = s.as_bitstr(); + let v = s.as_bit_str(); assert_eq!(v.get_chunk(0), 1); } @@ -21,7 +21,7 @@ fn aligned_chunk_spanning_two_words() { s.set(63, true); // last bit of word 0 s.set(64, true); // first bit of word 1 - let v = s.as_bitstr(); + let v = s.as_bit_str(); let chunk = v.get_chunk(63); assert_eq!(chunk & 1, 1); // bit 63 → LSB of chunk @@ -41,7 +41,7 @@ fn unaligned_chunk_in_offset_view() { s.set(63, true); s.set(64, true); // crosses into word 1 - let v = s.as_bitstr().slice(UsizeCO::try_new(61, 66).unwrap()); // 5 bits + let v = s.as_bit_str().slice(UsizeCO::try_new(61, 66).unwrap()); // 5 bits // Bits 61..65: 1,1,1,1,0 → 0b01111 = 15 assert_eq!(v.get_chunk(0), 0b01111); @@ -56,7 +56,7 @@ fn unaligned_chunk_in_offset_view() { #[test] fn chunk_at_bit_len_returns_zero() { let s = BitString::try_from("11111111").unwrap(); - let v = s.as_bitstr().slice(UsizeCO::try_new(2, 5).unwrap()); // 3 bits + let v = s.as_bit_str().slice(UsizeCO::try_new(2, 5).unwrap()); // 3 bits assert_eq!(v.get_chunk(3), 0); // exactly bit_len assert_eq!(v.get_chunk(4), 0); // beyond @@ -67,7 +67,7 @@ fn chunk_at_bit_len_returns_zero() { #[test] fn chunk_does_not_leak_bits_beyond_view() { let s = BitString::ones(130); - let v = s.as_bitstr().slice(UsizeCO::try_new(60, 70).unwrap()); // 10 bits + let v = s.as_bit_str().slice(UsizeCO::try_new(60, 70).unwrap()); // 10 bits let chunk = v.get_chunk(5); // 5 remaining valid bits assert_eq!(chunk, low_mask(5)); // exactly 5 ones, nothing leaked @@ -78,7 +78,7 @@ fn chunk_does_not_leak_bits_beyond_view() { fn chunk_at_last_bit() { let mut s = BitString::zeros(130); s.set(129, true); - let v = s.as_bitstr().slice(UsizeCO::try_new(128, 130).unwrap()); // 2 bits + let v = s.as_bit_str().slice(UsizeCO::try_new(128, 130).unwrap()); // 2 bits assert_eq!(v.get_chunk(1), 1); // just bit 129 assert_eq!(v.get_chunk(2), 0); // beyond view diff --git a/src/bit_str/impls_for_bit_arith/impls_for_count_ones/tests_for_count_ones.rs b/src/bit_str/impls_for_bit_arith/impls_for_count_ones/tests_for_count_ones.rs index 76306db..b265681 100644 --- a/src/bit_str/impls_for_bit_arith/impls_for_count_ones/tests_for_count_ones.rs +++ b/src/bit_str/impls_for_bit_arith/impls_for_count_ones/tests_for_count_ones.rs @@ -7,7 +7,7 @@ use crate::BitString; fn counts_empty_view() { let bits = BitString::try_from("10110").unwrap(); // Create an empty view by slicing beyond the source length. - let v = bits.as_bitstr().slice(UsizeCO::try_new(10, 20).unwrap()); + let v = bits.as_bit_str().slice(UsizeCO::try_new(10, 20).unwrap()); assert_eq!(v.count_ones(), 0); assert_eq!(v.count_zeros(), 0); @@ -21,12 +21,12 @@ fn word_aligned_fast_path() { bits.set(i, true); } // Full view: bit_len=130, aligned, 7 ones. - let v = bits.as_bitstr(); + let v = bits.as_bit_str(); assert_eq!(v.count_ones(), 7); assert_eq!(v.count_zeros(), 123); // Slice from word-aligned boundary (64..130), 6 ones. - let v = bits.as_bitstr().slice(UsizeCO::try_new(64, 130).unwrap()); + let v = bits.as_bit_str().slice(UsizeCO::try_new(64, 130).unwrap()); assert_eq!(v.count_ones(), 5); } @@ -39,7 +39,7 @@ fn unaligned_start() { bits.set(66, true); // View from bit 1 to bit 130 → len 129, unaligned start. - let v = bits.as_bitstr().slice(UsizeCO::try_new(1, 130).unwrap()); + let v = bits.as_bit_str().slice(UsizeCO::try_new(1, 130).unwrap()); assert_eq!(v.count_ones(), 2); assert_eq!(v.count_zeros(), 127); } @@ -49,7 +49,7 @@ fn unaligned_start() { fn all_ones_at_various_lengths() { for len in [1, 63, 64, 65, 127, 128, 129, 130] { let bits = BitString::ones(len); - let v = bits.as_bitstr(); + let v = bits.as_bit_str(); assert_eq!(v.count_ones(), len, "len={len}"); assert_eq!(v.count_zeros(), 0, "len={len}"); } @@ -60,7 +60,7 @@ fn all_ones_at_various_lengths() { fn all_zeros_at_various_lengths() { for len in [1, 63, 64, 65, 127, 128, 129, 130] { let bits = BitString::zeros(len); - let v = bits.as_bitstr(); + let v = bits.as_bit_str(); assert_eq!(v.count_ones(), 0, "len={len}"); assert_eq!(v.count_zeros(), len, "len={len}"); } @@ -70,7 +70,7 @@ fn all_zeros_at_various_lengths() { #[test] fn counts_mixed_bits_from_string() { let bits = BitString::try_from("1010011100").unwrap(); - let v = bits.as_bitstr(); + let v = bits.as_bit_str(); assert_eq!(v.count_ones(), 5); assert_eq!(v.count_zeros(), 5); @@ -83,12 +83,12 @@ fn unaligned_single_word() { // bits: 1 1 1 1 0 0 0 0 // View bits 1..7 → "111000" - let v = bits.as_bitstr().slice(UsizeCO::try_new(1, 7).unwrap()); + let v = bits.as_bit_str().slice(UsizeCO::try_new(1, 7).unwrap()); assert_eq!(v.count_ones(), 3); assert_eq!(v.count_zeros(), 3); // View bits 2..6 → "1100" - let v = bits.as_bitstr().slice(UsizeCO::try_new(2, 6).unwrap()); + let v = bits.as_bit_str().slice(UsizeCO::try_new(2, 6).unwrap()); assert_eq!(v.count_ones(), 2); assert_eq!(v.count_zeros(), 2); } @@ -100,7 +100,7 @@ fn invariant_ones_plus_zeros_equals_bit_len() { for i in (0..200).step_by(7) { bits.set(i, true); } - let full = bits.as_bitstr(); + 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] { @@ -131,7 +131,7 @@ fn unaligned_many_words() { // View from bit 1 (unaligned) covering bits 1..300 → 299 bits length. // Bit 1 is included; all 11 bits are in range [1, 300). - let v = bits.as_bitstr().slice(UsizeCO::try_new(1, 300).unwrap()); + let v = bits.as_bit_str().slice(UsizeCO::try_new(1, 300).unwrap()); assert_eq!(v.count_ones(), 11); assert_eq!(v.count_zeros(), 288); } @@ -144,12 +144,12 @@ fn aligned_one_word() { bits.set(63, true); // View word 0 (bits 0..64) - let v = bits.as_bitstr().slice(UsizeCO::try_new(0, 64).unwrap()); + let v = bits.as_bit_str().slice(UsizeCO::try_new(0, 64).unwrap()); assert_eq!(v.count_ones(), 2); assert_eq!(v.count_zeros(), 62); // View word 1 (bits 64..128) - let v = bits.as_bitstr().slice(UsizeCO::try_new(64, 128).unwrap()); + let v = bits.as_bit_str().slice(UsizeCO::try_new(64, 128).unwrap()); assert_eq!(v.count_ones(), 0); assert_eq!(v.count_zeros(), 64); } diff --git a/src/bit_str/impls_for_iter/tests_for_iter.rs b/src/bit_str/impls_for_iter/tests_for_iter.rs index 2d7d07d..1154ebf 100644 --- a/src/bit_str/impls_for_iter/tests_for_iter.rs +++ b/src/bit_str/impls_for_iter/tests_for_iter.rs @@ -6,7 +6,7 @@ use crate::BitString; #[test] fn iterates_bits_in_order() { let bits = BitString::try_from("101001").unwrap(); - let v = bits.as_bitstr(); + let v = bits.as_bit_str(); let values: Vec<_> = v.iter().collect(); @@ -17,7 +17,7 @@ fn iterates_bits_in_order() { fn iterates_empty_view() { let bits = BitString::try_from("10110").unwrap(); // Out-of-bounds slice clamps to empty. - let v = bits.as_bitstr().slice(UsizeCO::try_new(10, 20).unwrap()); + let v = bits.as_bit_str().slice(UsizeCO::try_new(10, 20).unwrap()); let mut iter = v.iter(); assert_eq!(iter.next(), None); @@ -29,7 +29,7 @@ fn iterates_empty_view() { #[test] fn supports_double_ended_iteration() { let bits = BitString::try_from("101001").unwrap(); - let v = bits.as_bitstr(); + let v = bits.as_bit_str(); let mut iter = v.iter(); assert_eq!(iter.next(), Some(true)); @@ -45,7 +45,7 @@ fn supports_double_ended_iteration() { #[test] fn size_hint_tracks_remaining_bits() { let bits = BitString::try_from("101001").unwrap(); - let v = bits.as_bitstr(); + let v = bits.as_bit_str(); let mut iter = v.iter(); assert_eq!(iter.size_hint(), (6, Some(6))); @@ -63,7 +63,7 @@ fn size_hint_tracks_remaining_bits() { #[test] fn remains_fused_after_exhaustion() { let bits = BitString::try_from("10").unwrap(); - let v = bits.as_bitstr(); + let v = bits.as_bit_str(); let mut iter = v.iter(); assert_eq!(iter.next(), Some(true)); @@ -77,7 +77,7 @@ fn remains_fused_after_exhaustion() { #[test] fn borrowed_into_iter_uses_bit_iterator() { let bits = BitString::try_from("1001").unwrap(); - let v = bits.as_bitstr(); + let v = bits.as_bit_str(); let values: Vec<_> = (&v).into_iter().collect(); @@ -94,7 +94,7 @@ fn works_across_word_boundaries() { bits.set(65, true); bits.set(129, true); - let v = bits.as_bitstr(); + let v = bits.as_bit_str(); let values: Vec<_> = v.iter().enumerate().map(|(i, b)| (i, b)).collect(); assert_eq!(values.len(), 130); @@ -115,7 +115,7 @@ fn iterates_offset_view() { let bits = BitString::try_from("101100").unwrap(); // bits: 1 0 1 1 0 0 // view bits 1..5 → 0 1 1 0 - let v = bits.as_bitstr().slice(UsizeCO::try_new(1, 5).unwrap()); + let v = bits.as_bit_str().slice(UsizeCO::try_new(1, 5).unwrap()); let values: Vec<_> = v.iter().collect(); assert_eq!(values, [false, true, true, false]); @@ -125,7 +125,7 @@ fn iterates_offset_view() { fn double_ended_on_offset_view() { let bits = BitString::try_from("101100").unwrap(); // view bits 2..6 → 1 1 0 0 - let v = bits.as_bitstr().slice(UsizeCO::try_new(2, 6).unwrap()); + let v = bits.as_bit_str().slice(UsizeCO::try_new(2, 6).unwrap()); let mut iter = v.iter(); assert_eq!(iter.next(), Some(true)); @@ -138,7 +138,7 @@ fn double_ended_on_offset_view() { #[test] fn into_iter_on_offset_view() { let bits = BitString::try_from("101100").unwrap(); - let v = bits.as_bitstr().slice(UsizeCO::try_new(1, 4).unwrap()); + let v = bits.as_bit_str().slice(UsizeCO::try_new(1, 4).unwrap()); let values: Vec<_> = (&v).into_iter().collect(); assert_eq!(values, [false, true, true]); @@ -147,7 +147,7 @@ fn into_iter_on_offset_view() { #[test] fn to_bool_vec_on_full_view() { let bits = BitString::try_from("11010").unwrap(); - let v = bits.as_bitstr(); + let v = bits.as_bit_str(); assert_eq!(v.to_bool_vec(), [true, true, false, true, false]); } @@ -156,7 +156,7 @@ fn to_bool_vec_on_full_view() { fn to_bool_vec_on_offset_view() { let bits = BitString::try_from("11010").unwrap(); // view bits 1..4 → 1 0 1 - let v = bits.as_bitstr().slice(UsizeCO::try_new(1, 4).unwrap()); + let v = bits.as_bit_str().slice(UsizeCO::try_new(1, 4).unwrap()); assert_eq!(v.to_bool_vec(), [true, false, true]); } @@ -164,7 +164,7 @@ fn to_bool_vec_on_offset_view() { #[test] fn for_loop_over_view() { let bits = BitString::try_from("101").unwrap(); - let v = bits.as_bitstr(); + let v = bits.as_bit_str(); let mut acc = Vec::new(); for b in &v { @@ -176,7 +176,7 @@ fn for_loop_over_view() { #[test] fn exact_size_is_correct() { let bits = BitString::try_from("1010011100").unwrap(); - let v = bits.as_bitstr(); + let v = bits.as_bit_str(); let mut iter = v.iter(); assert_eq!(iter.len(), 10); diff --git a/src/bit_str/impls_for_matching/impls_for_find.rs b/src/bit_str/impls_for_matching/impls_for_find.rs index 3033688..2b134bd 100644 --- a/src/bit_str/impls_for_matching/impls_for_find.rs +++ b/src/bit_str/impls_for_matching/impls_for_find.rs @@ -6,7 +6,7 @@ use crate::BitStr; impl<'bs> BitStr<'bs> { /// Returns `true` if `needle` is contained within `self`. #[inline] - pub fn contains(&self, needle: &BitStr<'_>) -> bool { + pub fn contains(&self, needle: BitStr<'_>) -> bool { if needle.bit_len == 0 { return true; } @@ -52,7 +52,7 @@ impl<'bs> BitStr<'bs> { /// Returns the index of the first occurrence of `needle`, or `None`. #[inline] - pub fn find(&self, needle: &BitStr<'_>) -> Option { + pub fn find(&self, needle: BitStr<'_>) -> Option { if needle.bit_len == 0 { return Some(0); } @@ -112,7 +112,7 @@ impl<'bs> BitStr<'bs> { /// Returns the index of the last occurrence of `needle`, or `None`. #[inline] - pub fn rfind(&self, needle: &BitStr<'_>) -> Option { + pub fn rfind(&self, needle: BitStr<'_>) -> Option { if needle.bit_len == 0 { return Some(self.bit_len); } diff --git a/src/bit_str/impls_for_matching/impls_for_find/tests_for_find.rs b/src/bit_str/impls_for_matching/impls_for_find/tests_for_find.rs index eef91e0..7611d64 100644 --- a/src/bit_str/impls_for_matching/impls_for_find/tests_for_find.rs +++ b/src/bit_str/impls_for_matching/impls_for_find/tests_for_find.rs @@ -7,37 +7,37 @@ use crate::BitString; #[test] fn contains_basic() { let bits = BitString::try_from("101100").unwrap(); - let v = bits.as_bitstr(); + let v = bits.as_bit_str(); let bs = BitString::try_from("01").unwrap(); - assert!(v.contains(&bs.as_bitstr())); + assert!(v.contains(bs.as_bit_str())); let bs = BitString::try_from("111").unwrap(); - assert!(!v.contains(&bs.as_bitstr())); + assert!(!v.contains(bs.as_bit_str())); } #[test] fn contains_empty_needle() { let bits = BitString::try_from("10110").unwrap(); - let v = bits.as_bitstr(); + let v = bits.as_bit_str(); let empty = BitString::new(); - assert!(v.contains(&empty.as_bitstr())); + assert!(v.contains(empty.as_bit_str())); } #[test] fn contains_longer_than_view_returns_false() { let bits = BitString::try_from("10").unwrap(); - let v = bits.as_bitstr(); + let v = bits.as_bit_str(); let bs = BitString::try_from("101").unwrap(); - assert!(!v.contains(&bs.as_bitstr())); + assert!(!v.contains(bs.as_bit_str())); } #[test] fn contains_on_offset_view() { let bits = BitString::try_from("110010").unwrap(); - let v = bits.as_bitstr().slice_from(1).slice_until(5); + let v = bits.as_bit_str().slice_from(1).slice_until(5); let bs = BitString::try_from("001").unwrap(); - assert!(v.contains(&bs.as_bitstr())); + assert!(v.contains(bs.as_bit_str())); let bs = BitString::try_from("11").unwrap(); - assert!(!v.contains(&bs.as_bitstr())); + assert!(!v.contains(bs.as_bit_str())); } // --------------------------------------------------------------------------- @@ -47,47 +47,47 @@ fn contains_on_offset_view() { #[test] fn find_first_occurrence() { let bits = BitString::try_from("10110010").unwrap(); - let v = bits.as_bitstr(); + let v = bits.as_bit_str(); let bs = BitString::try_from("10").unwrap(); - assert_eq!(v.find(&bs.as_bitstr()), Some(0)); + assert_eq!(v.find(bs.as_bit_str()), Some(0)); let bs = BitString::try_from("01").unwrap(); - assert_eq!(v.find(&bs.as_bitstr()), Some(1)); + assert_eq!(v.find(bs.as_bit_str()), Some(1)); let bs = BitString::try_from("111").unwrap(); - assert_eq!(v.find(&bs.as_bitstr()), None); + assert_eq!(v.find(bs.as_bit_str()), None); } #[test] fn find_empty_needle_returns_zero() { let bits = BitString::try_from("10110").unwrap(); - let v = bits.as_bitstr(); + let v = bits.as_bit_str(); let empty = BitString::new(); - assert_eq!(v.find(&empty.as_bitstr()), Some(0)); + assert_eq!(v.find(empty.as_bit_str()), Some(0)); } #[test] fn find_longer_than_view_returns_none() { let bits = BitString::try_from("10").unwrap(); - let v = bits.as_bitstr(); + let v = bits.as_bit_str(); let bs = BitString::try_from("101").unwrap(); - assert_eq!(v.find(&bs.as_bitstr()), None); + assert_eq!(v.find(bs.as_bit_str()), None); } #[test] fn find_on_offset_view() { let bits = BitString::try_from("11001001").unwrap(); - let v = bits.as_bitstr().slice_from(2).slice_until(7); + let v = bits.as_bit_str().slice_from(2).slice_until(7); let bs = BitString::try_from("10").unwrap(); - assert_eq!(v.find(&bs.as_bitstr()), Some(2)); + assert_eq!(v.find(bs.as_bit_str()), Some(2)); let bs = BitString::try_from("00").unwrap(); - assert_eq!(v.find(&bs.as_bitstr()), Some(0)); + assert_eq!(v.find(bs.as_bit_str()), Some(0)); } #[test] fn find_at_end_of_view() { let bits = BitString::try_from("11100").unwrap(); - let v = bits.as_bitstr(); + let v = bits.as_bit_str(); let bs = BitString::try_from("00").unwrap(); - assert_eq!(v.find(&bs.as_bitstr()), Some(3)); + assert_eq!(v.find(bs.as_bit_str()), Some(3)); } // --------------------------------------------------------------------------- @@ -97,40 +97,40 @@ fn find_at_end_of_view() { #[test] fn rfind_last_occurrence() { let bits = BitString::try_from("10110010").unwrap(); - let v = bits.as_bitstr(); + let v = bits.as_bit_str(); let bs = BitString::try_from("10").unwrap(); - assert_eq!(v.rfind(&bs.as_bitstr()), Some(6)); + assert_eq!(v.rfind(bs.as_bit_str()), Some(6)); } #[test] fn rfind_empty_needle_returns_bit_len() { let bits = BitString::try_from("10110").unwrap(); - let v = bits.as_bitstr(); + let v = bits.as_bit_str(); let empty = BitString::new(); - assert_eq!(v.rfind(&empty.as_bitstr()), Some(5)); + assert_eq!(v.rfind(empty.as_bit_str()), Some(5)); } #[test] fn rfind_longer_than_view_returns_none() { let bits = BitString::try_from("10").unwrap(); - let v = bits.as_bitstr(); + let v = bits.as_bit_str(); let bs = BitString::try_from("101").unwrap(); - assert_eq!(v.rfind(&bs.as_bitstr()), None); + assert_eq!(v.rfind(bs.as_bit_str()), None); } #[test] fn rfind_on_offset_view() { let bits = BitString::try_from("11001001").unwrap(); - let v = bits.as_bitstr().slice_from(2).slice_until(7); + let v = bits.as_bit_str().slice_from(2).slice_until(7); let bs = BitString::try_from("00").unwrap(); - assert_eq!(v.rfind(&bs.as_bitstr()), Some(3)); + assert_eq!(v.rfind(bs.as_bit_str()), Some(3)); } #[test] fn find_and_rfind_needle_appears_once() { let bits = BitString::try_from("11010").unwrap(); - let v = bits.as_bitstr(); + let v = bits.as_bit_str(); let bs = BitString::try_from("101").unwrap(); - assert_eq!(v.find(&bs.as_bitstr()), Some(1)); - assert_eq!(v.rfind(&bs.as_bitstr()), Some(1)); + assert_eq!(v.find(bs.as_bit_str()), Some(1)); + assert_eq!(v.rfind(bs.as_bit_str()), Some(1)); } diff --git a/src/bit_str/impls_for_matching/impls_for_matches_at.rs b/src/bit_str/impls_for_matching/impls_for_matches_at.rs index 7791f41..e378d11 100644 --- a/src/bit_str/impls_for_matching/impls_for_matches_at.rs +++ b/src/bit_str/impls_for_matching/impls_for_matches_at.rs @@ -10,7 +10,7 @@ impl<'bs> BitStr<'bs> { /// SIMD word-equality via [`BitsEq::eq_words`]. Otherwise falls /// back to a scalar word-at-a-time comparison. #[inline] - pub(crate) fn bits_equal_at(&self, offset: usize, needle: &BitStr<'_>) -> bool { + pub(crate) fn bits_equal_at(&self, offset: usize, needle: BitStr<'_>) -> bool { let n = needle.bit_len; if n == 0 { return true; @@ -77,7 +77,7 @@ impl<'bs> BitStr<'bs> { /// Returns `true` if `pattern` matches the bits starting at `index`. #[inline] - pub fn matches_at(&self, index: usize, pattern: &BitStr<'_>) -> bool { + pub fn matches_at(&self, index: usize, pattern: BitStr<'_>) -> bool { if index > self.bit_len { return false; } @@ -89,13 +89,13 @@ impl<'bs> BitStr<'bs> { /// Returns `true` if `prefix` is a prefix of `self`. #[inline] - pub fn starts_with(&self, prefix: &BitStr<'_>) -> bool { + pub fn starts_with(&self, prefix: BitStr<'_>) -> bool { self.matches_at(0, prefix) } /// Returns `true` if `suffix` is a suffix of `self`. #[inline] - pub fn ends_with(&self, suffix: &BitStr<'_>) -> bool { + pub fn ends_with(&self, suffix: BitStr<'_>) -> bool { if suffix.bit_len == 0 { return true; } @@ -106,5 +106,14 @@ impl<'bs> BitStr<'bs> { } } +#[cfg(test)] +mod tests_for_bits_equal_at; + +#[cfg(test)] +mod tests_for_ends_with; + #[cfg(test)] mod tests_for_matches_at; + +#[cfg(test)] +mod tests_for_starts_with; diff --git a/src/bit_str/impls_for_matching/impls_for_matches_at/tests_for_bits_equal_at.rs b/src/bit_str/impls_for_matching/impls_for_matches_at/tests_for_bits_equal_at.rs new file mode 100644 index 0000000..3957159 --- /dev/null +++ b/src/bit_str/impls_for_matching/impls_for_matches_at/tests_for_bits_equal_at.rs @@ -0,0 +1,56 @@ +use crate::BitString; + +#[test] +fn returns_true_when_needle_matches_at_offset() { + let haystack = BitString::try_from("00110110").unwrap(); + let haystack = haystack.as_bit_str(); + let needle = BitString::try_from("110").unwrap(); + + assert!(haystack.bits_equal_at(2, needle.as_bit_str())); + assert!(haystack.bits_equal_at(5, needle.as_bit_str())); +} + +#[test] +fn returns_false_when_needle_differs_at_offset() { + let haystack = BitString::try_from("00110110").unwrap(); + let haystack = haystack.as_bit_str(); + let needle = BitString::try_from("110").unwrap(); + + assert!(!haystack.bits_equal_at(0, needle.as_bit_str())); + assert!(!haystack.bits_equal_at(1, needle.as_bit_str())); + assert!(!haystack.bits_equal_at(3, needle.as_bit_str())); +} + +#[test] +fn empty_needle_matches_at_valid_boundary_offsets() { + let haystack = BitString::try_from("101001").unwrap(); + let haystack = haystack.as_bit_str(); + let needle = BitString::new(); + + assert!(haystack.bits_equal_at(0, needle.as_bit_str())); + assert!(haystack.bits_equal_at(3, needle.as_bit_str())); + assert!(haystack.bits_equal_at(haystack.bit_len(), 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 haystack = bits.as_bit_str(); + let needle = BitString::try_from("01110").unwrap(); + + assert!(haystack.bits_equal_at(62, needle.as_bit_str())); + assert!(!haystack.bits_equal_at(61, needle.as_bit_str())); +} + +#[test] +fn works_when_needle_reaches_haystack_end() { + let haystack = BitString::try_from("101001").unwrap(); + let haystack = haystack.as_bit_str(); + let needle = BitString::try_from("001").unwrap(); + + assert!(haystack.bits_equal_at(3, needle.as_bit_str())); +} diff --git a/src/bit_str/impls_for_matching/impls_for_matches_at/tests_for_ends_with.rs b/src/bit_str/impls_for_matching/impls_for_matches_at/tests_for_ends_with.rs new file mode 100644 index 0000000..e54acc2 --- /dev/null +++ b/src/bit_str/impls_for_matching/impls_for_matches_at/tests_for_ends_with.rs @@ -0,0 +1,56 @@ +use crate::BitString; + +// --------------------------------------------------------------------------- +// ends_with +// --------------------------------------------------------------------------- + +#[test] +fn ends_with_basic() { + let bits = BitString::try_from("101100").unwrap(); + let v = bits.as_bit_str(); + let s = BitString::try_from("100").unwrap(); + assert!(v.ends_with(s.as_bit_str())); + let s = BitString::try_from("10").unwrap(); + assert!(!v.ends_with(s.as_bit_str())); + let s = BitString::try_from("101100").unwrap(); + assert!(v.ends_with(s.as_bit_str())); +} + +#[test] +fn ends_with_empty_suffix() { + let bits = BitString::try_from("10110").unwrap(); + let v = bits.as_bit_str(); + let s = BitString::new(); + assert!(v.ends_with(s.as_bit_str())); +} + +#[test] +fn ends_with_longer_suffix_returns_false() { + let bits = BitString::try_from("101").unwrap(); + let v = bits.as_bit_str(); + let s = BitString::try_from("1101").unwrap(); + assert!(!v.ends_with(s.as_bit_str())); +} + +#[test] +fn ends_with_on_offset_view() { + let bits = BitString::try_from("110101").unwrap(); + // bits 1..6 → 1 0 1 0 1 + let v = bits.as_bit_str().slice_from(1).slice_until(5); + let s = BitString::try_from("01").unwrap(); + assert!(v.ends_with(s.as_bit_str())); + let s = BitString::try_from("10").unwrap(); + assert!(!v.ends_with(s.as_bit_str())); +} + +#[test] +fn matches_and_ends_with_across_word_boundaries() { + let mut bits = BitString::zeros(130); + bits.set(62, true); + bits.set(63, true); + bits.set(64, true); + bits.set(65, true); + let v = bits.as_bit_str(); + let p = BitString::try_from("1111").unwrap(); + assert!(v.matches_at(62, p.as_bit_str())); +} diff --git a/src/bit_str/impls_for_matching/impls_for_matches_at/tests_for_matches_at.rs b/src/bit_str/impls_for_matching/impls_for_matches_at/tests_for_matches_at.rs index b5e6e7c..922c5f8 100644 --- a/src/bit_str/impls_for_matching/impls_for_matches_at/tests_for_matches_at.rs +++ b/src/bit_str/impls_for_matching/impls_for_matches_at/tests_for_matches_at.rs @@ -7,139 +7,42 @@ use crate::BitString; #[test] fn matches_at_exact_match() { let bits = BitString::try_from("101100").unwrap(); - let v = bits.as_bitstr(); + let v = bits.as_bit_str(); let p = BitString::try_from("10").unwrap(); - assert!(v.matches_at(0, &p.as_bitstr())); - assert!(!v.matches_at(1, &p.as_bitstr())); + assert!(v.matches_at(0, p.as_bit_str())); + assert!(!v.matches_at(1, p.as_bit_str())); } #[test] fn matches_at_beyond_view_returns_false() { let bits = BitString::try_from("10110").unwrap(); - let v = bits.as_bitstr().slice_from(2); + let v = bits.as_bit_str().slice_from(2); let p = BitString::try_from("10").unwrap(); - assert!(!v.matches_at(2, &p.as_bitstr())); + assert!(!v.matches_at(2, p.as_bit_str())); } #[test] fn matches_at_pattern_too_long_returns_false() { let bits = BitString::try_from("10").unwrap(); - let v = bits.as_bitstr(); + let v = bits.as_bit_str(); let p = BitString::try_from("100").unwrap(); - assert!(!v.matches_at(0, &p.as_bitstr())); + assert!(!v.matches_at(0, p.as_bit_str())); } #[test] fn matches_at_empty_pattern_always_true() { let bits = BitString::try_from("10110").unwrap(); - let v = bits.as_bitstr(); + let v = bits.as_bit_str(); let p = BitString::new(); - assert!(v.matches_at(0, &p.as_bitstr())); - assert!(v.matches_at(3, &p.as_bitstr())); + assert!(v.matches_at(0, p.as_bit_str())); + assert!(v.matches_at(3, p.as_bit_str())); } #[test] fn matches_at_on_offset_view() { let bits = BitString::try_from("11100011").unwrap(); // bits: 1 1 1 0 0 0 1 1, view 2..7 → 1 0 0 0 1 - let v = bits.as_bitstr().slice_from(2).slice_until(7); + let v = bits.as_bit_str().slice_from(2).slice_until(7); let p = BitString::try_from("0001").unwrap(); - assert!(v.matches_at(1, &p.as_bitstr())); -} - -// --------------------------------------------------------------------------- -// starts_with -// --------------------------------------------------------------------------- - -#[test] -fn starts_with_basic() { - let bits = BitString::try_from("101100").unwrap(); - let v = bits.as_bitstr(); - let p = BitString::try_from("101").unwrap(); - assert!(v.starts_with(&p.as_bitstr())); - let p = BitString::try_from("11").unwrap(); - assert!(!v.starts_with(&p.as_bitstr())); - let p = BitString::try_from("101100").unwrap(); - assert!(v.starts_with(&p.as_bitstr())); -} - -#[test] -fn starts_with_empty_prefix() { - let bits = BitString::try_from("10110").unwrap(); - let v = bits.as_bitstr(); - let p = BitString::new(); - assert!(v.starts_with(&p.as_bitstr())); -} - -#[test] -fn starts_with_longer_prefix_returns_false() { - let bits = BitString::try_from("101").unwrap(); - let v = bits.as_bitstr(); - let p = BitString::try_from("1011").unwrap(); - assert!(!v.starts_with(&p.as_bitstr())); -} - -#[test] -fn starts_with_on_offset_view() { - let bits = BitString::try_from("110101").unwrap(); - let v = bits.as_bitstr().slice_from(1).slice_until(5); - let p = BitString::try_from("10").unwrap(); - assert!(v.starts_with(&p.as_bitstr())); - let p = BitString::try_from("11").unwrap(); - assert!(!v.starts_with(&p.as_bitstr())); -} - -// --------------------------------------------------------------------------- -// ends_with -// --------------------------------------------------------------------------- - -#[test] -fn ends_with_basic() { - let bits = BitString::try_from("101100").unwrap(); - let v = bits.as_bitstr(); - let s = BitString::try_from("100").unwrap(); - assert!(v.ends_with(&s.as_bitstr())); - let s = BitString::try_from("10").unwrap(); - assert!(!v.ends_with(&s.as_bitstr())); - let s = BitString::try_from("101100").unwrap(); - assert!(v.ends_with(&s.as_bitstr())); -} - -#[test] -fn ends_with_empty_suffix() { - let bits = BitString::try_from("10110").unwrap(); - let v = bits.as_bitstr(); - let s = BitString::new(); - assert!(v.ends_with(&s.as_bitstr())); -} - -#[test] -fn ends_with_longer_suffix_returns_false() { - let bits = BitString::try_from("101").unwrap(); - let v = bits.as_bitstr(); - let s = BitString::try_from("1101").unwrap(); - assert!(!v.ends_with(&s.as_bitstr())); -} - -#[test] -fn ends_with_on_offset_view() { - let bits = BitString::try_from("110101").unwrap(); - // bits 1..6 → 1 0 1 0 1 - let v = bits.as_bitstr().slice_from(1).slice_until(5); - let s = BitString::try_from("01").unwrap(); - assert!(v.ends_with(&s.as_bitstr())); - let s = BitString::try_from("10").unwrap(); - assert!(!v.ends_with(&s.as_bitstr())); -} - -#[test] -fn matches_and_ends_with_across_word_boundaries() { - let mut bits = BitString::zeros(130); - bits.set(62, true); - bits.set(63, true); - bits.set(64, true); - bits.set(65, true); - let v = bits.as_bitstr(); - let p = BitString::try_from("1111").unwrap(); - assert!(v.matches_at(62, &p.as_bitstr())); + assert!(v.matches_at(1, p.as_bit_str())); } diff --git a/src/bit_str/impls_for_matching/impls_for_matches_at/tests_for_starts_with.rs b/src/bit_str/impls_for_matching/impls_for_matches_at/tests_for_starts_with.rs new file mode 100644 index 0000000..2e9cfd4 --- /dev/null +++ b/src/bit_str/impls_for_matching/impls_for_matches_at/tests_for_starts_with.rs @@ -0,0 +1,43 @@ +use crate::BitString; + +// --------------------------------------------------------------------------- +// starts_with +// --------------------------------------------------------------------------- + +#[test] +fn starts_with_basic() { + let bits = BitString::try_from("101100").unwrap(); + let v = bits.as_bit_str(); + let p = BitString::try_from("101").unwrap(); + assert!(v.starts_with(p.as_bit_str())); + let p = BitString::try_from("11").unwrap(); + assert!(!v.starts_with(p.as_bit_str())); + let p = BitString::try_from("101100").unwrap(); + assert!(v.starts_with(p.as_bit_str())); +} + +#[test] +fn starts_with_empty_prefix() { + let bits = BitString::try_from("10110").unwrap(); + let v = bits.as_bit_str(); + let p = BitString::new(); + assert!(v.starts_with(p.as_bit_str())); +} + +#[test] +fn starts_with_longer_prefix_returns_false() { + let bits = BitString::try_from("101").unwrap(); + let v = bits.as_bit_str(); + let p = BitString::try_from("1011").unwrap(); + assert!(!v.starts_with(p.as_bit_str())); +} + +#[test] +fn starts_with_on_offset_view() { + let bits = BitString::try_from("110101").unwrap(); + let v = bits.as_bit_str().slice_from(1).slice_until(5); + let p = BitString::try_from("10").unwrap(); + assert!(v.starts_with(p.as_bit_str())); + let p = BitString::try_from("11").unwrap(); + assert!(!v.starts_with(p.as_bit_str())); +} diff --git a/src/bit_str/impls_for_matching/impls_for_strip.rs b/src/bit_str/impls_for_matching/impls_for_strip.rs index 45ac3ef..e8c1352 100644 --- a/src/bit_str/impls_for_matching/impls_for_strip.rs +++ b/src/bit_str/impls_for_matching/impls_for_strip.rs @@ -3,14 +3,14 @@ use crate::BitStr; impl<'bs> BitStr<'bs> { /// Strips `prefix` from the start, returning the remaining sub-view. #[inline] - pub fn strip_prefix(&self, prefix: &BitStr<'_>) -> Option { + pub fn strip_prefix(&self, prefix: BitStr<'_>) -> Option { self.starts_with(prefix) .then(|| self.slice_from(prefix.bit_len)) } /// Strips `suffix` from the end, returning the remaining sub-view. #[inline] - pub fn strip_suffix(&self, suffix: &BitStr<'_>) -> Option { + pub fn strip_suffix(&self, suffix: BitStr<'_>) -> Option { self.ends_with(suffix) .then(|| self.slice_until(self.bit_len - suffix.bit_len)) } diff --git a/src/bit_str/impls_for_matching/impls_for_strip/tests_for_strip.rs b/src/bit_str/impls_for_matching/impls_for_strip/tests_for_strip.rs index 8f29663..1f7369d 100644 --- a/src/bit_str/impls_for_matching/impls_for_strip/tests_for_strip.rs +++ b/src/bit_str/impls_for_matching/impls_for_strip/tests_for_strip.rs @@ -7,9 +7,9 @@ use crate::BitString; #[test] fn strip_prefix_removes_matching_prefix() { let bits = BitString::try_from("101100").unwrap(); - let v = bits.as_bitstr(); + let v = bits.as_bit_str(); let p = BitString::try_from("101").unwrap(); - let rest = v.strip_prefix(&p.as_bitstr()).unwrap(); + let rest = v.strip_prefix(p.as_bit_str()).unwrap(); assert_eq!(rest.bit_len(), 3); assert_eq!(rest.get(0), Some(true)); assert_eq!(rest.get(1), Some(false)); @@ -19,26 +19,26 @@ fn strip_prefix_removes_matching_prefix() { #[test] fn strip_prefix_non_matching_returns_none() { let bits = BitString::try_from("101100").unwrap(); - let v = bits.as_bitstr(); + let v = bits.as_bit_str(); let p = BitString::try_from("11").unwrap(); - assert!(v.strip_prefix(&p.as_bitstr()).is_none()); + assert!(v.strip_prefix(p.as_bit_str()).is_none()); } #[test] fn strip_prefix_empty() { let bits = BitString::try_from("10110").unwrap(); - let v = bits.as_bitstr(); + let v = bits.as_bit_str(); let p = BitString::new(); - let rest = v.strip_prefix(&p.as_bitstr()).unwrap(); + let rest = v.strip_prefix(p.as_bit_str()).unwrap(); assert_eq!(rest.bit_len(), v.bit_len()); } #[test] fn strip_prefix_entire_view() { let bits = BitString::try_from("101").unwrap(); - let v = bits.as_bitstr(); + let v = bits.as_bit_str(); let p = BitString::try_from("101").unwrap(); - let rest = v.strip_prefix(&p.as_bitstr()).unwrap(); + let rest = v.strip_prefix(p.as_bit_str()).unwrap(); assert_eq!(rest.bit_len(), 0); } @@ -46,9 +46,9 @@ fn strip_prefix_entire_view() { fn strip_prefix_on_offset_view() { let bits = BitString::try_from("110101").unwrap(); // view bits 1..6 → 1 0 1 0 1 - let v = bits.as_bitstr().slice_from(1).slice_until(5); + let v = bits.as_bit_str().slice_from(1).slice_until(5); let p = BitString::try_from("10").unwrap(); - let rest = v.strip_prefix(&p.as_bitstr()).unwrap(); + let rest = v.strip_prefix(p.as_bit_str()).unwrap(); assert_eq!(rest.bit_len(), 3); assert_eq!(rest.get(0), Some(true)); assert_eq!(rest.get(1), Some(false)); @@ -62,9 +62,9 @@ fn strip_prefix_on_offset_view() { #[test] fn strip_suffix_removes_matching_suffix() { let bits = BitString::try_from("101100").unwrap(); - let v = bits.as_bitstr(); + let v = bits.as_bit_str(); let s = BitString::try_from("100").unwrap(); - let rest = v.strip_suffix(&s.as_bitstr()).unwrap(); + let rest = v.strip_suffix(s.as_bit_str()).unwrap(); assert_eq!(rest.bit_len(), 3); assert_eq!(rest.get(0), Some(true)); assert_eq!(rest.get(1), Some(false)); @@ -74,26 +74,26 @@ fn strip_suffix_removes_matching_suffix() { #[test] fn strip_suffix_non_matching_returns_none() { let bits = BitString::try_from("101100").unwrap(); - let v = bits.as_bitstr(); + let v = bits.as_bit_str(); let s = BitString::try_from("10").unwrap(); - assert!(v.strip_suffix(&s.as_bitstr()).is_none()); + assert!(v.strip_suffix(s.as_bit_str()).is_none()); } #[test] fn strip_suffix_empty() { let bits = BitString::try_from("10110").unwrap(); - let v = bits.as_bitstr(); + let v = bits.as_bit_str(); let s = BitString::new(); - let rest = v.strip_suffix(&s.as_bitstr()).unwrap(); + let rest = v.strip_suffix(s.as_bit_str()).unwrap(); assert_eq!(rest.bit_len(), v.bit_len()); } #[test] fn strip_suffix_entire_view() { let bits = BitString::try_from("101").unwrap(); - let v = bits.as_bitstr(); + let v = bits.as_bit_str(); let s = BitString::try_from("101").unwrap(); - let rest = v.strip_suffix(&s.as_bitstr()).unwrap(); + let rest = v.strip_suffix(s.as_bit_str()).unwrap(); assert_eq!(rest.bit_len(), 0); } @@ -101,9 +101,9 @@ fn strip_suffix_entire_view() { fn strip_suffix_on_offset_view() { let bits = BitString::try_from("110101").unwrap(); // view bits 1..6 → 1 0 1 0 1 - let v = bits.as_bitstr().slice_from(1).slice_until(5); + let v = bits.as_bit_str().slice_from(1).slice_until(5); let s = BitString::try_from("01").unwrap(); - let rest = v.strip_suffix(&s.as_bitstr()).unwrap(); + let rest = v.strip_suffix(s.as_bit_str()).unwrap(); assert_eq!(rest.bit_len(), 3); assert_eq!(rest.get(0), Some(true)); assert_eq!(rest.get(1), Some(false)); diff --git a/src/bit_str/impls_for_slice/tests_for_slice.rs b/src/bit_str/impls_for_slice/tests_for_slice.rs index 47c3797..b6f3b27 100644 --- a/src/bit_str/impls_for_slice/tests_for_slice.rs +++ b/src/bit_str/impls_for_slice/tests_for_slice.rs @@ -9,7 +9,7 @@ use crate::BitString; #[test] fn slice_full_range_returns_identical_view() { let bits = BitString::try_from("1010011100").unwrap(); - let v = bits.as_bitstr(); + let v = bits.as_bit_str(); let s = v.slice(UsizeCO::try_new(0, 10).unwrap()); assert_eq!(s.bit_len(), v.bit_len()); @@ -23,7 +23,7 @@ fn slice_full_range_returns_identical_view() { #[test] fn slice_subrange_within_bounds() { let bits = BitString::try_from("11110000").unwrap(); - let v = bits.as_bitstr(); + let v = bits.as_bit_str(); // Slice bits 2..6 → "1100" let s = v.slice(UsizeCO::try_new(2, 6).unwrap()); @@ -37,7 +37,7 @@ fn slice_subrange_within_bounds() { #[test] fn slice_end_beyond_view_is_clamped() { let bits = BitString::try_from("10110").unwrap(); - let v = bits.as_bitstr(); + let v = bits.as_bit_str(); // [2, 100) → clamped to [2, 5) let s = v.slice(UsizeCO::try_new(2, 100).unwrap()); @@ -50,7 +50,7 @@ fn slice_end_beyond_view_is_clamped() { #[test] fn slice_start_beyond_view_is_empty() { let bits = BitString::try_from("10110").unwrap(); - let v = bits.as_bitstr(); + let v = bits.as_bit_str(); // [10, 20) → start clamped to 5, end clamped to 5 → empty let s = v.slice(UsizeCO::try_new(10, 20).unwrap()); @@ -60,7 +60,7 @@ fn slice_start_beyond_view_is_empty() { #[test] fn slice_single_bit() { let bits = BitString::try_from("10110").unwrap(); - let v = bits.as_bitstr(); + let v = bits.as_bit_str(); let s = v.slice(UsizeCO::try_new(0, 1).unwrap()); assert_eq!(s.bit_len(), 1); @@ -70,7 +70,7 @@ fn slice_single_bit() { #[test] fn slice_last_bit() { let bits = BitString::try_from("10110").unwrap(); - let v = bits.as_bitstr(); + let v = bits.as_bit_str(); let s = v.slice(UsizeCO::try_new(4, 5).unwrap()); assert_eq!(s.bit_len(), 1); @@ -80,7 +80,7 @@ fn slice_last_bit() { #[test] fn slice_chaining() { let bits = BitString::try_from("11110000").unwrap(); - let v = bits.as_bitstr(); + let v = bits.as_bit_str(); // First slice bits 2..7 → "11000" let s1 = v.slice(UsizeCO::try_new(2, 7).unwrap()); @@ -101,7 +101,7 @@ fn slice_chaining() { #[test] fn slice_from_zero_returns_identical_view() { let bits = BitString::try_from("1010011100").unwrap(); - let v = bits.as_bitstr(); + let v = bits.as_bit_str(); let s = v.slice_from(0); assert_eq!(s.bit_len(), v.bit_len()); @@ -111,7 +111,7 @@ fn slice_from_zero_returns_identical_view() { #[test] fn slice_from_mid() { let bits = BitString::try_from("11110000").unwrap(); - let v = bits.as_bitstr(); + let v = bits.as_bit_str(); let s = v.slice_from(4); assert_eq!(s.bit_len(), 4); @@ -124,7 +124,7 @@ fn slice_from_mid() { #[test] fn slice_from_returns_empty_at_boundary() { let bits = BitString::try_from("10110").unwrap(); - let v = bits.as_bitstr(); + let v = bits.as_bit_str(); let s = v.slice_from(5); assert_eq!(s.bit_len(), 0); @@ -133,7 +133,7 @@ fn slice_from_returns_empty_at_boundary() { #[test] fn slice_from_beyond_boundary_returns_empty() { let bits = BitString::try_from("10110").unwrap(); - let v = bits.as_bitstr(); + let v = bits.as_bit_str(); let s = v.slice_from(100); assert_eq!(s.bit_len(), 0); @@ -142,7 +142,7 @@ fn slice_from_beyond_boundary_returns_empty() { #[test] fn slice_from_chaining() { let bits = BitString::try_from("11110000").unwrap(); - let v = bits.as_bitstr(); + let v = bits.as_bit_str(); // slice_from(2) → bits 2..8 → "110000" let s1 = v.slice_from(2); @@ -163,7 +163,7 @@ fn slice_from_chaining() { #[test] fn slice_until_full_returns_identical_view() { let bits = BitString::try_from("1010011100").unwrap(); - let v = bits.as_bitstr(); + let v = bits.as_bit_str(); let s = v.slice_until(10); assert_eq!(s.bit_len(), v.bit_len()); @@ -173,7 +173,7 @@ fn slice_until_full_returns_identical_view() { #[test] fn slice_until_mid() { let bits = BitString::try_from("11110000").unwrap(); - let v = bits.as_bitstr(); + let v = bits.as_bit_str(); let s = v.slice_until(4); assert_eq!(s.bit_len(), 4); @@ -186,7 +186,7 @@ fn slice_until_mid() { #[test] fn slice_until_zero_returns_empty() { let bits = BitString::try_from("10110").unwrap(); - let v = bits.as_bitstr(); + let v = bits.as_bit_str(); let s = v.slice_until(0); assert_eq!(s.bit_len(), 0); @@ -195,7 +195,7 @@ fn slice_until_zero_returns_empty() { #[test] fn slice_until_beyond_boundary_is_clamped() { let bits = BitString::try_from("10110").unwrap(); - let v = bits.as_bitstr(); + let v = bits.as_bit_str(); let s = v.slice_until(100); assert_eq!(s.bit_len(), 5); @@ -204,7 +204,7 @@ fn slice_until_beyond_boundary_is_clamped() { #[test] fn slice_until_chaining() { let bits = BitString::try_from("11110000").unwrap(); - let v = bits.as_bitstr(); + let v = bits.as_bit_str(); // slice_until(6) → bits 0..6 → "111100" let s1 = v.slice_until(6); @@ -225,7 +225,7 @@ fn slice_until_chaining() { #[test] fn mixed_slice_and_slice_from() { let bits = BitString::try_from("1011010011").unwrap(); - let v = bits.as_bitstr(); + let v = bits.as_bit_str(); // slice(2..8) → bits 2..8 → "110100" let s1 = v.slice(UsizeCO::try_new(2, 8).unwrap()); @@ -239,7 +239,7 @@ fn mixed_slice_and_slice_from() { #[test] fn mixed_slice_and_slice_until() { let bits = BitString::try_from("1011010011").unwrap(); - let v = bits.as_bitstr(); + let v = bits.as_bit_str(); // slice(2..8) → bits 2..8 → "110100" let s1 = v.slice(UsizeCO::try_new(2, 8).unwrap()); @@ -256,7 +256,7 @@ fn mixed_slice_and_slice_until() { #[test] fn slice_on_already_offset_view() { let bits = BitString::try_from("111100001010").unwrap(); - let v = bits.as_bitstr(); + let v = bits.as_bit_str(); // Offset view from bit 3 let off = v.slice(UsizeCO::try_new(3, 12).unwrap()); @@ -276,7 +276,7 @@ fn slice_on_already_offset_view() { #[test] fn slice_from_on_already_offset_view() { let bits = BitString::try_from("111100001010").unwrap(); - let v = bits.as_bitstr(); + let v = bits.as_bit_str(); let off = v.slice(UsizeCO::try_new(3, 12).unwrap()); let s = off.slice_from(4); @@ -286,7 +286,7 @@ fn slice_from_on_already_offset_view() { #[test] fn slice_until_on_already_offset_view() { let bits = BitString::try_from("111100001010").unwrap(); - let v = bits.as_bitstr(); + let v = bits.as_bit_str(); let off = v.slice(UsizeCO::try_new(3, 12).unwrap()); let s = off.slice_until(4); diff --git a/src/bit_string.rs b/src/bit_string.rs index d740285..019af74 100644 --- a/src/bit_string.rs +++ b/src/bit_string.rs @@ -25,7 +25,7 @@ impl BitString { /// Returns a zero-copy [`BitStr`] view of the entire bit string. #[inline] - pub fn as_bitstr(&self) -> crate::BitStr<'_> { + pub fn as_bit_str(&self) -> crate::BitStr<'_> { crate::BitStr { source: self, start: 0, diff --git a/src/bit_string/impls_for_matching.rs b/src/bit_string/impls_for_matching.rs index 7262507..bb32565 100644 --- a/src/bit_string/impls_for_matching.rs +++ b/src/bit_string/impls_for_matching.rs @@ -1,10 +1,5 @@ -use crate::traits::*; - use super::*; mod impls_for_find; mod impls_for_matches_at; mod impls_for_strip; - -#[cfg(test)] -mod tests_for_bits_equal_at; 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 a506efd..7d3298e 100644 --- a/src/bit_string/impls_for_matching/impls_for_find.rs +++ b/src/bit_string/impls_for_matching/impls_for_find.rs @@ -1,71 +1,19 @@ -use crate::SMALL_WORDS; -use crate::traits::*; - use super::*; impl BitString { #[inline] - pub fn contains(&self, needle: &Self) -> bool { - if needle.bit_len == 0 { - return true; - } - if needle.bit_len > self.bit_len { - return false; - } - - self.words - .find_any_candidate(self.bit_len, needle.words(), needle.bit_len, &mut |pos| { - self.bits_equal_at(pos, needle) - }) - .is_some() + pub fn contains(&self, needle: crate::BitStr<'_>) -> bool { + self.as_bit_str().contains(needle) } - pub fn find(&self, needle: &Self) -> Option { - if needle.bit_len == 0 { - return Some(0); - } - if needle.bit_len > self.bit_len { - return None; - } - if self.words.len() >= SMALL_WORDS - && !self - .words - .find_any_candidate(self.bit_len, needle.words(), needle.bit_len, &mut |pos| { - self.bits_equal_at(pos, needle) - }) - .is_some() - { - return None; - } - - self.words - .find_first_word(self.bit_len, needle.words(), needle.bit_len, &mut |pos| { - self.bits_equal_at(pos, needle) - }) + #[inline] + pub fn find(&self, needle: crate::BitStr<'_>) -> Option { + self.as_bit_str().find(needle) } - pub fn rfind(&self, needle: &Self) -> Option { - if needle.bit_len == 0 { - return Some(self.bit_len); - } - if needle.bit_len > self.bit_len { - return None; - } - if self.words.len() >= SMALL_WORDS - && !self - .words - .find_any_candidate(self.bit_len, needle.words(), needle.bit_len, &mut |pos| { - self.bits_equal_at(pos, needle) - }) - .is_some() - { - return None; - } - - self.words - .find_last_word(self.bit_len, needle.words(), needle.bit_len, &mut |pos| { - self.bits_equal_at(pos, needle) - }) + #[inline] + pub fn rfind(&self, needle: crate::BitStr<'_>) -> Option { + self.as_bit_str().rfind(needle) } } 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 index b4378f1..21eaa73 100644 --- 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 @@ -5,7 +5,7 @@ fn returns_true_for_empty_needle() { let bits = BitString::try_from("101001").unwrap(); let needle = BitString::new(); - assert!(bits.contains(&needle)); + assert!(bits.contains(needle.as_bit_str())); } #[test] @@ -13,7 +13,7 @@ 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)); + assert!(bits.contains(needle.as_bit_str())); } #[test] @@ -21,7 +21,7 @@ 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)); + assert!(bits.contains(needle.as_bit_str())); } #[test] @@ -29,7 +29,7 @@ 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)); + assert!(bits.contains(needle.as_bit_str())); } #[test] @@ -37,7 +37,7 @@ 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)); + assert!(bits.contains(needle.as_bit_str())); } #[test] @@ -45,7 +45,7 @@ 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)); + assert!(!bits.contains(needle.as_bit_str())); } #[test] @@ -53,7 +53,7 @@ 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)); + assert!(!bits.contains(needle.as_bit_str())); } #[test] @@ -67,6 +67,6 @@ fn works_across_word_boundaries() { let present = BitString::try_from("01110").unwrap(); let absent = BitString::try_from("11110").unwrap(); - assert!(bits.contains(&present)); - assert!(!bits.contains(&absent)); + 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 index 78afd36..2554a68 100644 --- 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 @@ -5,7 +5,7 @@ fn empty_needle_matches_at_zero() { let bits = BitString::try_from("101001").unwrap(); let needle = BitString::new(); - assert_eq!(bits.find(&needle), Some(0)); + assert_eq!(bits.find(needle.as_bit_str()), Some(0)); } #[test] @@ -13,7 +13,7 @@ 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), None); + assert_eq!(bits.find(needle.as_bit_str()), None); } #[test] @@ -21,7 +21,7 @@ fn finds_needle_at_start() { let bits = BitString::try_from("101001").unwrap(); let needle = BitString::try_from("101").unwrap(); - assert_eq!(bits.find(&needle), Some(0)); + assert_eq!(bits.find(needle.as_bit_str()), Some(0)); } #[test] @@ -29,7 +29,7 @@ fn finds_needle_in_middle() { let bits = BitString::try_from("101001").unwrap(); let needle = BitString::try_from("100").unwrap(); - assert_eq!(bits.find(&needle), Some(2)); + assert_eq!(bits.find(needle.as_bit_str()), Some(2)); } #[test] @@ -37,7 +37,7 @@ fn finds_needle_at_end() { let bits = BitString::try_from("101001").unwrap(); let needle = BitString::try_from("001").unwrap(); - assert_eq!(bits.find(&needle), Some(3)); + assert_eq!(bits.find(needle.as_bit_str()), Some(3)); } #[test] @@ -45,7 +45,7 @@ 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), None); + assert_eq!(bits.find(needle.as_bit_str()), None); } #[test] @@ -53,7 +53,7 @@ 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), Some(0)); + assert_eq!(bits.find(needle.as_bit_str()), Some(0)); } #[test] @@ -65,5 +65,5 @@ fn finds_match_across_word_boundary() { bits.set(64, true); bits.set(65, true); - assert_eq!(bits.find(&needle), Some(63)); + 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 index dbe9062..5188f89 100644 --- 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 @@ -5,7 +5,7 @@ fn returns_len_for_empty_needle() { let bits = BitString::try_from("101001").unwrap(); let needle = BitString::new(); - assert_eq!(bits.rfind(&needle), Some(bits.bit_len())); + assert_eq!(bits.rfind(needle.as_bit_str()), Some(bits.bit_len())); } #[test] @@ -13,7 +13,7 @@ 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), None); + assert_eq!(bits.rfind(needle.as_bit_str()), None); } #[test] @@ -21,7 +21,7 @@ fn returns_last_match_index() { let bits = BitString::try_from("00110110").unwrap(); let needle = BitString::try_from("110").unwrap(); - assert_eq!(bits.rfind(&needle), Some(5)); + assert_eq!(bits.rfind(needle.as_bit_str()), Some(5)); } #[test] @@ -29,7 +29,7 @@ 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), Some(0)); + assert_eq!(bits.rfind(needle.as_bit_str()), Some(0)); } #[test] @@ -37,7 +37,7 @@ 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), Some(3)); + assert_eq!(bits.rfind(needle.as_bit_str()), Some(3)); } #[test] @@ -45,7 +45,7 @@ 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), None); + assert_eq!(bits.rfind(needle.as_bit_str()), None); } #[test] @@ -53,7 +53,7 @@ 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), Some(0)); + assert_eq!(bits.rfind(needle.as_bit_str()), Some(0)); } #[test] @@ -70,5 +70,5 @@ fn works_across_word_boundaries() { let needle = BitString::try_from("01110").unwrap(); - assert_eq!(bits.rfind(&needle), Some(126)); + 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 b7da80b..7dfc156 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 @@ -1,154 +1,22 @@ -use crate::WORD_BITS; -use crate::funcs_for_bits::low_mask; - use super::*; impl BitString { - /// Compare `needle` bits against `self` starting at `offset`. - /// - /// Delegates to [`BitsEq::eq_words`] which internally dispatches to - /// aligned or unaligned SIMD based on the intra-word shift. - /// Single-word patterns use a direct read+mask fast path. - #[inline] - pub(crate) fn bits_equal_at(&self, offset: usize, needle: &Self) -> bool { - let needle_bits = needle.bit_len; - if needle_bits == 0 { - return true; - } - let needle_words = needle.words(); - - // Sub-word fast path: the entire pattern fits in one u64. - if needle_bits <= WORD_BITS { - let h = self.words.read_word_at(offset); - let mask = low_mask(needle_bits); - return (h & mask) == (needle_words[0] & mask); - } - - let full_words = needle_bits / WORD_BITS; - - if !self.words.eq_words(needle_words, full_words, offset) { - return false; - } - - let rem_bits = needle_bits % WORD_BITS; - if rem_bits > 0 { - let mask = low_mask(rem_bits); - let h = self.words.read_word_at(offset + full_words * WORD_BITS); - if (h & mask) != (needle_words[full_words] & mask) { - return false; - } - } - - true - } - /// Returns `true` if `pattern` matches the bits starting at `index`. - /// - /// Delegates to [`bits_equal_at`] which uses SIMD word-equality for - /// long patterns and scalar comparison for short ones. #[inline] - pub fn matches_at(&self, index: usize, pattern: &Self) -> bool { - if index > self.bit_len { - return false; - } - - if pattern.bit_len > self.bit_len - index { - return false; - } - - self.bits_equal_at(index, pattern) + pub fn matches_at(&self, index: usize, pattern: crate::BitStr<'_>) -> bool { + self.as_bit_str().matches_at(index, pattern) } /// Returns `true` if `prefix` is a prefix of `self`. - /// - /// This is equivalent to [`matches_at`]`(0, prefix)` but optimized for - /// the word-aligned position-0 case. #[inline] - pub fn starts_with(&self, prefix: &Self) -> bool { - if prefix.bit_len == 0 { - return true; - } - if prefix.bit_len > self.bit_len { - return false; - } - - let pw = prefix.words(); - let sw: &[u64] = &self.words; - - // Sub-word fast path: one u64 read + mask. - if prefix.bit_len <= WORD_BITS { - let mask = low_mask(prefix.bit_len); - return (sw[0] & mask) == (pw[0] & mask); - } - - let full_words = prefix.bit_len / WORD_BITS; - - if !sw.eq_words(pw, full_words, 0) { - return false; - } - - let rem = prefix.bit_len % WORD_BITS; - if rem > 0 { - let mask = low_mask(rem); - if (sw[full_words] & mask) != (pw[full_words] & mask) { - return false; - } - } - - true + pub fn starts_with(&self, prefix: crate::BitStr<'_>) -> bool { + self.as_bit_str().starts_with(prefix) } /// Returns `true` if `suffix` is a suffix of `self`. #[inline] - pub fn ends_with(&self, suffix: &Self) -> bool { - if suffix.bit_len == 0 { - return true; - } - if suffix.bit_len > self.bit_len { - return false; - } - - let start = self.bit_len - suffix.bit_len; - let shift = start % WORD_BITS; - let base_word = start / WORD_BITS; - let sw: &[u64] = &self.words[base_word..]; - let pw = suffix.words(); - - // Sub-word fast path: one 64-bit window + mask. - if suffix.bit_len <= WORD_BITS { - let h = if shift == 0 { - sw[0] - } else { - let w0 = sw[0]; - let w1 = sw.get(1).copied().unwrap_or(0); - (w0 >> shift) | (w1 << (WORD_BITS - shift)) - }; - let mask = low_mask(suffix.bit_len); - return (h & mask) == (pw[0] & mask); - } - - let full_words = suffix.bit_len / WORD_BITS; - - if !self.words.eq_words(pw, full_words, start) { - return false; - } - - let rem = suffix.bit_len % WORD_BITS; - if rem > 0 { - let mask = low_mask(rem); - let h = if shift == 0 { - sw[full_words] - } else { - let w0 = sw[full_words]; - let w1 = sw.get(full_words + 1).copied().unwrap_or(0); - (w0 >> shift) | (w1 << (WORD_BITS - shift)) - }; - if (h & mask) != (pw[full_words] & mask) { - return false; - } - } - - true + pub fn ends_with(&self, suffix: crate::BitStr<'_>) -> bool { + self.as_bit_str().ends_with(suffix) } } 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 index fd378ba..2cbe30b 100644 --- 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 @@ -5,7 +5,7 @@ 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)); + assert!(!bits.ends_with(suffix.as_bit_str())); } #[test] @@ -13,7 +13,7 @@ fn empty_suffix_always_matches() { let bits = BitString::try_from("101").unwrap(); let suffix = BitString::new(); - assert!(bits.ends_with(&suffix)); + assert!(bits.ends_with(suffix.as_bit_str())); } #[test] @@ -21,7 +21,7 @@ fn matches_suffix() { let bits = BitString::try_from("101001").unwrap(); let suffix = BitString::try_from("001").unwrap(); - assert!(bits.ends_with(&suffix)); + assert!(bits.ends_with(suffix.as_bit_str())); } #[test] @@ -29,7 +29,7 @@ fn rejects_non_suffix() { let bits = BitString::try_from("101001").unwrap(); let suffix = BitString::try_from("101").unwrap(); - assert!(!bits.ends_with(&suffix)); + assert!(!bits.ends_with(suffix.as_bit_str())); } #[test] @@ -37,5 +37,5 @@ 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)); + 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 index 8e7ab96..88a93c7 100644 --- 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 @@ -5,7 +5,7 @@ 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)); + assert!(!bits.matches_at(bits.bit_len() + 1, pattern.as_bit_str())); } #[test] @@ -13,8 +13,8 @@ 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)); - assert!(!bits.matches_at(bits.bit_len(), &pattern)); + assert!(!bits.matches_at(3, pattern.as_bit_str())); + assert!(!bits.matches_at(bits.bit_len(), pattern.as_bit_str())); } #[test] @@ -22,5 +22,5 @@ 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)); + 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 index 1c2c9a8..6dec2de 100644 --- 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 @@ -5,7 +5,7 @@ fn returns_true_for_empty_prefix() { let bits = BitString::try_from("101001").unwrap(); let prefix = BitString::new(); - assert!(bits.starts_with(&prefix)); + assert!(bits.starts_with(prefix.as_bit_str())); } #[test] @@ -13,7 +13,7 @@ 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)); + assert!(bits.starts_with(prefix.as_bit_str())); } #[test] @@ -21,7 +21,7 @@ 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)); + assert!(bits.starts_with(prefix.as_bit_str())); } #[test] @@ -29,7 +29,7 @@ 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)); + assert!(!bits.starts_with(prefix.as_bit_str())); } #[test] @@ -37,7 +37,7 @@ 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)); + assert!(!bits.starts_with(prefix.as_bit_str())); } #[test] @@ -55,9 +55,9 @@ fn works_across_word_boundaries() { prefix.set(64, true); prefix.set(65, true); - assert!(bits.starts_with(&prefix)); + assert!(bits.starts_with(prefix.as_bit_str())); prefix.set(62, true); - assert!(!bits.starts_with(&prefix)); + 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 45c7a33..0a7a29d 100644 --- a/src/bit_string/impls_for_matching/impls_for_strip.rs +++ b/src/bit_string/impls_for_matching/impls_for_strip.rs @@ -1,13 +1,17 @@ use super::*; impl BitString { - pub fn strip_prefix(&self, prefix: &Self) -> Option { - self.starts_with(prefix) + #[inline] + pub fn strip_prefix(&self, prefix: crate::BitStr<'_>) -> Option { + self.as_bit_str() + .starts_with(prefix) .then(|| self.slice_from(prefix.bit_len)) } - pub fn strip_suffix(&self, suffix: &Self) -> Option { - self.ends_with(suffix) + #[inline] + pub fn strip_suffix(&self, suffix: crate::BitStr<'_>) -> Option { + self.as_bit_str() + .ends_with(suffix) .then(|| self.slice_until(self.bit_len - suffix.bit_len)) } } 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 index 7f94fdb..d1b8af3 100644 --- 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 @@ -7,7 +7,7 @@ fn empty_prefix_returns_original_copy() { let bits = BitString::try_from("101001").unwrap(); let prefix = BitString::new(); - let stripped = bits.strip_prefix(&prefix).unwrap(); + let stripped = bits.strip_prefix(prefix.as_bit_str()).unwrap(); assert_eq!(stripped.to_string(), "101001"); } @@ -17,7 +17,7 @@ fn strips_matching_prefix() { let bits = BitString::try_from("101001").unwrap(); let prefix = BitString::try_from("101").unwrap(); - let stripped = bits.strip_prefix(&prefix).unwrap(); + let stripped = bits.strip_prefix(prefix.as_bit_str()).unwrap(); assert_eq!(stripped.to_string(), "001"); } @@ -27,7 +27,7 @@ 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).unwrap(); + let stripped = bits.strip_prefix(prefix.as_bit_str()).unwrap(); assert!(stripped.is_empty()); assert_eq!(stripped.to_string(), ""); @@ -38,7 +38,7 @@ 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), None); + assert_eq!(bits.strip_prefix(prefix.as_bit_str()), None); } #[test] @@ -46,5 +46,5 @@ 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), None); + 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 index af08d94..b893d8d 100644 --- 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 @@ -7,7 +7,7 @@ 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).unwrap(); + let stripped = bits.strip_suffix(suffix.as_bit_str()).unwrap(); assert_eq!(stripped.to_string(), "101001"); } @@ -17,7 +17,7 @@ fn strips_matching_suffix() { let bits = BitString::try_from("101001").unwrap(); let suffix = BitString::try_from("001").unwrap(); - let stripped = bits.strip_suffix(&suffix).unwrap(); + let stripped = bits.strip_suffix(suffix.as_bit_str()).unwrap(); assert_eq!(stripped.to_string(), "101"); } @@ -27,7 +27,7 @@ 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).unwrap(); + let stripped = bits.strip_suffix(suffix.as_bit_str()).unwrap(); assert!(stripped.is_empty()); assert_eq!(stripped.to_string(), ""); @@ -38,7 +38,7 @@ 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), None); + assert_eq!(bits.strip_suffix(suffix.as_bit_str()), None); } #[test] @@ -46,7 +46,7 @@ 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), None); + assert_eq!(bits.strip_suffix(suffix.as_bit_str()), None); } #[test] @@ -54,7 +54,7 @@ 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).unwrap(); + let mut stripped = bits.strip_suffix(suffix.as_bit_str()).unwrap(); bits.set(0, false); stripped.set(0, false); @@ -79,7 +79,7 @@ fn works_across_word_boundaries() { suffix.set(2, true); suffix.set(66, true); - let stripped = bits.strip_suffix(&suffix).unwrap(); + let stripped = bits.strip_suffix(suffix.as_bit_str()).unwrap(); assert_eq!(stripped.bit_len(), 63); assert_eq!(stripped.get(0), Some(true)); @@ -101,5 +101,5 @@ fn returns_none_for_almost_matching_cross_word_suffix() { suffix.set(2, true); suffix.set(65, true); - assert_eq!(bits.strip_suffix(&suffix), None); + assert_eq!(bits.strip_suffix(suffix.as_bit_str()), None); } diff --git a/src/bit_string/impls_for_matching/tests_for_bits_equal_at.rs b/src/bit_string/impls_for_matching/tests_for_bits_equal_at.rs deleted file mode 100644 index ca0e2b8..0000000 --- a/src/bit_string/impls_for_matching/tests_for_bits_equal_at.rs +++ /dev/null @@ -1,52 +0,0 @@ -use crate::BitString; - -#[test] -fn returns_true_when_needle_matches_at_offset() { - let haystack = BitString::try_from("00110110").unwrap(); - let needle = BitString::try_from("110").unwrap(); - - assert!(haystack.bits_equal_at(2, &needle)); - assert!(haystack.bits_equal_at(5, &needle)); -} - -#[test] -fn returns_false_when_needle_differs_at_offset() { - let haystack = BitString::try_from("00110110").unwrap(); - let needle = BitString::try_from("110").unwrap(); - - assert!(!haystack.bits_equal_at(0, &needle)); - assert!(!haystack.bits_equal_at(1, &needle)); - assert!(!haystack.bits_equal_at(3, &needle)); -} - -#[test] -fn empty_needle_matches_at_valid_boundary_offsets() { - let haystack = BitString::try_from("101001").unwrap(); - let needle = BitString::new(); - - assert!(haystack.bits_equal_at(0, &needle)); - assert!(haystack.bits_equal_at(3, &needle)); - assert!(haystack.bits_equal_at(haystack.bit_len(), &needle)); -} - -#[test] -fn works_across_word_boundaries() { - let mut haystack = BitString::zeros(130); - - haystack.set(63, true); - haystack.set(64, true); - haystack.set(65, true); - - let needle = BitString::try_from("01110").unwrap(); - - assert!(haystack.bits_equal_at(62, &needle)); - assert!(!haystack.bits_equal_at(61, &needle)); -} - -#[test] -fn works_when_needle_reaches_haystack_end() { - let haystack = BitString::try_from("101001").unwrap(); - let needle = BitString::try_from("001").unwrap(); - - assert!(haystack.bits_equal_at(3, &needle)); -} diff --git a/src/traits/bits_eq/funcs_for_eq_words_aligned_core/tests_for_backend_equivalence.rs b/src/traits/bits_eq/funcs_for_eq_words_aligned_core/tests_for_backend_equivalence.rs index 9d90c73..61a6277 100644 --- a/src/traits/bits_eq/funcs_for_eq_words_aligned_core/tests_for_backend_equivalence.rs +++ b/src/traits/bits_eq/funcs_for_eq_words_aligned_core/tests_for_backend_equivalence.rs @@ -24,7 +24,7 @@ proptest! { let haystack = BitString::from_bool_iter((0..h_len).map(|i| (i * 17 + 3) % 5 == 0)); let prefix = BitString::from_bool_iter((0..p_len).map(|i| (i * 17 + 3) % 5 == 0)); - let result = haystack.starts_with(&prefix); + let result = haystack.starts_with(prefix.as_bit_str()); let expected = (0..p_len).all(|j| haystack.get(j) == prefix.get(j)); assert_eq!(result, expected); @@ -38,7 +38,7 @@ proptest! { let haystack = BitString::from_bool_iter(h_bits); let prefix = BitString::from_bool_iter(p_bits); - let result = haystack.starts_with(&prefix); + let result = haystack.starts_with(prefix.as_bit_str()); let expected = prefix.bit_len() <= haystack.bit_len() && (0..prefix.bit_len()).all(|j| haystack.get(j) == prefix.get(j)); diff --git a/src/traits/bits_eq/funcs_for_eq_words_unaligned_core/tests_for_backend_equivalence.rs b/src/traits/bits_eq/funcs_for_eq_words_unaligned_core/tests_for_backend_equivalence.rs index a2a50d3..ea78dd8 100644 --- a/src/traits/bits_eq/funcs_for_eq_words_unaligned_core/tests_for_backend_equivalence.rs +++ b/src/traits/bits_eq/funcs_for_eq_words_unaligned_core/tests_for_backend_equivalence.rs @@ -24,7 +24,7 @@ proptest! { let haystack = BitString::from_bool_iter((0..h_len).map(|i| (i * 13 + 7) % 5 == 0)); let suffix = BitString::from_bool_iter((0..s_len).map(|i| (i * 13 + 7) % 5 == 0)); - let result = haystack.ends_with(&suffix); + let result = haystack.ends_with(suffix.as_bit_str()); let start = h_len - s_len; let expected = (0..s_len).all(|j| haystack.get(start + j) == suffix.get(j)); assert_eq!(result, expected); @@ -38,7 +38,7 @@ proptest! { let haystack = BitString::from_bool_iter(h_bits); let suffix = BitString::from_bool_iter(s_bits); - let result = haystack.ends_with(&suffix); + let result = haystack.ends_with(suffix.as_bit_str()); let expected = suffix.bit_len() <= haystack.bit_len() && { let start = haystack.bit_len() - suffix.bit_len(); diff --git a/src/traits/bits_find/funcs_for_contains_core/tests_for_backend_equivalence.rs b/src/traits/bits_find/funcs_for_contains_core/tests_for_backend_equivalence.rs index ed9ff03..ca78b2d 100644 --- a/src/traits/bits_find/funcs_for_contains_core/tests_for_backend_equivalence.rs +++ b/src/traits/bits_find/funcs_for_contains_core/tests_for_backend_equivalence.rs @@ -31,7 +31,7 @@ proptest! { haystack.bit_len(), needle.words(), needle.bit_len(), - &mut |pos| haystack.bits_equal_at(pos, &needle), + &mut |pos| haystack.as_bit_str().bits_equal_at(pos, needle.as_bit_str()), ); // Brute-force reference: find any match. @@ -75,7 +75,7 @@ proptest! { needle.words(), needle_len, &mut |pos| { - let ok = haystack.bits_equal_at(pos, &needle); + let ok = haystack.as_bit_str().bits_equal_at(pos, needle.as_bit_str()); if ok { any_found = true; } ok }, diff --git a/src/traits/bits_find/funcs_for_find_core/tests_for_backend_equivalence.rs b/src/traits/bits_find/funcs_for_find_core/tests_for_backend_equivalence.rs index 75d3b90..11257f0 100644 --- a/src/traits/bits_find/funcs_for_find_core/tests_for_backend_equivalence.rs +++ b/src/traits/bits_find/funcs_for_find_core/tests_for_backend_equivalence.rs @@ -31,7 +31,7 @@ proptest! { let haystack_len = haystack.bit_len(); let needle_len = needle.bit_len(); - let result = haystack.find(&needle); + let result = haystack.find(needle.as_bit_str()); // Brute-force reference. let mut expected = None; diff --git a/src/traits/bits_find/funcs_for_rfind_core/tests_for_backend_equivalence.rs b/src/traits/bits_find/funcs_for_rfind_core/tests_for_backend_equivalence.rs index 8284e33..e14c2e5 100644 --- a/src/traits/bits_find/funcs_for_rfind_core/tests_for_backend_equivalence.rs +++ b/src/traits/bits_find/funcs_for_rfind_core/tests_for_backend_equivalence.rs @@ -31,7 +31,7 @@ proptest! { haystack.bit_len(), needle.words(), needle.bit_len(), - &mut |pos| haystack.bits_equal_at(pos, &needle), + &mut |pos| haystack.as_bit_str().bits_equal_at(pos, needle.as_bit_str()), ); // Brute-force: find rightmost match. @@ -70,7 +70,7 @@ proptest! { haystack.bit_len(), needle.words(), needle.bit_len(), - &mut |pos| haystack.bits_equal_at(pos, &needle), + &mut |pos| haystack.as_bit_str().bits_equal_at(pos, needle.as_bit_str()), ); let max_pos = haystack.bit_len().saturating_sub(needle.bit_len()); From c59176f5c0fbecf8c1b63d0b24f521edf93bbb97 Mon Sep 17 00:00:00 2001 From: juncheng Date: Wed, 24 Jun 2026 13:38:14 +0000 Subject: [PATCH 12/22] feat: Display and Debug for BitStr Display writes '1'/'0' for each bit in the view, reusing BitStr::get() which already accounts for the view offset. Debug wraps Display output in BitStr("...") format, matching BitString's debug style. Co-authored-by: Claude Co-authored-by: DeepSeek AI --- src/bit_str.rs | 1 + src/bit_str/impls_for_fmt.rs | 24 +++++++++ src/bit_str/impls_for_fmt/tests_for_fmt.rs | 63 ++++++++++++++++++++++ 3 files changed, 88 insertions(+) create mode 100644 src/bit_str/impls_for_fmt.rs create mode 100644 src/bit_str/impls_for_fmt/tests_for_fmt.rs diff --git a/src/bit_str.rs b/src/bit_str.rs index b884098..0544a31 100644 --- a/src/bit_str.rs +++ b/src/bit_str.rs @@ -25,6 +25,7 @@ pub struct BitStr<'bs> { pub mod errors; mod impls_for_access; mod impls_for_bit_arith; +mod impls_for_fmt; mod impls_for_iter; mod impls_for_matching; mod impls_for_predicates; diff --git a/src/bit_str/impls_for_fmt.rs b/src/bit_str/impls_for_fmt.rs new file mode 100644 index 0000000..4b3e4aa --- /dev/null +++ b/src/bit_str/impls_for_fmt.rs @@ -0,0 +1,24 @@ +use core::fmt; + +use crate::BitStr; + +impl fmt::Display for BitStr<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + for index in 0..self.bit_len { + f.write_str(if self.get(index).unwrap() { "1" } else { "0" })?; + } + + Ok(()) + } +} + +impl fmt::Debug for BitStr<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str("BitStr(\"")?; + fmt::Display::fmt(self, f)?; + f.write_str("\")") + } +} + +#[cfg(test)] +mod tests_for_fmt; diff --git a/src/bit_str/impls_for_fmt/tests_for_fmt.rs b/src/bit_str/impls_for_fmt/tests_for_fmt.rs new file mode 100644 index 0000000..5d53c35 --- /dev/null +++ b/src/bit_str/impls_for_fmt/tests_for_fmt.rs @@ -0,0 +1,63 @@ +use alloc::string::ToString; + +use crate::BitString; + +#[test] +fn display_full_view() { + let bits = BitString::try_from("101001").unwrap(); + assert_eq!(bits.as_bit_str().to_string(), "101001"); +} + +#[test] +fn display_empty_view() { + let bits = BitString::new(); + assert_eq!(bits.as_bit_str().to_string(), ""); +} + +#[test] +fn display_offset_view() { + let bits = BitString::try_from("110010").unwrap(); + let v = bits.as_bit_str().slice_from(2).slice_until(5); + assert_eq!(v.to_string(), "0010"); +} + +#[test] +fn display_single_bit_views() { + let bits = BitString::try_from("10").unwrap(); + assert_eq!( + bits.as_bit_str().slice_from(0).slice_until(1).to_string(), + "1" + ); + assert_eq!( + bits.as_bit_str().slice_from(1).slice_until(2).to_string(), + "0" + ); +} + +#[test] +fn display_across_word_boundary() { + let mut bits = BitString::zeros(130); + bits.set(62, true); + bits.set(63, true); + bits.set(64, true); + bits.set(65, true); + let expected = bits.to_string(); + assert_eq!(bits.as_bit_str().to_string(), expected); +} + +#[test] +fn debug_format() { + let bits = BitString::try_from("1010").unwrap(); + let v = bits.as_bit_str(); + assert_eq!(alloc::format!("{v:?}"), "BitStr(\"1010\")"); + + let v = bits.as_bit_str().slice_from(1).slice_until(3); + assert_eq!(alloc::format!("{v:?}"), "BitStr(\"010\")"); +} + +#[test] +fn debug_empty_view() { + let bits = BitString::new(); + let v = bits.as_bit_str(); + assert_eq!(alloc::format!("{v:?}"), "BitStr(\"\")"); +} From 288ae427cf0804b6b50fcac467ddb69e6acbacd0 Mon Sep 17 00:00:00 2001 From: juncheng Date: Wed, 24 Jun 2026 13:39:13 +0000 Subject: [PATCH 13/22] feat: PartialEq and Eq for BitStr MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit BitStr equality: compare bit_len first, then delegate to bits_equal_at which handles SIMD on both sides. Different sources and offsets are supported — two views are equal iff their bit contents match. Co-authored-by: Claude Co-authored-by: DeepSeek AI --- src/bit_str.rs | 1 + src/bit_str/impls_for_eq.rs | 18 ++++++ src/bit_str/impls_for_eq/tests_for_eq.rs | 71 ++++++++++++++++++++++++ 3 files changed, 90 insertions(+) create mode 100644 src/bit_str/impls_for_eq.rs create mode 100644 src/bit_str/impls_for_eq/tests_for_eq.rs diff --git a/src/bit_str.rs b/src/bit_str.rs index 0544a31..97292bf 100644 --- a/src/bit_str.rs +++ b/src/bit_str.rs @@ -25,6 +25,7 @@ pub struct BitStr<'bs> { pub mod errors; mod impls_for_access; mod impls_for_bit_arith; +mod impls_for_eq; mod impls_for_fmt; mod impls_for_iter; mod impls_for_matching; diff --git a/src/bit_str/impls_for_eq.rs b/src/bit_str/impls_for_eq.rs new file mode 100644 index 0000000..4236ac0 --- /dev/null +++ b/src/bit_str/impls_for_eq.rs @@ -0,0 +1,18 @@ +use crate::BitStr; + +impl PartialEq for BitStr<'_> { + fn eq(&self, other: &Self) -> bool { + if self.bit_len != other.bit_len { + return false; + } + if self.bit_len == 0 { + return true; + } + self.bits_equal_at(0, *other) + } +} + +impl Eq for BitStr<'_> {} + +#[cfg(test)] +mod tests_for_eq; diff --git a/src/bit_str/impls_for_eq/tests_for_eq.rs b/src/bit_str/impls_for_eq/tests_for_eq.rs new file mode 100644 index 0000000..eb5afdc --- /dev/null +++ b/src/bit_str/impls_for_eq/tests_for_eq.rs @@ -0,0 +1,71 @@ +use crate::BitString; + +#[test] +fn same_source_full_views_are_equal() { + let bits = BitString::try_from("101001").unwrap(); + assert_eq!(bits.as_bit_str(), bits.as_bit_str()); +} + +#[test] +fn different_sources_same_bits_are_equal() { + let a = BitString::try_from("101001").unwrap(); + let b = BitString::try_from("101001").unwrap(); + assert_eq!(a.as_bit_str(), b.as_bit_str()); +} + +#[test] +fn different_lengths_not_equal() { + let a = BitString::try_from("101001").unwrap(); + let b = BitString::try_from("101").unwrap(); + assert_ne!(a.as_bit_str(), b.as_bit_str()); +} + +#[test] +fn same_length_different_content_not_equal() { + let a = BitString::try_from("101001").unwrap(); + let b = BitString::try_from("111001").unwrap(); + assert_ne!(a.as_bit_str(), b.as_bit_str()); +} + +#[test] +fn offset_views_equal_when_content_matches() { + let bits = BitString::try_from("110010").unwrap(); + let v1 = bits.as_bit_str().slice_from(2).slice_until(5); + let v2 = bits.as_bit_str().slice_from(2).slice_until(5); + assert_eq!(v1, v2); +} + +#[test] +fn offset_views_not_equal_when_content_differs() { + let bits = BitString::try_from("110010").unwrap(); + let v1 = bits.as_bit_str().slice_from(2).slice_until(5); + let v2 = bits.as_bit_str().slice_from(1).slice_until(4); + assert_ne!(v1, v2); +} + +#[test] +fn empty_views_are_equal() { + let a = BitString::new(); + let b = BitString::try_from("101").unwrap(); + let empty = b.as_bit_str().slice_from(0).slice_until(0); + assert_eq!(a.as_bit_str(), empty); + assert_eq!(empty, BitString::new().as_bit_str()); +} + +#[test] +fn views_across_word_boundaries() { + let mut a = BitString::zeros(130); + a.set(62, true); + a.set(63, true); + a.set(64, true); + + let mut b = BitString::zeros(130); + b.set(62, true); + b.set(63, true); + b.set(64, true); + + assert_eq!(a.as_bit_str(), b.as_bit_str()); + + b.set(65, true); + assert_ne!(a.as_bit_str(), b.as_bit_str()); +} From cdcd1083732617d6cb45580c8101a06f21322324 Mon Sep 17 00:00:00 2001 From: juncheng Date: Wed, 24 Jun 2026 13:41:08 +0000 Subject: [PATCH 14/22] feat: to_bit_string() for BitStr Copy the bits in a BitStr view into a new owned BitString via BitString::slice, which performs a word-level copy from the source. Empty views short-circuit to BitString::new(). Co-authored-by: Claude Co-authored-by: DeepSeek AI --- src/bit_str.rs | 1 + src/bit_str/impls_for_to_bit_string.rs | 21 ++++++++ .../tests_for_to_bit_string.rs | 49 +++++++++++++++++++ 3 files changed, 71 insertions(+) create mode 100644 src/bit_str/impls_for_to_bit_string.rs create mode 100644 src/bit_str/impls_for_to_bit_string/tests_for_to_bit_string.rs diff --git a/src/bit_str.rs b/src/bit_str.rs index 97292bf..87981e8 100644 --- a/src/bit_str.rs +++ b/src/bit_str.rs @@ -31,6 +31,7 @@ mod impls_for_iter; mod impls_for_matching; mod impls_for_predicates; mod impls_for_slice; +mod impls_for_to_bit_string; // --------------------------------------------------------------------------- // Getters diff --git a/src/bit_str/impls_for_to_bit_string.rs b/src/bit_str/impls_for_to_bit_string.rs new file mode 100644 index 0000000..fba7a76 --- /dev/null +++ b/src/bit_str/impls_for_to_bit_string.rs @@ -0,0 +1,21 @@ +use int_interval::UsizeCO; + +use crate::{BitStr, BitString}; + +impl BitStr<'_> { + /// Copies the bits in this view into a new owned [`BitString`]. + /// + /// Delegates to [`BitString::slice`] which performs a word-level + /// copy from the source. + #[inline] + pub fn to_bit_string(&self) -> BitString { + if self.bit_len == 0 { + return BitString::new(); + } + self.source + .slice(UsizeCO::try_new(self.start, self.start + self.bit_len).unwrap()) + } +} + +#[cfg(test)] +mod tests_for_to_bit_string; diff --git a/src/bit_str/impls_for_to_bit_string/tests_for_to_bit_string.rs b/src/bit_str/impls_for_to_bit_string/tests_for_to_bit_string.rs new file mode 100644 index 0000000..7df2582 --- /dev/null +++ b/src/bit_str/impls_for_to_bit_string/tests_for_to_bit_string.rs @@ -0,0 +1,49 @@ +use alloc::string::ToString; + +use crate::BitString; + +#[test] +fn full_view_roundtrip() { + let bits = BitString::try_from("101001").unwrap(); + let view = bits.as_bit_str(); + let owned = view.to_bit_string(); + assert_eq!(owned, bits); +} + +#[test] +fn offset_view_roundtrip() { + let bits = BitString::try_from("110010").unwrap(); + let view = bits.as_bit_str().slice_from(2).slice_until(5); + let owned = view.to_bit_string(); + assert_eq!(owned.to_string(), "0010"); + assert_eq!(owned.bit_len(), 4); +} + +#[test] +fn empty_view_to_empty_bit_string() { + let bits = BitString::try_from("101").unwrap(); + let view = bits.as_bit_str().slice_from(0).slice_until(0); + let owned = view.to_bit_string(); + assert!(owned.is_empty()); +} + +#[test] +fn empty_source_to_empty_bit_string() { + let bits = BitString::new(); + let view = bits.as_bit_str(); + let owned = view.to_bit_string(); + assert!(owned.is_empty()); +} + +#[test] +fn across_word_boundary() { + let mut bits = BitString::zeros(130); + bits.set(62, true); + bits.set(63, true); + bits.set(64, true); + bits.set(65, true); + + let view = bits.as_bit_str().slice_from(60).slice_until(70); + let owned = view.to_bit_string(); + assert_eq!(view, owned.as_bit_str()); +} From a61376ca97b07fd78cacce5847588abf79aeb656 Mon Sep 17 00:00:00 2001 From: juncheng Date: Wed, 24 Jun 2026 13:51:30 +0000 Subject: [PATCH 15/22] feat: Hash for BitStr Hash implementation consistent with Eq: hashes bit_len first, then each full word via read_word_at (handling offset), and masks the last partial word so unused bits don't affect the hash. Co-authored-by: Claude Co-authored-by: DeepSeek AI --- src/bit_str.rs | 20 ++++- src/bit_str/impls_for_hash.rs | 32 +++++++ src/bit_str/impls_for_hash/tests_for_hash.rs | 84 +++++++++++++++++++ src/bit_str/impls_for_to_bit_string.rs | 21 ----- .../tests_for_to_bit_string.rs | 0 5 files changed, 135 insertions(+), 22 deletions(-) create mode 100644 src/bit_str/impls_for_hash.rs create mode 100644 src/bit_str/impls_for_hash/tests_for_hash.rs delete mode 100644 src/bit_str/impls_for_to_bit_string.rs rename src/bit_str/{impls_for_to_bit_string => }/tests_for_to_bit_string.rs (100%) diff --git a/src/bit_str.rs b/src/bit_str.rs index 87981e8..97475d5 100644 --- a/src/bit_str.rs +++ b/src/bit_str.rs @@ -1,3 +1,5 @@ +use int_interval::UsizeCO; + use crate::BitString; /// A zero-copy borrowed view of a [`BitString`] or subrange thereof. @@ -27,11 +29,11 @@ mod impls_for_access; mod impls_for_bit_arith; mod impls_for_eq; mod impls_for_fmt; +mod impls_for_hash; mod impls_for_iter; mod impls_for_matching; mod impls_for_predicates; mod impls_for_slice; -mod impls_for_to_bit_string; // --------------------------------------------------------------------------- // Getters @@ -54,4 +56,20 @@ impl<'bs> BitStr<'bs> { pub fn source(&self) -> &BitString { self.source } + + /// Copies the bits in this view into a new owned [`BitString`]. + /// + /// Delegates to [`BitString::slice`] which performs a word-level + /// copy from the source. + #[inline] + pub fn to_bit_string(&self) -> BitString { + if self.bit_len == 0 { + return BitString::new(); + } + self.source + .slice(UsizeCO::checked_from_start_len(self.start, self.bit_len).unwrap()) + } } + +#[cfg(test)] +mod tests_for_to_bit_string; diff --git a/src/bit_str/impls_for_hash.rs b/src/bit_str/impls_for_hash.rs new file mode 100644 index 0000000..9d8dd7c --- /dev/null +++ b/src/bit_str/impls_for_hash.rs @@ -0,0 +1,32 @@ +use core::hash::{Hash, Hasher}; + +use crate::traits::*; +use crate::{WORD_BITS, low_mask}; + +use crate::BitStr; + +impl Hash for BitStr<'_> { + fn hash(&self, state: &mut H) { + self.bit_len.hash(state); + if self.bit_len == 0 { + return; + } + + let words = self.source.words(); + let full_words = self.bit_len / WORD_BITS; + let rem = self.bit_len % WORD_BITS; + + for i in 0..full_words { + let w = words.read_word_at(self.start + i * WORD_BITS); + w.hash(state); + } + + if rem > 0 { + let w = words.read_word_at(self.start + full_words * WORD_BITS); + (w & low_mask(rem)).hash(state); + } + } +} + +#[cfg(test)] +mod tests_for_hash; diff --git a/src/bit_str/impls_for_hash/tests_for_hash.rs b/src/bit_str/impls_for_hash/tests_for_hash.rs new file mode 100644 index 0000000..f6d3369 --- /dev/null +++ b/src/bit_str/impls_for_hash/tests_for_hash.rs @@ -0,0 +1,84 @@ +use core::hash::{Hash, Hasher}; + +use crate::BitString; + +/// A trivial deterministic hasher for testing hash consistency. +struct TestHasher(u64); + +impl Hasher for TestHasher { + fn finish(&self) -> u64 { + self.0 + } + + fn write(&mut self, bytes: &[u8]) { + for &b in bytes { + self.0 = self.0.wrapping_mul(31).wrapping_add(b as u64); + } + } + + fn write_u64(&mut self, i: u64) { + self.0 = self.0.wrapping_mul(31).wrapping_add(i); + } + + fn write_usize(&mut self, i: usize) { + self.write_u64(i as u64); + } +} + +fn hash_one(v: &impl Hash) -> u64 { + let mut h = TestHasher(0); + v.hash(&mut h); + h.finish() +} + +#[test] +fn equal_views_have_same_hash() { + let a = BitString::try_from("101001").unwrap(); + let b = BitString::try_from("101001").unwrap(); + assert_eq!(hash_one(&a.as_bit_str()), hash_one(&b.as_bit_str())); +} + +#[test] +fn different_content_has_different_hash() { + let a = BitString::try_from("101001").unwrap(); + let b = BitString::try_from("111001").unwrap(); + assert_ne!(hash_one(&a.as_bit_str()), hash_one(&b.as_bit_str())); +} + +#[test] +fn different_lengths_have_different_hash() { + let a = BitString::try_from("101001").unwrap(); + let b = BitString::try_from("101").unwrap(); + assert_ne!(hash_one(&a.as_bit_str()), hash_one(&b.as_bit_str())); +} + +#[test] +fn empty_views_from_different_sources_have_same_hash() { + let a = BitString::new(); + let b = BitString::try_from("101").unwrap(); + let empty = b.as_bit_str().slice_from(0).slice_until(0); + assert_eq!(hash_one(&a.as_bit_str()), hash_one(&empty)); +} + +#[test] +fn offset_views_with_same_content_have_same_hash() { + let bits = BitString::try_from("110010").unwrap(); + let v1 = bits.as_bit_str().slice_from(1).slice_until(4); + let v2 = bits.as_bit_str().slice_from(1).slice_until(4); + assert_eq!(hash_one(&v1), hash_one(&v2)); +} + +#[test] +fn views_across_word_boundaries() { + let mut a = BitString::zeros(130); + a.set(62, true); + a.set(63, true); + a.set(64, true); + + let mut b = BitString::zeros(130); + b.set(62, true); + b.set(63, true); + b.set(64, true); + + assert_eq!(hash_one(&a.as_bit_str()), hash_one(&b.as_bit_str())); +} diff --git a/src/bit_str/impls_for_to_bit_string.rs b/src/bit_str/impls_for_to_bit_string.rs deleted file mode 100644 index fba7a76..0000000 --- a/src/bit_str/impls_for_to_bit_string.rs +++ /dev/null @@ -1,21 +0,0 @@ -use int_interval::UsizeCO; - -use crate::{BitStr, BitString}; - -impl BitStr<'_> { - /// Copies the bits in this view into a new owned [`BitString`]. - /// - /// Delegates to [`BitString::slice`] which performs a word-level - /// copy from the source. - #[inline] - pub fn to_bit_string(&self) -> BitString { - if self.bit_len == 0 { - return BitString::new(); - } - self.source - .slice(UsizeCO::try_new(self.start, self.start + self.bit_len).unwrap()) - } -} - -#[cfg(test)] -mod tests_for_to_bit_string; diff --git a/src/bit_str/impls_for_to_bit_string/tests_for_to_bit_string.rs b/src/bit_str/tests_for_to_bit_string.rs similarity index 100% rename from src/bit_str/impls_for_to_bit_string/tests_for_to_bit_string.rs rename to src/bit_str/tests_for_to_bit_string.rs From 94135214106ba7ee65670236efab33964b8d4bbe Mon Sep 17 00:00:00 2001 From: juncheng Date: Wed, 24 Jun 2026 14:01:29 +0000 Subject: [PATCH 16/22] =?UTF-8?q?perf:=20optimize=20BitStr=20Hash=20?= =?UTF-8?q?=E2=80=94=20bulk-hash=20aligned=20word=20slices?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When the view starts at a word boundary, hash full words as a &[u64] slice instead of iterating word-by-word. For unaligned views, hash the first partial word via read_word_at, then hash the middle aligned words as a contiguous slice, and finally the tail partial word. Co-authored-by: Claude Co-authored-by: DeepSeek AI --- src/bit_str/impls_for_hash.rs | 27 ++++++++++++++++++++------- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/src/bit_str/impls_for_hash.rs b/src/bit_str/impls_for_hash.rs index 9d8dd7c..8cf68f8 100644 --- a/src/bit_str/impls_for_hash.rs +++ b/src/bit_str/impls_for_hash.rs @@ -13,17 +13,30 @@ impl Hash for BitStr<'_> { } let words = self.source.words(); - let full_words = self.bit_len / WORD_BITS; - let rem = self.bit_len % WORD_BITS; + let (sw, s) = (self.start / WORD_BITS, self.start % WORD_BITS); + + if s == 0 { + // Fully word-aligned: bulk-hash full words as a slice. + let full_words = self.bit_len / WORD_BITS; + words[sw..][..full_words].hash(state); + } else { + // First partial word from the source word at `sw`. + let first_len = (WORD_BITS - s).min(self.bit_len); + (words.read_word_at(self.start) & low_mask(first_len)).hash(state); - for i in 0..full_words { - let w = words.read_word_at(self.start + i * WORD_BITS); - w.hash(state); + let remaining = self.bit_len - first_len; + let mid_words = remaining / WORD_BITS; + if mid_words > 0 { + // Middle words are aligned — hash as a contiguous slice. + words[sw + 1..][..mid_words].hash(state); + } } + // Tail partial word (always unaligned within the source). + let rem = self.bit_len % WORD_BITS; if rem > 0 { - let w = words.read_word_at(self.start + full_words * WORD_BITS); - (w & low_mask(rem)).hash(state); + let tail_start = self.start + (self.bit_len / WORD_BITS) * WORD_BITS; + (words.read_word_at(tail_start) & low_mask(rem)).hash(state); } } } From e9018edc33f9f53e0ca2a7af4afe02670ad17f3e Mon Sep 17 00:00:00 2001 From: juncheng Date: Wed, 24 Jun 2026 14:09:13 +0000 Subject: [PATCH 17/22] =?UTF-8?q?fix:=20BitStr=20Hash=20=E2=80=94=20remove?= =?UTF-8?q?=20[u64]=20slice=20length-prefix=20mismatch?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [u64]::hash injects write_length_prefix which makes aligned and unaligned views of the same content produce different hashes. Hash each full word individually instead, so both code paths emit the same Hasher call sequence. Adds regression test: unaligned view vs aligned roundtrip must hash equally. Co-authored-by: Claude Co-authored-by: DeepSeek AI --- src/bit_str/impls_for_hash.rs | 33 ++++++++++---------- src/bit_str/impls_for_hash/tests_for_hash.rs | 21 +++++++++++++ 2 files changed, 37 insertions(+), 17 deletions(-) diff --git a/src/bit_str/impls_for_hash.rs b/src/bit_str/impls_for_hash.rs index 8cf68f8..8be3296 100644 --- a/src/bit_str/impls_for_hash.rs +++ b/src/bit_str/impls_for_hash.rs @@ -13,29 +13,28 @@ impl Hash for BitStr<'_> { } let words = self.source.words(); - let (sw, s) = (self.start / WORD_BITS, self.start % WORD_BITS); + let full_words = self.bit_len / WORD_BITS; + let rem = self.bit_len % WORD_BITS; - if s == 0 { - // Fully word-aligned: bulk-hash full words as a slice. - let full_words = self.bit_len / WORD_BITS; - words[sw..][..full_words].hash(state); + if self.start % WORD_BITS == 0 { + // Aligned: hash full words element-by-element (not as a [u64] + // slice, which would inject a length prefix that the unaligned + // path doesn't emit). + let sw = self.start / WORD_BITS; + for w in &words[sw..][..full_words] { + w.hash(state); + } } else { - // First partial word from the source word at `sw`. - let first_len = (WORD_BITS - s).min(self.bit_len); - (words.read_word_at(self.start) & low_mask(first_len)).hash(state); - - let remaining = self.bit_len - first_len; - let mid_words = remaining / WORD_BITS; - if mid_words > 0 { - // Middle words are aligned — hash as a contiguous slice. - words[sw + 1..][..mid_words].hash(state); + // Unaligned: each view-word straddles a source-word boundary, + // so we must go through read_word_at word by word. + for i in 0..full_words { + let w = words.read_word_at(self.start + i * WORD_BITS); + w.hash(state); } } - // Tail partial word (always unaligned within the source). - let rem = self.bit_len % WORD_BITS; if rem > 0 { - let tail_start = self.start + (self.bit_len / WORD_BITS) * WORD_BITS; + let tail_start = self.start + full_words * WORD_BITS; (words.read_word_at(tail_start) & low_mask(rem)).hash(state); } } diff --git a/src/bit_str/impls_for_hash/tests_for_hash.rs b/src/bit_str/impls_for_hash/tests_for_hash.rs index f6d3369..01c621c 100644 --- a/src/bit_str/impls_for_hash/tests_for_hash.rs +++ b/src/bit_str/impls_for_hash/tests_for_hash.rs @@ -82,3 +82,24 @@ fn views_across_word_boundaries() { assert_eq!(hash_one(&a.as_bit_str()), hash_one(&b.as_bit_str())); } + +/// Unaligned views must hash identically to aligned views with the same content. +#[test] +fn unaligned_view_hashes_same_as_aligned_with_same_content() { + let mut source = BitString::zeros(200); + // Set bits that span the tail region — the buggy old code computed the + // wrong tail_start (off by s bits) and wrong tail_bits (bit_len%WORD_BITS + // instead of remaining%WORD_BITS) for unaligned views. + source.set(192, true); // lives in the tail of a bit_len=194, s=3 view + source.set(193, true); + + // Unaligned: start=3, bit_len=194 (first: 61, mid: 2×64=128, tail: 5) + let unaligned = source.as_bit_str().slice_from(3).slice_until(197); + + // Aligned equivalent via roundtrip. + let owned = unaligned.to_bit_string(); + let aligned = owned.as_bit_str(); + + assert_eq!(unaligned, aligned); + assert_eq!(hash_one(&unaligned), hash_one(&aligned)); +} From 5e0958981cbff1024e74dc65a66ad23b15289ff3 Mon Sep 17 00:00:00 2001 From: juncheng Date: Thu, 25 Jun 2026 14:03:37 +0000 Subject: [PATCH 18/22] =?UTF-8?q?feat:=20Hash=20for=20BitString=20?= =?UTF-8?q?=E2=80=94=20delegates=20to=20as=5Fbit=5Fstr()?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remove derive(Hash) from BitString and add a manual impl that delegates to self.as_bit_str().hash(state), ensuring hash(bit_string) always equals hash(bit_string.as_bit_str()). Tests consolidated into bit_str/impls_for_hash/tests_for_hash.rs. Co-authored-by: Claude Co-authored-by: DeepSeek AI --- src/bit_str/impls_for_hash/tests_for_hash.rs | 31 ++++++++++++++++++++ src/bit_string.rs | 3 +- src/bit_string/impls_for_hash.rs | 10 +++++++ 3 files changed, 43 insertions(+), 1 deletion(-) create mode 100644 src/bit_string/impls_for_hash.rs diff --git a/src/bit_str/impls_for_hash/tests_for_hash.rs b/src/bit_str/impls_for_hash/tests_for_hash.rs index 01c621c..e8d0deb 100644 --- a/src/bit_str/impls_for_hash/tests_for_hash.rs +++ b/src/bit_str/impls_for_hash/tests_for_hash.rs @@ -83,6 +83,37 @@ fn views_across_word_boundaries() { assert_eq!(hash_one(&a.as_bit_str()), hash_one(&b.as_bit_str())); } +// --------------------------------------------------------------------------- +// BitString ↔ BitStr hash consistency (BitString delegates to as_bit_str) +// --------------------------------------------------------------------------- + +#[test] +fn bit_string_hash_equals_bit_str_hash() { + let bs = BitString::try_from("101100101").unwrap(); + assert_eq!(hash_one(&bs), hash_one(&bs.as_bit_str())); +} + +#[test] +fn large_bit_string_matches_bit_str() { + let mut bs = BitString::zeros(1024); + for i in (0..1024).step_by(128) { + bs.set(i, true); + } + assert_eq!(hash_one(&bs), hash_one(&bs.as_bit_str())); +} + +#[test] +fn slice_to_bit_string_roundtrip_preserves_hash() { + let source = BitString::try_from("110010101111").unwrap(); + let v = source.as_bit_str().slice_from(2).slice_until(8); + let owned = v.to_bit_string(); + assert_eq!(hash_one(&v), hash_one(&owned)); +} + +// --------------------------------------------------------------------------- +// Unaligned views +// --------------------------------------------------------------------------- + /// Unaligned views must hash identically to aligned views with the same content. #[test] fn unaligned_view_hashes_same_as_aligned_with_same_content() { diff --git a/src/bit_string.rs b/src/bit_string.rs index 019af74..97b031f 100644 --- a/src/bit_string.rs +++ b/src/bit_string.rs @@ -2,7 +2,7 @@ use alloc::vec::Vec; use crate::WORD_BITS; -#[derive(Clone, PartialEq, Eq, Hash)] +#[derive(Clone, PartialEq, Eq)] pub struct BitString { words: Vec, bit_len: usize, @@ -40,6 +40,7 @@ mod impls_for_bit_arith; mod impls_for_construction; mod impls_for_editing; mod impls_for_fmt; +mod impls_for_hash; mod impls_for_iter; mod impls_for_matching; mod impls_for_predicates; diff --git a/src/bit_string/impls_for_hash.rs b/src/bit_string/impls_for_hash.rs new file mode 100644 index 0000000..cf0568e --- /dev/null +++ b/src/bit_string/impls_for_hash.rs @@ -0,0 +1,10 @@ +use core::hash::{Hash, Hasher}; + +use crate::BitString; + +impl Hash for BitString { + #[inline] + fn hash(&self, state: &mut H) { + self.as_bit_str().hash(state); + } +} From a8fdf67611f8f118620ec97adde07594f4caa939 Mon Sep 17 00:00:00 2001 From: juncheng Date: Thu, 25 Jun 2026 14:04:54 +0000 Subject: [PATCH 19/22] feat: PartialOrd + Ord for BitStr with SIMD-accelerated comparison MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit BitOrd trait (src/traits/bit_ord.rs): - bitwise_cmp(self, other) on u64 — LSB-first lexicographic ordering via trailing_zeros (tzcnt), shared by all SIMD backends. BitsOrd trait (src/traits/bits_ord.rs): - cmp_words(&self, other, count, offset) on [u64] — follows the same pattern as BitsEq: a single method taking self's intra-word offset, with other always word-aligned. Dispatches to aligned or unaligned SIMD based on offset % 64. SIMD backends (AVX2 4×u64 / SSE2 2×u64 / NEON 2×u64): - Aligned: pcmpeqq + movemask + trailing_ones to locate first diff. - Unaligned: shifted-window (src[i]>>shift)|(src[i+1]<<(64-shift)). - Both backends tested against scalar oracle via proptest. BitStr::cmp dispatch (src/bit_str/impls_for_ord.rs): - Follows bits_equal_at pattern: prefer word-aligned side as needle. - Falls back to scalar read_word_at when both sides are unaligned (rare). Benchmarks: benches/hash.rs + benches/ord.rs — compare BitString, BitStr (unaligned), and String/str across 64/4096/65536 bits. Co-authored-by: Claude Co-authored-by: DeepSeek AI --- Cargo.toml | 8 + benches/hash.rs | 282 ++++++++++++++++++ benches/ord.rs | 242 +++++++++++++++ src/bit_str.rs | 1 + src/bit_str/impls_for_ord.rs | 88 ++++++ src/bit_str/impls_for_ord/tests_for_ord.rs | 213 +++++++++++++ src/traits.rs | 4 + src/traits/bit_ord.rs | 21 ++ src/traits/bits_ord.rs | 25 ++ .../bits_ord/funcs_for_cmp_aligned_core.rs | 173 +++++++++++ .../tests_for_backend_equivalence.rs | 94 ++++++ .../bits_ord/funcs_for_cmp_unaligned_core.rs | 249 ++++++++++++++++ .../tests_for_backend_equivalence.rs | 95 ++++++ src/traits/bits_ord/impls_for_u64_slice.rs | 22 ++ 14 files changed, 1517 insertions(+) create mode 100644 benches/hash.rs create mode 100644 benches/ord.rs create mode 100644 src/bit_str/impls_for_ord.rs create mode 100644 src/bit_str/impls_for_ord/tests_for_ord.rs create mode 100644 src/traits/bit_ord.rs create mode 100644 src/traits/bits_ord.rs create mode 100644 src/traits/bits_ord/funcs_for_cmp_aligned_core.rs create mode 100644 src/traits/bits_ord/funcs_for_cmp_aligned_core/tests_for_backend_equivalence.rs create mode 100644 src/traits/bits_ord/funcs_for_cmp_unaligned_core.rs create mode 100644 src/traits/bits_ord/funcs_for_cmp_unaligned_core/tests_for_backend_equivalence.rs create mode 100644 src/traits/bits_ord/impls_for_u64_slice.rs diff --git a/Cargo.toml b/Cargo.toml index f8326f9..1dbdc7a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -111,3 +111,11 @@ harness = false [[bench]] name = "bit_ops_shl_assign" harness = false + +[[bench]] +name = "hash" +harness = false + +[[bench]] +name = "ord" +harness = false diff --git a/benches/hash.rs b/benches/hash.rs new file mode 100644 index 0000000..dacb8d1 --- /dev/null +++ b/benches/hash.rs @@ -0,0 +1,282 @@ +use std::collections::hash_map::DefaultHasher; +use std::hash::{Hash, Hasher}; + +use bit_string::BitString; +use divan::{Bencher, black_box}; + +fn main() { + divan::main(); +} + +#[derive(Clone, Copy)] +enum Pattern { + Dense, + Sparse, + Alternating, +} + +// --------------------------------------------------------------------------- +// BitString +// --------------------------------------------------------------------------- + +#[divan::bench(name = "hash/len_64/dense/bit_string")] +fn hash_len_64_dense_bit_string(b: Bencher) { + bench_bit_string(b, 64, Pattern::Dense); +} + +#[divan::bench(name = "hash/len_64/dense/bit_str")] +fn hash_len_64_dense_bit_str(b: Bencher) { + bench_bit_str(b, 64, Pattern::Dense); +} + +#[divan::bench(name = "hash/len_64/dense/string")] +fn hash_len_64_dense_string(b: Bencher) { + bench_string(b, 64, Pattern::Dense); +} + +#[divan::bench(name = "hash/len_64/dense/str")] +fn hash_len_64_dense_str(b: Bencher) { + bench_str(b, 64, Pattern::Dense); +} + +#[divan::bench(name = "hash/len_64/sparse/bit_string")] +fn hash_len_64_sparse_bit_string(b: Bencher) { + bench_bit_string(b, 64, Pattern::Sparse); +} + +#[divan::bench(name = "hash/len_64/sparse/bit_str")] +fn hash_len_64_sparse_bit_str(b: Bencher) { + bench_bit_str(b, 64, Pattern::Sparse); +} + +#[divan::bench(name = "hash/len_64/sparse/string")] +fn hash_len_64_sparse_string(b: Bencher) { + bench_string(b, 64, Pattern::Sparse); +} + +#[divan::bench(name = "hash/len_64/sparse/str")] +fn hash_len_64_sparse_str(b: Bencher) { + bench_str(b, 64, Pattern::Sparse); +} + +#[divan::bench(name = "hash/len_64/alternating/bit_string")] +fn hash_len_64_alternating_bit_string(b: Bencher) { + bench_bit_string(b, 64, Pattern::Alternating); +} + +#[divan::bench(name = "hash/len_64/alternating/bit_str")] +fn hash_len_64_alternating_bit_str(b: Bencher) { + bench_bit_str(b, 64, Pattern::Alternating); +} + +#[divan::bench(name = "hash/len_64/alternating/string")] +fn hash_len_64_alternating_string(b: Bencher) { + bench_string(b, 64, Pattern::Alternating); +} + +#[divan::bench(name = "hash/len_64/alternating/str")] +fn hash_len_64_alternating_str(b: Bencher) { + bench_str(b, 64, Pattern::Alternating); +} + +// --------------------------------------------------------------------------- +// len = 4096 +// --------------------------------------------------------------------------- + +#[divan::bench(name = "hash/len_4096/dense/bit_string")] +fn hash_len_4096_dense_bit_string(b: Bencher) { + bench_bit_string(b, 4096, Pattern::Dense); +} + +#[divan::bench(name = "hash/len_4096/dense/bit_str")] +fn hash_len_4096_dense_bit_str(b: Bencher) { + bench_bit_str(b, 4096, Pattern::Dense); +} + +#[divan::bench(name = "hash/len_4096/dense/string")] +fn hash_len_4096_dense_string(b: Bencher) { + bench_string(b, 4096, Pattern::Dense); +} + +#[divan::bench(name = "hash/len_4096/dense/str")] +fn hash_len_4096_dense_str(b: Bencher) { + bench_str(b, 4096, Pattern::Dense); +} + +#[divan::bench(name = "hash/len_4096/sparse/bit_string")] +fn hash_len_4096_sparse_bit_string(b: Bencher) { + bench_bit_string(b, 4096, Pattern::Sparse); +} + +#[divan::bench(name = "hash/len_4096/sparse/bit_str")] +fn hash_len_4096_sparse_bit_str(b: Bencher) { + bench_bit_str(b, 4096, Pattern::Sparse); +} + +#[divan::bench(name = "hash/len_4096/sparse/string")] +fn hash_len_4096_sparse_string(b: Bencher) { + bench_string(b, 4096, Pattern::Sparse); +} + +#[divan::bench(name = "hash/len_4096/sparse/str")] +fn hash_len_4096_sparse_str(b: Bencher) { + bench_str(b, 4096, Pattern::Sparse); +} + +#[divan::bench(name = "hash/len_4096/alternating/bit_string")] +fn hash_len_4096_alternating_bit_string(b: Bencher) { + bench_bit_string(b, 4096, Pattern::Alternating); +} + +#[divan::bench(name = "hash/len_4096/alternating/bit_str")] +fn hash_len_4096_alternating_bit_str(b: Bencher) { + bench_bit_str(b, 4096, Pattern::Alternating); +} + +#[divan::bench(name = "hash/len_4096/alternating/string")] +fn hash_len_4096_alternating_string(b: Bencher) { + bench_string(b, 4096, Pattern::Alternating); +} + +#[divan::bench(name = "hash/len_4096/alternating/str")] +fn hash_len_4096_alternating_str(b: Bencher) { + bench_str(b, 4096, Pattern::Alternating); +} + +// --------------------------------------------------------------------------- +// len = 65536 +// --------------------------------------------------------------------------- + +#[divan::bench(name = "hash/len_65536/dense/bit_string")] +fn hash_len_65536_dense_bit_string(b: Bencher) { + bench_bit_string(b, 65536, Pattern::Dense); +} + +#[divan::bench(name = "hash/len_65536/dense/bit_str")] +fn hash_len_65536_dense_bit_str(b: Bencher) { + bench_bit_str(b, 65536, Pattern::Dense); +} + +#[divan::bench(name = "hash/len_65536/dense/string")] +fn hash_len_65536_dense_string(b: Bencher) { + bench_string(b, 65536, Pattern::Dense); +} + +#[divan::bench(name = "hash/len_65536/dense/str")] +fn hash_len_65536_dense_str(b: Bencher) { + bench_str(b, 65536, Pattern::Dense); +} + +#[divan::bench(name = "hash/len_65536/sparse/bit_string")] +fn hash_len_65536_sparse_bit_string(b: Bencher) { + bench_bit_string(b, 65536, Pattern::Sparse); +} + +#[divan::bench(name = "hash/len_65536/sparse/bit_str")] +fn hash_len_65536_sparse_bit_str(b: Bencher) { + bench_bit_str(b, 65536, Pattern::Sparse); +} + +#[divan::bench(name = "hash/len_65536/sparse/string")] +fn hash_len_65536_sparse_string(b: Bencher) { + bench_string(b, 65536, Pattern::Sparse); +} + +#[divan::bench(name = "hash/len_65536/sparse/str")] +fn hash_len_65536_sparse_str(b: Bencher) { + bench_str(b, 65536, Pattern::Sparse); +} + +#[divan::bench(name = "hash/len_65536/alternating/bit_string")] +fn hash_len_65536_alternating_bit_string(b: Bencher) { + bench_bit_string(b, 65536, Pattern::Alternating); +} + +#[divan::bench(name = "hash/len_65536/alternating/bit_str")] +fn hash_len_65536_alternating_bit_str(b: Bencher) { + bench_bit_str(b, 65536, Pattern::Alternating); +} + +#[divan::bench(name = "hash/len_65536/alternating/string")] +fn hash_len_65536_alternating_string(b: Bencher) { + bench_string(b, 65536, Pattern::Alternating); +} + +#[divan::bench(name = "hash/len_65536/alternating/str")] +fn hash_len_65536_alternating_str(b: Bencher) { + bench_str(b, 65536, Pattern::Alternating); +} + +// --------------------------------------------------------------------------- +// Bench helpers +// --------------------------------------------------------------------------- + +fn bench_bit_string(bencher: Bencher, len: usize, pattern: Pattern) { + let bits = make_bit_string(len, pattern); + bencher.bench(|| { + let mut h = DefaultHasher::new(); + black_box(&bits).hash(&mut h); + black_box(h.finish()) + }); +} + +fn bench_bit_str(bencher: Bencher, len: usize, pattern: Pattern) { + let bits = make_bit_string(len, pattern); + let view = bits.as_bit_str(); + bencher.bench(|| { + let mut h = DefaultHasher::new(); + black_box(&view).hash(&mut h); + black_box(h.finish()) + }); +} + +fn bench_string(bencher: Bencher, len: usize, pattern: Pattern) { + let s = make_string(len, pattern); + bencher.bench(|| { + let mut h = DefaultHasher::new(); + black_box(&s).hash(&mut h); + black_box(h.finish()) + }); +} + +fn bench_str(bencher: Bencher, len: usize, pattern: Pattern) { + let s = make_string(len, pattern); + bencher.bench(|| { + let mut h = DefaultHasher::new(); + black_box(s.as_str()).hash(&mut h); + black_box(h.finish()) + }); +} + +// --------------------------------------------------------------------------- +// Constructors +// --------------------------------------------------------------------------- + +#[inline] +fn make_bit_string(len: usize, pattern: Pattern) -> BitString { + (0..len).map(|index| bit_at(index, pattern)).collect() +} + +#[inline] +fn make_string(len: usize, pattern: Pattern) -> String { + (0..len) + .map(|index| if bit_at(index, pattern) { '1' } else { '0' }) + .collect() +} + +#[inline] +fn bit_at(index: usize, pattern: Pattern) -> bool { + match pattern { + Pattern::Dense => mix64(index as u64) & 1 != 0, + Pattern::Sparse => mix64(index as u64) & 63 == 0, + Pattern::Alternating => index % 2 != 0, + } +} + +#[inline] +fn mix64(mut value: u64) -> u64 { + value = value.wrapping_add(0x9e37_79b9_7f4a_7c15); + value = (value ^ (value >> 30)).wrapping_mul(0xbf58_476d_1ce4_e5b9); + value = (value ^ (value >> 27)).wrapping_mul(0x94d0_49bb_1331_11eb); + value ^ (value >> 31) +} diff --git a/benches/ord.rs b/benches/ord.rs new file mode 100644 index 0000000..d42a39b --- /dev/null +++ b/benches/ord.rs @@ -0,0 +1,242 @@ +use bit_string::BitString; +use divan::{Bencher, black_box}; + +fn main() { + divan::main(); +} + +// --------------------------------------------------------------------------- +// len = 64 +// --------------------------------------------------------------------------- + +#[divan::bench(name = "cmp/len_64/identical/bit_string")] +fn cmp_len_64_identical_bit_string(b: Bencher) { + bench_bit_string(b, 64, CmpCase::Identical); +} + +#[divan::bench(name = "cmp/len_64/identical/bit_str_unaligned")] +fn cmp_len_64_identical_bit_str_unaligned(b: Bencher) { + bench_bit_str_unaligned(b, 64, CmpCase::Identical); +} + +#[divan::bench(name = "cmp/len_64/identical/string")] +fn cmp_len_64_identical_string(b: Bencher) { + bench_string(b, 64, CmpCase::Identical); +} + +#[divan::bench(name = "cmp/len_64/diff_first/bit_string")] +fn cmp_len_64_diff_first_bit_string(b: Bencher) { + bench_bit_string(b, 64, CmpCase::DifferAtFirst); +} + +#[divan::bench(name = "cmp/len_64/diff_first/bit_str_unaligned")] +fn cmp_len_64_diff_first_bit_str_unaligned(b: Bencher) { + bench_bit_str_unaligned(b, 64, CmpCase::DifferAtFirst); +} + +#[divan::bench(name = "cmp/len_64/diff_first/string")] +fn cmp_len_64_diff_first_string(b: Bencher) { + bench_string(b, 64, CmpCase::DifferAtFirst); +} + +#[divan::bench(name = "cmp/len_64/diff_last/bit_string")] +fn cmp_len_64_diff_last_bit_string(b: Bencher) { + bench_bit_string(b, 64, CmpCase::DifferAtLast); +} + +#[divan::bench(name = "cmp/len_64/diff_last/bit_str_unaligned")] +fn cmp_len_64_diff_last_bit_str_unaligned(b: Bencher) { + bench_bit_str_unaligned(b, 64, CmpCase::DifferAtLast); +} + +#[divan::bench(name = "cmp/len_64/diff_last/string")] +fn cmp_len_64_diff_last_string(b: Bencher) { + bench_string(b, 64, CmpCase::DifferAtLast); +} + +// --------------------------------------------------------------------------- +// len = 4096 +// --------------------------------------------------------------------------- + +#[divan::bench(name = "cmp/len_4096/identical/bit_string")] +fn cmp_len_4096_identical_bit_string(b: Bencher) { + bench_bit_string(b, 4096, CmpCase::Identical); +} + +#[divan::bench(name = "cmp/len_4096/identical/bit_str_unaligned")] +fn cmp_len_4096_identical_bit_str_unaligned(b: Bencher) { + bench_bit_str_unaligned(b, 4096, CmpCase::Identical); +} + +#[divan::bench(name = "cmp/len_4096/identical/string")] +fn cmp_len_4096_identical_string(b: Bencher) { + bench_string(b, 4096, CmpCase::Identical); +} + +#[divan::bench(name = "cmp/len_4096/diff_first/bit_string")] +fn cmp_len_4096_diff_first_bit_string(b: Bencher) { + bench_bit_string(b, 4096, CmpCase::DifferAtFirst); +} + +#[divan::bench(name = "cmp/len_4096/diff_first/bit_str_unaligned")] +fn cmp_len_4096_diff_first_bit_str_unaligned(b: Bencher) { + bench_bit_str_unaligned(b, 4096, CmpCase::DifferAtFirst); +} + +#[divan::bench(name = "cmp/len_4096/diff_first/string")] +fn cmp_len_4096_diff_first_string(b: Bencher) { + bench_string(b, 4096, CmpCase::DifferAtFirst); +} + +#[divan::bench(name = "cmp/len_4096/diff_last/bit_string")] +fn cmp_len_4096_diff_last_bit_string(b: Bencher) { + bench_bit_string(b, 4096, CmpCase::DifferAtLast); +} + +#[divan::bench(name = "cmp/len_4096/diff_last/bit_str_unaligned")] +fn cmp_len_4096_diff_last_bit_str_unaligned(b: Bencher) { + bench_bit_str_unaligned(b, 4096, CmpCase::DifferAtLast); +} + +#[divan::bench(name = "cmp/len_4096/diff_last/string")] +fn cmp_len_4096_diff_last_string(b: Bencher) { + bench_string(b, 4096, CmpCase::DifferAtLast); +} + +// --------------------------------------------------------------------------- +// len = 65536 +// --------------------------------------------------------------------------- + +#[divan::bench(name = "cmp/len_65536/identical/bit_string")] +fn cmp_len_65536_identical_bit_string(b: Bencher) { + bench_bit_string(b, 65536, CmpCase::Identical); +} + +#[divan::bench(name = "cmp/len_65536/identical/bit_str_unaligned")] +fn cmp_len_65536_identical_bit_str_unaligned(b: Bencher) { + bench_bit_str_unaligned(b, 65536, CmpCase::Identical); +} + +#[divan::bench(name = "cmp/len_65536/identical/string")] +fn cmp_len_65536_identical_string(b: Bencher) { + bench_string(b, 65536, CmpCase::Identical); +} + +#[divan::bench(name = "cmp/len_65536/diff_first/bit_string")] +fn cmp_len_65536_diff_first_bit_string(b: Bencher) { + bench_bit_string(b, 65536, CmpCase::DifferAtFirst); +} + +#[divan::bench(name = "cmp/len_65536/diff_first/bit_str_unaligned")] +fn cmp_len_65536_diff_first_bit_str_unaligned(b: Bencher) { + bench_bit_str_unaligned(b, 65536, CmpCase::DifferAtFirst); +} + +#[divan::bench(name = "cmp/len_65536/diff_first/string")] +fn cmp_len_65536_diff_first_string(b: Bencher) { + bench_string(b, 65536, CmpCase::DifferAtFirst); +} + +#[divan::bench(name = "cmp/len_65536/diff_last/bit_string")] +fn cmp_len_65536_diff_last_bit_string(b: Bencher) { + bench_bit_string(b, 65536, CmpCase::DifferAtLast); +} + +#[divan::bench(name = "cmp/len_65536/diff_last/bit_str_unaligned")] +fn cmp_len_65536_diff_last_bit_str_unaligned(b: Bencher) { + bench_bit_str_unaligned(b, 65536, CmpCase::DifferAtLast); +} + +#[divan::bench(name = "cmp/len_65536/diff_last/string")] +fn cmp_len_65536_diff_last_string(b: Bencher) { + bench_string(b, 65536, CmpCase::DifferAtLast); +} + +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- + +#[derive(Clone, Copy)] +enum CmpCase { + Identical, + DifferAtFirst, + DifferAtLast, +} + +fn bench_bit_string(bencher: Bencher, len: usize, case: CmpCase) { + let (a, b) = make_pair(len, case); + let va = a.as_bit_str(); + let vb = b.as_bit_str(); + bencher.bench(|| black_box(va).cmp(&black_box(vb))); +} + +/// Unaligned view (start=3) vs aligned `as_bit_str()`. +fn bench_bit_str_unaligned(bencher: Bencher, len: usize, case: CmpCase) { + let base: BitString = (0..len + 3).map(|i| mix64(i as u64) & 1 != 0).collect(); + let va = base.as_bit_str().slice_from(3).slice_until(3 + len); + // Exact aligned copy of the unaligned view's bits, then flip one + // bit if needed. + let mut vb = va.to_bit_string(); + match case { + CmpCase::Identical => {} + CmpCase::DifferAtFirst => _ = vb.set(0, !vb.get(0).unwrap()), + CmpCase::DifferAtLast => _ = vb.set(len - 1, !vb.get(len - 1).unwrap()), + } + bencher.bench(|| black_box(va).cmp(&black_box(vb.as_bit_str()))); +} + +fn bench_string(bencher: Bencher, len: usize, case: CmpCase) { + let (a, b) = make_string_pair(len, case); + bencher.bench(|| black_box(a.as_str()).cmp(black_box(b.as_str()))); +} + +// --------------------------------------------------------------------------- +// Constructors +// --------------------------------------------------------------------------- + +/// Build two aligned `BitString`s for comparison. +fn make_pair(len: usize, case: CmpCase) -> (BitString, BitString) { + let base: BitString = (0..len).map(|i| mix64(i as u64) & 1 != 0).collect(); + match case { + CmpCase::Identical => (base.clone(), base), + CmpCase::DifferAtFirst => { + let mut b = base.clone(); + b.set(0, !base.get(0).unwrap()); + (base, b) + } + CmpCase::DifferAtLast => { + let mut b = base.clone(); + b.set(len - 1, !base.get(len - 1).unwrap()); + (base, b) + } + } +} + +fn make_string_pair(len: usize, case: CmpCase) -> (String, String) { + let a: String = (0..len) + .map(|i| if mix64(i as u64) & 1 != 0 { '1' } else { '0' }) + .collect(); + let b = match case { + CmpCase::Identical => a.clone(), + _ => (0..len) + .map(|i| { + let base = mix64(i as u64) & 1 != 0; + let flip = match case { + CmpCase::DifferAtFirst => i == 0, + CmpCase::DifferAtLast => i == len - 1, + _ => unreachable!(), + }; + if base ^ flip { '1' } else { '0' } + }) + .collect(), + }; + (a, b) +} + +#[inline] +fn mix64(mut value: u64) -> u64 { + value = value.wrapping_add(0x9e37_79b9_7f4a_7c15); + value = (value ^ (value >> 30)).wrapping_mul(0xbf58_476d_1ce4_e5b9); + value = (value ^ (value >> 27)).wrapping_mul(0x94d0_49bb_1331_11eb); + value ^ (value >> 31) +} diff --git a/src/bit_str.rs b/src/bit_str.rs index 97475d5..4a81d5b 100644 --- a/src/bit_str.rs +++ b/src/bit_str.rs @@ -32,6 +32,7 @@ mod impls_for_fmt; mod impls_for_hash; mod impls_for_iter; mod impls_for_matching; +mod impls_for_ord; mod impls_for_predicates; mod impls_for_slice; diff --git a/src/bit_str/impls_for_ord.rs b/src/bit_str/impls_for_ord.rs new file mode 100644 index 0000000..dfe71df --- /dev/null +++ b/src/bit_str/impls_for_ord.rs @@ -0,0 +1,88 @@ +use core::cmp::Ordering; + +use crate::traits::*; +use crate::{WORD_BITS, low_mask}; + +use crate::BitStr; + +impl<'bs> BitStr<'bs> { + /// Lexicographic comparison of two bit strings. + /// + /// Compares bits from index 0 upward. At the first differing bit, the + /// side with a `1` is greater. When all bits in the common prefix are + /// equal, the longer bit string is greater. + #[inline] + pub fn cmp(&self, other: &BitStr<'bs>) -> Ordering { + let common = self.bit_len.min(other.bit_len); + if common == 0 { + return self.bit_len.cmp(&other.bit_len); + } + + let hs_words = self.source.words(); + let nd_words = other.source.words(); + let hs_base = self.start; + let nd_base = other.start; + let hs_aligned = hs_base % WORD_BITS == 0; + let nd_aligned = nd_base % WORD_BITS == 0; + + let full = common / WORD_BITS; + + // Full-word comparison — follow the same dispatch pattern as + // [`BitsEq::eq_words`]: SIMD when `other` is word-aligned. + if nd_aligned { + let nd_slice = &nd_words[nd_base / WORD_BITS..]; + if let Some(ord) = hs_words.cmp_words(nd_slice, full, hs_base) { + return ord; + } + } else if hs_aligned { + // Only `self` is aligned — swap so `other` becomes the + // word-aligned reference, then reverse. + let hs_slice = &hs_words[hs_base / WORD_BITS..]; + if let Some(ord) = nd_words.cmp_words(hs_slice, full, nd_base) { + return ord.reverse(); + } + } else { + // Both sides misaligned — scalar word-at-a-time (rare). + for i in 0..full { + let pos = i * WORD_BITS; + let a = hs_words.read_word_at(hs_base + pos); + let b = nd_words.read_word_at(nd_base + pos); + if a != b { + return a.bitwise_cmp(b); + } + } + } + + // Partial tail word. + let rem = common % WORD_BITS; + if rem > 0 { + let pos = full * WORD_BITS; + let mask = low_mask(rem); + let a = hs_words.read_word_at(hs_base + pos) & mask; + let b = nd_words.read_word_at(nd_base + pos) & mask; + if a != b { + return a.bitwise_cmp(b); + } + } + + // Common prefix identical — longer wins. + self.bit_len.cmp(&other.bit_len) + } +} + +impl PartialOrd for BitStr<'_> { + #[inline] + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for BitStr<'_> { + #[inline] + fn cmp(&self, other: &Self) -> Ordering { + self.cmp(other) + } +} + +#[cfg(test)] +mod tests_for_ord; diff --git a/src/bit_str/impls_for_ord/tests_for_ord.rs b/src/bit_str/impls_for_ord/tests_for_ord.rs new file mode 100644 index 0000000..100969d --- /dev/null +++ b/src/bit_str/impls_for_ord/tests_for_ord.rs @@ -0,0 +1,213 @@ +use alloc::string::{String, ToString}; +use alloc::vec; +use alloc::vec::Vec; +use core::cmp::Ordering; + +use crate::BitString; + +// --------------------------------------------------------------------------- +// Basic equal / different +// --------------------------------------------------------------------------- + +#[test] +fn equal_strings_are_equal() { + let a = BitString::try_from("101001").unwrap(); + let b = BitString::try_from("101001").unwrap(); + assert_eq!(a.as_bit_str().cmp(&b.as_bit_str()), Ordering::Equal); +} + +#[test] +fn different_at_first_bit() { + // "0..." < "1..." + let a = BitString::try_from("011").unwrap(); + let b = BitString::try_from("111").unwrap(); + assert_eq!(a.as_bit_str().cmp(&b.as_bit_str()), Ordering::Less); + assert_eq!(b.as_bit_str().cmp(&a.as_bit_str()), Ordering::Greater); +} + +#[test] +fn different_at_later_bit() { + // "101" vs "100" — differ at bit 2 + let a = BitString::try_from("100").unwrap(); + let b = BitString::try_from("101").unwrap(); + assert_eq!(a.as_bit_str().cmp(&b.as_bit_str()), Ordering::Less); + assert_eq!(b.as_bit_str().cmp(&a.as_bit_str()), Ordering::Greater); +} + +// --------------------------------------------------------------------------- +// Prefix relationship — longer wins +// --------------------------------------------------------------------------- + +#[test] +fn prefix_shorter_is_less() { + let a = BitString::try_from("101").unwrap(); + let b = BitString::try_from("1010").unwrap(); + assert_eq!(a.as_bit_str().cmp(&b.as_bit_str()), Ordering::Less); +} + +#[test] +fn longer_prefix_is_greater() { + let a = BitString::try_from("1010").unwrap(); + let b = BitString::try_from("101").unwrap(); + assert_eq!(a.as_bit_str().cmp(&b.as_bit_str()), Ordering::Greater); +} + +// --------------------------------------------------------------------------- +// Empty +// --------------------------------------------------------------------------- + +#[test] +fn empty_is_less_than_nonempty() { + let a = BitString::new(); + let b = BitString::try_from("0").unwrap(); + assert_eq!(a.as_bit_str().cmp(&b.as_bit_str()), Ordering::Less); +} + +#[test] +fn empty_equals_empty() { + let a = BitString::new(); + let b = BitString::new(); + assert_eq!(a.as_bit_str().cmp(&b.as_bit_str()), Ordering::Equal); +} + +// --------------------------------------------------------------------------- +// Cross-word boundary (aligned fast path) +// --------------------------------------------------------------------------- + +#[test] +fn cross_word_boundary_same_length() { + // 128 bits each, differ in the second word + let mut a = BitString::zeros(128); + let mut b = BitString::zeros(128); + a.set(70, true); // second word, bit 6 + b.set(70, false); + b.set(71, true); // second word, bit 7 + + // a[0..70] == b[0..70] (all zeros), a[70]=1, b[70]=0 → a > b + assert_eq!(a.as_bit_str().cmp(&b.as_bit_str()), Ordering::Greater); + assert_eq!(b.as_bit_str().cmp(&a.as_bit_str()), Ordering::Less); +} + +#[test] +fn cross_word_boundary_different_in_first_word() { + let mut a = BitString::zeros(130); + let b = BitString::zeros(130); + a.set(3, true); + // a > b (a[3]=1, b[3]=0, all others zero) + assert_eq!(a.as_bit_str().cmp(&b.as_bit_str()), Ordering::Greater); +} + +// --------------------------------------------------------------------------- +// Offset views (unaligned) +// --------------------------------------------------------------------------- + +#[test] +fn unaligned_views_compare_correctly() { + let source = BitString::try_from("111111111100000000001111111111").unwrap(); + // view starting at bit 3: "111111100000000001111111111" + // view starting at bit 4: "11111100000000001111111111" + let v1 = source.as_bit_str().slice_from(3); + let v2 = source.as_bit_str().slice_from(4); + // v1 > v2 because at common offset, v1[0]=1, v2[0]=1 (equal at bit 0 + // of the views), and eventually v1 is longer + // Actually v1 is one bit longer + assert_eq!(v1.cmp(&v2), Ordering::Greater); +} + +#[test] +fn unaligned_vs_aligned_same_content() { + let source = BitString::zeros(200); + let v = source.as_bit_str().slice_from(3).slice_until(197); + let owned = v.to_bit_string(); + assert_eq!(v.cmp(&owned.as_bit_str()), Ordering::Equal); +} + +// --------------------------------------------------------------------------- +// PartialOrd / Ord trait usage +// --------------------------------------------------------------------------- + +#[test] +fn partial_cmp_is_some() { + let a = BitString::try_from("101").unwrap(); + let b = BitString::try_from("100").unwrap(); + assert_eq!( + a.as_bit_str().partial_cmp(&b.as_bit_str()), + Some(Ordering::Greater) + ); +} + +#[test] +fn sort_bit_strs() { + let mut strings: Vec = ["101", "001", "111", "000", "010"] + .into_iter() + .map(|s| BitString::try_from(s).unwrap()) + .collect(); + strings.sort_by(|a, b| a.as_bit_str().cmp(&b.as_bit_str())); + + 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 min = a.as_bit_str().min(b.as_bit_str()); + let max = a.as_bit_str().max(b.as_bit_str()); + assert_eq!(min, a.as_bit_str()); + assert_eq!(max, b.as_bit_str()); +} + +// --------------------------------------------------------------------------- +// Transitivity / consistency +// --------------------------------------------------------------------------- + +#[test] +fn eq_implies_not_less_and_not_greater() { + let a = BitString::try_from("101010").unwrap(); + let b = BitString::try_from("101010").unwrap(); + let va = a.as_bit_str(); + let vb = b.as_bit_str(); + assert_eq!(va, vb); + assert!(!(va < vb)); + assert!(!(va > vb)); + assert!(va <= vb); + assert!(va >= vb); +} + +#[test] +fn ordering_consistent_with_equality() { + for bits in ["", "0", "1", "10", "01", "111", "000", "101010"] { + let bs = BitString::try_from(bits).unwrap(); + let v = bs.as_bit_str(); + assert_eq!(v.cmp(&v), Ordering::Equal, "self-cmp failed for {bits:?}"); + } +} + +// --------------------------------------------------------------------------- +// Large input — SIMD paths +// --------------------------------------------------------------------------- + +#[test] +fn large_identical() { + let a = BitString::zeros(65536); + let b = BitString::zeros(65536); + assert_eq!(a.as_bit_str().cmp(&b.as_bit_str()), Ordering::Equal); +} + +#[test] +fn large_differ_at_last_bit() { + let a = BitString::zeros(65536); + let mut b = BitString::zeros(65536); + b.set(65535, true); + // a < b (a[65535]=0, b[65535]=1) + assert_eq!(a.as_bit_str().cmp(&b.as_bit_str()), Ordering::Less); +} + +#[test] +fn large_differ_at_first_bit() { + let a = BitString::zeros(65536); + let mut b = BitString::zeros(65536); + b.set(0, true); + assert_eq!(a.as_bit_str().cmp(&b.as_bit_str()), Ordering::Less); +} diff --git a/src/traits.rs b/src/traits.rs index f3c8388..2cd7831 100644 --- a/src/traits.rs +++ b/src/traits.rs @@ -1,9 +1,13 @@ +pub(crate) mod bit_ord; pub(crate) mod bits_arith; pub(crate) mod bits_edit; pub(crate) mod bits_eq; pub(crate) mod bits_find; +pub(crate) mod bits_ord; +pub(crate) use bit_ord::*; pub(crate) use bits_arith::*; pub(crate) use bits_edit::*; pub(crate) use bits_eq::*; pub(crate) use bits_find::*; +pub(crate) use bits_ord::*; diff --git a/src/traits/bit_ord.rs b/src/traits/bit_ord.rs new file mode 100644 index 0000000..803f42e --- /dev/null +++ b/src/traits/bit_ord.rs @@ -0,0 +1,21 @@ +use core::cmp::Ordering; + +/// Lexicographic comparison of two `u64` values (LSB-first bit order). +/// +/// This is the word-level primitive used by [`BitsOrd`](super::BitsOrd) +/// to resolve ordering once the first differing word has been found. +pub(crate) trait BitOrd { + fn bitwise_cmp(self, other: Self) -> Ordering; +} + +impl BitOrd for u64 { + #[inline] + fn bitwise_cmp(self, other: u64) -> Ordering { + debug_assert!(self != other); + let diff = self ^ other; + let first = diff.trailing_zeros(); + let a_bit = (self >> first) & 1; + let b_bit = (other >> first) & 1; + a_bit.cmp(&b_bit) + } +} diff --git a/src/traits/bits_ord.rs b/src/traits/bits_ord.rs new file mode 100644 index 0000000..40a2498 --- /dev/null +++ b/src/traits/bits_ord.rs @@ -0,0 +1,25 @@ +use core::cmp::Ordering; + +/// Word-level lexicographic comparison on `[u64]` backing storage. +/// +/// Compares words from index 0 upward (LSB-first bit order within each word). +/// Returns `Some(Ordering)` at the first differing word, or `None` when all +/// `count` words are identical. +/// +/// `other` must be word-aligned. `self` may have an intra-word `offset` +/// (0 = word-aligned), in which case each logical word is reconstructed as +/// a shifted window `(self[i] >> shift) | (self[i+1] << (64-shift))`. +/// +/// Both paths dispatch to SIMD backends when available: +/// - AVX2 (x86/x86_64, 4×u64 per iteration) +/// - SSE2 (x86/x86_64, 2×u64 per iteration) +/// - NEON (aarch64, 2×u64 per iteration) +/// +/// Short inputs fall back to scalar in all backends. +pub(crate) trait BitsOrd { + fn cmp_words(&self, other: &[u64], count: usize, offset: usize) -> Option; +} + +pub(crate) mod funcs_for_cmp_aligned_core; +pub(crate) mod funcs_for_cmp_unaligned_core; +pub(crate) mod impls_for_u64_slice; diff --git a/src/traits/bits_ord/funcs_for_cmp_aligned_core.rs b/src/traits/bits_ord/funcs_for_cmp_aligned_core.rs new file mode 100644 index 0000000..ba16e56 --- /dev/null +++ b/src/traits/bits_ord/funcs_for_cmp_aligned_core.rs @@ -0,0 +1,173 @@ +use core::cmp::Ordering; + +use crate::SMALL_WORDS; +use crate::traits::BitOrd; + +/// Returns `Some(Ordering)` if the first `count` aligned words of `src` +/// and `other` differ, otherwise `None` (all equal). +/// +/// Dispatches to the best available SIMD backend at compile time. +/// Short inputs fall back to scalar. +#[inline] +pub(super) fn cmp_aligned_words(src: &[u64], other: &[u64], count: usize) -> Option { + if count < SMALL_WORDS { + return scalar_cmp_aligned(src, other, count); + } + + #[cfg(all( + any(target_arch = "x86", target_arch = "x86_64"), + target_feature = "avx2" + ))] + { + return unsafe { avx2::cmp_aligned(src, other, count) }; + } + + #[cfg(all( + any(target_arch = "x86", target_arch = "x86_64"), + target_feature = "sse2", + not(target_feature = "avx2") + ))] + { + return unsafe { sse2::cmp_aligned(src, other, count) }; + } + + #[cfg(all(target_arch = "aarch64", target_feature = "neon"))] + { + return unsafe { neon::cmp_aligned(src, other, count) }; + } + + #[allow(unreachable_code)] + scalar_cmp_aligned(src, other, count) +} + +#[inline] +fn scalar_cmp_aligned(src: &[u64], other: &[u64], count: usize) -> Option { + for i in 0..count { + if src[i] != other[i] { + return Some(BitOrd::bitwise_cmp(src[i], other[i])); + } + } + None +} + +// --------------------------------------------------------------------------- +// AVX2 — 4 × u64 per iteration +// --------------------------------------------------------------------------- + +#[allow(unused)] +#[cfg(any(target_arch = "x86", target_arch = "x86_64"))] +mod avx2 { + use core::cmp::Ordering; + + use crate::traits::BitOrd; + + #[cfg(target_arch = "x86")] + use core::arch::x86::{__m256i, _mm256_cmpeq_epi64, _mm256_loadu_si256, _mm256_movemask_pd}; + #[cfg(target_arch = "x86_64")] + use core::arch::x86_64::{__m256i, _mm256_cmpeq_epi64, _mm256_loadu_si256, _mm256_movemask_pd}; + + #[target_feature(enable = "avx2")] + pub(super) unsafe fn cmp_aligned(src: &[u64], other: &[u64], len: usize) -> Option { + let mut i = 0; + while i + 4 <= len { + let a = unsafe { _mm256_loadu_si256(src.as_ptr().add(i).cast::<__m256i>()) }; + let b = unsafe { _mm256_loadu_si256(other.as_ptr().add(i).cast::<__m256i>()) }; + let cmp = unsafe { _mm256_cmpeq_epi64(a, b) }; + let mask = unsafe { _mm256_movemask_pd(core::mem::transmute(cmp)) } as u32; + if mask != 0b1111 { + let lane = mask.trailing_ones() as usize; + return Some(BitOrd::bitwise_cmp(src[i + lane], other[i + lane])); + } + i += 4; + } + while i < len { + if src[i] != other[i] { + return Some(BitOrd::bitwise_cmp(src[i], other[i])); + } + i += 1; + } + None + } +} + +// --------------------------------------------------------------------------- +// SSE2 — 2 × u64 per iteration +// --------------------------------------------------------------------------- + +#[allow(unused)] +#[cfg(any(target_arch = "x86", target_arch = "x86_64"))] +mod sse2 { + use core::cmp::Ordering; + + use crate::traits::BitOrd; + + #[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")] + pub(super) unsafe fn cmp_aligned(src: &[u64], other: &[u64], len: usize) -> Option { + let mut i = 0; + while i + 2 <= len { + let a = unsafe { _mm_loadu_si128(src.as_ptr().add(i).cast::<__m128i>()) }; + let b = unsafe { _mm_loadu_si128(other.as_ptr().add(i).cast::<__m128i>()) }; + let cmp = unsafe { _mm_cmpeq_epi64(a, b) }; + let mask = unsafe { _mm_movemask_epi8(cmp) } as u32; + if mask != 0xFFFF { + // Each lane is 8 bytes → 8 high bits. The first zero byte + // tells us which lane differs. + let lane = mask.trailing_ones() as usize / 8; + return Some(BitOrd::bitwise_cmp(src[i + lane], other[i + lane])); + } + i += 2; + } + while i < len { + if src[i] != other[i] { + return Some(BitOrd::bitwise_cmp(src[i], other[i])); + } + i += 1; + } + None + } +} + +// --------------------------------------------------------------------------- +// NEON — 2 × u64 per iteration +// --------------------------------------------------------------------------- + +#[allow(unused)] +#[cfg(target_arch = "aarch64")] +mod neon { + use core::cmp::Ordering; + + use crate::traits::BitOrd; + use core::arch::aarch64::{uint64x2_t, vceqq_u64, vgetq_lane_u64, vld1q_u64}; + + #[target_feature(enable = "neon")] + pub(super) unsafe fn cmp_aligned(src: &[u64], other: &[u64], len: usize) -> Option { + let mut i = 0; + while i + 2 <= len { + let a = unsafe { vld1q_u64(src.as_ptr().add(i)) }; + let b = unsafe { vld1q_u64(other.as_ptr().add(i)) }; + let cmp = unsafe { vceqq_u64(a, b) }; + if unsafe { vgetq_lane_u64(cmp, 0) } == 0 { + return Some(BitOrd::bitwise_cmp(src[i], other[i])); + } + if unsafe { vgetq_lane_u64(cmp, 1) } == 0 { + return Some(BitOrd::bitwise_cmp(src[i + 1], other[i + 1])); + } + i += 2; + } + while i < len { + if src[i] != other[i] { + return Some(BitOrd::bitwise_cmp(src[i], other[i])); + } + i += 1; + } + None + } +} + +#[cfg(test)] +mod tests_for_backend_equivalence; diff --git a/src/traits/bits_ord/funcs_for_cmp_aligned_core/tests_for_backend_equivalence.rs b/src/traits/bits_ord/funcs_for_cmp_aligned_core/tests_for_backend_equivalence.rs new file mode 100644 index 0000000..bcce571 --- /dev/null +++ b/src/traits/bits_ord/funcs_for_cmp_aligned_core/tests_for_backend_equivalence.rs @@ -0,0 +1,94 @@ +//! Verify SIMD `cmp_aligned_words` against scalar for random inputs. + +use proptest::prelude::*; + +use crate::BitString; + +fn config() -> ProptestConfig { + ProptestConfig { + cases: 512, + max_shrink_iters: 128, + ..ProptestConfig::default() + } +} + +/// Scalar oracle: compare two bit strings bit by bit (LSB-first). +fn scalar_cmp(a: &BitString, b: &BitString) -> core::cmp::Ordering { + use core::cmp::Ordering; + let common = a.bit_len().min(b.bit_len()); + for i in 0..common { + match (a.get(i), b.get(i)) { + (Some(false), Some(true)) => return Ordering::Less, + (Some(true), Some(false)) => return Ordering::Greater, + _ => {} + } + } + a.bit_len().cmp(&b.bit_len()) +} + +proptest! { + #![proptest_config(config())] + + /// Small inputs — purely scalar path. + #[test] + fn small_inputs_match_scalar( + a_bits in proptest::collection::vec(proptest::bool::ANY, 0..=64), + b_bits in proptest::collection::vec(proptest::bool::ANY, 0..=64), + ) { + let a = BitString::from_bool_iter(a_bits); + let b = BitString::from_bool_iter(b_bits); + let result = a.as_bit_str().cmp(&b.as_bit_str()); + let expected = scalar_cmp(&a, &b); + assert_eq!(result, expected); + } + + /// Medium inputs — crossing the SIMD threshold. + #[test] + fn medium_inputs_match_scalar( + a_bits in proptest::collection::vec(proptest::bool::ANY, 64..=320), + b_bits in proptest::collection::vec(proptest::bool::ANY, 64..=320), + ) { + let a = BitString::from_bool_iter(a_bits); + let b = BitString::from_bool_iter(b_bits); + let result = a.as_bit_str().cmp(&b.as_bit_str()); + let expected = scalar_cmp(&a, &b); + assert_eq!(result, expected); + } + + /// Large inputs — SIMD main loop (many 4-word chunks on AVX2). + #[test] + fn large_inputs_match_scalar( + a_bits in proptest::collection::vec(proptest::bool::ANY, 256..=512), + b_bits in proptest::collection::vec(proptest::bool::ANY, 256..=512), + ) { + let a = BitString::from_bool_iter(a_bits); + let b = BitString::from_bool_iter(b_bits); + let result = a.as_bit_str().cmp(&b.as_bit_str()); + let expected = scalar_cmp(&a, &b); + assert_eq!(result, expected); + } + + /// Offset (unaligned) views — exercises the scalar read_word_at paths. + #[test] + fn offset_views_match_scalar( + src_a in proptest::collection::vec(proptest::bool::ANY, 1..=256), + src_b in proptest::collection::vec(proptest::bool::ANY, 1..=256), + skip_a in 0usize..=32, + skip_b in 0usize..=32, + ) { + let base_a = BitString::from_bool_iter(src_a); + let base_b = BitString::from_bool_iter(src_b); + let a_len = base_a.bit_len().saturating_sub(skip_a); + let b_len = base_b.bit_len().saturating_sub(skip_b); + if a_len == 0 || b_len == 0 { + return Ok(()); + } + let a = base_a.as_bit_str().slice_from(skip_a).slice_until(skip_a + a_len); + let b = base_b.as_bit_str().slice_from(skip_b).slice_until(skip_b + b_len); + let result = a.cmp(&b); + + // Reconstruct expected via to_bit_string roundtrip + let expected = scalar_cmp(&a.to_bit_string(), &b.to_bit_string()); + assert_eq!(result, expected); + } +} diff --git a/src/traits/bits_ord/funcs_for_cmp_unaligned_core.rs b/src/traits/bits_ord/funcs_for_cmp_unaligned_core.rs new file mode 100644 index 0000000..dc4ee6c --- /dev/null +++ b/src/traits/bits_ord/funcs_for_cmp_unaligned_core.rs @@ -0,0 +1,249 @@ +//! SIMD word-level unaligned (shifted-window) comparison. +//! +//! `self` has a non-zero intra-word `shift`; `other` is word-aligned. +//! Each logical word of `self` spans two source words, reconstructed as +//! `(src[i] >> shift) | (src[i+1] << (WORD_BITS - shift))`. + +use core::cmp::Ordering; + +use crate::traits::BitOrd; +use crate::{SMALL_WORDS, WORD_BITS}; + +/// Returns `Some(Ordering)` at the first differing word, or `None` when all +/// `count` shifted windows match `other`. +#[inline] +pub(super) fn cmp_unaligned_words( + src: &[u64], + other: &[u64], + count: usize, + shift: usize, +) -> Option { + debug_assert!(shift > 0 && shift < WORD_BITS); + + if count < SMALL_WORDS { + return scalar_cmp_unaligned(src, other, count, shift); + } + + #[cfg(all( + any(target_arch = "x86", target_arch = "x86_64"), + target_feature = "avx2" + ))] + { + return unsafe { avx2::cmp_unaligned(src, other, count, shift) }; + } + + #[cfg(all( + any(target_arch = "x86", target_arch = "x86_64"), + target_feature = "sse2", + not(target_feature = "avx2") + ))] + { + return unsafe { sse2::cmp_unaligned(src, other, count, shift) }; + } + + #[cfg(all(target_arch = "aarch64", target_feature = "neon"))] + { + return unsafe { neon::cmp_unaligned(src, other, count, shift) }; + } + + #[allow(unreachable_code)] + scalar_cmp_unaligned(src, other, count, shift) +} + +#[inline] +fn scalar_cmp_unaligned( + src: &[u64], + other: &[u64], + count: usize, + shift: usize, +) -> Option { + for i in 0..count { + let w0 = src[i]; + let w1 = src[i + 1]; + let window = (w0 >> shift) | (w1 << (WORD_BITS - shift)); + if window != other[i] { + return Some(BitOrd::bitwise_cmp(window, other[i])); + } + } + None +} + +// --------------------------------------------------------------------------- +// AVX2 — 4 × u64 per iteration +// --------------------------------------------------------------------------- + +#[allow(unused)] +#[cfg(any(target_arch = "x86", target_arch = "x86_64"))] +mod avx2 { + use core::cmp::Ordering; + + use crate::WORD_BITS; + use crate::traits::BitOrd; + + #[cfg(target_arch = "x86")] + use core::arch::x86::{ + __m128i, __m256i, _mm_set1_epi64x, _mm256_cmpeq_epi64, _mm256_loadu_si256, + _mm256_movemask_pd, _mm256_or_si256, _mm256_sll_epi64, _mm256_srl_epi64, + }; + #[cfg(target_arch = "x86_64")] + use core::arch::x86_64::{ + __m128i, __m256i, _mm_set1_epi64x, _mm256_cmpeq_epi64, _mm256_loadu_si256, + _mm256_movemask_pd, _mm256_or_si256, _mm256_sll_epi64, _mm256_srl_epi64, + }; + + #[target_feature(enable = "avx2")] + pub(super) unsafe fn cmp_unaligned( + src: &[u64], + other: &[u64], + len: usize, + shift: usize, + ) -> Option { + let count_lo = unsafe { _mm_set1_epi64x(shift as i64) }; + let count_hi = unsafe { _mm_set1_epi64x((WORD_BITS - shift) as i64) }; + let mut i = 0; + while i + 4 <= len { + // Load src[i..i+4] and src[i+1..i+5]. + let w0 = unsafe { _mm256_loadu_si256(src.as_ptr().add(i).cast::<__m256i>()) }; + let w1 = unsafe { _mm256_loadu_si256(src.as_ptr().add(i + 1).cast::<__m256i>()) }; + let lo = unsafe { _mm256_srl_epi64(w0, count_lo) }; + let hi = unsafe { _mm256_sll_epi64(w1, count_hi) }; + let window = unsafe { _mm256_or_si256(lo, hi) }; + let b = unsafe { _mm256_loadu_si256(other.as_ptr().add(i).cast::<__m256i>()) }; + let cmp = unsafe { _mm256_cmpeq_epi64(window, b) }; + let mask = unsafe { _mm256_movemask_pd(core::mem::transmute(cmp)) } as u32; + if mask != 0b1111 { + let lane = mask.trailing_ones() as usize; + let sw = (src[i + lane] >> shift) | (src[i + lane + 1] << (WORD_BITS - shift)); + return Some(BitOrd::bitwise_cmp(sw, other[i + lane])); + } + i += 4; + } + while i < len { + let sw = (src[i] >> shift) | (src[i + 1] << (WORD_BITS - shift)); + if sw != other[i] { + return Some(BitOrd::bitwise_cmp(sw, other[i])); + } + i += 1; + } + None + } +} + +// --------------------------------------------------------------------------- +// SSE2 — 2 × u64 per iteration +// --------------------------------------------------------------------------- + +#[allow(unused)] +#[cfg(any(target_arch = "x86", target_arch = "x86_64"))] +mod sse2 { + use core::cmp::Ordering; + + use crate::WORD_BITS; + use crate::traits::BitOrd; + + #[cfg(target_arch = "x86")] + use core::arch::x86::{ + __m128i, _mm_cmpeq_epi64, _mm_loadu_si128, _mm_movemask_epi8, _mm_or_si128, + _mm_set1_epi64x, _mm_sll_epi64, _mm_srl_epi64, + }; + #[cfg(target_arch = "x86_64")] + use core::arch::x86_64::{ + __m128i, _mm_cmpeq_epi64, _mm_loadu_si128, _mm_movemask_epi8, _mm_or_si128, + _mm_set1_epi64x, _mm_sll_epi64, _mm_srl_epi64, + }; + + #[target_feature(enable = "sse2")] + pub(super) unsafe fn cmp_unaligned( + src: &[u64], + other: &[u64], + len: usize, + shift: usize, + ) -> Option { + let count_lo = unsafe { _mm_set1_epi64x(shift as i64) }; + let count_hi = unsafe { _mm_set1_epi64x((WORD_BITS - shift) as i64) }; + let mut i = 0; + while i + 2 <= len { + // Load src[i..i+2] and src[i+1..i+3]. + let w0 = unsafe { _mm_loadu_si128(src.as_ptr().add(i).cast::<__m128i>()) }; + let w1 = unsafe { _mm_loadu_si128(src.as_ptr().add(i + 1).cast::<__m128i>()) }; + let lo = unsafe { _mm_srl_epi64(w0, count_lo) }; + let hi = unsafe { _mm_sll_epi64(w1, count_hi) }; + let window = unsafe { _mm_or_si128(lo, hi) }; + let b = unsafe { _mm_loadu_si128(other.as_ptr().add(i).cast::<__m128i>()) }; + let cmp = unsafe { _mm_cmpeq_epi64(window, b) }; + let mask = unsafe { _mm_movemask_epi8(cmp) } as u32; + if mask != 0xFFFF { + let lane = mask.trailing_ones() as usize / 8; + let sw = (src[i + lane] >> shift) | (src[i + lane + 1] << (WORD_BITS - shift)); + return Some(BitOrd::bitwise_cmp(sw, other[i + lane])); + } + i += 2; + } + while i < len { + let sw = (src[i] >> shift) | (src[i + 1] << (WORD_BITS - shift)); + if sw != other[i] { + return Some(BitOrd::bitwise_cmp(sw, other[i])); + } + i += 1; + } + None + } +} + +// --------------------------------------------------------------------------- +// NEON — 2 × u64 per iteration +// --------------------------------------------------------------------------- + +#[allow(unused)] +#[cfg(target_arch = "aarch64")] +mod neon { + use core::cmp::Ordering; + + use crate::WORD_BITS; + use crate::traits::BitOrd; + + use core::arch::aarch64::{ + vceqq_u64, vdupq_n_s64, vgetq_lane_u64, vld1q_u64, vorrq_u64, vshlq_u64, + }; + + #[target_feature(enable = "neon")] + pub(super) unsafe fn cmp_unaligned( + src: &[u64], + other: &[u64], + len: usize, + shift: usize, + ) -> Option { + let neg_shift = unsafe { vdupq_n_s64(-(shift as i64)) }; + let pos_shift = unsafe { vdupq_n_s64((WORD_BITS - shift) as i64) }; + let mut i = 0; + while i + 2 <= len { + let w0 = unsafe { vld1q_u64(src.as_ptr().add(i)) }; + let w1 = unsafe { vld1q_u64(src.as_ptr().add(i + 1)) }; + let lo = unsafe { vshlq_u64(w0, neg_shift) }; + let hi = unsafe { vshlq_u64(w1, pos_shift) }; + let window = unsafe { vorrq_u64(lo, hi) }; + let expected = unsafe { vld1q_u64(other.as_ptr().add(i)) }; + let cmp = unsafe { vceqq_u64(window, expected) }; + if unsafe { vgetq_lane_u64(cmp, 0) } == 0 { + let sw = (src[i] >> shift) | (src[i + 1] << (WORD_BITS - shift)); + return Some(BitOrd::bitwise_cmp(sw, other[i])); + } + if unsafe { vgetq_lane_u64(cmp, 1) } == 0 { + let sw = (src[i + 1] >> shift) | (src[i + 2] << (WORD_BITS - shift)); + return Some(BitOrd::bitwise_cmp(sw, other[i + 1])); + } + i += 2; + } + while i < len { + let sw = (src[i] >> shift) | (src[i + 1] << (WORD_BITS - shift)); + if sw != other[i] { + return Some(BitOrd::bitwise_cmp(sw, other[i])); + } + i += 1; + } + None + } +} + +#[cfg(test)] +mod tests_for_backend_equivalence; diff --git a/src/traits/bits_ord/funcs_for_cmp_unaligned_core/tests_for_backend_equivalence.rs b/src/traits/bits_ord/funcs_for_cmp_unaligned_core/tests_for_backend_equivalence.rs new file mode 100644 index 0000000..866f91e --- /dev/null +++ b/src/traits/bits_ord/funcs_for_cmp_unaligned_core/tests_for_backend_equivalence.rs @@ -0,0 +1,95 @@ +//! Verify SIMD unaligned word comparison against scalar for random inputs. + +use core::cmp::Ordering; + +use proptest::prelude::*; + +use crate::BitString; + +fn config() -> ProptestConfig { + ProptestConfig { + cases: 512, + max_shrink_iters: 128, + ..ProptestConfig::default() + } +} + +/// Scalar oracle: compare two bit strings bit by bit (LSB-first). +fn scalar_cmp_bits(a: &BitString, b: &BitString) -> Ordering { + let common = a.bit_len().min(b.bit_len()); + for i in 0..common { + match (a.get(i), b.get(i)) { + (Some(false), Some(true)) => return Ordering::Less, + (Some(true), Some(false)) => return Ordering::Greater, + _ => {} + } + } + a.bit_len().cmp(&b.bit_len()) +} + +proptest! { + #![proptest_config(config())] + + /// Haystack unaligned, needle aligned — exercises `cmp_unaligned_words`. + #[test] + fn unaligned_hs_vs_aligned_nd( + hs_bits in proptest::collection::vec(proptest::bool::ANY, 65..=320), + nd_bits in proptest::collection::vec(proptest::bool::ANY, 1..=256), + skip in 1usize..=32, + ) { + let base = BitString::from_bool_iter(hs_bits); + let hs_len = base.bit_len().saturating_sub(skip); + if hs_len == 0 || hs_len < nd_bits.len() { + return Ok(()); + } + let hs = base.as_bit_str().slice_from(skip).slice_until(skip + hs_len); + let nd = BitString::from_bool_iter(nd_bits); + + let result = hs.cmp(&nd.as_bit_str()); + let expected = scalar_cmp_bits(&hs.to_bit_string(), &nd); + assert_eq!(result, expected); + } + + /// Both unaligned — exercises the scalar fallback (double read_word_at). + #[test] + fn both_unaligned( + src_a in proptest::collection::vec(proptest::bool::ANY, 65..=256), + src_b in proptest::collection::vec(proptest::bool::ANY, 65..=256), + skip_a in 1usize..=32, + skip_b in 1usize..=32, + ) { + let base_a = BitString::from_bool_iter(src_a); + let base_b = BitString::from_bool_iter(src_b); + let a_len = base_a.bit_len().saturating_sub(skip_a); + let b_len = base_b.bit_len().saturating_sub(skip_b); + if a_len == 0 || b_len == 0 { + return Ok(()); + } + let a = base_a.as_bit_str().slice_from(skip_a).slice_until(skip_a + a_len); + let b = base_b.as_bit_str().slice_from(skip_b).slice_until(skip_b + b_len); + + let result = a.cmp(&b); + let expected = scalar_cmp_bits(&a.to_bit_string(), &b.to_bit_string()); + assert_eq!(result, expected); + } + + /// Long unaligned haystack vs aligned needle — SIMD main loop. + #[test] + fn long_unaligned_vs_aligned( + hs_bits in proptest::collection::vec(proptest::bool::ANY, 256..=512), + nd_bits in proptest::collection::vec(proptest::bool::ANY, 64..=320), + skip in 1usize..=32, + ) { + let base = BitString::from_bool_iter(hs_bits); + let hs_len = base.bit_len().saturating_sub(skip); + if hs_len < nd_bits.len() { + return Ok(()); + } + let hs = base.as_bit_str().slice_from(skip).slice_until(skip + hs_len); + let nd = BitString::from_bool_iter(nd_bits); + + let result = hs.cmp(&nd.as_bit_str()); + let expected = scalar_cmp_bits(&hs.to_bit_string(), &nd); + assert_eq!(result, expected); + } +} diff --git a/src/traits/bits_ord/impls_for_u64_slice.rs b/src/traits/bits_ord/impls_for_u64_slice.rs new file mode 100644 index 0000000..bc3d153 --- /dev/null +++ b/src/traits/bits_ord/impls_for_u64_slice.rs @@ -0,0 +1,22 @@ +use core::cmp::Ordering; + +use crate::WORD_BITS; + +use super::BitsOrd; +use super::funcs_for_cmp_aligned_core; +use super::funcs_for_cmp_unaligned_core; + +impl BitsOrd for [u64] { + #[inline] + fn cmp_words(&self, other: &[u64], count: usize, offset: usize) -> Option { + let shift = offset % WORD_BITS; + let base = offset / WORD_BITS; + let sw = &self[base..]; + + if shift == 0 { + funcs_for_cmp_aligned_core::cmp_aligned_words(sw, other, count) + } else { + funcs_for_cmp_unaligned_core::cmp_unaligned_words(sw, other, count, shift) + } + } +} From d196b4407490ab8da4b770ff9afbba9934193042 Mon Sep 17 00:00:00 2001 From: juncheng Date: Sat, 27 Jun 2026 00:48:44 +0000 Subject: [PATCH 20/22] =?UTF-8?q?feat:=20PartialOrd=20+=20Ord=20for=20BitS?= =?UTF-8?q?tring=20=E2=80=94=20delegates=20to=20as=5Fbit=5Fstr()?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit BitString now supports lexicographic comparison via PartialOrd and Ord. Implementation follows the same pattern as Hash — both traits delegate to as_bit_str() for zero-code-duplication. - src/bit_string/impls_for_ord.rs: PartialOrd + Ord impl (inline delegation) - src/bit_string/impls_for_ord/tests_for_ord.rs: 10 tests (sort, min/max, cross-word, delegation consistency) - src/bit_string.rs: register impls_for_ord module Co-Authored-By: Claude Co-Authored-By: DeepSeek AI --- src/bit_string.rs | 1 + src/bit_string/impls_for_ord.rs | 20 ++++ src/bit_string/impls_for_ord/tests_for_ord.rs | 105 ++++++++++++++++++ 3 files changed, 126 insertions(+) create mode 100644 src/bit_string/impls_for_ord.rs create mode 100644 src/bit_string/impls_for_ord/tests_for_ord.rs diff --git a/src/bit_string.rs b/src/bit_string.rs index 97b031f..474e7e6 100644 --- a/src/bit_string.rs +++ b/src/bit_string.rs @@ -43,6 +43,7 @@ mod impls_for_fmt; mod impls_for_hash; mod impls_for_iter; mod impls_for_matching; +mod impls_for_ord; mod impls_for_predicates; #[cfg(test)] diff --git a/src/bit_string/impls_for_ord.rs b/src/bit_string/impls_for_ord.rs new file mode 100644 index 0000000..ccfeb6f --- /dev/null +++ b/src/bit_string/impls_for_ord.rs @@ -0,0 +1,20 @@ +use core::cmp::Ordering; + +use crate::BitString; + +impl PartialOrd for BitString { + #[inline] + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for BitString { + #[inline] + fn cmp(&self, other: &Self) -> Ordering { + 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 new file mode 100644 index 0000000..efa3611 --- /dev/null +++ b/src/bit_string/impls_for_ord/tests_for_ord.rs @@ -0,0 +1,105 @@ +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 eba49be2a76ac57d7569a8e71b707990ea3cbbb5 Mon Sep 17 00:00:00 2001 From: juncheng Date: Sat, 27 Jun 2026 00:53:00 +0000 Subject: [PATCH 21/22] ci: extend CodSpeed filter to cover bit_str benchmarks The CodSpeed run_command now filters for both 'bit_string' and 'bit_str' benchmark names, instead of only 'bit_string'. This ensures SIMD-accelerated BitStr benchmarks (ord, hash, matching, etc.) are also tracked for performance regressions. Co-Authored-By: Claude Co-Authored-By: DeepSeek AI --- .github/workflows/codspeed.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/codspeed.yml b/.github/workflows/codspeed.yml index 6c9b09e..c399cff 100644 --- a/.github/workflows/codspeed.yml +++ b/.github/workflows/codspeed.yml @@ -16,4 +16,4 @@ jobs: with: rust_toolchain: stable build_command: cargo codspeed build -m simulation - run_command: cargo codspeed run -m simulation 'bit_string' + run_command: cargo codspeed run -m simulation 'bit_string,bit_str' From e2156a69183b7d7b01f987913ec3a86153e27f76 Mon Sep 17 00:00:00 2001 From: juncheng Date: Sat, 27 Jun 2026 01:02:46 +0000 Subject: [PATCH 22/22] fix: ci use common prefix 'bit_' for CodSpeed filter The previous comma-separated 'bit_string,bit_str' was treated as a single literal filter pattern by cargo-codspeed, which uses substring matching. Since no benchmark name contains a comma, zero benches were detected. Use the common prefix 'bit_' which matches both bit_string and bit_str benchmarks (136 total) without matching competitors (bitvec_simd, string). Co-Authored-By: Claude Co-Authored-By: DeepSeek AI --- .github/workflows/codspeed.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/codspeed.yml b/.github/workflows/codspeed.yml index c399cff..2934b69 100644 --- a/.github/workflows/codspeed.yml +++ b/.github/workflows/codspeed.yml @@ -16,4 +16,4 @@ jobs: with: rust_toolchain: stable build_command: cargo codspeed build -m simulation - run_command: cargo codspeed run -m simulation 'bit_string,bit_str' + run_command: cargo codspeed run -m simulation bit_