Skip to main content

vortex_array/expr/
expression.rs

1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: Copyright the Vortex contributors
3
4use std::cell::Cell;
5use std::fmt;
6use std::fmt::Debug;
7use std::fmt::Display;
8use std::fmt::Formatter;
9use std::hash::Hash;
10use std::sync::Arc;
11
12use itertools::Itertools;
13use vortex_error::VortexExpect;
14use vortex_error::VortexResult;
15use vortex_error::vortex_ensure;
16
17use crate::dtype::DType;
18use crate::expr::display::DisplayTreeExpr;
19use crate::expr::traversal::TraversalOrder;
20use crate::expr::traversal::pre_order_visit_down;
21use crate::scalar_fn::ScalarFnRef;
22use crate::scalar_fn::ScalarFnVTable;
23
24/// An empty child slice, returned by [`Expression::children`] for childless variants.
25const NO_CHILDREN: &[Expression] = &[];
26
27/// A node in a Vortex expression tree.
28///
29/// Most nodes are a scalar function applied to child expressions. [`Expression::Root`] is the scope
30/// itself: a language primitive rather than a registered function, because its dtype comes from the
31/// scope rather than from children and it is not executable. A [`ScalarFnVTable`] can answer neither
32/// of those, so `Root` is a variant instead.
33#[derive(Clone, Debug, PartialEq, Eq, Hash)]
34pub enum Expression {
35    /// A scalar function applied to child expressions.
36    Scalar {
37        /// The scalar fn for this node.
38        scalar_fn: ScalarFnRef,
39        /// Any children of this expression.
40        children: Arc<Vec<Expression>>,
41    },
42    /// The full scope of the expression evaluation.
43    Root,
44}
45
46impl Expression {
47    /// Create a new expression node from a scalar_fn expression and its children.
48    pub fn try_new(
49        scalar_fn: ScalarFnRef,
50        children: impl IntoIterator<Item = Expression>,
51    ) -> VortexResult<Self> {
52        let children = Vec::from_iter(children);
53
54        vortex_ensure!(
55            scalar_fn.signature().arity().matches(children.len()),
56            "Expression arity mismatch: expected {} children but got {}",
57            scalar_fn.signature().arity(),
58            children.len()
59        );
60
61        Ok(Self::Scalar {
62            scalar_fn,
63            children: children.into(),
64        })
65    }
66
67    /// Whether this expression is the scope root.
68    pub fn is_root(&self) -> bool {
69        matches!(self, Self::Root)
70    }
71
72    /// Returns the scalar fn for this expression, or `None` if it is not a scalar node.
73    pub fn as_scalar(&self) -> Option<&ScalarFnRef> {
74        match self {
75            Self::Scalar { scalar_fn, .. } => Some(scalar_fn),
76            Self::Root => None,
77        }
78    }
79
80    /// Whether this expression's scalar fn is of the given vtable type.
81    pub fn is<V: ScalarFnVTable>(&self) -> bool {
82        self.as_scalar().is_some_and(|sf| sf.is::<V>())
83    }
84
85    /// The typed options for this expression if its scalar fn matches the given vtable type.
86    pub fn as_opt<V: ScalarFnVTable>(&self) -> Option<&V::Options> {
87        self.as_scalar().and_then(|sf| sf.as_opt::<V>())
88    }
89
90    /// The typed options for this expression.
91    ///
92    /// # Panics
93    ///
94    /// Panics if the vtable type does not match.
95    pub fn as_<V: ScalarFnVTable>(&self) -> &V::Options {
96        self.as_opt::<V>()
97            .vortex_expect("Expression options type mismatch")
98    }
99
100    /// Returns the children of this expression.
101    pub fn children(&self) -> &[Expression] {
102        match self {
103            Self::Scalar { children, .. } => children.as_slice(),
104            Self::Root => NO_CHILDREN,
105        }
106    }
107
108    /// Returns the n'th child of this expression.
109    pub fn child(&self, n: usize) -> &Expression {
110        &self.children()[n]
111    }
112
113    /// Replace the children of this expression with the provided new children.
114    pub fn with_children(
115        self,
116        children: impl IntoIterator<Item = Expression>,
117    ) -> VortexResult<Self> {
118        let children = Vec::from_iter(children);
119        match &self {
120            Self::Root => {
121                vortex_ensure!(
122                    children.is_empty(),
123                    "Expression arity mismatch: root expects 0 children but got {}",
124                    children.len()
125                );
126                Ok(Self::Root)
127            }
128            Self::Scalar { scalar_fn, .. } => {
129                vortex_ensure!(
130                    scalar_fn.signature().arity().matches(children.len()),
131                    "Expression arity mismatch: expected {} children but got {}",
132                    scalar_fn.signature().arity(),
133                    children.len()
134                );
135                Ok(Self::Scalar {
136                    scalar_fn: scalar_fn.clone(),
137                    children: children.into(),
138                })
139            }
140        }
141    }
142
143    /// Computes the return dtype of this expression given the input dtype.
144    pub fn return_dtype(&self, scope: &DType) -> VortexResult<DType> {
145        match self {
146            Self::Root => Ok(scope.clone()),
147            Self::Scalar {
148                scalar_fn,
149                children,
150            } => {
151                let dtypes: Vec<_> = children
152                    .iter()
153                    .map(|c| c.return_dtype(scope))
154                    .try_collect()?;
155                scalar_fn.return_dtype(&dtypes)
156            }
157        }
158    }
159
160    /// Format the expression as a compact string.
161    ///
162    /// Since this is a recursive formatter, it is exposed on the public Expression type.
163    /// See fmt_data that is only implemented on the vtable trait.
164    pub fn fmt_sql(&self, f: &mut Formatter<'_>) -> fmt::Result {
165        match self {
166            Self::Root => write!(f, "$"),
167            Self::Scalar { scalar_fn, .. } => scalar_fn.fmt_sql(self, f),
168        }
169    }
170
171    /// Display the expression as a formatted tree structure.
172    ///
173    /// This provides a hierarchical view of the expression that shows the relationships
174    /// between parent and child expressions, making complex nested expressions easier
175    /// to understand and debug.
176    ///
177    /// # Example
178    ///
179    /// ```rust
180    /// # use vortex_array::dtype::{DType, Nullability, PType};
181    /// # use vortex_array::scalar_fn::fns::like::{Like, LikeOptions};
182    /// # use vortex_array::scalar_fn::ScalarFnVTableExt;
183    /// # use vortex_array::expr::{and, cast, eq, get_item, gt, lit, not, root, select};
184    /// // Build a complex nested expression
185    /// let complex_expr = select(
186    ///     ["result"],
187    ///     and(
188    ///         not(eq(get_item("status", root()), lit("inactive"))),
189    ///         and(
190    ///             Like.new_expr(LikeOptions::default(), [get_item("name", root()), lit("%admin%")]),
191    ///             gt(
192    ///                 cast(get_item("score", root()), DType::Primitive(PType::F64, Nullability::NonNullable)),
193    ///                 lit(75.0)
194    ///             )
195    ///         )
196    ///     )
197    /// );
198    ///
199    /// println!("{}", complex_expr.display_tree());
200    /// ```
201    ///
202    /// This produces output like:
203    ///
204    /// ```text
205    /// Select(include): {result}
206    /// └── Binary(and)
207    ///     ├── lhs: Not
208    ///     │   └── Binary(=)
209    ///     │       ├── lhs: GetItem(status)
210    ///     │       │   └── Root
211    ///     │       └── rhs: Literal(value: "inactive", dtype: utf8)
212    ///     └── rhs: Binary(and)
213    ///         ├── lhs: Like
214    ///         │   ├── child: GetItem(name)
215    ///         │   │   └── Root
216    ///         │   └── pattern: Literal(value: "%admin%", dtype: utf8)
217    ///         └── rhs: Binary(>)
218    ///             ├── lhs: Cast(target: f64)
219    ///             │   └── GetItem(score)
220    ///             │       └── Root
221    ///             └── rhs: Literal(value: 75f64, dtype: f64)
222    /// ```
223    pub fn display_tree(&self) -> impl Display {
224        DisplayTreeExpr(self)
225    }
226
227    /// Returns true if this expression contains expression E inside.
228    ///
229    /// # Example
230    ///
231    /// ```rust
232    /// # use vortex_array::scalar_fn::fns::literal::Literal;
233    /// # use vortex_array::expr::{eq, lit, root};
234    /// let expression = &eq(root(), lit(3u64));
235    /// assert!(expression.contains::<Literal>().unwrap());
236    /// let expression = root();
237    /// assert!(!expression.contains::<Literal>().unwrap());
238    /// ```
239    pub fn contains<E: ScalarFnVTable>(&self) -> VortexResult<bool> {
240        let mut contains = false;
241        pre_order_visit_down(self, |node| {
242            if node.is::<E>() {
243                contains = true;
244                return Ok(TraversalOrder::Stop);
245            }
246            Ok(TraversalOrder::Continue)
247        })?;
248        Ok(contains)
249    }
250}
251
252/// The default display implementation for expressions uses the 'SQL'-style format.
253impl Display for Expression {
254    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
255        self.fmt_sql(f)
256    }
257}
258
259// Switch to iterative cleanup after this many recursive drops.
260const MAX_DROP_DEPTH: u32 = 32;
261
262thread_local! {
263    static DROP_DEPTH: Cell<u32> = const { Cell::new(0) };
264}
265
266// Increments the thread-local drop depth until the guard is dropped.
267struct DropDepthGuard;
268
269impl DropDepthGuard {
270    fn enter() -> Option<Self> {
271        DROP_DEPTH.with(|depth| {
272            let current = depth.get();
273            (current < MAX_DROP_DEPTH).then(|| {
274                depth.set(current + 1);
275                Self
276            })
277        })
278    }
279}
280
281impl Drop for DropDepthGuard {
282    fn drop(&mut self) {
283        DROP_DEPTH.with(|depth| depth.set(depth.get() - 1));
284    }
285}
286
287impl Drop for Expression {
288    fn drop(&mut self) {
289        let Self::Scalar { children, .. } = self else {
290            return;
291        };
292        let Some(children) = Arc::get_mut(children) else {
293            return;
294        };
295        if children.is_empty() {
296            return;
297        }
298
299        let mut children_to_drop = std::mem::take(children);
300
301        match DropDepthGuard::enter() {
302            Some(_guard) => drop(children_to_drop),
303            None => {
304                while let Some(mut child) = children_to_drop.pop() {
305                    if let Self::Scalar { children, .. } = &mut child
306                        && let Some(expr_children) = Arc::get_mut(children)
307                    {
308                        children_to_drop.append(expr_children);
309                    }
310                }
311            }
312        }
313    }
314}
315
316#[cfg(test)]
317mod tests {
318    use std::thread;
319
320    use super::*;
321    use crate::expr::lit;
322    use crate::expr::not;
323
324    fn deep_expression(depth: usize) -> Expression {
325        let mut expr = lit(true);
326        for _ in 0..depth {
327            expr = not(expr);
328        }
329        expr
330    }
331
332    #[test]
333    fn deep_expression_drops_within_a_small_stack() -> VortexResult<()> {
334        const DEPTH: usize = 100_000;
335        const STACK_SIZE: usize = 256 * 1024;
336
337        let dropper = thread::Builder::new()
338            .stack_size(STACK_SIZE)
339            .spawn(|| drop(deep_expression(DEPTH)))?;
340
341        assert!(
342            dropper.join().is_ok(),
343            "dropping a tree of depth {DEPTH} exhausted a {STACK_SIZE} byte stack"
344        );
345
346        Ok(())
347    }
348
349    #[test]
350    fn shallow_expression_keeps_shared_children() {
351        let expr = not(lit(true));
352        let shared = expr.clone();
353
354        drop(expr);
355
356        assert_eq!(shared.children().len(), 1);
357    }
358}