use crate::algebra::{Algebra, BinaryOperator, Expression, Term, TriplePattern, Variable};
use std::collections::HashSet;
pub trait OptimizationPass: Send + Sync {
fn name(&self) -> &str;
fn apply(&self, algebra: &mut Algebra) -> bool;
}
fn produced_variables(algebra: &Algebra) -> HashSet<Variable> {
let mut vars = HashSet::new();
collect_produced(algebra, &mut vars);
vars
}
fn collect_produced(algebra: &Algebra, vars: &mut HashSet<Variable>) {
match algebra {
Algebra::Bgp(patterns) => {
for p in patterns {
add_pattern_vars(p, vars);
}
}
Algebra::Join { left, right }
| Algebra::Union { left, right }
| Algebra::Minus { left, right } => {
collect_produced(left, vars);
collect_produced(right, vars);
}
Algebra::LeftJoin { left, right, .. } => {
collect_produced(left, vars);
collect_produced(right, vars);
}
Algebra::Filter { pattern, .. }
| Algebra::Distinct { pattern }
| Algebra::Reduced { pattern }
| Algebra::Slice { pattern, .. }
| Algebra::OrderBy { pattern, .. } => {
collect_produced(pattern, vars);
}
Algebra::Project { pattern, variables } => {
collect_produced(pattern, vars);
vars.extend(variables.iter().cloned());
}
Algebra::Extend {
pattern, variable, ..
} => {
collect_produced(pattern, vars);
vars.insert(variable.clone());
}
Algebra::Group {
pattern,
variables,
aggregates,
} => {
collect_produced(pattern, vars);
for gc in variables {
if let Some(alias) = &gc.alias {
vars.insert(alias.clone());
}
}
for (v, _) in aggregates {
vars.insert(v.clone());
}
}
Algebra::Graph { pattern, .. } | Algebra::Service { pattern, .. } => {
collect_produced(pattern, vars);
}
Algebra::Table | Algebra::Zero | Algebra::Empty => {}
Algebra::Values { variables, .. } => {
vars.extend(variables.iter().cloned());
}
Algebra::PropertyPath {
subject, object, ..
} => {
if let Term::Variable(v) = subject {
vars.insert(v.clone());
}
if let Term::Variable(v) = object {
vars.insert(v.clone());
}
}
_ => {}
}
}
fn add_pattern_vars(pattern: &TriplePattern, vars: &mut HashSet<Variable>) {
if let Term::Variable(v) = &pattern.subject {
vars.insert(v.clone());
}
if let Term::Variable(v) = &pattern.predicate {
vars.insert(v.clone());
}
if let Term::Variable(v) = &pattern.object {
vars.insert(v.clone());
}
}
fn is_empty_algebra(algebra: &Algebra) -> bool {
matches!(algebra, Algebra::Bgp(patterns) if patterns.is_empty())
}
fn evaluate_constant_bool(expr: &Expression) -> Option<bool> {
match expr {
Expression::Literal(lit) => match lit.value.as_str() {
"true" => Some(true),
"false" => Some(false),
"1" => Some(true),
"0" => Some(false),
_ => None,
},
Expression::Binary { op, left, right } => {
let l = evaluate_constant_bool(left);
let r = evaluate_constant_bool(right);
match op {
BinaryOperator::And => match (l, r) {
(Some(false), _) | (_, Some(false)) => Some(false),
(Some(true), Some(true)) => Some(true),
_ => None,
},
BinaryOperator::Or => match (l, r) {
(Some(true), _) | (_, Some(true)) => Some(true),
(Some(false), Some(false)) => Some(false),
_ => None,
},
BinaryOperator::Equal => {
equal_literals(left, right)
}
BinaryOperator::NotEqual => equal_literals(left, right).map(|eq| !eq),
_ => None,
}
}
Expression::Unary {
op: crate::algebra::UnaryOperator::Not,
operand,
} => evaluate_constant_bool(operand).map(|v| !v),
_ => None,
}
}
fn equal_literals(left: &Expression, right: &Expression) -> Option<bool> {
match (left, right) {
(Expression::Literal(l), Expression::Literal(r)) => Some(l == r),
_ => None,
}
}
fn expr_contains_variable(expr: &Expression) -> bool {
match expr {
Expression::Variable(_) => true,
Expression::Bound(_) => true,
Expression::Binary { left, right, .. } => {
expr_contains_variable(left) || expr_contains_variable(right)
}
Expression::Unary { operand, .. } => expr_contains_variable(operand),
Expression::Function { args, .. } => args.iter().any(expr_contains_variable),
Expression::Conditional {
condition,
then_expr,
else_expr,
} => {
expr_contains_variable(condition)
|| expr_contains_variable(then_expr)
|| expr_contains_variable(else_expr)
}
Expression::Exists(inner) | Expression::NotExists(inner) => {
let _ = inner;
true
}
_ => false,
}
}
pub struct ConstantFoldingPass;
impl OptimizationPass for ConstantFoldingPass {
fn name(&self) -> &str {
"constant_folding"
}
fn apply(&self, algebra: &mut Algebra) -> bool {
fold_constant_recursive(algebra)
}
}
fn fold_constant_recursive(algebra: &mut Algebra) -> bool {
match algebra {
Algebra::Filter { pattern, condition } => {
let child_changed = fold_constant_recursive(pattern);
if !expr_contains_variable(condition) {
if let Some(constant_value) = evaluate_constant_bool(condition) {
if constant_value {
let inner = std::mem::replace(pattern.as_mut(), Algebra::Bgp(vec![]));
*algebra = inner;
} else {
*algebra = Algebra::Bgp(vec![]);
}
return true;
}
}
child_changed
}
Algebra::Join { left, right } => {
let l = fold_constant_recursive(left);
let r = fold_constant_recursive(right);
l || r
}
Algebra::Union { left, right } => {
let l = fold_constant_recursive(left);
let r = fold_constant_recursive(right);
l || r
}
Algebra::LeftJoin { left, right, .. } => {
let l = fold_constant_recursive(left);
let r = fold_constant_recursive(right);
l || r
}
Algebra::Project { pattern, .. }
| Algebra::Distinct { pattern }
| Algebra::Reduced { pattern }
| Algebra::OrderBy { pattern, .. }
| Algebra::Slice { pattern, .. } => fold_constant_recursive(pattern),
Algebra::Extend { pattern, .. } => fold_constant_recursive(pattern),
Algebra::Graph { pattern, .. } | Algebra::Service { pattern, .. } => {
fold_constant_recursive(pattern)
}
Algebra::Group { pattern, .. } => fold_constant_recursive(pattern),
_ => false,
}
}
pub struct UnusedVariableEliminationPass;
impl OptimizationPass for UnusedVariableEliminationPass {
fn name(&self) -> &str {
"unused_var_elimination"
}
fn apply(&self, algebra: &mut Algebra) -> bool {
eliminate_unused_vars(algebra)
}
}
fn eliminate_unused_vars(algebra: &mut Algebra) -> bool {
match algebra {
Algebra::Project { pattern, variables } => {
let child_changed = eliminate_unused_vars(pattern);
let produced = produced_variables(pattern);
let before = variables.len();
variables.retain(|v| produced.contains(v));
let after = variables.len();
let changed = before != after || child_changed;
if variables.is_empty() {
*algebra = Algebra::Bgp(vec![]);
return true;
}
changed
}
Algebra::Filter { pattern, .. }
| Algebra::Distinct { pattern }
| Algebra::Reduced { pattern }
| Algebra::Slice { pattern, .. }
| Algebra::OrderBy { pattern, .. } => eliminate_unused_vars(pattern),
Algebra::Join { left, right } => {
let l = eliminate_unused_vars(left);
let r = eliminate_unused_vars(right);
l || r
}
Algebra::Union { left, right } => {
let l = eliminate_unused_vars(left);
let r = eliminate_unused_vars(right);
l || r
}
Algebra::LeftJoin { left, right, .. } => {
let l = eliminate_unused_vars(left);
let r = eliminate_unused_vars(right);
l || r
}
Algebra::Graph { pattern, .. } | Algebra::Service { pattern, .. } => {
eliminate_unused_vars(pattern)
}
Algebra::Group { pattern, .. } => eliminate_unused_vars(pattern),
_ => false,
}
}
pub struct RedundantJoinEliminationPass;
impl OptimizationPass for RedundantJoinEliminationPass {
fn name(&self) -> &str {
"redundant_join_elimination"
}
fn apply(&self, algebra: &mut Algebra) -> bool {
eliminate_redundant_joins(algebra)
}
}
fn eliminate_redundant_joins(algebra: &mut Algebra) -> bool {
match algebra {
Algebra::Join { left, right } => {
let l = eliminate_redundant_joins(left);
let r = eliminate_redundant_joins(right);
if is_empty_algebra(left) || is_empty_algebra(right) {
*algebra = Algebra::Bgp(vec![]);
return true;
}
l || r
}
Algebra::Filter { pattern, .. }
| Algebra::Distinct { pattern }
| Algebra::Reduced { pattern }
| Algebra::Slice { pattern, .. }
| Algebra::OrderBy { pattern, .. } => eliminate_redundant_joins(pattern),
Algebra::Project { pattern, .. } => eliminate_redundant_joins(pattern),
Algebra::Union { left, right } => {
let l = eliminate_redundant_joins(left);
let r = eliminate_redundant_joins(right);
l || r
}
Algebra::LeftJoin { left, right, .. } => {
let l = eliminate_redundant_joins(left);
let r = eliminate_redundant_joins(right);
l || r
}
Algebra::Graph { pattern, .. } | Algebra::Service { pattern, .. } => {
eliminate_redundant_joins(pattern)
}
Algebra::Group { pattern, .. } => eliminate_redundant_joins(pattern),
_ => false,
}
}
#[derive(Debug, Clone, Default)]
pub struct PipelineResult {
pub passes_run: usize,
pub total_iterations: usize,
pub changed: bool,
pub pass_names: Vec<String>,
}
pub struct OptimizationPipeline {
passes: Vec<Box<dyn OptimizationPass>>,
max_iterations: usize,
}
impl OptimizationPipeline {
pub fn new() -> Self {
Self {
passes: Vec::new(),
max_iterations: 10,
}
}
pub fn add_pass(mut self, pass: Box<dyn OptimizationPass>) -> Self {
self.passes.push(pass);
self
}
pub fn with_max_iterations(mut self, n: usize) -> Self {
self.max_iterations = n.max(1);
self
}
pub fn run(&self, algebra: &mut Algebra) -> PipelineResult {
let pass_names: Vec<String> = self.passes.iter().map(|p| p.name().to_string()).collect();
let mut result = PipelineResult {
pass_names,
..Default::default()
};
for _iter in 0..self.max_iterations {
let mut iteration_changed = false;
for pass in &self.passes {
let changed = pass.apply(algebra);
result.passes_run += 1;
if changed {
iteration_changed = true;
result.changed = true;
}
}
result.total_iterations += 1;
if !iteration_changed {
break;
}
}
result
}
pub fn default_pipeline() -> Self {
Self::new()
.add_pass(Box::new(ConstantFoldingPass))
.add_pass(Box::new(UnusedVariableEliminationPass))
.add_pass(Box::new(RedundantJoinEliminationPass))
}
}
impl Default for OptimizationPipeline {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::algebra::{Literal as AlgLiteral, Term, TriplePattern, Variable};
use oxirs_core::model::NamedNode;
fn make_var(name: &str) -> Variable {
Variable::new(name).expect("valid variable name")
}
fn make_iri(iri: &str) -> Term {
Term::Iri(NamedNode::new(iri).expect("valid IRI"))
}
fn make_var_term(name: &str) -> Term {
Term::Variable(make_var(name))
}
fn make_triple_pattern(s: Term, p: Term, o: Term) -> TriplePattern {
TriplePattern {
subject: s,
predicate: p,
object: o,
}
}
fn make_bgp_with_var(var: &str) -> Algebra {
Algebra::Bgp(vec![make_triple_pattern(
make_var_term(var),
make_iri("http://example.org/p"),
make_iri("http://example.org/o"),
)])
}
fn make_empty_bgp() -> Algebra {
Algebra::Bgp(vec![])
}
fn make_true_filter_condition() -> Expression {
Expression::Literal(AlgLiteral {
value: "true".to_string(),
language: None,
datatype: None,
})
}
fn make_false_filter_condition() -> Expression {
Expression::Literal(AlgLiteral {
value: "false".to_string(),
language: None,
datatype: None,
})
}
fn make_equal_literals(a: &str, b: &str) -> Expression {
Expression::Binary {
op: BinaryOperator::Equal,
left: Box::new(Expression::Literal(AlgLiteral {
value: a.to_string(),
language: None,
datatype: None,
})),
right: Box::new(Expression::Literal(AlgLiteral {
value: b.to_string(),
language: None,
datatype: None,
})),
}
}
#[test]
fn test_constant_folding_true_filter_removed() {
let inner = make_bgp_with_var("x");
let mut algebra = Algebra::Filter {
pattern: Box::new(inner.clone()),
condition: make_true_filter_condition(),
};
let changed = ConstantFoldingPass.apply(&mut algebra);
assert!(changed);
assert_eq!(algebra, inner);
}
#[test]
fn test_constant_folding_false_filter_produces_empty() {
let inner = make_bgp_with_var("x");
let mut algebra = Algebra::Filter {
pattern: Box::new(inner),
condition: make_false_filter_condition(),
};
let changed = ConstantFoldingPass.apply(&mut algebra);
assert!(changed);
assert!(is_empty_algebra(&algebra));
}
#[test]
fn test_constant_folding_equal_literals_true() {
let inner = make_bgp_with_var("x");
let mut algebra = Algebra::Filter {
pattern: Box::new(inner.clone()),
condition: make_equal_literals("hello", "hello"),
};
let changed = ConstantFoldingPass.apply(&mut algebra);
assert!(changed);
assert_eq!(algebra, inner);
}
#[test]
fn test_constant_folding_equal_literals_false() {
let inner = make_bgp_with_var("x");
let mut algebra = Algebra::Filter {
pattern: Box::new(inner),
condition: make_equal_literals("a", "b"),
};
let changed = ConstantFoldingPass.apply(&mut algebra);
assert!(changed);
assert!(is_empty_algebra(&algebra));
}
#[test]
fn test_constant_folding_variable_filter_not_folded() {
let inner = make_bgp_with_var("x");
let var_filter = Expression::Binary {
op: BinaryOperator::Equal,
left: Box::new(Expression::Variable(make_var("x"))),
right: Box::new(Expression::Literal(AlgLiteral {
value: "42".to_string(),
language: None,
datatype: None,
})),
};
let mut algebra = Algebra::Filter {
pattern: Box::new(inner),
condition: var_filter,
};
let changed = ConstantFoldingPass.apply(&mut algebra);
assert!(!changed);
}
#[test]
fn test_constant_folding_no_filter_not_changed() {
let mut algebra = make_bgp_with_var("x");
let changed = ConstantFoldingPass.apply(&mut algebra);
assert!(!changed);
}
#[test]
fn test_constant_folding_nested_filter() {
let with_false_filter = Algebra::Filter {
pattern: Box::new(make_bgp_with_var("x")),
condition: make_false_filter_condition(),
};
let mut algebra = Algebra::Join {
left: Box::new(make_bgp_with_var("y")),
right: Box::new(with_false_filter),
};
let changed = ConstantFoldingPass.apply(&mut algebra);
assert!(changed);
if let Algebra::Join { right, .. } = &algebra {
assert!(is_empty_algebra(right));
} else {
panic!("expected Join");
}
}
#[test]
fn test_constant_folding_and_true_true() {
let cond = Expression::Binary {
op: BinaryOperator::And,
left: Box::new(make_true_filter_condition()),
right: Box::new(make_true_filter_condition()),
};
let inner = make_bgp_with_var("x");
let mut algebra = Algebra::Filter {
pattern: Box::new(inner.clone()),
condition: cond,
};
let changed = ConstantFoldingPass.apply(&mut algebra);
assert!(changed);
assert_eq!(algebra, inner);
}
#[test]
fn test_constant_folding_or_false_false() {
let cond = Expression::Binary {
op: BinaryOperator::Or,
left: Box::new(make_false_filter_condition()),
right: Box::new(make_false_filter_condition()),
};
let inner = make_bgp_with_var("x");
let mut algebra = Algebra::Filter {
pattern: Box::new(inner),
condition: cond,
};
let changed = ConstantFoldingPass.apply(&mut algebra);
assert!(changed);
assert!(is_empty_algebra(&algebra));
}
#[test]
fn test_unused_var_elimination_removes_absent_var() {
let inner = make_bgp_with_var("x");
let x_var = make_var("x");
let ghost_var = make_var("ghost");
let mut algebra = Algebra::Project {
pattern: Box::new(inner),
variables: vec![x_var.clone(), ghost_var],
};
let changed = UnusedVariableEliminationPass.apply(&mut algebra);
assert!(changed);
if let Algebra::Project { variables, .. } = &algebra {
assert_eq!(variables, &[x_var]);
} else {
panic!("expected Project");
}
}
#[test]
fn test_unused_var_elimination_empty_project_becomes_empty_bgp() {
let inner = make_bgp_with_var("x");
let mut algebra = Algebra::Project {
pattern: Box::new(inner),
variables: vec![make_var("does_not_exist")],
};
let changed = UnusedVariableEliminationPass.apply(&mut algebra);
assert!(changed);
assert!(is_empty_algebra(&algebra));
}
#[test]
fn test_unused_var_elimination_all_vars_present_no_change() {
let inner = make_bgp_with_var("x");
let x_var = make_var("x");
let mut algebra = Algebra::Project {
pattern: Box::new(inner),
variables: vec![x_var],
};
let changed = UnusedVariableEliminationPass.apply(&mut algebra);
assert!(!changed);
}
#[test]
fn test_unused_var_elimination_non_project_no_change() {
let mut algebra = make_bgp_with_var("x");
let changed = UnusedVariableEliminationPass.apply(&mut algebra);
assert!(!changed);
}
#[test]
fn test_unused_var_join_both_sides() {
let left = make_bgp_with_var("a");
let right = make_bgp_with_var("b");
let join = Algebra::Join {
left: Box::new(left),
right: Box::new(right),
};
let mut algebra = Algebra::Project {
pattern: Box::new(join),
variables: vec![make_var("a"), make_var("b"), make_var("c")],
};
let changed = UnusedVariableEliminationPass.apply(&mut algebra);
assert!(changed);
if let Algebra::Project { variables, .. } = &algebra {
assert!(variables.contains(&make_var("a")));
assert!(variables.contains(&make_var("b")));
assert!(!variables.contains(&make_var("c")));
} else {
panic!("expected Project");
}
}
#[test]
fn test_redundant_join_left_empty() {
let mut algebra = Algebra::Join {
left: Box::new(make_empty_bgp()),
right: Box::new(make_bgp_with_var("x")),
};
let changed = RedundantJoinEliminationPass.apply(&mut algebra);
assert!(changed);
assert!(is_empty_algebra(&algebra));
}
#[test]
fn test_redundant_join_right_empty() {
let mut algebra = Algebra::Join {
left: Box::new(make_bgp_with_var("x")),
right: Box::new(make_empty_bgp()),
};
let changed = RedundantJoinEliminationPass.apply(&mut algebra);
assert!(changed);
assert!(is_empty_algebra(&algebra));
}
#[test]
fn test_redundant_join_both_empty() {
let mut algebra = Algebra::Join {
left: Box::new(make_empty_bgp()),
right: Box::new(make_empty_bgp()),
};
let changed = RedundantJoinEliminationPass.apply(&mut algebra);
assert!(changed);
assert!(is_empty_algebra(&algebra));
}
#[test]
fn test_redundant_join_no_empty_no_change() {
let mut algebra = Algebra::Join {
left: Box::new(make_bgp_with_var("x")),
right: Box::new(make_bgp_with_var("y")),
};
let changed = RedundantJoinEliminationPass.apply(&mut algebra);
assert!(!changed);
}
#[test]
fn test_redundant_join_nested_chain() {
let inner = Algebra::Join {
left: Box::new(make_empty_bgp()),
right: Box::new(make_bgp_with_var("x")),
};
let mut algebra = Algebra::Join {
left: Box::new(inner),
right: Box::new(make_bgp_with_var("y")),
};
let changed1 = RedundantJoinEliminationPass.apply(&mut algebra);
assert!(changed1);
assert!(is_empty_algebra(&algebra));
let changed2 = RedundantJoinEliminationPass.apply(&mut algebra);
assert!(!changed2);
}
#[test]
fn test_pipeline_default_passes_included() {
let pipeline = OptimizationPipeline::default_pipeline();
assert_eq!(pipeline.passes.len(), 3);
assert_eq!(pipeline.passes[0].name(), "constant_folding");
assert_eq!(pipeline.passes[1].name(), "unused_var_elimination");
assert_eq!(pipeline.passes[2].name(), "redundant_join_elimination");
}
#[test]
fn test_pipeline_run_reports_changed() {
let pipeline = OptimizationPipeline::default_pipeline();
let mut algebra = Algebra::Filter {
pattern: Box::new(make_bgp_with_var("x")),
condition: make_false_filter_condition(),
};
let result = pipeline.run(&mut algebra);
assert!(result.changed);
assert!(result.total_iterations >= 1);
assert!(result.passes_run >= 1);
assert!(is_empty_algebra(&algebra));
}
#[test]
fn test_pipeline_run_no_change_converges_immediately() {
let pipeline = OptimizationPipeline::default_pipeline();
let mut algebra = make_bgp_with_var("x");
let result = pipeline.run(&mut algebra);
assert!(!result.changed);
assert_eq!(result.total_iterations, 1);
}
#[test]
fn test_pipeline_run_pass_names_in_result() {
let pipeline = OptimizationPipeline::default_pipeline();
let mut algebra = make_bgp_with_var("x");
let result = pipeline.run(&mut algebra);
assert!(result.pass_names.contains(&"constant_folding".to_string()));
assert!(result
.pass_names
.contains(&"unused_var_elimination".to_string()));
assert!(result
.pass_names
.contains(&"redundant_join_elimination".to_string()));
}
#[test]
fn test_pipeline_empty_pipeline_no_change() {
let pipeline = OptimizationPipeline::new();
let mut algebra = make_bgp_with_var("x");
let result = pipeline.run(&mut algebra);
assert!(!result.changed);
}
#[test]
fn test_pipeline_combined_fold_and_join_elimination() {
let pipeline = OptimizationPipeline::default_pipeline();
let filter_false = Algebra::Filter {
pattern: Box::new(make_bgp_with_var("x")),
condition: make_false_filter_condition(),
};
let mut algebra = Algebra::Join {
left: Box::new(make_bgp_with_var("y")),
right: Box::new(filter_false),
};
let result = pipeline.run(&mut algebra);
assert!(result.changed);
assert!(is_empty_algebra(&algebra));
}
#[test]
fn test_pipeline_result_total_iterations_bounded() {
let pipeline = OptimizationPipeline::new().with_max_iterations(2);
let mut algebra = make_bgp_with_var("x");
let result = pipeline.run(&mut algebra);
assert!(result.total_iterations <= 2);
}
}