Skip to main content

vortex_array/scalar_fn/fns/mask/
kernel.rs

1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: Copyright the Vortex contributors
3
4use vortex_error::VortexResult;
5use vortex_error::vortex_err;
6
7use crate::ArrayRef;
8use crate::ExecutionCtx;
9use crate::array::ArrayView;
10use crate::array::VTable;
11use crate::arrays::Bool;
12use crate::arrays::Constant;
13use crate::arrays::scalar_fn::ExactScalarFn;
14use crate::arrays::scalar_fn::ScalarFnArrayView;
15use crate::kernel::ExecuteParentKernel;
16use crate::optimizer::rules::ArrayParentReduceRule;
17use crate::scalar_fn::fns::mask::Mask as MaskExpr;
18
19/// Mask an array without reading buffers.
20///
21/// This trait is for mask implementations that can operate purely on array metadata and
22/// structure without needing to read or execute on the underlying buffers. Implementations
23/// should return `None` if masking requires buffer access.
24///
25/// The `mask` parameter is a boolean array where true=keep/valid, false=null-out.
26///
27/// # Preconditions
28///
29/// The mask is guaranteed to have the same length as the array. Trivial cases
30/// (`AllValid`, `AllInvalid`, `NonNullable`) are handled by the caller before dispatch.
31pub trait MaskReduce: VTable {
32    fn mask(array: ArrayView<'_, Self>, mask: &ArrayRef) -> VortexResult<Option<ArrayRef>>;
33}
34
35/// Mask an array, potentially reading buffers.
36///
37/// Unlike [`MaskReduce`], this trait is for mask implementations that may need to read
38/// and execute on the underlying buffers to produce the masked result.
39///
40/// The `mask` parameter is a boolean array where true=keep/valid, false=null-out.
41///
42/// # Preconditions
43///
44/// The mask is guaranteed to have the same length as the array. Trivial cases
45/// (`AllValid`, `AllInvalid`, `NonNullable`) are handled by the caller before dispatch.
46pub trait MaskKernel: VTable {
47    fn mask(
48        array: ArrayView<'_, Self>,
49        mask: &ArrayRef,
50        ctx: &mut ExecutionCtx,
51    ) -> VortexResult<Option<ArrayRef>>;
52}
53
54/// Adaptor that wraps a [`MaskReduce`] impl as an [`ArrayParentReduceRule`].
55#[derive(Default, Debug)]
56pub struct MaskReduceAdaptor<V>(pub V);
57
58impl<V> ArrayParentReduceRule<V> for MaskReduceAdaptor<V>
59where
60    V: MaskReduce,
61{
62    type Parent = ExactScalarFn<MaskExpr>;
63
64    fn reduce_parent(
65        &self,
66        array: ArrayView<'_, V>,
67        parent: ScalarFnArrayView<'_, MaskExpr>,
68        child_idx: usize,
69    ) -> VortexResult<Option<ArrayRef>> {
70        // Only reduce the input child (index 0), not the mask child (index 1).
71        if child_idx != 0 {
72            return Ok(None);
73        }
74        // Reduce only when the mask (child 1) is readable from metadata: a concrete `Bool` or a
75        // `Constant`. `Mask::return_dtype` guarantees the mask is `Bool(NonNullable)`, so a
76        // `Constant` here is a non-nullable Boolean. Other encodings may need execution, so leave
77        // them to the kernel.
78        let parent_ref: ArrayRef = (*parent).clone();
79        let mask_child = parent_ref
80            .nth_child(1)
81            .ok_or_else(|| vortex_err!("Mask expression must have 2 children"))?;
82        if mask_child.as_opt::<Bool>().is_none() && mask_child.as_opt::<Constant>().is_none() {
83            return Ok(None);
84        }
85        <V as MaskReduce>::mask(array, &mask_child)
86    }
87}
88
89/// Adaptor that wraps a [`MaskKernel`] impl as an [`ExecuteParentKernel`].
90#[derive(Default, Debug)]
91pub struct MaskExecuteAdaptor<V>(pub V);
92
93impl<V> ExecuteParentKernel<V> for MaskExecuteAdaptor<V>
94where
95    V: MaskKernel,
96{
97    type Parent = ExactScalarFn<MaskExpr>;
98
99    fn execute_parent(
100        &self,
101        array: ArrayView<'_, V>,
102        parent: ScalarFnArrayView<'_, MaskExpr>,
103        child_idx: usize,
104        ctx: &mut ExecutionCtx,
105    ) -> VortexResult<Option<ArrayRef>> {
106        // Only execute the input child (index 0), not the mask child (index 1).
107        if child_idx != 0 {
108            return Ok(None);
109        }
110        let mask_child = parent
111            .nth_child(1)
112            .ok_or_else(|| vortex_err!("Mask expression must have 2 children"))?;
113        <V as MaskKernel>::mask(array, &mask_child, ctx)
114    }
115}
116
117#[cfg(test)]
118mod tests {
119    use rstest::rstest;
120    use vortex_buffer::buffer;
121    use vortex_error::VortexResult;
122
123    use crate::IntoArray;
124    use crate::arrays::ConstantArray;
125    use crate::arrays::Primitive;
126    use crate::arrays::PrimitiveArray;
127    use crate::arrays::ScalarFn;
128    use crate::arrays::scalar_fn::ScalarFnFactoryExt;
129    use crate::assert_arrays_eq;
130    use crate::dtype::Nullability;
131    use crate::executor::VortexSessionExecute;
132    use crate::optimizer::ArrayOptimizer;
133    use crate::scalar::Scalar;
134    use crate::scalar_fn::EmptyOptions;
135    use crate::scalar_fn::fns::mask::Mask as MaskExpr;
136
137    /// A constant Boolean mask child must take the metadata-only reduction path (pushing into the
138    /// input encoding) rather than surviving as a `ScalarFn` wrapper that falls through to
139    /// execution. Asserting the optimized encoding makes this fail before the adaptor accepts
140    /// `Constant` masks, not just verifying values that could pass through the execution fallback.
141    #[rstest]
142    #[case(true)]
143    #[case(false)]
144    fn constant_mask_reduces_into_input(#[case] mask_value: bool) -> VortexResult<()> {
145        let input = buffer![1i32, 2, 3, 4, 5].into_array();
146        let mask = ConstantArray::new(
147            Scalar::bool(mask_value, Nullability::NonNullable),
148            input.len(),
149        )
150        .into_array();
151
152        let masked = MaskExpr.try_new_array(input.len(), EmptyOptions, [input, mask])?;
153        assert!(
154            masked.is::<ScalarFn>(),
155            "expected an un-optimized ScalarFn wrapper before optimization"
156        );
157
158        let optimized = masked.optimize()?;
159        assert!(
160            !optimized.is::<ScalarFn>(),
161            "constant mask should not fall through to execution, got {}",
162            optimized.encoding_id()
163        );
164        assert!(
165            optimized.is::<Primitive>(),
166            "constant mask should reduce into the Primitive input, got {}",
167            optimized.encoding_id()
168        );
169
170        let mut ctx = crate::array_session().create_execution_ctx();
171        let expected = if mask_value {
172            PrimitiveArray::from_option_iter([Some(1i32), Some(2), Some(3), Some(4), Some(5)])
173        } else {
174            PrimitiveArray::from_option_iter([None::<i32>, None, None, None, None])
175        };
176        assert_arrays_eq!(optimized, expected, &mut ctx);
177
178        Ok(())
179    }
180}