diff --git a/.github/workflows/codspeed.yml b/.github/workflows/codspeed.yml index 6c9b09e..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' + run_command: cargo codspeed run -m simulation bit_ 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/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/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 new file mode 100644 index 0000000..4a81d5b --- /dev/null +++ b/src/bit_str.rs @@ -0,0 +1,76 @@ +use int_interval::UsizeCO; + +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 `'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> { + pub(crate) source: &'bs BitString, + pub(crate) start: usize, + /// Number of bits in this view. + pub(crate) bit_len: usize, +} + +pub mod errors; +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_ord; +mod impls_for_predicates; +mod impls_for_slice; + +// --------------------------------------------------------------------------- +// 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 + } + + /// 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/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..fcb165a --- /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_bit_str(); + 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_bit_str(); + + 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_bit_str(); + + 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_bit_str(); + + 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_bit_str().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_bit_str().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_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 + 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_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)); + 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_bit_str().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_bit_str().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..c0360b8 --- /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_bit_str(); + 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_bit_str(); + + 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_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); + // 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_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 + 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_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 + 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_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 +} + +// --------------------------------------------------------------------------- +// 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_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..b265681 --- /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_bit_str().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_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_bit_str().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_bit_str().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_bit_str(); + 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_bit_str(); + 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_bit_str(); + + 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_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_bit_str().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_bit_str(); + + for start in [0, 1, 5, 63, 64, 65, 127, 128] { + for len in [10, 63, 64, 65, 128, 129] { + let end = (start + len).min(full.bit_len()); + // 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_bit_str().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_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_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_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()); +} 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(\"\")"); +} diff --git a/src/bit_str/impls_for_hash.rs b/src/bit_str/impls_for_hash.rs new file mode 100644 index 0000000..8be3296 --- /dev/null +++ b/src/bit_str/impls_for_hash.rs @@ -0,0 +1,44 @@ +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; + + 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 { + // 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); + } + } + + if rem > 0 { + let tail_start = self.start + full_words * WORD_BITS; + (words.read_word_at(tail_start) & 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..e8d0deb --- /dev/null +++ b/src/bit_str/impls_for_hash/tests_for_hash.rs @@ -0,0 +1,136 @@ +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())); +} + +// --------------------------------------------------------------------------- +// 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() { + 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)); +} 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..1154ebf --- /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_bit_str(); + + 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_bit_str().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_bit_str(); + 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_bit_str(); + 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_bit_str(); + 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_bit_str(); + + 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_bit_str(); + 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_bit_str().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_bit_str().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_bit_str().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_bit_str(); + + 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_bit_str().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_bit_str(); + + 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_bit_str(); + + 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); +} diff --git a/src/bit_str/impls_for_matching.rs b/src/bit_str/impls_for_matching.rs new file mode 100644 index 0000000..569ef14 --- /dev/null +++ b/src/bit_str/impls_for_matching.rs @@ -0,0 +1,3 @@ +mod impls_for_find; +mod impls_for_matches_at; +mod impls_for_strip; 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..2b134bd --- /dev/null +++ b/src/bit_str/impls_for_matching/impls_for_find.rs @@ -0,0 +1,177 @@ +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: BitStr<'_>) -> bool { + if needle.bit_len == 0 { + return true; + } + 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_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; + } + let aligned = &words[sw + 1..]; + return aligned + .find_any_candidate(remaining, needle_words, needle_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_len, &mut |pos| { + self.bits_equal_at(pos, needle) + }) + .is_some() + } + + /// Returns the index of the first occurrence of `needle`, or `None`. + #[inline] + pub fn find(&self, needle: BitStr<'_>) -> Option { + if needle.bit_len == 0 { + return Some(0); + } + 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.source.words(); + let needle_len = needle.bit_len; + + // Word-aligned fast path. + 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; + } + + aligned + .find_first_word(remaining, needle_words, needle_len, &mut |pos| { + self.bits_equal_at(pos + first_bits, needle) + }) + .map(|pos| pos + first_bits) + } + + /// Returns the index of the last occurrence of `needle`, or `None`. + #[inline] + pub fn rfind(&self, needle: BitStr<'_>) -> Option { + if needle.bit_len == 0 { + return Some(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.source.words(); + let needle_len = needle.bit_len; + + // 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), + ); + } + + // 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; + + 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 + } +} + +#[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..7611d64 --- /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_bit_str(); + let bs = BitString::try_from("01").unwrap(); + assert!(v.contains(bs.as_bit_str())); + let bs = BitString::try_from("111").unwrap(); + assert!(!v.contains(bs.as_bit_str())); +} + +#[test] +fn contains_empty_needle() { + let bits = BitString::try_from("10110").unwrap(); + let v = bits.as_bit_str(); + let empty = BitString::new(); + 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_bit_str(); + let bs = BitString::try_from("101").unwrap(); + assert!(!v.contains(bs.as_bit_str())); +} + +#[test] +fn contains_on_offset_view() { + let bits = BitString::try_from("110010").unwrap(); + let v = bits.as_bit_str().slice_from(1).slice_until(5); + let bs = BitString::try_from("001").unwrap(); + assert!(v.contains(bs.as_bit_str())); + let bs = BitString::try_from("11").unwrap(); + assert!(!v.contains(bs.as_bit_str())); +} + +// --------------------------------------------------------------------------- +// find +// --------------------------------------------------------------------------- + +#[test] +fn find_first_occurrence() { + let bits = BitString::try_from("10110010").unwrap(); + let v = bits.as_bit_str(); + let bs = BitString::try_from("10").unwrap(); + assert_eq!(v.find(bs.as_bit_str()), Some(0)); + let bs = BitString::try_from("01").unwrap(); + assert_eq!(v.find(bs.as_bit_str()), Some(1)); + let bs = BitString::try_from("111").unwrap(); + 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_bit_str(); + let empty = BitString::new(); + 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_bit_str(); + let bs = BitString::try_from("101").unwrap(); + 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_bit_str().slice_from(2).slice_until(7); + let bs = BitString::try_from("10").unwrap(); + assert_eq!(v.find(bs.as_bit_str()), Some(2)); + let bs = BitString::try_from("00").unwrap(); + 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_bit_str(); + let bs = BitString::try_from("00").unwrap(); + assert_eq!(v.find(bs.as_bit_str()), Some(3)); +} + +// --------------------------------------------------------------------------- +// rfind +// --------------------------------------------------------------------------- + +#[test] +fn rfind_last_occurrence() { + let bits = BitString::try_from("10110010").unwrap(); + let v = bits.as_bit_str(); + let bs = BitString::try_from("10").unwrap(); + 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_bit_str(); + let empty = BitString::new(); + 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_bit_str(); + let bs = BitString::try_from("101").unwrap(); + 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_bit_str().slice_from(2).slice_until(7); + let bs = BitString::try_from("00").unwrap(); + 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_bit_str(); + let bs = BitString::try_from("101").unwrap(); + 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 new file mode 100644 index 0000000..e378d11 --- /dev/null +++ b/src/bit_str/impls_for_matching/impls_for_matches_at.rs @@ -0,0 +1,119 @@ +use crate::traits::*; +use crate::{WORD_BITS, low_mask}; + +use crate::BitStr; + +impl<'bs> BitStr<'bs> { + /// Compare `needle` bits against `self` starting at `offset`. + /// + /// 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: 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: BitStr<'_>) -> 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: 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 { + 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_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 new file mode 100644 index 0000000..922c5f8 --- /dev/null +++ b/src/bit_str/impls_for_matching/impls_for_matches_at/tests_for_matches_at.rs @@ -0,0 +1,48 @@ +use crate::BitString; + +// --------------------------------------------------------------------------- +// matches_at +// --------------------------------------------------------------------------- + +#[test] +fn matches_at_exact_match() { + let bits = BitString::try_from("101100").unwrap(); + let v = bits.as_bit_str(); + let p = BitString::try_from("10").unwrap(); + 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_bit_str().slice_from(2); + let p = BitString::try_from("10").unwrap(); + 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_bit_str(); + let p = BitString::try_from("100").unwrap(); + 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_bit_str(); + let p = BitString::new(); + 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_bit_str().slice_from(2).slice_until(7); + let p = BitString::try_from("0001").unwrap(); + 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 new file mode 100644 index 0000000..e8c1352 --- /dev/null +++ b/src/bit_str/impls_for_matching/impls_for_strip.rs @@ -0,0 +1,20 @@ +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 { + 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 { + 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..1f7369d --- /dev/null +++ b/src/bit_str/impls_for_matching/impls_for_strip/tests_for_strip.rs @@ -0,0 +1,111 @@ +use crate::BitString; + +// --------------------------------------------------------------------------- +// strip_prefix +// --------------------------------------------------------------------------- + +#[test] +fn strip_prefix_removes_matching_prefix() { + let bits = BitString::try_from("101100").unwrap(); + let v = bits.as_bit_str(); + let p = BitString::try_from("101").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)); + 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_bit_str(); + let p = BitString::try_from("11").unwrap(); + 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_bit_str(); + let p = BitString::new(); + 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_bit_str(); + let p = BitString::try_from("101").unwrap(); + let rest = v.strip_prefix(p.as_bit_str()).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_bit_str().slice_from(1).slice_until(5); + let p = BitString::try_from("10").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)); + 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_bit_str(); + let s = BitString::try_from("100").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)); + 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_bit_str(); + let s = BitString::try_from("10").unwrap(); + 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_bit_str(); + let s = BitString::new(); + 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_bit_str(); + let s = BitString::try_from("101").unwrap(); + let rest = v.strip_suffix(s.as_bit_str()).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_bit_str().slice_from(1).slice_until(5); + let s = BitString::try_from("01").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)); + assert_eq!(rest.get(2), Some(true)); +} 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/bit_str/impls_for_predicates.rs b/src/bit_str/impls_for_predicates.rs new file mode 100644 index 0000000..b574661 --- /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 { + 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() + } +} diff --git a/src/bit_str/impls_for_slice.rs b/src/bit_str/impls_for_slice.rs new file mode 100644 index 0000000..9b89e99 --- /dev/null +++ b/src/bit_str/impls_for_slice.rs @@ -0,0 +1,50 @@ +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, + } + } + + /// 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 { + 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 { + 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..b6f3b27 --- /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_bit_str(); + 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_bit_str(); + + // 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_bit_str(); + + // [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_bit_str(); + + // [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_bit_str(); + + 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_bit_str(); + + 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_bit_str(); + + // 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_bit_str(); + 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_bit_str(); + + 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_bit_str(); + + 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_bit_str(); + + 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_bit_str(); + + // 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_bit_str(); + 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_bit_str(); + + 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_bit_str(); + + 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_bit_str(); + + 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_bit_str(); + + // 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_bit_str(); + + // 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_bit_str(); + + // 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_bit_str(); + + // 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_bit_str(); + + 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_bit_str(); + + let off = v.slice(UsizeCO::try_new(3, 12).unwrap()); + let s = off.slice_until(4); + assert_eq!(s.get(3), v.get(6)); +} diff --git a/src/bit_str/tests_for_to_bit_string.rs b/src/bit_str/tests_for_to_bit_string.rs new file mode 100644 index 0000000..7df2582 --- /dev/null +++ b/src/bit_str/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()); +} diff --git a/src/bit_string.rs b/src/bit_string.rs index 82eea95..474e7e6 100644 --- a/src/bit_string.rs +++ b/src/bit_string.rs @@ -2,22 +2,49 @@ 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, } +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 + } + + /// Returns a zero-copy [`BitStr`] view of the entire bit string. + #[inline] + pub fn as_bit_str(&self) -> crate::BitStr<'_> { + crate::BitStr { + source: self, + start: 0, + bit_len: self.bit_len, + } + } +} + pub mod errors; mod impls_for_access; 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; - -pub(crate) mod traits; +mod impls_for_ord; +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 e7faaac..5ded899 100644 --- a/src/bit_string/impls_for_access.rs +++ b/src/bit_string/impls_for_access.rs @@ -1,18 +1,8 @@ -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(|| { @@ -21,64 +11,12 @@ impl BitString { word & mask != 0 }) } -} - -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() - } -} - -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 - } #[inline] pub fn first(&self) -> Option { 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 +24,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_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); + } +} 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..bb32565 100644 --- a/src/bit_string/impls_for_matching.rs +++ b/src/bit_string/impls_for_matching.rs @@ -1,10 +1,5 @@ -use crate::bit_string::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 81a2828..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,87 +1,19 @@ -use crate::SMALL_WORDS; - 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 { - if needle.bit_len == 0 { - return true; - } - if needle.bit_len > self.bit_len { - return false; - } - - funcs_for_contains_core::find_any_candidate( - &self.words, - self.bit_len, - needle.as_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 - && !funcs_for_contains_core::find_any_candidate( - &self.words, - self.bit_len, - needle.as_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.as_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 - && !funcs_for_contains_core::find_any_candidate( - &self.words, - self.bit_len, - needle.as_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.as_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 1cd0da4..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.as_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.as_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.as_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/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)); +} 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/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..e2864ca 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -5,11 +5,15 @@ 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_str::errors::*; pub use bit_string::errors::*; diff --git a/src/bit_string/traits.rs b/src/traits.rs similarity index 50% rename from src/bit_string/traits.rs rename to src/traits.rs index 497117a..2cd7831 100644 --- a/src/bit_string/traits.rs +++ b/src/traits.rs @@ -1,7 +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/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 90% 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 index 9d90c73..61a6277 100644 --- 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 @@ -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/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 92% 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 index a2a50d3..ea78dd8 100644 --- 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 @@ -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/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 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 90% 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 index 420e551..ca78b2d 100644 --- 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 @@ -27,11 +27,11 @@ 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), + &mut |pos| haystack.as_bit_str().bits_equal_at(pos, needle.as_bit_str()), ); // Brute-force reference: find any match. @@ -70,12 +70,12 @@ 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); + let ok = haystack.as_bit_str().bits_equal_at(pos, needle.as_bit_str()); if ok { any_found = true; } ok }, 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 95% 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 index 624d7fd..11257f0 100644 --- 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 @@ -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; @@ -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.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 89% 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 index 96d6eaa..e14c2e5 100644 --- 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 @@ -27,11 +27,11 @@ 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), + &mut |pos| haystack.as_bit_str().bits_equal_at(pos, needle.as_bit_str()), ); // Brute-force: find rightmost match. @@ -66,11 +66,11 @@ 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), + &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()); 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, + ) + } +} 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) + } + } +}