Skip to main content

vortex_buffer/bit/
mod.rs

1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: Copyright the Vortex contributors
3
4//! Packed bitmaps that can be used to store boolean values.
5//!
6//! This module provides a wrapper on top of the `Buffer` type to store mutable and immutable
7//! bitsets. The bitsets are stored in little-endian order, meaning that the least significant bit
8//! of the first byte is the first bit in the bitset.
9#[cfg(feature = "arrow")]
10mod arrow;
11mod buf;
12mod buf_mut;
13mod count_ones;
14mod macros;
15mod meta;
16mod ops;
17mod pack;
18mod select;
19mod take;
20mod view;
21
22pub use arrow_buffer::bit_chunk_iterator::BitChunkIterator;
23pub use arrow_buffer::bit_chunk_iterator::BitChunks;
24pub use arrow_buffer::bit_chunk_iterator::UnalignedBitChunk;
25pub use arrow_buffer::bit_chunk_iterator::UnalignedBitChunkIterator;
26pub use arrow_buffer::bit_iterator::BitIndexIterator;
27pub use arrow_buffer::bit_iterator::BitIterator;
28pub use arrow_buffer::bit_iterator::BitSliceIterator;
29pub use buf::*;
30pub use buf_mut::*;
31pub use meta::*;
32pub use pack::*;
33pub use take::*;
34pub use view::*;
35
36/// Packs up to 64 boolean values into a little-endian `u64` word.
37///
38/// This is [`collect_bool_words`] for a single word: a full 64-bit word is materialized as a
39/// `[bool; 64]` and packed with the baseline SIMD byte→bit instruction of the target; shorter
40/// lengths fall back to the bit-at-a-time [`collect_bool_word_scalar`] loop.
41#[inline]
42pub fn collect_bool_word<F>(len: usize, f: F) -> u64
43where
44    F: FnMut(usize) -> bool,
45{
46    assert!(len <= 64, "cannot pack {len} bits into a u64 word");
47
48    let mut word = [0u64; 1];
49    collect_bool_words_inline(&mut word, len, f);
50    word[0]
51}
52
53/// Pack `len` boolean values returned by `f` into the prefix of `words`, LSB-first,
54/// 64 bits per `u64`. `words` must have capacity for at least `len.div_ceil(64)` entries.
55///
56/// `f` is invoked exactly once per index, in ascending order `0..len`.
57///
58/// Writes via `=` (not `|=`), so the destination need not be zero-initialised.
59///
60/// The word loop packs with the baseline SIMD kernel of the target (SSE2 on x86-64, NEON on
61/// aarch64), which inlines fully into the caller together with the predicate and the
62/// `[bool; 64]` materialization — wider kernels would sit behind a non-inlinable
63/// `#[target_feature]` boundary that deoptimizes expensive predicates. See
64/// [`BitBuffer::collect_bool`] for the performance note on
65/// avoiding bounds checks in `f`.
66///
67/// Prefer this entry point for every predicate; only switch to
68/// [`collect_bool_words_multiversioned`] after carefully checking that your specific `f`
69/// meets its contract.
70#[inline]
71pub fn collect_bool_words<F>(words: &mut [u64], len: usize, f: F)
72where
73    F: FnMut(usize) -> bool,
74{
75    let num_words = len.div_ceil(64);
76    assert!(
77        words.len() >= num_words,
78        "words slice has {} entries, need at least {num_words}",
79        words.len(),
80    );
81
82    collect_bool_words_inline(words, len, f)
83}
84
85/// Read up to 8 bytes as a little-endian `u64`, zero-padding the high bytes when fewer than 8
86/// bytes are supplied.
87///
88/// This preserves Vortex's least-significant-bit-first bitmap numbering on little- and big-endian
89/// targets. For a full 8-byte slice it lowers to a single word load.
90#[inline]
91pub fn read_u64_le(bytes: &[u8]) -> u64 {
92    debug_assert!(bytes.len() <= 8);
93    let mut buf = [0u8; 8];
94    buf[..bytes.len()].copy_from_slice(bytes);
95    u64::from_le_bytes(buf)
96}
97
98/// Splice a packed word `w` (whose bits above the highest valid bit are zero) into
99/// `words` at the given bit position.
100///
101/// The destination word at `bit_offset / 64` is OR'd, preserving any bits below
102/// `bit_offset % 64`. When `w` has high bits that spill into the next word, those
103/// bits are *assigned* (not OR'd) — so callers must ensure that next slot is zero
104/// (e.g. via `BufferMut::zeroed`).
105///
106/// `words.len()` need only cover the slots `w` actually writes to: skipping the
107/// spillover when its bits are all zero means a tail that fits entirely in the
108/// leading word never touches `words[dest_word + 1]`.
109#[inline]
110pub fn splice_word_at_bit(words: &mut [u64], bit_offset: usize, word: u64) {
111    let dest_word = bit_offset / 64;
112    let bit_in_word = bit_offset % 64;
113    words[dest_word] |= word << bit_in_word;
114    if bit_in_word != 0 {
115        let high = word >> (64 - bit_in_word);
116        if high != 0 {
117            words[dest_word + 1] = high;
118        }
119    }
120}
121
122/// Pack `len` boolean values returned by `f` into `words` starting at bit position
123/// `bit_offset`, LSB-first.
124///
125/// Composes [`collect_bool_word`] (pack up to 64 bools into a u64) with
126/// [`splice_word_at_bit`] (merge the packed word into the destination via shift-OR).
127///
128/// `words` must have at least `(bit_offset + len).div_ceil(64)` entries; see
129/// [`splice_word_at_bit`] for zero-init requirements on words above the cursor.
130#[inline]
131pub fn pack_bools_into_words<F>(words: &mut [u64], bit_offset: usize, len: usize, mut f: F)
132where
133    F: FnMut(usize) -> bool,
134{
135    if len == 0 {
136        return;
137    }
138    let num_words = (bit_offset + len).div_ceil(64);
139    assert!(
140        words.len() >= num_words,
141        "words slice has {} entries, need at least {num_words}",
142        words.len(),
143    );
144
145    let mut done = 0;
146    while len - done >= 64 {
147        let word = collect_bool_word(64, |bit| f(done + bit));
148        splice_word_at_bit(words, bit_offset + done, word);
149        done += 64;
150    }
151    let tail = len - done;
152    if tail > 0 {
153        let word = collect_bool_word(tail, |bit| f(done + bit));
154        splice_word_at_bit(words, bit_offset + done, word);
155    }
156}
157
158/// Get the bit value at `index` out of `buf`.
159///
160/// # Panics
161///
162/// Panics if `index` is not between 0 and length of `buf * 8`.
163#[allow(clippy::inline_always)]
164#[inline(always)]
165pub fn get_bit(buf: &[u8], index: usize) -> bool {
166    buf[index / 8] & (1 << (index % 8)) != 0
167}
168
169/// Get the bit value at `index` out of `buf` without bounds checking.
170///
171/// # Safety
172///
173/// `index` must be between 0 and length of `buf * 8`.
174#[allow(clippy::inline_always)]
175#[inline(always)]
176pub unsafe fn get_bit_unchecked(buf: *const u8, index: usize) -> bool {
177    (unsafe { *buf.add(index / 8) } & (1 << (index % 8))) != 0
178}
179
180/// Set the bit value at `index` in `buf` without bounds checking.
181///
182/// # Safety
183///
184/// `index` must be between 0 and length of `buf * 8`.
185#[allow(clippy::inline_always)]
186#[inline(always)]
187pub unsafe fn set_bit_unchecked(buf: *mut u8, index: usize) {
188    unsafe { *buf.add(index / 8) |= 1 << (index % 8) };
189}
190
191/// Unset the bit value at `index` in `buf` without bounds checking.
192///
193/// # Safety
194///
195/// `index` must be between 0 and length of `buf * 8`.
196#[allow(clippy::inline_always)]
197#[inline(always)]
198pub unsafe fn unset_bit_unchecked(buf: *mut u8, index: usize) {
199    unsafe { *buf.add(index / 8) &= !(1 << (index % 8)) };
200}
201
202#[cfg(test)]
203mod tests {
204    use super::collect_bool_word;
205    use super::pack_bools_into_words;
206    use super::read_u64_le;
207
208    #[test]
209    fn collect_bool_word_packs_lsb_first() {
210        let word = collect_bool_word(5, |idx| idx.is_multiple_of(2));
211        assert_eq!(word, 0b10101);
212    }
213
214    #[test]
215    fn collect_bool_word_empty() {
216        assert_eq!(collect_bool_word(0, |_| true), 0);
217    }
218
219    #[test]
220    fn read_u64_le_zero_pads_tail() {
221        assert_eq!(read_u64_le(&[0x34, 0x12]), 0x1234);
222        assert_eq!(read_u64_le(&[0xff; 8]), u64::MAX);
223    }
224
225    #[test]
226    #[should_panic(expected = "cannot pack 65 bits into a u64 word")]
227    fn collect_bool_word_rejects_too_many_bits() {
228        let _ = collect_bool_word(65, |_| true);
229    }
230
231    fn pack(bit_offset: usize, len: usize, f: impl Fn(usize) -> bool) -> Vec<bool> {
232        let num_words = (bit_offset + len).div_ceil(64);
233        let mut words = vec![0u64; num_words];
234        pack_bools_into_words(&mut words, bit_offset, len, &f);
235        (0..bit_offset + len)
236            .map(|i| (words[i / 64] >> (i % 64)) & 1 == 1)
237            .collect()
238    }
239
240    #[test]
241    fn pack_bools_aligned_multi_word_with_tail() {
242        let bits = pack(0, 130, |i| i.is_multiple_of(3));
243        for i in 0..130 {
244            assert_eq!(bits[i], i.is_multiple_of(3), "bit {i}");
245        }
246    }
247
248    #[test]
249    fn pack_bools_unaligned_crossing_words() {
250        let bits = pack(40, 200, |i| i.is_multiple_of(7));
251        assert!(bits[..40].iter().all(|&b| !b));
252        for i in 0..200 {
253            assert_eq!(bits[40 + i], i.is_multiple_of(7), "bit {}", 40 + i);
254        }
255    }
256
257    #[test]
258    fn pack_bools_preserves_low_bits_of_leading_word() {
259        let mut words = vec![0u64; 2];
260        words[0] = 0b11111;
261        pack_bools_into_words(&mut words, 5, 70, |_| true);
262        for i in 0..5 {
263            assert_eq!((words[0] >> i) & 1, 1, "preserved bit {i}");
264        }
265        for i in 5..75 {
266            assert_eq!((words[i / 64] >> (i % 64)) & 1, 1, "extended bit {i}");
267        }
268    }
269}