use symplex::matrix;
use symplex::prelude::*;
#[test]
fn a1_compile_empty_vars_on_expr_with_x() {
let ctx = Context::new();
let x = ctx.symbol("x");
let expr = &x + 1;
let result = expr.compile(&[]);
assert!(
result.is_err(),
"BUG: compile(&[]) on expr containing x should return None, got Some"
);
}
#[test]
fn a2_compile_extra_vars_on_one_variable_expr() {
let ctx = Context::new();
let x = ctx.symbol("x");
let expr = &x.powi(2) + 1;
let result = expr.compile(&["x", "y", "z"]);
match result {
Ok(f) => {
let val = f(&[2.0, 0.0, 0.0]);
assert!(
(val - 5.0).abs() < 1e-10,
"BUG: compiled fn returned {val}, expected 5.0"
);
}
Err(_) => {
}
}
}
#[test]
fn a3_compile_nonexistent_var_name() {
let ctx = Context::new();
let x = ctx.symbol("x");
let expr = &x + 1;
let result = expr.compile(&["nonexistent"]);
match result {
Ok(f) => {
let val = f(&[3.0]);
assert!(
val.is_nan() || val.is_infinite(),
"BUG: compile with wrong var name returned concrete value {val}"
);
}
Err(_) => {
}
}
}
#[test]
fn a4_eval_f64_zero() {
let ctx = Context::new();
let zero = ctx.int(0);
let result = zero.eval_f64();
match result {
Ok(val) => assert!(
val.abs() < 1e-15,
"BUG: eval_f64 of 0 returned {val}, expected 0.0"
),
Err(e) => panic!("BUG: eval_f64 of 0 returned Err: {e}"),
}
}
#[test]
fn a5_eval_f64_infinity() {
let ctx = Context::new();
let inf = ctx.infinity();
let result = inf.eval_f64();
match result {
Ok(val) => {
assert!(
val.is_infinite() && val > 0.0,
"eval_f64 of ∞ returned {val}, expected +inf"
);
}
Err(_) => {
}
}
}
#[test]
fn a6_eval_f64_nan() {
let ctx = Context::new();
let nan = ctx.nan();
let result = nan.eval_f64();
match result {
Ok(val) => {
assert!(val.is_nan(), "eval_f64 of NaN returned {val}, expected NaN");
}
Err(_) => {
}
}
}
#[test]
fn a7_eval_f64_neg_infinity() {
let ctx = Context::new();
let neg_inf = ctx.neg_infinity();
let result = neg_inf.eval_f64();
match result {
Ok(val) => {
assert!(
val.is_infinite() && val < 0.0,
"eval_f64 of -∞ returned {val}, expected -inf"
);
}
Err(_) => {
}
}
}
#[test]
fn a8_compile_constant_expression_no_vars() {
let ctx = Context::new();
let expr = &ctx.int(3) + &ctx.int(4);
let result = expr.compile(&[]);
match result {
Ok(f) => {
let val = f(&[]);
assert!(
(val - 7.0).abs() < 1e-10,
"BUG: compile of 3+4 with no vars returned {val}, expected 7.0"
);
}
Err(_) => panic!("BUG: compile of pure constant 3+4 with &[] returned Err"),
}
}
#[test]
fn a9_eval_f64_large_integer() {
let ctx = Context::new();
let big = ctx.int(i64::MAX);
let result = big.eval_f64();
match result {
Ok(val) => {
assert!(
val > 0.0,
"large int should eval to positive f64, got {val}"
);
}
Err(e) => {
eprintln!("Note: eval_f64(i64::MAX) returned Err: {e}");
}
}
}
#[test]
fn a10_eval_f64_imaginary_unit() {
let ctx = Context::new();
let i = ctx.i_unit();
let result = i.eval_f64();
assert!(
result.is_err(),
"BUG: eval_f64 of i should return Err, got Ok({:?})",
result.ok()
);
}
#[test]
fn b1_matrix_0x0_not_constructible() {
let result = Matrix::new(vec![]);
assert!(
result.is_err(),
"BUG: Matrix::new(vec![]) should return Err"
);
}
#[test]
fn b2_matrix_empty_row() {
let result = Matrix::new(vec![vec![]]);
assert!(
result.is_err(),
"BUG: Matrix::new(vec![vec![]]) should return Err"
);
}
#[test]
fn b3_matrix_1x1_det() {
let ctx = Context::new();
let five = ctx.int(5);
let m = Matrix::new(vec![vec![five.clone()]]).unwrap();
let d = m.det().unwrap();
assert_eq!(
format!("{d}"),
"5",
"BUG: det of [[5]] should be 5, got {d}"
);
}
#[test]
fn b4_matrix_1x1_inv() {
let ctx = Context::new();
let five = ctx.int(5);
let m = Matrix::new(vec![vec![five]]).unwrap();
let inv = m.inv().unwrap();
let entry = inv.get(0, 0);
let s = format!("{entry}");
assert!(
s == "1/5" || s == "0.2",
"BUG: inv of [[5]] should be [[1/5]], got [[{s}]]"
);
}
#[test]
fn b5_matrix_1x1_eigenvals() {
let ctx = Context::new();
let five = ctx.int(5);
let m = Matrix::new(vec![vec![five]]).unwrap();
let evals = m.eigenvals();
match evals {
Ok(vals) => {
assert!(
vals.len() == 1,
"BUG: eigenvals of [[5]] should have exactly 1 eigenvalue, got {}",
vals.len()
);
if vals.len() == 1 {
let s = format!("{}", vals[0]);
assert_eq!(s, "5", "BUG: eigenvalue of [[5]] should be 5, got {s}");
}
}
Err(e) => panic!("BUG: eigenvals of [[5]] failed: {e}"),
}
}
#[test]
fn b6_det_non_square() {
let ctx = Context::new();
let m = Matrix::new(vec![
vec![ctx.int(1), ctx.int(2), ctx.int(3)],
vec![ctx.int(4), ctx.int(5), ctx.int(6)],
])
.unwrap();
let result = m.det();
assert!(result.is_err(), "BUG: det of 2×3 matrix should return Err");
}
#[test]
fn b7_inv_singular_matrix() {
let ctx = Context::new();
let m = Matrix::new(vec![
vec![ctx.int(1), ctx.int(2)],
vec![ctx.int(2), ctx.int(4)],
])
.unwrap();
let result = m.inv();
assert!(
result.is_err(),
"BUG: inv of singular matrix should return Err, got Ok"
);
}
#[test]
fn b8_eigenvals_non_square() {
let ctx = Context::new();
let m = Matrix::new(vec![
vec![ctx.int(1), ctx.int(2), ctx.int(3)],
vec![ctx.int(4), ctx.int(5), ctx.int(6)],
])
.unwrap();
let result = m.eigenvals();
assert!(
result.is_err(),
"BUG: eigenvals of non-square matrix should return Err"
);
}
#[test]
fn b9_inv_non_square() {
let ctx = Context::new();
let m = Matrix::new(vec![
vec![ctx.int(1), ctx.int(2), ctx.int(3)],
vec![ctx.int(4), ctx.int(5), ctx.int(6)],
])
.unwrap();
let result = m.inv();
assert!(
result.is_err(),
"BUG: inv of non-square matrix should return Err"
);
}
#[test]
fn b10_trace_non_square() {
let ctx = Context::new();
let m = Matrix::new(vec![
vec![ctx.int(1), ctx.int(2), ctx.int(3)],
vec![ctx.int(4), ctx.int(5), ctx.int(6)],
])
.unwrap();
let result = m.trace();
assert!(
result.is_err(),
"BUG: trace of non-square matrix should return Err"
);
}
#[test]
fn b11_matrix_jagged_rows() {
let ctx = Context::new();
let result = Matrix::new(vec![
vec![ctx.int(1), ctx.int(2)],
vec![ctx.int(3)], ]);
assert!(result.is_err(), "BUG: jagged rows should return Err");
}
#[test]
fn b12_matrix_inv_zero_matrix() {
let ctx = Context::new();
let m = Matrix::new(vec![
vec![ctx.int(0), ctx.int(0)],
vec![ctx.int(0), ctx.int(0)],
])
.unwrap();
let result = m.inv();
assert!(result.is_err(), "BUG: inv of zero matrix should return Err");
}
#[test]
fn b13_char_poly_non_square() {
let ctx = Context::new();
let m = Matrix::new(vec![
vec![ctx.int(1), ctx.int(2), ctx.int(3)],
vec![ctx.int(4), ctx.int(5), ctx.int(6)],
])
.unwrap();
let lam = ctx.symbol("lambda");
let result = m.char_poly(&lam);
assert!(
result.is_err(),
"BUG: char_poly of non-square matrix should return Err"
);
}
#[test]
fn b14_identity_matrix_inv() {
let ctx = Context::new();
let id = Matrix::identity(&ctx, 3);
let inv = id.inv().unwrap();
for i in 0..3 {
for j in 0..3 {
let entry = inv.get(i, j).eval();
let s = format!("{entry}");
if i == j {
assert_eq!(
s, "1",
"BUG: I^-1 diagonal entry ({i},{j}) = {s}, expected 1"
);
} else {
assert_eq!(
s, "0",
"BUG: I^-1 off-diagonal entry ({i},{j}) = {s}, expected 0"
);
}
}
}
}
#[test]
fn c1_solve_zero_for_x() {
let ctx = Context::new();
let x = ctx.symbol("x");
let zero = ctx.int(0);
let result = zero.solve(&x);
assert!(
matches!(result, Err(SymplexError::InfiniteSolutions { .. })),
"solve(0, x) = {result:?}"
);
}
#[test]
fn c2_solve_constant_nonzero() {
let ctx = Context::new();
let x = ctx.symbol("x");
let five = ctx.int(5);
let result = five.solve(&x);
match result {
Ok(sols) => {
assert!(
sols.is_empty(),
"BUG: solve(5, x) should return empty, got {} solution(s): {:?}",
sols.len(),
sols.iter().map(|s| format!("{s}")).collect::<Vec<_>>()
);
}
Err(_) => {
}
}
}
#[test]
fn c3_solve_x_for_x() {
let ctx = Context::new();
let x = ctx.symbol("x");
let solutions = x.solve_or_empty(&x);
assert!(!solutions.is_empty(), "BUG: solve(x, x) should find x=0");
if !solutions.is_empty() {
let s = format!("{}", solutions[0]);
assert_eq!(s, "0", "BUG: solve(x, x) should give 0, got {s}");
}
}
#[test]
fn c4_solve_x_squared() {
let ctx = Context::new();
let x = ctx.symbol("x");
let expr = x.powi(2);
let solutions = expr.solve_or_empty(&x);
assert!(
!solutions.is_empty(),
"BUG: solve(x^2, x) should find at least one root (0)"
);
for sol in &solutions {
let s = format!("{sol}");
assert_eq!(
s, "0",
"BUG: solve(x^2, x) should only have root 0, got {s}"
);
}
}
#[test]
fn c5_solve_sin_x() {
let ctx = Context::new();
let x = ctx.symbol("x");
let expr = x.sin();
let sols = expr.solve(&x).expect("sin(x) = 0 is solvable");
let mut strs: Vec<String> = sols.iter().map(|s| s.to_string()).collect();
strs.sort();
assert_eq!(strs, ["0", "pi"], "solve(sin(x), x)");
for s in &sols {
let r = expr.subs(&x, s).eval_f64().unwrap();
assert!(r.abs() < 1e-12, "sin({s}) = {r}");
}
let fam = expr.solve_general(&x).unwrap();
assert_eq!(fam.parameters.len(), 1);
let at_k3: Vec<f64> = fam
.instance(3)
.iter()
.map(|e| e.eval_f64().unwrap())
.collect();
assert!(
at_k3
.iter()
.all(|v| (v / std::f64::consts::PI - (v / std::f64::consts::PI).round()).abs() < 1e-12),
"{at_k3:?}"
);
}
#[test]
fn c6_solve_exp_x() {
let ctx = Context::new();
let x = ctx.symbol("x");
let expr = x.exp();
let result = expr.solve(&x);
match result {
Ok(sols) => {
let spurious: Vec<String> = sols
.iter()
.filter(|s| {
let txt = format!("{s}");
txt.contains("ln(0)") || txt.contains("oo") || txt.contains("-oo")
})
.map(|s| format!("{s}"))
.collect();
assert!(
sols.is_empty(),
"BUG (solve/exp): solve(exp(x), x) should return empty — exp(x) is never zero. \
Got {} solution(s): {:?}. Spurious entries: {:?}",
sols.len(),
sols.iter().map(|s| format!("{s}")).collect::<Vec<_>>(),
spurious,
);
}
Err(_) => {
}
}
}
#[test]
fn c7_solve_x_squared_plus_1() {
let ctx = Context::new();
let x = ctx.symbol("x");
let expr = &x.powi(2) + 1;
let sols = expr.solve(&x).expect("x^2 + 1 = 0 has complex roots");
let mut vals: Vec<Complex64> = sols.iter().map(|s| s.eval_complex64().unwrap()).collect();
vals.sort_by(|a, b| a.im.partial_cmp(&b.im).unwrap());
assert_eq!(vals.len(), 2, "solve(x^2 + 1, x) = {sols:?}");
assert!(
(vals[0].re).abs() < 1e-12 && (vals[0].im + 1.0).abs() < 1e-12,
"{vals:?}"
);
assert!(
(vals[1].re).abs() < 1e-12 && (vals[1].im - 1.0).abs() < 1e-12,
"{vals:?}"
);
}
#[test]
fn c8_solve_linear() {
let ctx = Context::new();
let x = ctx.symbol("x");
let expr = &(&x * 2) - 6;
let solutions = expr.solve_or_empty(&x);
assert_eq!(
solutions.len(),
1,
"BUG: solve(2x-6, x) should have 1 solution"
);
let s = format!("{}", solutions[0]);
assert_eq!(s, "3", "BUG: solve(2x-6, x) should give 3, got {s}");
}
#[test]
fn c9_solve_or_empty_on_impossible() {
let ctx = Context::new();
let x = ctx.symbol("x");
let expr = x.exp() + x.sin() + 1;
let solutions = expr.solve_or_empty(&x);
for s in &solutions {
let r = expr.subs(&x, s).eval_f64().unwrap_or(f64::NAN);
assert!(r.abs() < 1e-9, "claimed root {s} has residual {r}");
}
}
#[test]
fn d1_series_1_over_x_at_zero() {
let ctx = Context::new();
let x = ctx.symbol("x");
let zero = ctx.int(0);
let expr = &ctx.int(1) / &x;
let result = expr.series(&x, &zero, 5);
assert_eq!(result, expr, "series(1/x, x, 0, 5) = {result}");
}
#[test]
fn d2_series_tan_at_pi_over_2() {
let ctx = Context::new();
let x = ctx.symbol("x");
let pi_half = &ctx.pi() / &ctx.int(2);
let expr = x.tan();
let result = expr.series(&x, &pi_half, 3);
let s = format!("{result}");
eprintln!("series(tan(x), x, pi/2, 3) = {s}");
assert!(
!s.contains("tan(1/2*pi)"),
"BUG (series/pole): series(tan(x), x, pi/2, 3) contains unevaluated tan(pi/2) \
which is ±∞. The series engine should detect the pole and either produce a \
Laurent series or return an error. Got: {s}"
);
}
#[test]
fn d3_series_ln_at_zero() {
let ctx = Context::new();
let x = ctx.symbol("x");
let zero = ctx.int(0);
let expr = x.ln();
let result = expr.series(&x, &zero, 5);
assert!(
result.has_unevaluated(),
"series(ln(x), x, 0, 5) = {result}"
);
assert!(expr.try_series(&x, &zero, 5).is_err());
}
#[test]
fn d4_series_exp_1_over_x_at_zero() {
let ctx = Context::new();
let x = ctx.symbol("x");
let zero = ctx.int(0);
let one_over_x = &ctx.int(1) / &x;
let expr = one_over_x.exp();
let result = expr.series(&x, &zero, 5);
assert!(
result.has_unevaluated(),
"series(exp(1/x), x, 0, 5) = {result}"
);
}
#[test]
fn d5_series_order_zero() {
let ctx = Context::new();
let x = ctx.symbol("x");
let zero = ctx.int(0);
let result = x.series(&x, &zero, 0);
assert!(result.is_zero_structural(), "series(x, x, 0, 0) = {result}");
assert!(x.series(&x, &zero, 1).is_zero_structural());
assert_eq!(x.series(&x, &zero, 2), x);
}
#[test]
fn d6_series_exp_at_zero_order_4() {
let ctx = Context::new();
let x = ctx.symbol("x");
let zero = ctx.int(0);
let expr = x.exp();
let result = expr.series(&x, &zero, 4);
let expanded = result.expand().eval();
assert_eq!(expanded.coeff(&x, 0), Some(ctx.int(1)), "{expanded}");
assert_eq!(expanded.coeff(&x, 1), Some(ctx.int(1)), "{expanded}");
assert_eq!(
expanded.coeff(&x, 2),
Some(ctx.rational(1, 2)),
"{expanded}"
);
assert_eq!(
expanded.coeff(&x, 3),
Some(ctx.rational(1, 6)),
"{expanded}"
);
assert_eq!(expanded.degree(&x), Some(3), "{expanded}");
}
#[test]
fn d7_series_constant() {
let ctx = Context::new();
let x = ctx.symbol("x");
let zero = ctx.int(0);
let five = ctx.int(5);
let result = five.series(&x, &zero, 3);
assert_eq!(result, five, "series(5, x, 0, 3) = {result}");
}
#[test]
fn e1_neg_of_positive_is_not_positive() {
let ctx = Context::new();
let x = ctx.symbol("x").assume(Assumption::Positive);
let neg_x = -&x;
let result = neg_x.is_positive();
assert_ne!(
result,
Some(true),
"BUG: -x should not be positive when x is positive"
);
eprintln!("(-x).is_positive() when x is positive = {:?}", result);
}
#[test]
fn e2_twice_integer_is_even() {
let ctx = Context::new();
let x = ctx.symbol("x").assume(Assumption::Integer);
let two_x = &x * 2;
let result = two_x.is_even();
eprintln!("(2*x).is_even() when x is integer = {:?}", result);
assert_ne!(
result,
Some(false),
"BUG: 2*x should not be odd when x is integer"
);
}
#[test]
fn e3_square_of_real_is_nonnegative() {
let ctx = Context::new();
let x = ctx.symbol("x").assume(Assumption::Real);
let x_sq = x.powi(2);
let result = x_sq.is_nonnegative();
eprintln!("(x^2).is_nonnegative() when x is real = {:?}", result);
assert_ne!(
result,
Some(false),
"BUG: x^2 should never be reported as possibly negative for real x"
);
}
#[test]
fn e4_positive_implies_real() {
let ctx = Context::new();
let x = ctx.symbol("x").assume(Assumption::Positive);
let is_real = x.is_real();
let is_nn = x.is_nonnegative();
let is_nz = x.is_nonzero();
eprintln!("x positive → is_real={is_real:?}, is_nonneg={is_nn:?}, is_nonzero={is_nz:?}");
if is_real == Some(false) {
panic!("BUG: positive x should be real");
}
if is_nn == Some(false) {
panic!("BUG: positive x should be nonnegative");
}
}
#[test]
fn e5_zero_is_zero() {
let ctx = Context::new();
let z = ctx.int(0);
assert_eq!(
z.is_zero(),
Some(true),
"BUG: 0.is_zero() should be Some(true)"
);
}
#[test]
fn e6_integer_positive_is_natural() {
let ctx = Context::new();
let x = ctx
.symbol("x")
.assume(Assumption::Integer)
.assume(Assumption::Positive);
let result = x.is_nonnegative();
assert_ne!(
result,
Some(false),
"BUG: integer positive x should be nonnegative"
);
}
#[test]
fn e7_sum_of_positives_is_positive() {
let ctx = Context::new();
let x = ctx.symbol("x").assume(Assumption::Positive);
let y = ctx.symbol("y").assume(Assumption::Positive);
let sum = &x + &y;
let result = sum.is_positive();
eprintln!("(x+y).is_positive() when both positive = {:?}", result);
assert_ne!(
result,
Some(false),
"BUG: x+y should not be non-positive when both x,y are positive"
);
}
#[test]
fn e8_product_of_positives_is_positive() {
let ctx = Context::new();
let x = ctx.symbol("x").assume(Assumption::Positive);
let y = ctx.symbol("y").assume(Assumption::Positive);
let prod = &x * &y;
let result = prod.is_positive();
eprintln!("(x*y).is_positive() when both positive = {:?}", result);
assert_ne!(
result,
Some(false),
"BUG: x*y should not be non-positive when both x,y are positive"
);
}
#[test]
fn e9_is_positive_on_literal_negative() {
let ctx = Context::new();
let neg3 = ctx.int(-3);
let result = neg3.is_positive();
assert_eq!(
result,
Some(false),
"BUG: (-3).is_positive() should be Some(false), got {:?}",
result
);
}
#[test]
fn e10_is_integer_on_literal_integer() {
let ctx = Context::new();
let n = ctx.int(42);
let result = n.is_integer();
assert_eq!(
result,
Some(true),
"BUG: 42.is_integer() should be Some(true), got {:?}",
result
);
}
#[test]
fn f1_concurrent_simplify_and_diff() {
use std::thread;
let ctx = Context::new();
let handles: Vec<_> = (0..8)
.map(|t| {
let ctx = ctx.clone();
thread::spawn(move || {
let x = ctx.symbol("x");
for i in 1..=100 {
let expr = &x.powi(i % 7 + 1) + &(&x * ctx.int(i)) + ctx.int(i * i);
let simplified = expr.simplify();
let deriv = simplified.diff(&x);
let s = format!("{deriv}");
assert!(!s.is_empty(), "Thread {t}: empty derivative string");
}
})
})
.collect();
for (i, h) in handles.into_iter().enumerate() {
h.join()
.unwrap_or_else(|e| panic!("Thread {i} panicked: {e:?}"));
}
}
#[test]
fn f2_concurrent_same_expression() {
use std::sync::Arc;
use std::thread;
let ctx = Context::new();
let x = ctx.symbol("x");
let expr = &x.powi(2) + &(&x * 2) + 1;
let expr = Arc::new(expr);
let handles: Vec<_> = (0..4)
.map(|_| {
let expr = Arc::clone(&expr);
thread::spawn(move || {
let result = expr.simplify();
format!("{result}")
})
})
.collect();
let results: Vec<String> = handles.into_iter().map(|h| h.join().unwrap()).collect();
let first = &results[0];
for (i, r) in results.iter().enumerate() {
assert_eq!(
r, first,
"BUG: thread {i} got different simplify result: {r} vs {first}"
);
}
}
#[test]
fn f3_concurrent_eval_f64() {
use std::thread;
let ctx = Context::new();
let handles: Vec<_> = (0..8)
.map(|t| {
let ctx = ctx.clone();
thread::spawn(move || {
for i in 1..=50 {
let x = ctx.symbol("x");
let val = ctx.int(i);
let expr = &x.powi(2) + 1;
let subbed = expr.subs_i64(&x, i);
let result = subbed.eval_f64();
match result {
Ok(v) => {
let expected = (i * i + 1) as f64;
assert!(
(v - expected).abs() < 1e-6,
"Thread {t}: eval_f64 of {i}^2+1 = {v}, expected {expected}"
);
}
Err(e) => panic!("Thread {t}: eval_f64 failed: {e}"),
}
drop(val);
}
})
})
.collect();
for (i, h) in handles.into_iter().enumerate() {
h.join()
.unwrap_or_else(|e| panic!("Thread {i} panicked: {e:?}"));
}
}
#[test]
fn f4_concurrent_solve() {
use std::thread;
let ctx = Context::new();
let handles: Vec<_> = (0..4)
.map(|t| {
let ctx = ctx.clone();
thread::spawn(move || {
let x = ctx.symbol("x");
for i in 1i64..=20 {
let expr = &x - ctx.int(i);
let sols = expr.solve_or_empty(&x);
assert!(
!sols.is_empty(),
"Thread {t}: solve(x - {i}, x) returned empty"
);
let s = format!("{}", sols[0]);
assert_eq!(
s,
format!("{i}"),
"Thread {t}: solve(x - {i}, x) gave {s}, expected {i}"
);
}
})
})
.collect();
for (i, h) in handles.into_iter().enumerate() {
h.join()
.unwrap_or_else(|e| panic!("Thread {i} panicked: {e:?}"));
}
}
#[test]
fn g1_diff_constant() {
let ctx = Context::new();
let x = ctx.symbol("x");
let five = ctx.int(5);
let deriv = five.diff(&x);
assert!(
deriv.is_zero_structural(),
"BUG: diff(5, x) should be 0, got {deriv}"
);
}
#[test]
fn g2_integrate_zero() {
let ctx = Context::new();
let x = ctx.symbol("x");
let zero = ctx.int(0);
let result = zero.integrate(&x);
let s = format!("{result}");
assert!(
result.is_zero_structural() || s == "0",
"BUG: integrate(0, x) should be 0, got {s}"
);
}
#[test]
fn g3_factor_pure_number() {
let ctx = Context::new();
let x = ctx.symbol("x");
let twelve = ctx.int(12);
let result = twelve.factor(&x);
assert_eq!(result, twelve, "factor(12, x) = {result}");
}
#[test]
fn g4_expand_atom() {
let ctx = Context::new();
let x = ctx.symbol("x");
let result = x.expand();
let s = format!("{result}");
assert_eq!(s, "x", "BUG: expand(x) should return x, got {s}");
}
#[test]
fn g5_simplify_number() {
let ctx = Context::new();
let n = ctx.int(42);
let result = n.simplify();
let s = format!("{result}");
assert_eq!(s, "42", "BUG: simplify(42) should return 42, got {s}");
}
#[test]
fn g6_very_long_symbol_name() {
let ctx = Context::new();
let long_name: String = (0..200).map(|i| format!("a{i}_")).collect();
let sym = ctx.symbol(&long_name);
let expr = &sym + 1;
let deriv = expr.diff(&sym);
let s = format!("{deriv}");
assert_eq!(
s, "1",
"BUG: diff of long-named symbol should be 1, got {s}"
);
}
#[test]
fn g7_symbol_with_underscore() {
let ctx = Context::new();
let x1 = ctx.symbol("x_1");
let expr = &x1.powi(2) + 1;
let s = format!("{expr}");
assert!(
s.contains("x_1"),
"Symbol x_1 should appear in expression: {s}"
);
}
#[test]
fn g8_symbol_with_prime() {
let ctx = Context::new();
let x_prime = ctx.symbol("x'");
let expr = &x_prime + 1;
assert_eq!(format!("{expr}"), "x' + 1");
assert_ne!(x_prime, ctx.symbol("x"));
assert_eq!(expr.free_symbols().len(), 1);
}
#[test]
fn g9_diff_zero() {
let ctx = Context::new();
let x = ctx.symbol("x");
let zero = ctx.int(0);
let deriv = zero.diff(&x);
assert!(
deriv.is_zero_structural(),
"BUG: diff(0, x) should be 0, got {deriv}"
);
}
#[test]
fn g10_diff_variable_wrt_itself() {
let ctx = Context::new();
let x = ctx.symbol("x");
let deriv = x.diff(&x);
let s = format!("{deriv}");
assert_eq!(s, "1", "BUG: diff(x, x) should be 1, got {s}");
}
#[test]
fn g11_diff_variable_wrt_other() {
let ctx = Context::new();
let x = ctx.symbol("x");
let y = ctx.symbol("y");
let deriv = x.diff(&y);
assert!(
deriv.is_zero_structural(),
"BUG: diff(x, y) should be 0, got {deriv}"
);
}
#[test]
fn g12_subs_chain() {
let ctx = Context::new();
let x = ctx.symbol("x");
let y = ctx.symbol("y");
let expr = x.powi(2);
let step1 = expr.subs(&x, &y);
let step2 = step1.subs_i64(&y, 5);
let val = step2.eval_f64().unwrap();
assert!(
(val - 25.0).abs() < 1e-10,
"BUG: (x^2)[x→y][y→5] should be 25, got {val}"
);
}
#[test]
fn g13_eval_of_pi() {
let ctx = Context::new();
let pi = ctx.pi();
let val = pi.eval_f64().unwrap();
assert!(
(val - std::f64::consts::PI).abs() < 1e-10,
"BUG: pi.eval_f64() = {val}, expected {}",
std::f64::consts::PI
);
}
#[test]
fn g14_eval_of_e() {
let ctx = Context::new();
let e = ctx.e();
let val = e.eval_f64().unwrap();
assert!(
(val - std::f64::consts::E).abs() < 1e-10,
"BUG: e.eval_f64() = {val}, expected {}",
std::f64::consts::E
);
}
#[test]
fn g15_factorize_pure_number() {
let ctx = Context::new();
let n = ctx.int(12);
let factors = n.factorize();
match factors {
Some(f) => {
eprintln!("factorize(12) = {:?}", f);
assert!(!f.is_empty(), "factorize(12) returned empty vec");
}
None => {
panic!("BUG: factorize(12) returned None");
}
}
}
#[test]
fn g16_factorize_zero() {
let ctx = Context::new();
let zero = ctx.int(0);
let factors = zero.factorize();
assert!(factors.is_none(), "factorize(0) = {factors:?}");
}
#[test]
fn g17_factorize_one() {
let ctx = Context::new();
let one = ctx.int(1);
let factors = one.factorize();
assert_eq!(
factors.as_deref(),
Some(&[][..]),
"factorize(1) = {factors:?}"
);
}
#[test]
fn g18_factorize_negative() {
let ctx = Context::new();
let n = ctx.int(-12);
let factors = n.factorize();
let f = factors.expect("factorize(-12)");
let shown: Vec<(String, u32)> = f.iter().map(|(p, e)| (p.to_string(), *e)).collect();
assert_eq!(shown, [("2".to_string(), 2), ("3".to_string(), 1)]);
}
#[test]
fn g19_double_simplify() {
let ctx = Context::new();
let x = ctx.symbol("x");
let expr = &x.powi(2) + &(&x * 2) + 1;
let s1 = expr.simplify();
let s1_str = format!("{s1}");
let s2 = s1.simplify();
let s2_str = format!("{s2}");
assert_eq!(
s1_str, s2_str,
"BUG: simplify should be idempotent: first={s1_str}, second={s2_str}"
);
}
#[test]
fn g20_display_special_values() {
let ctx = Context::new();
let inf = ctx.infinity();
let neg_inf = ctx.neg_infinity();
let nan = ctx.nan();
let i = ctx.i_unit();
let pi = ctx.pi();
let e = ctx.e();
let s_inf = format!("{inf}");
let s_neg_inf = format!("{neg_inf}");
let s_nan = format!("{nan}");
let s_i = format!("{i}");
let s_pi = format!("{pi}");
let s_e = format!("{e}");
assert!(!s_inf.is_empty(), "Display of ∞ is empty");
assert!(!s_neg_inf.is_empty(), "Display of -∞ is empty");
assert!(!s_nan.is_empty(), "Display of NaN is empty");
assert!(!s_i.is_empty(), "Display of i is empty");
assert!(!s_pi.is_empty(), "Display of π is empty");
assert!(!s_e.is_empty(), "Display of e is empty");
eprintln!("inf={s_inf}, -inf={s_neg_inf}, nan={s_nan}, i={s_i}, pi={s_pi}, e={s_e}");
}
#[test]
fn g21_arithmetic_with_infinity() {
let ctx = Context::new();
let inf = ctx.infinity();
let x = ctx.symbol("x");
let r1 = &inf + &x;
assert_eq!(r1, inf, "inf + x = {r1}");
let r2 = &inf * 2;
assert_eq!(r2, inf, "inf * 2 = {r2}");
let zero = ctx.int(0);
let r3 = &zero * &inf;
assert!(!r3.has_unevaluated(), "0 * inf = {r3}");
}
#[test]
fn g22_arithmetic_with_nan() {
let ctx = Context::new();
let nan = ctx.nan();
let x = ctx.symbol("x");
let r1 = &nan + &x;
assert_eq!(r1, nan, "nan + x = {r1}");
let r2 = &nan * &ctx.int(2);
assert_eq!(r2, nan, "nan * 2 = {r2}");
}
#[test]
fn g23_compile_with_pi_and_e() {
let ctx = Context::new();
let x = ctx.symbol("x");
let expr = &x + &ctx.pi();
let func = expr.compile(&["x"]);
match func {
Ok(f) => {
let val = f(&[0.0]);
assert!(
(val - std::f64::consts::PI).abs() < 1e-10,
"BUG: compiled (x + pi) at x=0 gave {val}, expected pi"
);
}
Err(_) => {
panic!("BUG: compile(x + pi) returned None");
}
}
}
#[test]
fn g24_eval_nested_function() {
let ctx = Context::new();
let zero = ctx.int(0);
let expr = zero.cos().sin();
let val = expr.eval_f64().unwrap();
let expected = 1.0_f64.sin();
assert!(
(val - expected).abs() < 1e-10,
"BUG: sin(cos(0)) = {val}, expected {expected}"
);
}
#[test]
fn g25_is_constant_checks() {
let ctx = Context::new();
let x = ctx.symbol("x");
let five = ctx.int(5);
let pi = ctx.pi();
let expr_with_x = &x + 1;
assert!(five.is_constant(), "BUG: 5 should be constant");
assert!(pi.is_constant(), "BUG: pi should be constant");
assert!(
!expr_with_x.is_constant(),
"BUG: x+1 should not be constant"
);
}
#[test]
fn g26_subs_with_itself() {
let ctx = Context::new();
let x = ctx.symbol("x");
let result = x.subs(&x, &x);
let s = format!("{result}");
assert_eq!(s, "x", "BUG: x.subs(x, x) should be x, got {s}");
}
#[test]
fn g27_eval_rational() {
let ctx = Context::new();
let third = ctx.rational(1, 3);
let val = third.eval_f64().unwrap();
assert!(
(val - 1.0 / 3.0).abs() < 1e-10,
"BUG: eval_f64(1/3) = {val}, expected ~0.333"
);
}
#[test]
fn g28_rational_zero_denominator() {
let ctx = Context::new();
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
let r = ctx.rational(1, 0);
format!("{r}")
}));
assert_eq!(result.ok().as_deref(), Some("zoo"));
assert_eq!(format!("{}", &ctx.int(1) / &ctx.int(0)), "zoo");
assert_eq!(ctx.rational(0, 0), ctx.nan());
}
#[test]
fn bug_rational_zero_denominator_panics() {
let ctx = Context::new();
let r = ctx.rational(1, 0);
assert_eq!(format!("{r}"), "zoo");
}
#[test]
fn g29_matrix_determinant_identity() {
let ctx = Context::new();
for n in 1..=5 {
let id = Matrix::identity(&ctx, n);
let d = id.det().unwrap();
let val = d.eval_f64().unwrap();
assert!(
(val - 1.0).abs() < 1e-10,
"BUG: det(I_{n}) = {val}, expected 1.0"
);
}
}
#[test]
fn g30_free_symbols_of_constant() {
let ctx = Context::new();
let five = ctx.int(5);
let syms = five.free_symbols();
assert!(
syms.is_empty(),
"BUG: free_symbols of 5 should be empty, got {:?}",
syms.iter().map(|s| format!("{s}")).collect::<Vec<_>>()
);
}
#[test]
fn g31_free_symbols_of_expression() {
let ctx = Context::new();
let x = ctx.symbol("x");
let y = ctx.symbol("y");
let expr = &x.powi(2) + &y;
let syms = expr.free_symbols();
assert_eq!(
syms.len(),
2,
"BUG: free_symbols of x^2+y should have 2 symbols, got {}",
syms.len()
);
}
#[test]
fn g32_count_ops_empty_and_atom() {
let ctx = Context::new();
let five = ctx.int(5);
let x = ctx.symbol("x");
assert_eq!(five.count_ops(), 0);
assert_eq!(x.count_ops(), 0);
assert_eq!((&x + &five).count_ops(), 1);
}
#[test]
fn g33_expr_type_checks() {
let ctx = Context::new();
let five = ctx.int(5);
let x = ctx.symbol("x");
let sum = &x + 1;
match five.expr_type() {
ExprType::Number => {} other => panic!("BUG: expr_type of 5 should be Number, got {other:?}"),
}
match x.expr_type() {
ExprType::Symbol => {} other => panic!("BUG: expr_type of x should be Symbol, got {other:?}"),
}
match sum.expr_type() {
ExprType::Add => {} other => eprintln!("Note: expr_type of x+1 is {other:?} (may be canonicalized)"),
}
}
#[test]
fn g34_integrate_constant() {
let ctx = Context::new();
let x = ctx.symbol("x");
let five = ctx.int(5);
let result = five.integrate(&x);
let s = format!("{result}");
assert!(
s.contains("x"),
"BUG: integrate(5, x) should contain x, got {s}"
);
let round_trip = result.diff(&x);
let rs = format!("{round_trip}");
assert!(
rs == "5",
"BUG: d/dx(integrate(5, x)) should be 5, got {rs}"
);
}
#[test]
fn g35_matrix_scale_by_zero() {
let ctx = Context::new();
let m = matrix![ctx, [1, 2], [3, 4]];
let zero = ctx.int(0);
let result = m.scale(&zero);
for i in 0..2 {
for j in 0..2 {
let entry = result.get(i, j).eval();
assert!(
entry.is_zero_structural(),
"BUG: zero-scaled matrix entry ({i},{j}) = {entry}, expected 0"
);
}
}
}
#[test]
fn g36_matrix_add_self() {
let ctx = Context::new();
let m = matrix![ctx, [1, 2], [3, 4]];
let sum = &m + &m;
let doubled = m.scale(&ctx.int(2));
for i in 0..2 {
for j in 0..2 {
let s_entry = sum.get(i, j).eval_f64().unwrap();
let d_entry = doubled.get(i, j).eval_f64().unwrap();
assert!(
(s_entry - d_entry).abs() < 1e-10,
"BUG: (M+M)[{i},{j}] = {s_entry}, 2*M[{i},{j}] = {d_entry}"
);
}
}
}
#[test]
fn g37_solve_quadratic() {
let ctx = Context::new();
let x = ctx.symbol("x");
let expr = &x.powi(2) - &(&x * 5) + 6;
let solutions = expr.solve_or_empty(&x);
let mut strs: Vec<String> = solutions.iter().map(|s| format!("{s}")).collect();
strs.sort();
assert!(
strs.contains(&"2".to_string()) && strs.contains(&"3".to_string()),
"BUG: roots of x^2-5x+6 should be {{2,3}}, got {strs:?}"
);
}
#[test]
fn g38_expand_product() {
let ctx = Context::new();
let x = ctx.symbol("x");
let expr = (&x + 1).powi(2);
let expanded = expr.expand();
let s = format!("{expanded}");
assert!(
s.contains("x^2"),
"BUG: expand((x+1)^2) should contain x^2: {s}"
);
}
#[test]
fn g39_eval_decimal_of_sqrt2() {
let ctx = Context::new();
let two = ctx.int(2);
let sqrt2 = two.sqrt();
let val = sqrt2.eval_f64().unwrap();
assert!(
(val - std::f64::consts::SQRT_2).abs() < 1e-10,
"BUG: sqrt(2).eval_f64() = {val}, expected {}",
std::f64::consts::SQRT_2
);
}
#[test]
fn g40_multiple_symbols_same_name() {
let ctx = Context::new();
let x1 = ctx.symbol("x");
let x2 = ctx.symbol("x");
let diff = &x1 - &x2;
assert!(
diff.is_zero_structural(),
"BUG: two symbols with same name should be identical, diff = {diff}"
);
}
#[test]
fn g41_compile_then_eval_many_points() {
let ctx = Context::new();
let x = ctx.symbol("x");
let expr = &x.powi(3) - &x + 1;
let f = expr.compile(&["x"]).expect("should compile x^3 - x + 1");
for i in -100..=100 {
let xv = i as f64 / 10.0;
let got = f(&[xv]);
let expected = xv.powi(3) - xv + 1.0;
assert!(
(got - expected).abs() < 1e-6,
"BUG: compiled fn at x={xv}: got {got}, expected {expected}"
);
}
}
#[test]
fn g42_compile_two_variables() {
let ctx = Context::new();
let x = ctx.symbol("x");
let y = ctx.symbol("y");
let expr = &x * &y + &x + &y;
let f = expr.compile(&["x", "y"]).expect("should compile x*y+x+y");
let val = f(&[3.0, 4.0]);
let expected = 3.0 * 4.0 + 3.0 + 4.0; assert!(
(val - expected).abs() < 1e-10,
"BUG: compiled x*y+x+y at (3,4) = {val}, expected {expected}"
);
}
#[test]
fn g43_series_sin_maclaurin() {
let ctx = Context::new();
let x = ctx.symbol("x");
let expr = x.sin();
let result = expr.maclaurin(&x, 5);
let expanded = result.expand();
let s = format!("{expanded}");
eprintln!("maclaurin(sin(x), 5) = {s}");
assert!(s.contains("x"), "Maclaurin of sin(x) should contain x: {s}");
}
#[test]
fn g44_context_node_count() {
let ctx = Context::new();
let initial = ctx.node_count();
let x = ctx.symbol("x");
for i in 0..100 {
let _ = &x + ctx.int(i);
}
let final_count = ctx.node_count();
assert!(
final_count >= initial,
"BUG: node_count decreased from {initial} to {final_count}"
);
}
#[test]
fn g45_args_of_various_types() {
let ctx = Context::new();
let x = ctx.symbol("x");
let five = ctx.int(5);
let sum = &x + &five;
let prod = &x * &five;
let pow = x.powi(2);
let sin_x = x.sin();
assert!(five.args().is_empty());
assert!(x.args().is_empty());
assert_eq!(sum.args().len(), 2, "args(x + 5) = {:?}", sum.args());
assert_eq!(prod.args().len(), 2, "args(x * 5) = {:?}", prod.args());
assert_eq!(pow.args(), vec![x.clone(), ctx.int(2)], "args(x^2)");
assert_eq!(sin_x.args(), vec![x.clone()], "args(sin x)");
}
#[test]
fn g46_has_unevaluated_on_concrete() {
let ctx = Context::new();
let x = ctx.symbol("x");
let expr = &x.powi(2) + 1;
assert!(
!expr.has_unevaluated(),
"BUG: x^2 + 1 should not have unevaluated forms"
);
}
#[test]
fn g47_equals_structural_vs_mathematical() {
let ctx = Context::new();
let x = ctx.symbol("x");
let e1 = (&x + 1).powi(2);
let e2 = &x.powi(2) + &(&x * 2) + 1;
let result = e1.equals(&e2);
assert_eq!(result, Some(true), "(x+1)^2 equals x^2+2x+1?");
assert_ne!(
result,
Some(false),
"BUG: (x+1)^2 should not be reported as NOT equal to x^2+2x+1"
);
}
#[test]
fn g48_solve_ode_edge_case() {
let ctx = Context::new();
let x = ctx.symbol("x");
let expr = &x + 1;
let result = expr.try_diff(&x);
match result {
Ok(d) => {
assert_eq!(format!("{d}"), "1", "diff(x+1, x) should be 1");
}
Err(e) => {
panic!("BUG: try_diff(x+1, x) returned Err: {e}");
}
}
}
#[test]
fn g49_context_default() {
let ctx = Context::default();
let x = ctx.symbol("x");
let expr = x.powi(2);
let s = format!("{expr}");
assert_eq!(s, "x^2", "BUG: Context::default() should work, got: {s}");
}
#[test]
fn g50_matrix_transpose_rectangular() {
let ctx = Context::new();
let m = Matrix::new(vec![
vec![ctx.int(1), ctx.int(2), ctx.int(3)],
vec![ctx.int(4), ctx.int(5), ctx.int(6)],
])
.unwrap();
assert_eq!(m.nrows(), 2);
assert_eq!(m.ncols(), 3);
let t = m.transpose();
assert_eq!(t.nrows(), 3, "BUG: transpose of 2×3 should have 3 rows");
assert_eq!(t.ncols(), 2, "BUG: transpose of 2×3 should have 2 cols");
let val = t.get(2, 0).eval_f64().unwrap();
assert!(
(val - 3.0).abs() < 1e-10,
"BUG: transposed element [2,0] should be 3, got {val}"
);
}