Skip to main content

vortex_array/expr/
bound_expression.rs

1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: Copyright the Vortex contributors
3
4use std::fmt;
5use std::fmt::Display;
6use std::fmt::Formatter;
7use std::hash::Hash;
8use std::hash::Hasher;
9use std::sync::Arc;
10
11use itertools::Itertools;
12use vortex_error::VortexExpect;
13use vortex_error::VortexResult;
14use vortex_error::vortex_ensure;
15use vortex_session::VortexSession;
16
17use crate::dtype::DType;
18use crate::expr::Expression;
19use crate::expr::display::DisplayTreeExpr;
20use crate::expr::scope::Scope;
21use crate::expr::traversal::TraversalOrder;
22use crate::expr::traversal::pre_order_visit_down;
23use crate::scalar_fn::EmptyOptions;
24use crate::scalar_fn::ExpressionReduceNode;
25use crate::scalar_fn::ReduceNode;
26use crate::scalar_fn::ReduceNodeValidity;
27use crate::scalar_fn::ScalarFnRef;
28use crate::scalar_fn::ScalarFnVTable;
29use crate::scalar_fn::ScalarFnVTableExt;
30use crate::scalar_fn::fns::is_not_null::IsNotNull;
31use crate::stats::rewrite::falsify;
32use crate::stats::rewrite::satisfy;
33
34/// An [`Expression`] that has been type-checked against a [`Scope`].
35///
36/// Every node carries its own dtype, so reading one is a field access rather than a walk of the
37/// subtree. Holding a `BoundExpression` is proof that the whole tree type-checked.
38///
39/// Binding is purely logical: it deals only in [`DType`]s and never sees an array, a length, or an
40/// encoding.
41#[derive(Clone, Debug, PartialEq, Eq, Hash)]
42pub enum BoundExpression {
43    /// A scalar function applied to bound children.
44    Scalar {
45        /// The dtype this node evaluates to.
46        dtype: DType,
47        /// The scalar function for this node.
48        scalar_fn: ScalarFnRef,
49        /// The bound children, in argument order.
50        ///
51        /// Sharing keeps clones cheap even though the iterative [`Drop`] implementation prevents
52        /// consumers from destructuring a `BoundExpression` by value.
53        children: Arc<Vec<BoundExpression>>,
54    },
55    /// The scope itself. Its dtype is the scope's root dtype.
56    Root {
57        /// The dtype this node evaluates to.
58        dtype: DType,
59    },
60}
61
62/// A bound-expression wrapper that compares shared tree identity instead of structure.
63#[derive(Clone, Debug)]
64pub struct ExactBoundExpr(pub BoundExpression);
65
66impl PartialEq for ExactBoundExpr {
67    fn eq(&self, other: &Self) -> bool {
68        match (&self.0, &other.0) {
69            (
70                BoundExpression::Root { dtype: lhs_dtype },
71                BoundExpression::Root { dtype: rhs_dtype },
72            ) => lhs_dtype == rhs_dtype,
73            (
74                BoundExpression::Scalar {
75                    dtype: lhs_dtype,
76                    scalar_fn: lhs_fn,
77                    children: lhs_children,
78                },
79                BoundExpression::Scalar {
80                    dtype: rhs_dtype,
81                    scalar_fn: rhs_fn,
82                    children: rhs_children,
83                },
84            ) => {
85                lhs_fn == rhs_fn
86                    && Arc::ptr_eq(lhs_children, rhs_children)
87                    && lhs_dtype == rhs_dtype
88            }
89            _ => false,
90        }
91    }
92}
93
94impl Eq for ExactBoundExpr {}
95
96impl Hash for ExactBoundExpr {
97    fn hash<H: Hasher>(&self, state: &mut H) {
98        // DType differences are resolved by equality. Omitting the potentially lazy dtype keeps
99        // identity-keyed cache lookups from deserializing an entire schema just to compute a hash.
100        match &self.0 {
101            BoundExpression::Root { .. } => state.write_u8(0),
102            BoundExpression::Scalar {
103                scalar_fn,
104                children,
105                ..
106            } => {
107                state.write_u8(1);
108                scalar_fn.hash(state);
109                Arc::as_ptr(children).hash(state);
110            }
111        }
112    }
113}
114
115impl BoundExpression {
116    /// Create a bound root expression with the given dtype.
117    pub fn new_root(dtype: DType) -> Self {
118        Self::Root { dtype }
119    }
120
121    /// Create a bound scalar node from a scalar function and already-bound children.
122    pub fn try_new(
123        scalar_fn: ScalarFnRef,
124        children: impl IntoIterator<Item = BoundExpression>,
125    ) -> VortexResult<Self> {
126        Self::try_new_vec(scalar_fn, children.into_iter().collect())
127    }
128
129    fn try_new_vec(scalar_fn: ScalarFnRef, children: Vec<BoundExpression>) -> VortexResult<Self> {
130        vortex_ensure!(
131            scalar_fn.signature().arity().matches(children.len()),
132            "Expression arity mismatch: expected {} children but got {}",
133            scalar_fn.signature().arity(),
134            children.len()
135        );
136
137        let arg_dtypes = children
138            .iter()
139            .map(|child| child.dtype().clone())
140            .collect_vec();
141        let dtype = scalar_fn.return_dtype(&arg_dtypes)?;
142
143        Ok(Self::Scalar {
144            dtype,
145            scalar_fn,
146            children: children.into(),
147        })
148    }
149
150    /// Rebuild this node with new bound children, recomputing its dtype.
151    pub fn with_children(
152        self,
153        children: impl IntoIterator<Item = BoundExpression>,
154    ) -> VortexResult<Self> {
155        let children = Vec::from_iter(children);
156        let BoundExpression::Scalar { scalar_fn, .. } = &self else {
157            vortex_ensure!(
158                children.is_empty(),
159                "Root expression cannot have {} children",
160                children.len()
161            );
162            return Ok(self);
163        };
164
165        Self::try_new_vec(scalar_fn.clone(), children)
166    }
167
168    /// The dtype this expression evaluates to.
169    pub fn dtype(&self) -> &DType {
170        match self {
171            Self::Scalar { dtype, .. } | Self::Root { dtype } => dtype,
172        }
173    }
174
175    /// The bound children of this node, in argument order. Empty for [`BoundExpression::Root`].
176    pub fn children(&self) -> &[BoundExpression] {
177        match self {
178            Self::Scalar { children, .. } => children.as_slice(),
179            Self::Root { .. } => &[],
180        }
181    }
182
183    /// Return the child at `index`.
184    pub fn child(&self, index: usize) -> &BoundExpression {
185        &self.children()[index]
186    }
187
188    /// The scalar function for this node, or `None` if it is the scope root.
189    pub fn as_scalar(&self) -> Option<&ScalarFnRef> {
190        match self {
191            Self::Scalar { scalar_fn, .. } => Some(scalar_fn),
192            Self::Root { .. } => None,
193        }
194    }
195
196    /// Return whether this node uses the given scalar-function vtable.
197    pub fn is<V: ScalarFnVTable>(&self) -> bool {
198        self.as_scalar().is_some_and(ScalarFnRef::is::<V>)
199    }
200
201    /// Return whether this expression tree contains a node using the given scalar-function vtable.
202    pub fn contains<V: ScalarFnVTable>(&self) -> VortexResult<bool> {
203        let mut contains = false;
204        pre_order_visit_down(self, |node| {
205            if node.is::<V>() {
206                contains = true;
207                return Ok(TraversalOrder::Stop);
208            }
209            Ok(TraversalOrder::Continue)
210        })?;
211        Ok(contains)
212    }
213
214    /// Return the typed scalar-function options when this node uses the given vtable.
215    pub fn as_opt<V: ScalarFnVTable>(&self) -> Option<&V::Options> {
216        self.as_scalar().and_then(ScalarFnRef::as_opt::<V>)
217    }
218
219    /// Return the typed scalar-function options for this node.
220    ///
221    /// # Panics
222    ///
223    /// Panics when this node is the scope root or uses a different scalar-function vtable.
224    pub fn as_<V: ScalarFnVTable>(&self) -> &V::Options {
225        self.as_opt::<V>()
226            .vortex_expect("Bound expression options type mismatch")
227    }
228
229    /// Whether this node is the scope root.
230    pub fn is_root(&self) -> bool {
231        matches!(self, Self::Root { .. })
232    }
233
234    /// Return whether every scope root in this expression has `dtype`.
235    ///
236    /// Expressions without a scope root, such as literals, match every dtype.
237    pub fn is_root_bound_to(&self, dtype: &DType) -> bool {
238        let mut is_bound_to = true;
239        pre_order_visit_down(self, |node| {
240            if node.is_root() && node.dtype() != dtype {
241                is_bound_to = false;
242                return Ok(TraversalOrder::Stop);
243            }
244            Ok(TraversalOrder::Continue)
245        })
246        .vortex_expect("bound expression traversal cannot not fail");
247        is_bound_to
248    }
249
250    /// Returns a new expression representing the validity mask output of this expression.
251    ///
252    /// Returned expression evaluates to a non-nullable boolean array.
253    /// When scalar function's validity is irreducible, returns
254    /// "is_not_null(self)", which requires evaluating "self".
255    pub fn validity(&self) -> VortexResult<BoundExpression> {
256        match self {
257            // The scope is exactly as valid as itself.
258            Self::Root { .. } => Ok(self.clone()),
259            Self::Scalar { .. } => {
260                let node = ExpressionReduceNode::new(self);
261                Ok(match node.validity()? {
262                    ReduceNodeValidity::Reduced(reduced) => reduced.into_expression(),
263                    // IsNotNull(x) -> x.validity() symbolic reduction rule works only
264                    // when node's.validity is Reduced to avoid infinite recursion.
265                    ReduceNodeValidity::Irreducible => {
266                        IsNotNull.try_new_bound_expr(EmptyOptions, [self.clone()])?
267                    }
268                })
269            }
270        }
271    }
272
273    /// Return an expression that proves this predicate is definitely false from statistics.
274    pub fn falsify(&self, session: &VortexSession) -> VortexResult<Option<BoundExpression>> {
275        falsify(self, session)
276    }
277
278    /// Return an expression that proves this predicate is definitely true from statistics.
279    pub fn satisfy(&self, session: &VortexSession) -> VortexResult<Option<BoundExpression>> {
280        satisfy(self, session)
281    }
282
283    /// Display the bound expression as a formatted tree structure.
284    pub fn display_tree(&self) -> impl Display {
285        DisplayTreeExpr(self)
286    }
287}
288
289impl Display for BoundExpression {
290    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
291        match self {
292            Self::Scalar { scalar_fn, .. } => scalar_fn.fmt_sql(self, f),
293            Self::Root { .. } => f.write_str("$"),
294        }
295    }
296}
297
298impl Expression {
299    /// Bind this expression against a root dtype, type-checking every node in a single walk.
300    ///
301    /// The returned tree carries a dtype on each node, so callers needing types at more than one
302    /// node should bind once and read fields rather than calling
303    /// [`return_dtype`](Expression::return_dtype) repeatedly.
304    pub fn bind(&self, dtype: &DType) -> VortexResult<BoundExpression> {
305        self.bind_scope(&Scope::new(dtype.clone()))
306    }
307
308    /// Bind this expression against an explicit [`Scope`].
309    pub fn bind_scope(&self, scope: &Scope) -> VortexResult<BoundExpression> {
310        if self.is_root() {
311            return Ok(BoundExpression::new_root(scope.root().clone()));
312        }
313
314        let children: Vec<_> = self
315            .children()
316            .iter()
317            .map(|child| child.bind_scope(scope))
318            .try_collect()?;
319        let scalar_fn = self
320            .as_scalar()
321            .vortex_expect("root was handled above, so this is a scalar node");
322        BoundExpression::try_new(scalar_fn.clone(), children)
323    }
324}
325
326/// Iterative drop to avoid stack overflows on deep trees.
327impl Drop for BoundExpression {
328    fn drop(&mut self) {
329        let Self::Scalar { children, .. } = self else {
330            return;
331        };
332        let Some(children) = Arc::get_mut(children) else {
333            return;
334        };
335
336        let mut to_drop = std::mem::take(children);
337        while let Some(mut child) = to_drop.pop() {
338            if let BoundExpression::Scalar { children, .. } = &mut child
339                && let Some(grandchildren) = Arc::get_mut(children)
340            {
341                to_drop.append(grandchildren);
342            }
343        }
344    }
345}
346
347#[cfg(test)]
348mod tests {
349    use vortex_error::VortexResult;
350
351    use super::*;
352    use crate::dtype::Nullability;
353    use crate::dtype::PType;
354    use crate::expr::col;
355    use crate::expr::eq;
356    use crate::expr::lit;
357    use crate::expr::root;
358    use crate::expr::test_harness::struct_dtype;
359    use crate::scalar_fn::fns::literal::Literal;
360
361    fn scope() -> Scope {
362        Scope::new(struct_dtype())
363    }
364
365    #[test]
366    fn root_binds_to_the_scope() -> VortexResult<()> {
367        let bound = root().bind_scope(&scope())?;
368        assert!(bound.is_root());
369        assert_eq!(bound.dtype(), &struct_dtype());
370        assert_eq!(bound, BoundExpression::new_root(struct_dtype()));
371        Ok(())
372    }
373
374    #[test]
375    fn every_node_carries_its_dtype() -> VortexResult<()> {
376        let expr = eq(col("a"), lit(1_i32));
377        let bound = expr.bind_scope(&scope())?;
378
379        assert_eq!(bound.dtype(), &DType::Bool(Nullability::NonNullable));
380
381        let lhs = &bound.children()[0];
382        assert_eq!(
383            lhs.dtype(),
384            &DType::Primitive(PType::I32, Nullability::NonNullable)
385        );
386        assert_eq!(lhs.children()[0].dtype(), &struct_dtype());
387        Ok(())
388    }
389
390    #[test]
391    fn bind_agrees_with_return_dtype() -> VortexResult<()> {
392        for expr in [root(), col("a"), eq(col("a"), lit(1_i32)), lit(true)] {
393            assert_eq!(
394                expr.bind(&struct_dtype())?.dtype(),
395                &expr.return_dtype(&struct_dtype())?,
396                "disagreement for {expr}"
397            );
398        }
399        Ok(())
400    }
401
402    #[test]
403    fn contains_scalar_function() -> VortexResult<()> {
404        let bound = eq(col("a"), lit(1_i32)).bind_scope(&scope())?;
405        assert!(bound.contains::<Literal>()?);
406        assert!(!root().bind_scope(&scope())?.contains::<Literal>()?);
407        Ok(())
408    }
409
410    #[test]
411    fn bound_to_checks_every_root() -> VortexResult<()> {
412        let dtype = struct_dtype();
413        let bound = eq(col("a"), col("a")).bind(&dtype)?;
414        assert!(bound.is_root_bound_to(&dtype));
415        assert!(!bound.is_root_bound_to(&DType::Bool(Nullability::NonNullable)));
416        assert!(
417            lit(true)
418                .bind(&dtype)?
419                .is_root_bound_to(&DType::Bool(Nullability::NonNullable))
420        );
421        Ok(())
422    }
423
424    #[test]
425    fn bound_display_matches_unbound() -> VortexResult<()> {
426        for expr in [root(), col("a"), eq(col("a"), lit(1_i32)), lit(true)] {
427            let bound = expr.bind_scope(&scope())?;
428            assert_eq!(bound.to_string(), expr.to_string());
429            assert_eq!(
430                bound.display_tree().to_string(),
431                expr.display_tree().to_string()
432            );
433        }
434        Ok(())
435    }
436
437    #[test]
438    fn clone_shares_children() -> VortexResult<()> {
439        let bound = eq(col("a"), lit(1_i32)).bind_scope(&scope())?;
440        let cloned = bound.clone();
441
442        let (
443            BoundExpression::Scalar { children: a, .. },
444            BoundExpression::Scalar { children: b, .. },
445        ) = (&bound, &cloned)
446        else {
447            unreachable!("eq is a scalar node")
448        };
449        assert!(Arc::ptr_eq(a, b));
450        Ok(())
451    }
452
453    #[test]
454    fn repeated_subtree_is_bound_per_occurrence() -> VortexResult<()> {
455        let shared = col("a");
456        let bound = eq(shared.clone(), shared).bind_scope(&scope())?;
457        let children = bound.children();
458        assert_eq!(children[0].dtype(), children[1].dtype());
459        Ok(())
460    }
461
462    #[test]
463    fn structural_and_exact_equality_are_distinct() -> VortexResult<()> {
464        let expr = eq(col("a"), lit(1_i32));
465        let bound = expr.bind_scope(&scope())?;
466        let independently_bound = expr.bind_scope(&scope())?;
467
468        assert_eq!(bound, independently_bound);
469        assert_eq!(ExactBoundExpr(bound.clone()), ExactBoundExpr(bound.clone()));
470        assert_ne!(ExactBoundExpr(bound), ExactBoundExpr(independently_bound));
471        Ok(())
472    }
473
474    #[test]
475    fn binding_reports_a_type_error() {
476        let expr = eq(col("a"), lit("nope"));
477        assert!(expr.bind_scope(&scope()).is_err());
478    }
479}