vortex_array/compute/
boolean.rs

1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: Copyright the Vortex contributors
3
4use std::any::Any;
5use std::sync::LazyLock;
6
7use arcref::ArcRef;
8use arrow_array::cast::AsArray;
9use arrow_schema::DataType;
10use vortex_dtype::DType;
11use vortex_error::{VortexError, VortexExpect, VortexResult, vortex_bail, vortex_err};
12
13use crate::arrow::{FromArrowArray, IntoArrowArray};
14use crate::compute::{ComputeFn, ComputeFnVTable, InvocationArgs, Kernel, Options, Output};
15use crate::vtable::VTable;
16use crate::{Array, ArrayRef};
17
18static BOOLEAN_FN: LazyLock<ComputeFn> = LazyLock::new(|| {
19    let compute = ComputeFn::new("boolean".into(), ArcRef::new_ref(&Boolean));
20    for kernel in inventory::iter::<BooleanKernelRef> {
21        compute.register_kernel(kernel.0.clone());
22    }
23    compute
24});
25
26/// Point-wise logical _and_ between two Boolean arrays.
27///
28/// This method uses Arrow-style null propagation rather than the Kleene logic semantics. This
29/// semantics is also known as "Bochvar logic" and "weak Kleene logic".
30///
31/// See also [BooleanOperator::And]
32pub fn and(lhs: &dyn Array, rhs: &dyn Array) -> VortexResult<ArrayRef> {
33    boolean(lhs, rhs, BooleanOperator::And)
34}
35
36/// Point-wise Kleene logical _and_ between two Boolean arrays.
37///
38/// See also [BooleanOperator::AndKleene]
39pub fn and_kleene(lhs: &dyn Array, rhs: &dyn Array) -> VortexResult<ArrayRef> {
40    boolean(lhs, rhs, BooleanOperator::AndKleene)
41}
42
43/// Point-wise logical _or_ between two Boolean arrays.
44///
45/// This method uses Arrow-style null propagation rather than the Kleene logic semantics. This
46/// semantics is also known as "Bochvar logic" and "weak Kleene logic".
47///
48/// See also [BooleanOperator::Or]
49pub fn or(lhs: &dyn Array, rhs: &dyn Array) -> VortexResult<ArrayRef> {
50    boolean(lhs, rhs, BooleanOperator::Or)
51}
52
53/// Point-wise Kleene logical _or_ between two Boolean arrays.
54///
55/// See also [BooleanOperator::OrKleene]
56pub fn or_kleene(lhs: &dyn Array, rhs: &dyn Array) -> VortexResult<ArrayRef> {
57    boolean(lhs, rhs, BooleanOperator::OrKleene)
58}
59
60/// Point-wise logical operator between two Boolean arrays.
61///
62/// This method uses Arrow-style null propagation rather than the Kleene logic semantics. This
63/// semantics is also known as "Bochvar logic" and "weak Kleene logic".
64pub fn boolean(lhs: &dyn Array, rhs: &dyn Array, op: BooleanOperator) -> VortexResult<ArrayRef> {
65    BOOLEAN_FN
66        .invoke(&InvocationArgs {
67            inputs: &[lhs.into(), rhs.into()],
68            options: &op,
69        })?
70        .unwrap_array()
71}
72
73pub struct BooleanKernelRef(ArcRef<dyn Kernel>);
74inventory::collect!(BooleanKernelRef);
75
76pub trait BooleanKernel: VTable {
77    fn boolean(
78        &self,
79        array: &Self::Array,
80        other: &dyn Array,
81        op: BooleanOperator,
82    ) -> VortexResult<Option<ArrayRef>>;
83}
84
85#[derive(Debug)]
86pub struct BooleanKernelAdapter<V: VTable>(pub V);
87
88impl<V: VTable + BooleanKernel> BooleanKernelAdapter<V> {
89    pub const fn lift(&'static self) -> BooleanKernelRef {
90        BooleanKernelRef(ArcRef::new_ref(self))
91    }
92}
93
94impl<V: VTable + BooleanKernel> Kernel for BooleanKernelAdapter<V> {
95    fn invoke(&self, args: &InvocationArgs) -> VortexResult<Option<Output>> {
96        let inputs = BooleanArgs::try_from(args)?;
97        let Some(array) = inputs.lhs.as_opt::<V>() else {
98            return Ok(None);
99        };
100        Ok(V::boolean(&self.0, array, inputs.rhs, inputs.operator)?.map(|array| array.into()))
101    }
102}
103
104struct Boolean;
105
106impl ComputeFnVTable for Boolean {
107    fn invoke(
108        &self,
109        args: &InvocationArgs,
110        kernels: &[ArcRef<dyn Kernel>],
111    ) -> VortexResult<Output> {
112        let BooleanArgs { lhs, rhs, operator } = BooleanArgs::try_from(args)?;
113
114        let rhs_is_constant = rhs.is_constant();
115
116        // If LHS is constant, then we make sure it's on the RHS.
117        if lhs.is_constant() && !rhs_is_constant {
118            return Ok(boolean(rhs, lhs, operator)?.into());
119        }
120
121        // If the RHS is constant and the LHS is Arrow, we can't do any better than arrow_compare.
122        if lhs.is_arrow() && (rhs.is_arrow() || rhs_is_constant) {
123            return Ok(arrow_boolean(lhs.to_array(), rhs.to_array(), operator)?.into());
124        }
125
126        // Check if either LHS or RHS supports the operation directly.
127        for kernel in kernels {
128            if let Some(output) = kernel.invoke(args)? {
129                return Ok(output);
130            }
131        }
132        if let Some(output) = lhs.invoke(&BOOLEAN_FN, args)? {
133            return Ok(output);
134        }
135
136        let inverse_args = InvocationArgs {
137            inputs: &[rhs.into(), lhs.into()],
138            options: &operator,
139        };
140        for kernel in kernels {
141            if let Some(output) = kernel.invoke(&inverse_args)? {
142                return Ok(output);
143            }
144        }
145        if let Some(output) = rhs.invoke(&BOOLEAN_FN, &inverse_args)? {
146            return Ok(output);
147        }
148
149        log::debug!(
150            "No boolean implementation found for LHS {}, RHS {}, and operator {:?} (or inverse)",
151            rhs.encoding_id(),
152            lhs.encoding_id(),
153            operator,
154        );
155
156        // If neither side implements the trait, then we delegate to Arrow compute.
157        Ok(arrow_boolean(lhs.to_array(), rhs.to_array(), operator)?.into())
158    }
159
160    fn return_dtype(&self, args: &InvocationArgs) -> VortexResult<DType> {
161        let BooleanArgs { lhs, rhs, .. } = BooleanArgs::try_from(args)?;
162
163        if !lhs.dtype().is_boolean()
164            || !rhs.dtype().is_boolean()
165            || !lhs.dtype().eq_ignore_nullability(rhs.dtype())
166        {
167            vortex_bail!(
168                "Boolean operations are only supported on boolean arrays: {} and {}",
169                lhs.dtype(),
170                rhs.dtype()
171            )
172        }
173
174        Ok(DType::Bool(
175            (lhs.dtype().is_nullable() || rhs.dtype().is_nullable()).into(),
176        ))
177    }
178
179    fn return_len(&self, args: &InvocationArgs) -> VortexResult<usize> {
180        let BooleanArgs { lhs, rhs, .. } = BooleanArgs::try_from(args)?;
181
182        if lhs.len() != rhs.len() {
183            vortex_bail!(
184                "Boolean operations aren't supported on arrays of different lengths: {} and {}",
185                lhs.len(),
186                rhs.len()
187            )
188        }
189
190        Ok(lhs.len())
191    }
192
193    fn is_elementwise(&self) -> bool {
194        true
195    }
196}
197
198/// Operations over the nullable Boolean values.
199///
200/// All three operators accept and produce values from the set {true, false, and null}.
201#[derive(Debug, Clone, Copy, PartialEq, Eq)]
202pub enum BooleanOperator {
203    /// Logical and, unless either value is null, in which case the result is null.
204    ///
205    /// | A ∧ B |       | **B** |       |       |
206    /// |:-----:|:-----:|:-----:|:-----:|:-----:|
207    /// |       |       | **F** | **U** | **T** |
208    /// | **A** | **F** | F     | U     | F     |
209    /// |       | **U** | U     | U     | U     |
210    /// |       | **T** | F     | U     | T     |
211    And,
212    /// [Kleene (three-valued) logical and](https://en.wikipedia.org/wiki/Three-valued_logic#Kleene_and_Priest_logics).
213    ///
214    /// | A ∧ B |       | **B** |       |       |
215    /// |:-----:|:-----:|:-----:|:-----:|:-----:|
216    /// |       |       | **F** | **U** | **T** |
217    /// | **A** | **F** | F     | F     | F     |
218    /// |       | **U** | F     | U     | U     |
219    /// |       | **T** | F     | U     | T     |
220    AndKleene,
221    /// Logical or, unless either value is null, in which case the result is null.
222    ///
223    /// | A ∨ B |       | **B** |       |       |
224    /// |:-----:|:-----:|:-----:|:-----:|:-----:|
225    /// |       |       | **F** | **U** | **T** |
226    /// | **A** | **F** | F     | U     | T     |
227    /// |       | **U** | U     | U     | U     |
228    /// |       | **T** | T     | U     | T     |
229    Or,
230    /// [Kleene (three-valued) logical or](https://en.wikipedia.org/wiki/Three-valued_logic#Kleene_and_Priest_logics).
231    ///
232    /// | A ∨ B |       | **B** |       |       |
233    /// |:-----:|:-----:|:-----:|:-----:|:-----:|
234    /// |       |       | **F** | **U** | **T** |
235    /// | **A** | **F** | F     | U     | T     |
236    /// |       | **U** | U     | U     | T     |
237    /// |       | **T** | T     | T     | T     |
238    OrKleene,
239    // AndNot,
240    // AndNotKleene,
241    // Xor,
242}
243
244impl Options for BooleanOperator {
245    fn as_any(&self) -> &dyn Any {
246        self
247    }
248}
249
250struct BooleanArgs<'a> {
251    lhs: &'a dyn Array,
252    rhs: &'a dyn Array,
253    operator: BooleanOperator,
254}
255
256impl<'a> TryFrom<&InvocationArgs<'a>> for BooleanArgs<'a> {
257    type Error = VortexError;
258
259    fn try_from(value: &InvocationArgs<'a>) -> VortexResult<Self> {
260        if value.inputs.len() != 2 {
261            vortex_bail!("Expected 2 inputs, found {}", value.inputs.len());
262        }
263        let lhs = value.inputs[0]
264            .array()
265            .ok_or_else(|| vortex_err!("Expected input 0 to be an array"))?;
266        let rhs = value.inputs[1]
267            .array()
268            .ok_or_else(|| vortex_err!("Expected input 1 to be an array"))?;
269        let operator = value
270            .options
271            .as_any()
272            .downcast_ref::<BooleanOperator>()
273            .vortex_expect("Expected options to be an operator");
274
275        Ok(BooleanArgs {
276            lhs,
277            rhs,
278            operator: *operator,
279        })
280    }
281}
282
283/// Implementation of `BinaryBooleanFn` using the Arrow crate.
284///
285/// Note that other encodings should handle a constant RHS value, so we can assume here that
286/// the RHS is not constant and expand to a full array.
287pub(crate) fn arrow_boolean(
288    lhs: ArrayRef,
289    rhs: ArrayRef,
290    operator: BooleanOperator,
291) -> VortexResult<ArrayRef> {
292    let nullable = lhs.dtype().is_nullable() || rhs.dtype().is_nullable();
293
294    let lhs = lhs.into_arrow(&DataType::Boolean)?.as_boolean().clone();
295    let rhs = rhs.into_arrow(&DataType::Boolean)?.as_boolean().clone();
296
297    let array = match operator {
298        BooleanOperator::And => arrow_arith::boolean::and(&lhs, &rhs)?,
299        BooleanOperator::AndKleene => arrow_arith::boolean::and_kleene(&lhs, &rhs)?,
300        BooleanOperator::Or => arrow_arith::boolean::or(&lhs, &rhs)?,
301        BooleanOperator::OrKleene => arrow_arith::boolean::or_kleene(&lhs, &rhs)?,
302    };
303
304    Ok(ArrayRef::from_arrow(&array, nullable))
305}
306
307#[cfg(test)]
308mod tests {
309    use rstest::rstest;
310
311    use super::*;
312    use crate::IntoArray;
313    use crate::arrays::BoolArray;
314    use crate::canonical::ToCanonical;
315    #[rstest]
316    #[case(BoolArray::from_iter([Some(true), Some(true), Some(false), Some(false)].into_iter())
317    .into_array(), BoolArray::from_iter([Some(true), Some(false), Some(true), Some(false)].into_iter())
318    .into_array())]
319    #[case(BoolArray::from_iter([Some(true), Some(false), Some(true), Some(false)].into_iter()).into_array(),
320        BoolArray::from_iter([Some(true), Some(true), Some(false), Some(false)].into_iter()).into_array())]
321    fn test_or(#[case] lhs: ArrayRef, #[case] rhs: ArrayRef) {
322        let r = or(&lhs, &rhs).unwrap();
323
324        let r = r.to_bool().unwrap().into_array();
325
326        let v0 = r.scalar_at(0).as_bool().value();
327        let v1 = r.scalar_at(1).as_bool().value();
328        let v2 = r.scalar_at(2).as_bool().value();
329        let v3 = r.scalar_at(3).as_bool().value();
330
331        assert!(v0.unwrap());
332        assert!(v1.unwrap());
333        assert!(v2.unwrap());
334        assert!(!v3.unwrap());
335    }
336
337    #[rstest]
338    #[case(BoolArray::from_iter([Some(true), Some(true), Some(false), Some(false)].into_iter())
339    .into_array(), BoolArray::from_iter([Some(true), Some(false), Some(true), Some(false)].into_iter())
340    .into_array())]
341    #[case(BoolArray::from_iter([Some(true), Some(false), Some(true), Some(false)].into_iter()).into_array(),
342        BoolArray::from_iter([Some(true), Some(true), Some(false), Some(false)].into_iter()).into_array())]
343    fn test_and(#[case] lhs: ArrayRef, #[case] rhs: ArrayRef) {
344        let r = and(&lhs, &rhs).unwrap().to_bool().unwrap().into_array();
345
346        let v0 = r.scalar_at(0).as_bool().value();
347        let v1 = r.scalar_at(1).as_bool().value();
348        let v2 = r.scalar_at(2).as_bool().value();
349        let v3 = r.scalar_at(3).as_bool().value();
350
351        assert!(v0.unwrap());
352        assert!(!v1.unwrap());
353        assert!(!v2.unwrap());
354        assert!(!v3.unwrap());
355    }
356}