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::ScalarFnRef;
24use crate::scalar_fn::ScalarFnVTable;
25use crate::scalar_fn::fns::root::Root;
26use crate::stats::rewrite::StatsRewriteCtx;
27
28/// An [`Expression`] that has been type-checked against a [`Scope`].
29///
30/// Every node carries its own dtype, so reading one is a field access rather than a walk of the
31/// subtree. Holding a `BoundExpression` is proof that the whole tree type-checked.
32///
33/// Binding is purely logical: it deals only in [`DType`]s and never sees an array, a length, or an
34/// encoding.
35#[derive(Clone, Debug, PartialEq, Eq, Hash)]
36pub struct BoundExpression {
37    kind: BoundKind,
38    dtype: DType,
39}
40
41/// The per-variant contents of a [`BoundExpression`], mirroring the logical variants of
42/// [`Expression`].
43#[derive(Clone, Debug, PartialEq, Eq, Hash)]
44pub enum BoundKind {
45    /// A scalar function applied to bound children.
46    Scalar {
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}
58
59/// A bound-expression wrapper that compares shared tree identity instead of structure.
60#[derive(Clone, Debug)]
61pub struct ExactBoundExpr(pub BoundExpression);
62
63impl PartialEq for ExactBoundExpr {
64    fn eq(&self, other: &Self) -> bool {
65        match (&self.0.kind, &other.0.kind) {
66            (BoundKind::Root, BoundKind::Root) => self.0.dtype == other.0.dtype,
67            (
68                BoundKind::Scalar {
69                    scalar_fn: lhs_fn,
70                    children: lhs_children,
71                },
72                BoundKind::Scalar {
73                    scalar_fn: rhs_fn,
74                    children: rhs_children,
75                },
76            ) => {
77                lhs_fn == rhs_fn
78                    && Arc::ptr_eq(lhs_children, rhs_children)
79                    && self.0.dtype == other.0.dtype
80            }
81            _ => false,
82        }
83    }
84}
85
86impl Eq for ExactBoundExpr {}
87
88impl Hash for ExactBoundExpr {
89    fn hash<H: Hasher>(&self, state: &mut H) {
90        // DType differences are resolved by equality. Omitting the potentially lazy dtype keeps
91        // identity-keyed cache lookups from deserializing an entire schema just to compute a hash.
92        match &self.0.kind {
93            BoundKind::Root => state.write_u8(0),
94            BoundKind::Scalar {
95                scalar_fn,
96                children,
97            } => {
98                state.write_u8(1);
99                scalar_fn.hash(state);
100                Arc::as_ptr(children).hash(state);
101            }
102        }
103    }
104}
105
106impl BoundExpression {
107    /// Create a bound root expression with the given dtype.
108    pub fn new_root(dtype: DType) -> Self {
109        Self {
110            kind: BoundKind::Root,
111            dtype,
112        }
113    }
114
115    /// Create a bound scalar node from a scalar function and already-bound children.
116    pub fn try_new(
117        scalar_fn: ScalarFnRef,
118        children: impl IntoIterator<Item = BoundExpression>,
119    ) -> VortexResult<Self> {
120        let children = Vec::from_iter(children);
121        vortex_ensure!(
122            scalar_fn.signature().arity().matches(children.len()),
123            "Expression arity mismatch: expected {} children but got {}",
124            scalar_fn.signature().arity(),
125            children.len()
126        );
127
128        let arg_dtypes = children
129            .iter()
130            .map(|child| child.dtype().clone())
131            .collect_vec();
132        let dtype = scalar_fn.return_dtype(&arg_dtypes)?;
133
134        Ok(Self {
135            kind: BoundKind::Scalar {
136                scalar_fn,
137                children: children.into(),
138            },
139            dtype,
140        })
141    }
142
143    /// Rebuild this node with new bound children, recomputing its dtype.
144    pub fn with_children(
145        self,
146        children: impl IntoIterator<Item = BoundExpression>,
147    ) -> VortexResult<Self> {
148        let children = Vec::from_iter(children);
149        let BoundKind::Scalar { scalar_fn, .. } = &self.kind else {
150            vortex_ensure!(
151                children.is_empty(),
152                "Root expression cannot have {} children",
153                children.len()
154            );
155            return Ok(self);
156        };
157
158        Self::try_new(scalar_fn.clone(), children)
159    }
160
161    /// The dtype this expression evaluates to.
162    pub fn dtype(&self) -> &DType {
163        &self.dtype
164    }
165
166    /// The per-variant contents of this node.
167    pub fn kind(&self) -> &BoundKind {
168        &self.kind
169    }
170
171    /// The bound children of this node, in argument order. Empty for [`BoundKind::Root`].
172    pub fn children(&self) -> &[BoundExpression] {
173        match &self.kind {
174            BoundKind::Scalar { children, .. } => children.as_slice(),
175            BoundKind::Root => &[],
176        }
177    }
178
179    /// Return the child at `index`.
180    pub fn child(&self, index: usize) -> &BoundExpression {
181        &self.children()[index]
182    }
183
184    /// The scalar function for this node, or `None` if it is the scope root.
185    pub fn as_scalar(&self) -> Option<&ScalarFnRef> {
186        match &self.kind {
187            BoundKind::Scalar { scalar_fn, .. } => Some(scalar_fn),
188            BoundKind::Root => None,
189        }
190    }
191
192    /// Return whether this node uses the given scalar-function vtable.
193    pub fn is<V: ScalarFnVTable>(&self) -> bool {
194        self.as_scalar().is_some_and(ScalarFnRef::is::<V>)
195    }
196
197    /// Return whether this expression tree contains a node using the given scalar-function vtable.
198    pub fn contains<V: ScalarFnVTable>(&self) -> VortexResult<bool> {
199        let mut contains = false;
200        pre_order_visit_down(self, |node| {
201            if node.is::<V>() {
202                contains = true;
203                return Ok(TraversalOrder::Stop);
204            }
205            Ok(TraversalOrder::Continue)
206        })?;
207        Ok(contains)
208    }
209
210    /// Return the typed scalar-function options when this node uses the given vtable.
211    pub fn as_opt<V: ScalarFnVTable>(&self) -> Option<&V::Options> {
212        self.as_scalar().and_then(ScalarFnRef::as_opt::<V>)
213    }
214
215    /// Return the typed scalar-function options for this node.
216    ///
217    /// # Panics
218    ///
219    /// Panics when this node is the scope root or uses a different scalar-function vtable.
220    pub fn as_<V: ScalarFnVTable>(&self) -> &V::Options {
221        self.as_opt::<V>()
222            .vortex_expect("Bound expression options type mismatch")
223    }
224
225    /// Whether this node is the scope root.
226    pub fn is_root(&self) -> bool {
227        matches!(self.kind, BoundKind::Root)
228    }
229
230    /// Return an expression that proves this predicate is definitely false from statistics.
231    pub fn falsify(&self, session: &VortexSession) -> VortexResult<Option<BoundExpression>> {
232        StatsRewriteCtx::new(session).falsify(self)
233    }
234
235    /// Return an expression that proves this predicate is definitely true from statistics.
236    pub fn satisfy(&self, session: &VortexSession) -> VortexResult<Option<BoundExpression>> {
237        StatsRewriteCtx::new(session).satisfy(self)
238    }
239
240    /// Display the bound expression as a formatted tree structure.
241    pub fn display_tree(&self) -> impl Display {
242        DisplayTreeExpr(self)
243    }
244}
245
246impl Display for BoundExpression {
247    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
248        match self.kind() {
249            BoundKind::Scalar { scalar_fn, .. } => scalar_fn.fmt_sql(self, f),
250            BoundKind::Root => f.write_str("$"),
251        }
252    }
253}
254
255impl Expression {
256    /// Bind this expression against a root dtype, type-checking every node in a single walk.
257    ///
258    /// The returned tree carries a dtype on each node, so callers needing types at more than one
259    /// node should bind once and read fields rather than calling
260    /// [`return_dtype`](Expression::return_dtype) repeatedly.
261    pub fn bind(&self, dtype: &DType) -> VortexResult<BoundExpression> {
262        self.bind_scope(&Scope::new(dtype.clone()))
263    }
264
265    /// Bind this expression against an explicit [`Scope`].
266    pub fn bind_scope(&self, scope: &Scope) -> VortexResult<BoundExpression> {
267        if self.is::<Root>() {
268            return Ok(BoundExpression::new_root(scope.root().clone()));
269        }
270
271        let children: Vec<_> = self
272            .children()
273            .iter()
274            .map(|child| child.bind_scope(scope))
275            .try_collect()?;
276        BoundExpression::try_new(self.scalar_fn().clone(), children)
277    }
278}
279
280/// Iterative drop to avoid stack overflows on deep trees.
281impl Drop for BoundExpression {
282    fn drop(&mut self) {
283        let BoundKind::Scalar { children, .. } = &mut self.kind else {
284            return;
285        };
286        let Some(children) = Arc::get_mut(children) else {
287            return;
288        };
289
290        let mut to_drop = std::mem::take(children);
291        while let Some(mut child) = to_drop.pop() {
292            if let BoundKind::Scalar { children, .. } = &mut child.kind
293                && let Some(grandchildren) = Arc::get_mut(children)
294            {
295                to_drop.append(grandchildren);
296            }
297        }
298    }
299}
300
301#[cfg(test)]
302mod tests {
303    use vortex_error::VortexResult;
304
305    use super::*;
306    use crate::dtype::Nullability;
307    use crate::dtype::PType;
308    use crate::expr::col;
309    use crate::expr::eq;
310    use crate::expr::lit;
311    use crate::expr::root;
312    use crate::expr::test_harness::struct_dtype;
313    use crate::scalar_fn::fns::literal::Literal;
314
315    fn scope() -> Scope {
316        Scope::new(struct_dtype())
317    }
318
319    #[test]
320    fn root_binds_to_the_scope() -> VortexResult<()> {
321        let bound = root().bind_scope(&scope())?;
322        assert!(bound.is_root());
323        assert_eq!(bound.dtype(), &struct_dtype());
324        assert_eq!(bound, BoundExpression::new_root(struct_dtype()));
325        Ok(())
326    }
327
328    #[test]
329    fn every_node_carries_its_dtype() -> VortexResult<()> {
330        let expr = eq(col("a"), lit(1_i32));
331        let bound = expr.bind_scope(&scope())?;
332
333        assert_eq!(bound.dtype(), &DType::Bool(Nullability::NonNullable));
334
335        let lhs = &bound.children()[0];
336        assert_eq!(
337            lhs.dtype(),
338            &DType::Primitive(PType::I32, Nullability::NonNullable)
339        );
340        assert_eq!(lhs.children()[0].dtype(), &struct_dtype());
341        Ok(())
342    }
343
344    #[test]
345    fn bind_agrees_with_return_dtype() -> VortexResult<()> {
346        for expr in [root(), col("a"), eq(col("a"), lit(1_i32)), lit(true)] {
347            assert_eq!(
348                expr.bind(&struct_dtype())?.dtype(),
349                &expr.return_dtype(&struct_dtype())?,
350                "disagreement for {expr}"
351            );
352        }
353        Ok(())
354    }
355
356    #[test]
357    fn contains_scalar_function() -> VortexResult<()> {
358        let bound = eq(col("a"), lit(1_i32)).bind_scope(&scope())?;
359        assert!(bound.contains::<Literal>()?);
360        assert!(!root().bind_scope(&scope())?.contains::<Literal>()?);
361        Ok(())
362    }
363
364    #[test]
365    fn bound_display_matches_unbound() -> VortexResult<()> {
366        for expr in [root(), col("a"), eq(col("a"), lit(1_i32)), lit(true)] {
367            let bound = expr.bind_scope(&scope())?;
368            assert_eq!(bound.to_string(), expr.to_string());
369            assert_eq!(
370                bound.display_tree().to_string(),
371                expr.display_tree().to_string()
372            );
373        }
374        Ok(())
375    }
376
377    #[test]
378    fn clone_shares_children() -> VortexResult<()> {
379        let bound = eq(col("a"), lit(1_i32)).bind_scope(&scope())?;
380        let cloned = bound.clone();
381
382        let (BoundKind::Scalar { children: a, .. }, BoundKind::Scalar { children: b, .. }) =
383            (bound.kind(), cloned.kind())
384        else {
385            unreachable!("eq is a scalar node")
386        };
387        assert!(Arc::ptr_eq(a, b));
388        Ok(())
389    }
390
391    #[test]
392    fn repeated_subtree_is_bound_per_occurrence() -> VortexResult<()> {
393        let shared = col("a");
394        let bound = eq(shared.clone(), shared).bind_scope(&scope())?;
395        let children = bound.children();
396        assert_eq!(children[0].dtype(), children[1].dtype());
397        Ok(())
398    }
399
400    #[test]
401    fn structural_and_exact_equality_are_distinct() -> VortexResult<()> {
402        let expr = eq(col("a"), lit(1_i32));
403        let bound = expr.bind_scope(&scope())?;
404        let independently_bound = expr.bind_scope(&scope())?;
405
406        assert_eq!(bound, independently_bound);
407        assert_eq!(ExactBoundExpr(bound.clone()), ExactBoundExpr(bound.clone()));
408        assert_ne!(ExactBoundExpr(bound), ExactBoundExpr(independently_bound));
409        Ok(())
410    }
411
412    #[test]
413    fn binding_reports_a_type_error() {
414        let expr = eq(col("a"), lit("nope"));
415        assert!(expr.bind_scope(&scope()).is_err());
416    }
417}