use crate::iteration::comprehension::ast::Comprehension;
use crate::iteration::comprehension::predicate::{
CoordSet, Factorization, PredicateInfo,
};
pub fn apply<F>(ast: &Comprehension, analyze: &F) -> Option<Comprehension>
where
F: Fn(&str, &CoordSet) -> PredicateInfo,
{
let Comprehension::Filter { child, predicate } = ast else {
return None;
};
let Comprehension::Cartesian { children: cart_children } = child.as_ref() else {
return None;
};
let coord_names = child.coordinate_names();
let metadata = child.metadata();
let coords = CoordSet::from_metadata(&coord_names, &metadata);
let info = analyze(predicate, &coords);
let per_axis = match info.factorization {
Factorization::PerAxis(m) => m,
_ => return None,
};
let mut new_children: Vec<Comprehension> = Vec::with_capacity(cart_children.len());
let mut any_change = false;
for child in cart_children {
let child_coords = child.coordinate_names();
let owned_subs: Vec<String> = per_axis
.iter()
.filter_map(|(axis, sub_pred)| {
if child_coords.iter().any(|n| n == axis) {
Some(sub_pred.clone())
} else {
None
}
})
.collect();
if owned_subs.is_empty() {
new_children.push(child.clone());
} else {
any_change = true;
let combined_pred = if owned_subs.len() == 1 {
owned_subs.into_iter().next().unwrap()
} else {
owned_subs
.iter()
.map(|s| format!("({s})"))
.collect::<Vec<_>>()
.join(" && ")
};
new_children.push(Comprehension::filter(child.clone(), combined_pred));
}
}
if !any_change {
return None;
}
Some(Comprehension::Cartesian { children: new_children })
}
#[cfg(test)]
mod tests {
use super::*;
use crate::iteration::comprehension::predicate::analyze;
use crate::iteration::comprehension::source::{LiteralValue, Source};
fn clause(name: &str, vs: &[i64]) -> Comprehension {
Comprehension::clause(
name,
Source::Literal {
values: vs.iter().map(|n| LiteralValue::Int(*n)).collect(),
},
)
}
#[test]
fn r5_pushes_per_axis_filter_into_one_child() {
let cart = Comprehension::cartesian(vec![clause("k", &[1, 5, 10]), clause("limit", &[100, 200])]);
let ast = Comprehension::filter(cart, "{k} > 5");
let result = apply(&ast, &analyze).unwrap();
match result {
Comprehension::Cartesian { children } => {
assert_eq!(children.len(), 2);
assert!(matches!(&children[0], Comprehension::Filter { .. }));
assert!(matches!(&children[1], Comprehension::Clause { name, .. } if name == "limit"));
}
other => panic!("expected Cartesian, got {other:?}"),
}
}
#[test]
fn r5_pushes_per_axis_conjunction_to_both_children() {
let cart = Comprehension::cartesian(vec![clause("k", &[1, 5, 10]), clause("limit", &[100, 200, 300])]);
let ast = Comprehension::filter(cart, "{k} > 5 && {limit} < 200");
let result = apply(&ast, &analyze).unwrap();
match result {
Comprehension::Cartesian { children } => {
assert!(matches!(&children[0], Comprehension::Filter { .. }));
assert!(matches!(&children[1], Comprehension::Filter { .. }));
}
other => panic!("expected Cartesian, got {other:?}"),
}
}
#[test]
fn r5_does_not_fire_for_cross_axis_predicate() {
let cart = Comprehension::cartesian(vec![clause("k", &[1, 5, 10]), clause("limit", &[10, 100])]);
let ast = Comprehension::filter(cart, "{k} * {limit} > 100");
assert_eq!(apply(&ast, &analyze), None);
}
#[test]
fn r5_does_not_fire_without_cartesian_child() {
let ast = Comprehension::filter(clause("k", &[1, 2, 3]), "{k} > 0");
assert_eq!(apply(&ast, &analyze), None);
}
#[test]
fn r5_does_not_fire_for_opaque_predicate() {
let cart = Comprehension::cartesian(vec![clause("k", &[1]), clause("l", &[2])]);
let ast = Comprehension::filter(cart, "polynomial_factorization({k}) > 0");
assert_eq!(apply(&ast, &analyze), None);
}
}