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#[archive_attr(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 0.7 archived access violates Stacked Borrows inside the dependency;
323 // see vec/tests.rs for the same exclusion rationale.
324 #[cfg_attr(miri, ignore)]
325 fn test_bitmask_rkyv() {
326 use rkyv::Deserialize;
327
328 let mask = BitMask::<8>::from_bools(&[true, false, true, true, false, false, true, false]);
329 let bytes = rkyv::to_bytes::<_, 256>(&mask).unwrap();
330
331 let archived = unsafe { rkyv::archived_root::<BitMask<8>>(&bytes[..]) };
332 let deserialized: BitMask<8> = archived.deserialize(&mut rkyv::Infallible).unwrap();
333 assert_eq!(deserialized, mask);
334 assert_eq!(deserialized.0, mask.0);
335 }
336}