1use vortex_error::VortexResult;
5
6use crate::expr::BoundExpression;
7use crate::scalar_fn::ExpressionReduceNode;
8
9impl BoundExpression {
10 pub fn optimize(&self) -> VortexResult<BoundExpression> {
16 Ok(self.try_optimize()?.unwrap_or_else(|| self.clone()))
17 }
18
19 fn simplify_node(&self) -> VortexResult<Option<BoundExpression>> {
23 match self {
24 BoundExpression::Scalar { scalar_fn, .. } => scalar_fn.simplify(self),
25 BoundExpression::Root { .. } => Ok(None),
26 }
27 }
28
29 fn reduce_node<'a>(
31 &self,
32 node: &ExpressionReduceNode<'a>,
33 ) -> VortexResult<Option<ExpressionReduceNode<'a>>> {
34 match self {
35 BoundExpression::Scalar { scalar_fn, .. } => scalar_fn.reduce_expression(node),
36 BoundExpression::Root { .. } => Ok(None),
37 }
38 }
39
40 fn try_optimize(&self) -> VortexResult<Option<BoundExpression>> {
42 let mut current: Option<BoundExpression> = None;
45 let mut loop_counter = 0;
46
47 loop {
48 if loop_counter > 100 {
49 vortex_error::vortex_bail!(
50 "Exceeded maximum optimization iterations (possible infinite loop)"
51 );
52 }
53 loop_counter += 1;
54
55 let expr = current.as_ref().unwrap_or(self);
56 let mut changed = false;
57
58 if let Some(simplified) = expr.simplify_node()? {
59 current = Some(simplified);
60 changed = true;
61 }
62
63 let reduced = {
66 let expr = current.as_ref().unwrap_or(self);
67 expr.reduce_node(&ExpressionReduceNode::new(expr))?
68 .map(ExpressionReduceNode::into_expression)
69 };
70 if let Some(reduced_expr) = reduced {
71 current = Some(reduced_expr);
72 changed = true;
73 }
74
75 if !changed {
76 break;
77 }
78 }
79
80 Ok(current)
81 }
82
83 pub fn optimize_recursive(&self) -> VortexResult<BoundExpression> {
87 Ok(self
88 .try_optimize_recursive()?
89 .unwrap_or_else(|| self.clone()))
90 }
91
92 pub fn try_optimize_recursive(&self) -> VortexResult<Option<BoundExpression>> {
94 let mut current = self.try_optimize()?;
96
97 let expr = current.as_ref().unwrap_or(self);
100 let children = expr.children();
101 let mut new_children: Option<Vec<BoundExpression>> = None;
102 for (idx, child) in children.iter().enumerate() {
103 if let Some(optimized) = child.try_optimize_recursive()? {
104 new_children
105 .get_or_insert_with(|| children[..idx].to_vec())
106 .push(optimized);
107 } else if let Some(new_children) = new_children.as_mut() {
108 new_children.push(child.clone());
109 }
110 }
111
112 if let Some(new_children) = new_children {
113 let updated = expr.clone().with_children(new_children)?;
114
115 current = Some(updated.try_optimize()?.unwrap_or(updated));
117 }
118
119 Ok(current)
120 }
121}
122
123#[cfg(test)]
124mod tests {
125 use rstest::rstest;
126 use vortex_buffer::buffer;
127 use vortex_error::VortexResult;
128 use vortex_error::vortex_err;
129
130 use crate::IntoArray;
131 use crate::VortexSessionExecute;
132 use crate::array_session;
133 use crate::arrays::BoolArray;
134 use crate::arrays::StructArray;
135 use crate::assert_arrays_eq;
136 use crate::dtype::DType;
137 use crate::dtype::FieldName;
138 use crate::dtype::Nullability;
139 use crate::dtype::PType;
140 use crate::dtype::StructFields;
141 use crate::expr::Expression;
142 use crate::expr::and;
143 use crate::expr::cast;
144 use crate::expr::eq;
145 use crate::expr::get_item;
146 use crate::expr::lit;
147 use crate::expr::lt_eq;
148 use crate::expr::merge;
149 use crate::expr::or;
150 use crate::expr::pack;
151 use crate::expr::root;
152 use crate::expr::select;
153 use crate::expr::transform::replace;
154 use crate::expr::zip_expr;
155 use crate::scalar::Scalar;
156 use crate::scalar_fn::fns::literal::Literal;
157
158 #[test]
159 fn optimize_or_chain_correctness() -> VortexResult<()> {
160 let expr = or(
161 eq(get_item("x", root()), lit(1i32)),
162 eq(get_item("x", root()), lit(2i32)),
163 );
164 let scope = DType::Struct(
165 StructFields::new(
166 ["x"].into(),
167 vec![DType::Primitive(PType::I32, Nullability::NonNullable)],
168 ),
169 Nullability::NonNullable,
170 );
171 let optimized = expr.bind(&scope)?.optimize_recursive()?;
172
173 let data =
174 StructArray::from_fields(&[("x", buffer![0i32, 1, 2, 3].into_array())])?.into_array();
175 let actual = data.apply_bound(&optimized)?;
176 assert_arrays_eq!(
177 actual,
178 BoolArray::from_iter([false, true, true, false]),
179 &mut array_session().create_execution_ctx()
180 );
181 Ok(())
182 }
183
184 #[test]
185 fn optimize_folds_cast_of_literal_in_comparison() -> VortexResult<()> {
186 let expr = lt_eq(
187 get_item("x", root()),
188 cast(
189 lit(3i32),
190 DType::Primitive(PType::F64, Nullability::NonNullable),
191 ),
192 );
193 let scope = DType::Struct(
194 StructFields::new(
195 ["x"].into(),
196 vec![DType::Primitive(PType::F64, Nullability::NonNullable)],
197 ),
198 Nullability::NonNullable,
199 );
200 let optimized = expr.bind(&scope)?.optimize_recursive()?;
201
202 let rhs = optimized
205 .child(1)
206 .as_opt::<Literal>()
207 .ok_or_else(|| vortex_err!("expected a bare literal RHS, got {optimized}"))?;
208 assert_eq!(rhs, &Scalar::primitive(3.0f64, Nullability::NonNullable));
209 Ok(())
210 }
211
212 #[rstest]
213 #[case::and_annihilator(and(root(), lit(false)), DType::Bool(Nullability::Nullable))]
214 #[case::or_annihilator(or(root(), lit(true)), DType::Bool(Nullability::Nullable))]
215 #[case::and_nullable_identity(
216 and(root(), lit(Scalar::from(Some(true)))),
217 DType::Bool(Nullability::NonNullable)
218 )]
219 #[case::or_nullable_identity(
220 or(root(), lit(Scalar::from(Some(false)))),
221 DType::Bool(Nullability::NonNullable)
222 )]
223 #[case::zip_true(
224 zip_expr(lit(true), lit(1i32), root()),
225 DType::Primitive(PType::I32, Nullability::Nullable)
226 )]
227 #[case::zip_false(
228 zip_expr(lit(false), root(), lit(1i32)),
229 DType::Primitive(PType::I32, Nullability::Nullable)
230 )]
231 fn literal_simplification_preserves_result_dtype(
232 #[case] expr: Expression,
233 #[case] scope: DType,
234 ) -> VortexResult<()> {
235 let original = expr.bind(&scope)?;
236 let optimized = expr.bind(&scope)?.optimize_recursive()?;
237 assert_eq!(optimized.dtype(), original.dtype());
238
239 Ok(())
240 }
241
242 #[test]
246 fn optimize_select_of_pack_reads_pack_fields() -> VortexResult<()> {
247 let field_names = (0..4)
248 .map(|idx| FieldName::from(format!("field_{idx}")))
249 .collect::<Vec<_>>();
250 let fields = StructFields::new(
251 field_names.clone().into(),
252 vec![DType::Primitive(PType::U64, Nullability::NonNullable); field_names.len()],
253 );
254 let scope = DType::Struct(fields, Nullability::NonNullable);
255
256 let expr = select(
257 field_names
258 .iter()
259 .cloned()
260 .chain(["input_row_idx".into(), "input_file_idx".into()])
261 .collect::<Vec<_>>(),
262 merge([
263 root(),
264 pack(
265 [
266 ("input_row_idx", lit(0_u64)),
267 ("input_file_idx", lit(1_u64)),
268 ],
269 Nullability::NonNullable,
270 ),
271 ]),
272 );
273 let root_pack = pack(
274 field_names
275 .iter()
276 .map(|name| (name.clone(), get_item(name.clone(), root())))
277 .collect::<Vec<_>>(),
278 Nullability::NonNullable,
279 );
280 let expr = replace(expr, &root(), root_pack);
281
282 let expected = pack(
283 field_names
284 .into_iter()
285 .map(|name| (name.clone(), get_item(name, root())))
286 .chain([
287 ("input_row_idx".into(), lit(0_u64)),
288 ("input_file_idx".into(), lit(1_u64)),
289 ])
290 .collect::<Vec<_>>(),
291 Nullability::NonNullable,
292 );
293
294 let expected = expected.bind(&scope)?;
295
296 let optimized = expr.bind(&scope)?.optimize_recursive()?;
297 assert_eq!(optimized, expected, "got {optimized}");
298 assert_eq!(optimized.optimize_recursive()?, expected);
299
300 Ok(())
301 }
302
303 #[test]
306 fn optimize_merge_of_packs_reads_pack_fields() -> VortexResult<()> {
307 let scope = DType::Struct(StructFields::empty(), Nullability::NonNullable);
308 let expr = merge([
309 pack([("a", lit(0_u64))], Nullability::NonNullable),
310 pack([("b", lit(1_u64))], Nullability::NonNullable),
311 ]);
312
313 let expected = pack(
314 [("a", lit(0_u64)), ("b", lit(1_u64))],
315 Nullability::NonNullable,
316 );
317
318 let expected = expected.bind(&scope)?;
319
320 let optimized = expr.bind(&scope)?.optimize_recursive()?;
321 assert_eq!(optimized, expected, "got {optimized}");
322 assert_eq!(optimized.optimize_recursive()?, expected);
323
324 Ok(())
325 }
326}