Skip to main content

subetha_pointers/
adaptive_rasp_batch.rs

1//! `RaspBatch<T>` - structure-of-arrays (SoA) storage for bounds-
2//! checked pointers, designed for high-throughput SIMD batch
3//! validation.
4//!
5//! Where `RaspPointer<T>` is an array-of-structures (AoS) 16-byte
6//! pointer that fits in one XMM register, `RaspBatch<T>` flips the
7//! layout: instead of storing the four fields (ptr, base, length,
8//! perms) packed in 16 bytes per pointer, it stores N pointers as
9//! four parallel `Vec`s. Each field's values are contiguous in
10//! memory, so SIMD batch validation can load 4 consecutive ptrs
11//! into one YMM register via a single `vmovdqu` - no GPR→SIMD
12//! domain crossings, no per-call ABI prologue overhead, and the
13//! `vpcmpgtq` packed-quadword compare runs at its design speed.
14//!
15//! # Memory layout
16//!
17//! ```text
18//! ptrs:    [u64, u64, u64, u64, ...]   (8 bytes per slot, contiguous)
19//! bases:   [u64, u64, u64, u64, ...]
20//! lengths: [u32, u32, u32, u32, ...]   (4 bytes per slot, contiguous)
21//! perms:   [u32, u32, u32, u32, ...]   (sealed = high bit of u32)
22//! ```
23//!
24//! All four `Vec`s share the same length; index `i` reads
25//! `(ptrs[i], bases[i], lengths[i], perms[i])` as the i-th pointer.
26//!
27//! # SIMD batch validation
28//!
29//! `check_read_all_avx2` validates the entire batch by processing 4
30//! consecutive entries per loop iteration:
31//!
32//! 1. `vmovdqu ymm0, [ptrs+offset]`    - 4 u64 ptrs into one YMM
33//! 2. `vmovdqu ymm1, [bases+offset]`   - 4 u64 bases into one YMM
34//! 3. `vpcmpgtq ymm2, ymm1, ymm0`      - parallel "base > ptr" check
35//! 4. `vmovdqu xmm3, [lengths+offset]` - 4 u32 lengths into one XMM
36//! 5. `vpmovzxdq ymm3, xmm3`           - zero-extend to 4 u64 lanes
37//! 6. `vpaddq ymm4, ymm1, ymm3`        - region_end = base + length
38//! 7. `vpaddq ymm5, ymm0, [size_t]`    - `access_end = ptr + size_of::<T>()`
39//! 8. `vpcmpgtq ymm6, ymm5, ymm4`      - parallel "access_end > region_end"
40//! 9. `vmovdqu xmm7, [perms+offset]`   - 4 u32 perms
41//! 10. permission + sealed checks via SIMD masks
42//!
43//! Total: ~12 SIMD instructions per 4 pointers = 3 instructions per
44//! pointer. The AoS path's ~30 instructions per 4 pointers (12
45//! `vmovq` GPR→SIMD crossings + 6 `vpunpcklqdq` + 3 `vinserti128` +
46//! 2 `vpcmpgtq`) is folded into 12 contiguous-load + arithmetic
47//! instructions with zero domain crossings.
48
49use std::marker::PhantomData;
50
51/// Per-pointer permission flags. Multiple permissions OR together;
52/// the sealed bit lives at position 31 of the u32 perms field.
53#[repr(u32)]
54#[derive(Debug, Clone, Copy, PartialEq, Eq)]
55pub enum RaspPermission {
56    None    = 0,
57    Read    = 1 << 0,
58    Write   = 1 << 1,
59    Execute = 1 << 2,
60}
61
62/// Sealed bit position in the u32 perms field. A sealed RASP returns
63/// `Err(Sealed)` from all `check_*` paths regardless of permission
64/// bits. Sealing is cooperative: a caller who skips the check and
65/// dereferences via the raw pointer bypasses sealing.
66const SEALED_BIT_U32: u32 = 1 << 31;
67
68/// Errors returned from RASP bounds / permission checks.
69#[derive(Debug, Clone, Copy, PartialEq, Eq)]
70pub enum RaspError {
71    OutOfBounds,
72    PermissionDenied,
73    Sealed,
74    AddressOverflow,
75    /// Length did not fit in the layout's 32-bit length field, or
76    /// the batch is already at capacity (`u32::MAX` entries).
77    LayoutTooWide,
78    /// The current CPU does not support the SIMD feature needed by
79    /// a SIMD-only entry point. The scalar entry points are always
80    /// available.
81    FeatureNotSupported,
82}
83
84/// Structure-of-arrays storage for bounds-checked pointers.
85///
86/// `T` is a phantom type; each entry refers to an externally-owned
87/// `[T]` region. The caller is responsible for keeping the regions
88/// alive for the lifetime of the batch. Use `push_from_slice` with
89/// the returned slice as a borrow anchor.
90pub struct RaspBatch<T> {
91    ptrs: Vec<u64>,
92    bases: Vec<u64>,
93    lengths: Vec<u32>,
94    perms: Vec<u32>,
95    _phantom: PhantomData<*const T>,
96}
97
98unsafe impl<T: Send> Send for RaspBatch<T> {}
99unsafe impl<T: Sync> Sync for RaspBatch<T> {}
100
101/// Position-independent reference into a `RaspBatch<T>`.
102#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
103pub struct RaspBatchIndex<T> {
104    idx: u32,
105    _phantom: PhantomData<T>,
106}
107
108impl<T> RaspBatchIndex<T> {
109    pub const fn new(idx: u32) -> Self {
110        Self { idx, _phantom: PhantomData }
111    }
112    pub const fn raw(&self) -> u32 { self.idx }
113}
114
115impl<T> Default for RaspBatch<T> {
116    fn default() -> Self { Self::new() }
117}
118
119impl<T> RaspBatch<T> {
120    pub fn new() -> Self {
121        Self {
122            ptrs: Vec::new(),
123            bases: Vec::new(),
124            lengths: Vec::new(),
125            perms: Vec::new(),
126            _phantom: PhantomData,
127        }
128    }
129
130    pub fn with_capacity(n: usize) -> Self {
131        Self {
132            ptrs: Vec::with_capacity(n),
133            bases: Vec::with_capacity(n),
134            lengths: Vec::with_capacity(n),
135            perms: Vec::with_capacity(n),
136            _phantom: PhantomData,
137        }
138    }
139
140    #[inline]
141    pub fn len(&self) -> usize { self.ptrs.len() }
142    #[inline]
143    pub fn is_empty(&self) -> bool { self.ptrs.is_empty() }
144    pub fn capacity(&self) -> usize { self.ptrs.capacity() }
145
146    /// Push a new pointer from a borrowed slice. The returned slice
147    /// is the lifetime anchor; the batch's pointer is valid only as
148    /// long as the anchor is held.
149    pub fn push_from_slice<'a>(
150        &mut self,
151        slice: &'a [T],
152        perms: u32,
153    ) -> Result<(RaspBatchIndex<T>, &'a [T]), RaspError> {
154        let ptr_usize = slice.as_ptr() as usize;
155        let length_bytes = std::mem::size_of_val(slice);
156        if length_bytes > u32::MAX as usize {
157            return Err(RaspError::LayoutTooWide);
158        }
159        let idx = self.ptrs.len();
160        if idx >= u32::MAX as usize {
161            return Err(RaspError::LayoutTooWide);
162        }
163        self.ptrs.push(ptr_usize as u64);
164        self.bases.push(ptr_usize as u64);
165        self.lengths.push(length_bytes as u32);
166        self.perms.push(perms);
167        Ok((RaspBatchIndex::new(idx as u32), slice))
168    }
169
170    /// Push from raw integer fields (caller manages target lifetime).
171    pub fn push_raw(
172        &mut self,
173        ptr: u64,
174        base: u64,
175        length: u32,
176        perms: u32,
177    ) -> Result<RaspBatchIndex<T>, RaspError> {
178        let idx = self.ptrs.len();
179        if idx >= u32::MAX as usize {
180            return Err(RaspError::LayoutTooWide);
181        }
182        if ptr < base {
183            return Err(RaspError::OutOfBounds);
184        }
185        let access_end = (ptr as usize).checked_add(std::mem::size_of::<T>())
186            .ok_or(RaspError::AddressOverflow)?;
187        let region_end = (base as usize).checked_add(length as usize)
188            .ok_or(RaspError::AddressOverflow)?;
189        if access_end > region_end {
190            return Err(RaspError::OutOfBounds);
191        }
192        self.ptrs.push(ptr);
193        self.bases.push(base);
194        self.lengths.push(length);
195        self.perms.push(perms);
196        Ok(RaspBatchIndex::new(idx as u32))
197    }
198
199    /// Per-element scalar check. Used by per-index call sites and as
200    /// the correctness oracle for the SIMD batch path.
201    pub fn check_read_scalar(
202        &self,
203        idx: RaspBatchIndex<T>,
204    ) -> Result<(), RaspError> {
205        let i = idx.idx as usize;
206        if i >= self.ptrs.len() {
207            return Err(RaspError::OutOfBounds);
208        }
209        let p = self.perms[i];
210        if p & SEALED_BIT_U32 != 0 {
211            return Err(RaspError::Sealed);
212        }
213        if p & (RaspPermission::Read as u32) == 0 {
214            return Err(RaspError::PermissionDenied);
215        }
216        let ptr_u = self.ptrs[i] as usize;
217        let base_u = self.bases[i] as usize;
218        if ptr_u < base_u {
219            return Err(RaspError::OutOfBounds);
220        }
221        let access_end = ptr_u.checked_add(std::mem::size_of::<T>())
222            .ok_or(RaspError::AddressOverflow)?;
223        let region_end = base_u.checked_add(self.lengths[i] as usize)
224            .ok_or(RaspError::AddressOverflow)?;
225        if access_end > region_end {
226            return Err(RaspError::OutOfBounds);
227        }
228        Ok(())
229    }
230
231    /// Raw pointer at the given index. Returns `None` for an
232    /// out-of-range index. The pointer is NOT validated; pair with
233    /// `check_read_scalar` (or use `read_at` which combines both).
234    pub fn raw_ptr(&self, idx: RaspBatchIndex<T>) -> Option<*const T> {
235        let i = idx.idx as usize;
236        if i >= self.ptrs.len() { return None; }
237        Some(self.ptrs[i] as usize as *const T)
238    }
239
240    /// Bounds + permission check followed by a dereference. The
241    /// caller's responsibility for the original target lifetime
242    /// still applies (see `push_from_slice`'s borrow anchor).
243    ///
244    /// # Safety
245    ///
246    /// The target of the pointer at `idx` must still be valid
247    /// (alive, properly aligned for `T`, and accessible via the
248    /// permissions encoded). The check enforces the permissions
249    /// recorded at push time; it does NOT prove the target's
250    /// allocation has not been freed.
251    pub unsafe fn read_at(&self, idx: RaspBatchIndex<T>) -> Result<T, RaspError>
252    where T: Copy,
253    {
254        self.check_read_scalar(idx)?;
255        let i = idx.idx as usize;
256        let p = self.ptrs[i] as usize as *const T;
257        // SAFETY: check_read_scalar succeeded, so bounds + perms are
258        // satisfied. The caller is responsible for the target's
259        // continued validity per push_from_slice's contract.
260        Ok(unsafe { *p })
261    }
262
263    /// Batch scalar check across the whole array. Reference path.
264    pub fn check_read_all_scalar(&self) -> Vec<Result<(), RaspError>> {
265        let n = self.len();
266        let mut out = Vec::with_capacity(n);
267        for i in 0..n {
268            out.push(self.check_read_scalar(RaspBatchIndex::new(i as u32)));
269        }
270        out
271    }
272
273    /// Count of valid entries (Ok results) via scalar path. Avoids
274    /// the `Vec<Result>` allocation when only the count is needed.
275    pub fn count_valid_scalar(&self) -> u32 {
276        let n = self.len();
277        let mut count = 0u32;
278        for i in 0..n {
279            if self.check_read_scalar(RaspBatchIndex::new(i as u32)).is_ok() {
280                count += 1;
281            }
282        }
283        count
284    }
285
286    /// AVX2-accelerated count of valid entries. Processes 4 elements
287    /// per loop iteration using contiguous SIMD loads from each
288    /// parallel `Vec`.
289    ///
290    /// # Safety
291    ///
292    /// Caller must guarantee AVX2 is supported by the runtime CPU.
293    #[cfg(any(target_arch = "x86_64", target_arch = "x86"))]
294    #[target_feature(enable = "avx2")]
295    #[inline]
296    pub unsafe fn count_valid_avx2(&self) -> u32 {
297        #[cfg(target_arch = "x86_64")]
298        use std::arch::x86_64::*;
299        #[cfg(target_arch = "x86")]
300        use std::arch::x86::*;
301
302        let n = self.len();
303        if n == 0 {
304            return 0;
305        }
306        let chunks = n / 4;
307        let size_t = std::mem::size_of::<T>() as u64;
308        let size_t_vec = _mm256_set1_epi64x(size_t as i64);
309        let read_bit_vec = _mm_set1_epi32(RaspPermission::Read as i32);
310        let zero128 = _mm_setzero_si128();
311
312        let mut count_in_simd = 0u32;
313        for c in 0..chunks {
314            let off = c * 4;
315            // Contiguous loads - no GPR→SIMD crossings.
316            // SAFETY: c < chunks = n/4, so off+4 <= n; in-bounds.
317            let v_ptrs = unsafe {
318                _mm256_loadu_si256(self.ptrs.as_ptr().add(off) as *const __m256i)
319            };
320            let v_bases = unsafe {
321                _mm256_loadu_si256(self.bases.as_ptr().add(off) as *const __m256i)
322            };
323            let lengths_xmm = unsafe {
324                _mm_loadu_si128(self.lengths.as_ptr().add(off) as *const __m128i)
325            };
326            let perms_xmm = unsafe {
327                _mm_loadu_si128(self.perms.as_ptr().add(off) as *const __m128i)
328            };
329
330            // Lower bound: base > ptr → fail
331            let cmp_lower = _mm256_cmpgt_epi64(v_bases, v_ptrs);
332
333            // Upper bound: access_end > region_end → fail
334            let v_lengths_u64 = _mm256_cvtepu32_epi64(lengths_xmm);
335            let v_region_end = _mm256_add_epi64(v_bases, v_lengths_u64);
336            let v_access_end = _mm256_add_epi64(v_ptrs, size_t_vec);
337            let cmp_upper = _mm256_cmpgt_epi64(v_access_end, v_region_end);
338
339            // Sealed: high bit of u32 perms (sign bit). _mm_movemask_ps
340            // emits one bit per 4-byte lane from the sign bit. So a
341            // 4-bit mask, one bit per lane, bit set = sealed.
342            let sealed_mask4 =
343                _mm_movemask_ps(_mm_castsi128_ps(perms_xmm)) as u32;
344
345            // No-read: AND with Read bit, compare to zero. Set bit per
346            // lane means that lane LACKS the read permission.
347            let read_anded = _mm_and_si128(perms_xmm, read_bit_vec);
348            let read_eq_zero = _mm_cmpeq_epi32(read_anded, zero128);
349            let no_read_mask4 =
350                _mm_movemask_ps(_mm_castsi128_ps(read_eq_zero)) as u32;
351
352            // VPCMPGTQ produces all-ones in matching 64-bit lanes,
353            // including bit 63. _mm256_movemask_pd reads bit 63 of
354            // each 64-bit lane and emits a 4-bit mask directly -
355            // one VMOVMSKPD instead of VPMOVMSKB + 4 conditional ORs.
356            let oob_lower_4 =
357                _mm256_movemask_pd(_mm256_castsi256_pd(cmp_lower)) as u32;
358            let oob_upper_4 =
359                _mm256_movemask_pd(_mm256_castsi256_pd(cmp_upper)) as u32;
360
361            // A lane is invalid if ANY of the 4 failure conditions
362            // are set.
363            let any_fail = sealed_mask4 | no_read_mask4 | oob_lower_4 | oob_upper_4;
364            // Valid lanes: bits 0..4 NOT set in any_fail.
365            let valid_in_chunk = 4 - (any_fail & 0xF).count_ones();
366            count_in_simd += valid_in_chunk;
367        }
368
369        // Remainder (n % 4) via scalar.
370        let mut count_scalar = 0u32;
371        for i in (chunks * 4)..n {
372            if self.check_read_scalar(RaspBatchIndex::new(i as u32)).is_ok() {
373                count_scalar += 1;
374            }
375        }
376        count_in_simd + count_scalar
377    }
378
379    /// AVX-512F count of valid entries. Processes 8 elements per
380    /// iteration: ZMM (8x u64) loads for ptrs/bases, YMM (8x u32)
381    /// loads for lengths/perms, mask-producing
382    /// `_mm512_cmpgt_epi64_mask` for both bounds checks. Doubles the
383    /// per-iteration throughput of the AVX2 path.
384    ///
385    /// # Safety
386    /// Caller must guarantee AVX-512F is supported by the runtime CPU.
387    #[cfg(target_arch = "x86_64")]
388    #[target_feature(enable = "avx512f")]
389    #[inline]
390    pub unsafe fn count_valid_avx512(&self) -> u32 {
391        use std::arch::x86_64::*;
392
393        let n = self.len();
394        if n == 0 {
395            return 0;
396        }
397        let chunks = n / 8;
398        let size_t = std::mem::size_of::<T>() as u64;
399        let size_t_vec = _mm512_set1_epi64(size_t as i64);
400        let read_bit_vec = _mm256_set1_epi32(RaspPermission::Read as i32);
401        let zero256 = _mm256_setzero_si256();
402
403        let mut count_in_simd = 0u32;
404        for c in 0..chunks {
405            let off = c * 8;
406            // SAFETY: c < chunks = n/8, so off+8 <= n; in-bounds.
407            let v_ptrs = unsafe {
408                _mm512_loadu_si512(self.ptrs.as_ptr().add(off) as *const __m512i)
409            };
410            let v_bases = unsafe {
411                _mm512_loadu_si512(self.bases.as_ptr().add(off) as *const __m512i)
412            };
413            let lengths_ymm = unsafe {
414                _mm256_loadu_si256(self.lengths.as_ptr().add(off) as *const __m256i)
415            };
416            let perms_ymm = unsafe {
417                _mm256_loadu_si256(self.perms.as_ptr().add(off) as *const __m256i)
418            };
419
420            // Lower bound: base > ptr fails. Returns __mmask8 directly,
421            // one bit per lane.
422            let oob_lower_mask: u8 = _mm512_cmpgt_epi64_mask(v_bases, v_ptrs);
423
424            // Upper bound: access_end > region_end fails.
425            let v_lengths_u64 = _mm512_cvtepu32_epi64(lengths_ymm);
426            let v_region_end = _mm512_add_epi64(v_bases, v_lengths_u64);
427            let v_access_end = _mm512_add_epi64(v_ptrs, size_t_vec);
428            let oob_upper_mask: u8 =
429                _mm512_cmpgt_epi64_mask(v_access_end, v_region_end);
430
431            // Sealed: high bit of each u32 in perms. movemask_ps reads
432            // bit 31 of each 32-bit lane and emits an 8-bit mask for
433            // the full 256-bit register.
434            let sealed_mask8 =
435                _mm256_movemask_ps(_mm256_castsi256_ps(perms_ymm)) as u32 & 0xFF;
436
437            // No-read: AND with Read bit, cmpeq vs zero, movemask.
438            let read_anded = _mm256_and_si256(perms_ymm, read_bit_vec);
439            let read_eq_zero = _mm256_cmpeq_epi32(read_anded, zero256);
440            let no_read_mask8 =
441                _mm256_movemask_ps(_mm256_castsi256_ps(read_eq_zero)) as u32 & 0xFF;
442
443            let any_fail = (oob_lower_mask as u32)
444                | (oob_upper_mask as u32)
445                | sealed_mask8
446                | no_read_mask8;
447            let valid_in_chunk = 8 - (any_fail & 0xFF).count_ones();
448            count_in_simd += valid_in_chunk;
449        }
450
451        // Remainder (n % 8) via scalar.
452        let mut count_scalar = 0u32;
453        for i in (chunks * 8)..n {
454            if self.check_read_scalar(RaspBatchIndex::new(i as u32)).is_ok() {
455                count_scalar += 1;
456            }
457        }
458        count_in_simd + count_scalar
459    }
460
461    /// Cross-platform safe count of valid entries. Dispatches to
462    /// `count_valid_avx512` when AVX-512F is present, then
463    /// `count_valid_avx2`, then `count_valid_scalar`.
464    pub fn count_valid(&self) -> u32 {
465        #[cfg(target_arch = "x86_64")]
466        {
467            if std::is_x86_feature_detected!("avx512f") {
468                // SAFETY: feature-detected.
469                return unsafe { self.count_valid_avx512() };
470            }
471        }
472        #[cfg(any(target_arch = "x86_64", target_arch = "x86"))]
473        {
474            if std::is_x86_feature_detected!("avx2") {
475                // SAFETY: feature-detected.
476                return unsafe { self.count_valid_avx2() };
477            }
478        }
479        self.count_valid_scalar()
480    }
481
482    /// AVX2-accelerated full validation that writes per-index results.
483    /// Slower than `count_valid_avx2` because it materialises a
484    /// `Vec<Result>` instead of just counting; use this when the
485    /// caller needs per-index error attribution.
486    ///
487    /// # Safety
488    ///
489    /// Caller must guarantee AVX2 is supported by the runtime CPU.
490    #[cfg(any(target_arch = "x86_64", target_arch = "x86"))]
491    #[target_feature(enable = "avx2")]
492    #[inline]
493    pub unsafe fn check_read_all_avx2(&self) -> Vec<Result<(), RaspError>> {
494        #[cfg(target_arch = "x86_64")]
495        use std::arch::x86_64::*;
496        #[cfg(target_arch = "x86")]
497        use std::arch::x86::*;
498
499        let n = self.len();
500        let mut results: Vec<Result<(), RaspError>> = vec![Ok(()); n];
501        if n == 0 {
502            return results;
503        }
504        let chunks = n / 4;
505        let size_t = std::mem::size_of::<T>() as u64;
506        let size_t_vec = _mm256_set1_epi64x(size_t as i64);
507        let read_bit_vec = _mm_set1_epi32(RaspPermission::Read as i32);
508        let zero128 = _mm_setzero_si128();
509
510        for c in 0..chunks {
511            let off = c * 4;
512            // SAFETY: c < chunks = n/4, so off+4 <= n.
513            let v_ptrs = unsafe {
514                _mm256_loadu_si256(self.ptrs.as_ptr().add(off) as *const __m256i)
515            };
516            let v_bases = unsafe {
517                _mm256_loadu_si256(self.bases.as_ptr().add(off) as *const __m256i)
518            };
519            let lengths_xmm = unsafe {
520                _mm_loadu_si128(self.lengths.as_ptr().add(off) as *const __m128i)
521            };
522            let perms_xmm = unsafe {
523                _mm_loadu_si128(self.perms.as_ptr().add(off) as *const __m128i)
524            };
525
526            let cmp_lower = _mm256_cmpgt_epi64(v_bases, v_ptrs);
527            let v_lengths_u64 = _mm256_cvtepu32_epi64(lengths_xmm);
528            let v_region_end = _mm256_add_epi64(v_bases, v_lengths_u64);
529            let v_access_end = _mm256_add_epi64(v_ptrs, size_t_vec);
530            let cmp_upper = _mm256_cmpgt_epi64(v_access_end, v_region_end);
531
532            let sealed_mask4 =
533                _mm_movemask_ps(_mm_castsi128_ps(perms_xmm)) as u32;
534            let read_anded = _mm_and_si128(perms_xmm, read_bit_vec);
535            let read_eq_zero = _mm_cmpeq_epi32(read_anded, zero128);
536            let no_read_mask4 =
537                _mm_movemask_ps(_mm_castsi128_ps(read_eq_zero)) as u32;
538
539            // VPCMPGTQ → bit 63 set in each matching 64-bit lane →
540            // _mm256_movemask_pd emits a 4-bit lane mask directly.
541            let oob_lower_4 =
542                _mm256_movemask_pd(_mm256_castsi256_pd(cmp_lower)) as u32;
543            let oob_upper_4 =
544                _mm256_movemask_pd(_mm256_castsi256_pd(cmp_upper)) as u32;
545
546            for i in 0..4 {
547                let bit = 1u32 << i;
548                if sealed_mask4 & bit != 0 {
549                    results[off + i] = Err(RaspError::Sealed);
550                } else if no_read_mask4 & bit != 0 {
551                    results[off + i] = Err(RaspError::PermissionDenied);
552                } else if (oob_lower_4 | oob_upper_4) & bit != 0 {
553                    results[off + i] = Err(RaspError::OutOfBounds);
554                }
555            }
556        }
557        // SIMD-tail cleanup: indexed form reads more naturally here than
558        // the iterator equivalent (the loop body would still need both
559        // `i` and the slot).
560        #[allow(clippy::needless_range_loop)]
561        for i in (chunks * 4)..n {
562            results[i] = self.check_read_scalar(RaspBatchIndex::new(i as u32));
563        }
564        results
565    }
566
567    /// AVX-512F full validation that writes per-index results.
568    /// Processes 8 elements per iteration via ZMM loads and
569    /// mask-producing `_mm512_cmpgt_epi64_mask`. Bit-exact equivalent
570    /// of `check_read_all_avx2` and `check_read_all_scalar`.
571    ///
572    /// # Safety
573    /// Caller must guarantee AVX-512F is supported by the runtime CPU.
574    #[cfg(target_arch = "x86_64")]
575    #[target_feature(enable = "avx512f")]
576    #[inline]
577    pub unsafe fn check_read_all_avx512(&self) -> Vec<Result<(), RaspError>> {
578        use std::arch::x86_64::*;
579
580        let n = self.len();
581        let mut results: Vec<Result<(), RaspError>> = vec![Ok(()); n];
582        if n == 0 {
583            return results;
584        }
585        let chunks = n / 8;
586        let size_t = std::mem::size_of::<T>() as u64;
587        let size_t_vec = _mm512_set1_epi64(size_t as i64);
588        let read_bit_vec = _mm256_set1_epi32(RaspPermission::Read as i32);
589        let zero256 = _mm256_setzero_si256();
590
591        for c in 0..chunks {
592            let off = c * 8;
593            // SAFETY: c < chunks = n/8, so off+8 <= n.
594            let v_ptrs = unsafe {
595                _mm512_loadu_si512(self.ptrs.as_ptr().add(off) as *const __m512i)
596            };
597            let v_bases = unsafe {
598                _mm512_loadu_si512(self.bases.as_ptr().add(off) as *const __m512i)
599            };
600            let lengths_ymm = unsafe {
601                _mm256_loadu_si256(self.lengths.as_ptr().add(off) as *const __m256i)
602            };
603            let perms_ymm = unsafe {
604                _mm256_loadu_si256(self.perms.as_ptr().add(off) as *const __m256i)
605            };
606
607            let oob_lower_mask: u8 = _mm512_cmpgt_epi64_mask(v_bases, v_ptrs);
608            let v_lengths_u64 = _mm512_cvtepu32_epi64(lengths_ymm);
609            let v_region_end = _mm512_add_epi64(v_bases, v_lengths_u64);
610            let v_access_end = _mm512_add_epi64(v_ptrs, size_t_vec);
611            let oob_upper_mask: u8 =
612                _mm512_cmpgt_epi64_mask(v_access_end, v_region_end);
613
614            let sealed_mask8 =
615                _mm256_movemask_ps(_mm256_castsi256_ps(perms_ymm)) as u32 & 0xFF;
616            let read_anded = _mm256_and_si256(perms_ymm, read_bit_vec);
617            let read_eq_zero = _mm256_cmpeq_epi32(read_anded, zero256);
618            let no_read_mask8 =
619                _mm256_movemask_ps(_mm256_castsi256_ps(read_eq_zero)) as u32 & 0xFF;
620
621            for i in 0..8 {
622                let bit = 1u32 << i;
623                if sealed_mask8 & bit != 0 {
624                    results[off + i] = Err(RaspError::Sealed);
625                } else if no_read_mask8 & bit != 0 {
626                    results[off + i] = Err(RaspError::PermissionDenied);
627                } else if ((oob_lower_mask as u32) | (oob_upper_mask as u32)) & bit
628                    != 0
629                {
630                    results[off + i] = Err(RaspError::OutOfBounds);
631                }
632            }
633        }
634        // SIMD-tail cleanup via scalar.
635        #[allow(clippy::needless_range_loop)]
636        for i in (chunks * 8)..n {
637            results[i] = self.check_read_scalar(RaspBatchIndex::new(i as u32));
638        }
639        results
640    }
641
642    /// Cross-platform safe full validation.
643    pub fn check_read_all(&self) -> Vec<Result<(), RaspError>> {
644        #[cfg(target_arch = "x86_64")]
645        {
646            if std::is_x86_feature_detected!("avx512f") {
647                // SAFETY: feature-detected.
648                return unsafe { self.check_read_all_avx512() };
649            }
650        }
651        #[cfg(any(target_arch = "x86_64", target_arch = "x86"))]
652        {
653            if std::is_x86_feature_detected!("avx2") {
654                // SAFETY: feature-detected.
655                return unsafe { self.check_read_all_avx2() };
656            }
657        }
658        self.check_read_all_scalar()
659    }
660}
661
662#[cfg(test)]
663mod tests {
664    use super::*;
665
666    #[test]
667    fn empty_batch_returns_zero_count() {
668        let b: RaspBatch<u8> = RaspBatch::new();
669        assert!(b.is_empty());
670        assert_eq!(b.count_valid(), 0);
671        assert_eq!(b.check_read_all().len(), 0);
672    }
673
674    #[test]
675    fn push_from_slice_and_read_single() {
676        let storage: Vec<u64> = vec![42; 8];
677        let mut b: RaspBatch<u64> = RaspBatch::new();
678        let (idx, _anchor) = b.push_from_slice(
679            &storage, RaspPermission::Read as u32,
680        ).unwrap();
681        assert_eq!(b.len(), 1);
682        assert_eq!(b.check_read_scalar(idx), Ok(()));
683    }
684
685    #[test]
686    fn count_valid_scalar_matches_check_all() {
687        let storages: Vec<Vec<u64>> = (0..8).map(|_| vec![0; 8]).collect();
688        let mut b: RaspBatch<u64> = RaspBatch::new();
689        for (i, s) in storages.iter().enumerate() {
690            let perms = if i % 2 == 0 {
691                RaspPermission::Read as u32
692            } else {
693                RaspPermission::None as u32
694            };
695            b.push_from_slice(s, perms).unwrap();
696        }
697        let count = b.count_valid_scalar();
698        let all = b.check_read_all_scalar();
699        let oks = all.iter().filter(|r| r.is_ok()).count() as u32;
700        assert_eq!(count, oks);
701        assert_eq!(count, 4);
702    }
703
704    #[test]
705    fn count_valid_avx2_matches_scalar_oracle() {
706        if !std::is_x86_feature_detected!("avx2") {
707            return;
708        }
709        let storages: Vec<Vec<u64>> = (0..16).map(|_| vec![0; 8]).collect();
710        let perms_r = RaspPermission::Read as u32;
711        let sealed = perms_r | SEALED_BIT_U32;
712        let perms_none = RaspPermission::None as u32;
713        let perms_rw =
714            (RaspPermission::Read as u32) | (RaspPermission::Write as u32);
715        let choices = [perms_r, sealed, perms_none, perms_rw];
716
717        let mut b: RaspBatch<u64> = RaspBatch::new();
718        for (i, s) in storages.iter().enumerate() {
719            b.push_from_slice(s, choices[i % 4]).unwrap();
720        }
721        let scalar = b.count_valid_scalar();
722        // SAFETY: feature-detected above.
723        let avx2 = unsafe { b.count_valid_avx2() };
724        let dispatched = b.count_valid();
725        assert_eq!(scalar, avx2, "AVX2 count mismatch");
726        assert_eq!(scalar, dispatched, "Dispatched count mismatch");
727        // Per the choices: lanes 0 and 3 are valid (Read; Read+Write),
728        // lanes 1 and 2 are invalid (sealed; none).
729        // 16 entries / 4 choices = 4 of each.
730        assert_eq!(scalar, 8);
731    }
732
733    #[test]
734    fn check_read_all_avx2_per_lane_results_match_scalar() {
735        if !std::is_x86_feature_detected!("avx2") {
736            return;
737        }
738        let storages: Vec<Vec<u64>> = (0..12).map(|_| vec![0; 8]).collect();
739        let perms_r = RaspPermission::Read as u32;
740        let sealed = perms_r | SEALED_BIT_U32;
741        let perms_none = RaspPermission::None as u32;
742        let choices = [perms_r, sealed, perms_none];
743
744        let mut b: RaspBatch<u64> = RaspBatch::new();
745        for (i, s) in storages.iter().enumerate() {
746            b.push_from_slice(s, choices[i % 3]).unwrap();
747        }
748        let scalar = b.check_read_all_scalar();
749        let avx2 = unsafe { b.check_read_all_avx2() };
750        assert_eq!(scalar.len(), avx2.len());
751        for (i, (s, v)) in scalar.iter().zip(avx2.iter()).enumerate() {
752            assert_eq!(s, v, "Mismatch at lane {i}");
753        }
754    }
755
756    #[test]
757    fn remainder_lanes_handled_by_scalar_fallback() {
758        if !std::is_x86_feature_detected!("avx2") {
759            return;
760        }
761        // 13 entries: 3 full chunks of 4, plus 1 remainder lane.
762        // The AVX2 path must hand the remainder to scalar.
763        let storages: Vec<Vec<u64>> = (0..13).map(|_| vec![0; 8]).collect();
764        let mut b: RaspBatch<u64> = RaspBatch::new();
765        for s in &storages {
766            b.push_from_slice(s, RaspPermission::Read as u32).unwrap();
767        }
768        let scalar = b.count_valid_scalar();
769        let avx2 = unsafe { b.count_valid_avx2() };
770        assert_eq!(scalar, avx2);
771        assert_eq!(scalar, 13);
772    }
773
774    #[test]
775    fn push_raw_validates_bounds_at_construction() {
776        let mut b: RaspBatch<u8> = RaspBatch::new();
777        // ptr below base.
778        let r = b.push_raw(0x0FFF, 0x1000, 16, RaspPermission::Read as u32);
779        assert_eq!(r.err(), Some(RaspError::OutOfBounds));
780        // access_end past region_end.
781        let r = b.push_raw(0x1010, 0x1000, 8, RaspPermission::Read as u32);
782        assert_eq!(r.err(), Some(RaspError::OutOfBounds));
783        // OK case.
784        let r = b.push_raw(0x1000, 0x1000, 16, RaspPermission::Read as u32);
785        assert!(r.is_ok());
786    }
787
788    #[test]
789    fn sealed_bit_blocks_read_in_simd_path() {
790        if !std::is_x86_feature_detected!("avx2") {
791            return;
792        }
793        let storages: Vec<Vec<u64>> = (0..4).map(|_| vec![0; 8]).collect();
794        let mut b: RaspBatch<u64> = RaspBatch::new();
795        let perms_r = RaspPermission::Read as u32;
796        let sealed = perms_r | SEALED_BIT_U32;
797        for (i, s) in storages.iter().enumerate() {
798            let p = if i == 2 { sealed } else { perms_r };
799            b.push_from_slice(s, p).unwrap();
800        }
801        // SAFETY: feature-detected.
802        let results = unsafe { b.check_read_all_avx2() };
803        assert_eq!(results[0], Ok(()));
804        assert_eq!(results[1], Ok(()));
805        assert_eq!(results[2], Err(RaspError::Sealed));
806        assert_eq!(results[3], Ok(()));
807    }
808
809    #[test]
810    fn no_read_permission_caught_in_simd_path() {
811        if !std::is_x86_feature_detected!("avx2") {
812            return;
813        }
814        let storages: Vec<Vec<u64>> = (0..4).map(|_| vec![0; 8]).collect();
815        let mut b: RaspBatch<u64> = RaspBatch::new();
816        let perms_r = RaspPermission::Read as u32;
817        let perms_w_only = RaspPermission::Write as u32;
818        for (i, s) in storages.iter().enumerate() {
819            let p = if i == 1 { perms_w_only } else { perms_r };
820            b.push_from_slice(s, p).unwrap();
821        }
822        let results = unsafe { b.check_read_all_avx2() };
823        assert_eq!(results[0], Ok(()));
824        assert_eq!(results[1], Err(RaspError::PermissionDenied));
825        assert_eq!(results[2], Ok(()));
826        assert_eq!(results[3], Ok(()));
827    }
828
829    #[test]
830    fn large_batch_simd_vs_scalar_parity() {
831        if !std::is_x86_feature_detected!("avx2") {
832            return;
833        }
834        // 1024 entries, varied perms.
835        let storages: Vec<Vec<u64>> = (0..1024).map(|_| vec![0; 8]).collect();
836        let mut b: RaspBatch<u64> = RaspBatch::new();
837        let perms_r = RaspPermission::Read as u32;
838        let sealed = perms_r | SEALED_BIT_U32;
839        let perms_none = RaspPermission::None as u32;
840        let choices = [perms_r, sealed, perms_none, perms_r];
841        for (i, s) in storages.iter().enumerate() {
842            b.push_from_slice(s, choices[i % 4]).unwrap();
843        }
844        let scalar = b.count_valid_scalar();
845        let avx2 = unsafe { b.count_valid_avx2() };
846        assert_eq!(scalar, avx2);
847        // 2 of 4 choices are valid (perms_r at indices 0 and 3),
848        // so 512 entries valid out of 1024.
849        assert_eq!(scalar, 512);
850    }
851}