use num_bigint::BigInt;
use num_integer::Integer;
use num_rational::Ratio;
use num_traits::{One, Signed, Zero};
use rustc_hash::FxHashMap;
use smallvec::SmallVec;
use crate::base::arena::Arena;
use crate::base::node::{ExprId, ExprNode};
use crate::base::walk;
use crate::poly::Poly;
use crate::poly::multipoly::{GrevLex, MultiPoly};
pub(crate) const MAX_EXPR_POLY_DEGREE: usize = 10_000;
pub(crate) fn expr_to_poly(arena: &Arena, expr: ExprId, var: ExprId) -> Option<Poly> {
if !contains_id(arena, expr, var) {
let coeff = expr_to_rational(arena, expr)?;
return Some(Poly::constant(coeff));
}
if expr == var {
return Some(Poly::x());
}
let post_order = walk::post_order_ids(arena, expr);
let mut cache: FxHashMap<ExprId, Poly> = FxHashMap::default();
for &id in &post_order {
let poly = convert_node(arena, id, var, &cache)?;
cache.insert(id, poly);
}
cache.remove(&expr)
}
fn convert_node(
arena: &Arena,
id: ExprId,
var: ExprId,
cache: &FxHashMap<ExprId, Poly>,
) -> Option<Poly> {
if id == var {
return Some(Poly::x());
}
let node = arena.node(id);
match node {
ExprNode::Num(nid) => {
let r = arena.num(*nid).clone();
Some(Poly::constant(r))
}
ExprNode::Symbol(_) => {
if !contains_id(arena, id, var) {
None
} else {
Some(Poly::x())
}
}
ExprNode::Pi
| ExprNode::E
| ExprNode::ImaginaryUnit
| ExprNode::EulerGamma
| ExprNode::Catalan
| ExprNode::GoldenRatio
| ExprNode::PhysicalConstant(_, _) => None,
ExprNode::Infinity | ExprNode::NegInfinity | ExprNode::ComplexInfinity | ExprNode::NaN => {
None
}
ExprNode::Add(children) => {
let mut result = Poly::zero();
for &child in children.iter() {
let child_poly = cache.get(&child)?;
result = &result + child_poly;
}
Some(result)
}
ExprNode::Mul(children) => {
let mut result = Poly::from_int(1);
for &child in children.iter() {
let child_poly = cache.get(&child)?;
result = &result * child_poly;
}
Some(result)
}
ExprNode::Pow(base, exp) => {
let base_poly = cache.get(base)?;
if contains_id(arena, *exp, var) {
return None; }
let exp_val = expr_to_rational(arena, *exp)?;
if !exp_val.is_integer() {
return None; }
let n: i64 = exp_val.to_integer().try_into().ok()?;
if n < 0 {
return None; }
let n = usize::try_from(n).ok()?;
let base_degree = base_poly.degree().unwrap_or(0);
if n > MAX_EXPR_POLY_DEGREE || base_degree.checked_mul(n)? > MAX_EXPR_POLY_DEGREE {
return None;
}
let mut result = Poly::from_int(1);
for _ in 0..n {
result = &result * base_poly;
}
Some(result)
}
ExprNode::Neg(inner) => {
let inner_poly = cache.get(inner)?;
Some(-inner_poly)
}
ExprNode::Sin(_)
| ExprNode::Cos(_)
| ExprNode::Tan(_)
| ExprNode::Asin(_)
| ExprNode::Acos(_)
| ExprNode::Atan(_)
| ExprNode::Atan2(_, _)
| ExprNode::Sinh(_)
| ExprNode::Cosh(_)
| ExprNode::Tanh(_)
| ExprNode::Asinh(_)
| ExprNode::Acosh(_)
| ExprNode::Atanh(_)
| ExprNode::Exp(_)
| ExprNode::Ln(_)
| ExprNode::Abs(_)
| ExprNode::Sign(_)
| ExprNode::Heaviside(_)
| ExprNode::DiracDelta(_)
| ExprNode::Floor(_)
| ExprNode::Ceiling(_) => {
None
}
ExprNode::Apply(_, _)
| ExprNode::Derivative(_, _)
| ExprNode::Integral(_, _)
| ExprNode::DefiniteIntegral(_, _, _, _) => None,
ExprNode::Min(_)
| ExprNode::Max(_)
| ExprNode::Sum(_, _, _, _)
| ExprNode::Product_(_, _, _, _) => None,
ExprNode::Factorial(_) | ExprNode::Binomial(_, _) => None,
ExprNode::Gamma(_)
| ExprNode::LogGamma(_)
| ExprNode::Digamma(_)
| ExprNode::Erf(_)
| ExprNode::Erfc(_)
| ExprNode::LambertW(_)
| ExprNode::Beta(_, _)
| ExprNode::Re(_)
| ExprNode::Im(_)
| ExprNode::Conjugate(_)
| ExprNode::Arg(_)
| ExprNode::Si(_)
| ExprNode::Ci(_)
| ExprNode::Ei(_)
| ExprNode::Li(_)
| ExprNode::Zeta(_)
| ExprNode::Polygamma(_, _)
| ExprNode::KroneckerDelta(_, _) => None,
ExprNode::BoolTrue
| ExprNode::BoolFalse
| ExprNode::Gt(_, _)
| ExprNode::Ge(_, _)
| ExprNode::Eq_(_, _)
| ExprNode::Ne(_, _)
| ExprNode::And(_)
| ExprNode::Or(_)
| ExprNode::Not(_)
| ExprNode::Piecewise(_) => None,
ExprNode::EmptySet
| ExprNode::UniversalSet
| ExprNode::Interval(_, _, _)
| ExprNode::FiniteSet(_)
| ExprNode::SetUnion(_)
| ExprNode::SetIntersection(_)
| ExprNode::SetComplement(_, _) => None,
ExprNode::Limit(_, _, _)
| ExprNode::Series(_, _, _, _)
| ExprNode::LaplaceTransform(_, _, _)
| ExprNode::InverseLaplaceTransform(_, _, _)
| ExprNode::Residue(_, _, _)
| ExprNode::RootOf(_, _, _)
| ExprNode::DSolve(_, _, _)
| ExprNode::RootSum(_, _, _)
| ExprNode::ConditionSet(_, _)
| ExprNode::Subs(_, _, _) => None,
}
}
fn expr_to_rational(arena: &Arena, id: ExprId) -> Option<Ratio<BigInt>> {
match arena.node(id) {
ExprNode::Num(nid) => Some(arena.num(*nid).clone()),
_ => None,
}
}
fn contains_id(arena: &Arena, haystack: ExprId, needle: ExprId) -> bool {
if haystack == needle {
return true;
}
let mut visited: FxHashMap<ExprId, ()> = FxHashMap::default();
let mut stack: Vec<ExprId> = vec![haystack];
while let Some(id) = stack.pop() {
if id == needle {
return true;
}
if visited.contains_key(&id) {
continue;
}
visited.insert(id, ());
let children = arena.node(id).children();
stack.extend_from_slice(&children);
}
false
}
pub(crate) fn poly_to_expr(arena: &mut Arena, poly: &Poly, var: ExprId) -> ExprId {
if poly.is_zero() {
return arena.zero;
}
let coeffs = poly.coeffs();
let mut terms: SmallVec<[ExprId; 8]> = SmallVec::new();
for (i, c) in coeffs.iter().enumerate() {
if c.is_zero() {
continue;
}
let coeff_id = {
let nid = arena.intern_num(c.clone());
arena.intern(ExprNode::Num(nid))
};
if i == 0 {
terms.push(coeff_id);
} else {
let power = if i == 1 {
var
} else {
let exp = arena.int(i as i64);
arena.pow(var, exp)
};
if c.is_one() {
terms.push(power);
} else {
terms.push(arena.mul(&[coeff_id, power]));
}
}
}
match terms.len() {
0 => arena.zero,
1 => terms[0],
_ => arena.add(&terms),
}
}
pub(crate) fn ratfn_to_expr(
arena: &mut Arena,
rf: &crate::poly::ratfn::RationalFn,
var: ExprId,
) -> ExprId {
let n = poly_to_expr(arena, rf.numer(), var);
if rf.denom().is_constant() {
let d_val = rf.denom().coeff(0);
if d_val.is_one() {
return n;
}
}
let d = poly_to_expr(arena, rf.denom(), var);
arena.div(n, d)
}
pub(crate) fn expr_to_multipoly(
arena: &Arena,
expr: ExprId,
vars: &[ExprId],
) -> Option<MultiPoly<GrevLex>> {
let nv = vars.len();
let var_index: FxHashMap<ExprId, usize> =
vars.iter().enumerate().map(|(i, &v)| (v, i)).collect();
let post_order = walk::post_order_ids(arena, expr);
let mut cache: FxHashMap<ExprId, MultiPoly<GrevLex>> = FxHashMap::default();
for &id in &post_order {
if let Some(&i) = var_index.get(&id) {
cache.insert(id, MultiPoly::var(nv, i));
continue;
}
let poly = match arena.node(id) {
ExprNode::Num(nid) => MultiPoly::constant(nv, arena.num(*nid).clone()),
ExprNode::Add(children) => {
let mut acc = MultiPoly::zero(nv);
for c in children.iter() {
acc = acc.add(cache.get(c)?);
}
acc
}
ExprNode::Mul(children) => {
let mut acc = MultiPoly::from_int(nv, 1);
for c in children.iter() {
acc = acc.mul(cache.get(c)?);
}
acc
}
ExprNode::Pow(base, exp) => {
let base_poly = cache.get(base)?;
let exp_val = expr_to_rational(arena, *exp)?;
if !exp_val.is_integer() {
return None;
}
let n: u32 = exp_val.to_integer().try_into().ok()?;
let mut acc = MultiPoly::from_int(nv, 1);
for _ in 0..n {
acc = acc.mul(base_poly);
}
acc
}
ExprNode::Neg(inner) => cache.get(inner)?.neg(),
_ => return None,
};
cache.insert(id, poly);
}
cache.remove(&expr)
}
pub(crate) fn multipoly_to_expr(
arena: &mut Arena,
poly: &MultiPoly<GrevLex>,
vars: &[ExprId],
) -> ExprId {
let mut terms: Vec<ExprId> = Vec::with_capacity(poly.num_terms());
for (exp, c) in poly.terms() {
let mut factors: SmallVec<[ExprId; 6]> = SmallVec::new();
if !c.is_one() {
let nid = arena.intern_num(c.clone());
factors.push(arena.intern(ExprNode::Num(nid)));
}
for (i, &e) in exp.iter().enumerate() {
if e == 0 {
continue;
}
if e == 1 {
factors.push(vars[i]);
} else {
let e_id = arena.int(e as i64);
factors.push(arena.pow(vars[i], e_id));
}
}
let term = match factors.len() {
0 => arena.one,
1 => factors[0],
_ => arena.mul(&factors),
};
terms.push(term);
}
match terms.len() {
0 => arena.zero,
1 => terms[0],
_ => arena.add(&terms),
}
}
pub(crate) fn symbolic_multipoly_terms(
arena: &mut Arena,
expr: ExprId,
gens: &[ExprId],
) -> Option<Vec<(Vec<u32>, ExprId)>> {
let expanded = crate::transforms::expand::expand(arena, expr);
let terms: Vec<ExprId> = match arena.node(expanded) {
ExprNode::Add(children) => children.to_vec(),
_ => vec![expanded],
};
let mut buckets: std::collections::BTreeMap<Vec<u32>, Vec<ExprId>> =
std::collections::BTreeMap::new();
for term in terms {
let (exps, coeff) = term_exponents_coeff(arena, term, gens)?;
buckets.entry(exps).or_default().push(coeff);
}
let mut out = Vec::with_capacity(buckets.len());
for (exps, bucket) in buckets {
let c = if bucket.len() == 1 {
bucket[0]
} else {
arena.add(&bucket)
};
let c = crate::transforms::eval::eval(arena, c);
if !arena.is_zero_structural(c) {
out.push((exps, c));
}
}
Some(out)
}
pub(crate) fn non_polynomial_reason(
arena: &Arena,
expr: ExprId,
gens: &[ExprId],
) -> Option<String> {
let mut visited: rustc_hash::FxHashSet<ExprId> = rustc_hash::FxHashSet::default();
let mut stack: Vec<ExprId> = vec![expr];
while let Some(id) = stack.pop() {
if !visited.insert(id) {
continue;
}
let node = arena.node(id).clone();
match node {
ExprNode::Add(_)
| ExprNode::Mul(_)
| ExprNode::Neg(_)
| ExprNode::Symbol(_)
| ExprNode::Num(_) => {
stack.extend(node.children());
}
ExprNode::Pow(base, exp) => {
let shown = |g: ExprId| arena.display(g).to_string();
if let Some(&g) = gens.iter().find(|&&g| contains_any(arena, exp, &[g])) {
return Some(format!(
"generator `{}` occurs in the exponent of `{}`",
shown(g),
arena.display(id)
));
}
if let Some(&g) = gens.iter().find(|&&g| contains_any(arena, base, &[g])) {
let ok = arena
.as_num(exp)
.is_some_and(|n| n.is_integer() && !n.is_negative());
if !ok {
let kind = match arena.as_num(exp) {
Some(n) if n.is_integer() => "a negative power (a rational function)",
Some(_) => "a fractional power (an algebraic function)",
None => "a symbolic power",
};
return Some(format!(
"generator `{}` occurs under {kind} in `{}`",
shown(g),
arena.display(id)
));
}
}
stack.push(base);
}
other => {
if let Some(&g) = gens.iter().find(|&&g| contains_any(arena, id, &[g])) {
return Some(format!(
"generator `{}` occurs inside the function `{}`",
arena.display(g),
arena.display(id)
));
}
stack.extend(other.children());
}
}
}
None
}
fn contains_any(arena: &Arena, expr: ExprId, gens: &[ExprId]) -> bool {
if gens.contains(&expr) {
return true;
}
let mut visited: rustc_hash::FxHashSet<ExprId> = rustc_hash::FxHashSet::default();
let mut stack: Vec<ExprId> = vec![expr];
while let Some(id) = stack.pop() {
if !visited.insert(id) {
continue;
}
if gens.contains(&id) {
return true;
}
stack.extend(arena.node(id).children());
}
false
}
fn term_exponents_coeff(
arena: &mut Arena,
term: ExprId,
gens: &[ExprId],
) -> Option<(Vec<u32>, ExprId)> {
let mut exps = vec![0u32; gens.len()];
if !contains_any(arena, term, gens) {
return Some((exps, term));
}
let mut consts: SmallVec<[ExprId; 4]> = SmallVec::new();
match arena.node(term).clone() {
ExprNode::Mul(children) => {
for &child in &children {
accumulate_factor(arena, child, gens, &mut exps, &mut consts)?;
}
}
_ => accumulate_factor(arena, term, gens, &mut exps, &mut consts)?,
}
let c = match consts.len() {
0 => arena.one,
1 => consts[0],
_ => arena.mul(&consts),
};
Some((exps, c))
}
fn accumulate_factor(
arena: &mut Arena,
factor: ExprId,
gens: &[ExprId],
exps: &mut [u32],
consts: &mut SmallVec<[ExprId; 4]>,
) -> Option<()> {
if let Some(i) = gens.iter().position(|&g| g == factor) {
exps[i] = exps[i].checked_add(1)?;
return Some(());
}
if !contains_any(arena, factor, gens) {
consts.push(factor);
return Some(());
}
match arena.node(factor).clone() {
ExprNode::Pow(base, exp) => {
let n = arena.as_num(exp)?.clone();
if !n.is_integer() || n.is_negative() {
return None;
}
let d: u32 = n.to_integer().try_into().ok()?;
if let Some(i) = gens.iter().position(|&g| g == base) {
exps[i] = exps[i].checked_add(d)?;
return Some(());
}
if let ExprNode::Mul(children) = arena.node(base).clone() {
let mut inner_exps = vec![0u32; gens.len()];
let mut inner_consts: SmallVec<[ExprId; 4]> = SmallVec::new();
for &c in &children {
accumulate_factor(arena, c, gens, &mut inner_exps, &mut inner_consts)?;
}
for (e, ie) in exps.iter_mut().zip(&inner_exps) {
*e = e.checked_add(ie.checked_mul(d)?)?;
}
if !inner_consts.is_empty() {
let c = if inner_consts.len() == 1 {
inner_consts[0]
} else {
arena.mul(&inner_consts)
};
consts.push(arena.pow(c, exp));
}
return Some(());
}
None
}
ExprNode::Neg(inner) => {
consts.push(arena.neg_one);
accumulate_factor(arena, inner, gens, exps, consts)
}
_ => None,
}
}
pub(crate) fn as_numer_denom(arena: &mut Arena, expr: ExprId) -> (ExprId, ExprId) {
let node = arena.node(expr).clone();
match node {
ExprNode::Pow(base, exp) => {
if let Some(r) = arena.as_num(exp) {
let r = r.clone();
if r.is_negative() {
let pos_exp = {
let neg_r = -r;
let nid = arena.intern_num(neg_r);
arena.intern(ExprNode::Num(nid))
};
let denom = arena.pow(base, pos_exp);
return (arena.one, denom);
}
}
(expr, arena.one)
}
ExprNode::Mul(ref children) => {
let children = children.clone();
let mut numer_factors: SmallVec<[ExprId; 6]> = SmallVec::new();
let mut denom_factors: SmallVec<[ExprId; 6]> = SmallVec::new();
for &child in &children {
let (n, d) = as_numer_denom(arena, child);
if n != arena.one {
numer_factors.push(n);
}
if d != arena.one {
denom_factors.push(d);
}
}
let numer = if numer_factors.is_empty() {
arena.one
} else if numer_factors.len() == 1 {
numer_factors[0]
} else {
arena.mul(&numer_factors)
};
let denom = if denom_factors.is_empty() {
arena.one
} else if denom_factors.len() == 1 {
denom_factors[0]
} else {
arena.mul(&denom_factors)
};
(numer, denom)
}
_ => (expr, arena.one),
}
}
pub(crate) fn cancel(arena: &mut Arena, expr: ExprId, var: ExprId) -> ExprId {
let (numer, denom) = as_numer_denom(arena, expr);
if denom == arena.one {
return expr;
}
let numer_poly = match expr_to_poly(arena, numer, var) {
Some(p) => p,
None => return expr,
};
let denom_poly = match expr_to_poly(arena, denom, var) {
Some(p) if !p.is_zero() => p,
_ => return expr,
};
let gcd = Poly::gcd(&numer_poly, &denom_poly);
let (mut new_numer, mut new_denom, mut changed) = if gcd.is_constant() && gcd.coeff(0).is_one()
{
(numer_poly, denom_poly, false)
} else {
(numer_poly.div(&gcd), denom_poly.div(&gcd), true)
};
let n_content = new_numer.content();
let d_content = new_denom.content();
if !n_content.is_zero() && !d_content.is_zero() {
let numer_gcd = n_content.numer().gcd(d_content.numer());
let denom_lcm = n_content.denom().lcm(d_content.denom());
let content_gcd = Ratio::new(numer_gcd, denom_lcm);
if !content_gcd.is_one() {
let inv = Ratio::one() / content_gcd;
new_numer = new_numer.scale(&inv);
new_denom = new_denom.scale(&inv);
changed = true;
}
}
if !changed {
return expr;
}
let new_numer_expr = poly_to_expr(arena, &new_numer, var);
if new_denom.is_zero() {
return expr;
}
if new_denom.degree() == Some(0) && new_denom.coeff(0).is_one() {
return new_numer_expr;
}
let new_denom_expr = poly_to_expr(arena, &new_denom, var);
let neg_one = arena.neg_one;
let denom_inv = arena.pow(new_denom_expr, neg_one);
arena.mul(&[new_numer_expr, denom_inv])
}
pub(crate) fn collect(arena: &mut Arena, expr: ExprId, var: ExprId) -> ExprId {
let poly = match expr_to_poly(arena, expr, var) {
Some(p) => p,
None => return expr,
};
poly_to_expr(arena, &poly, var)
}
pub(crate) fn poly_degree(arena: &Arena, expr: ExprId, var: ExprId) -> Option<usize> {
let poly = expr_to_poly(arena, expr, var)?;
poly.degree()
}
pub(crate) fn poly_coefficients(
arena: &mut Arena,
expr: ExprId,
var: ExprId,
) -> Option<Vec<ExprId>> {
let poly = expr_to_poly(arena, expr, var)?;
let rational_coeffs = poly.coeffs();
let mut result = Vec::with_capacity(rational_coeffs.len());
for c in rational_coeffs {
let nid = arena.intern_num(c.clone());
result.push(arena.intern(crate::base::node::ExprNode::Num(nid)));
}
Some(result)
}
pub(crate) fn together(arena: &mut Arena, expr: ExprId) -> ExprId {
let node = arena.node(expr).clone();
let children = match node {
ExprNode::Add(ref ch) => ch.clone(),
_ => return expr,
};
let mut parts: Vec<(ExprId, ExprId)> = Vec::with_capacity(children.len());
let mut all_denom_one = true;
for &child in &children {
let (n, d) = as_numer_denom(arena, child);
if d != arena.one {
all_denom_one = false;
}
parts.push((n, d));
}
if all_denom_one {
return expr;
}
let mut unique_denoms: Vec<ExprId> = Vec::new();
for &(_, d) in &parts {
if d != arena.one && !unique_denoms.contains(&d) {
unique_denoms.push(d);
}
}
let (n, d) = match try_together_poly_lcm(arena, &parts, &unique_denoms) {
Some(nd) => nd,
None => together_product_fallback(arena, &parts, &unique_denoms),
};
arena.div(n, d)
}
pub(crate) fn fraction_parts(arena: &mut Arena, expr: ExprId) -> (ExprId, ExprId) {
let one = arena.one;
let order = walk::post_order_ids(arena, expr);
let mut cache: FxHashMap<ExprId, (ExprId, ExprId)> = FxHashMap::default();
let lookup = |cache: &FxHashMap<ExprId, (ExprId, ExprId)>, id: ExprId| {
cache.get(&id).copied().unwrap_or((id, one))
};
for &id in &order {
let node = arena.node(id).clone();
let parts = match node {
ExprNode::Num(nid) => {
let r = arena.num(nid).clone();
if r.is_integer() {
(id, one)
} else {
let n = arena.big_int(r.numer().clone());
let d = arena.big_int(r.denom().clone());
(n, d)
}
}
ExprNode::Add(children) => {
let parts: Vec<(ExprId, ExprId)> =
children.iter().map(|&c| lookup(&cache, c)).collect();
combine_fraction_sum(arena, &parts)
}
ExprNode::Mul(children) => {
let mut numers: SmallVec<[ExprId; 6]> = SmallVec::new();
let mut denoms: SmallVec<[ExprId; 6]> = SmallVec::new();
for &c in children.iter() {
let (n, d) = lookup(&cache, c);
if n != one {
numers.push(n);
}
if d != one {
denoms.push(d);
}
}
(
product_or_one(arena, &numers),
product_or_one(arena, &denoms),
)
}
ExprNode::Pow(base, exp) => match arena.as_num(exp).cloned() {
Some(k) if k.is_integer() && !k.is_zero() => {
let (bn, bd) = lookup(&cache, base);
if bn == base && bd == one {
if k.is_negative() {
let m = arena.big_int(-k.to_integer());
(one, arena.pow(base, m))
} else {
(id, one)
}
} else {
let m = arena.big_int(k.to_integer().abs());
let bn_m = arena.pow(bn, m);
let bd_m = arena.pow(bd, m);
if k.is_negative() {
(bd_m, bn_m)
} else {
(bn_m, bd_m)
}
}
}
_ => (id, one),
},
ExprNode::Neg(inner) => {
let (n, d) = lookup(&cache, inner);
(arena.neg(n), d)
}
_ => (id, one),
};
cache.insert(id, parts);
}
lookup(&cache, expr)
}
pub(crate) fn together_deep(arena: &mut Arena, expr: ExprId) -> ExprId {
let (n, d) = fraction_parts(arena, expr);
if d == arena.one {
return n;
}
if arena.is_zero_structural(d) {
if !arena.is_zero_structural(n) && expands_to_zero(arena, n) {
return arena.nan;
}
} else if vanishes_identically(arena, d) {
if expands_to_zero(arena, n) {
return arena.nan;
}
let zero = arena.zero;
return arena.div(n, zero);
}
arena.div(n, d)
}
fn expands_to_zero(arena: &mut Arena, expr: ExprId) -> bool {
let expanded = crate::transforms::expand::expand(arena, expr);
let expanded = crate::transforms::eval::eval(arena, expanded);
arena.is_zero_structural(expanded)
}
pub(crate) fn vanishes_identically(arena: &mut Arena, expr: ExprId) -> bool {
if arena.is_zero_structural(expr) {
return true;
}
may_vanish_identically(arena, expr) && expands_to_zero(arena, expr)
}
const RESIDUE_PRIME: u64 = (1 << 61) - 1;
const RESIDUE_ROOT_SCALE: u64 = 720_720;
const RESIDUE_GENERATOR_SALT: u64 = 0x5851_F42D_4C95_7F2D;
const RESIDUE_EXPONENT_SALT: u64 = 0x1405_7B7E_F767_814F;
const RESIDUE_ANGLE_SALT: u64 = 0x2F3B_9D07_A6C1_E485;
fn residue_mul(a: u64, b: u64) -> u64 {
let p = RESIDUE_PRIME;
let x = u128::from(a) * u128::from(b);
let folded = (x & u128::from(p)) + (x >> 61);
let mut r = ((folded & u128::from(p)) + (folded >> 61)) as u64;
while r >= p {
r -= p;
}
r
}
fn residue_pow(mut b: u64, mut e: u64) -> u64 {
let mut r = 1u64;
while e > 0 {
if e & 1 == 1 {
r = residue_mul(r, b);
}
b = residue_mul(b, b);
e >>= 1;
}
r
}
fn residue_hash(mut z: u64) -> u64 {
z = z.wrapping_add(0x9E37_79B9_7F4A_7C15);
z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
z ^ (z >> 31)
}
fn residue_unit(key: u64) -> u64 {
residue_hash(key) % (RESIDUE_PRIME - 1) + 1
}
fn residue_exponent(q: &crate::base::numeric::Q) -> Option<u64> {
use num_traits::ToPrimitive;
let order = RESIDUE_PRIME - 1;
if q.is_integer()
&& let Some(n) = q.numer().to_i128()
{
let n = n.rem_euclid(i128::from(order)) as u128;
return Some(((n * u128::from(RESIDUE_ROOT_SCALE)) % u128::from(order)) as u64);
}
let order_big = BigInt::from(order);
let scale = BigInt::from(RESIDUE_ROOT_SCALE);
let g = q.denom().gcd(&scale);
let rest = (q.denom() / &g).mod_floor(&order_big).to_u64()?;
let inv = inverse_modulo(rest, order)?;
let k = (q.numer() * (scale / g)).mod_floor(&order_big).to_u64()?;
Some(((u128::from(k) * u128::from(inv)) % u128::from(order)) as u64)
}
fn inverse_modulo(a: u64, m: u64) -> Option<u64> {
let (mut r0, mut r1) = (i128::from(m), i128::from(a));
let (mut t0, mut t1) = (0i128, 1i128);
while r1 != 0 {
let q = r0 / r1;
(r0, r1) = (r1, r0 - q * r1);
(t0, t1) = (t1, t0 - q * t1);
}
(r0 == 1).then(|| t0.rem_euclid(i128::from(m)) as u64)
}
fn residue_scaled_power(u: u64, q: &crate::base::numeric::Q) -> Option<u64> {
residue_exponent(q).map(|k| residue_pow(u, k))
}
type Residue = (u64, u64);
fn residue2_add(x: Residue, y: Residue) -> Residue {
((x.0 + y.0) % RESIDUE_PRIME, (x.1 + y.1) % RESIDUE_PRIME)
}
fn residue2_neg(x: Residue) -> Residue {
(
(RESIDUE_PRIME - x.0) % RESIDUE_PRIME,
(RESIDUE_PRIME - x.1) % RESIDUE_PRIME,
)
}
fn residue2_mul(x: Residue, y: Residue) -> Residue {
let p = RESIDUE_PRIME;
let re = (residue_mul(x.0, y.0) + p - residue_mul(x.1, y.1)) % p;
let im = (residue_mul(x.0, y.1) + residue_mul(x.1, y.0)) % p;
(re, im)
}
fn residue2_inv(x: Residue) -> Option<Residue> {
let p = RESIDUE_PRIME;
let norm = (residue_mul(x.0, x.0) + residue_mul(x.1, x.1)) % p;
if norm == 0 {
return None;
}
let n = residue_pow(norm, p - 2);
Some((residue_mul(x.0, n), residue_mul((p - x.1) % p, n)))
}
fn residue2_pow(mut b: Residue, mut e: u128) -> Residue {
let mut r: Residue = (1, 0);
while e > 0 {
if e & 1 == 1 {
r = residue2_mul(r, b);
}
b = residue2_mul(b, b);
e >>= 1;
}
r
}
fn residue_sqrt(s: u64) -> Residue {
let p = RESIDUE_PRIME;
let t = residue_pow(s, (p + 1) / 4);
if residue_mul(t, t) == s {
(t, 0)
} else {
(0, residue_pow((p - s) % p, (p + 1) / 4))
}
}
fn residue_integer_sqrt(n: &BigInt) -> Residue {
use num_traits::ToPrimitive;
let p = RESIDUE_PRIME;
let Some(mut rest) = n.to_u64() else {
return residue_sqrt(n.mod_floor(&BigInt::from(p)).to_u64().unwrap_or(0));
};
let mut value: Residue = (1, 0);
let mut q: u64 = 2;
while q < 1000 && q.saturating_mul(q) <= rest {
let mut e = 0u32;
while rest.is_multiple_of(q) {
rest /= q;
e += 1;
}
if e > 0 {
value = residue2_mul(value, (residue_pow(q, u64::from(e / 2)), 0));
if e % 2 == 1 {
value = residue2_mul(value, residue_sqrt(q));
}
}
q += if q == 2 { 1 } else { 2 };
}
if rest > 1 {
value = residue2_mul(value, residue_sqrt(rest % p));
}
value
}
fn is_plain_generator(arena: &Arena, g: ExprId) -> bool {
!matches!(
arena.node(g),
ExprNode::Num(_)
| ExprNode::Add(_)
| ExprNode::Mul(_)
| ExprNode::Neg(_)
| ExprNode::Pow(_, _)
| ExprNode::Exp(_)
| ExprNode::E
| ExprNode::Sin(_)
| ExprNode::Cos(_)
| ExprNode::Tan(_)
| ExprNode::Sinh(_)
| ExprNode::Cosh(_)
| ExprNode::Tanh(_)
)
}
fn generator_residue(arena: &Arena, id: ExprId) -> Residue {
let plain = |g: ExprId| {
residue_pow(
residue_unit(u64::from(g.0) ^ RESIDUE_GENERATOR_SALT),
RESIDUE_ROOT_SCALE,
)
};
let derived = match arena.node(id) {
ExprNode::ImaginaryUnit => return (0, 1),
ExprNode::E => Some(residue_pow(
residue_unit(RESIDUE_EXPONENT_SALT),
RESIDUE_ROOT_SCALE,
)),
ExprNode::Exp(u) => exp_residue(arena, *u),
ExprNode::Sin(u) | ExprNode::Cos(u) | ExprNode::Tan(u) => {
if let Some(t) = exponent_unit(arena, *u, RESIDUE_ANGLE_SALT) {
return circular_residue(arena.node(id), t);
}
None
}
ExprNode::Sinh(u) | ExprNode::Cosh(u) | ExprNode::Tanh(u) => {
if let Some(h) = exp_residue(arena, *u) {
return circular_residue(arena.node(id), h);
}
None
}
ExprNode::Pow(g, q) if is_plain_generator(arena, *g) => arena.as_num(*q).and_then(|q| {
residue_scaled_power(residue_unit(u64::from(g.0) ^ RESIDUE_GENERATOR_SALT), q)
}),
ExprNode::Pow(n, q) => {
if let (Some(n), Some(q)) = (arena.as_num(*n), arena.as_num(*q))
&& let Some(value) = numeric_radical_residue(n, q)
{
return value;
}
None
}
_ => None,
};
(derived.unwrap_or_else(|| plain(id)), 0)
}
fn numeric_radical_residue(
n: &crate::base::numeric::Q,
q: &crate::base::numeric::Q,
) -> Option<Residue> {
use num_traits::ToPrimitive;
if !n.is_integer() || !n.is_positive() || *q.denom() != BigInt::from(2) {
return None;
}
let root = residue_integer_sqrt(n.numer());
let m = q.numer().to_i64()?;
let base = if m < 0 { residue2_inv(root)? } else { root };
Some(residue2_pow(base, u128::from(m.unsigned_abs())))
}
fn circular_residue(node: &ExprNode, t: u64) -> Residue {
let p = RESIDUE_PRIME;
let half = p.div_ceil(2);
let inv = residue_pow(t, p - 2);
let difference = (t + p - inv) % p;
let sum = (t + inv) % p;
let quotient = || residue_mul(difference, residue_pow(sum, p - 2));
match node {
ExprNode::Sin(_) => (0, residue_mul((p - difference) % p, half)),
ExprNode::Cos(_) | ExprNode::Cosh(_) => (residue_mul(sum, half), 0),
ExprNode::Tan(_) => (0, (p - quotient()) % p),
ExprNode::Sinh(_) => (residue_mul(difference, half), 0),
_ => (quotient(), 0),
}
}
fn exp_residue(arena: &Arena, u: ExprId) -> Option<u64> {
exponent_unit(arena, u, RESIDUE_EXPONENT_SALT)
}
fn exponent_unit(arena: &Arena, u: ExprId, salt: u64) -> Option<u64> {
let terms: &[ExprId] = match arena.node(u) {
ExprNode::Add(children) => children,
_ => std::slice::from_ref(&u),
};
let mut value = 1u64;
for &t in terms {
let (mut t, mut sign) = (t, crate::base::numeric::Q::one());
if let ExprNode::Neg(inner) = arena.node(t) {
t = *inner;
sign = -sign;
}
let (coeff, key) = match arena.node(t) {
ExprNode::Num(nid) => (arena.num(*nid).clone(), salt),
ExprNode::Mul(children) => {
let (coeff, rest) = match children.first().and_then(|&c| arena.as_num(c)) {
Some(q) => (q.clone(), &children[1..]),
None => (crate::base::numeric::Q::one(), &children[..]),
};
let key = rest
.iter()
.fold(salt, |h, c| residue_hash(h ^ u64::from(c.0)));
(coeff, key)
}
_ => (
crate::base::numeric::Q::one(),
residue_hash(salt ^ u64::from(t.0)),
),
};
value = residue_mul(
value,
residue_scaled_power(residue_unit(key), &(coeff * sign))?,
);
}
Some(value)
}
pub(crate) fn may_vanish_identically(arena: &Arena, expr: ExprId) -> bool {
let values = residues_at_random_point(arena, expr);
!values
.get(&expr)
.copied()
.flatten()
.is_some_and(|r| r != (0, 0))
}
pub(crate) fn may_have_vanishing_denominator(arena: &Arena, expr: ExprId) -> bool {
let values = residues_at_random_point(arena, expr);
values.keys().any(|&id| match arena.node(id) {
ExprNode::Pow(base, e) => {
arena
.as_num(*e)
.is_some_and(|q| q.is_integer() && q.is_negative())
&& !values
.get(base)
.copied()
.flatten()
.is_some_and(|r| r != (0, 0))
}
_ => false,
})
}
fn residues_at_random_point(arena: &Arena, expr: ExprId) -> FxHashMap<ExprId, Option<Residue>> {
let p = RESIDUE_PRIME;
let inv = |a: u64| (a != 0).then(|| residue_pow(a, p - 2));
let big_mod = |n: &BigInt, m: u64| -> u64 {
use num_traits::ToPrimitive;
n.mod_floor(&BigInt::from(m)).to_u64().unwrap_or(0)
};
let integer_exponent = |e: ExprId| arena.as_num(e).filter(|q| q.is_integer());
let structural = |id: ExprId| match arena.node(id) {
ExprNode::Num(_) | ExprNode::Add(_) | ExprNode::Mul(_) | ExprNode::Neg(_) => true,
ExprNode::Pow(_, e) => integer_exponent(*e).is_some(),
_ => false,
};
let mut values: FxHashMap<ExprId, Option<Residue>> = FxHashMap::default();
let mut stack: Vec<(ExprId, bool)> = vec![(expr, false)];
while let Some((id, children_done)) = stack.pop() {
if values.contains_key(&id) {
continue;
}
if !structural(id) {
values.insert(id, Some(generator_residue(arena, id)));
continue;
}
let node = arena.node(id);
if !children_done {
stack.push((id, true));
match node {
ExprNode::Pow(base, _) => stack.push((*base, false)),
n => stack.extend(n.children().into_iter().map(|c| (c, false))),
}
continue;
}
let get = |c: &ExprId| values.get(c).copied().flatten();
let value = match node {
ExprNode::Num(nid) => {
let q = arena.num(*nid);
inv(big_mod(q.denom(), p)).map(|d| (residue_mul(big_mod(q.numer(), p), d), 0))
}
ExprNode::Add(children) => children
.iter()
.try_fold((0u64, 0u64), |acc, c| get(c).map(|v| residue2_add(acc, v))),
ExprNode::Mul(children) => children
.iter()
.try_fold((1u64, 0u64), |acc, c| get(c).map(|v| residue2_mul(acc, v))),
ExprNode::Neg(c) => get(c).map(residue2_neg),
ExprNode::Pow(base, e) => match (get(base), integer_exponent(*e)) {
(Some(b), Some(k)) => {
let n = k.to_integer();
let b = if n.is_negative() {
residue2_inv(b)
} else {
Some(b)
};
b.map(|b| match (b, n.is_zero()) {
(_, true) => (1, 0),
((0, 0), false) => (0, 0),
_ => {
let e =
num_traits::ToPrimitive::to_u128(&n.abs()).unwrap_or_else(|| {
let order = BigInt::from(p) * BigInt::from(p) - BigInt::one();
num_traits::ToPrimitive::to_u128(&n.abs().mod_floor(&order))
.unwrap_or(0)
});
residue2_pow(b, e)
}
})
}
_ => None,
},
_ => None,
};
values.insert(id, value);
}
values
}
fn product_or_one(arena: &mut Arena, factors: &[ExprId]) -> ExprId {
match factors.len() {
0 => arena.one,
1 => factors[0],
_ => arena.mul(factors),
}
}
fn combine_fraction_sum(arena: &mut Arena, parts: &[(ExprId, ExprId)]) -> (ExprId, ExprId) {
let one = arena.one;
if parts.iter().all(|&(_, d)| d == one) {
let numers: SmallVec<[ExprId; 6]> = parts.iter().map(|&(n, _)| n).collect();
return (arena.add(&numers), one);
}
let mut unique_denoms: Vec<ExprId> = Vec::new();
for &(_, d) in parts {
if d != one && !unique_denoms.contains(&d) {
unique_denoms.push(d);
}
}
match try_together_poly_lcm(arena, parts, &unique_denoms) {
Some(nd) => nd,
None => together_product_fallback(arena, parts, &unique_denoms),
}
}
fn try_together_poly_lcm(
arena: &mut Arena,
parts: &[(ExprId, ExprId)],
unique_denoms: &[ExprId],
) -> Option<(ExprId, ExprId)> {
if unique_denoms.is_empty() {
return None;
}
let mut all_syms: Vec<ExprId> = Vec::new();
for &d in unique_denoms {
all_syms.extend(walk::free_symbols(arena, d));
}
all_syms.sort_by_key(|id| id.0);
all_syms.dedup();
if all_syms.len() != 1 {
return None;
}
let var = all_syms[0];
let mut denom_poly_map: Vec<(ExprId, Poly)> = Vec::new();
for &d in unique_denoms {
let p = expr_to_poly(arena, d, var)?;
if p.is_zero() {
return None;
}
denom_poly_map.push((d, p));
}
let mut lcm = denom_poly_map[0].1.clone();
for (_, p) in &denom_poly_map[1..] {
let g = Poly::gcd(&lcm, p);
if g.is_zero() {
return None;
}
let a_over_g = lcm.div(&g);
lcm = &a_over_g * p;
}
let common_denom_expr = poly_to_expr(arena, &lcm, var);
let one_poly = Poly::constant(Ratio::one());
let mut scaled_numers: SmallVec<[ExprId; 6]> = SmallVec::new();
for &(n, d) in parts {
let d_poly = if d == arena.one {
&one_poly
} else {
match denom_poly_map.iter().find(|(id, _)| *id == d) {
Some((_, p)) => p,
None => return None,
}
};
let scale_poly = lcm.div(d_poly);
if scale_poly.degree() == Some(0) && scale_poly.coeff(0).is_one() {
scaled_numers.push(n);
} else {
let scale_expr = poly_to_expr(arena, &scale_poly, var);
let scaled = arena.mul(&[n, scale_expr]);
scaled_numers.push(scaled);
}
}
let numer_sum = arena.add(&scaled_numers);
Some((numer_sum, common_denom_expr))
}
fn together_product_fallback(
arena: &mut Arena,
parts: &[(ExprId, ExprId)],
unique_denoms: &[ExprId],
) -> (ExprId, ExprId) {
let common_denom = if unique_denoms.len() == 1 {
unique_denoms[0]
} else {
arena.mul(unique_denoms)
};
let mut scaled_numers: SmallVec<[ExprId; 6]> = SmallVec::new();
for &(n, d) in parts {
if d == arena.one {
let scaled = arena.mul(&[n, common_denom]);
scaled_numers.push(scaled);
} else {
let mut other_denoms: Vec<ExprId> = Vec::new();
for &ud in unique_denoms {
if ud != d {
other_denoms.push(ud);
}
}
if other_denoms.is_empty() {
scaled_numers.push(n);
} else {
let scale = if other_denoms.len() == 1 {
other_denoms[0]
} else {
arena.mul(&other_denoms)
};
let scaled = arena.mul(&[n, scale]);
scaled_numers.push(scaled);
}
}
}
let numer_sum = arena.add(&scaled_numers);
(numer_sum, common_denom)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::base::arena::Arena;
fn sym(a: &mut Arena, name: &str) -> ExprId {
a.symbol(name)
}
fn display(a: &Arena, id: ExprId) -> String {
a.display(id).to_string()
}
#[test]
fn expr_to_poly_constant() {
let mut a = Arena::new();
let x = sym(&mut a, "x");
let five = a.int(5);
let p = expr_to_poly(&a, five, x).unwrap();
assert_eq!(format!("{p}"), "5");
}
#[test]
fn expr_to_poly_variable() {
let mut a = Arena::new();
let x = sym(&mut a, "x");
let p = expr_to_poly(&a, x, x).unwrap();
assert_eq!(format!("{p}"), "θ");
}
#[test]
fn expr_to_poly_linear() {
let mut a = Arena::new();
let x = sym(&mut a, "x");
let two = a.int(2);
let three = a.int(3);
let two_x = a.mul(&[two, x]);
let expr = a.add(&[two_x, three]);
let p = expr_to_poly(&a, expr, x).unwrap();
assert_eq!(p.degree(), Some(1));
assert_eq!(format!("{p}"), "2*θ + 3");
}
#[test]
fn expr_to_poly_quadratic() {
let mut a = Arena::new();
let x = sym(&mut a, "x");
let two = a.int(2);
let x_sq = a.pow(x, two);
let two_x = a.mul(&[two, x]);
let one = a.one;
let expr = a.add(&[x_sq, two_x, one]);
let p = expr_to_poly(&a, expr, x).unwrap();
assert_eq!(p.degree(), Some(2));
let val = p.eval(&Ratio::from_integer(BigInt::from(3)));
assert_eq!(val, Ratio::from_integer(BigInt::from(16)));
}
#[test]
fn expr_to_poly_fails_for_sin() {
let mut a = Arena::new();
let x = sym(&mut a, "x");
let expr = a.sin(x);
assert!(expr_to_poly(&a, expr, x).is_none());
}
#[test]
fn expr_to_poly_fails_for_fractional_power() {
let mut a = Arena::new();
let x = sym(&mut a, "x");
let half = a.rational(1, 2);
let expr = a.pow(x, half);
assert!(expr_to_poly(&a, expr, x).is_none());
}
#[test]
fn expr_to_poly_fails_for_negative_power() {
let mut a = Arena::new();
let x = sym(&mut a, "x");
let neg_one = a.int(-1);
let expr = a.pow(x, neg_one);
assert!(expr_to_poly(&a, expr, x).is_none());
}
#[test]
fn expr_to_poly_product() {
let mut a = Arena::new();
let x = sym(&mut a, "x");
let one = a.one;
let two = a.int(2);
let x_sq = a.pow(x, two);
let expr = a.sub(x_sq, one);
let p = expr_to_poly(&a, expr, x).unwrap();
assert_eq!(p.degree(), Some(2));
let val_1 = p.eval(&Ratio::from_integer(BigInt::from(1)));
let val_neg1 = p.eval(&Ratio::from_integer(BigInt::from(-1)));
assert!(val_1.is_zero(), "x²-1 at x=1 should be 0");
assert!(val_neg1.is_zero(), "x²-1 at x=-1 should be 0");
}
#[test]
fn poly_to_expr_constant() {
let mut a = Arena::new();
let x = sym(&mut a, "x");
let p = Poly::from_int(7);
let expr = poly_to_expr(&mut a, &p, x);
assert_eq!(display(&a, expr), "7");
}
#[test]
fn poly_to_expr_linear() {
let mut a = Arena::new();
let x = sym(&mut a, "x");
let p = Poly::from_coeffs(vec![
Ratio::from_integer(BigInt::from(3)),
Ratio::from_integer(BigInt::from(2)),
]);
let expr = poly_to_expr(&mut a, &p, x);
assert_eq!(display(&a, expr), "2*x + 3");
}
#[test]
fn poly_to_expr_zero() {
let mut a = Arena::new();
let x = sym(&mut a, "x");
let p = Poly::zero();
let expr = poly_to_expr(&mut a, &p, x);
assert_eq!(expr, a.zero);
}
#[test]
fn poly_to_expr_quadratic() {
let mut a = Arena::new();
let x = sym(&mut a, "x");
let p = Poly::from_coeffs(vec![
Ratio::from_integer(BigInt::from(1)),
Ratio::from_integer(BigInt::from(2)),
Ratio::from_integer(BigInt::from(1)),
]);
let expr = poly_to_expr(&mut a, &p, x);
assert_eq!(display(&a, expr), "x^2 + 2*x + 1");
}
#[test]
fn roundtrip_expr_poly_expr() {
let mut a = Arena::new();
let x = sym(&mut a, "x");
let two = a.int(2);
let three = a.int(3);
let x_sq = a.pow(x, two);
let three_x = a.mul(&[three, x]);
let orig = a.add(&[x_sq, three_x, two]);
let poly = expr_to_poly(&a, orig, x).unwrap();
let rebuilt = poly_to_expr(&mut a, &poly, x);
assert_eq!(orig, rebuilt, "roundtrip should preserve expression");
}
#[test]
fn numer_denom_plain_symbol() {
let mut a = Arena::new();
let x = sym(&mut a, "x");
let (n, d) = as_numer_denom(&mut a, x);
assert_eq!(n, x);
assert_eq!(d, a.one);
}
#[test]
fn numer_denom_inverse() {
let mut a = Arena::new();
let x = sym(&mut a, "x");
let neg_one = a.int(-1);
let expr = a.pow(x, neg_one); let (n, d) = as_numer_denom(&mut a, expr);
assert_eq!(n, a.one, "numerator of 1/x should be 1");
assert_eq!(d, x, "denominator of 1/x should be x");
}
#[test]
fn numer_denom_fraction() {
let mut a = Arena::new();
let x = sym(&mut a, "x");
let y = sym(&mut a, "y");
let expr = a.div(x, y);
let (n, d) = as_numer_denom(&mut a, expr);
assert_eq!(display(&a, n), "x");
assert_eq!(display(&a, d), "y");
}
#[test]
fn cancel_x_squared_minus_1_over_x_minus_1() {
let mut a = Arena::new();
let x = sym(&mut a, "x");
let two = a.int(2);
let one = a.one;
let x_sq = a.pow(x, two);
let numer = a.sub(x_sq, one);
let denom = a.sub(x, one);
let expr = a.div(numer, denom);
let result = cancel(&mut a, expr, x);
let s = display(&a, result);
assert_eq!(s, "x + 1", "cancel should give x + 1, got: {s}");
}
#[test]
fn cancel_no_common_factor() {
let mut a = Arena::new();
let x = sym(&mut a, "x");
let two = a.int(2);
let one = a.one;
let x_sq = a.pow(x, two);
let numer = a.add(&[x_sq, one]);
let denom = a.add(&[x, one]);
let expr = a.div(numer, denom);
let result = cancel(&mut a, expr, x);
assert_eq!(result, expr, "no common factor → unchanged");
}
#[test]
fn cancel_already_simple() {
let mut a = Arena::new();
let x = sym(&mut a, "x");
let expr = a.add(&[x, a.one]);
let result = cancel(&mut a, expr, x);
assert_eq!(result, expr, "no denominator → unchanged");
}
#[test]
fn cancel_quadratic_common_factor() {
let mut a = Arena::new();
let x = sym(&mut a, "x");
let two = a.int(2);
let three = a.int(3);
let six = a.int(6);
let x2 = a.pow(x, two);
let x3 = a.pow(x, three);
let six_x2 = a.mul(&[six, x2]);
let eleven = a.int(11);
let eleven_x = a.mul(&[eleven, x]);
let neg_six_x2 = a.neg(six_x2);
let neg_six = a.neg(six);
let numer = a.add(&[x3, neg_six_x2, eleven_x, neg_six]);
let three_x = a.mul(&[three, x]);
let neg_three_x = a.neg(three_x);
let denom = a.add(&[x2, neg_three_x, two]);
let expr = a.div(numer, denom);
let result = cancel(&mut a, expr, x);
let s = display(&a, result);
assert!(
s.contains('x') && s.contains('3'),
"cancel should give x - 3, got: {s}"
);
let ten = a.int(10);
let val = crate::transforms::subs::subs(&mut a, result, x, ten);
assert_eq!(display(&a, val), "7", "at x=10, x-3 = 7");
}
#[test]
fn cancel_with_coefficients() {
let mut a = Arena::new();
let x = sym(&mut a, "x");
let two = a.int(2);
let x_sq = a.pow(x, two);
let two_x_sq = a.mul(&[two, x_sq]);
let numer = a.sub(two_x_sq, two);
let two_x = a.mul(&[two, x]);
let denom = a.sub(two_x, two);
let expr = a.div(numer, denom);
let result = cancel(&mut a, expr, x);
let s = display(&a, result);
assert_eq!(s, "x + 1", "cancel should give x + 1, got: {s}");
}
#[test]
fn cancel_non_polynomial_returns_unchanged() {
let mut a = Arena::new();
let x = sym(&mut a, "x");
let sin_x = a.sin(x);
let expr = a.div(sin_x, x);
let result = cancel(&mut a, expr, x);
assert_eq!(result, expr, "non-polynomial should be unchanged");
}
#[test]
fn cancel_constant_factor() {
let mut a = Arena::new();
let x = sym(&mut a, "x");
let two = a.int(2);
let two_x = a.mul(&[two, x]);
let two_x_plus_2 = a.add(&[two_x, two]); let frac = a.div(two_x_plus_2, two); let result = cancel(&mut a, frac, x);
let expected = a.add(&[x, a.one]); assert_eq!(result, expected, "(2x+2)/2 should cancel to x+1");
}
#[test]
fn together_uses_lcm_not_product() {
let mut a = Arena::new();
let x = sym(&mut a, "x");
let sym_a = sym(&mut a, "a");
let sym_b = sym(&mut a, "b");
let one = a.one;
let two = a.int(2);
let x_minus_1 = a.sub(x, one);
let x_minus_1_sq = a.pow(x_minus_1, two);
let frac1 = a.div(sym_a, x_minus_1); let frac2 = a.div(sym_b, x_minus_1_sq); let sum = a.add(&[frac1, frac2]);
let result = together(&mut a, sum);
let s = display(&a, result);
assert!(
!s.contains("^3"),
"together should use LCM not product. Got: {s}"
);
}
#[test]
fn together_simplifies_common_factors() {
let mut a = Arena::new();
let x = sym(&mut a, "x");
let sym_a = sym(&mut a, "a");
let sym_b = sym(&mut a, "b");
let one = a.one;
let x_minus_1 = a.sub(x, one);
let two = a.int(2);
let x_minus_1_sq = a.pow(x_minus_1, two);
let frac1 = a.div(sym_a, x_minus_1);
let frac2 = a.div(sym_b, x_minus_1_sq);
let sum = a.add(&[frac1, frac2]);
let result = together(&mut a, sum);
let result_str = display(&a, result);
assert!(
!result_str.contains("^3"),
"together should use LCM not product. Got: {result_str}"
);
}
}