Skip to main content

hermes_simd_core/
mask.rs

1//! Bit-packed lane mask for SIMD predicated operations.
2//!
3//! `BitMask<N>` stores a predicate for up to 64 SIMD lanes in the bits of a `u64`.
4//! Bit `i` of the inner value corresponds to lane `i`; bits `[N..]` are always zero.
5//!
6//! # Design rationale
7//!
8//! The prior `mask_from_bools(&[bool])` approach passed a byte-per-element slice on every
9//! hot vector call — 8× more memory than necessary, and requires a loop to convert.
10//! `BitMask<N>` eliminates this by:
11//!
12//! - AVX-512: direct `transmute` from `__mmask16` / `__mmask8` (both `u16`/`u8` ⊆ `u64`).
13//! - Scalar: `from_bools` is a single bitwise-OR loop; `leading_k` is a const expression.
14//! - AVX2 / NEON: `mask_from_bitmask` on `SimdKernel` expands `BitMask<N>` to the native
15//!   float blend mask (`__m256` / `uint32x4_t`) once at the entry point.
16//!
17//! All `BitMask` methods are either `const` or trivially inlineable.
18
19/// Bit-packed predicate mask for exactly `N` SIMD lanes.
20///
21/// The inner `u64` stores one bit per lane: bit `i` = lane `i` is active.
22/// Invariant: `(self.0 >> N) == 0` (high bits always clear).
23///
24/// `N` must satisfy `N <= 64`. Violations are caught by a const assertion in `ALL_ACTIVE`.
25#[derive(rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)]
26#[rkyv(derive(Clone, Copy, Debug, PartialEq, Eq, Hash))]
27#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
28#[repr(transparent)]
29pub struct BitMask<const N: usize>(pub u64);
30
31impl<const N: usize> BitMask<N> {
32    /// All `N` lanes active.
33    ///
34    /// # Panics (compile-time)
35    /// Panics at compile time if `N > 64`.
36    pub const ALL_ACTIVE: Self = {
37        // Const assertion: N must fit in u64.
38        assert!(N <= 64, "BitMask<N>: N must be <= 64");
39        // (1u64 << 64) would overflow; handle that case with u64::MAX.
40        let bits = if N >= 64 {
41            u64::MAX
42        } else {
43            (1u64 << N).wrapping_sub(1)
44        };
45        Self(bits)
46    };
47
48    /// No lanes active.
49    pub const NONE_ACTIVE: Self = Self(0);
50
51    /// First `k` lanes active, rest inactive. Clamps `k` to `N`.
52    ///
53    /// This is a `const fn` so it can be used in const contexts.
54    ///
55    /// # Examples
56    /// ```
57    /// use hermes_simd_core::mask::BitMask;
58    /// assert_eq!(BitMask::<8>::leading_k(5).0, 0b00011111);
59    /// assert_eq!(BitMask::<8>::leading_k(0).0, 0);
60    /// assert_eq!(BitMask::<8>::leading_k(8).0, 0xFF);
61    /// ```
62    #[inline(always)]
63    pub const fn leading_k(k: usize) -> Self {
64        let k = if k > N { N } else { k };
65        let bits = if k >= 64 {
66            u64::MAX
67        } else {
68            (1u64 << k).wrapping_sub(1)
69        };
70        Self(bits)
71    }
72
73    /// Build a mask from a `bool` slice of length `N`.
74    ///
75    /// Bit `i` is set if `bits[i]` is `true`.
76    ///
77    /// # Panics
78    /// Panics in debug mode if `bits.len() != N`.
79    #[inline(always)]
80    pub fn from_bools(bits: &[bool]) -> Self {
81        debug_assert_eq!(
82            bits.len(),
83            N,
84            "BitMask::from_bools: slice length must equal N"
85        );
86        let mut m = 0u64;
87        // Single pass, no branching beyond the loop iterator.
88        for (i, &b) in bits.iter().enumerate().take(N) {
89            m |= (b as u64) << i;
90        }
91        Self(m)
92    }
93
94    /// Number of active (set) lanes.
95    #[inline(always)]
96    pub fn popcount(self) -> u32 {
97        self.0.count_ones()
98    }
99
100    /// Returns `true` if all `N` lanes are active.
101    #[inline(always)]
102    pub fn is_all_active(self) -> bool {
103        self.0 == Self::ALL_ACTIVE.0
104    }
105
106    /// Returns `true` if no lanes are active.
107    #[inline(always)]
108    pub fn is_none_active(self) -> bool {
109        self.0 == 0
110    }
111
112    /// Returns `true` if lane `i` is active.
113    ///
114    /// # Panics
115    /// Panics in debug mode if `i >= N`.
116    #[inline(always)]
117    pub fn is_lane_active(self, i: usize) -> bool {
118        debug_assert!(i < N, "BitMask::is_lane_active: lane index out of range");
119        (self.0 >> i) & 1 == 1
120    }
121
122    /// Bitwise AND of two masks.
123    #[inline(always)]
124    pub fn and(self, other: Self) -> Self {
125        Self(self.0 & other.0)
126    }
127
128    /// Bitwise OR of two masks.
129    #[inline(always)]
130    pub fn or(self, other: Self) -> Self {
131        Self(self.0 | other.0)
132    }
133
134    /// Expand this mask to a `[bool; N]` array.
135    ///
136    /// Useful for scalar fallbacks and debugging. Not performance-critical.
137    pub fn to_bools(self) -> [bool; N]
138    where
139        [bool; N]: Sized,
140    {
141        core::array::from_fn(|i| (self.0 >> i) & 1 == 1)
142    }
143}
144
145impl<const N: usize> BitMask<N> {
146    /// Convert this `BitMask<N>` to the native hardware mask type for `Arch`.
147    ///
148    /// Delegates to [`SimdKernel::mask_from_bitmask`](crate::kernel::SimdKernel::mask_from_bitmask)
149    /// using the inner `u64` value.
150    /// Zero runtime cost: the compiler inlines this into a single instruction on
151    /// AVX-512 (`KMOV`), a vector comparison + blend mask on AVX2, or a bool-array
152    /// copy on scalar backends.
153    ///
154    /// # Safety
155    /// Processor must support the target feature of `Arch`.
156    ///
157    /// # Example
158    /// ```rust
159    /// use hermes_simd_core::mask::BitMask;
160    /// use hermes_simd_core::kernel::SimdKernel;
161    /// use hermes_simd_intrinsics::Scalar;
162    ///
163    /// let bm = BitMask::<4>::leading_k(3);
164    /// // SAFETY: `Scalar` has no target-feature precondition.
165    /// let native: <Scalar as SimdKernel<f32>>::Mask =
166    ///     unsafe { bm.to_native_mask::<f32, Scalar>() };
167    ///
168    /// assert_eq!(native, [true, true, true, false]);
169    /// ```
170    #[inline(always)]
171    pub unsafe fn to_native_mask<T, Arch>(self) -> Arch::Mask
172    where
173        T: crate::scalar::Scalar,
174        Arch: crate::kernel::SimdKernel<T>,
175    {
176        Arch::mask_from_bitmask(self.0)
177    }
178}
179
180// ---------------------------------------------------------------------------
181// BitMaskIter — active lane index iterator using bit manipulation
182// ---------------------------------------------------------------------------
183
184/// Iterator over active lane indices of a [`BitMask<N>`].
185///
186/// Yields the index (0..N) of each set bit in ascending order.
187///
188/// # Algorithm
189///
190/// Uses `u64::trailing_zeros` to jump directly to the next set bit in O(1) per step,
191/// then clears that bit with `remaining &= remaining - 1` (Kernighan's bit trick).
192/// Total cost is O(popcount), not O(N) — critical for sparse masks.
193///
194/// # Size
195///
196/// `size_of::<BitMaskIter<N>>()` is 8 bytes (one `u64`). The `lane` field is removed;
197/// position is recovered from `trailing_zeros` each time.
198#[derive(Clone, Copy, Debug)]
199pub struct BitMaskIter<const N: usize> {
200    /// Remaining active bits. Bits are cleared as they are consumed.
201    remaining: u64,
202}
203
204impl<const N: usize> Iterator for BitMaskIter<N> {
205    type Item = usize;
206
207    /// Returns the index of the next active lane, or `None` if no lanes remain.
208    ///
209    /// Clears the lowest set bit after returning its index.
210    #[inline(always)]
211    fn next(&mut self) -> Option<usize> {
212        if self.remaining == 0 {
213            return None;
214        }
215        // trailing_zeros gives the position of the lowest set bit.
216        let idx = self.remaining.trailing_zeros() as usize;
217        // Guard: respect the N-lane bound (high bits of a partial mask could be set
218        // only if BitMask invariant is violated, but we check defensively).
219        if idx >= N {
220            return None;
221        }
222        // Kernighan's trick: clear the lowest set bit in one instruction.
223        self.remaining &= self.remaining.wrapping_sub(1);
224        Some(idx)
225    }
226
227    /// Returns exact bounds: `(popcount, Some(popcount))`.
228    #[inline(always)]
229    fn size_hint(&self) -> (usize, Option<usize>) {
230        let n = self.remaining.count_ones() as usize;
231        (n, Some(n))
232    }
233}
234
235impl<const N: usize> ExactSizeIterator for BitMaskIter<N> {}
236
237impl<const N: usize> DoubleEndedIterator for BitMaskIter<N> {
238    /// Returns the index of the highest active lane remaining.
239    #[inline(always)]
240    fn next_back(&mut self) -> Option<usize> {
241        if self.remaining == 0 {
242            return None;
243        }
244        // leading_zeros + bit position = highest set bit.
245        let idx = 63 - self.remaining.leading_zeros() as usize;
246        if idx >= N {
247            return None;
248        }
249        // Clear the highest set bit.
250        self.remaining &= !(1u64 << idx);
251        Some(idx)
252    }
253}
254
255impl<const N: usize> IntoIterator for BitMask<N> {
256    type Item = usize;
257    type IntoIter = BitMaskIter<N>;
258
259    /// Iterate over active lane indices in ascending order.
260    ///
261    /// # Example
262    /// ```rust
263    /// use hermes_simd_core::mask::BitMask;
264    ///
265    /// let mask = BitMask::<8>::from_bools(&[true, false, true, false, true, false, false, false]);
266    /// let indices: Vec<usize> = mask.into_iter().collect();
267    ///
268    /// assert_eq!(indices, vec![0, 2, 4]);
269    /// ```
270    #[inline(always)]
271    fn into_iter(self) -> BitMaskIter<N> {
272        BitMaskIter { remaining: self.0 }
273    }
274}
275
276impl<const N: usize> BitMask<N> {
277    /// Convenience method to iterate active lane indices without consuming.
278    ///
279    /// Equivalent to `(*self).into_iter()` since `BitMask<N>: Copy`.
280    #[inline(always)]
281    pub fn active_lanes(self) -> BitMaskIter<N> {
282        self.into_iter()
283    }
284}
285
286impl<const N: usize> Default for BitMask<N> {
287    #[inline(always)]
288    fn default() -> Self {
289        Self::NONE_ACTIVE
290    }
291}
292
293impl<const N: usize> core::ops::BitAnd for BitMask<N> {
294    type Output = Self;
295    #[inline(always)]
296    fn bitand(self, rhs: Self) -> Self {
297        self.and(rhs)
298    }
299}
300
301impl<const N: usize> core::ops::BitOr for BitMask<N> {
302    type Output = Self;
303    #[inline(always)]
304    fn bitor(self, rhs: Self) -> Self {
305        self.or(rhs)
306    }
307}
308
309impl<const N: usize> core::ops::Not for BitMask<N> {
310    type Output = Self;
311    #[inline(always)]
312    fn not(self) -> Self {
313        Self(!self.0 & Self::ALL_ACTIVE.0)
314    }
315}
316
317#[cfg(test)]
318mod rkyv_tests {
319    use super::*;
320
321    #[test]
322    // rkyv archived access violates Stacked Borrows inside the dependency;
323    // see vec/tests.rs for the rationale and the 0.8.17 re-probe.
324    #[cfg_attr(miri, ignore)]
325    fn test_bitmask_rkyv() {
326        let mask = BitMask::<8>::from_bools(&[true, false, true, true, false, false, true, false]);
327        let bytes = rkyv::to_bytes::<rkyv::rancor::Error>(&mask).unwrap();
328
329        let archived = rkyv::access::<rkyv::Archived<BitMask<8>>, rkyv::rancor::Error>(&bytes)
330            .expect("validated access");
331        let deserialized: BitMask<8> =
332            rkyv::deserialize::<_, rkyv::rancor::Error>(archived).unwrap();
333        assert_eq!(deserialized, mask);
334        assert_eq!(deserialized.0, mask.0);
335    }
336}