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