use std::hash::Hash;
use std::marker::PhantomData;
use crate::core::filter::{BloomFilter, MutableBloomFilter};
use crate::core::params::{optimal_bit_count, optimal_hash_count};
use crate::error::{BloomCraftError, Result};
use crate::hash::{BloomHasher, StdHasher};
pub const BLOCK_SIZE_BITS: usize = 512;
pub const BLOCK_SIZE_WORDS: usize = 8;
const _: () = assert!(BLOCK_SIZE_BITS == BLOCK_SIZE_WORDS * 64);
const _: () = assert!(BLOCK_SIZE_BITS.is_power_of_two());
#[derive(Debug, Clone)]
pub struct RegisterBlockedBloomFilter<T, H = StdHasher>
where
H: BloomHasher + Clone + Default,
{
blocks: Vec<u64>,
num_blocks: usize,
bits_per_block: usize,
k: usize,
hasher: H,
expected_items: usize,
target_fpr: f64,
item_count: usize,
_phantom: PhantomData<T>,
}
impl<T, H> RegisterBlockedBloomFilter<T, H>
where
H: BloomHasher + Clone + Default,
{
pub const BLOCK_SIZE_BITS: usize = BLOCK_SIZE_BITS;
pub const BLOCK_SIZE_WORDS: usize = BLOCK_SIZE_WORDS;
}
impl<T, H> RegisterBlockedBloomFilter<T, H>
where
T: Hash,
H: BloomHasher + Clone + Default,
{
pub fn new(expected_items: usize, fpr: f64) -> Result<Self> {
Self::with_hasher(expected_items, fpr, H::default())
}
pub fn with_hasher(expected_items: usize, fpr: f64, hasher: H) -> Result<Self> {
if expected_items == 0 {
return Err(BloomCraftError::invalid_item_count(expected_items));
}
if fpr <= 0.0 || fpr >= 1.0 {
return Err(BloomCraftError::fp_rate_out_of_bounds(fpr));
}
let adjusted_fpr = fpr / 2.5;
let total_bits = optimal_bit_count(expected_items, adjusted_fpr)?;
let raw_blocks = total_bits.div_ceil(BLOCK_SIZE_BITS);
let num_blocks = raw_blocks.next_power_of_two().max(1);
let actual_total_bits = num_blocks * BLOCK_SIZE_BITS;
let k = optimal_hash_count(actual_total_bits, expected_items)?.clamp(2, 16);
let total_words = num_blocks * BLOCK_SIZE_WORDS;
let blocks = vec![0u64; total_words];
debug_assert!(num_blocks.is_power_of_two(), "INV-1 violated");
debug_assert!(num_blocks >= 1, "INV-1 violated");
debug_assert_eq!(blocks.len(), num_blocks * BLOCK_SIZE_WORDS, "INV-2 violated");
debug_assert!((2..=16).contains(&k), "INV-4 violated");
debug_assert!(fpr > 0.0 && fpr < 1.0 && fpr.is_finite(), "INV-5 violated");
Ok(Self {
blocks,
num_blocks,
bits_per_block: BLOCK_SIZE_BITS,
k,
hasher,
expected_items,
target_fpr: fpr,
item_count: 0,
_phantom: PhantomData,
})
}
#[inline]
pub const fn num_blocks(&self) -> usize {
self.num_blocks
}
#[inline]
pub const fn bits_per_block(&self) -> usize {
self.bits_per_block
}
#[inline]
pub const fn target_fpr(&self) -> f64 {
self.target_fpr
}
#[inline]
fn hash_item(&self, item: &T) -> (u64, u64) {
self.hasher.hash_item(item)
}
#[inline]
fn hash_to_block(&self, h1: u64) -> usize {
debug_assert!(self.num_blocks.is_power_of_two(), "INV-1 violated");
(h1 as usize) & (self.num_blocks - 1)
}
#[inline]
fn block(&self, block_idx: usize) -> &[u64] {
debug_assert!(block_idx < self.num_blocks);
let start = block_idx * BLOCK_SIZE_WORDS;
&self.blocks[start..start + BLOCK_SIZE_WORDS]
}
#[inline]
fn block_mut(&mut self, block_idx: usize) -> &mut [u64] {
debug_assert!(block_idx < self.num_blocks);
let start = block_idx * BLOCK_SIZE_WORDS;
&mut self.blocks[start..start + BLOCK_SIZE_WORDS]
}
}
impl<T, H> BloomFilter<T> for RegisterBlockedBloomFilter<T, H>
where
T: Hash + Send + Sync,
H: BloomHasher + Clone + Default + Send + Sync,
{
#[inline]
fn insert(&mut self, item: &T) {
let (h1, h2) = self.hash_item(item);
let h2 = h2 | 1;
let block_idx = self.hash_to_block(h1);
let k = self.k;
let block = self.block_mut(block_idx);
let mut hash = h1.rotate_left(16) ^ h2;
for _ in 0..k {
let bit_idx = (hash as usize) & (BLOCK_SIZE_BITS - 1);
let word_idx = bit_idx / 64;
let bit_offset = bit_idx % 64;
block[word_idx] |= 1u64 << bit_offset;
hash = hash.wrapping_add(h2);
}
self.item_count += 1;
}
#[inline]
fn contains(&self, item: &T) -> bool {
let (h1, h2) = self.hash_item(item);
let h2 = h2 | 1;
let block_idx = self.hash_to_block(h1);
let block = self.block(block_idx);
let mut hash = h1.rotate_left(16) ^ h2;
for _ in 0..self.k {
let bit_idx = (hash as usize) & (BLOCK_SIZE_BITS - 1);
let word_idx = bit_idx / 64;
let bit_offset = bit_idx % 64;
if block[word_idx] & (1u64 << bit_offset) == 0 {
return false;
}
hash = hash.wrapping_add(h2);
}
true
}
fn clear(&mut self) {
self.blocks.fill(0u64);
self.item_count = 0;
debug_assert_eq!(self.count_set_bits(), 0);
debug_assert_eq!(self.item_count, 0);
}
#[inline]
fn is_empty(&self) -> bool {
self.item_count == 0
}
#[inline]
fn len(&self) -> usize {
self.item_count
}
fn false_positive_rate(&self) -> f64 {
if self.item_count == 0 {
return 0.0;
}
let n = self.item_count as f64;
let m = self.bit_count() as f64;
let k = self.k as f64;
let fill_rate = 1.0_f64 - (-k * n / m).exp();
fill_rate.powi(self.k as i32)
}
#[inline]
fn expected_items(&self) -> usize {
self.expected_items
}
#[inline]
fn bit_count(&self) -> usize {
self.num_blocks * self.bits_per_block
}
#[inline]
fn hash_count(&self) -> usize {
self.k
}
fn estimate_count(&self) -> usize {
let set_bits: usize = self.blocks.iter().map(|w| w.count_ones() as usize).sum();
if set_bits == 0 {
return 0;
}
let m = self.bit_count() as f64;
let k = self.k as f64;
let max_x = m - self.num_blocks as f64;
let x = (set_bits as f64).min(max_x);
let estimated = -(m / k) * (1.0_f64 - x / m).ln();
estimated.max(0.0) as usize
}
fn count_set_bits(&self) -> usize {
self.blocks.iter().map(|w| w.count_ones() as usize).sum()
}
}
impl<T, H> MutableBloomFilter<T> for RegisterBlockedBloomFilter<T, H>
where
T: Hash + Send + Sync,
H: BloomHasher + Clone + Default + Send + Sync,
{
}
#[cfg(test)]
mod tests {
use super::*;
type Filter = RegisterBlockedBloomFilter<u64>;
#[test]
fn construction_succeeds_with_valid_params() {
let f = Filter::new(1_000, 0.01);
assert!(f.is_ok(), "expected Ok, got {:?}", f.err());
}
#[test]
fn construction_stores_expected_items_and_target_fpr() {
let f = Filter::new(5_000, 0.05).unwrap();
assert_eq!(f.expected_items(), 5_000);
assert_eq!(f.target_fpr(), 0.05);
}
#[test]
fn construction_with_hasher_produces_identical_structure() {
let f1 = Filter::new(1_000, 0.01).unwrap();
let f2 =
RegisterBlockedBloomFilter::<u64>::with_hasher(1_000, 0.01, StdHasher::default())
.unwrap();
assert_eq!(f1.num_blocks(), f2.num_blocks());
assert_eq!(f1.hash_count(), f2.hash_count());
assert_eq!(f1.bit_count(), f2.bit_count());
}
#[test]
fn construction_rejects_zero_expected_items() {
let err = Filter::new(0, 0.01).unwrap_err();
assert!(
matches!(err, BloomCraftError::InvalidItemCount { .. }),
"expected InvalidItemCount, got: {:?}",
err
);
}
#[test]
fn construction_rejects_fpr_of_zero() {
let err = Filter::new(1_000, 0.0).unwrap_err();
assert!(matches!(err, BloomCraftError::FalsePositiveRateOutOfBounds { .. }));
}
#[test]
fn construction_rejects_fpr_of_one() {
let err = Filter::new(1_000, 1.0).unwrap_err();
assert!(matches!(err, BloomCraftError::FalsePositiveRateOutOfBounds { .. }));
}
#[test]
fn construction_rejects_fpr_above_one() {
let err = Filter::new(1_000, 1.5).unwrap_err();
assert!(matches!(err, BloomCraftError::FalsePositiveRateOutOfBounds { .. }));
}
#[test]
fn construction_rejects_fpr_negative() {
let err = Filter::new(1_000, -0.01).unwrap_err();
assert!(matches!(err, BloomCraftError::FalsePositiveRateOutOfBounds { .. }));
}
#[test]
fn num_blocks_is_power_of_two_for_all_capacities() {
for &cap in &[1usize, 2, 10, 99, 100, 1_000, 9_999, 10_000, 100_000, 1_000_000] {
let f = Filter::new(cap, 0.01).expect("valid params");
let nb = f.num_blocks();
assert!(nb.is_power_of_two(), "cap={cap}: num_blocks={nb} not a power of two");
assert!(nb >= 1, "cap={cap}: num_blocks must be >= 1");
}
}
#[test]
fn bit_count_equals_num_blocks_times_block_size() {
for &cap in &[100usize, 10_000, 1_000_000] {
let f = Filter::new(cap, 0.01).unwrap();
assert_eq!(
f.bit_count(),
f.num_blocks() * Filter::BLOCK_SIZE_BITS,
"bit_count invariant violated for cap={cap}"
);
}
}
#[test]
fn backing_vec_length_equals_num_blocks_times_block_words() {
let f = Filter::new(10_000, 0.01).unwrap();
assert_eq!(f.blocks.len(), f.num_blocks() * Filter::BLOCK_SIZE_WORDS);
}
#[test]
fn bits_per_block_is_always_512() {
for &cap in &[100usize, 50_000] {
let f = Filter::new(cap, 0.01).unwrap();
assert_eq!(f.bits_per_block(), 512);
}
}
#[test]
fn hash_count_is_within_clamped_range() {
for &cap in &[100usize, 10_000, 1_000_000] {
for &fpr in &[0.5_f64, 0.1, 0.01, 0.001, 0.0001] {
let f = Filter::new(cap, fpr).unwrap();
let k = f.hash_count();
assert!(k >= 2 && k <= 16, "k={k} out of [2, 16] for cap={cap}, fpr={fpr}");
}
}
}
#[test]
fn associated_constants_match_instance_methods() {
let f = Filter::new(1_000, 0.01).unwrap();
assert_eq!(f.bits_per_block(), Filter::BLOCK_SIZE_BITS);
assert_eq!(Filter::BLOCK_SIZE_BITS, BLOCK_SIZE_BITS);
assert_eq!(Filter::BLOCK_SIZE_WORDS, BLOCK_SIZE_WORDS);
}
#[test]
fn no_false_negatives_at_small_scale() {
let mut f = Filter::new(1_000, 0.01).unwrap();
let items: Vec<u64> = (0..1_000).collect();
for &item in &items {
f.insert(&item);
}
for &item in &items {
assert!(f.contains(&item), "false negative for item={item}");
}
}
#[test]
fn no_false_negatives_at_capacity() {
let n = 10_000usize;
let mut f = Filter::new(n, 0.01).unwrap();
for i in 0u64..n as u64 {
f.insert(&i);
}
for i in 0u64..n as u64 {
assert!(f.contains(&i), "false negative at capacity for item={i}");
}
}
#[test]
fn no_false_negatives_at_large_scale() {
let n = 50_000usize;
let mut f = Filter::new(n, 0.001).unwrap();
for i in 0u64..n as u64 {
f.insert(&i);
}
for i in 0u64..n as u64 {
assert!(f.contains(&i), "false negative for item={i}");
}
}
#[test]
fn empirical_fpr_within_three_times_target_at_1pct() {
let n = 10_000usize;
let target_fpr = 0.01_f64;
let mut f = Filter::new(n, target_fpr).unwrap();
for i in 0u64..n as u64 {
f.insert(&i);
}
let probe_count = 10_000usize;
let false_positives: usize = (n as u64..n as u64 + probe_count as u64)
.filter(|i| f.contains(i))
.count();
let empirical = false_positives as f64 / probe_count as f64;
assert!(
empirical < 3.0 * target_fpr,
"empirical FPR {empirical:.4} exceeds 3× target {target_fpr:.4}"
);
}
#[test]
fn empirical_fpr_within_three_times_target_at_01pct() {
let n = 20_000usize;
let target_fpr = 0.001_f64;
let mut f = Filter::new(n, target_fpr).unwrap();
for i in 0u64..n as u64 {
f.insert(&i);
}
let probe_count = 50_000usize;
let false_positives: usize = (n as u64..n as u64 + probe_count as u64)
.filter(|i| f.contains(i))
.count();
let empirical = false_positives as f64 / probe_count as f64;
assert!(
empirical < 3.0 * target_fpr,
"empirical FPR {empirical:.5} exceeds 3× target {target_fpr:.5}"
);
}
#[test]
fn empty_filter_reports_is_empty_true() {
let f = Filter::new(1_000, 0.01).unwrap();
assert!(f.is_empty());
}
#[test]
fn empty_filter_reports_len_zero() {
let f = Filter::new(1_000, 0.01).unwrap();
assert_eq!(f.len(), 0);
}
#[test]
fn empty_filter_returns_false_for_all_contains() {
let f = Filter::new(1_000, 0.01).unwrap();
for i in 0u64..200 {
assert!(!f.contains(&i), "empty filter returned true for {i}");
}
}
#[test]
fn empty_filter_false_positive_rate_is_zero() {
let f = Filter::new(1_000, 0.01).unwrap();
assert_eq!(f.false_positive_rate(), 0.0);
}
#[test]
fn insert_transitions_is_empty_to_false() {
let mut f = Filter::new(1_000, 0.01).unwrap();
f.insert(&1u64);
assert!(!f.is_empty());
}
#[test]
fn len_equals_number_of_insert_calls() {
let mut f = Filter::new(1_000, 0.01).unwrap();
for i in 0u64..100 {
f.insert(&i);
assert_eq!(f.len(), (i + 1) as usize);
}
}
#[test]
fn len_counts_duplicate_inserts() {
let mut f = Filter::new(1_000, 0.01).unwrap();
f.insert(&42u64);
f.insert(&42u64);
f.insert(&42u64);
assert_eq!(f.len(), 3);
assert!(f.contains(&42u64));
}
#[test]
fn is_empty_and_len_are_consistent_after_insert_and_clear() {
let mut f = Filter::new(1_000, 0.01).unwrap();
assert!(f.is_empty());
assert_eq!(f.len(), 0);
f.insert(&1u64);
assert!(!f.is_empty());
assert_eq!(f.len(), 1);
f.clear();
assert!(f.is_empty());
assert_eq!(f.len(), 0);
}
#[test]
fn clear_zeroes_all_bits_and_resets_count() {
let mut f = Filter::new(1_000, 0.01).unwrap();
for i in 0u64..100 {
f.insert(&i);
}
assert_eq!(f.len(), 100);
assert!(!f.is_empty());
f.clear();
assert!(f.is_empty());
assert_eq!(f.len(), 0);
assert_eq!(f.count_set_bits(), 0);
}
#[test]
fn clear_makes_all_previously_inserted_items_not_found() {
let mut f = Filter::new(1_000, 0.01).unwrap();
let items: Vec<u64> = (0..100).collect();
for &item in &items {
f.insert(&item);
}
f.clear();
for &item in &items {
assert!(!f.contains(&item), "item {item} found after clear");
}
}
#[test]
fn false_positive_rate_increases_monotonically_with_fill() {
let mut f = Filter::new(10_000, 0.01).unwrap();
let mut prev_fpr = f.false_positive_rate();
assert_eq!(prev_fpr, 0.0);
for i in 0u64..10_000 {
f.insert(&i);
if i % 1_000 == 999 {
let fpr = f.false_positive_rate();
assert!(
fpr >= prev_fpr,
"FPR decreased from {prev_fpr:.6} to {fpr:.6} at i={i}"
);
prev_fpr = fpr;
}
}
}
#[test]
fn false_positive_rate_is_zero_after_clear() {
let mut f = Filter::new(10_000, 0.01).unwrap();
for i in 0u64..5_000 {
f.insert(&i);
}
assert!(f.false_positive_rate() > 0.0);
f.clear();
assert_eq!(f.false_positive_rate(), 0.0);
}
#[test]
fn count_set_bits_zero_on_empty_filter() {
let f = Filter::new(1_000, 0.01).unwrap();
assert_eq!(f.count_set_bits(), 0);
}
#[test]
fn count_set_bits_nonzero_after_insert() {
let mut f = Filter::new(1_000, 0.01).unwrap();
f.insert(&1u64);
assert!(f.count_set_bits() > 0);
}
#[test]
fn count_set_bits_never_exceeds_bit_count() {
let mut f = Filter::new(1_000, 0.01).unwrap();
for i in 0u64..5_000 {
f.insert(&i);
}
assert!(f.count_set_bits() <= f.bit_count());
}
#[test]
fn estimate_count_zero_on_empty_filter() {
let f = Filter::new(1_000, 0.01).unwrap();
assert_eq!(f.estimate_count(), 0);
}
#[test]
fn estimate_count_within_50_percent_at_half_capacity() {
let n = 10_000usize;
let mut f = Filter::new(n, 0.01).unwrap();
let insert_count = n / 2;
for i in 0u64..insert_count as u64 {
f.insert(&i);
}
let estimate = f.estimate_count();
let lower = insert_count / 2;
let upper = insert_count * 2;
assert!(
estimate >= lower && estimate <= upper,
"estimate_count {estimate} far from {insert_count} (expected [{lower}, {upper}])"
);
}
#[test]
fn clone_produces_independent_instance() {
let mut original = Filter::new(1_000, 0.01).unwrap();
original.insert(&10u64);
original.insert(&20u64);
let mut cloned = original.clone();
cloned.insert(&999u64);
cloned.clear();
assert_eq!(original.len(), 2);
assert!(original.contains(&10u64));
assert!(original.contains(&20u64));
assert!(cloned.is_empty());
}
#[test]
fn clone_has_identical_structural_params() {
let original = Filter::new(5_000, 0.005).unwrap();
let cloned = original.clone();
assert_eq!(original.num_blocks(), cloned.num_blocks());
assert_eq!(original.bit_count(), cloned.bit_count());
assert_eq!(original.hash_count(), cloned.hash_count());
assert_eq!(original.bits_per_block(), cloned.bits_per_block());
assert_eq!(original.target_fpr(), cloned.target_fpr());
assert_eq!(original.expected_items(), cloned.expected_items());
}
#[test]
fn works_with_string_items() {
let mut f = RegisterBlockedBloomFilter::<String>::new(500, 0.01).unwrap();
let words = ["hello", "world", "rust", "bloom", "filter"];
for word in &words {
f.insert(&word.to_string());
}
for word in &words {
assert!(f.contains(&word.to_string()), "false negative for '{word}'");
}
}
#[test]
fn filter_satisfies_send_and_sync() {
fn assert_send_sync<T: Send + Sync>() {}
assert_send_sync::<Filter>();
assert_send_sync::<RegisterBlockedBloomFilter<String>>();
}
#[test]
fn hash_to_block_always_in_range() {
let f = Filter::new(10_000, 0.01).unwrap();
let num_blocks = f.num_blocks();
let test_hashes = [
0u64,
1,
u64::MAX,
u64::MAX / 2,
0xDEAD_BEEF_CAFE_BABE,
0x0101_0101_0101_0101,
0xFFFF_FFFF_0000_0000,
];
for &h in &test_hashes {
let idx = f.hash_to_block(h);
assert!(idx < num_blocks, "hash_to_block({h:#x}) = {idx} >= num_blocks={num_blocks}");
}
}
#[test]
fn block_selection_uses_low_bits_of_h1() {
let f = Filter::new(10_000, 0.01).unwrap();
let mask = f.num_blocks() - 1;
for h in 0u64..1_000 {
let expected = (h as usize) & mask;
let actual = f.hash_to_block(h);
assert_eq!(actual, expected, "h={h}");
}
}
#[derive(Debug, Clone, Default)]
struct ZeroH2Hasher;
impl BloomHasher for ZeroH2Hasher {
fn name(&self) -> &'static str {
"ZeroH2Hasher"
}
fn hash_bytes(&self, _bytes: &[u8]) -> u64 {
0
}
fn hash_item<T: Hash>(&self, _item: &T) -> (u64, u64) {
(1, 0)
}
}
#[test]
fn h2_eq_zero_produces_distinct_probes() {
let mut f = RegisterBlockedBloomFilter::<u64, ZeroH2Hasher>::with_hasher(
100,
0.01,
ZeroH2Hasher,
)
.unwrap();
let k = f.hash_count();
assert!(k >= 2, "k={k} must be >= 2");
f.insert(&42u64);
let set = f.count_set_bits();
assert!(
set >= 2,
"probe collapse detected: {set} bit(s) set for k={k}, expected >= 2"
);
assert!(f.contains(&42u64), "item not found after insertion with ZeroH2Hasher");
}
#[test]
fn over_capacity_insertion_no_false_negatives() {
let n = 1_000usize;
let mut f = Filter::new(n, 0.01).unwrap();
let over = 5 * n;
for i in 0u64..over as u64 {
f.insert(&i);
}
for i in 0u64..over as u64 {
assert!(f.contains(&i), "false negative at over-capacity for item={i}");
}
}
#[test]
fn clear_and_reinsert_no_false_negatives() {
let mut f = Filter::new(5_000, 0.01).unwrap();
let items: Vec<u64> = (0..5_000).collect();
for &item in &items {
f.insert(&item);
}
f.clear();
for &item in &items {
assert!(!f.contains(&item), "item {item} found after clear");
}
for &item in &items {
f.insert(&item);
}
for &item in &items {
assert!(f.contains(&item), "false negative after reinsert for item={item}");
}
}
}