Skip to main content

vortex_compute/lane_kernels/
map_into.rs

1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: Copyright the Vortex contributors
3
4//! Out-of-place lane kernels: read from an [`IndexedSource`] and write into a
5//! caller-provided `&mut [MaybeUninit<R>]`.
6
7use std::mem::MaybeUninit;
8use std::ops::BitOrAssign;
9
10use vortex_buffer::BitBuffer;
11
12use crate::lane_kernels::CHUNK_LEN;
13use crate::lane_kernels::source::IndexedSource;
14
15/// Extension trait providing out-of-place lane-kernel methods on any [`IndexedSource`].
16///
17/// All methods have default implementations and are inherited via the blanket
18/// `impl<S: IndexedSource> IndexedSourceExt for S` below. Bring the trait into
19/// scope (`use vortex_compute::lane_kernels::IndexedSourceExt;`) to call
20/// them with method syntax: `values.try_map_masked_into(&mask, &mut out, f)`.
21///
22/// Callbacks implement [`Fn`] because each lane must be independent. A callback that mutates
23/// captured state introduces a loop-carried dependency that can prevent vectorization.
24pub trait IndexedSourceExt: IndexedSource + Sized {
25    /// Fallible map with mask-aware error attribution. `f` returns `Option<R>`;
26    /// `None` indicates a per-lane failure (e.g. range overflow on a narrowing cast).
27    ///
28    /// **Null-lane failures are filtered automatically.** The closure is called on
29    /// every lane regardless of validity; if a null lane's stored value causes `f(v)`
30    /// to return `None`, the kernel does *not* propagate that as `Err`. The per-lane
31    /// `is_none()` flags are bit-packed into a `u64` at the lane's position, then
32    /// AND-combined with the chunk's validity bitmap — null-lane bits vanish.
33    ///
34    /// The closure shape is the same as [`try_map_into`] (`Fn(Item) -> Option<R>`);
35    /// the mask parameter is what makes this kernel mask-aware. Callers that need to
36    /// distinguish null lanes inside the closure (e.g. to short-circuit an expensive
37    /// computation) should construct their own per-lane validity check externally; for
38    /// the common case, the kernel's automatic filter is sufficient.
39    ///
40    /// On failure returns `Err(failing_lane_index)`. Lanes whose `f` returned `None`
41    /// write `R::default()` into `out`, but the contents of `out` must not be relied
42    /// upon when this function returns `Err`.
43    ///
44    /// [`try_map_into`]: IndexedSourceExt::try_map_into
45    ///
46    /// # Panics
47    ///
48    /// Panics if `self.len() != mask.len()` or `out.len() != self.len()`.
49    #[inline]
50    fn try_map_masked_into<R, F>(
51        self,
52        mask: &BitBuffer,
53        out: &mut [MaybeUninit<R>],
54        f: F,
55    ) -> Result<(), usize>
56    where
57        R: Copy + Default,
58        F: Fn(Self::Item) -> Option<R>,
59    {
60        #[allow(clippy::inline_always)]
61        #[inline(always)]
62        fn chunk<S, R, F>(
63            values: &S,
64            out: &mut [MaybeUninit<R>],
65            f: &F,
66            src_chunk: u64,
67            base: usize,
68            count: usize,
69        ) -> Option<usize>
70        where
71            S: IndexedSource,
72            R: Copy + Default,
73            F: Fn(S::Item) -> Option<R>,
74        {
75            let mut fail_bits: u64 = 0;
76            for bit_idx in 0..count {
77                let idx = base + bit_idx;
78                // SAFETY: caller guarantees base + count <= len.
79                let val = unsafe { values.get_unchecked(idx) };
80                let opt = f(val);
81                fail_bits |= (opt.is_none() as u64) << bit_idx;
82                let result = opt.unwrap_or_default();
83                unsafe { out.get_unchecked_mut(idx).write(result) };
84            }
85            let valid_failures = fail_bits & src_chunk;
86            (valid_failures != 0).then_some(base + valid_failures.trailing_zeros() as usize)
87        }
88
89        let values = self;
90        let len = values.len();
91        assert_eq!(len, mask.len(), "values and mask must have the same length");
92        assert_eq!(out.len(), len, "out must have the same length as values");
93
94        let chunks = mask.chunks();
95        let chunks_count = len / 64;
96        let remainder = len % 64;
97
98        for (chunk_idx, src_chunk) in chunks.iter().enumerate() {
99            if let Some(idx) = chunk(&values, out, &f, src_chunk, chunk_idx * 64, 64) {
100                return Err(idx);
101            }
102        }
103        if remainder != 0
104            && let Some(idx) = chunk(
105                &values,
106                out,
107                &f,
108                chunks.remainder_bits(),
109                chunks_count * 64,
110                remainder,
111            )
112        {
113            return Err(idx);
114        }
115        Ok(())
116    }
117
118    /// Apply `f(value)` lane-by-lane with **no validity awareness at all** — every
119    /// closure invocation is treated as "happened", regardless of whether the lane
120    /// is null. Use this only when the input is known non-nullable.
121    ///
122    /// # Panics
123    ///
124    /// Panics if `out.len() != self.len()`.
125    #[inline]
126    fn map_into<R, F>(self, out: &mut [MaybeUninit<R>], f: F)
127    where
128        F: Fn(Self::Item) -> R,
129    {
130        #[allow(clippy::inline_always)]
131        #[inline(always)]
132        fn chunk<S, R, F>(values: &S, out: &mut [MaybeUninit<R>], f: &F, base: usize, count: usize)
133        where
134            S: IndexedSource,
135            F: Fn(S::Item) -> R,
136        {
137            for bit_idx in 0..count {
138                let idx = base + bit_idx;
139                // SAFETY: caller guarantees base + count <= len.
140                let val = unsafe { values.get_unchecked(idx) };
141                unsafe { out.get_unchecked_mut(idx).write(f(val)) };
142            }
143        }
144
145        let values = self;
146        let len = values.len();
147        assert_eq!(out.len(), len, "out must have the same length as values");
148
149        let chunks_count = len / CHUNK_LEN;
150        let remainder = len % CHUNK_LEN;
151
152        for chunk_idx in 0..chunks_count {
153            chunk(&values, out, &f, chunk_idx * CHUNK_LEN, CHUNK_LEN);
154        }
155        if remainder != 0 {
156            chunk(&values, out, &f, chunks_count * CHUNK_LEN, remainder);
157        }
158    }
159
160    /// Apply the predicate `f(value)` lane-by-lane and bit-pack the results into
161    /// `words`, LSB-first, 64 lanes per `u64`.
162    ///
163    /// This is the kernel shape behind comparison operators: each lane read is an
164    /// independent indexed load (drive two columns via [`LaneZip`]) and the 64
165    /// per-lane booleans of a chunk reduce into a single word with `OR + shift`,
166    /// which the autovectorizer lowers to a vector compare plus movemask.
167    ///
168    /// Words are written with `=` (not `|=`), so `words` need not be
169    /// zero-initialised. Bits at positions `>= self.len()` in the last word are
170    /// written as zero.
171    ///
172    /// Like [`map_into`], this kernel has no validity awareness; pair the packed
173    /// bits with a separately computed validity mask.
174    ///
175    /// [`LaneZip`]: crate::lane_kernels::LaneZip
176    /// [`map_into`]: IndexedSourceExt::map_into
177    ///
178    /// # Panics
179    ///
180    /// Panics if `words.len() < self.len().div_ceil(64)`.
181    #[inline]
182    fn map_bits_into<F>(self, words: &mut [u64], f: F)
183    where
184        F: Fn(Self::Item) -> bool,
185    {
186        #[allow(clippy::inline_always)]
187        #[inline(always)]
188        fn chunk<S, F>(values: &S, f: &F, base: usize, count: usize) -> u64
189        where
190            S: IndexedSource,
191            F: Fn(S::Item) -> bool,
192        {
193            let mut packed: u64 = 0;
194            for bit_idx in 0..count {
195                // SAFETY: caller guarantees base + count <= len.
196                let val = unsafe { values.get_unchecked(base + bit_idx) };
197                packed |= (f(val) as u64) << bit_idx;
198            }
199            packed
200        }
201
202        let values = self;
203        let len = values.len();
204        let num_words = len.div_ceil(64);
205        assert!(
206            words.len() >= num_words,
207            "words slice has {} entries, need at least {num_words}",
208            words.len(),
209        );
210
211        let full = len / 64;
212        let remainder = len % 64;
213
214        for word_idx in 0..full {
215            words[word_idx] = chunk(&values, &f, word_idx * 64, 64);
216        }
217        if remainder != 0 {
218            words[full] = chunk(&values, &f, full * 64, remainder);
219        }
220    }
221
222    /// Split value/failure map with **no validity awareness at all**: write every lane's value
223    /// unconditionally and OR-reduce its failure evidence into the return.
224    ///
225    /// The fastest checked shape, running at the speed of the unchecked [`map_into`] in exchange
226    /// for reporting only _that_ some lane failed and never exiting early. Re-run the now known
227    /// cold input through [`try_map_into`] or [`try_map_masked_into`] to attribute the failure or
228    /// to drop the null-lane ones. The evidence reduces inside the kernel because a captured `&mut`
229    /// becomes a loop-carried memory dependence that blocks vectorization.
230    ///
231    /// Anything other than [`Default`] means failure, and `bool` is the ordinary `Fail`. Wider
232    /// words exist for operations where deriving a `bool` costs the vectorization it guards.
233    /// **`Fail` must be no wider than `R`**, asserted below, or the reduction rather than the
234    /// operation decides how many lanes fit in a vector.
235    ///
236    /// [`map_into`]: IndexedSourceExt::map_into
237    /// [`try_map_into`]: IndexedSourceExt::try_map_into
238    /// [`try_map_masked_into`]: IndexedSourceExt::try_map_masked_into
239    ///
240    /// # Panics
241    ///
242    /// Panics if `out.len() != self.len()`.
243    #[inline]
244    fn map_checked_into<R, Fail, Apply>(self, out: &mut [MaybeUninit<R>], apply: Apply) -> Fail
245    where
246        Fail: Copy + Default + BitOrAssign,
247        Apply: Fn(Self::Item) -> (R, Fail),
248    {
249        const {
250            assert!(
251                size_of::<Fail>() <= size_of::<R>(),
252                "failure evidence must be no wider than the value, or it bounds the vector width"
253            )
254        };
255
256        let values = self;
257        let len = values.len();
258        assert_eq!(out.len(), len, "out must have the same length as values");
259
260        let mut failed = Fail::default();
261        for idx in 0..len {
262            // SAFETY: idx < len by the loop bound, and out.len() == len.
263            let val = unsafe { values.get_unchecked(idx) };
264
265            let (result, failure) = apply(val);
266            failed |= failure;
267
268            // SAFETY: idx < len == out.len().
269            unsafe { out.get_unchecked_mut(idx).write(result) };
270        }
271        failed
272    }
273
274    /// Fallible map with **no validity awareness at all** — every `None` returned
275    /// by the closure is treated as a failure, even at null lanes.
276    ///
277    /// # Use this only for non-nullable inputs.
278    ///
279    /// For nullable inputs with a fallible closure, use [`try_map_masked_into`] —
280    /// it has the same value-only closure shape (and the same perf win) but
281    /// **correctly suppresses null-lane failures** via per-chunk
282    /// `fail_bits & mask_chunk`.
283    ///
284    /// Using this kernel on a nullable input where a null lane's stored value
285    /// would cause `f` to return `None` will produce a spurious `Err`. This is a
286    /// correctness footgun on purpose — the name and this doc are how the API
287    /// signals "you must know your input has no nulls."
288    ///
289    /// On failure returns `Err(failing_lane_index)`.
290    ///
291    /// [`try_map_masked_into`]: IndexedSourceExt::try_map_masked_into
292    ///
293    /// # Panics
294    ///
295    /// Panics if `out.len() != self.len()`.
296    #[inline]
297    fn try_map_into<R, F>(self, out: &mut [MaybeUninit<R>], f: F) -> Result<(), usize>
298    where
299        R: Copy + Default,
300        F: Fn(Self::Item) -> Option<R>,
301    {
302        /// Returns `true` if any lane in `[base, base+count)` failed (OR-reduced);
303        /// the cold attribution path is called at the kernel level so it can be
304        /// inlined separately for full vs remainder.
305        #[allow(clippy::inline_always)]
306        #[inline(always)]
307        fn chunk<S, R, F>(
308            values: &S,
309            out: &mut [MaybeUninit<R>],
310            f: &F,
311            base: usize,
312            count: usize,
313        ) -> bool
314        where
315            S: IndexedSource,
316            R: Copy + Default,
317            F: Fn(S::Item) -> Option<R>,
318        {
319            let mut fail_acc: u64 = 0;
320            for bit_idx in 0..count {
321                let idx = base + bit_idx;
322                // SAFETY: caller guarantees base + count <= len.
323                let val = unsafe { values.get_unchecked(idx) };
324                let opt = f(val);
325                fail_acc |= opt.is_none() as u64;
326                let result = opt.unwrap_or_default();
327                unsafe { out.get_unchecked_mut(idx).write(result) };
328            }
329            fail_acc != 0
330        }
331
332        let values = self;
333        let len = values.len();
334        assert_eq!(out.len(), len, "out must have the same length as values");
335
336        let chunks_count = len / CHUNK_LEN;
337        let remainder = len % CHUNK_LEN;
338
339        for chunk_idx in 0..chunks_count {
340            let base = chunk_idx * CHUNK_LEN;
341            if chunk(&values, out, &f, base, CHUNK_LEN) {
342                return Err(attribute_failure_no_mask(&values, base, CHUNK_LEN, &f));
343            }
344        }
345        if remainder != 0 {
346            let base = chunks_count * CHUNK_LEN;
347            if chunk(&values, out, &f, base, remainder) {
348                return Err(attribute_failure_no_mask(&values, base, remainder, &f));
349            }
350        }
351        Ok(())
352    }
353}
354
355impl<S: IndexedSource> IndexedSourceExt for S {}
356
357/// Shared cold scan: walks a chunk, returns the first lane index where
358/// `lane_fails(bit_idx, value)` returns `true`. Used by
359/// [`attribute_failure_no_mask`].
360///
361/// Caller guarantees `base + chunk_len <= values.len()`.
362#[cold]
363#[inline(never)]
364fn cold_scan<S>(
365    values: &S,
366    base: usize,
367    chunk_len: usize,
368    lane_fails: impl Fn(usize /* bit_idx */, S::Item) -> bool,
369) -> usize
370where
371    S: IndexedSource,
372{
373    for bit_idx in 0..chunk_len {
374        let idx = base + bit_idx;
375        // SAFETY: caller guarantees idx < values.len().
376        let val = unsafe { values.get_unchecked(idx) };
377        if lane_fails(bit_idx, val) {
378            return idx;
379        }
380    }
381    unreachable!("cold_scan called without a failing lane")
382}
383
384/// Cold attribution for the no-mask variant. Replays `f` over the chunk to find
385/// the first lane that returns `None`.
386#[inline]
387fn attribute_failure_no_mask<S, R, F>(values: &S, base: usize, chunk_len: usize, f: &F) -> usize
388where
389    S: IndexedSource,
390    F: Fn(S::Item) -> Option<R>,
391{
392    cold_scan(values, base, chunk_len, |_bit_idx, val| f(val).is_none())
393}
394
395#[cfg(test)]
396#[allow(clippy::cast_possible_truncation)]
397mod tests {
398    use vortex_buffer::BitBuffer;
399    use vortex_buffer::BitBufferMut;
400
401    use super::*;
402
403    fn write_t<T: Copy>(out: Vec<MaybeUninit<T>>) -> Vec<T> {
404        // SAFETY: tests always fully initialize the buffer.
405        unsafe { std::mem::transmute(out) }
406    }
407
408    #[test]
409    fn try_map_masked_into_all_ok() {
410        let values: Vec<u64> = (0..200).collect();
411        let mask = BitBuffer::new_set(200);
412        let mut out = vec![MaybeUninit::<u32>::uninit(); 200];
413        let res = values.as_slice().try_map_masked_into(&mask, &mut out, |v| {
414            (v <= u32::MAX as u64).then_some(v as u32)
415        });
416        assert!(res.is_ok());
417        let got = write_t(out);
418        assert_eq!(got, (0..200u32).collect::<Vec<_>>());
419    }
420
421    #[test]
422    fn try_map_masked_into_overflow_fails() {
423        let mut values: Vec<u64> = (0..200).collect();
424        values[137] = (u32::MAX as u64) + 1;
425        let mask = BitBuffer::new_set(200);
426        let mut out = vec![MaybeUninit::<u32>::uninit(); 200];
427        let res = values.as_slice().try_map_masked_into(&mask, &mut out, |v| {
428            (v <= u32::MAX as u64).then_some(v as u32)
429        });
430        assert_eq!(res, Err(137));
431    }
432
433    #[test]
434    fn try_map_masked_into_overflow_reports_first_failing_lane() {
435        let mut values: Vec<u64> = (0..200).collect();
436        values[50] = u64::MAX;
437        values[51] = u64::MAX;
438        values[137] = u64::MAX;
439        let mask = BitBuffer::new_set(200);
440        let mut out = vec![MaybeUninit::<u32>::uninit(); 200];
441        let res = values.as_slice().try_map_masked_into(&mask, &mut out, |v| {
442            (v <= u32::MAX as u64).then_some(v as u32)
443        });
444        assert_eq!(res, Err(50));
445    }
446
447    #[test]
448    fn try_map_masked_into_value_only_closure_filters_null_overflow() {
449        let mut values: Vec<u64> = (0..200).collect();
450        values[5] = u64::MAX;
451        values[42] = u64::MAX;
452        let mask = {
453            let mut m = BitBufferMut::with_capacity(200);
454            for i in 0..200 {
455                m.append(i != 5 && i != 42);
456            }
457            m.freeze()
458        };
459        let mut out = vec![MaybeUninit::<u32>::uninit(); 200];
460        let res = values.as_slice().try_map_masked_into(&mask, &mut out, |v| {
461            (v <= u32::MAX as u64).then_some(v as u32)
462        });
463        assert!(
464            res.is_ok(),
465            "null-lane overflow should be filtered by the cold path"
466        );
467    }
468
469    #[test]
470    fn try_map_masked_into_value_only_closure_reports_first_valid_failure() {
471        let mut values: Vec<u64> = (0..200).collect();
472        values[5] = u64::MAX;
473        values[42] = u64::MAX;
474        values[77] = u64::MAX;
475        values[100] = u64::MAX;
476        let mask = {
477            let mut m = BitBufferMut::with_capacity(200);
478            for i in 0..200 {
479                m.append(i != 5 && i != 42);
480            }
481            m.freeze()
482        };
483        let mut out = vec![MaybeUninit::<u32>::uninit(); 200];
484        let res = values.as_slice().try_map_masked_into(&mask, &mut out, |v| {
485            (v <= u32::MAX as u64).then_some(v as u32)
486        });
487        assert_eq!(res, Err(77));
488    }
489
490    #[test]
491    fn try_map_masked_into_null_lane_bypasses_check() {
492        let mut values: Vec<u64> = (0..200).collect();
493        values[5] = u64::MAX;
494        let mask = {
495            let mut m = BitBufferMut::with_capacity(200);
496            for i in 0..200 {
497                m.append(i != 5);
498            }
499            m.freeze()
500        };
501        let mut out = vec![MaybeUninit::<u32>::uninit(); 200];
502        let res = values.as_slice().try_map_masked_into(&mask, &mut out, |v| {
503            (v <= u32::MAX as u64).then_some(v as u32)
504        });
505        assert!(res.is_ok());
506        let got = write_t(out);
507        assert_eq!(got[5], 0);
508        assert_eq!(got[6], 6);
509    }
510
511    #[test]
512    fn try_map_masked_into_branchful_matches_branchless() {
513        let mut values: Vec<u64> = (0..130).map(|i| i as u64 * 7).collect();
514        values[2] = u64::MAX;
515        values[65] = u32::MAX as u64;
516        let mask = {
517            let mut m = BitBufferMut::with_capacity(130);
518            for i in 0..130 {
519                m.append(!matches!(i, 2 | 17 | 99));
520            }
521            m.freeze()
522        };
523
524        let mut branchless = vec![MaybeUninit::<u32>::uninit(); 130];
525        let mut branchful = vec![MaybeUninit::<u32>::uninit(); 130];
526        values
527            .as_slice()
528            .try_map_masked_into(&mask, &mut branchless, |v| {
529                (v <= u32::MAX as u64).then_some(v as u32)
530            })
531            .unwrap();
532        values
533            .as_slice()
534            .try_map_masked_into(&mask, &mut branchful, |v| u32::try_from(v).ok())
535            .unwrap();
536
537        assert_eq!(write_t(branchful), write_t(branchless));
538    }
539
540    #[test]
541    fn try_map_masked_into_partial_chunk() {
542        let values: Vec<u64> = (0..130).collect();
543        let mask = BitBuffer::new_set(130);
544        let mut out = vec![MaybeUninit::<u32>::uninit(); 130];
545        let res = values.as_slice().try_map_masked_into(&mask, &mut out, |v| {
546            (v <= u32::MAX as u64).then_some(v as u32)
547        });
548        assert!(res.is_ok());
549        let got = write_t(out);
550        assert_eq!(got.len(), 130);
551        assert_eq!(got[129], 129);
552    }
553
554    #[test]
555    fn try_map_masked_into_sliced_mask_unaligned_offset() {
556        let big = BitBuffer::new_set(256);
557        let mask = big.slice(13..143);
558        assert_eq!(mask.len(), 130);
559
560        let values: Vec<u64> = (0..130).collect();
561        let mut out = vec![MaybeUninit::<u32>::uninit(); 130];
562        let res = values.as_slice().try_map_masked_into(&mask, &mut out, |v| {
563            (v <= u32::MAX as u64).then_some(v as u32)
564        });
565        assert!(res.is_ok());
566        let got = write_t(out);
567        assert_eq!(got, (0..130u32).collect::<Vec<_>>());
568    }
569
570    #[test]
571    fn try_map_masked_into_sliced_mask_with_overflow() {
572        let big = BitBuffer::new_set(256);
573        let mask = big.slice(13..143);
574        assert_eq!(mask.len(), 130);
575
576        let mut values: Vec<u64> = (0..130).collect();
577        values[77] = u64::MAX;
578        let mut out = vec![MaybeUninit::<u32>::uninit(); 130];
579        let res = values.as_slice().try_map_masked_into(&mask, &mut out, |v| {
580            (v <= u32::MAX as u64).then_some(v as u32)
581        });
582        assert_eq!(res, Err(77));
583    }
584
585    #[test]
586    fn try_map_masked_into_sliced_mask_null_lanes() {
587        let mut m = BitBufferMut::with_capacity(256);
588        for i in 0..256 {
589            m.append(i % 3 != 0);
590        }
591        let big = m.freeze();
592        let mask = big.slice(13..143);
593        assert_eq!(mask.len(), 130);
594
595        let mut values: Vec<u64> = (0..130).collect();
596        values[2] = u64::MAX;
597        let mut out = vec![MaybeUninit::<u32>::uninit(); 130];
598        let res = values.as_slice().try_map_masked_into(&mask, &mut out, |v| {
599            (v <= u32::MAX as u64).then_some(v as u32)
600        });
601        assert!(res.is_ok(), "null lane should bypass the range check");
602    }
603
604    #[test]
605    fn map_checked_into_writes_all_lanes_and_reduces_flag() {
606        let mut values: Vec<u64> = (0..130).collect();
607        let mut out = vec![MaybeUninit::<u32>::uninit(); 130];
608        let failed = values
609            .as_slice()
610            .map_checked_into(&mut out, |v| (v as u32, v > u32::MAX as u64));
611        assert!(!failed);
612        assert_eq!(write_t(out), (0..130u32).collect::<Vec<_>>());
613
614        values[77] = (u32::MAX as u64) + 1;
615        let mut out = vec![MaybeUninit::<u32>::uninit(); 130];
616        let failed = values
617            .as_slice()
618            .map_checked_into(&mut out, |v| (v as u32, v > u32::MAX as u64));
619        assert!(failed);
620        // Failing lanes still write their (wrapped) value.
621        assert_eq!(write_t(out)[76], 76);
622    }
623
624    #[test]
625    fn map_bits_into_packs_full_and_remainder_words() {
626        let values: Vec<u32> = (0..130).collect();
627        let mut words = vec![u64::MAX; 3];
628        values.as_slice().map_bits_into(&mut words, |v| v % 2 == 0);
629
630        for idx in 0..130 {
631            let bit = (words[idx / 64] >> (idx % 64)) & 1;
632            assert_eq!(bit == 1, idx % 2 == 0, "lane {idx}");
633        }
634        // Bits past `len` in the remainder word must be written as zero.
635        assert_eq!(words[2] >> 2, 0);
636    }
637
638    #[test]
639    fn map_bits_into_lane_zip_compare() {
640        use crate::lane_kernels::LaneZip;
641
642        let lhs: Vec<i64> = (0..100).collect();
643        let rhs: Vec<i64> = (0..100).rev().collect();
644        let mut words = vec![0u64; 2];
645        LaneZip::new(lhs.as_slice(), rhs.as_slice()).map_bits_into(&mut words, |(a, b)| a >= b);
646
647        for idx in 0..100 {
648            let bit = (words[idx / 64] >> (idx % 64)) & 1;
649            assert_eq!(bit == 1, lhs[idx] >= rhs[idx], "lane {idx}");
650        }
651    }
652
653    #[test]
654    #[should_panic(expected = "words slice has 1 entries")]
655    fn map_bits_into_words_too_short_panics() {
656        let values: Vec<u32> = (0..65).collect();
657        let mut words = vec![0u64; 1];
658        values.as_slice().map_bits_into(&mut words, |v| v > 0);
659    }
660
661    #[test]
662    fn try_map_masked_into_overflow_in_remainder() {
663        let mut values: Vec<u64> = (0..130).collect();
664        values[129] = (u32::MAX as u64) + 1;
665        let mask = BitBuffer::new_set(130);
666        let mut out = vec![MaybeUninit::<u32>::uninit(); 130];
667        let res = values.as_slice().try_map_masked_into(&mask, &mut out, |v| {
668            (v <= u32::MAX as u64).then_some(v as u32)
669        });
670        assert_eq!(res, Err(129));
671    }
672}