use std::cell::RefCell;
use itertools::Itertools;
use vortex_error::VortexResult;
use vortex_error::vortex_err;
use vortex_utils::aliases::hash_map::HashMap;
use crate::dtype::DType;
use crate::expr::Expression;
use crate::expr::transform::match_between::find_between;
use crate::scalar_fn::ExpressionReduceNode;
use crate::scalar_fn::SimplifyCtx;
impl Expression {
pub fn optimize(&self, scope: &DType) -> VortexResult<Expression> {
let cache = SimplifyCache::new(scope);
Ok(self.try_optimize(&cache)?.unwrap_or_else(|| self.clone()))
}
fn simplify_untyped_node(&self) -> VortexResult<Option<Expression>> {
match self {
Expression::Scalar { scalar_fn, .. } => scalar_fn.simplify_untyped(self),
Expression::Root => Ok(None),
}
}
fn simplify_node(&self, ctx: &dyn SimplifyCtx) -> VortexResult<Option<Expression>> {
match self {
Expression::Scalar { scalar_fn, .. } => scalar_fn.simplify(self, ctx),
Expression::Root => Ok(None),
}
}
fn reduce_node<'a>(
&self,
node: &ExpressionReduceNode<'a>,
) -> VortexResult<Option<ExpressionReduceNode<'a>>> {
match self {
Expression::Scalar { scalar_fn, .. } => scalar_fn.reduce_expression(node),
Expression::Root => Ok(None),
}
}
fn try_optimize(&self, cache: &SimplifyCache<'_>) -> VortexResult<Option<Expression>> {
let mut current: Option<Expression> = None;
let mut loop_counter = 0;
loop {
if loop_counter > 100 {
vortex_error::vortex_bail!(
"Exceeded maximum optimization iterations (possible infinite loop)"
);
}
loop_counter += 1;
let expr = current.as_ref().unwrap_or(self);
let mut changed = false;
if let Some(simplified) = expr.simplify_untyped_node()? {
current = Some(simplified);
changed = true;
}
let expr = current.as_ref().unwrap_or(self);
if let Some(simplified) = expr.simplify_node(cache)? {
current = Some(simplified);
changed = true;
}
let reduced = {
let expr = current.as_ref().unwrap_or(self);
let reduce_node = ExpressionReduceNode::new(expr, cache.scope);
expr.reduce_node(&reduce_node)?
.map(ExpressionReduceNode::into_expression)
};
if let Some(reduced_expr) = reduced {
current = Some(reduced_expr);
changed = true;
}
if !changed {
break;
}
}
Ok(current)
}
pub fn optimize_recursive(&self, scope: &DType) -> VortexResult<Expression> {
Ok(self
.clone()
.try_optimize_recursive(scope)?
.unwrap_or_else(|| self.clone()))
}
pub fn try_optimize_recursive(&self, scope: &DType) -> VortexResult<Option<Expression>> {
let cache = SimplifyCache::new(scope);
let result = self.try_optimize_recursive_inner(&cache)?;
Ok(Some(find_between(result.unwrap_or_else(|| self.clone()))))
}
fn try_optimize_recursive_inner(
&self,
cache: &SimplifyCache<'_>,
) -> VortexResult<Option<Expression>> {
let mut current = self.try_optimize(cache)?;
let expr = current.as_ref().unwrap_or(self);
let children = expr.children();
let mut new_children: Option<Vec<Expression>> = None;
for (idx, child) in children.iter().enumerate() {
if let Some(optimized) = child.try_optimize_recursive_inner(cache)? {
new_children
.get_or_insert_with(|| children[..idx].to_vec())
.push(optimized);
} else if let Some(new_children) = new_children.as_mut() {
new_children.push(child.clone());
}
}
if let Some(new_children) = new_children {
let updated = expr.clone().with_children(new_children)?;
current = Some(updated.try_optimize(cache)?.unwrap_or(updated));
}
Ok(current)
}
}
struct SimplifyCache<'a> {
scope: &'a DType,
dtype_cache: RefCell<HashMap<Expression, DType>>,
}
impl<'a> SimplifyCache<'a> {
fn new(scope: &'a DType) -> Self {
Self {
scope,
dtype_cache: RefCell::new(HashMap::new()),
}
}
}
impl SimplifyCtx for SimplifyCache<'_> {
fn return_dtype(&self, expr: &Expression) -> VortexResult<DType> {
if expr.is_root() {
return Ok(self.scope.clone());
}
if let Some(dtype) = self.dtype_cache.borrow().get(expr) {
return Ok(dtype.clone());
}
let input_dtypes: Vec<_> = expr
.children()
.iter()
.map(|c| self.return_dtype(c))
.try_collect()?;
let dtype = expr
.as_scalar()
.ok_or_else(|| vortex_err!("cannot type a non-scalar expression: {expr}"))?
.return_dtype(&input_dtypes)?;
self.dtype_cache
.borrow_mut()
.insert(expr.clone(), dtype.clone());
Ok(dtype)
}
}
#[cfg(test)]
mod tests {
use vortex_error::VortexResult;
use vortex_error::vortex_err;
use crate::dtype::DType;
use crate::dtype::Nullability;
use crate::dtype::PType;
use crate::dtype::StructFields;
use crate::expr::cast;
use crate::expr::eq;
use crate::expr::get_item;
use crate::expr::lit;
use crate::expr::lt_eq;
use crate::expr::or;
use crate::expr::root;
use crate::scalar::Scalar;
use crate::scalar_fn::fns::literal::Literal;
#[test]
fn optimize_or_chain_correctness() -> VortexResult<()> {
let expr = or(
eq(get_item("x", root()), lit(1i32)),
eq(get_item("x", root()), lit(2i32)),
);
let scope = DType::Struct(
StructFields::new(
["x"].into(),
vec![DType::Primitive(PType::I32, Nullability::NonNullable)],
),
Nullability::NonNullable,
);
let optimized = expr.optimize_recursive(&scope)?;
let s = optimized.to_string();
assert!(s.contains("$.x"), "expected $.x in {s}");
assert!(s.contains("1i32") || s.contains('1'), "expected 1 in {s}");
assert!(s.contains("2i32") || s.contains('2'), "expected 2 in {s}");
Ok(())
}
#[test]
fn optimize_folds_cast_of_literal_in_comparison() -> VortexResult<()> {
let expr = lt_eq(
get_item("x", root()),
cast(
lit(3i32),
DType::Primitive(PType::F64, Nullability::NonNullable),
),
);
let scope = DType::Struct(
StructFields::new(
["x"].into(),
vec![DType::Primitive(PType::F64, Nullability::NonNullable)],
),
Nullability::NonNullable,
);
let optimized = expr.optimize_recursive(&scope)?;
let rhs = optimized
.child(1)
.as_opt::<Literal>()
.ok_or_else(|| vortex_err!("expected a bare literal RHS, got {optimized}"))?;
assert_eq!(rhs, &Scalar::primitive(3.0f64, Nullability::NonNullable));
Ok(())
}
}