vortex_array/expr/
optimize.rs1use vortex_error::VortexResult;
5
6use crate::expr::BoundExpression;
7use crate::expr::transform::match_between::find_between;
8use crate::scalar_fn::ExpressionReduceNode;
9
10impl BoundExpression {
11 pub fn optimize(&self) -> VortexResult<BoundExpression> {
17 Ok(self.try_optimize()?.unwrap_or_else(|| self.clone()))
18 }
19
20 fn simplify_node(&self) -> VortexResult<Option<BoundExpression>> {
24 match self {
25 BoundExpression::Scalar { scalar_fn, .. } => scalar_fn.simplify(self),
26 BoundExpression::Root { .. } => Ok(None),
27 }
28 }
29
30 fn reduce_node<'a>(
32 &self,
33 node: &ExpressionReduceNode<'a>,
34 ) -> VortexResult<Option<ExpressionReduceNode<'a>>> {
35 match self {
36 BoundExpression::Scalar { scalar_fn, .. } => scalar_fn.reduce_expression(node),
37 BoundExpression::Root { .. } => Ok(None),
38 }
39 }
40
41 fn try_optimize(&self) -> VortexResult<Option<BoundExpression>> {
43 let mut current: Option<BoundExpression> = None;
46 let mut loop_counter = 0;
47
48 loop {
49 if loop_counter > 100 {
50 vortex_error::vortex_bail!(
51 "Exceeded maximum optimization iterations (possible infinite loop)"
52 );
53 }
54 loop_counter += 1;
55
56 let expr = current.as_ref().unwrap_or(self);
57 let mut changed = false;
58
59 if let Some(simplified) = expr.simplify_node()? {
60 current = Some(simplified);
61 changed = true;
62 }
63
64 let reduced = {
67 let expr = current.as_ref().unwrap_or(self);
68 expr.reduce_node(&ExpressionReduceNode::new(expr))?
69 .map(ExpressionReduceNode::into_expression)
70 };
71 if let Some(reduced_expr) = reduced {
72 current = Some(reduced_expr);
73 changed = true;
74 }
75
76 if !changed {
77 break;
78 }
79 }
80
81 Ok(current)
82 }
83
84 pub fn optimize_recursive(&self) -> VortexResult<BoundExpression> {
88 Ok(self
89 .try_optimize_recursive()?
90 .unwrap_or_else(|| self.clone()))
91 }
92
93 pub fn try_optimize_recursive(&self) -> VortexResult<Option<BoundExpression>> {
95 let result = self.try_optimize_recursive_inner()?;
96
97 Ok(Some(find_between(result.unwrap_or_else(|| self.clone()))))
101 }
102
103 fn try_optimize_recursive_inner(&self) -> VortexResult<Option<BoundExpression>> {
104 let mut current = self.try_optimize()?;
106
107 let expr = current.as_ref().unwrap_or(self);
110 let children = expr.children();
111 let mut new_children: Option<Vec<BoundExpression>> = None;
112 for (idx, child) in children.iter().enumerate() {
113 if let Some(optimized) = child.try_optimize_recursive_inner()? {
114 new_children
115 .get_or_insert_with(|| children[..idx].to_vec())
116 .push(optimized);
117 } else if let Some(new_children) = new_children.as_mut() {
118 new_children.push(child.clone());
119 }
120 }
121
122 if let Some(new_children) = new_children {
123 let updated = expr.clone().with_children(new_children)?;
124
125 current = Some(updated.try_optimize()?.unwrap_or(updated));
127 }
128
129 Ok(current)
130 }
131}
132
133#[cfg(test)]
134mod tests {
135 use rstest::rstest;
136 use vortex_error::VortexResult;
137 use vortex_error::vortex_err;
138
139 use crate::dtype::DType;
140 use crate::dtype::Nullability;
141 use crate::dtype::PType;
142 use crate::dtype::StructFields;
143 use crate::expr::Expression;
144 use crate::expr::and;
145 use crate::expr::cast;
146 use crate::expr::eq;
147 use crate::expr::get_item;
148 use crate::expr::lit;
149 use crate::expr::lt_eq;
150 use crate::expr::or;
151 use crate::expr::root;
152 use crate::expr::zip_expr;
153 use crate::scalar::Scalar;
154 use crate::scalar_fn::fns::literal::Literal;
155
156 #[test]
157 fn optimize_or_chain_correctness() -> VortexResult<()> {
158 let expr = or(
159 eq(get_item("x", root()), lit(1i32)),
160 eq(get_item("x", root()), lit(2i32)),
161 );
162 let scope = DType::Struct(
163 StructFields::new(
164 ["x"].into(),
165 vec![DType::Primitive(PType::I32, Nullability::NonNullable)],
166 ),
167 Nullability::NonNullable,
168 );
169 let optimized = expr.bind(&scope)?.optimize_recursive()?;
170
171 let s = optimized.to_string();
172 assert!(s.contains("$.x"), "expected $.x in {s}");
173 assert!(s.contains("1i32") || s.contains('1'), "expected 1 in {s}");
174 assert!(s.contains("2i32") || s.contains('2'), "expected 2 in {s}");
175 Ok(())
176 }
177
178 #[test]
179 fn optimize_folds_cast_of_literal_in_comparison() -> VortexResult<()> {
180 let expr = lt_eq(
181 get_item("x", root()),
182 cast(
183 lit(3i32),
184 DType::Primitive(PType::F64, Nullability::NonNullable),
185 ),
186 );
187 let scope = DType::Struct(
188 StructFields::new(
189 ["x"].into(),
190 vec![DType::Primitive(PType::F64, Nullability::NonNullable)],
191 ),
192 Nullability::NonNullable,
193 );
194 let optimized = expr.bind(&scope)?.optimize_recursive()?;
195
196 let rhs = optimized
199 .child(1)
200 .as_opt::<Literal>()
201 .ok_or_else(|| vortex_err!("expected a bare literal RHS, got {optimized}"))?;
202 assert_eq!(rhs, &Scalar::primitive(3.0f64, Nullability::NonNullable));
203 Ok(())
204 }
205
206 #[rstest]
207 #[case::and_annihilator(and(root(), lit(false)), DType::Bool(Nullability::Nullable))]
208 #[case::or_annihilator(or(root(), lit(true)), DType::Bool(Nullability::Nullable))]
209 #[case::and_nullable_identity(
210 and(root(), lit(Scalar::from(Some(true)))),
211 DType::Bool(Nullability::NonNullable)
212 )]
213 #[case::or_nullable_identity(
214 or(root(), lit(Scalar::from(Some(false)))),
215 DType::Bool(Nullability::NonNullable)
216 )]
217 #[case::zip_true(
218 zip_expr(lit(true), lit(1i32), root()),
219 DType::Primitive(PType::I32, Nullability::Nullable)
220 )]
221 #[case::zip_false(
222 zip_expr(lit(false), root(), lit(1i32)),
223 DType::Primitive(PType::I32, Nullability::Nullable)
224 )]
225 fn literal_simplification_preserves_result_dtype(
226 #[case] expr: Expression,
227 #[case] scope: DType,
228 ) -> VortexResult<()> {
229 let original = expr.bind(&scope)?;
230 let optimized = expr.bind(&scope)?.optimize_recursive()?;
231 assert_eq!(optimized.dtype(), original.dtype());
232 Ok(())
233 }
234}