use std::cmp::Ordering;
use num_bigint::BigInt;
use num_rational::Ratio;
use num_traits::{One, Pow as NumPow, Signed, ToPrimitive, Zero};
use rustc_hash::{FxHashMap, FxHashSet};
use smallvec::{SmallVec, smallvec};
use crate::base::arena::Arena;
use crate::base::node::{ExprId, ExprNode};
use crate::base::numeric::Q;
pub(crate) fn canon_add(arena: &mut Arena, args: &[ExprId]) -> ExprId {
if args.is_empty() {
return arena.zero;
}
if args.len() == 1 {
return args[0];
}
if args.len() == 2 {
let (a, b) = (args[0], args[1]);
if let (Some(na), Some(nb)) = (arena.as_num(a), arena.as_num(b)) {
let sum = na.clone() + nb.clone();
if sum.is_zero() {
tracing::debug!("canon_add fast path: two numerics sum to zero");
return arena.zero;
}
let nid = arena.intern_num(sum);
return arena.intern(ExprNode::Num(nid));
}
}
let mut constant: Q = Ratio::zero();
let mut terms: FxHashMap<ExprId, Q> = FxHashMap::default();
let mut has_pos_inf = false;
let mut has_neg_inf = false;
let mut has_zoo = false;
let mut stack: SmallVec<[ExprId; 16]> = SmallVec::from_slice(args);
while let Some(id) = stack.pop() {
match arena.node(id).clone() {
ExprNode::Add(children) => {
stack.extend_from_slice(&children);
}
ExprNode::NaN => {
return arena.nan;
}
ExprNode::Infinity => {
if has_neg_inf || has_zoo {
return arena.nan; }
has_pos_inf = true;
}
ExprNode::NegInfinity => {
if has_pos_inf || has_zoo {
return arena.nan; }
has_neg_inf = true;
}
ExprNode::ComplexInfinity => {
if has_zoo || has_pos_inf || has_neg_inf {
return arena.nan; }
has_zoo = true;
}
ExprNode::Num(nid) => {
constant += arena.num(nid).clone();
}
ExprNode::Neg(inner) => {
let neg_one: Q = -Ratio::one();
let (c, key) = arena.as_coeff_term(inner);
let combined = neg_one * c;
if key == arena.one {
constant += combined;
} else {
*terms.entry(key).or_insert_with(Ratio::zero) += combined;
}
}
_ => {
let (c, key) = arena.as_coeff_term(id);
if key == arena.one {
constant += c;
} else {
*terms.entry(key).or_insert_with(Ratio::zero) += c;
}
}
}
}
if terms.keys().any(|&k| is_directed_infinity(arena, k)) {
return add_directed_infinities(arena, &terms, has_pos_inf, has_neg_inf, has_zoo);
}
if has_pos_inf {
return arena.infinity;
}
if has_neg_inf {
return arena.neg_infinity;
}
if has_zoo {
return arena.complex_infinity;
}
let non_zero_terms: SmallVec<[(ExprId, Q); 8]> =
terms.into_iter().filter(|(_, c)| !c.is_zero()).collect();
let mut result_args: SmallVec<[ExprId; 6]> = SmallVec::new();
if !constant.is_zero() {
let nid = arena.intern_num(constant);
result_args.push(arena.intern(ExprNode::Num(nid)));
}
let mut symbolic_args: SmallVec<[ExprId; 6]> = non_zero_terms
.into_iter()
.map(|(key, coeff)| arena.make_coeff_term(coeff, key))
.collect();
symbolic_args.sort_by(|a, b| arena.sort_key(*a).cmp(arena.sort_key(*b)));
result_args.extend(symbolic_args);
let result = match result_args.len() {
0 => arena.zero,
1 => result_args[0],
_ => arena.intern(ExprNode::Add(result_args)),
};
#[cfg(debug_assertions)]
{
let errors = verify_canonical_shallow(arena, result);
if !errors.is_empty() {
tracing::debug!("canon_add: non-canonical result: {:?}", errors);
}
}
result
}
fn add_directed_infinities(
arena: &mut Arena,
terms: &FxHashMap<ExprId, Q>,
has_pos_inf: bool,
has_neg_inf: bool,
has_zoo: bool,
) -> ExprId {
if has_zoo {
return arena.nan;
}
let mut out: SmallVec<[ExprId; 6]> = SmallVec::new();
for (&key, c) in terms {
if !is_directed_infinity(arena, key) {
continue;
}
if c.is_zero() {
tracing::debug!("canon_add: directed infinities cancel → nan");
return arena.nan;
}
out.push(arena.make_coeff_term(c.clone(), key));
}
if has_pos_inf {
out.push(arena.infinity);
}
if has_neg_inf {
out.push(arena.neg_infinity);
}
if out.len() == 1 {
return out[0];
}
out.sort_by(|a, b| arena.sort_key(*a).cmp(arena.sort_key(*b)));
let result = arena.intern(ExprNode::Add(out));
debug_assert!(
verify_canonical_shallow(arena, result).is_empty(),
"canon_add: non-canonical sum of directed infinities: {:?}",
verify_canonical_shallow(arena, result)
);
result
}
fn zero_times_rest<'a>(
arena: &Arena,
saw_infinity: bool,
remaining: impl IntoIterator<Item = &'a ExprId>,
) -> ExprId {
if saw_infinity {
return arena.nan;
}
let mut stack: SmallVec<[ExprId; 16]> = remaining.into_iter().copied().collect();
while let Some(id) = stack.pop() {
match arena.node(id) {
ExprNode::NaN
| ExprNode::Infinity
| ExprNode::NegInfinity
| ExprNode::ComplexInfinity => {
tracing::debug!("canon_mul: 0 * ∞ → NaN");
return arena.nan;
}
ExprNode::Mul(children) | ExprNode::Add(children) => {
stack.extend_from_slice(children);
}
ExprNode::Neg(inner) => stack.push(*inner),
_ => {}
}
}
arena.zero
}
pub(crate) fn canon_mul(arena: &mut Arena, args: &[ExprId]) -> ExprId {
if args.is_empty() {
return arena.one;
}
if args.len() == 1 {
return args[0];
}
if args.len() == 2 {
let (a, b) = (args[0], args[1]);
if let (Some(na), Some(nb)) = (arena.as_num(a), arena.as_num(b)) {
let product = na.clone() * nb.clone();
if product.is_zero() {
tracing::debug!("canon_mul fast path: two numerics multiply to zero");
return arena.zero;
}
if product.is_one() {
return arena.one;
}
let nid = arena.intern_num(product);
return arena.intern(ExprNode::Num(nid));
}
}
let mut coeff: Q = Ratio::one();
let mut bases: FxHashMap<ExprId, SmallVec<[ExprId; 4]>> = FxHashMap::default();
let mut saw_infinity = false;
let mut stack: SmallVec<[ExprId; 16]> = SmallVec::from_slice(args);
while let Some(id) = stack.pop() {
match arena.node(id).clone() {
ExprNode::Mul(children) => {
stack.extend_from_slice(&children);
}
ExprNode::NaN => {
return arena.nan;
}
ExprNode::Infinity | ExprNode::NegInfinity | ExprNode::ComplexInfinity => {
saw_infinity = true;
match arena.node(id) {
ExprNode::NegInfinity => {
coeff = -coeff;
}
ExprNode::ComplexInfinity => {
return handle_mul_with_zoo(arena, &mut stack, &coeff);
}
_ => {} }
}
ExprNode::Num(nid) => {
let val = arena.num(nid).clone();
coeff *= val;
if coeff.is_zero() {
return zero_times_rest(arena, saw_infinity, stack.iter().chain(bases.keys()));
}
}
ExprNode::Neg(inner) => {
coeff = -coeff;
stack.push(inner);
}
_ => {
let (base, exp) = arena.as_base_exp(id);
bases.entry(base).or_default().push(exp);
}
}
}
if coeff.is_zero() {
return zero_times_rest(arena, saw_infinity, bases.keys());
}
let mut factors: SmallVec<[(ExprId, ExprId); 8]> = SmallVec::new();
for (base, exponents) in &bases {
let combined_exp: ExprId = if exponents.len() == 1 {
exponents[0]
} else {
canon_add(arena, exponents)
};
factors.push((*base, combined_exp));
}
{
let mut combined_factors: SmallVec<[(ExprId, ExprId); 8]> = SmallVec::new();
let mut exp_groups: FxHashMap<ExprId, (Q, usize)> = FxHashMap::default();
let mut factor_used: SmallVec<[bool; 8]> = smallvec::smallvec![false; factors.len()];
for (i, &(base, exp_id)) in factors.iter().enumerate() {
let dominated = 'check: {
let base_r = match arena.as_num(base) {
Some(r) if r.is_positive() => r.clone(),
_ => break 'check false,
};
let exp_r = match arena.as_num(exp_id) {
Some(r) if !r.is_integer() => r,
_ => break 'check false,
};
let _ = exp_r;
let entry = exp_groups
.entry(exp_id)
.or_insert_with(|| (Ratio::one(), 0));
entry.0 *= &base_r;
entry.1 += 1;
factor_used[i] = true;
true
};
let _ = dominated;
}
let any_combined = exp_groups.values().any(|&(_, count)| count > 1);
if any_combined {
tracing::trace!(
n_groups = exp_groups.len(),
"canon_mul: exponent-grouping combined positive numeric bases"
);
for (i, &(base, exp_id)) in factors.iter().enumerate() {
if !factor_used[i] {
combined_factors.push((base, exp_id));
}
}
for (exp_id, (product, count)) in &exp_groups {
if *count <= 1 {
for (i, &(_, e)) in factors.iter().enumerate() {
if factor_used[i] && e == *exp_id {
combined_factors.push(factors[i]);
break;
}
}
} else {
let prod_nid = arena.intern_num(product.clone());
let prod_base = arena.intern(ExprNode::Num(prod_nid));
combined_factors.push((prod_base, *exp_id));
}
}
factors = combined_factors;
}
}
factors.sort_by(|(a, _), (b, _)| arena.sort_key(*a).cmp(arena.sort_key(*b)));
let mut result_args: SmallVec<[ExprId; 6]> = SmallVec::new();
let mut has_nested_mul = false;
let singular = |arena: &Arena, id: ExprId| {
matches!(
arena.node(id),
ExprNode::Infinity | ExprNode::NegInfinity | ExprNode::ComplexInfinity | ExprNode::NaN
)
};
for (base, exp) in factors {
if exp == arena.one {
if let Some(val) = arena.as_num(base) {
coeff *= val.clone();
continue;
}
if singular(arena, base) {
has_nested_mul = true;
}
if matches!(arena.node(base), ExprNode::Mul(_)) {
has_nested_mul = true;
}
result_args.push(base);
} else if arena.is_zero_structural(exp) {
continue;
} else {
let pow_id = canon_pow(arena, base, exp);
if pow_id == arena.one {
continue;
}
if let Some(val) = arena.as_num(pow_id) {
coeff *= val.clone();
continue;
}
if matches!(arena.node(pow_id), ExprNode::Mul(_)) || singular(arena, pow_id) {
has_nested_mul = true;
}
result_args.push(pow_id);
}
}
if !has_nested_mul && result_args.len() > 1 {
let mut seen: FxHashSet<ExprId> = FxHashSet::default();
for &r in &result_args {
if !seen.insert(arena.as_base_exp(r).0) {
has_nested_mul = true;
break;
}
}
}
if has_nested_mul && !coeff.is_zero() {
tracing::trace!("canon_mul: canon_pow produced a Mul factor; re-flattening");
let mut all: SmallVec<[ExprId; 8]> = SmallVec::new();
if !coeff.is_one() {
let nid = arena.intern_num(coeff);
all.push(arena.intern(ExprNode::Num(nid)));
}
all.extend(result_args);
if saw_infinity {
all.push(arena.infinity);
}
return canon_mul(arena, &all);
}
if coeff.is_zero() {
return zero_times_rest(arena, saw_infinity, result_args.iter());
}
if !coeff.is_one() || result_args.is_empty() {
let nid = arena.intern_num(coeff.clone());
result_args.insert(0, arena.intern(ExprNode::Num(nid)));
}
if saw_infinity {
return directed_infinity(arena, coeff.is_negative(), &result_args);
}
result_args.sort_by(|a, b| arena.sort_key(*a).cmp(arena.sort_key(*b)));
if result_args.len() == 2 {
let first = result_args[0];
let second = result_args[1];
if let ExprNode::Num(_) = arena.node(first)
&& let ExprNode::Add(add_children) = arena.node(second).clone()
{
let distributed: SmallVec<[ExprId; 6]> = add_children
.iter()
.map(|&child| canon_mul(arena, &[first, child]))
.collect();
return canon_add(arena, &distributed);
}
}
let result = match result_args.len() {
0 => arena.one,
1 => result_args[0],
_ => arena.intern(ExprNode::Mul(result_args)),
};
debug_assert!(
verify_canonical_shallow(arena, result).is_empty(),
"canon_mul produced non-canonical result: {:?}",
verify_canonical_shallow(arena, result)
);
result
}
fn directed_infinity(arena: &mut Arena, negative: bool, factors: &[ExprId]) -> ExprId {
let mut negative = negative;
let mut kept: SmallVec<[ExprId; 6]> = SmallVec::new();
for &f in factors.iter().filter(|&&f| arena.as_num(f).is_none()) {
match known_sign(arena, f) {
Some(Ordering::Greater) => {}
Some(Ordering::Less) => negative = !negative,
_ => kept.push(f),
}
}
if kept.is_empty() {
return if negative {
arena.neg_infinity
} else {
arena.infinity
};
}
if negative {
kept.push(arena.neg_one);
}
kept.push(arena.infinity);
kept.sort_by(|a, b| arena.sort_key(*a).cmp(arena.sort_key(*b)));
let result = arena.intern(ExprNode::Mul(kept));
debug_assert!(
verify_canonical_shallow(arena, result).is_empty(),
"directed infinity not canonical: {:?}",
verify_canonical_shallow(arena, result)
);
result
}
pub(crate) fn is_directed_infinity(arena: &Arena, id: ExprId) -> bool {
matches!(arena.node(id), ExprNode::Mul(ch) if ch.contains(&arena.infinity))
}
fn known_sign(arena: &Arena, f: ExprId) -> Option<Ordering> {
use crate::base::assumptions::{AssumptionCache, Props};
let mut cache = AssumptionCache::new();
if cache.query(arena, f, Props::POSITIVE) == Some(true) {
return Some(Ordering::Greater);
}
if cache.query(arena, f, Props::NEGATIVE) == Some(true) {
return Some(Ordering::Less);
}
if cache.query(arena, f, Props::REAL) != Some(true) {
return None;
}
arena.sign_of_real_constant(f)
}
fn handle_mul_with_zoo(
arena: &mut Arena,
remaining: &mut SmallVec<[ExprId; 16]>,
coeff: &Q,
) -> ExprId {
if coeff.is_zero() {
return arena.nan;
}
while let Some(id) = remaining.pop() {
match arena.node(id).clone() {
ExprNode::NaN => return arena.nan,
ExprNode::Num(nid) if arena.num(nid).is_zero() => {
return arena.nan;
}
ExprNode::Mul(children) => {
remaining.extend_from_slice(&children);
}
_ => {}
}
}
arena.intern(ExprNode::ComplexInfinity)
}
pub(crate) fn canon_pow(arena: &mut Arena, base: ExprId, exp: ExprId) -> ExprId {
if base == arena.e_const {
return arena.intern(ExprNode::Exp(exp));
}
if let ExprNode::Pow(inner_base, inner_exp) = arena.node(base).clone()
&& let (Some(b), Some(c)) = (arena.as_num(inner_exp), arena.as_num(exp))
&& c.is_integer()
{
tracing::trace!("canon_pow: flattening Pow(Pow(a,b),c) → Pow(a,b*c)");
let product = b * c;
let prod_id = arena.intern_num(product);
let prod_expr = arena.intern(ExprNode::Num(prod_id));
return canon_pow(arena, inner_base, prod_expr);
}
if let ExprNode::Mul(ref children) = arena.node(base).clone()
&& let Some(exp_r) = arena.as_num(exp)
&& exp_r.is_integer()
{
let n_i64: i64 = exp_r.to_integer().try_into().unwrap_or(i64::MAX);
if n_i64.unsigned_abs() <= 10 && children.len() >= 2 {
tracing::trace!(
n = n_i64,
n_factors = children.len(),
"canon_pow: distributing integer power over Mul"
);
let distributed: SmallVec<[ExprId; 6]> = children
.iter()
.map(|&child| canon_pow(arena, child, exp))
.collect();
return canon_mul(arena, &distributed);
}
if children.len() >= 2
&& let Some(&first) = children.first()
&& matches!(arena.node(first), ExprNode::Num(_))
&& n_i64 != i64::MAX
{
let coeff_pow = canon_pow(arena, first, exp);
let rest: SmallVec<[ExprId; 6]> = children[1..].iter().copied().collect();
let rest_id = if rest.len() == 1 {
rest[0]
} else {
canon_mul(arena, &rest)
};
let rest_pow = canon_pow(arena, rest_id, exp);
return canon_mul(arena, &[coeff_pow, rest_pow]);
}
}
if base == arena.i_unit
&& let Some(exp_r) = arena.as_num(exp)
&& exp_r.is_integer()
{
use num_integer::Integer;
let exp_int = exp_r.to_integer();
let four = BigInt::from(4);
let remainder = exp_int.mod_floor(&four);
let r: u32 = remainder.try_into().unwrap_or(0);
return match r {
0 => arena.one,
1 => arena.i_unit,
2 => arena.neg_one,
_ => arena.neg(arena.i_unit),
};
}
if base == arena.neg_one
&& let Some(exp_r) = arena.as_num(exp)
{
let denom = exp_r.denom();
let numer = exp_r.numer();
if *denom == BigInt::from(2) {
let n = numer.clone();
let four = BigInt::from(4);
use num_integer::Integer;
let remainder = n.mod_floor(&four);
let r: u32 = (&remainder).try_into().unwrap_or(0);
return match r {
0 => arena.one,
1 => arena.i_unit,
2 => arena.neg_one,
_ => arena.neg(arena.i_unit),
};
}
}
if let Some(base_r) = arena.as_num(base)
&& base_r.is_negative()
&& let Some(exp_r) = arena.as_num(exp)
&& *exp_r == Ratio::new(1.into(), 2.into())
{
let abs_base = {
let abs_val = -base_r.clone();
let nid = arena.intern_num(abs_val);
arena.intern(ExprNode::Num(nid))
};
let sqrt_abs = canon_pow(arena, abs_base, exp);
return arena.mul(&[arena.i_unit, sqrt_abs]);
}
if base == arena.nan || exp == arena.nan {
return arena.nan;
}
if exp == arena.zero {
if base == arena.infinity || base == arena.neg_infinity || base == arena.complex_infinity {
return arena.nan;
}
return arena.one;
}
if exp == arena.one {
return base;
}
if base == arena.one {
return arena.one;
}
if base == arena.zero
&& let Some(e) = arena.as_num(exp)
&& e.is_positive()
{
return arena.zero;
}
if base == arena.zero
&& let Some(e) = arena.as_num(exp)
&& e.is_negative()
{
return arena.complex_infinity;
}
if base == arena.zero && exp == arena.infinity {
return arena.zero;
}
if base == arena.zero && exp == arena.neg_infinity {
return arena.complex_infinity;
}
if base == arena.zero && exp == arena.complex_infinity {
return arena.nan;
}
if base == arena.zero
&& let Some(value) = zero_power(arena, exp)
{
return value;
}
if base == arena.infinity
&& let Some(e) = arena.as_num(exp)
&& e.is_positive()
{
return arena.infinity;
}
if base == arena.infinity
&& let Some(e) = arena.as_num(exp)
&& e.is_negative()
{
return arena.zero;
}
if base == arena.infinity && exp == arena.infinity {
return arena.infinity;
}
if base == arena.infinity && exp == arena.neg_infinity {
return arena.zero;
}
if base == arena.neg_infinity
&& let Some(e) = arena.as_num(exp)
&& e.is_integer()
&& e.is_positive()
{
let n: i64 = e.to_integer().try_into().unwrap_or(0);
return if n % 2 == 0 {
arena.infinity
} else {
arena.neg_infinity
};
}
if base == arena.neg_infinity
&& let Some(e) = arena.as_num(exp)
&& e.is_positive()
&& !e.is_integer()
{
let unit = canon_pow(arena, arena.neg_one, exp);
return canon_mul(arena, &[unit, arena.infinity]);
}
if base == arena.neg_infinity
&& let Some(e) = arena.as_num(exp)
&& e.is_negative()
{
return arena.zero;
}
if base == arena.neg_infinity && (exp == arena.infinity || exp == arena.neg_infinity) {
return arena.nan;
}
if base == arena.complex_infinity
&& let Some(e) = arena.as_num(exp)
&& e.is_positive()
{
return arena.complex_infinity;
}
if base == arena.complex_infinity
&& let Some(e) = arena.as_num(exp)
&& e.is_negative()
{
return arena.zero;
}
if base == arena.complex_infinity
&& (exp == arena.complex_infinity || exp == arena.infinity || exp == arena.neg_infinity)
{
return arena.nan;
}
if let (Some(b), Some(e)) = (arena.as_num(base).cloned(), arena.as_num(exp).cloned())
&& let Some(result) = eval_numeric_pow(arena, &b, &e)
{
return result;
}
if let (Some(base_r), Some(exp_r)) = (arena.as_num(base).cloned(), arena.as_num(exp).cloned())
&& base_r.is_positive()
&& !exp_r.is_integer()
&& let Some(result) = canon_radical(arena, base, &base_r, exp, &exp_r)
{
return result;
}
let result = arena.intern(ExprNode::Pow(base, exp));
#[cfg(debug_assertions)]
{
let errors = verify_canonical_shallow(arena, result);
if !errors.is_empty() {
tracing::debug!("canon_pow: non-canonical result: {:?}", errors);
}
}
result
}
fn zero_power(arena: &mut Arena, exp: ExprId) -> Option<ExprId> {
use crate::base::assumptions::{AssumptionCache, Props};
if arena.as_num(exp).is_some() {
return None;
}
let terms: SmallVec<[ExprId; 6]> = match arena.node(exp) {
ExprNode::Add(t) => t.clone(),
_ => smallvec![exp],
};
let is_imaginary_multiple =
|id: ExprId| matches!(classify_arg(arena, id), Arg::ImaginaryMultiple(_));
let real: SmallVec<[ExprId; 6]> = terms
.iter()
.copied()
.filter(|&t| !is_imaginary_multiple(t))
.collect();
let has_imaginary = real.len() < terms.len();
let re = canon_add(arena, &real);
let mut cache = AssumptionCache::new();
if re == arena.zero || cache.query(arena, re, Props::REAL) != Some(true) {
return (has_imaginary && re == arena.zero).then_some(arena.nan);
}
if has_imaginary {
return Some(arena.nan);
}
if cache.query(arena, re, Props::POSITIVE) == Some(true) {
Some(arena.zero)
} else if cache.query(arena, re, Props::NEGATIVE) == Some(true) {
Some(arena.complex_infinity)
} else {
None
}
}
fn eval_numeric_pow(arena: &mut Arena, b: &Q, e: &Q) -> Option<ExprId> {
if !e.is_integer() {
return None;
}
let exp_int: BigInt = e.to_integer();
let max_exp = arena.config.max_pow_exponent as u64;
let abs_exp: BigInt = exp_int.abs();
if abs_exp > BigInt::from(max_exp) {
return None;
}
if b.is_zero() {
return None;
}
let exp_u32: u32 = match abs_exp.try_into() {
Ok(v) => v,
Err(_) => return None,
};
if exp_u32 != 1
&& min_pow_digits(b.numer(), exp_u32) + min_pow_digits(b.denom(), exp_u32)
> arena.config.max_result_digits as f64 + 2.0
{
return None;
}
let numer: BigInt = b.numer().clone();
let denom: BigInt = b.denom().clone();
let pow_n: BigInt = NumPow::pow(numer, exp_u32);
let pow_d: BigInt = NumPow::pow(denom, exp_u32);
let result = if exp_int.is_negative() {
if pow_n.is_zero() {
return None;
}
Ratio::new(pow_d, pow_n)
} else {
Ratio::new(pow_n, pow_d)
};
if exp_u32 != 1 {
let digit_count = result.numer().to_string().len() + result.denom().to_string().len();
if digit_count > arena.config.max_result_digits {
return None;
}
}
let nid = arena.intern_num(result);
Some(arena.intern(ExprNode::Num(nid)))
}
fn min_pow_digits(x: &BigInt, n: u32) -> f64 {
let bits = x.bits().saturating_sub(1) as f64 * f64::from(n);
(bits * std::f64::consts::LOG10_2).floor() + 1.0
}
fn pow_exceeds_digit_guard(arena: &Arena, x: &BigInt, n: u32) -> bool {
min_pow_digits(x, n) > arena.config.max_result_digits as f64 + 1.0
}
fn canon_radical(
arena: &mut Arena,
base: ExprId,
base_r: &Q,
exp: ExprId,
exp_r: &Q,
) -> Option<ExprId> {
debug_assert!(base_r.is_positive() && !exp_r.is_integer());
if !base_r.is_integer() {
tracing::trace!("canon_radical: splitting rational base");
let p = arena.big_int(base_r.numer().clone());
let q = arena.big_int(base_r.denom().clone());
let neg_exp = {
let nid = arena.intern_num(-exp_r.clone());
arena.intern(ExprNode::Num(nid))
};
let p_pow = canon_pow(arena, p, exp);
let q_pow = canon_pow(arena, q, neg_exp);
return Some(arena.mul(&[p_pow, q_pow]));
}
let n = base_r.to_integer();
if n <= BigInt::one() {
return None;
}
if exp_r.is_negative() {
let a = -exp_r.numer().clone();
let b = exp_r.denom().clone();
let (k, s) = num_integer::Integer::div_rem(&a, &b);
if (&k + BigInt::one()) > BigInt::from(arena.config.max_pow_exponent) {
return None;
}
tracing::trace!("canon_radical: rationalising negative fractional exponent");
let pos_exp = {
let nid = arena.intern_num(Ratio::new(&b - &s, b.clone()));
arena.intern(ExprNode::Num(nid))
};
let int_exp = {
let nid = arena.intern_num(Ratio::from_integer(-(k + BigInt::one())));
arena.intern(ExprNode::Num(nid))
};
let coeff = canon_pow(arena, base, int_exp);
arena.as_num(coeff)?;
let radical = canon_pow(arena, base, pos_exp);
return Some(arena.mul(&[coeff, radical]));
}
let b: u32 = exp_r.denom().to_u32()?;
let a: u32 = exp_r.numer().to_u32()?;
let (outside, inside) = split_perfect_power(&n, b);
if outside.is_one() {
if a > b
&& (a / b) <= arena.config.max_pow_exponent as u32
&& !pow_exceeds_digit_guard(arena, &n, a / b)
{
let k = a / b;
let s = a % b;
let integer_part = arena.big_int(NumPow::pow(n.clone(), k));
let frac_exp = {
let nid = arena.intern_num(Ratio::new(BigInt::from(s), BigInt::from(b)));
arena.intern(ExprNode::Num(nid))
};
let radical = arena.intern(ExprNode::Pow(base, frac_exp));
return Some(arena.mul(&[integer_part, radical]));
}
return None;
}
if a > arena.config.max_pow_exponent as u32 || pow_exceeds_digit_guard(arena, &outside, a) {
return None;
}
tracing::trace!("canon_radical: extracted perfect power factor");
let outside_pow = NumPow::pow(outside, a);
let outside_expr = arena.big_int(outside_pow);
if inside.is_one() {
return Some(outside_expr);
}
let inside_base = arena.big_int(inside);
let inside_radical = if a > b {
canon_pow(arena, inside_base, exp)
} else {
arena.intern(ExprNode::Pow(inside_base, exp))
};
Some(arena.mul(&[outside_expr, inside_radical]))
}
const RADICAL_FACTOR_MAX_BITS: u64 = 84;
const RADICAL_MEMO_MIN_BITS: u64 = 32;
const RADICAL_MEMO_CAPACITY: usize = 256;
type RadicalParts = (Vec<(BigInt, u32)>, BigInt);
thread_local! {
static RADICAL_PARTS: std::cell::RefCell<FxHashMap<BigInt, std::rc::Rc<RadicalParts>>> =
std::cell::RefCell::new(FxHashMap::default());
}
fn radical_parts(n: &BigInt) -> std::rc::Rc<RadicalParts> {
let compute = || {
std::rc::Rc::new(crate::domains::ntheory::squarefree_parts_bounded(
n,
RADICAL_FACTOR_MAX_BITS,
))
};
if n.bits() <= RADICAL_MEMO_MIN_BITS {
return compute();
}
let cached = RADICAL_PARTS
.try_with(|memo| memo.try_borrow().ok().and_then(|m| m.get(n).cloned()))
.ok()
.flatten();
if let Some(parts) = cached {
return parts;
}
let parts = compute();
let _ = RADICAL_PARTS.try_with(|memo| {
if let Ok(mut memo) = memo.try_borrow_mut() {
if memo.len() >= RADICAL_MEMO_CAPACITY {
memo.clear();
}
memo.insert(n.clone(), std::rc::Rc::clone(&parts));
}
});
parts
}
pub(crate) fn split_perfect_power(n: &BigInt, k: u32) -> (BigInt, BigInt) {
if k < 2 || *n <= BigInt::one() {
return (BigInt::one(), n.clone());
}
let root = n.nth_root(k);
if NumPow::pow(root.clone(), k) == *n {
return (root, BigInt::one());
}
let parts = radical_parts(n);
let (pieces, cofactor) = &*parts;
let mut outside = BigInt::one();
let mut inside = cofactor.clone();
for (d, e) in pieces {
if *e >= k {
outside *= NumPow::pow(d.clone(), e / k);
}
if e % k > 0 {
inside *= NumPow::pow(d.clone(), e % k);
}
}
(outside, inside)
}
pub(crate) fn canon_neg(arena: &mut Arena, expr: ExprId) -> ExprId {
let result = match arena.node(expr).clone() {
ExprNode::Neg(inner) => inner,
ExprNode::Num(nid) => {
let val = arena.num(nid).clone();
let neg_val = -val;
let neg_nid = arena.intern_num(neg_val);
arena.intern(ExprNode::Num(neg_nid))
}
ExprNode::NaN => arena.nan,
ExprNode::Infinity => arena.neg_infinity,
ExprNode::NegInfinity => arena.infinity,
ExprNode::ComplexInfinity => arena.intern(ExprNode::ComplexInfinity),
ExprNode::Add(children) => {
let negated: SmallVec<[ExprId; 6]> = children
.iter()
.map(|&child| canon_neg(arena, child))
.collect();
canon_add(arena, &negated)
}
ExprNode::Mul(ref children) if !children.is_empty() => {
if let ExprNode::Num(nid) = arena.node(children[0]) {
let nid = *nid;
let val = arena.num(nid).clone();
let neg_val = -val;
let neg_nid = arena.intern_num(neg_val);
let neg_coeff = arena.intern(ExprNode::Num(neg_nid));
let mut new_args: SmallVec<[ExprId; 6]> = smallvec![neg_coeff];
new_args.extend_from_slice(&children[1..]);
canon_mul(arena, &new_args)
} else {
let neg_one = arena.neg_one;
let mut all: SmallVec<[ExprId; 6]> = smallvec![neg_one];
let children = children.clone();
all.extend_from_slice(&children);
canon_mul(arena, &all)
}
}
_ => {
let neg_one = arena.neg_one;
canon_mul(arena, &[neg_one, expr])
}
};
#[cfg(debug_assertions)]
{
let errors = verify_canonical_shallow(arena, result);
if !errors.is_empty() {
tracing::debug!("canon_neg: non-canonical result: {:?}", errors);
}
}
result
}
enum Folded {
Rational(Q),
Imaginary(Q),
Infinity,
NegInfinity,
ImaginaryInfinity(bool),
ComplexInfinity,
NaN,
}
impl Folded {
fn int(n: i64) -> Self {
Folded::Rational(Q::from_integer(BigInt::from(n)))
}
fn ratio(n: i64, d: i64) -> Self {
Folded::Rational(Q::new(BigInt::from(n), BigInt::from(d)))
}
}
enum Arg {
Rational(Q),
PiMultiple(Q),
ImaginaryPiMultiple(Q),
ImaginaryMultiple(Q),
E,
Infinity,
NegInfinity,
ImaginaryInfinity(bool),
ComplexInfinity,
NaN,
Other,
}
fn classify_arg(arena: &Arena, id: ExprId) -> Arg {
if let Some(q) = inverse_trig_pi_multiple(arena, id) {
return Arg::PiMultiple(q);
}
match arena.node(id) {
ExprNode::Num(n) => Arg::Rational(arena.num(*n).clone()),
ExprNode::Ln(w) if *w == arena.neg_one => Arg::ImaginaryPiMultiple(Q::one()),
ExprNode::Pi => Arg::PiMultiple(Q::one()),
ExprNode::ImaginaryUnit => Arg::ImaginaryMultiple(Q::one()),
ExprNode::E => Arg::E,
ExprNode::Infinity => Arg::Infinity,
ExprNode::NegInfinity => Arg::NegInfinity,
ExprNode::ComplexInfinity => Arg::ComplexInfinity,
ExprNode::NaN => Arg::NaN,
ExprNode::Mul(children) => {
let (q, rest) = match children.first().map(|&c| arena.node(c)) {
Some(ExprNode::Num(n)) => (arena.num(*n).clone(), &children[1..]),
_ => (Q::one(), &children[..]),
};
let (pi, i) = (arena.pi, arena.i_unit);
match *rest {
[a] if a == pi => Arg::PiMultiple(q),
[a] if inverse_trig_pi_multiple(arena, a).is_some() => {
Arg::PiMultiple(q * inverse_trig_pi_multiple(arena, a).unwrap_or_default())
}
[a] if matches!(arena.node(a), ExprNode::Ln(w) if *w == arena.neg_one) => {
Arg::ImaginaryPiMultiple(q)
}
[a] if a == i => Arg::ImaginaryMultiple(q),
[a, b] if (a == pi && b == i) || (a == i && b == pi) => Arg::ImaginaryPiMultiple(q),
[a, b] if a == i && b == arena.infinity => Arg::ImaginaryInfinity(q.is_negative()),
_ => Arg::Other,
}
}
_ => Arg::Other,
}
}
fn inverse_trig_pi_multiple(arena: &Arena, id: ExprId) -> Option<Q> {
let (a, table): (ExprId, &[(i64, i64, i64, i64)]) = match *arena.node(id) {
ExprNode::Asin(a) => (
a,
&[(1, 1, 1, 2), (-1, 1, -1, 2), (1, 2, 1, 6), (-1, 2, -1, 6)],
),
ExprNode::Acos(a) => (
a,
&[(0, 1, 1, 2), (-1, 1, 1, 1), (1, 2, 1, 3), (-1, 2, 2, 3)],
),
ExprNode::Atan(a) => {
if a == arena.infinity {
return Some(Q::new(BigInt::from(1), BigInt::from(2)));
}
if a == arena.neg_infinity {
return Some(Q::new(BigInt::from(-1), BigInt::from(2)));
}
(a, &[(1, 1, 1, 4), (-1, 1, -1, 4)])
}
_ => return None,
};
let v = arena.as_num(a)?;
table.iter().find_map(|&(p, q, r, s)| {
(*v == Q::new(BigInt::from(p), BigInt::from(q)))
.then(|| Q::new(BigInt::from(r), BigInt::from(s)))
})
}
fn mod_rational(q: &Q, m: i64) -> Q {
let m = Q::from_integer(BigInt::from(m));
q - (q / &m).floor() * &m
}
fn in_units(q: &Q, n: i64) -> Option<i64> {
let t = q * Q::from_integer(BigInt::from(n));
if t.is_integer() {
t.to_integer().to_i64()
} else {
None
}
}
fn sin_pi(q: &Q) -> Option<Folded> {
Some(match in_units(&mod_rational(q, 2), 6)? {
0 | 6 => Folded::int(0),
1 | 5 => Folded::ratio(1, 2),
3 => Folded::int(1),
7 | 11 => Folded::ratio(-1, 2),
9 => Folded::int(-1),
_ => return None,
})
}
fn cos_pi(q: &Q) -> Option<Folded> {
Some(match in_units(&mod_rational(q, 2), 6)? {
0 => Folded::int(1),
2 | 10 => Folded::ratio(1, 2),
3 | 9 => Folded::int(0),
4 | 8 => Folded::ratio(-1, 2),
6 => Folded::int(-1),
_ => return None,
})
}
fn tan_pi(q: &Q) -> Option<Folded> {
Some(match in_units(&mod_rational(q, 1), 4)? {
0 => Folded::int(0),
1 => Folded::int(1),
2 => Folded::ComplexInfinity,
3 => Folded::int(-1),
_ => return None,
})
}
fn is_nonpositive_int(q: &Q) -> bool {
q.is_integer() && !q.is_positive()
}
pub(crate) fn canon_function(arena: &mut Arena, node: &ExprNode) -> Option<ExprId> {
if let ExprNode::Abs(a) = *node
&& is_positive_constant(arena, a)
{
return Some(a);
}
if let ExprNode::Sign(a) = *node
&& is_positive_constant(arena, a)
{
return Some(arena.one);
}
if let ExprNode::Abs(a) = *node {
let is_unit = |c: ExprId| match *arena.node(c) {
ExprNode::ImaginaryUnit => true,
ExprNode::Pow(b, e) => {
let unit_base = b == arena.i_unit
|| b == arena.neg_one
|| matches!(classify_arg(arena, b), Arg::ImaginaryMultiple(q) if (-&q).is_one());
unit_base && arena.as_num(e).is_some()
}
ExprNode::Exp(x) => matches!(classify_arg(arena, x), Arg::ImaginaryPiMultiple(_)),
_ => false,
};
let is_positive = |c: ExprId| match *arena.node(c) {
ExprNode::Infinity => true,
ExprNode::Pow(b, e) => {
(is_positive_constant(arena, b) || arena.as_num(b).is_some_and(|q| q.is_positive()))
&& arena.as_num(e).is_some()
}
_ => is_positive_constant(arena, c),
};
if is_unit(a) {
return Some(arena.one);
}
if let ExprNode::Mul(children) = arena.node(a) {
let mut kept: SmallVec<[(ExprId, Option<Q>); 6]> = SmallVec::new();
let mut all_known = true;
for &c in children.iter() {
if let Some(q) = arena.as_num(c) {
kept.push((c, Some(q.abs())));
} else if is_positive(c) {
kept.push((c, None));
} else if !is_unit(c) {
all_known = false;
break;
}
}
if all_known {
let factors: SmallVec<[ExprId; 6]> = kept
.into_iter()
.map(|(c, q)| match q {
Some(q) => {
let nid = arena.intern_num(q);
arena.intern(ExprNode::Num(nid))
}
None => c,
})
.collect();
return Some(canon_mul(arena, &factors));
}
}
}
let folded = match *node {
ExprNode::Sin(a) => match classify_arg(arena, a) {
Arg::Rational(q) if q.is_zero() => Folded::int(0),
Arg::PiMultiple(q) => sin_pi(&q)?,
Arg::ImaginaryInfinity(neg) => Folded::ImaginaryInfinity(neg),
Arg::ComplexInfinity | Arg::NaN => Folded::NaN,
_ => return None,
},
ExprNode::Cos(a) => match classify_arg(arena, a) {
Arg::Rational(q) if q.is_zero() => Folded::int(1),
Arg::PiMultiple(q) => cos_pi(&q)?,
Arg::ImaginaryInfinity(_) => Folded::Infinity,
Arg::ComplexInfinity | Arg::NaN => Folded::NaN,
_ => return None,
},
ExprNode::Tan(a) => match classify_arg(arena, a) {
Arg::Rational(q) if q.is_zero() => Folded::int(0),
Arg::PiMultiple(q) => tan_pi(&q)?,
Arg::ImaginaryInfinity(neg) => {
Folded::Imaginary(if neg { -Q::one() } else { Q::one() })
}
Arg::ComplexInfinity | Arg::NaN => Folded::NaN,
_ => return None,
},
ExprNode::Exp(a) => match classify_arg(arena, a) {
Arg::Rational(q) if q.is_zero() => Folded::int(1),
Arg::ImaginaryPiMultiple(q) => match in_units(&mod_rational(&q, 2), 2)? {
0 => Folded::int(1),
1 => Folded::Imaginary(Q::one()),
2 => Folded::int(-1),
3 => Folded::Imaginary(-Q::one()),
_ => return None,
},
Arg::Infinity => Folded::Infinity,
Arg::NegInfinity => Folded::int(0),
Arg::ImaginaryInfinity(_) | Arg::ComplexInfinity | Arg::NaN => Folded::NaN,
_ => return None,
},
ExprNode::Ln(a) => match classify_arg(arena, a) {
Arg::Rational(q) if q.is_zero() => Folded::ComplexInfinity,
Arg::Rational(q) if q.is_one() => Folded::int(0),
Arg::E => Folded::int(1),
Arg::Infinity | Arg::NegInfinity | Arg::ImaginaryInfinity(_) => Folded::Infinity,
Arg::ComplexInfinity => Folded::ComplexInfinity,
Arg::NaN => Folded::NaN,
_ => return None,
},
ExprNode::Sinh(a) => match classify_arg(arena, a) {
Arg::Rational(q) if q.is_zero() => Folded::int(0),
Arg::ImaginaryPiMultiple(q) => times_i(sin_pi(&q)?)?,
Arg::Infinity => Folded::Infinity,
Arg::NegInfinity => Folded::NegInfinity,
Arg::ComplexInfinity | Arg::NaN => Folded::NaN,
_ => return None,
},
ExprNode::Cosh(a) => match classify_arg(arena, a) {
Arg::Rational(q) if q.is_zero() => Folded::int(1),
Arg::ImaginaryPiMultiple(q) => cos_pi(&q)?,
Arg::Infinity | Arg::NegInfinity => Folded::Infinity,
Arg::ComplexInfinity | Arg::NaN => Folded::NaN,
_ => return None,
},
ExprNode::Tanh(a) => match classify_arg(arena, a) {
Arg::Rational(q) if q.is_zero() => Folded::int(0),
Arg::ImaginaryPiMultiple(q) => times_i(tan_pi(&q)?)?,
Arg::Infinity => Folded::int(1),
Arg::NegInfinity => Folded::int(-1),
Arg::ComplexInfinity | Arg::NaN => Folded::NaN,
_ => return None,
},
ExprNode::Asin(a) => match classify_arg(arena, a) {
Arg::Rational(q) if q.is_zero() => Folded::int(0),
Arg::Infinity => Folded::ImaginaryInfinity(true),
Arg::NegInfinity => Folded::ImaginaryInfinity(false),
Arg::ImaginaryInfinity(neg) => Folded::ImaginaryInfinity(neg),
Arg::ComplexInfinity => Folded::ComplexInfinity,
Arg::NaN => Folded::NaN,
_ => return None,
},
ExprNode::Acos(a) => match classify_arg(arena, a) {
Arg::Rational(q) if q.is_one() => Folded::int(0),
Arg::Infinity => Folded::ImaginaryInfinity(false),
Arg::NegInfinity => Folded::ImaginaryInfinity(true),
Arg::ImaginaryInfinity(neg) => Folded::ImaginaryInfinity(!neg),
Arg::ComplexInfinity => Folded::ComplexInfinity,
Arg::NaN => Folded::NaN,
_ => return None,
},
ExprNode::Atan(a) => match classify_arg(arena, a) {
Arg::Rational(q) if q.is_zero() => Folded::int(0),
Arg::ImaginaryMultiple(q) if q.is_one() || (-&q).is_one() => {
Folded::ImaginaryInfinity(q.is_negative())
}
Arg::NaN => Folded::NaN,
_ => return None,
},
ExprNode::Asinh(a) => match classify_arg(arena, a) {
Arg::Rational(q) if q.is_zero() => Folded::int(0),
Arg::Infinity => Folded::Infinity,
Arg::NegInfinity => Folded::NegInfinity,
Arg::ImaginaryInfinity(true) => Folded::NegInfinity,
Arg::ImaginaryInfinity(false) => Folded::Infinity,
Arg::ComplexInfinity => Folded::ComplexInfinity,
Arg::NaN => Folded::NaN,
_ => return None,
},
ExprNode::Acosh(a) => match classify_arg(arena, a) {
Arg::Rational(q) if q.is_one() => Folded::int(0),
Arg::Infinity | Arg::NegInfinity | Arg::ImaginaryInfinity(_) => Folded::Infinity,
Arg::ComplexInfinity => Folded::ComplexInfinity,
Arg::NaN => Folded::NaN,
_ => return None,
},
ExprNode::Atanh(a) => match classify_arg(arena, a) {
Arg::Rational(q) if q.is_zero() => Folded::int(0),
Arg::Rational(q) if q.is_one() => Folded::Infinity,
Arg::Rational(q) if (-&q).is_one() => Folded::NegInfinity,
Arg::NaN => Folded::NaN,
_ => return None,
},
ExprNode::Atan2(y, x) => match (classify_arg(arena, y), classify_arg(arena, x)) {
(Arg::NaN, _) | (_, Arg::NaN) => Folded::NaN,
(Arg::Rational(y), Arg::Rational(x)) if y.is_zero() && x.is_zero() => Folded::NaN,
(Arg::Rational(y), Arg::Rational(x)) if y.is_zero() && x.is_positive() => {
Folded::int(0)
}
_ => return None,
},
ExprNode::Gamma(a) => match classify_arg(arena, a) {
Arg::Rational(q) if is_nonpositive_int(&q) => Folded::ComplexInfinity,
Arg::Infinity => Folded::Infinity,
Arg::NaN => Folded::NaN,
_ => return None,
},
ExprNode::LogGamma(a) => match classify_arg(arena, a) {
Arg::Rational(q) if is_nonpositive_int(&q) => Folded::Infinity,
Arg::Rational(q) if q.is_one() || in_units(&q, 1) == Some(2) => Folded::int(0),
Arg::Infinity => Folded::Infinity,
Arg::NaN => Folded::NaN,
_ => return None,
},
ExprNode::Digamma(a) => match classify_arg(arena, a) {
Arg::Rational(q) if is_nonpositive_int(&q) => Folded::ComplexInfinity,
Arg::Infinity => Folded::Infinity,
Arg::NaN => Folded::NaN,
_ => return None,
},
ExprNode::Erf(a) => match classify_arg(arena, a) {
Arg::Rational(q) if q.is_zero() => Folded::int(0),
Arg::Infinity => Folded::int(1),
Arg::NegInfinity => Folded::int(-1),
Arg::ImaginaryInfinity(neg) => Folded::ImaginaryInfinity(neg),
Arg::NaN => Folded::NaN,
_ => return None,
},
ExprNode::Erfc(a) => match classify_arg(arena, a) {
Arg::Rational(q) if q.is_zero() => Folded::int(1),
Arg::Infinity => Folded::int(0),
Arg::NegInfinity => Folded::int(2),
Arg::ImaginaryInfinity(neg) => Folded::ImaginaryInfinity(!neg),
Arg::NaN => Folded::NaN,
_ => return None,
},
ExprNode::LambertW(a) => match classify_arg(arena, a) {
Arg::Rational(q) if q.is_zero() => Folded::int(0),
Arg::E => Folded::int(1),
Arg::Infinity => Folded::Infinity,
Arg::NaN => Folded::NaN,
Arg::Other if is_minus_inverse_e(arena, a) => Folded::int(-1),
_ => return None,
},
ExprNode::Floor(a) => match classify_arg(arena, a) {
Arg::Rational(q) => Folded::Rational(q.floor()),
Arg::Infinity => Folded::Infinity,
Arg::NegInfinity => Folded::NegInfinity,
Arg::NaN => Folded::NaN,
_ => return None,
},
ExprNode::Ceiling(a) => match classify_arg(arena, a) {
Arg::Rational(q) => Folded::Rational(q.ceil()),
Arg::Infinity => Folded::Infinity,
Arg::NegInfinity => Folded::NegInfinity,
Arg::NaN => Folded::NaN,
_ => return None,
},
ExprNode::Heaviside(a) => match classify_arg(arena, a) {
Arg::Rational(q) if q.is_zero() => Folded::ratio(1, 2),
Arg::Rational(q) if q.is_positive() => Folded::int(1),
Arg::Rational(_) | Arg::NegInfinity => Folded::int(0),
Arg::Infinity => Folded::int(1),
Arg::NaN => Folded::NaN,
_ => return None,
},
ExprNode::DiracDelta(a) => match classify_arg(arena, a) {
Arg::Rational(q) if !q.is_zero() => Folded::int(0),
Arg::Infinity | Arg::NegInfinity => Folded::int(0),
Arg::NaN => Folded::NaN,
_ => return None,
},
ExprNode::Abs(a) => match classify_arg(arena, a) {
Arg::Rational(q) => Folded::Rational(q.abs()),
Arg::Infinity | Arg::NegInfinity | Arg::ComplexInfinity => Folded::Infinity,
Arg::NaN => Folded::NaN,
_ => return None,
},
ExprNode::Sign(a) => match classify_arg(arena, a) {
Arg::Rational(q) => Folded::Rational(q.signum()),
Arg::Infinity => Folded::int(1),
Arg::NegInfinity => Folded::int(-1),
Arg::ImaginaryInfinity(neg) => {
Folded::Imaginary(if neg { -Q::one() } else { Q::one() })
}
Arg::NaN => Folded::NaN,
_ => return None,
},
ExprNode::Factorial(a) => match classify_arg(arena, a) {
Arg::Rational(q) if q.is_integer() && q.is_negative() => Folded::ComplexInfinity,
Arg::Infinity => Folded::Infinity,
Arg::NaN => Folded::NaN,
_ => return None,
},
ExprNode::Binomial(n, k) => match (classify_arg(arena, n), classify_arg(arena, k)) {
(Arg::NaN, _) | (_, Arg::NaN) => Folded::NaN,
(Arg::Rational(n), Arg::Rational(k)) if k.is_integer() => {
let vanishes = if n.is_integer() {
!n.is_negative() && (k.is_negative() || k > n)
} else {
k.is_negative()
};
if !vanishes {
return None;
}
Folded::int(0)
}
_ => return None,
},
ExprNode::Beta(a, b) => match (classify_arg(arena, a), classify_arg(arena, b)) {
(Arg::NaN, _) | (_, Arg::NaN) => Folded::NaN,
(Arg::Rational(a), Arg::Rational(b)) if beta_has_pole(&a, &b) => {
Folded::ComplexInfinity
}
_ => return None,
},
ExprNode::Apply(head, ref args) => lib_function_value(arena, head, args)?,
_ => return None,
};
Some(match folded {
Folded::Rational(q) => {
if q.is_zero() {
arena.zero
} else if q.is_one() {
arena.one
} else {
let nid = arena.intern_num(q);
arena.intern(ExprNode::Num(nid))
}
}
Folded::Imaginary(q) => {
let i = arena.i_unit;
if q.is_one() {
i
} else {
let nid = arena.intern_num(q);
let c = arena.intern(ExprNode::Num(nid));
canon_mul(arena, &[c, i])
}
}
Folded::Infinity => arena.infinity,
Folded::NegInfinity => arena.neg_infinity,
Folded::ImaginaryInfinity(neg) => {
let i = arena.i_unit;
let direction = if neg { canon_neg(arena, i) } else { i };
canon_mul(arena, &[direction, arena.infinity])
}
Folded::ComplexInfinity => arena.complex_infinity,
Folded::NaN => arena.nan,
})
}
fn is_positive_constant(arena: &Arena, a: ExprId) -> bool {
matches!(
arena.node(a),
ExprNode::Pi
| ExprNode::E
| ExprNode::EulerGamma
| ExprNode::Catalan
| ExprNode::GoldenRatio
)
}
fn times_i(v: Folded) -> Option<Folded> {
match v {
Folded::Rational(q) if q.is_zero() => Some(Folded::int(0)),
Folded::Rational(q) => Some(Folded::Imaginary(q)),
Folded::ComplexInfinity => Some(Folded::ComplexInfinity),
_ => None,
}
}
fn is_minus_inverse_e(arena: &Arena, id: ExprId) -> bool {
let ExprNode::Mul(children) = arena.node(id) else {
return false;
};
matches!(**children, [c, e] if c == arena.neg_one
&& matches!(arena.node(e), ExprNode::Exp(m) if *m == arena.neg_one))
}
fn beta_has_pole(a: &Q, b: &Q) -> bool {
is_nonpositive_int(a) != is_nonpositive_int(b) && !is_nonpositive_int(&(a + b))
}
fn lib_function_value(
arena: &Arena,
head: crate::base::node::SymbolId,
args: &[ExprId],
) -> Option<Folded> {
use crate::base::libfn::LibFn;
let special = |id: ExprId| {
matches!(
arena.node(id),
ExprNode::Num(_)
| ExprNode::NaN
| ExprNode::Infinity
| ExprNode::NegInfinity
| ExprNode::ComplexInfinity
)
};
if !args.iter().any(|&a| special(a)) {
return None;
}
let f = arena.lib_fn(head)?;
if args.contains(&arena.nan) {
return Some(Folded::NaN);
}
let num = |i: usize| args.get(i).and_then(|&a| arena.as_num(a));
let is = |i: usize, v: i64| num(i).is_some_and(|q| *q == Q::from_integer(BigInt::from(v)));
Some(match f {
LibFn::BesselJ | LibFn::BesselI if is(1, 0) && num(0)?.is_positive() => Folded::int(0),
LibFn::BesselJ | LibFn::BesselI
if is(1, 0) && num(0)?.is_integer() && !num(0)?.is_zero() =>
{
Folded::int(0)
}
LibFn::BesselJ | LibFn::BesselI if is(1, 0) && num(0)?.is_negative() => {
Folded::ComplexInfinity
}
LibFn::BesselY if is(1, 0) && num(0)?.is_zero() => Folded::NegInfinity,
LibFn::BesselK if is(1, 0) && num(0)?.is_zero() => Folded::Infinity,
LibFn::BesselY | LibFn::BesselK if is(1, 0) && num(0).is_some() => Folded::ComplexInfinity,
LibFn::Erfi | LibFn::ErfInv | LibFn::Shi | LibFn::FresnelS | LibFn::FresnelC
if is(0, 0) =>
{
Folded::int(0)
}
LibFn::ErfInv if is(0, 1) => Folded::Infinity,
LibFn::ErfInv if is(0, -1) => Folded::NegInfinity,
LibFn::ErfcInv if is(0, 1) => Folded::int(0),
LibFn::ErfcInv if is(0, 0) => Folded::Infinity,
LibFn::ErfcInv if is(0, 2) => Folded::NegInfinity,
LibFn::Chi if is(0, 0) => Folded::ComplexInfinity,
LibFn::EllipticK if is(0, 1) => Folded::ComplexInfinity,
LibFn::PolyLog if is(1, 0) => Folded::int(0),
_ => return None,
})
}
pub(crate) fn canon_interval(arena: &mut Arena, start: ExprId, end: ExprId, flags: u8) -> ExprId {
use crate::base::node::{INTERVAL_LEFT_OPEN, INTERVAL_RIGHT_OPEN};
tracing::debug!(
"canon_interval: start={:?}, end={:?}, flags={}",
start,
end,
flags
);
if let (Some(s), Some(e)) = (arena.as_num(start), arena.as_num(end)) {
let s = s.clone();
let e = e.clone();
use std::cmp::Ordering;
match s.cmp(&e) {
Ordering::Greater => {
return arena.intern(ExprNode::EmptySet);
}
Ordering::Equal => {
if flags & (INTERVAL_LEFT_OPEN | INTERVAL_RIGHT_OPEN) != 0 {
return arena.intern(ExprNode::EmptySet);
} else {
return arena.intern(ExprNode::FiniteSet(smallvec![start]));
}
}
Ordering::Less => {
}
}
}
arena.intern(ExprNode::Interval(start, end, flags))
}
pub(crate) fn canon_finite_set(arena: &mut Arena, elements: &[ExprId]) -> ExprId {
tracing::debug!("canon_finite_set: {} elements", elements.len());
if elements.is_empty() {
return arena.intern(ExprNode::EmptySet);
}
let mut deduped: SmallVec<[ExprId; 4]> = SmallVec::new();
let mut seen = FxHashSet::default();
for &elem in elements {
if seen.insert(elem) {
deduped.push(elem);
}
}
deduped.sort_by(|&a, &b| arena.sort_key(a).cmp(arena.sort_key(b)));
if deduped.len() == 1 {
}
arena.intern(ExprNode::FiniteSet(deduped))
}
pub(crate) fn canon_set_union(arena: &mut Arena, sets: &[ExprId]) -> ExprId {
tracing::debug!("canon_set_union: {} children", sets.len());
let mut flat: SmallVec<[ExprId; 4]> = SmallVec::new();
let mut stack: SmallVec<[ExprId; 8]> = sets.iter().copied().collect();
while let Some(s) = stack.pop() {
match arena.node(s).clone() {
ExprNode::SetUnion(children) => {
for &child in &children {
stack.push(child);
}
}
ExprNode::EmptySet => {
}
ExprNode::UniversalSet => {
return arena.intern(ExprNode::UniversalSet);
}
_ => {
flat.push(s);
}
}
}
if flat.is_empty() {
return arena.intern(ExprNode::EmptySet);
}
let mut seen = FxHashSet::default();
flat.retain(|id| seen.insert(*id));
flat.sort_by(|&a, &b| arena.sort_key(a).cmp(arena.sort_key(b)));
if flat.len() == 1 {
return flat[0];
}
arena.intern(ExprNode::SetUnion(flat))
}
pub(crate) fn canon_set_intersection(arena: &mut Arena, sets: &[ExprId]) -> ExprId {
tracing::debug!("canon_set_intersection: {} children", sets.len());
let mut flat: SmallVec<[ExprId; 4]> = SmallVec::new();
let mut stack: SmallVec<[ExprId; 8]> = sets.iter().copied().collect();
while let Some(s) = stack.pop() {
match arena.node(s).clone() {
ExprNode::SetIntersection(children) => {
for &child in &children {
stack.push(child);
}
}
ExprNode::UniversalSet => {
}
ExprNode::EmptySet => {
return arena.intern(ExprNode::EmptySet);
}
_ => {
flat.push(s);
}
}
}
if flat.is_empty() {
return arena.intern(ExprNode::UniversalSet);
}
let mut seen = FxHashSet::default();
flat.retain(|id| seen.insert(*id));
flat.sort_by(|&a, &b| arena.sort_key(a).cmp(arena.sort_key(b)));
if flat.len() == 1 {
return flat[0];
}
arena.intern(ExprNode::SetIntersection(flat))
}
#[cfg_attr(not(test), allow(dead_code))]
pub(crate) fn verify_canonical(arena: &mut Arena, id: ExprId) -> Vec<String> {
let mut errors = Vec::new();
let mut visited: FxHashSet<ExprId> = FxHashSet::default();
let mut stack: Vec<ExprId> = vec![id];
while let Some(cur) = stack.pop() {
if !visited.insert(cur) {
continue;
}
verify_node(arena, cur, &mut errors);
arena.node(cur).for_each_child(|c| stack.push(c));
}
errors
}
pub(crate) fn verify_canonical_shallow(arena: &mut Arena, id: ExprId) -> Vec<String> {
let mut errors = Vec::new();
verify_node(arena, id, &mut errors);
errors
}
fn verify_node(arena: &mut Arena, id: ExprId, errors: &mut Vec<String>) {
match arena.node(id).clone() {
ExprNode::Add(ref children) => {
if children.len() < 2 {
errors.push(format!("Add with {} children (need >= 2)", children.len()));
}
for (i, &child) in children.iter().enumerate() {
if child == arena.zero {
errors.push(format!("Add child {i} is zero"));
}
}
for (i, &child) in children.iter().enumerate() {
if matches!(arena.node(child), ExprNode::Add(_)) {
errors.push(format!("Add child {i} is nested Add (not flattened)"));
}
}
for i in 1..children.len() {
let key_prev = arena.sort_key(children[i - 1]);
let key_curr = arena.sort_key(children[i]);
if key_prev > key_curr {
errors.push(format!(
"Add children {}/{} not sorted: {:?} > {:?}",
i - 1,
i,
key_prev,
key_curr
));
}
}
{
let mut seen_keys = FxHashSet::default();
for (idx, &child) in children.iter().enumerate() {
let (_, key) = arena.as_coeff_term(child);
if !seen_keys.insert(key) {
errors.push(format!(
"Add child {idx} has duplicate term key (like terms not merged)"
));
}
}
}
}
ExprNode::Mul(ref children) => {
if children.len() < 2 {
errors.push(format!("Mul with {} children (need >= 2)", children.len()));
}
for (i, &child) in children.iter().enumerate() {
if child == arena.one {
errors.push(format!("Mul child {i} is one"));
}
}
for (i, &child) in children.iter().enumerate() {
if matches!(arena.node(child), ExprNode::Mul(_)) {
errors.push(format!("Mul child {i} is nested Mul (not flattened)"));
}
}
for i in 1..children.len() {
let key_prev = arena.sort_key(children[i - 1]);
let key_curr = arena.sort_key(children[i]);
if key_prev > key_curr {
errors.push(format!("Mul children {}/{} not sorted", i - 1, i));
}
}
let num_count = children
.iter()
.filter(|&&c| matches!(arena.node(c), ExprNode::Num(_)))
.count();
if num_count > 1 {
errors.push(format!(
"Mul has {num_count} Num children (should be at most 1)"
));
}
}
ExprNode::Neg(inner) => {
if matches!(arena.node(inner), ExprNode::Neg(_)) {
errors.push("Double negation Neg(Neg(...))".to_string());
}
if inner == arena.zero {
errors.push("Neg(0) should be 0".to_string());
}
if matches!(arena.node(inner), ExprNode::Num(_)) {
errors.push("Neg(Num) should be folded into negative Num".to_string());
}
}
ExprNode::Pow(base, exp) => {
if exp == arena.zero {
errors.push("Pow(x, 0) should be 1".to_string());
}
if exp == arena.one {
errors.push("Pow(x, 1) should be x".to_string());
}
if base == arena.one {
errors.push("Pow(1, x) should be 1".to_string());
}
}
ExprNode::And(ref children) | ExprNode::Or(ref children) => {
let label = if matches!(arena.node(id), ExprNode::And(_)) {
"And"
} else {
"Or"
};
if children.len() < 2 {
errors.push(format!(
"{} with {} children (need >= 2)",
label,
children.len()
));
}
for i in 1..children.len() {
let key_prev = arena.sort_key(children[i - 1]);
let key_curr = arena.sort_key(children[i]);
if key_prev > key_curr {
errors.push(format!(
"{} children {}/{} not sorted: {:?} > {:?}",
label,
i - 1,
i,
key_prev,
key_curr
));
}
}
}
_ => {}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::base::arena::Arena;
fn s(arena: &mut Arena, name: &str) -> ExprId {
arena.symbol(name)
}
fn display(arena: &Arena, id: ExprId) -> String {
arena.display(id).to_string()
}
#[test]
fn add_combines_like_terms() {
let mut a = Arena::new();
let x = s(&mut a, "x");
let two = a.int(2);
let two_x = a.mul(&[two, x]);
let three = a.int(3);
let three_x = a.mul(&[three, x]);
let result = a.add(&[two_x, three_x]);
assert_eq!(display(&a, result), "5*x");
}
#[test]
fn add_combines_identical_symbols() {
let mut a = Arena::new();
let x = s(&mut a, "x");
let result = a.add(&[x, x]);
assert_eq!(display(&a, result), "2*x");
}
#[test]
fn add_flattens_nested_add() {
let mut a = Arena::new();
let x = s(&mut a, "x");
let y = s(&mut a, "y");
let z = s(&mut a, "z");
let inner = a.intern(ExprNode::Add(smallvec![x, y]));
let result = a.add(&[inner, z]);
assert_eq!(display(&a, result), "x + y + z");
}
#[test]
fn add_drops_zeros() {
let mut a = Arena::new();
let x = s(&mut a, "x");
let zero = a.zero;
let result = a.add(&[x, zero]);
assert_eq!(result, x);
}
#[test]
fn add_evaluates_numeric_sum() {
let mut a = Arena::new();
let two = a.int(2);
let three = a.int(3);
let result = a.add(&[two, three]);
assert_eq!(display(&a, result), "5");
}
#[test]
fn add_nan_propagates() {
let mut a = Arena::new();
let x = s(&mut a, "x");
let nan = a.nan;
let result = a.add(&[x, nan]);
assert_eq!(result, a.nan);
}
#[test]
fn add_cancellation_to_zero() {
let mut a = Arena::new();
let x = s(&mut a, "x");
let neg_x = a.neg(x);
let result = a.add(&[x, neg_x]);
assert_eq!(result, a.zero);
}
#[test]
fn add_numeric_and_symbolic() {
let mut a = Arena::new();
let x = s(&mut a, "x");
let three = a.int(3);
let two = a.int(2);
let two_x = a.mul(&[two, x]);
let result = a.add(&[x, two_x, three]);
assert_eq!(display(&a, result), "3*x + 3");
}
#[test]
fn add_multiple_like_terms() {
let mut a = Arena::new();
let x = s(&mut a, "x");
let ab = {
let aa = s(&mut a, "a");
let bb = s(&mut a, "b");
a.mul(&[aa, bb])
};
let e1 = ab;
let two = a.int(2);
let e2 = a.mul(&[two, ab]);
let five = a.int(5);
let e3 = a.mul(&[five, ab]);
let result = a.add(&[e1, e2, e3, x]);
assert_eq!(display(&a, result), "8*a*b + x");
}
#[test]
fn add_oo_minus_oo_is_nan() {
let mut a = Arena::new();
let result = a.add(&[a.infinity, a.neg_infinity]);
assert_eq!(result, a.nan);
}
#[test]
fn add_oo_plus_finite_is_oo() {
let mut a = Arena::new();
let x = s(&mut a, "x");
let result = a.add(&[a.infinity, x]);
assert_eq!(result, a.infinity);
}
#[test]
fn mul_combines_like_bases() {
let mut a = Arena::new();
let x = s(&mut a, "x");
let result = a.mul(&[x, x]);
assert_eq!(display(&a, result), "x^2");
}
#[test]
fn mul_combines_powers() {
let mut a = Arena::new();
let x = s(&mut a, "x");
let two = a.int(2);
let x2 = a.pow(x, two);
let three = a.int(3);
let x3 = a.pow(x, three);
let result = a.mul(&[x2, x3]);
assert_eq!(display(&a, result), "x^5");
}
#[test]
fn mul_flattens_nested_mul() {
let mut a = Arena::new();
let x = s(&mut a, "x");
let y = s(&mut a, "y");
let z = s(&mut a, "z");
let inner = a.intern(ExprNode::Mul(smallvec![x, y]));
let result = a.mul(&[inner, z]);
assert_eq!(display(&a, result), "x*y*z");
}
#[test]
fn mul_collects_numeric_coefficient() {
let mut a = Arena::new();
let x = s(&mut a, "x");
let two = a.int(2);
let three = a.int(3);
let result = a.mul(&[two, x, three]);
assert_eq!(display(&a, result), "6*x");
}
#[test]
fn mul_by_zero() {
let mut a = Arena::new();
let x = s(&mut a, "x");
let zero = a.zero;
let result = a.mul(&[x, zero]);
assert_eq!(result, a.zero);
}
#[test]
fn mul_by_one() {
let mut a = Arena::new();
let x = s(&mut a, "x");
let one = a.one;
let result = a.mul(&[one, x]);
assert_eq!(result, x);
}
#[test]
fn mul_nan_propagates() {
let mut a = Arena::new();
let x = s(&mut a, "x");
let nan = a.nan;
let result = a.mul(&[x, nan]);
assert_eq!(result, a.nan);
}
#[test]
fn mul_zero_times_infinity_is_nan() {
let mut a = Arena::new();
let result = a.mul(&[a.zero, a.infinity]);
assert_eq!(result, a.nan);
}
#[test]
fn mul_rational_coefficients() {
let mut a = Arena::new();
let r1 = a.rational(2, 3);
let r2 = a.rational(3, 4);
let result = a.mul(&[r1, r2]);
assert_eq!(display(&a, result), "1/2");
}
#[test]
fn mul_neg_neg_is_positive() {
let mut a = Arena::new();
let neg1 = a.int(-1);
let result = a.mul(&[neg1, neg1]);
assert_eq!(display(&a, result), "1");
}
#[test]
fn pow_x_zero_is_one() {
let mut a = Arena::new();
let x = s(&mut a, "x");
let result = a.pow(x, a.zero);
assert_eq!(result, a.one);
}
#[test]
fn pow_x_one_is_x() {
let mut a = Arena::new();
let x = s(&mut a, "x");
let result = a.pow(x, a.one);
assert_eq!(result, x);
}
#[test]
fn pow_one_x_is_one() {
let mut a = Arena::new();
let x = s(&mut a, "x");
let result = a.pow(a.one, x);
assert_eq!(result, a.one);
}
#[test]
fn pow_zero_positive_is_zero() {
let mut a = Arena::new();
let base = a.zero;
let exp = a.int(5);
let result = a.pow(base, exp);
assert_eq!(result, a.zero);
}
#[test]
fn pow_evaluates_numeric() {
let mut a = Arena::new();
let base = a.int(2);
let exp = a.int(10);
let result = a.pow(base, exp);
assert_eq!(display(&a, result), "1024");
}
#[test]
fn pow_evaluates_rational() {
let mut a = Arena::new();
let half = a.rational(1, 2);
let exp = a.int(3);
let result = a.pow(half, exp);
assert_eq!(display(&a, result), "1/8");
}
#[test]
fn pow_evaluates_negative_exponent() {
let mut a = Arena::new();
let base = a.int(2);
let exp = a.int(-3);
let result = a.pow(base, exp);
assert_eq!(display(&a, result), "1/8");
}
#[test]
fn pow_nan_propagates_base() {
let mut a = Arena::new();
let base = a.nan;
let exp = a.int(2);
let result = a.pow(base, exp);
assert_eq!(result, a.nan);
}
#[test]
fn pow_nan_propagates_exp() {
let mut a = Arena::new();
let x = s(&mut a, "x");
let result = a.pow(x, a.nan);
assert_eq!(result, a.nan);
}
#[test]
fn pow_perfect_square_simplifies() {
let mut a = Arena::new();
let base = a.int(4);
let exp = a.rational(1, 2);
let result = a.pow(base, exp);
assert_eq!(display(&a, result), "2");
}
#[test]
fn pow_huge_exponent_stays_unevaluated() {
let mut a = Arena::new();
let base = a.int(2);
let exp = a.int(5000);
let result = a.pow(base, exp);
assert_eq!(display(&a, result), "2^5000");
}
#[test]
fn neg_double_negation() {
let mut a = Arena::new();
let x = s(&mut a, "x");
let neg_x = a.neg(x);
let neg_neg_x = a.neg(neg_x);
assert_eq!(neg_neg_x, x);
}
#[test]
fn neg_numeric() {
let mut a = Arena::new();
let three = a.int(3);
let result = a.neg(three);
assert_eq!(display(&a, result), "-3");
}
#[test]
fn neg_distributes_over_add() {
let mut a = Arena::new();
let x = s(&mut a, "x");
let y = s(&mut a, "y");
let sum = a.add(&[x, y]);
let result = a.neg(sum);
assert_eq!(display(&a, result), "-x - y");
}
#[test]
fn neg_of_mul() {
let mut a = Arena::new();
let x = s(&mut a, "x");
let two = a.int(2);
let two_x = a.mul(&[two, x]);
let result = a.neg(two_x);
assert_eq!(display(&a, result), "-2*x");
}
#[test]
fn neg_of_symbol() {
let mut a = Arena::new();
let x = s(&mut a, "x");
let result = a.neg(x);
assert_eq!(display(&a, result), "-x");
}
#[test]
fn mixed_a_times_b_plus_b_times_a() {
let mut a = Arena::new();
let x = s(&mut a, "x");
let y = s(&mut a, "y");
let xy = a.mul(&[x, y]);
let yx = a.mul(&[y, x]);
let result = a.add(&[xy, yx]);
assert_eq!(display(&a, result), "2*x*y");
}
#[test]
fn mixed_a_minus_a_is_zero() {
let mut a = Arena::new();
let x = s(&mut a, "x");
let result = a.sub(x, x);
assert_eq!(result, a.zero);
}
#[test]
fn mixed_division() {
let mut a = Arena::new();
let x = s(&mut a, "x");
let y = s(&mut a, "y");
let result = a.div(x, y);
assert_eq!(display(&a, result), "x/y");
}
#[test]
fn mixed_x_plus_2x_plus_3() {
let mut a = Arena::new();
let x = s(&mut a, "x");
let two = a.int(2);
let two_x = a.mul(&[two, x]);
let three = a.int(3);
let result = a.add(&[x, two_x, three]);
assert_eq!(display(&a, result), "3*x + 3");
}
#[test]
fn mixed_polynomial_canonical_form() {
let mut a = Arena::new();
let x = s(&mut a, "x");
let exp2 = a.int(2);
let x2 = a.pow(x, exp2);
let two = a.int(2);
let two_x = a.mul(&[two, x]);
let one = a.int(1);
let result = a.add(&[x2, two_x, one]);
assert_eq!(display(&a, result), "x^2 + 2*x + 1");
}
#[test]
fn mixed_compound_collection() {
let mut a_arena = Arena::new();
let aa = s(&mut a_arena, "a");
let bb = s(&mut a_arena, "b");
let ab = a_arena.mul(&[aa, bb]);
let ba = a_arena.mul(&[bb, aa]);
let result = a_arena.add(&[ab, ba, ab]);
assert_eq!(display(&a_arena, result), "3*a*b");
}
#[test]
fn mixed_subtract_to_zero() {
let mut a = Arena::new();
let aa = s(&mut a, "a");
let bb = s(&mut a, "b");
let ba = a.mul(&[bb, aa]);
let neg_b = a.neg(bb);
let ab = a.mul(&[aa, bb]);
let neg_ab = a.neg(ab);
let result = a.add(&[ba, neg_b, neg_ab, bb]);
assert_eq!(result, a.zero);
}
#[test]
fn mixed_rational_arithmetic_in_add() {
let mut a = Arena::new();
let sym_a = s(&mut a, "a");
let two = a.int(2);
let five = a.int(5);
let result = a.add(&[two, sym_a, five]);
assert_eq!(display(&a, result), "a + 7");
}
#[test]
fn mixed_mul_abc_times_2() {
let mut a = Arena::new();
let aa = s(&mut a, "a");
let bb = s(&mut a, "b");
let cc = s(&mut a, "c");
let two = a.int(2);
let result = a.mul(&[aa, bb, cc, two]);
assert_eq!(display(&a, result), "2*a*b*c");
}
#[test]
fn dedup_after_canonicalization() {
let mut a = Arena::new();
let x = s(&mut a, "x");
let e1 = a.add(&[x, x]); let two = a.int(2);
let e2 = a.mul(&[two, x]); assert_eq!(e1, e2, "canonical forms should hash-cons to same ExprId");
}
#[test]
fn no_auto_expand_pow() {
let mut a = Arena::new();
let x = s(&mut a, "x");
let sum = a.add(&[x, a.one]);
let exp = a.int(2);
let result = a.pow(sum, exp);
assert_eq!(display(&a, result), "(x + 1)^2");
}
#[test]
fn no_auto_distribute_mul_over_add() {
let mut a = Arena::new();
let x = s(&mut a, "x");
let y = s(&mut a, "y");
let z = s(&mut a, "z");
let sum = a.add(&[y, z]);
let result = a.mul(&[x, sum]);
assert_eq!(display(&a, result), "x*(y + z)");
}
#[test]
fn constant_applications_with_rational_or_singular_values_fold() {
let mut a = Arena::new();
let r = a.sin(a.zero);
assert_eq!(r, a.zero);
let r = a.cos(a.pi);
assert_eq!(r, a.neg_one);
let r = a.ln(a.zero);
assert_eq!(r, a.complex_infinity);
let half = a.rational(1, 2);
let half_pi = a.mul(&[half, a.pi]);
let r = a.tan(half_pi);
assert_eq!(r, a.complex_infinity);
let r = a.gamma(a.zero);
assert_eq!(r, a.complex_infinity);
let s = a.sin(a.zero);
let inv = a.pow(s, a.neg_one);
let q = a.mul(&[s, inv]);
assert_eq!(q, a.nan, "sin(0)/sin(0) is 0/0");
}
#[test]
fn irrational_special_values_stay_for_eval() {
let mut a = Arena::new();
let quarter = a.rational(1, 4);
let arg = a.mul(&[quarter, a.pi]);
let result = a.sin(arg);
assert_eq!(display(&a, result), "sin(1/4*pi)");
}
#[test]
fn no_auto_cancel_fraction() {
let mut a = Arena::new();
let x = s(&mut a, "x");
let exp = a.int(2);
let x2 = a.pow(x, exp);
let numer = a.sub(x2, a.one);
let denom = a.sub(x, a.one);
let result = a.div(numer, denom);
let d = display(&a, result);
assert!(!d.contains("x + 1"), "should not auto-cancel: got '{d}'");
}
#[test]
fn scaling_100_term_sum() {
let mut a = Arena::new();
let symbols: Vec<ExprId> = (0..100).map(|i| s(&mut a, &format!("x{i}"))).collect();
let result = a.add(&symbols);
if let ExprNode::Add(args) = a.node(result) {
assert_eq!(args.len(), 100);
} else {
panic!("expected Add with 100 terms");
}
}
#[test]
fn scaling_1000_term_sum() {
let mut a = Arena::new();
let symbols: Vec<ExprId> = (0..1000).map(|i| s(&mut a, &format!("x{i}"))).collect();
let start = std::time::Instant::now();
let result = a.add(&symbols);
let elapsed = start.elapsed();
assert!(
elapsed.as_millis() < 500,
"1000-term sum took {elapsed:?}, expected < 500ms"
);
if let ExprNode::Add(args) = a.node(result) {
assert_eq!(args.len(), 1000);
}
}
#[test]
fn scaling_like_term_collection() {
let mut a = Arena::new();
let x = s(&mut a, "x");
let terms: Vec<ExprId> = (1..=100)
.map(|i| {
let coeff = a.int(i);
a.mul(&[coeff, x])
})
.collect();
let result = a.add(&terms);
assert_eq!(display(&a, result), "5050*x");
}
#[test]
fn i_squared_is_neg_one() {
let mut a = Arena::new();
let i = a.i_unit;
let two = a.int(2);
let result = a.pow(i, two);
assert_eq!(result, a.neg_one);
}
#[test]
fn i_cubed_is_neg_i() {
let mut a = Arena::new();
let i = a.i_unit;
let three = a.int(3);
let result = a.pow(i, three);
assert_eq!(display(&a, result), "-I");
}
#[test]
fn i_fourth_is_one() {
let mut a = Arena::new();
let i = a.i_unit;
let four = a.int(4);
let result = a.pow(i, four);
assert_eq!(result, a.one);
}
#[test]
fn i_to_neg_one() {
let mut a = Arena::new();
let i = a.i_unit;
let neg1 = a.int(-1);
let result = a.pow(i, neg1);
assert_eq!(display(&a, result), "-I");
}
#[test]
fn i_to_neg_two() {
let mut a = Arena::new();
let i = a.i_unit;
let neg2 = a.int(-2);
let result = a.pow(i, neg2);
assert_eq!(result, a.neg_one);
}
#[test]
fn i_to_100() {
let mut a = Arena::new();
let i = a.i_unit;
let hundred = a.int(100);
let result = a.pow(i, hundred);
assert_eq!(result, a.one);
}
#[test]
fn one_plus_i_squared() {
let mut a = Arena::new();
let one = a.one;
let i = a.i_unit;
let two = a.int(2);
let term1 = a.mul(&[one, one]); let term2 = a.mul(&[one, i]); let term3 = a.mul(&[i, one]); let term4 = a.pow(i, two); let result = a.add(&[term1, term2, term3, term4]);
assert_eq!(display(&a, result), "2*I");
}
#[test]
fn neg_one_to_half_is_i() {
let mut a = Arena::new();
let half = a.rational(1, 2);
let result = a.pow(a.neg_one, half);
assert_eq!(result, a.i_unit);
}
#[test]
fn neg_one_to_three_halves_is_neg_i() {
let mut a = Arena::new();
let three_halves = a.rational(3, 2);
let result = a.pow(a.neg_one, three_halves);
assert_eq!(display(&a, result), "-I");
}
#[test]
fn sqrt_neg_one_is_i() {
let mut a = Arena::new();
let half = a.rational(1, 2);
let result = a.pow(a.neg_one, half);
assert_eq!(result, a.i_unit);
}
#[test]
fn neg_one_to_one_is_neg_one() {
let mut a = Arena::new();
let one = a.one;
let result = a.pow(a.neg_one, one);
assert_eq!(result, a.neg_one);
}
#[test]
fn sqrt_neg_4_is_2i() {
let mut a = Arena::new();
let neg4 = a.int(-4);
let half = a.rational(1, 2);
let result = a.pow(neg4, half);
assert_eq!(display(&a, result), "2*I");
}
#[test]
fn sqrt_neg_2_is_i_sqrt_2() {
let mut a = Arena::new();
let neg2 = a.int(-2);
let half = a.rational(1, 2);
let result = a.pow(neg2, half);
let s = display(&a, result);
assert!(s.contains("I"), "expected I in {s}");
assert!(s.contains("sqrt(2)"), "expected sqrt(2) in {s}");
}
#[test]
fn sqrt_neg_9_is_3i() {
let mut a = Arena::new();
let neg9 = a.int(-9);
let half = a.rational(1, 2);
let result = a.pow(neg9, half);
assert_eq!(display(&a, result), "3*I");
}
#[test]
fn verify_canonical_add_is_clean() {
let mut a = Arena::new();
let x = s(&mut a, "x");
let y = s(&mut a, "y");
let expr = a.add(&[x, y]);
let errors = verify_canonical(&mut a, expr);
assert!(errors.is_empty(), "errors: {:?}", errors);
}
#[test]
fn verify_canonical_mul_is_clean() {
let mut a = Arena::new();
let x = s(&mut a, "x");
let two = a.int(2);
let expr = a.mul(&[two, x]);
let errors = verify_canonical(&mut a, expr);
assert!(errors.is_empty(), "errors: {:?}", errors);
}
#[test]
fn verify_canonical_complex_expr() {
let mut a = Arena::new();
let x = s(&mut a, "x");
let y = s(&mut a, "y");
let two = a.int(2);
let x2 = a.pow(x, two);
let xy = a.mul(&[x, y]);
let expr = a.add(&[x2, xy, y]);
let errors = verify_canonical(&mut a, expr);
assert!(errors.is_empty(), "errors: {:?}", errors);
}
#[test]
fn sqrt_12_simplifies() {
let mut a = Arena::new();
let twelve = a.int(12);
let half = a.rational(1, 2);
let result = a.pow(twelve, half);
assert_eq!(display(&a, result), "2*sqrt(3)");
}
#[test]
fn sqrt_8_simplifies() {
let mut a = Arena::new();
let base = a.int(8);
let exp = a.rational(1, 2);
let result = a.pow(base, exp);
assert_eq!(display(&a, result), "2*sqrt(2)");
}
#[test]
fn sqrt_7_stays() {
let mut a = Arena::new();
let base = a.int(7);
let exp = a.rational(1, 2);
let result = a.pow(base, exp);
assert_eq!(display(&a, result), "sqrt(7)");
}
#[test]
fn sqrt_18_simplifies() {
let mut a = Arena::new();
let base = a.int(18);
let exp = a.rational(1, 2);
let result = a.pow(base, exp);
assert_eq!(display(&a, result), "3*sqrt(2)");
}
#[test]
fn sqrt_9_simplifies_to_3() {
let mut a = Arena::new();
let base = a.int(9);
let exp = a.rational(1, 2);
let result = a.pow(base, exp);
assert_eq!(display(&a, result), "3");
}
#[test]
fn cbrt_24_simplifies() {
let mut a = Arena::new();
let base = a.int(24);
let exp = a.rational(1, 3);
let result = a.pow(base, exp);
assert_eq!(display(&a, result), "2*cbrt(3)");
}
#[test]
fn cbrt_8_simplifies_to_2() {
let mut a = Arena::new();
let base = a.int(8);
let exp = a.rational(1, 3);
let result = a.pow(base, exp);
assert_eq!(display(&a, result), "2");
}
#[test]
fn split_perfect_power_basic() {
let sp = |n: i64, k: u32| {
let (o, i) = split_perfect_power(&BigInt::from(n), k);
(o.try_into().unwrap_or(-1i64), i.try_into().unwrap_or(-1i64))
};
assert_eq!(sp(12, 2), (2, 3));
assert_eq!(sp(8, 2), (2, 2));
assert_eq!(sp(18, 2), (3, 2));
assert_eq!(sp(9, 2), (3, 1));
assert_eq!(sp(7, 2), (1, 7));
assert_eq!(sp(8, 3), (2, 1));
assert_eq!(sp(24, 3), (2, 3));
assert_eq!(sp(1, 2), (1, 1));
assert_eq!(sp(3_912_608_155_036_063, 2), (23_641_997, 7));
}
#[test]
fn split_perfect_power_leaves_large_composite_alone() {
let n = BigInt::parse_bytes(b"40000000000000002239200000000000000867204", 10).unwrap();
let (o, i) = split_perfect_power(&n, 2);
assert_eq!(o, BigInt::from(2));
assert_eq!(&i * 4, n);
let p = BigInt::parse_bytes(b"10000000000000000051", 10).unwrap();
let (o, i) = split_perfect_power(&(&p * &p * 3), 2);
assert_eq!(o, p);
assert_eq!(i, BigInt::from(3));
}
fn split_perfect_power_by_factorint(
n: &BigInt,
k: u32,
(factors, cofactor): &(Vec<(BigInt, u32)>, BigInt),
) -> (BigInt, BigInt) {
let root = n.nth_root(k);
if NumPow::pow(root.clone(), k) == *n {
return (root, BigInt::one());
}
let mut outside = BigInt::one();
let mut inside = cofactor.clone();
for (p, e) in factors {
if *e >= k {
outside *= NumPow::pow(p.clone(), e / k);
}
if e % k > 0 {
inside *= NumPow::pow(p.clone(), e % k);
}
}
(outside, inside)
}
#[test]
fn split_perfect_power_matches_the_factorint_split() {
use crate::domains::ntheory::nextprime;
let mut state = 0x2545_F491_4F6C_DD1Du64;
let mut prime = |bits: u32| {
state ^= state << 13;
state ^= state >> 7;
state ^= state << 17;
let offset = BigInt::from(state) % (BigInt::one() << (bits - 2));
nextprime((BigInt::one() << (bits - 1)) + offset)
};
let mut inputs: Vec<BigInt> = Vec::new();
for _ in 0..4 {
let (a, b, c) = (prime(17), prime(24), prime(33));
let (d, e) = (prime(40), prime(44));
let big = prime(46) * prime(47); inputs.extend([
&a * &b,
&a * &a * &b,
&a * &b * &b * 12,
NumPow::pow(&a, 3u32) * &b,
&a * &b * &c,
&c * &c * &a * 45,
(&a * &b).pow(2u32) * &c,
&d * &e,
&d * &e * 65_521,
&c * &d,
&c * &c * 7,
&big * 3,
&big * &big * 3,
NumPow::pow(&big, 3u32) * 4,
&c * &c * &big,
prime(90),
prime(90) * &a * &a,
]);
}
for n in &inputs {
let factored = crate::domains::ntheory::factorint_bounded(n, RADICAL_FACTOR_MAX_BITS);
for k in [2, 3, 4, 6] {
assert_eq!(
split_perfect_power(n, k),
split_perfect_power_by_factorint(n, k, &factored),
"n = {n}, k = {k}"
);
}
}
}
}