From 48f58d87fc28231995d178db4331d3edfa0d04c4 Mon Sep 17 00:00:00 2001 From: Camden Dixie O'Brien Date: Sat, 26 Sep 2026 11:43:53 +0100 Subject: [PATCH] Implement constant folding --- src/eval.rs | 1 + src/eval/const_fold.rs | 606 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 607 insertions(+) create mode 100644 src/eval/const_fold.rs diff --git a/src/eval.rs b/src/eval.rs index f3548a5..4cd38b6 100644 --- a/src/eval.rs +++ b/src/eval.rs @@ -1,4 +1,5 @@ pub mod normalise; +pub mod const_fold; #[derive(Debug)] pub enum Error { diff --git a/src/eval/const_fold.rs b/src/eval/const_fold.rs new file mode 100644 index 0000000..d85bcff --- /dev/null +++ b/src/eval/const_fold.rs @@ -0,0 +1,606 @@ +use crate::expression::Expression; +use crate::eval::{Error, Evaluator}; + +pub struct ConstFolder; + +impl ConstFolder { + pub fn new() -> Self { + ConstFolder {} + } +} + +impl Evaluator for ConstFolder { + type Expr = Expression; + type Error = Error; + + fn eval(&mut self, input: Expression) -> Result { + Ok(const_fold(input)) + } +} + +fn const_fold(expr: Expression) -> Expression { + match expr { + Expression::Integer(_) => expr, + Expression::Symbol(_) => expr, + + Expression::Sum(x, y) => + fold_assoc( + x, y, 0, &add, &is_sum, &extract_sum, Expression::Sum), + Expression::Product(x, y) => + fold_assoc( + x, y, 1, &multiply, &is_product, &extract_product, + Expression::Product), + + Expression::Difference(x, y) => + try_fold(x, y, subtract, Expression::Difference), + Expression::Quotient(x, y) => + try_fold(x, y, simplify, Expression::Quotient), + Expression::Exponent(x, y) => + try_fold(x, y, power, Expression::Exponent), + } +} + +fn fold_assoc( + x: std::boxed::Box, + y: std::boxed::Box, + identity: i32, + eval: &impl Fn(i32, i32) -> i32, + is_op: &impl Fn(&Expression) -> bool, + extract: &impl Fn( + std::boxed::Box) + -> Option<( + std::boxed::Box, std::boxed::Box)>, + cons: impl Fn( + std::boxed::Box, + std::boxed::Box) -> Expression) + -> Expression { + let mut terms = Vec::new(); + let consts = eval( + collect_assoc(x, &mut terms, identity, eval, is_op, extract), + collect_assoc(y, &mut terms, identity, eval, is_op, extract)); + build_expr(terms, consts, identity, cons) +} + +fn collect_assoc( + mut expr: std::boxed::Box, + terms: &mut Vec>, + identity: i32, + eval: &impl Fn(i32, i32) -> i32, + is_op: &impl Fn(&Expression) -> bool, + extract: &impl Fn( + std::boxed::Box) + -> Option<( + std::boxed::Box, std::boxed::Box)>) + -> i32 +{ + expr = const_fold_boxed(expr); + if let Expression::Integer(x) = *expr { + x + } else if is_op(expr.as_ref()) { + let (x, y) = extract(expr).unwrap(); + eval( + collect_assoc(x, terms, identity, eval, is_op, extract), + collect_assoc(y, terms, identity, eval, is_op, extract)) + } else { + terms.push(expr); + identity + } +} + +fn build_expr( + mut terms: Vec>, + consts: i32, + identity: i32, + cons: impl Fn( + std::boxed::Box, + std::boxed::Box) -> Expression) + -> Expression { + let integer = Expression::Integer(consts); + if terms.is_empty() { + return integer; + } + + let mut tail = if consts != identity { + integer + } else { + *terms.pop().unwrap() + }; + loop { + match terms.pop() { + None => break tail, + Some(term) => + tail = cons(term, std::boxed::Box::new(tail)), + } + } +} + + +fn try_fold( + x: std::boxed::Box, + y: std::boxed::Box, + op: impl FnOnce(i32, i32) -> Expression, + cons: impl FnOnce( + std::boxed::Box, + std::boxed::Box) -> Expression) + -> Expression { + let folded_x = const_fold_boxed(x); + let folded_y = const_fold_boxed(y); + match (folded_x.integer(), folded_y.integer()) { + (Some(x_value), Some(y_value)) => op(x_value, y_value), + _ => cons(folded_x, folded_y), + } +} + +fn const_fold_boxed( + mut b: std::boxed::Box) -> std::boxed::Box { + *b = const_fold(*b); + b +} + +fn is_sum(expr: &Expression) -> bool { + if let Expression::Sum(_, _) = expr { + true + } else { + false + } +} + +fn extract_sum(expr: std::boxed::Box) + -> Option<(std::boxed::Box, std::boxed::Box)> +{ + if let Expression::Sum(x, y) = *expr { + Some((x, y)) + } else { + None + } +} + +fn is_product(expr: &Expression) -> bool { + if let Expression::Product(_, _) = expr { + true + } else { + false + } +} + +fn extract_product(expr: std::boxed::Box) + -> Option<(std::boxed::Box, std::boxed::Box)> +{ + if let Expression::Product(x, y) = *expr { + Some((x, y)) + } else { + None + } +} + +fn add(x: i32, y: i32) -> i32 { + x + y +} + +fn multiply(x: i32, y: i32) -> i32 { + x * y +} + +fn subtract(x: i32, y: i32) -> Expression { + Expression::Integer(x - y) +} + +fn power(x: i32, y: i32) -> Expression { + if y >= 0 { + Expression::Integer(x.pow(y as u32)) + } else { + Expression::quotient( + Expression::Integer(1), + Expression::Integer(x.pow(-y as u32))) + } +} + +fn simplify(num: i32, denom: i32) -> Expression { + let factor = gcd(num, denom); + if denom / factor == 1 { + Expression::Integer(num / factor) + } else if factor != 1 { + Expression::quotient( + Expression::Integer(num / factor), + Expression::Integer(denom / factor)) + } else { + Expression::quotient( + Expression::Integer(num), + Expression::Integer(denom)) + } +} + +fn gcd(mut x: i32, mut y: i32) -> i32 { + while y != 0 { + (x, y) = (y, x % y); + } + x +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn integer_is_unchanged() { + let input = Expression::Integer(0); + let result = ConstFolder::new().eval(input).unwrap(); + assert_eq!(result, Expression::Integer(0)); + } + + #[test] + fn symbol_is_unchanged() { + let input = Expression::Symbol('x'); + let result = ConstFolder::new().eval(input).unwrap(); + assert_eq!(result, Expression::Symbol('x')); + } + + #[test] + fn sum_of_symbol_and_symbol_is_unchanged() { + let input = Expression::sum( + Expression::Symbol('x'), + Expression::Symbol('y')); + let result = ConstFolder::new().eval(input).unwrap(); + assert_eq!( + result, + Expression::sum( + Expression::Symbol('x'), + Expression::Symbol('y'))); + } + + #[test] + fn sum_of_symbol_and_integer_is_unchanged() { + let input = Expression::sum( + Expression::Symbol('x'), + Expression::Integer(1)); + let result = ConstFolder::new().eval(input).unwrap(); + assert_eq!( + result, + Expression::sum( + Expression::Symbol('x'), + Expression::Integer(1))); + } + + #[test] + fn sum_of_integers_is_evaluated() { + let input = Expression::sum( + Expression::Integer(1), + Expression::Integer(2)); + let result = ConstFolder::new().eval(input).unwrap(); + assert_eq!( + result, + Expression::Integer(3)); + } + + #[test] + fn difference_of_symbol_and_symbol_is_unchanged() { + let input = Expression::difference( + Expression::Symbol('x'), + Expression::Symbol('y')); + let result = ConstFolder::new().eval(input).unwrap(); + assert_eq!( + result, + Expression::difference( + Expression::Symbol('x'), + Expression::Symbol('y'))); + } + + #[test] + fn difference_of_symbol_and_integer_is_unchanged() { + let input = Expression::difference( + Expression::Symbol('x'), + Expression::Integer(1)); + let result = ConstFolder::new().eval(input).unwrap(); + assert_eq!( + result, + Expression::difference( + Expression::Symbol('x'), + Expression::Integer(1))); + } + + #[test] + fn difference_of_integers_is_evaluated() { + let input = Expression::difference( + Expression::Integer(2), + Expression::Integer(1)); + let result = ConstFolder::new().eval(input).unwrap(); + assert_eq!( + result, + Expression::Integer(1)); + } + + #[test] + fn product_of_symbol_and_symbol_is_unchanged() { + let input = Expression::product( + Expression::Symbol('x'), + Expression::Symbol('y')); + let result = ConstFolder::new().eval(input).unwrap(); + assert_eq!( + result, + Expression::product( + Expression::Symbol('x'), + Expression::Symbol('y'))); + } + + #[test] + fn product_of_symbol_and_integer_is_unchanged() { + let input = Expression::product( + Expression::Symbol('x'), + Expression::Integer(2)); + let result = ConstFolder::new().eval(input).unwrap(); + assert_eq!( + result, + Expression::product( + Expression::Symbol('x'), + Expression::Integer(2))); + } + + #[test] + fn product_of_integers_is_evaluated() { + let input = Expression::product( + Expression::Integer(3), + Expression::Integer(2)); + let result = ConstFolder::new().eval(input).unwrap(); + assert_eq!( + result, + Expression::Integer(6)); + } + + #[test] + fn exponent_of_symbol_and_symbol_is_unchanged() { + let input = Expression::exponent( + Expression::Symbol('x'), + Expression::Symbol('y')); + let result = ConstFolder::new().eval(input).unwrap(); + assert_eq!( + result, + Expression::exponent( + Expression::Symbol('x'), + Expression::Symbol('y'))); + } + + #[test] + fn exponent_of_symbol_and_integer_is_unchanged() { + let input = Expression::exponent( + Expression::Symbol('x'), + Expression::Integer(1)); + let result = ConstFolder::new().eval(input).unwrap(); + assert_eq!( + result, + Expression::exponent( + Expression::Symbol('x'), + Expression::Integer(1))); + } + + #[test] + fn exponent_of_integer_and_positive_integer_is_evaluated() { + let input = Expression::exponent( + Expression::Integer(3), + Expression::Integer(2)); + let result = ConstFolder::new().eval(input).unwrap(); + assert_eq!(result, Expression::Integer(9)); + } + + #[test] + fn exponent_of_integer_and_zero_is_evaluated() { + let input = Expression::exponent( + Expression::Integer(3), + Expression::Integer(0)); + let result = ConstFolder::new().eval(input).unwrap(); + assert_eq!(result, Expression::Integer(1)); + } + + #[test] + fn exponent_of_integer_and_positive_integer_is_partially_evaluated() { + let input = Expression::exponent( + Expression::Integer(3), + Expression::Integer(-2)); + let result = ConstFolder::new().eval(input).unwrap(); + assert_eq!( + result, + Expression::quotient( + Expression::Integer(1), + Expression::Integer(9))); + } + + #[test] + fn quotient_of_symbol_and_symbol_is_unchanged() { + let input = Expression::quotient( + Expression::Symbol('x'), + Expression::Symbol('y')); + let result = ConstFolder::new().eval(input).unwrap(); + assert_eq!( + result, + Expression::quotient( + Expression::Symbol('x'), + Expression::Symbol('y'))); + } + + #[test] + fn quotient_of_symbol_and_integer_is_unchanged() { + let input = Expression::quotient( + Expression::Symbol('x'), + Expression::Integer(1)); + let result = ConstFolder::new().eval(input).unwrap(); + assert_eq!( + result, + Expression::quotient( + Expression::Symbol('x'), + Expression::Integer(1))); + } + + #[test] + fn quotient_of_divisible_integers_is_evaluated() { + let input = Expression::quotient( + Expression::Integer(6), + Expression::Integer(2)); + let result = ConstFolder::new().eval(input).unwrap(); + assert_eq!( + result, + Expression::Integer(3)); + } + + #[test] + fn quotient_of_coprime_integers_is_unchanged() { + let input = Expression::quotient( + Expression::Integer(9), + Expression::Integer(8)); + let result = ConstFolder::new().eval(input).unwrap(); + assert_eq!( + result, + Expression::quotient( + Expression::Integer(9), + Expression::Integer(8))); + } + + #[test] + fn quotient_of_integers_with_shared_factor_is_simplified() { + let input = Expression::quotient( + Expression::Integer(6), + Expression::Integer(8)); + let result = ConstFolder::new().eval(input).unwrap(); + assert_eq!( + result, + Expression::quotient( + Expression::Integer(3), + Expression::Integer(4))); + } + + #[test] + fn quotient_of_integer_with_one_is_evaluated() { + let input = Expression::quotient( + Expression::Integer(5), + Expression::Integer(1)); + let result = ConstFolder::new().eval(input).unwrap(); + assert_eq!(result, Expression::Integer(5)); + } + + #[test] + fn sum_of_integer_and_sum_of_integers_is_evaluated() { + let input = Expression::sum( + Expression::Integer(1), + Expression::sum( + Expression::Integer(2), + Expression::Integer(3))); + let result = ConstFolder::new().eval(input).unwrap(); + assert_eq!(result, Expression::Integer(6)); + } + + #[test] + fn sum_of_symbol_and_sum_of_integers_is_partially_evaluated() { + let input = Expression::sum( + Expression::Symbol('x'), + Expression::sum( + Expression::Integer(2), + Expression::Integer(3))); + let result = ConstFolder::new().eval(input).unwrap(); + assert_eq!( + result, + Expression::sum( + Expression::Symbol('x'), + Expression::Integer(5))); + } + + #[test] + fn sum_of_sum_of_symbol_and_int_and_int_is_partially_evaluated() { + let input = Expression::sum( + Expression::sum( + Expression::Symbol('x'), + Expression::Integer(1)), + Expression::Integer(1)); + let result = ConstFolder::new().eval(input).unwrap(); + assert_eq!( + result, + Expression::sum( + Expression::Symbol('x'), + Expression::Integer(2))); + } + + #[test] + fn product_of_integer_and_product_of_integers_is_evaluated() { + let input = Expression::product( + Expression::Integer(2), + Expression::product( + Expression::Integer(3), + Expression::Integer(4))); + let result = ConstFolder::new().eval(input).unwrap(); + assert_eq!(result, Expression::Integer(24)); + } + + #[test] + fn product_of_symbol_and_product_of_integers_is_partially_evaluated() { + let input = Expression::product( + Expression::Symbol('x'), + Expression::product( + Expression::Integer(2), + Expression::Integer(3))); + let result = ConstFolder::new().eval(input).unwrap(); + assert_eq!( + result, + Expression::product( + Expression::Symbol('x'), + Expression::Integer(6))); + } + + #[test] + fn product_of_product_of_symbol_and_int_and_int_is_partially_evaluated() + { + let input = Expression::product( + Expression::product( + Expression::Symbol('x'), + Expression::Integer(2)), + Expression::Integer(2)); + let result = ConstFolder::new().eval(input).unwrap(); + assert_eq!( + result, + Expression::product( + Expression::Symbol('x'), + Expression::Integer(4))); + } + + #[test] + fn sum_of_symbol_with_zero_simplifies() + { + let input = Expression::sum( + Expression::Symbol('x'), + Expression::Integer(0)); + let result = ConstFolder::new().eval(input).unwrap(); + assert_eq!(result, Expression::Symbol('x')); + } + + #[test] + fn product_of_symbol_with_one_simplifies() + { + let input = Expression::product( + Expression::Symbol('x'), + Expression::Integer(1)); + let result = ConstFolder::new().eval(input).unwrap(); + assert_eq!(result, Expression::Symbol('x')); + } + + #[test] + fn sum_of_products_of_integers_is_evaluated() { + let input = Expression::sum( + Expression::product( + Expression::Integer(2), + Expression::Integer(3)), + Expression::product( + Expression::Integer(4), + Expression::Integer(5))); + let result = ConstFolder::new().eval(input).unwrap(); + assert_eq!(result, Expression::Integer(26)); + } + + #[test] + fn product_of_sums_of_integers_is_evaluated() { + let input = Expression::product( + Expression::sum( + Expression::Integer(1), + Expression::Integer(2)), + Expression::sum( + Expression::Integer(3), + Expression::Integer(4))); + let result = ConstFolder::new().eval(input).unwrap(); + assert_eq!(result, Expression::Integer(21)); + } +}