Skip to main content

subetha_cxc/
shared_bit_vec.rs

1//! `SharedBitVec` - cross-process bit-packed boolean array.
2//!
3//! Fixed-size bit array stored in an MMF. Each underlying u64 word
4//! is operated on atomically (fetch_or for set, fetch_and for clear)
5//! so concurrent writers don't lose updates.
6//!
7//! # Layout
8//!
9//! ```text
10//! +---------------------------+
11//! | BitVecHeader (64B)        |
12//! |   magic, capacity_bits    |
13//! +---------------------------+
14//! | words[ceil(cap/64)]       |  AtomicU64 each
15//! +---------------------------+
16//! ```
17//!
18//! # Concurrency
19//!
20//! - `set(i)` -> `word.fetch_or(1 << bit, AcqRel)`. Multiple
21//!   writers setting distinct bits in the same word compose
22//!   correctly (RMW; no lost updates).
23//! - `clear(i)` -> `word.fetch_and(!(1 << bit), AcqRel)`.
24//! - `toggle(i)` -> `word.fetch_xor(1 << bit, AcqRel)`.
25//! - `get(i)` -> `word.load(Acquire) & (1 << bit) != 0`.
26//! - `set_range(lo, hi)` / `clear_range(lo, hi)` use RMW on
27//!   boundary words and `store` on fully-covered interior words.
28//!   Interior stores are safe because they overwrite all 64 bits;
29//!   no concurrent writer can be modifying interior bits THIS
30//!   call expects to keep (we're setting/clearing them all).
31//!
32//! # Use cases
33//!
34//! - Cross-process set membership (presence/absence flags).
35//! - Bloom filter backing array.
36//! - Allocation bitmaps (slot in use / free).
37//! - Feature flag arrays.
38//! - Multi-process work-stealing claim bits.
39
40use std::fs::{File, OpenOptions};
41use std::mem::size_of;
42use std::path::Path;
43use std::sync::atomic::{AtomicU64, Ordering};
44
45use memmap2::{MmapMut, MmapOptions};
46
47pub const BITVEC_MAGIC: u64 = 0x4150_4256_4543_2031;
48pub const BITS_PER_WORD: usize = 64;
49
50#[repr(C, align(64))]
51pub struct BitVecHeader {
52    pub magic: u64,
53    pub capacity_bits: u64,
54    pub word_count: u64,
55    _pad: [u8; 40],
56}
57
58const _: () = {
59    assert!(size_of::<BitVecHeader>() == 64);
60};
61
62pub const fn bit_vec_file_size(capacity_bits: usize) -> usize {
63    let word_count = capacity_bits.div_ceil(BITS_PER_WORD);
64    size_of::<BitVecHeader>() + word_count * size_of::<AtomicU64>()
65}
66
67#[derive(Debug, Clone, Copy, PartialEq, Eq)]
68pub enum BitVecError {
69    OutOfBounds,
70    LayoutMismatch,
71    IoError(std::io::ErrorKind),
72}
73
74impl From<std::io::Error> for BitVecError {
75    fn from(e: std::io::Error) -> Self { Self::IoError(e.kind()) }
76}
77
78pub struct SharedBitVec {
79    _file: File,
80    mmap: MmapMut,
81    capacity_bits: usize,
82    word_count: usize,
83    header_sidecar: subetha_core::HandshakeHeader,
84    ring_sidecar: Box<subetha_core::ObservationRing>,
85}
86
87unsafe impl Send for SharedBitVec {}
88unsafe impl Sync for SharedBitVec {}
89
90impl subetha_sidecar::AdaptiveInstance for SharedBitVec {
91    fn header(&self) -> &subetha_core::HandshakeHeader { &self.header_sidecar }
92    fn ring(&self) -> &subetha_core::ObservationRing { &self.ring_sidecar }
93    fn make_policy(&self) -> Box<dyn subetha_sidecar::Policy> {
94        Box::new(subetha_sidecar::NoMigrationPolicy)
95    }
96}
97
98impl SharedBitVec {
99    pub fn create(
100        path: impl AsRef<Path>, capacity_bits: usize,
101    ) -> Result<Self, BitVecError> {
102        assert!(capacity_bits >= 1);
103        let word_count = capacity_bits.div_ceil(BITS_PER_WORD);
104        let total = bit_vec_file_size(capacity_bits);
105        let file = OpenOptions::new()
106            .read(true).write(true).create(true).truncate(true)
107            .open(path.as_ref())?;
108        file.set_len(total as u64)?;
109        let mut mmap = unsafe { MmapOptions::new().len(total).map_mut(&file)? };
110        let hdr = mmap.as_mut_ptr() as *mut BitVecHeader;
111        unsafe {
112            std::ptr::write_bytes(hdr as *mut u8, 0, size_of::<BitVecHeader>());
113            (*hdr).magic = BITVEC_MAGIC;
114            (*hdr).capacity_bits = capacity_bits as u64;
115            (*hdr).word_count = word_count as u64;
116        }
117        Ok(Self {
118            _file: file, mmap, capacity_bits, word_count,
119            header_sidecar: subetha_core::HandshakeHeader::new(),
120            ring_sidecar: Box::new(subetha_core::ObservationRing::new()),
121        })
122    }
123
124    pub fn open(
125        path: impl AsRef<Path>, expected_capacity_bits: usize,
126    ) -> Result<Self, BitVecError> {
127        let total = bit_vec_file_size(expected_capacity_bits);
128        let file = OpenOptions::new().read(true).write(true).open(path.as_ref())?;
129        if file.metadata()?.len() < total as u64 {
130            return Err(BitVecError::LayoutMismatch);
131        }
132        let mmap = unsafe { MmapOptions::new().len(total).map_mut(&file)? };
133        let hdr = unsafe { &*(mmap.as_ptr() as *const BitVecHeader) };
134        if hdr.magic != BITVEC_MAGIC || hdr.capacity_bits != expected_capacity_bits as u64 {
135            return Err(BitVecError::LayoutMismatch);
136        }
137        let word_count = hdr.word_count as usize;
138        Ok(Self {
139            _file: file, mmap, capacity_bits: expected_capacity_bits, word_count,
140            header_sidecar: subetha_core::HandshakeHeader::new(),
141            ring_sidecar: Box::new(subetha_core::ObservationRing::new()),
142        })
143    }
144
145    #[inline]
146    pub fn capacity_bits(&self) -> usize { self.capacity_bits }
147
148    #[inline]
149    pub fn capacity_words(&self) -> usize { self.word_count }
150
151    fn word(&self, word_idx: usize) -> &AtomicU64 {
152        let base = unsafe { self.mmap.as_ptr().add(size_of::<BitVecHeader>()) };
153        unsafe { &*(base.add(word_idx * size_of::<AtomicU64>()) as *const AtomicU64) }
154    }
155
156    #[inline]
157    fn check_bounds(&self, bit: usize) -> Result<(), BitVecError> {
158        if bit >= self.capacity_bits { Err(BitVecError::OutOfBounds) } else { Ok(()) }
159    }
160
161    /// Set bit `i` to 1. Returns the previous value at that bit.
162    pub fn set(&self, i: usize) -> Result<bool, BitVecError> {
163        if let Err(e) = self.check_bounds(i) {
164            self.ring_sidecar
165                .push_op(crate::sidecar_ops::bit_vec::OP_SET, 1);
166            return Err(e);
167        }
168        let (w, b) = (i / BITS_PER_WORD, i % BITS_PER_WORD);
169        let mask = 1u64 << b;
170        let prev = self.word(w).fetch_or(mask, Ordering::AcqRel);
171        self.ring_sidecar
172            .push_op(crate::sidecar_ops::bit_vec::OP_SET, 0);
173        Ok((prev & mask) != 0)
174    }
175
176    /// Clear bit `i` to 0. Returns the previous value at that bit.
177    pub fn clear(&self, i: usize) -> Result<bool, BitVecError> {
178        if let Err(e) = self.check_bounds(i) {
179            self.ring_sidecar
180                .push_op(crate::sidecar_ops::bit_vec::OP_CLEAR, 1);
181            return Err(e);
182        }
183        let (w, b) = (i / BITS_PER_WORD, i % BITS_PER_WORD);
184        let mask = 1u64 << b;
185        let prev = self.word(w).fetch_and(!mask, Ordering::AcqRel);
186        self.ring_sidecar
187            .push_op(crate::sidecar_ops::bit_vec::OP_CLEAR, 0);
188        Ok((prev & mask) != 0)
189    }
190
191    /// Flip bit `i`. Returns the new value at that bit.
192    pub fn toggle(&self, i: usize) -> Result<bool, BitVecError> {
193        if let Err(e) = self.check_bounds(i) {
194            self.ring_sidecar
195                .push_op(crate::sidecar_ops::bit_vec::OP_TOGGLE, 1);
196            return Err(e);
197        }
198        let (w, b) = (i / BITS_PER_WORD, i % BITS_PER_WORD);
199        let mask = 1u64 << b;
200        let prev = self.word(w).fetch_xor(mask, Ordering::AcqRel);
201        self.ring_sidecar
202            .push_op(crate::sidecar_ops::bit_vec::OP_TOGGLE, 0);
203        // New value is the flip of the previous.
204        Ok((prev & mask) == 0)
205    }
206
207    /// Read bit `i`.
208    pub fn get(&self, i: usize) -> Result<bool, BitVecError> {
209        if let Err(e) = self.check_bounds(i) {
210            self.ring_sidecar
211                .push_op(crate::sidecar_ops::bit_vec::OP_GET, 1);
212            return Err(e);
213        }
214        let (w, b) = (i / BITS_PER_WORD, i % BITS_PER_WORD);
215        let mask = 1u64 << b;
216        let v = (self.word(w).load(Ordering::Acquire) & mask) != 0;
217        self.ring_sidecar
218            .push_op(crate::sidecar_ops::bit_vec::OP_GET, 0);
219        Ok(v)
220    }
221
222    /// Set all bits in `lo..hi` (exclusive end). Uses RMW on boundary
223    /// words and unconditional store on interior words.
224    pub fn set_range(&self, lo: usize, hi: usize) -> Result<(), BitVecError> {
225        if lo > hi || hi > self.capacity_bits {
226            self.ring_sidecar
227                .push_op(crate::sidecar_ops::bit_vec::OP_RANGE, 1);
228            return Err(BitVecError::OutOfBounds);
229        }
230        if lo != hi { self.range_op(lo, hi, RangeOp::Set); }
231        self.ring_sidecar
232            .push_op(crate::sidecar_ops::bit_vec::OP_RANGE, 0);
233        Ok(())
234    }
235
236    /// Clear all bits in `lo..hi` (exclusive end). Same RMW-on-
237    /// boundary, store-on-interior pattern.
238    pub fn clear_range(&self, lo: usize, hi: usize) -> Result<(), BitVecError> {
239        if lo > hi || hi > self.capacity_bits {
240            self.ring_sidecar
241                .push_op(crate::sidecar_ops::bit_vec::OP_RANGE, 1);
242            return Err(BitVecError::OutOfBounds);
243        }
244        if lo != hi { self.range_op(lo, hi, RangeOp::Clear); }
245        self.ring_sidecar
246            .push_op(crate::sidecar_ops::bit_vec::OP_RANGE, 0);
247        Ok(())
248    }
249
250    fn range_op(&self, lo: usize, hi: usize, op: RangeOp) {
251        let lo_word = lo / BITS_PER_WORD;
252        let hi_word = (hi - 1) / BITS_PER_WORD;  // inclusive end word
253        let lo_bit = lo % BITS_PER_WORD;
254        let hi_bit_excl = hi - hi_word * BITS_PER_WORD;
255        if lo_word == hi_word {
256            // Single-word case: mask covers bits [lo_bit, hi_bit_excl).
257            let count = hi - lo;
258            let mask = if count == BITS_PER_WORD { u64::MAX }
259                       else { ((1u64 << count) - 1) << lo_bit };
260            match op {
261                RangeOp::Set => { self.word(lo_word).fetch_or(mask, Ordering::AcqRel); }
262                RangeOp::Clear => { self.word(lo_word).fetch_and(!mask, Ordering::AcqRel); }
263            }
264            return;
265        }
266        // First word: partial cover at the high end.
267        let lo_mask = !((1u64 << lo_bit) - 1);
268        match op {
269            RangeOp::Set => { self.word(lo_word).fetch_or(lo_mask, Ordering::AcqRel); }
270            RangeOp::Clear => { self.word(lo_word).fetch_and(!lo_mask, Ordering::AcqRel); }
271        }
272        // Interior words: fully covered, plain Release-store.
273        for w in (lo_word + 1)..hi_word {
274            let v = match op {
275                RangeOp::Set => u64::MAX,
276                RangeOp::Clear => 0,
277            };
278            self.word(w).store(v, Ordering::Release);
279        }
280        // Last word: partial cover at the low end.
281        let hi_mask = if hi_bit_excl == BITS_PER_WORD { u64::MAX }
282                      else { (1u64 << hi_bit_excl) - 1 };
283        match op {
284            RangeOp::Set => { self.word(hi_word).fetch_or(hi_mask, Ordering::AcqRel); }
285            RangeOp::Clear => { self.word(hi_word).fetch_and(!hi_mask, Ordering::AcqRel); }
286        }
287    }
288
289    /// Count total number of 1-bits across all words. O(words).
290    pub fn count_ones(&self) -> usize {
291        let mut sum = 0usize;
292        for w in 0..self.word_count {
293            sum += self.word(w).load(Ordering::Acquire).count_ones() as usize;
294        }
295        self.ring_sidecar
296            .push_op(crate::sidecar_ops::bit_vec::OP_COUNT_ONES, 0);
297        // The last word may have padding bits beyond capacity_bits;
298        // those start as 0 and should never be set by our public API,
299        // so they don't affect the count.
300        sum
301    }
302
303    /// Count total number of 0-bits in the valid range.
304    pub fn count_zeros(&self) -> usize {
305        self.capacity_bits.saturating_sub(self.count_ones())
306    }
307
308    pub fn is_all_set(&self) -> bool { self.count_ones() == self.capacity_bits }
309    pub fn is_all_clear(&self) -> bool { self.count_ones() == 0 }
310
311    /// Set every bit. Unconditional Release-store on every word,
312    /// with the last word masked to only valid bits so count_ones
313    /// stays accurate.
314    pub fn set_all(&self) {
315        if self.word_count == 0 { return; }
316        let full_words = self.word_count - 1;
317        for w in 0..full_words {
318            self.word(w).store(u64::MAX, Ordering::Release);
319        }
320        // Last word: only set bits that fall within capacity_bits.
321        let last_word_bits = self.capacity_bits - full_words * BITS_PER_WORD;
322        let last_mask = if last_word_bits == BITS_PER_WORD { u64::MAX }
323                        else { (1u64 << last_word_bits) - 1 };
324        self.word(full_words).store(last_mask, Ordering::Release);
325    }
326
327    /// Clear every bit.
328    pub fn clear_all(&self) {
329        for w in 0..self.word_count {
330            self.word(w).store(0, Ordering::Release);
331        }
332    }
333
334    pub fn flush(&self) -> Result<(), BitVecError> {
335        self.mmap.flush()?;
336        Ok(())
337    }
338
339    pub fn flush_async(&self) -> Result<(), BitVecError> {
340        self.mmap.flush_async()?;
341        Ok(())
342    }
343}
344
345#[derive(Clone, Copy)]
346enum RangeOp { Set, Clear }
347
348#[cfg(test)]
349mod tests {
350    use super::*;
351    use std::sync::Arc;
352    use std::thread;
353
354    fn tmp(name: &str) -> std::path::PathBuf {
355        let mut p = std::env::temp_dir();
356        let pid = std::process::id();
357        p.push(format!("subetha-bitvec-{name}-{pid}.bin"));
358        p
359    }
360
361    #[test]
362    fn create_initial_all_zero() {
363        let p = tmp("init");
364        let b = SharedBitVec::create(&p, 100).unwrap();
365        assert_eq!(b.capacity_bits(), 100);
366        assert_eq!(b.count_ones(), 0);
367        assert!(b.is_all_clear());
368        for i in 0..100 {
369            assert!(!b.get(i).unwrap(), "bit {i} should start clear");
370        }
371        std::fs::remove_file(&p).ok();
372    }
373
374    #[test]
375    fn set_get_round_trip() {
376        let p = tmp("rt");
377        let b = SharedBitVec::create(&p, 200).unwrap();
378        assert!(!b.set(5).unwrap());
379        assert!(!b.set(63).unwrap());
380        assert!(!b.set(64).unwrap());
381        assert!(!b.set(199).unwrap());
382        assert!(b.get(5).unwrap());
383        assert!(b.get(63).unwrap());
384        assert!(b.get(64).unwrap());
385        assert!(b.get(199).unwrap());
386        assert!(!b.get(6).unwrap());
387        assert!(!b.get(0).unwrap());
388        // Setting again returns the previous value (now true).
389        assert!(b.set(5).unwrap());
390        std::fs::remove_file(&p).ok();
391    }
392
393    #[test]
394    fn clear_works() {
395        let p = tmp("clear");
396        let b = SharedBitVec::create(&p, 64).unwrap();
397        b.set(10).unwrap();
398        b.set(20).unwrap();
399        assert!(b.clear(10).unwrap());
400        assert!(!b.get(10).unwrap());
401        assert!(b.get(20).unwrap());
402        // Clearing already-clear bit returns false.
403        assert!(!b.clear(0).unwrap());
404        std::fs::remove_file(&p).ok();
405    }
406
407    #[test]
408    fn toggle_alternates() {
409        let p = tmp("toggle");
410        let b = SharedBitVec::create(&p, 8).unwrap();
411        assert!(b.toggle(3).unwrap());   // was 0, now 1
412        assert!(b.get(3).unwrap());
413        assert!(!b.toggle(3).unwrap());  // was 1, now 0
414        assert!(!b.get(3).unwrap());
415        std::fs::remove_file(&p).ok();
416    }
417
418    #[test]
419    fn count_ones_accurate() {
420        let p = tmp("count");
421        let b = SharedBitVec::create(&p, 200).unwrap();
422        for i in [0, 7, 31, 63, 64, 100, 199] {
423            b.set(i).unwrap();
424        }
425        assert_eq!(b.count_ones(), 7);
426        assert_eq!(b.count_zeros(), 200 - 7);
427        std::fs::remove_file(&p).ok();
428    }
429
430    #[test]
431    fn set_range_single_word() {
432        let p = tmp("range-single");
433        let b = SharedBitVec::create(&p, 64).unwrap();
434        b.set_range(10, 20).unwrap();
435        for i in 0..64 {
436            let expected = (10..20).contains(&i);
437            assert_eq!(b.get(i).unwrap(), expected, "bit {i}");
438        }
439        std::fs::remove_file(&p).ok();
440    }
441
442    #[test]
443    fn set_range_spans_multiple_words() {
444        let p = tmp("range-multi");
445        let b = SharedBitVec::create(&p, 200).unwrap();
446        b.set_range(50, 150).unwrap();
447        for i in 0..200 {
448            let expected = (50..150).contains(&i);
449            assert_eq!(b.get(i).unwrap(), expected, "bit {i}");
450        }
451        assert_eq!(b.count_ones(), 100);
452        std::fs::remove_file(&p).ok();
453    }
454
455    #[test]
456    fn clear_range_after_set_all() {
457        let p = tmp("clear-range");
458        let b = SharedBitVec::create(&p, 200).unwrap();
459        b.set_all();
460        assert!(b.is_all_set());
461        b.clear_range(70, 130).unwrap();
462        for i in 0..200 {
463            let expected = !(70..130).contains(&i);
464            assert_eq!(b.get(i).unwrap(), expected, "bit {i}");
465        }
466        std::fs::remove_file(&p).ok();
467    }
468
469    #[test]
470    fn out_of_bounds_rejected() {
471        let p = tmp("oob");
472        let b = SharedBitVec::create(&p, 8).unwrap();
473        assert_eq!(b.set(8).err(), Some(BitVecError::OutOfBounds));
474        assert_eq!(b.set(999).err(), Some(BitVecError::OutOfBounds));
475        assert_eq!(b.get(8).err(), Some(BitVecError::OutOfBounds));
476        assert_eq!(b.set_range(0, 9).err(), Some(BitVecError::OutOfBounds));
477        std::fs::remove_file(&p).ok();
478    }
479
480    #[test]
481    fn set_all_and_clear_all() {
482        let p = tmp("all");
483        let b = SharedBitVec::create(&p, 130).unwrap();
484        b.set_all();
485        assert!(b.is_all_set());
486        for i in 0..130 { assert!(b.get(i).unwrap()); }
487        b.clear_all();
488        assert!(b.is_all_clear());
489        for i in 0..130 { assert!(!b.get(i).unwrap()); }
490        std::fs::remove_file(&p).ok();
491    }
492
493    #[test]
494    fn cross_handle_visibility() {
495        let p = tmp("cross-handle");
496        let writer = SharedBitVec::create(&p, 100).unwrap();
497        let reader = SharedBitVec::open(&p, 100).unwrap();
498        writer.set(42).unwrap();
499        writer.set(77).unwrap();
500        assert!(reader.get(42).unwrap());
501        assert!(reader.get(77).unwrap());
502        assert!(!reader.get(0).unwrap());
503        reader.clear(42).unwrap();
504        assert!(!writer.get(42).unwrap());
505        std::fs::remove_file(&p).ok();
506    }
507
508    #[test]
509    fn concurrent_setters_of_disjoint_bits_all_visible() {
510        // 4 threads each set 100 distinct bits; all 400 must be visible.
511        let p = tmp("concurrent-disjoint");
512        let b = Arc::new(SharedBitVec::create(&p, 1000).unwrap());
513        let n_threads = 4;
514        let per_thread = 100;
515        let mut handles = vec![];
516        for t in 0..n_threads {
517            let b = b.clone();
518            handles.push(thread::spawn(move || {
519                for i in 0..per_thread {
520                    let bit = t * per_thread + i;
521                    b.set(bit).unwrap();
522                }
523            }));
524        }
525        for h in handles { h.join().unwrap(); }
526        assert_eq!(b.count_ones(), n_threads * per_thread);
527        for t in 0..n_threads {
528            for i in 0..per_thread {
529                assert!(b.get(t * per_thread + i).unwrap());
530            }
531        }
532        std::fs::remove_file(&p).ok();
533    }
534
535    #[test]
536    fn concurrent_setters_of_same_word_distinct_bits_all_visible() {
537        // 64 threads each set ONE distinct bit in the same word.
538        // Without atomic RMW this would race and lose updates.
539        let p = tmp("concurrent-same-word");
540        let b = Arc::new(SharedBitVec::create(&p, 64).unwrap());
541        let mut handles = vec![];
542        for bit in 0..64 {
543            let b = b.clone();
544            handles.push(thread::spawn(move || {
545                b.set(bit).unwrap();
546            }));
547        }
548        for h in handles { h.join().unwrap(); }
549        assert!(b.is_all_set(), "every bit in the word should be set");
550        std::fs::remove_file(&p).ok();
551    }
552
553    #[test]
554    fn concurrent_setters_of_same_bit_idempotent() {
555        // 16 threads all set bit 42. Result: bit 42 set exactly once.
556        let p = tmp("concurrent-same-bit");
557        let b = Arc::new(SharedBitVec::create(&p, 100).unwrap());
558        let mut handles = vec![];
559        for _ in 0..16 {
560            let b = b.clone();
561            handles.push(thread::spawn(move || b.set(42).unwrap()));
562        }
563        let prev_values: Vec<bool> = handles.into_iter()
564            .map(|h| h.join().unwrap()).collect();
565        // Exactly one set saw prev=false; the others saw prev=true.
566        let false_count = prev_values.iter().filter(|&&v| !v).count();
567        let true_count = prev_values.iter().filter(|&&v| v).count();
568        assert_eq!(false_count, 1, "exactly one setter should see prev=false");
569        assert_eq!(true_count, 15);
570        assert!(b.get(42).unwrap());
571        assert_eq!(b.count_ones(), 1);
572        std::fs::remove_file(&p).ok();
573    }
574
575    #[test]
576    fn disk_persistence_survives_reopen() {
577        let p = tmp("disk");
578        {
579            let b = SharedBitVec::create(&p, 200).unwrap();
580            for i in [5, 50, 100, 150, 199] { b.set(i).unwrap(); }
581            b.flush().unwrap();
582        }
583        let b2 = SharedBitVec::open(&p, 200).unwrap();
584        assert_eq!(b2.count_ones(), 5);
585        for i in [5, 50, 100, 150, 199] { assert!(b2.get(i).unwrap()); }
586        for i in [0, 4, 6, 49, 51] { assert!(!b2.get(i).unwrap()); }
587        std::fs::remove_file(&p).ok();
588    }
589
590    #[test]
591    fn capacity_words_matches_packed_count() {
592        let p = tmp("words");
593        let b1 = SharedBitVec::create(&p, 64).unwrap();
594        assert_eq!(b1.capacity_words(), 1);
595        drop(b1);
596        std::fs::remove_file(&p).ok();
597
598        let p2 = tmp("words2");
599        let b2 = SharedBitVec::create(&p2, 65).unwrap();
600        assert_eq!(b2.capacity_words(), 2);
601        drop(b2);
602        std::fs::remove_file(&p2).ok();
603
604        let p3 = tmp("words3");
605        let b3 = SharedBitVec::create(&p3, 1000).unwrap();
606        assert_eq!(b3.capacity_words(), 16);  // ceil(1000/64) = 16
607        drop(b3);
608        std::fs::remove_file(&p3).ok();
609    }
610
611    #[test]
612    fn allocation_bitmap_pattern() {
613        // Realistic use: cross-process allocation bitmap. set() to
614        // claim a slot, clear() to release. count_ones reports
615        // current usage.
616        let p = tmp("alloc-pattern");
617        let b = SharedBitVec::create(&p, 1024).unwrap();
618        // Claim slots 0..10.
619        for i in 0..10 { assert!(!b.set(i).unwrap()); }
620        assert_eq!(b.count_ones(), 10);
621        // Release slot 5.
622        assert!(b.clear(5).unwrap());
623        assert_eq!(b.count_ones(), 9);
624        // Re-claim slot 5.
625        assert!(!b.set(5).unwrap());
626        assert_eq!(b.count_ones(), 10);
627        std::fs::remove_file(&p).ok();
628    }
629}