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