use symplex::prelude::*;
macro_rules! assert_simplifies_to {
($expr:expr, $expected:expr) => {{
let result = ($expr).simplify();
let s = format!("{result}");
assert_eq!(
s, $expected,
"expected simplify to produce '{}', got '{}'",
$expected, s,
);
}};
}
macro_rules! assert_simplify_unchanged {
($expr:expr) => {{
let before = format!("{}", $expr);
let result = ($expr).simplify();
let after = format!("{result}");
assert_eq!(
before, after,
"expected simplify to leave '{}' unchanged, but got '{}'",
before, after,
);
}};
}
macro_rules! assert_trace_contains_rule {
($expr:expr, $rule_name:expr) => {{
let before = format!("{}", $expr);
let result = ($expr).simplify();
let after = format!("{result}");
assert_ne!(
before, after,
"expected rule '{}' to fire (expression should change), but simplify left it as '{}'",
$rule_name, before,
);
}};
}
macro_rules! assert_trace_empty {
($expr:expr) => {{
let before = format!("{}", $expr);
let result = ($expr).simplify();
let after = format!("{result}");
assert_eq!(
before, after,
"expected no rules to fire, but simplify changed '{}' to '{}'",
before, after,
);
}};
}
macro_rules! assert_value_preserved {
($expr:expr, $var:expr, $val:expr, $tol:expr) => {{
let before_val = ($expr).subs(&$var, &$val).eval_f64();
let after_val = ($expr).simplify().subs(&$var, &$val).eval_f64();
match (before_val, after_val) {
(Ok(v1), Ok(v2)) => {
assert!(
(v1 - v2).abs() < $tol,
"value not preserved: before={}, after={}, diff={}",
v1,
v2,
(v1 - v2).abs(),
);
}
(Err(e1), _) => panic!("could not evaluate original expression: {e1}"),
(_, Err(e2)) => panic!("could not evaluate simplified expression: {e2}"),
}
}};
}
#[test]
fn rule_pythagorean_fires() {
let ctx = Context::new();
let x = ctx.symbol("x");
assert_simplifies_to!(&x.sin().powi(2) + &x.cos().powi(2), "1");
}
#[test]
fn rule_pythagorean_trace() {
let ctx = Context::new();
let x = ctx.symbol("x");
assert_trace_contains_rule!(&x.sin().powi(2) + &x.cos().powi(2), "pythagorean");
}
#[test]
fn rule_pythagorean_different_args_no_fire() {
let ctx = Context::new();
let x = ctx.symbol("x");
let y = ctx.symbol("y");
let expr = &x.sin().powi(2) + &y.cos().powi(2);
let s = format!("{}", expr.simplify());
assert_ne!(s, "1", "sin²(x)+cos²(y) must not become 1");
}
#[test]
fn rule_pythagorean_value_preserved() {
let ctx = Context::new();
let x = ctx.symbol("x");
let pt = ctx.rational(7, 10);
assert_value_preserved!(&x.sin().powi(2) + &x.cos().powi(2), x, pt, 1e-10);
}
#[test]
fn rule_exp_ln_fires() {
let ctx = Context::new();
let x = ctx.symbol("x");
assert_simplifies_to!(x.ln().exp(), "x");
}
#[test]
fn rule_exp_ln_trace() {
let ctx = Context::new();
let x = ctx.symbol("x");
assert_trace_contains_rule!(x.ln().exp(), "exp_ln");
}
#[test]
fn rule_exp_ln_plus_one_no_fire() {
let ctx = Context::new();
let x = ctx.symbol("x");
let expr = (&x.ln() + 1).exp();
let s = format!("{}", expr.simplify());
assert_ne!(s, "x", "exp(ln(x)+1) must not become x");
}
#[test]
fn rule_exp_ln_value_preserved() {
let ctx = Context::new();
let x = ctx.symbol("x");
let pt = ctx.int(3);
assert_value_preserved!(x.ln().exp(), x, pt, 1e-10);
}
#[test]
fn rule_ln_exp_fires_for_real_symbol() {
let ctx = Context::new();
let x = ctx.symbol_with("x", &[Assumption::Real]);
let result = x.exp().ln().simplify();
assert_eq!(format!("{result}"), "x");
}
#[test]
fn rule_ln_exp_fires_for_unassumed_symbol() {
let ctx = Context::new();
let x = ctx.symbol("x");
let expr = x.exp().ln();
let s = format!("{}", expr.simplify());
assert_eq!(
s, "x",
"ln(exp(x)) should simplify to x for unassumed symbols"
);
}
#[test]
fn rule_ln_exp_trace_for_real() {
let ctx = Context::new();
let x = ctx.symbol_with("x", &[Assumption::Real]);
assert_trace_contains_rule!(x.exp().ln(), "ln_exp");
}
#[test]
fn rule_abs_abs_fires() {
let ctx = Context::new();
let x = ctx.symbol("x");
assert_simplifies_to!(x.abs().abs(), "abs(x)");
}
#[test]
fn rule_abs_abs_trace() {
let ctx = Context::new();
let x = ctx.symbol("x");
let expr = x.abs().abs();
assert_eq!(
format!("{expr}"),
"abs(x)",
"abs(abs(x)) should canonicalize to abs(x)"
);
}
#[test]
fn rule_abs_abs_value_preserved() {
let ctx = Context::new();
let x = ctx.symbol("x");
let pt = ctx.int(-3);
assert_value_preserved!(x.abs().abs(), x, pt, 1e-10);
}
#[test]
fn rule_sqrt_sq_fires() {
let ctx = Context::new();
let x = ctx.symbol("x");
assert_simplifies_to!(x.powi(2).sqrt(), "abs(x)");
}
#[test]
fn rule_sqrt_sq_trace() {
let ctx = Context::new();
let x = ctx.symbol("x");
assert_trace_contains_rule!(x.powi(2).sqrt(), "sqrt_sq");
}
#[test]
fn rule_sqrt_cube_no_fire() {
let ctx = Context::new();
let x = ctx.symbol("x");
let expr = x.powi(3).sqrt();
let s = format!("{}", expr.simplify());
assert_ne!(s, "abs(x)", "sqrt(x^3) must not become abs(x)");
}
#[test]
fn rule_sqrt_sq_value_preserved() {
let ctx = Context::new();
let x = ctx.symbol("x");
let pt = ctx.int(-4);
assert_value_preserved!(x.powi(2).sqrt(), x, pt, 1e-10);
}
#[test]
fn rule_cosh_sinh_identity_fires() {
let ctx = Context::new();
let x = ctx.symbol("x");
assert_simplifies_to!(&x.cosh().powi(2) - &x.sinh().powi(2), "1");
}
#[test]
fn rule_cosh_sinh_identity_trace() {
let ctx = Context::new();
let x = ctx.symbol("x");
assert_trace_contains_rule!(&x.cosh().powi(2) - &x.sinh().powi(2), "cosh_sinh_identity");
}
#[test]
fn rule_cosh_sinh_wrong_sign_no_fire() {
let ctx = Context::new();
let x = ctx.symbol("x");
let expr = &x.cosh().powi(2) + &x.sinh().powi(2);
let s = format!("{}", expr.simplify());
assert_ne!(s, "1", "cosh²+sinh² must not become 1");
}
#[test]
fn rule_cosh_sinh_different_args_no_fire() {
let ctx = Context::new();
let x = ctx.symbol("x");
let y = ctx.symbol("y");
let expr = &x.cosh().powi(2) - &y.sinh().powi(2);
let s = format!("{}", expr.simplify());
assert_ne!(s, "1", "cosh²(x)-sinh²(y) must not become 1");
}
#[test]
fn rule_cosh_sinh_value_preserved() {
let ctx = Context::new();
let x = ctx.symbol("x");
let pt = ctx.rational(8, 10);
assert_value_preserved!(&x.cosh().powi(2) - &x.sinh().powi(2), x, pt, 1e-10);
}
#[test]
fn rule_pow_pow_fires_integer_exponents() {
let ctx = Context::new();
let x = ctx.symbol("x");
assert_simplifies_to!(x.powi(2).powi(3), "x^6");
}
#[test]
fn rule_pow_pow_integer_exponents_simplifies() {
let ctx = Context::new();
let x = ctx.symbol("x");
assert_simplifies_to!(x.powi(2).powi(3), "x^6");
}
#[test]
fn rule_pow_pow_blocked_both_fractional() {
let ctx = Context::new();
let x = ctx.symbol("x");
let half = ctx.rational(1, 2);
let third = ctx.rational(1, 3);
let expr = x.pow(&half).pow(&third);
assert_simplify_unchanged!(expr);
}
#[test]
fn rule_asinh_sinh_fires() {
let ctx = Context::new();
let x = ctx.symbol("x");
assert_simplifies_to!(x.sinh().asinh(), "x");
}
#[test]
fn rule_asinh_sinh_trace() {
let ctx = Context::new();
let x = ctx.symbol("x");
assert_trace_contains_rule!(x.sinh().asinh(), "asinh_sinh");
}
#[test]
fn rule_asinh_sinh_value_preserved() {
let ctx = Context::new();
let x = ctx.symbol("x");
let pt = ctx.rational(3, 2);
assert_value_preserved!(x.sinh().asinh(), x, pt, 1e-10);
}
#[test]
fn rule_acosh_cosh_fires() {
let ctx = Context::new();
let x = ctx.symbol("x");
assert_simplifies_to!(x.cosh().acosh(), "abs(x)");
}
#[test]
fn rule_acosh_cosh_trace() {
let ctx = Context::new();
let x = ctx.symbol("x");
assert_trace_contains_rule!(x.cosh().acosh(), "acosh_cosh");
}
#[test]
fn rule_acosh_cosh_value_preserved() {
let ctx = Context::new();
let x = ctx.symbol("x");
let pt = ctx.rational(3, 2);
assert_value_preserved!(x.cosh().acosh(), x, pt, 1e-10);
}
#[test]
fn rule_atanh_tanh_fires() {
let ctx = Context::new();
let x = ctx.symbol("x");
assert_simplifies_to!(x.tanh().atanh(), "x");
}
#[test]
fn rule_atanh_tanh_trace() {
let ctx = Context::new();
let x = ctx.symbol("x");
assert_trace_contains_rule!(x.tanh().atanh(), "atanh_tanh");
}
#[test]
fn rule_sin_div_cos_fires() {
let ctx = Context::new();
let x = ctx.symbol("x");
let expr = &x.sin() / &x.cos();
assert_simplifies_to!(expr, "tan(x)");
}
#[test]
fn rule_sin_div_cos_trace() {
let ctx = Context::new();
let x = ctx.symbol("x");
let expr = &x.sin() / &x.cos();
assert_trace_contains_rule!(expr, "sin_div_cos");
}
#[test]
fn rule_sin_div_cos_different_args_no_fire() {
let ctx = Context::new();
let x = ctx.symbol("x");
let y = ctx.symbol("y");
let expr = &x.sin() / &y.cos();
let s = format!("{}", expr.simplify());
assert!(
!s.contains("tan("),
"sin(x)/cos(y) must not become tan, got: {s}"
);
}
#[test]
fn rule_sin_div_cos_value_preserved() {
let ctx = Context::new();
let x = ctx.symbol("x");
let pt = ctx.rational(1, 2);
assert_value_preserved!(&x.sin() / &x.cos(), x, pt, 1e-10);
}
#[test]
fn rule_cos_div_sin_fires() {
let ctx = Context::new();
let x = ctx.symbol("x");
let expr = &x.cos() / &x.sin();
let s = format!("{}", expr.simplify());
assert!(
s.contains("tan"),
"cos(x)/sin(x) should simplify to involve tan, got: {s}"
);
}
#[test]
fn rule_cos_div_sin_trace() {
let ctx = Context::new();
let x = ctx.symbol("x");
let expr = &x.cos() / &x.sin();
assert_trace_contains_rule!(expr, "cos_div_sin");
}
#[test]
fn rule_cos_div_sin_value_preserved() {
let ctx = Context::new();
let x = ctx.symbol("x");
let pt = ctx.rational(1, 2);
assert_value_preserved!(&x.cos() / &x.sin(), x, pt, 1e-10);
}
#[test]
fn rule_sinh_div_cosh_fires() {
let ctx = Context::new();
let x = ctx.symbol("x");
let expr = &x.sinh() / &x.cosh();
assert_simplifies_to!(expr, "tanh(x)");
}
#[test]
fn rule_sinh_div_cosh_trace() {
let ctx = Context::new();
let x = ctx.symbol("x");
let expr = &x.sinh() / &x.cosh();
assert_trace_contains_rule!(expr, "sinh_div_cosh");
}
#[test]
fn rule_sinh_div_cosh_different_args_no_fire() {
let ctx = Context::new();
let x = ctx.symbol("x");
let y = ctx.symbol("y");
let expr = &x.sinh() / &y.cosh();
let s = format!("{}", expr.simplify());
assert!(
!s.contains("tanh("),
"sinh(x)/cosh(y) must not become tanh, got: {s}"
);
}
#[test]
fn rule_sinh_div_cosh_value_preserved() {
let ctx = Context::new();
let x = ctx.symbol("x");
let pt = ctx.rational(1, 2);
assert_value_preserved!(&x.sinh() / &x.cosh(), x, pt, 1e-10);
}
#[test]
fn rule_exp_mul_fires() {
let ctx = Context::new();
let a = ctx.symbol("a");
let b = ctx.symbol("b");
let expr = &a.exp() * &b.exp();
assert_simplifies_to!(expr, "exp(a + b)");
}
#[test]
fn rule_exp_mul_trace() {
let ctx = Context::new();
let a = ctx.symbol("a");
let b = ctx.symbol("b");
let expr = &a.exp() * &b.exp();
assert_trace_contains_rule!(expr, "exp_mul");
}
#[test]
fn rule_exp_mul_not_both_exp_no_fire() {
let ctx = Context::new();
let x = ctx.symbol("x");
let expr = &x.exp() * &x.sin();
assert_simplify_unchanged!(expr);
}
#[test]
fn rule_exp_mul_value_preserved() {
let ctx = Context::new();
let a = ctx.symbol("a");
let b = ctx.symbol("b");
let expr = &a.exp() * &b.exp();
let one = ctx.int(1);
let two = ctx.int(2);
let before_val = expr
.subs(&a, &one)
.subs(&b, &two)
.eval_f64()
.expect("evalf before");
let after_val = expr
.simplify()
.subs(&a, &one)
.subs(&b, &two)
.eval_f64()
.expect("evalf after");
assert!(
(before_val - after_val).abs() < 1e-10,
"exp_mul value not preserved: {before_val} vs {after_val}"
);
}
#[test]
fn rule_exp_log_denest_fires_numeric() {
let ctx = Context::new();
let x = ctx.symbol("x");
assert_simplifies_to!((&x.ln() * 3).exp(), "x^3");
}
#[test]
fn rule_exp_log_denest_fires_symbolic() {
let ctx = Context::new();
let x = ctx.symbol("x");
let a = ctx.symbol("a");
assert_simplifies_to!((&x.ln() * &a).exp(), "x^a");
}
#[test]
fn rule_exp_log_denest_trace() {
let ctx = Context::new();
let x = ctx.symbol("x");
assert_trace_contains_rule!((&x.ln() * 3).exp(), "exp_log_denest");
}
#[test]
fn rule_exp_log_denest_value_preserved() {
let ctx = Context::new();
let x = ctx.symbol("x");
let pt = ctx.int(2);
assert_value_preserved!((&x.ln() * 3).exp(), x, pt, 1e-10);
}
#[test]
fn rule_abs_positive_fires_for_literal() {
let ctx = Context::new();
let five = ctx.int(5);
assert_simplifies_to!(five.abs(), "5");
}
#[test]
fn rule_abs_positive_trace_for_literal() {
let ctx = Context::new();
let five = ctx.int(5);
let expr = five.abs();
assert_eq!(format!("{expr}"), "5", "abs(5) should canonicalize to 5");
}
#[test]
fn rule_abs_positive_no_fire_for_symbol() {
let ctx = Context::new();
let x = ctx.symbol("x");
assert_simplify_unchanged!(x.abs());
}
#[test]
fn rule_abs_positive_no_fire_for_negative_literal() {
let ctx = Context::new();
let neg3 = ctx.int(-3);
let result = neg3.abs().eval();
assert_eq!(format!("{result}"), "3");
}
#[test]
fn rule_sin_asin_fires() {
let ctx = Context::new();
let x = ctx.symbol("x");
assert_simplifies_to!(x.asin().sin(), "x");
}
#[test]
fn rule_sin_asin_trace() {
let ctx = Context::new();
let x = ctx.symbol("x");
assert_trace_contains_rule!(x.asin().sin(), "sin_asin");
}
#[test]
fn rule_sin_asin_value_preserved() {
let ctx = Context::new();
let x = ctx.symbol("x");
let pt = ctx.rational(1, 2);
assert_value_preserved!(x.asin().sin(), x, pt, 1e-10);
}
#[test]
fn rule_cos_acos_fires() {
let ctx = Context::new();
let x = ctx.symbol("x");
assert_simplifies_to!(x.acos().cos(), "x");
}
#[test]
fn rule_cos_acos_trace() {
let ctx = Context::new();
let x = ctx.symbol("x");
assert_trace_contains_rule!(x.acos().cos(), "cos_acos");
}
#[test]
fn rule_cos_acos_value_preserved() {
let ctx = Context::new();
let x = ctx.symbol("x");
let pt = ctx.rational(1, 2);
assert_value_preserved!(x.acos().cos(), x, pt, 1e-10);
}
#[test]
fn rule_tan_atan_fires() {
let ctx = Context::new();
let x = ctx.symbol("x");
assert_simplifies_to!(x.atan().tan(), "x");
}
#[test]
fn rule_tan_atan_trace() {
let ctx = Context::new();
let x = ctx.symbol("x");
assert_trace_contains_rule!(x.atan().tan(), "tan_atan");
}
#[test]
fn rule_tan_atan_value_preserved() {
let ctx = Context::new();
let x = ctx.symbol("x");
let pt = ctx.rational(3, 2);
assert_value_preserved!(x.atan().tan(), x, pt, 1e-10);
}
#[test]
fn rule_sinh_asinh_fires() {
let ctx = Context::new();
let x = ctx.symbol("x");
assert_simplifies_to!(x.asinh().sinh(), "x");
}
#[test]
fn rule_sinh_asinh_trace() {
let ctx = Context::new();
let x = ctx.symbol("x");
assert_trace_contains_rule!(x.asinh().sinh(), "sinh_asinh");
}
#[test]
fn rule_sinh_asinh_value_preserved() {
let ctx = Context::new();
let x = ctx.symbol("x");
let pt = ctx.rational(5, 3);
assert_value_preserved!(x.asinh().sinh(), x, pt, 1e-10);
}
#[test]
fn rule_cosh_acosh_fires() {
let ctx = Context::new();
let x = ctx.symbol("x");
assert_simplifies_to!(x.acosh().cosh(), "x");
}
#[test]
fn rule_cosh_acosh_trace() {
let ctx = Context::new();
let x = ctx.symbol("x");
assert_trace_contains_rule!(x.acosh().cosh(), "cosh_acosh");
}
#[test]
fn rule_cosh_acosh_value_preserved() {
let ctx = Context::new();
let x = ctx.symbol("x");
let pt = ctx.int(2);
assert_value_preserved!(x.acosh().cosh(), x, pt, 1e-10);
}
#[test]
fn rule_tanh_atanh_fires() {
let ctx = Context::new();
let x = ctx.symbol("x");
assert_simplifies_to!(x.atanh().tanh(), "x");
}
#[test]
fn rule_tanh_atanh_trace() {
let ctx = Context::new();
let x = ctx.symbol("x");
assert_trace_contains_rule!(x.atanh().tanh(), "tanh_atanh");
}
#[test]
fn rule_tanh_atanh_value_preserved() {
let ctx = Context::new();
let x = ctx.symbol("x");
let pt = ctx.rational(1, 3);
assert_value_preserved!(x.atanh().tanh(), x, pt, 1e-10);
}
#[test]
fn rule_asin_sin_does_not_fire() {
let ctx = Context::new();
let x = ctx.symbol("x");
assert_simplifies_to!(x.sin().asin(), "asin(sin(x))");
}
#[test]
fn rule_acos_cos_does_not_fire() {
let ctx = Context::new();
let x = ctx.symbol("x");
assert_simplifies_to!(x.cos().acos(), "acos(cos(x))");
}
#[test]
fn rule_atan_tan_does_not_fire() {
let ctx = Context::new();
let x = ctx.symbol("x");
assert_simplifies_to!(x.tan().atan(), "atan(tan(x))");
}
#[test]
fn rule_pythagorean_mixed_sinh_cos_no_fire() {
let ctx = Context::new();
let x = ctx.symbol("x");
let expr = &x.sinh().powi(2) + &x.cos().powi(2);
let s = format!("{}", expr.simplify());
assert_ne!(s, "1", "sinh²(x)+cos²(x) must not become 1, got: {s}");
}
#[test]
fn rule_exp_ln_nested_no_fire() {
let ctx = Context::new();
let x = ctx.symbol("x");
assert_simplifies_to!(x.ln().ln().exp(), "ln(x)");
}
#[test]
fn composition_exp_ln_then_pythagorean() {
let ctx = Context::new();
let x = ctx.symbol("x");
let inner = x.ln().exp(); let expr = &inner.sin().powi(2) + &inner.cos().powi(2);
assert_simplifies_to!(expr, "1");
}
#[test]
fn composition_abs_abs_abs_collapses() {
let ctx = Context::new();
let x = ctx.symbol("x");
let expr = x.abs().abs().abs();
let s1 = expr.simplify();
let s2 = s1.simplify();
assert_eq!(format!("{s2}"), "abs(x)");
}
#[test]
fn composition_pow_pow_chain() {
let ctx = Context::new();
let x = ctx.symbol("x");
let expr = x.powi(2).powi(3).powi(2);
let s1 = expr.simplify();
let s2 = s1.simplify();
let s = format!("{s2}");
assert!(
s == "x^12" || s == "x^6^2",
"expected x^12 eventually, got: {s}"
);
}
#[test]
fn simplify_pythagorean_fires() {
let ctx = Context::new();
let x = ctx.symbol("x");
let result = (&x.sin().powi(2) + &x.cos().powi(2)).simplify();
assert_eq!(
format!("{result}"),
"1",
"pythagorean simplification should produce 1"
);
}
#[test]
fn trace_no_steps_for_atom() {
let ctx = Context::new();
let five = ctx.int(5);
assert_trace_empty!(five);
}
#[test]
fn trace_no_steps_for_irreducible_symbol() {
let ctx = Context::new();
let x = ctx.symbol("x");
assert_trace_empty!(x);
}
#[test]
fn value_preserved_cos_div_sin() {
let ctx = Context::new();
let x = ctx.symbol("x");
let pt = ctx.rational(11, 10);
assert_value_preserved!(&x.cos() / &x.sin(), x, pt, 1e-10);
}
#[test]
fn value_preserved_atanh_tanh() {
let ctx = Context::new();
let x = ctx.symbol("x");
let pt = ctx.rational(1, 4);
assert_value_preserved!(x.tanh().atanh(), x, pt, 1e-10);
}
#[test]
fn value_preserved_abs_abs() {
let ctx = Context::new();
let x = ctx.symbol("x");
let pt = ctx.int(-7);
assert_value_preserved!(x.abs().abs(), x, pt, 1e-10);
}
#[test]
fn value_preserved_pow_pow() {
let ctx = Context::new();
let x = ctx.symbol("x");
let pt = ctx.int(2);
assert_value_preserved!(x.powi(2).powi(3), x, pt, 1e-10);
}