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, q_add, q_mul};
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 = q_add(na, nb);
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 merged: SmallVec<[ExprId; 4]> = SmallVec::new();
let mut collect = |terms: &mut FxHashMap<ExprId, Q>, key: ExprId, c: Q| match terms.entry(key) {
std::collections::hash_map::Entry::Occupied(mut slot) => {
let sum = q_add(slot.get(), &c);
*slot.get_mut() = sum;
merged.push(key);
}
std::collections::hash_map::Entry::Vacant(slot) => {
slot.insert(c);
}
};
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 = q_add(&constant, arena.num(nid));
}
ExprNode::Neg(inner) => {
let (c, key) = arena.as_coeff_term(inner);
let combined = -c;
if key == arena.one {
constant = q_add(&constant, &combined);
} else {
collect(&mut terms, key, combined);
}
}
_ => {
let (c, key) = arena.as_coeff_term(id);
if key == arena.one {
constant = q_add(&constant, &c);
} else {
collect(&mut terms, key, c);
}
}
}
}
if (has_pos_inf || has_neg_inf || has_zoo) && has_identically_infinite_term(arena, &terms) {
tracing::debug!("canon_add: infinity plus a term over an identically zero sum → nan");
return arena.nan;
}
if !merged.is_empty() && cancels_infinite_terms(arena, &terms, &merged) {
tracing::debug!("canon_add: like infinite terms cancel → nan");
return arena.nan;
}
if terms.keys().any(|&k| is_directed_infinity(arena, k)) {
let infinities = Infinities {
pos: has_pos_inf,
neg: has_neg_inf,
zoo: has_zoo,
};
return add_directed_infinities(arena, &terms, &constant, infinities);
}
if has_zoo {
return arena.complex_infinity;
}
if has_pos_inf || has_neg_inf {
let inf = if has_pos_inf {
arena.infinity
} else {
arena.neg_infinity
};
return infinity_plus_terms(arena, &terms, &constant, &[inf], true);
}
if !merged.is_empty() && merges_undefined_terms(arena, &merged) {
tracing::debug!("canon_add: like terms undefined at every point collected → nan");
return arena.nan;
}
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 merges_undefined_terms(arena: &mut Arena, keys: &[ExprId]) -> bool {
if vanishing_check_active() {
return false;
}
let mut seen: SmallVec<[ExprId; 4]> = SmallVec::new();
let mut memo = EverywhereMemo::default();
for &key in keys {
if seen.contains(&key) {
continue;
}
seen.push(key);
if term_infinite_or_undefined(arena, &mut memo, key) {
return true;
}
}
false
}
fn term_infinite_or_undefined(arena: &mut Arena, memo: &mut EverywhereMemo, t: ExprId) -> bool {
let factors: SmallVec<[ExprId; 6]> = match arena.node(t) {
ExprNode::Mul(children) => children.clone(),
_ => smallvec![t],
};
factors.into_iter().any(|f| match *arena.node(f) {
ExprNode::Pow(base, e)
if !arena.node(base).is_atom() && arena.as_num(e).is_some_and(Signed::is_negative) =>
{
memo.zero_or_undefined(arena, base)
}
ExprNode::Pow(base, e)
if matches!(arena.node(base), ExprNode::Add(_))
&& arena.as_num(e).is_some_and(Signed::is_positive) =>
{
memo.infinite_or_undefined(arena, base)
}
ExprNode::Add(_) => memo.infinite_or_undefined(arena, f),
_ => false,
})
}
fn has_identically_infinite_term(arena: &mut Arena, terms: &FxHashMap<ExprId, Q>) -> bool {
let keys: SmallVec<[ExprId; 6]> = terms
.keys()
.copied()
.filter(|&t| !is_directed_infinity(arena, t))
.collect();
let mut memo = EverywhereMemo::default();
keys.into_iter()
.any(|t| term_infinite_or_undefined(arena, &mut memo, t))
}
#[derive(Clone, Copy)]
struct Infinities {
pos: bool,
neg: bool,
zoo: bool,
}
fn add_directed_infinities(
arena: &mut Arena,
terms: &FxHashMap<ExprId, Q>,
constant: &Q,
infinities: Infinities,
) -> ExprId {
if infinities.zoo {
return arena.nan;
}
let mut out: SmallVec<[ExprId; 6]> = SmallVec::new();
let mut real_direction = infinities.pos || infinities.neg;
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;
}
real_direction |= has_real_direction(arena, key);
out.push(arena.make_coeff_term(c.clone(), key));
}
if infinities.pos {
out.push(arena.infinity);
}
if infinities.neg {
out.push(arena.neg_infinity);
}
infinity_plus_terms(arena, terms, constant, &out, real_direction)
}
fn infinity_plus_terms(
arena: &mut Arena,
terms: &FxHashMap<ExprId, Q>,
constant: &Q,
infinite: &[ExprId],
absorb_real: bool,
) -> ExprId {
use crate::base::assumptions::{AssumptionCache, Props};
let mut out: SmallVec<[ExprId; 6]> = SmallVec::from_slice(infinite);
if !absorb_real && !constant.is_zero() {
let nid = arena.intern_num(constant.clone());
out.push(arena.intern(ExprNode::Num(nid)));
}
let mut cache = AssumptionCache::new();
for (&key, c) in terms {
if c.is_zero() || is_directed_infinity(arena, key) {
continue;
}
if absorb_real && cache.query(arena, key, Props::REAL) == Some(true) {
continue;
}
out.push(arena.make_coeff_term(c.clone(), key));
}
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 with an infinite term: {:?}",
verify_canonical_shallow(arena, result)
);
result
}
fn has_real_direction(arena: &Arena, key: ExprId) -> bool {
use crate::base::assumptions::{AssumptionCache, Props};
let ExprNode::Mul(children) = arena.node(key) else {
return false;
};
let mut cache = AssumptionCache::new();
children.iter().all(|&c| {
c == arena.infinity
|| arena.as_num(c).is_some()
|| cache.query(arena, c, Props::REAL) == Some(true)
})
}
pub(crate) fn is_infinite_sum(arena: &Arena, id: ExprId) -> bool {
let ExprNode::Add(children) = arena.node(id) else {
return false;
};
let mut infinite = 0usize;
for &c in children.iter() {
match arena.node(c) {
ExprNode::Infinity | ExprNode::NegInfinity => infinite += 1,
ExprNode::ComplexInfinity | ExprNode::NaN => return false,
_ if is_directed_infinity(arena, c) => infinite += 1,
_ => {}
}
}
infinite == 1
}
fn cancels_infinite_terms(arena: &Arena, terms: &FxHashMap<ExprId, Q>, merged: &[ExprId]) -> bool {
merged.iter().any(|key| {
terms.get(key).is_some_and(Zero::is_zero) && {
let factors: &[ExprId] = match arena.node(*key) {
ExprNode::Mul(children) => children,
_ => std::slice::from_ref(key),
};
factors.iter().any(|&f| match *arena.node(f) {
ExprNode::Pow(b, e) => {
arena.as_num(e).is_some_and(Signed::is_positive) && is_infinite_sum(arena, b)
}
_ => is_infinite_sum(arena, f),
})
}
})
}
fn zero_times_rest<'a>(
arena: &mut Arena,
saw_infinity: bool,
remaining: impl IntoIterator<Item = &'a ExprId>,
denominators: impl IntoIterator<Item = ExprId>,
) -> ExprId {
if saw_infinity {
return arena.nan;
}
let mut denominators: SmallVec<[ExprId; 4]> = denominators.into_iter().collect();
let mut applications: SmallVec<[ExprId; 4]> = SmallVec::new();
let mut stack: SmallVec<[(ExprId, bool); 16]> =
remaining.into_iter().map(|&id| (id, true)).collect();
stack.extend(denominators.iter().map(|&d| (d, false)));
while let Some((id, outside)) = 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(children.iter().map(|&c| (c, outside)));
}
ExprNode::Neg(inner) => stack.push((*inner, outside)),
ExprNode::Pow(base, e)
if outside && arena.as_num(*e).is_some_and(Signed::is_negative) =>
{
denominators.push(*base);
}
ExprNode::Pow(base, e) if outside && arena.as_num(*e).is_some() => {
stack.push((*base, outside));
}
node if outside && application_arguments(arena, node).is_some() => {
applications.push(id);
}
_ => {}
}
}
let mut memo = EverywhereMemo::default();
for d in denominators {
if memo.zero_or_undefined(arena, d) {
tracing::debug!("canon_mul: 0 over an identically zero or undefined denominator → nan");
return arena.nan;
}
}
for f in applications {
if memo.infinite_or_undefined(arena, f) {
tracing::debug!("canon_mul: 0 times a function undefined at every point → nan");
return arena.nan;
}
}
arena.zero
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum Everywhere {
Zero,
Infinite,
Undefined,
Unknown,
}
#[derive(Default)]
struct EverywhereMemo {
value: FxHashMap<ExprId, Everywhere>,
vanishes: FxHashMap<ExprId, bool>,
}
impl EverywhereMemo {
fn vanishes(&mut self, arena: &mut Arena, f: ExprId) -> bool {
if let Some(&known) = self.vanishes.get(&f) {
return known;
}
let zero = !arena.node(f).is_atom() && factor_vanishes(arena, f);
self.vanishes.insert(f, zero);
zero
}
fn zero_or_undefined(&mut self, arena: &mut Arena, d: ExprId) -> bool {
match everywhere(arena, d, self) {
Everywhere::Zero | Everywhere::Undefined => true,
Everywhere::Infinite => false,
Everywhere::Unknown => self.vanishes(arena, d),
}
}
fn infinite_or_undefined(&mut self, arena: &mut Arena, f: ExprId) -> bool {
matches!(
everywhere(arena, f, self),
Everywhere::Infinite | Everywhere::Undefined
)
}
}
fn application_arguments(arena: &Arena, node: &ExprNode) -> Option<SmallVec<[ExprId; 2]>> {
match node {
ExprNode::Floor(a)
| ExprNode::Ceiling(a)
| ExprNode::Sin(a)
| ExprNode::Cos(a)
| ExprNode::Tan(a)
| ExprNode::Exp(a)
| ExprNode::Ln(a)
| ExprNode::Abs(a)
| ExprNode::Asin(a)
| ExprNode::Acos(a)
| ExprNode::Atan(a)
| ExprNode::Sinh(a)
| ExprNode::Cosh(a)
| ExprNode::Tanh(a)
| ExprNode::Asinh(a)
| ExprNode::Acosh(a)
| ExprNode::Atanh(a)
| ExprNode::Sign(a)
| ExprNode::Gamma(a)
| ExprNode::LogGamma(a)
| ExprNode::Digamma(a)
| ExprNode::Erf(a)
| ExprNode::Erfc(a)
| ExprNode::LambertW(a)
| ExprNode::Si(a)
| ExprNode::Ci(a)
| ExprNode::Ei(a)
| ExprNode::Li(a)
| ExprNode::Zeta(a)
| ExprNode::Factorial(a) => Some(smallvec![*a]),
ExprNode::Atan2(a, b)
| ExprNode::Beta(a, b)
| ExprNode::Polygamma(a, b)
| ExprNode::Binomial(a, b) => Some(smallvec![*a, *b]),
ExprNode::Pow(b, e) if arena.as_num(*e).is_none() => Some(smallvec![*b, *e]),
ExprNode::Apply(_, args) => Some(args.iter().copied().collect()),
_ => None,
}
}
fn everywhere(arena: &mut Arena, root: ExprId, memo: &mut EverywhereMemo) -> Everywhere {
if let Some(&known) = memo.value.get(&root) {
return known;
}
let mut stack: SmallVec<[(ExprId, bool); 16]> = smallvec![(root, false)];
while let Some(&(id, expanded)) = stack.last() {
if memo.value.contains_key(&id) {
stack.pop();
continue;
}
let node = arena.node(id).clone();
if !expanded {
if let Some(top) = stack.last_mut() {
top.1 = true;
}
let children: SmallVec<[ExprId; 6]> = match &node {
ExprNode::Add(ch) | ExprNode::Mul(ch) => ch.clone(),
ExprNode::Neg(c) => smallvec![*c],
ExprNode::Pow(b, e) if arena.as_num(*e).is_some() => smallvec![*b],
n => application_arguments(arena, n)
.map(|a| a.into_iter().collect())
.unwrap_or_default(),
};
stack.extend(
children
.into_iter()
.filter(|c| !memo.value.contains_key(c))
.map(|c| (c, false)),
);
continue;
}
stack.pop();
let get = |memo: &EverywhereMemo, c: ExprId| {
memo.value.get(&c).copied().unwrap_or(Everywhere::Unknown)
};
let value = match &node {
ExprNode::NaN => Everywhere::Undefined,
ExprNode::ComplexInfinity => Everywhere::Infinite,
ExprNode::Num(nid) if arena.num(*nid).is_zero() => Everywhere::Zero,
ExprNode::Neg(c) => get(memo, *c),
ExprNode::Pow(b, e) if arena.as_num(*e).is_some() => {
let negative = arena.as_num(*e).is_some_and(Signed::is_negative);
match (get(memo, *b), negative) {
(Everywhere::Undefined, _) => Everywhere::Undefined,
(Everywhere::Zero, false) | (Everywhere::Infinite, true) => Everywhere::Zero,
(Everywhere::Zero, true) | (Everywhere::Infinite, false) => {
Everywhere::Infinite
}
(Everywhere::Unknown, true) if memo.vanishes(arena, *b) => Everywhere::Infinite,
(Everywhere::Unknown, _) => Everywhere::Unknown,
}
}
ExprNode::Mul(children) => {
let values: SmallVec<[Everywhere; 6]> =
children.iter().map(|&c| get(memo, c)).collect();
let infinite = values.contains(&Everywhere::Infinite);
if values.contains(&Everywhere::Undefined)
|| (infinite && values.contains(&Everywhere::Zero))
{
Everywhere::Undefined
} else if infinite {
let mut undefined = false;
for (&c, &v) in children.iter().zip(&values) {
let positive = match arena.node(c) {
ExprNode::Pow(_, e) => arena.as_num(*e).is_none_or(Signed::is_positive),
_ => true,
};
if v == Everywhere::Unknown && positive && memo.vanishes(arena, c) {
undefined = true;
break;
}
}
if undefined {
Everywhere::Undefined
} else {
Everywhere::Infinite
}
} else if values.contains(&Everywhere::Zero) {
Everywhere::Zero
} else {
Everywhere::Unknown
}
}
ExprNode::Add(children) => {
let values: SmallVec<[Everywhere; 6]> =
children.iter().map(|&c| get(memo, c)).collect();
let infinite = values
.iter()
.filter(|&&v| v == Everywhere::Infinite)
.count();
if values.contains(&Everywhere::Undefined) || infinite > 1 {
Everywhere::Undefined
} else if infinite == 1 {
Everywhere::Infinite
} else if values.iter().all(|&v| v == Everywhere::Zero) {
Everywhere::Zero
} else {
Everywhere::Unknown
}
}
n => match application_arguments(arena, n) {
Some(args) => {
let values: SmallVec<[Everywhere; 2]> =
args.iter().map(|&c| get(memo, c)).collect();
if values.contains(&Everywhere::Undefined) {
Everywhere::Undefined
} else if values
.iter()
.any(|v| matches!(v, Everywhere::Infinite | Everywhere::Zero))
{
application_at_special_values(arena, id, &args, &values)
} else {
Everywhere::Unknown
}
}
None => Everywhere::Unknown,
},
};
memo.value.insert(id, value);
}
memo.value
.get(&root)
.copied()
.unwrap_or(Everywhere::Unknown)
}
pub(crate) fn everywhere_values(
arena: &mut Arena,
ids: &[ExprId],
zero_bases: &[ExprId],
) -> SmallVec<[Everywhere; 4]> {
let mut memo = EverywhereMemo::default();
for &b in zero_bases {
memo.vanishes.insert(b, true);
}
ids.iter()
.map(|&id| everywhere(arena, id, &mut memo))
.collect()
}
fn application_at_special_values(
arena: &mut Arena,
id: ExprId,
args: &[ExprId],
values: &[Everywhere],
) -> Everywhere {
let (zoo, zero) = (arena.complex_infinity, arena.zero);
let replacement: SmallVec<[(ExprId, ExprId); 2]> = args
.iter()
.zip(values)
.filter_map(|(&a, v)| match v {
Everywhere::Infinite => Some((a, zoo)),
Everywhere::Zero => Some((a, zero)),
_ => None,
})
.collect();
let at = crate::base::walk::rebuild_with(arena, id, &|c| {
replacement
.iter()
.find(|(a, _)| *a == c)
.map_or(c, |&(_, v)| v)
});
match arena.node(at) {
ExprNode::NaN => Everywhere::Undefined,
ExprNode::ComplexInfinity => Everywhere::Infinite,
ExprNode::Num(nid) if arena.num(*nid).is_zero() => Everywhere::Zero,
_ => Everywhere::Unknown,
}
}
fn split_bases(
arena: &Arena,
bases: &FxHashMap<ExprId, SmallVec<[ExprId; 4]>>,
) -> (SmallVec<[ExprId; 8]>, SmallVec<[ExprId; 4]>) {
let mut positive: SmallVec<[ExprId; 8]> = SmallVec::new();
let mut negative: SmallVec<[ExprId; 4]> = SmallVec::new();
for (&b, exps) in bases {
if exps
.iter()
.any(|&e| arena.as_num(e).is_some_and(Signed::is_negative))
{
negative.push(b);
} else {
positive.push(b);
}
}
(positive, negative)
}
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 = q_mul(na, nb);
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, &bases, &coeff);
}
_ => {} }
}
ExprNode::Num(nid) => {
coeff = q_mul(&coeff, arena.num(nid));
if coeff.is_zero() {
let (positive, negative) = split_bases(arena, &bases);
return zero_times_rest(
arena,
saw_infinity,
stack.iter().chain(positive.iter()),
negative,
);
}
}
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() {
let (positive, negative) = split_bases(arena, &bases);
return zero_times_rest(arena, saw_infinity, positive.iter(), negative);
}
if cancels_vanishing_sum(arena, &bases) {
tracing::debug!("canon_mul: s·s⁻¹ with s identically 0 → nan");
return arena.nan;
}
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;
match exp_groups.entry(exp_id) {
std::collections::hash_map::Entry::Occupied(mut slot) => {
let entry = slot.get_mut();
entry.0 = q_mul(&entry.0, &base_r);
entry.1 += 1;
}
std::collections::hash_map::Entry::Vacant(slot) => {
slot.insert((base_r, 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 = q_mul(&coeff, val);
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 = q_mul(&coeff, val);
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]>,
bases: &FxHashMap<ExprId, SmallVec<[ExprId; 4]>>,
coeff: &Q,
) -> ExprId {
if coeff.is_zero() {
return arena.nan;
}
let mut factors: SmallVec<[ExprId; 4]> = SmallVec::new();
let mut denominators: SmallVec<[ExprId; 4]> = SmallVec::new();
for (&b, exps) in bases {
let numeric: SmallVec<[&Q; 4]> = exps.iter().filter_map(|&e| arena.as_num(e)).collect();
if numeric.iter().any(|q| q.is_positive()) {
factors.push(b);
}
if numeric.iter().any(|q| q.is_negative()) {
denominators.push(b);
}
}
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);
}
ExprNode::Neg(inner) => remaining.push(inner),
ExprNode::Num(_) => {}
ExprNode::Pow(base, e) if arena.as_num(e).is_some_and(Signed::is_negative) => {
denominators.push(base);
}
_ => factors.push(id),
}
}
let mut memo = EverywhereMemo::default();
for f in factors {
if memo.zero_or_undefined(arena, f) {
tracing::debug!("canon_mul: zoo times an identically zero or undefined factor → nan");
return arena.nan;
}
}
for d in denominators {
if memo.infinite_or_undefined(arena, d) {
tracing::debug!("canon_mul: zoo over an identically infinite sum → nan");
return arena.nan;
}
}
arena.intern(ExprNode::ComplexInfinity)
}
pub(crate) fn zero_candidate_sums(arena: &Arena, f: ExprId) -> SmallVec<[ExprId; 4]> {
let mut sums: SmallVec<[ExprId; 4]> = SmallVec::new();
match arena.node(f) {
ExprNode::Add(_) => {
sums.push(f);
return sums;
}
node if node.is_atom() => return sums,
_ => {}
}
let mut visited: FxHashSet<ExprId> = FxHashSet::default();
let mut stack: SmallVec<[ExprId; 8]> = smallvec![f];
while let Some(id) = stack.pop() {
if !visited.insert(id) {
continue;
}
match arena.node(id) {
ExprNode::Add(_) => sums.push(id),
ExprNode::Mul(children) => stack.extend_from_slice(children),
ExprNode::Neg(inner) => stack.push(*inner),
ExprNode::Pow(base, e) if arena.as_num(*e).is_some_and(Signed::is_positive) => {
stack.push(*base);
}
ExprNode::Sin(a)
| ExprNode::Tan(a)
| ExprNode::Sinh(a)
| ExprNode::Tanh(a)
| ExprNode::Asin(a)
| ExprNode::Atan(a)
| ExprNode::Asinh(a)
| ExprNode::Atanh(a)
| ExprNode::Abs(a)
| ExprNode::Sign(a)
| ExprNode::LambertW(a)
| ExprNode::Floor(a)
| ExprNode::Ceiling(a) => stack.push(*a),
_ => {}
}
}
sums
}
pub(crate) fn may_vanish_identically_as_factor(arena: &Arena, f: ExprId) -> bool {
zero_candidate_sums(arena, f)
.into_iter()
.any(|s| arena.may_vanish_identically(s))
}
pub(crate) fn vanishes_identically_as_factor(arena: &mut Arena, f: ExprId) -> bool {
zero_candidate_sums(arena, f)
.into_iter()
.any(|s| sum_vanishes_identically(arena, s))
}
fn cancels_vanishing_sum(
arena: &mut Arena,
bases: &FxHashMap<ExprId, SmallVec<[ExprId; 4]>>,
) -> bool {
let mixed: SmallVec<[ExprId; 2]> = bases
.iter()
.filter(|&(&b, exps)| {
exps.len() > 1
&& !arena.node(b).is_atom()
&& exps
.iter()
.any(|&e| arena.as_num(e).is_some_and(Signed::is_positive))
&& exps
.iter()
.any(|&e| arena.as_num(e).is_some_and(Signed::is_negative))
})
.map(|(&b, _)| b)
.collect();
mixed.into_iter().any(|b| factor_vanishes(arena, b))
}
fn may_vanish_by_identity(arena: &Arena, d: ExprId) -> bool {
if arena.node(d).is_atom() {
return false;
}
let mut visited: FxHashSet<ExprId> = FxHashSet::default();
let mut stack: SmallVec<[ExprId; 16]> = smallvec![d];
let mut function = false;
while let Some(id) = stack.pop() {
if !visited.insert(id) {
continue;
}
match arena.node(id) {
ExprNode::Infinity
| ExprNode::NegInfinity
| ExprNode::ComplexInfinity
| ExprNode::NaN => return false,
ExprNode::Num(_) | ExprNode::Symbol(_) => {}
ExprNode::Add(children) | ExprNode::Mul(children) => stack.extend_from_slice(children),
ExprNode::Neg(inner) => stack.push(*inner),
ExprNode::Pow(base, e) if arena.as_num(*e).is_some_and(|q| q.is_integer()) => {
stack.push(*base);
}
_ => function = true,
}
}
function
}
pub(crate) fn identity_denominator_candidates(
arena: &Arena,
nodes: &[ExprId],
) -> SmallVec<[ExprId; 2]> {
let negative_base = |c: ExprId| match arena.node(c) {
ExprNode::Pow(base, e) if arena.as_num(*e).is_some_and(Signed::is_negative) => Some(*base),
_ => None,
};
let mut transcendental: FxHashMap<ExprId, bool> = FxHashMap::default();
let mut may_vanish: FxHashMap<ExprId, bool> = FxHashMap::default();
let mut out: SmallVec<[ExprId; 2]> = SmallVec::new();
for &id in nodes {
let ExprNode::Mul(children) = arena.node(id) else {
continue;
};
let mut denominators: SmallVec<[ExprId; 2]> = SmallVec::new();
for &c in children {
if let Some(b) = negative_base(c)
&& *transcendental
.entry(b)
.or_insert_with(|| may_vanish_by_identity(arena, b))
{
denominators.push(b);
}
}
if denominators.is_empty() {
continue;
}
let zero_candidate = children.iter().any(|&c| {
negative_base(c).is_none()
&& *may_vanish
.entry(c)
.or_insert_with(|| may_vanish_identically_as_factor(arena, c))
});
if !zero_candidate {
continue;
}
for d in denominators {
if !out.contains(&d) {
out.push(d);
}
}
}
out
}
pub(crate) fn nodes_over_vanishing_bases(
arena: &Arena,
order: &[ExprId],
zero: impl Fn(ExprId) -> bool,
) -> FxHashSet<ExprId> {
let mut infinite: FxHashSet<ExprId> = FxHashSet::default();
for &id in order {
let is_infinite = match arena.node(id) {
ExprNode::Add(ch) | ExprNode::Mul(ch) => ch.iter().any(|c| infinite.contains(c)),
ExprNode::Neg(c) => infinite.contains(c),
ExprNode::Pow(b, e) => match arena.as_num(*e) {
Some(q) if q.is_negative() => zero(*b),
Some(_) => infinite.contains(b),
None => false,
},
_ => false,
};
if is_infinite {
infinite.insert(id);
}
}
infinite
}
pub(crate) fn sum_partner_denominators(
arena: &Arena,
nodes: &[ExprId],
vanishes: impl Fn(ExprId) -> bool,
) -> SmallVec<[ExprId; 2]> {
let denominators = |t: ExprId| -> SmallVec<[ExprId; 2]> {
let factors: &[ExprId] = match arena.node(t) {
ExprNode::Mul(children) => children,
_ => std::slice::from_ref(&t),
};
factors
.iter()
.filter_map(|&f| match arena.node(f) {
ExprNode::Pow(base, e) if arena.as_num(*e).is_some_and(Signed::is_negative) => {
Some(*base)
}
_ => None,
})
.collect()
};
let mut out: SmallVec<[ExprId; 2]> = SmallVec::new();
for &id in nodes {
let ExprNode::Add(terms) = arena.node(id) else {
continue;
};
let per_term: SmallVec<[SmallVec<[ExprId; 2]>; 6]> =
terms.iter().map(|&t| denominators(t)).collect();
let infinite: SmallVec<[bool; 6]> = per_term
.iter()
.map(|ds| ds.iter().any(|&d| vanishes(d)))
.collect();
if infinite.iter().filter(|&&b| b).count() != 1 {
continue;
}
for (ds, &inf) in per_term.iter().zip(&infinite) {
if inf {
continue;
}
for &d in ds {
if !out.contains(&d) && may_vanish_by_identity(arena, d) {
out.push(d);
}
}
}
}
out
}
pub(crate) fn factor_vanishes(arena: &mut Arena, f: ExprId) -> bool {
vanishes_identically_as_factor(arena, f)
|| constant_vanishes(arena, f)
|| sum_under_zero_chain(arena, f).is_some_and(|s| s != f && constant_vanishes(arena, s))
}
fn sum_under_zero_chain(arena: &Arena, f: ExprId) -> Option<ExprId> {
let mut id = f;
loop {
id = match arena.node(id) {
ExprNode::Add(_) => return Some(id),
ExprNode::Neg(inner) => *inner,
ExprNode::Pow(base, e) if arena.as_num(*e).is_some_and(Signed::is_positive) => *base,
ExprNode::Sin(a)
| ExprNode::Tan(a)
| ExprNode::Sinh(a)
| ExprNode::Tanh(a)
| ExprNode::Asin(a)
| ExprNode::Atan(a)
| ExprNode::Asinh(a)
| ExprNode::Atanh(a)
| ExprNode::Abs(a)
| ExprNode::Sign(a)
| ExprNode::LambertW(a)
| ExprNode::Floor(a)
| ExprNode::Ceiling(a) => *a,
_ => return None,
};
}
}
fn is_numeric_constant(arena: &Arena, f: ExprId) -> bool {
let mut visited: FxHashSet<ExprId> = FxHashSet::default();
let mut stack: SmallVec<[ExprId; 16]> = smallvec![f];
while let Some(id) = stack.pop() {
if !visited.insert(id) {
continue;
}
match arena.node(id) {
ExprNode::Symbol(_)
| ExprNode::Infinity
| ExprNode::NegInfinity
| ExprNode::ComplexInfinity
| ExprNode::NaN => return false,
node => node.for_each_child(|c| stack.push(c)),
}
}
!crate::base::walk::has_unevaluated(arena, f)
}
pub(crate) fn constant_vanishes(arena: &mut Arena, f: ExprId) -> bool {
use crate::base::assumptions::{AssumptionCache, Props};
if arena.node(f).is_atom() || vanishing_check_active() || !is_numeric_constant(arena, f) {
return false;
}
if AssumptionCache::new().query(arena, f, Props::NONZERO) == Some(true) {
return false;
}
if costly_to_evaluate(arena, f) && !arena.may_vanish_identically(f) {
return false;
}
arena.vanishes_by_identity(f)
}
pub(crate) fn with_costly_parts_as_symbols(arena: &mut Arena, f: ExprId) -> Option<ExprId> {
let mut costly: SmallVec<[ExprId; 2]> = SmallVec::new();
let mut visited: FxHashSet<ExprId> = FxHashSet::default();
let mut stack: SmallVec<[ExprId; 16]> = smallvec![f];
while let Some(id) = stack.pop() {
if !visited.insert(id) {
continue;
}
if is_costly_node(arena, id) {
costly.push(id);
} else {
arena.node(id).for_each_child(|c| stack.push(c));
}
}
if costly.is_empty() {
return None;
}
let mut cache: FxHashMap<ExprId, ExprId> = FxHashMap::default();
for (k, &c) in costly.iter().enumerate() {
let symbol = arena.symbol(&format!("__costly_constant_{k}"));
cache.insert(c, symbol);
}
for id in crate::base::walk::post_order_ids(arena, f) {
if cache.contains_key(&id) || arena.node(id).is_atom() {
continue;
}
let rebuilt = crate::base::walk::rebuild_with_cache(arena, id, &cache);
if rebuilt != id {
cache.insert(id, rebuilt);
}
}
let general = cache.get(&f).copied().unwrap_or(f);
(general != f && !costly_to_evaluate(arena, general)).then_some(general)
}
const COSTLY_PARAMETER: i64 = 100_000;
const COSTLY_DEGREE: i64 = 200;
pub(crate) fn costly_to_evaluate(arena: &Arena, f: ExprId) -> bool {
let mut visited: FxHashSet<ExprId> = FxHashSet::default();
let mut stack: SmallVec<[ExprId; 16]> = smallvec![f];
while let Some(id) = stack.pop() {
if !visited.insert(id) {
continue;
}
if is_costly_node(arena, id) {
return true;
}
arena.node(id).for_each_child(|c| stack.push(c));
}
false
}
fn is_costly_node(arena: &Arena, id: ExprId) -> bool {
use crate::base::libfn::LibFn;
let node = arena.node(id);
let special = matches!(
node,
ExprNode::Gamma(_)
| ExprNode::LogGamma(_)
| ExprNode::Digamma(_)
| ExprNode::Polygamma(..)
| ExprNode::Zeta(_)
| ExprNode::Beta(..)
| ExprNode::Factorial(_)
| ExprNode::Binomial(..)
| ExprNode::Erf(_)
| ExprNode::Erfc(_)
| ExprNode::Ei(_)
| ExprNode::Li(_)
| ExprNode::Si(_)
| ExprNode::Ci(_)
| ExprNode::LambertW(_)
| ExprNode::Apply(..)
);
if !special {
return false;
}
if let ExprNode::Apply(sid, args) = node
&& matches!(
arena.lib_fn(*sid),
Some(
LibFn::Legendre
| LibFn::ChebyshevT
| LibFn::ChebyshevU
| LibFn::Hermite
| LibFn::Laguerre
| LibFn::Gegenbauer
| LibFn::Jacobi
| LibFn::AssocLegendre
| LibFn::AssocLaguerre
)
)
{
let degree_bound = Q::from_integer(BigInt::from(COSTLY_DEGREE));
if args
.first()
.and_then(|&n| arena.as_num(n))
.is_some_and(|q| q.abs() >= degree_bound)
{
return true;
}
}
let bound = Q::from_integer(BigInt::from(COSTLY_PARAMETER));
let mut costly = false;
node.for_each_child(|c| costly |= arena.as_num(c).is_some_and(|q| q.abs() >= bound));
costly
}
thread_local! {
static IN_VANISHING_CHECK: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
}
pub(crate) fn vanishing_check_active() -> bool {
IN_VANISHING_CHECK
.try_with(std::cell::Cell::get)
.unwrap_or(true)
}
struct VanishingCheckGuard;
impl VanishingCheckGuard {
fn enter() -> Option<Self> {
IN_VANISHING_CHECK
.try_with(|active| !active.replace(true))
.unwrap_or(false)
.then_some(VanishingCheckGuard)
}
}
impl Drop for VanishingCheckGuard {
fn drop(&mut self) {
let _ = IN_VANISHING_CHECK.try_with(|active| active.set(false));
}
}
pub(crate) fn sum_vanishes_identically(arena: &mut Arena, s: ExprId) -> bool {
if !matches!(arena.node(s), ExprNode::Add(_))
|| !arena.may_vanish_identically(s)
|| arena.may_have_vanishing_denominator(s)
{
return false;
}
let Some(_guard) = VanishingCheckGuard::enter() else {
return false;
};
let s = if costly_to_evaluate(arena, s) {
match with_costly_parts_as_symbols(arena, s) {
Some(general) => general,
None => return false,
}
} else {
s
};
let (numerator, _) = arena.as_numer_denom_expr(s);
arena.vanishes_identically(numerator)
}
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 = q_mul(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 let Some(e) = arena.as_num(exp)
&& !e.is_positive()
&& is_infinite_sum(arena, base)
{
return if e.is_zero() { arena.nan } else { arena.zero };
}
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();
if b.is_one() {
return Some(arena.one);
}
if *b == -Q::one() {
let odd = num_integer::Integer::is_odd(&exp_int);
return Some(if odd { arena.neg_one } else { arena.one });
}
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;
}
if pow_n.is_negative() {
Ratio::new_raw(-pow_d, -pow_n)
} else {
Ratio::new_raw(pow_d, pow_n)
}
} else {
Ratio::new_raw(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());
static RADICAL_SPLITS: std::cell::RefCell<RadicalSplits> =
std::cell::RefCell::new(FxHashMap::default());
}
type RadicalSplits = FxHashMap<(BigInt, u32), std::rc::Rc<(BigInt, BigInt)>>;
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());
}
if n.bits() <= RADICAL_MEMO_MIN_BITS {
return split_perfect_power_uncached(n, k);
}
let key = (n.clone(), k);
let cached = RADICAL_SPLITS
.try_with(|memo| memo.try_borrow().ok().and_then(|m| m.get(&key).cloned()))
.ok()
.flatten();
if let Some(split) = cached {
return (*split).clone();
}
let split = std::rc::Rc::new(split_perfect_power_uncached(n, k));
let _ = RADICAL_SPLITS.try_with(|memo| {
if let Ok(mut memo) = memo.try_borrow_mut() {
if memo.len() >= RADICAL_MEMO_CAPACITY {
memo.clear();
}
memo.insert(key, std::rc::Rc::clone(&split));
}
});
(*split).clone()
}
fn split_perfect_power_uncached(n: &BigInt, k: u32) -> (BigInt, BigInt) {
if crate::domains::ntheory::may_be_kth_power(n, k) {
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),
InfinitePlus(InfDir, Q, Q),
Built(ExprId),
ComplexInfinity,
NaN,
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
enum InfDir {
Pos,
Neg,
PosImag,
NegImag,
}
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),
InfinitePlus(InfDir, SmallVec<[ExprId; 4]>),
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_mul(
&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,
}
}
ExprNode::Add(children) if is_infinite_sum(arena, id) => {
let mut direction = None;
let mut finite: SmallVec<[ExprId; 4]> = SmallVec::new();
for &c in children.iter() {
let d = match *arena.node(c) {
ExprNode::Infinity => Some(InfDir::Pos),
ExprNode::NegInfinity => Some(InfDir::Neg),
_ if is_directed_infinity(arena, c) => match classify_arg(arena, c) {
Arg::ImaginaryInfinity(neg) => Some(if neg {
InfDir::NegImag
} else {
InfDir::PosImag
}),
_ => return Arg::Other,
},
_ => None,
};
match d {
Some(d) => direction = Some(d),
None => finite.push(c),
}
}
match direction {
Some(d) => Arg::InfinitePlus(d, finite),
None => Arg::Other,
}
}
_ => Arg::Other,
}
}
#[derive(Clone, Copy, Debug)]
pub(crate) struct InfiniteArgument {
pub(crate) imaginary: bool,
pub(crate) negative: bool,
pub(crate) side: Option<Ordering>,
}
pub(crate) fn infinite_argument(arena: &Arena, id: ExprId) -> Option<InfiniteArgument> {
let (d, side) = match classify_arg(arena, id) {
Arg::Infinity => (InfDir::Pos, Some(Ordering::Equal)),
Arg::NegInfinity => (InfDir::Neg, Some(Ordering::Equal)),
Arg::ImaginaryInfinity(neg) => (
if neg {
InfDir::NegImag
} else {
InfDir::PosImag
},
Some(Ordering::Equal),
),
Arg::InfinitePlus(d, c) => {
let imaginary = matches!(d, InfDir::Pos | InfDir::Neg);
(d, finite_part_sign(arena, &c, imaginary))
}
_ => return None,
};
Some(InfiniteArgument {
imaginary: matches!(d, InfDir::PosImag | InfDir::NegImag),
negative: matches!(d, InfDir::Neg | InfDir::NegImag),
side,
})
}
fn finite_part_sign(arena: &Arena, terms: &[ExprId], imaginary: bool) -> Option<Ordering> {
use crate::base::assumptions::{AssumptionCache, Props};
let mut cache = AssumptionCache::new();
let mut total = Ordering::Equal;
for &t in terms {
let factors: &[ExprId] = match arena.node(t) {
ExprNode::Mul(children) => children,
_ => std::slice::from_ref(&t),
};
let i_count = factors.iter().filter(|&&f| f == arena.i_unit).count();
let (re_sign, im_sign) = if i_count == 1 {
let mut sign = Ordering::Greater;
for &f in factors.iter().filter(|&&f| f != arena.i_unit) {
let s = match arena.as_num(f) {
Some(q) => q.cmp(&Q::zero()),
None => known_sign(arena, f)?,
};
if s == Ordering::Less {
sign = sign.reverse();
}
}
(Ordering::Equal, sign)
} else if cache.query(arena, t, Props::REAL) == Some(true) {
let s = match arena.as_num(t) {
Some(q) => q.cmp(&Q::zero()),
None => known_sign(arena, t)?,
};
(s, Ordering::Equal)
} else {
return None;
};
let s = if imaginary { im_sign } else { re_sign };
match (total, s) {
(_, Ordering::Equal) => {}
(Ordering::Equal, s) => total = s,
(t, s) if t != s => return None,
_ => {}
}
}
Some(total)
}
fn exp_direction(arena: &mut Arena, terms: &[ExprId], inner: ExprId, outer: ExprId) -> Folded {
let c = canon_add(arena, terms);
let scaled = canon_mul(arena, &[inner, c]);
let e = arena.exp(scaled);
let inf = arena.infinity;
Folded::Built(canon_mul(arena, &[outer, e, inf]))
}
fn signed(s: Ordering, q: Q) -> Q {
if s == Ordering::Less { -q } else { q }
}
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 {
use num_integer::Integer;
let r = q.numer().mod_floor(&(q.denom() * BigInt::from(m)));
if r.is_zero() {
Q::zero()
} else {
Ratio::new_raw(r, q.denom().clone())
}
}
fn in_units(q: &Q, n: i64) -> Option<i64> {
let b = q.denom().to_i64()?;
if b <= 0 || n % b != 0 {
return None;
}
(q.numer() * BigInt::from(n / b)).to_i64()
}
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::Min(args) | ExprNode::Max(args) = node {
return fold_min_max(arena, args, matches!(node, ExprNode::Min(_)));
}
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::InfinitePlus(InfDir::PosImag, c) => {
let (minus_i, i) = (canon_neg(arena, arena.i_unit), arena.i_unit);
exp_direction(arena, &c, minus_i, i)
}
Arg::InfinitePlus(InfDir::NegImag, c) => {
let (minus_i, i) = (canon_neg(arena, arena.i_unit), arena.i_unit);
exp_direction(arena, &c, i, minus_i)
}
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::InfinitePlus(d @ (InfDir::PosImag | InfDir::NegImag), c) => {
let i = arena.i_unit;
let inner = if d == InfDir::PosImag {
canon_neg(arena, i)
} else {
i
};
exp_direction(arena, &c, inner, arena.one)
}
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::InfinitePlus(InfDir::PosImag, _) => Folded::Imaginary(Q::one()),
Arg::InfinitePlus(InfDir::NegImag, _) => Folded::Imaginary(-Q::one()),
Arg::ComplexInfinity | Arg::NaN => Folded::NaN,
_ => return None,
},
ExprNode::Exp(a) => match classify_arg(arena, a) {
Arg::InfinitePlus(InfDir::Pos, c) => exp_direction(arena, &c, arena.one, arena.one),
Arg::InfinitePlus(InfDir::Neg, _) => Folded::int(0),
Arg::InfinitePlus(_, _) => Folded::NaN,
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::InfinitePlus(InfDir::Pos, _) => Folded::Infinity,
Arg::NegInfinity => Folded::InfinitePlus(InfDir::Pos, Q::one(), Q::zero()),
Arg::InfinitePlus(InfDir::Neg, c) => match finite_part_sign(arena, &c, true)? {
Ordering::Equal => return None,
s => Folded::InfinitePlus(InfDir::Pos, signed(s, Q::one()), Q::zero()),
},
Arg::ImaginaryInfinity(true) | Arg::InfinitePlus(InfDir::NegImag, _) => {
Folded::InfinitePlus(InfDir::Pos, Q::new((-1).into(), 2.into()), Q::zero())
}
Arg::ImaginaryInfinity(false) | Arg::InfinitePlus(InfDir::PosImag, _) => {
Folded::InfinitePlus(InfDir::Pos, Q::new(1.into(), 2.into()), Q::zero())
}
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::InfinitePlus(InfDir::Pos, c) => exp_direction(arena, &c, arena.one, arena.one),
Arg::InfinitePlus(InfDir::Neg, c) => {
exp_direction(arena, &c, arena.neg_one, arena.neg_one)
}
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::InfinitePlus(InfDir::Pos, c) => exp_direction(arena, &c, arena.one, arena.one),
Arg::InfinitePlus(InfDir::Neg, c) => exp_direction(arena, &c, arena.neg_one, arena.one),
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 | Arg::InfinitePlus(InfDir::Pos, _) => Folded::int(1),
Arg::NegInfinity | Arg::InfinitePlus(InfDir::Neg, _) => 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::InfinitePlus(InfDir::NegImag, Q::new(1.into(), 2.into()), Q::zero())
}
Arg::NegInfinity => {
Folded::InfinitePlus(InfDir::PosImag, Q::new((-1).into(), 2.into()), Q::zero())
}
Arg::InfinitePlus(d @ (InfDir::Pos | InfDir::Neg), c) => {
let half = Q::new(if d == InfDir::Pos { 1 } else { -1 }.into(), 2.into());
match finite_part_sign(arena, &c, true)? {
Ordering::Greater => Folded::InfinitePlus(InfDir::PosImag, half, Q::zero()),
Ordering::Less => Folded::InfinitePlus(InfDir::NegImag, half, Q::zero()),
Ordering::Equal => return None,
}
}
Arg::ImaginaryInfinity(neg) => Folded::ImaginaryInfinity(neg),
Arg::InfinitePlus(d, _) => Folded::ImaginaryInfinity(d == InfDir::NegImag),
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::InfinitePlus(InfDir::NegImag, Q::one(), Q::zero()),
Arg::InfinitePlus(d @ (InfDir::Pos | InfDir::Neg), c) => {
let q = if d == InfDir::Pos {
Q::zero()
} else {
Q::one()
};
match finite_part_sign(arena, &c, true)? {
Ordering::Greater => Folded::InfinitePlus(InfDir::NegImag, q, Q::zero()),
Ordering::Less => Folded::InfinitePlus(InfDir::PosImag, q, Q::zero()),
Ordering::Equal => return None,
}
}
Arg::ImaginaryInfinity(neg) => {
let d = if neg {
InfDir::PosImag
} else {
InfDir::NegImag
};
Folded::InfinitePlus(d, Q::new(1.into(), 2.into()), Q::zero())
}
Arg::InfinitePlus(d, _) => {
let d = if d == InfDir::NegImag {
InfDir::PosImag
} else {
InfDir::NegImag
};
Folded::InfinitePlus(d, Q::new(1.into(), 2.into()), Q::zero())
}
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::InfinitePlus(InfDir::Pos, _) => Folded::Infinity,
Arg::InfinitePlus(InfDir::Neg, _) => Folded::NegInfinity,
Arg::ImaginaryInfinity(neg) => {
let (d, q) = if neg {
(InfDir::Neg, Q::new((-1).into(), 2.into()))
} else {
(InfDir::Pos, Q::new(1.into(), 2.into()))
};
Folded::InfinitePlus(d, q, Q::zero())
}
Arg::InfinitePlus(d, c) => {
let up = d == InfDir::PosImag;
let q = Q::new(if up { 1 } else { -1 }.into(), 2.into());
let real = match finite_part_sign(arena, &c, false)? {
Ordering::Greater => InfDir::Pos,
Ordering::Less => InfDir::Neg,
Ordering::Equal if up => InfDir::Pos,
Ordering::Equal => InfDir::Neg,
};
Folded::InfinitePlus(real, q, Q::zero())
}
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::InfinitePlus(InfDir::Pos, _) => Folded::Infinity,
Arg::NegInfinity => Folded::InfinitePlus(InfDir::Pos, Q::one(), Q::zero()),
Arg::InfinitePlus(InfDir::Neg, c) => match finite_part_sign(arena, &c, true)? {
Ordering::Equal => return None,
s => Folded::InfinitePlus(InfDir::Pos, signed(s, Q::one()), Q::zero()),
},
Arg::ImaginaryInfinity(true) | Arg::InfinitePlus(InfDir::NegImag, _) => {
Folded::InfinitePlus(InfDir::Pos, Q::new((-1).into(), 2.into()), Q::zero())
}
Arg::ImaginaryInfinity(false) | Arg::InfinitePlus(InfDir::PosImag, _) => {
Folded::InfinitePlus(InfDir::Pos, Q::new(1.into(), 2.into()), Q::zero())
}
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 | Arg::InfinitePlus(InfDir::Pos, _) => Folded::int(1),
Arg::NegInfinity | Arg::InfinitePlus(InfDir::Neg, _) => 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 | Arg::InfinitePlus(InfDir::Pos, _) => Folded::int(0),
Arg::NegInfinity | Arg::InfinitePlus(InfDir::Neg, _) => Folded::int(2),
Arg::ImaginaryInfinity(neg) => {
let d = if neg {
InfDir::PosImag
} else {
InfDir::NegImag
};
Folded::InfinitePlus(d, Q::zero(), Q::one())
}
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::InfinitePlus(_, _) => 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::InfinitePlus(InfDir::Pos, _) => Folded::int(1),
Arg::InfinitePlus(InfDir::Neg, _) => Folded::int(-1),
Arg::InfinitePlus(InfDir::PosImag, _) => Folded::Imaginary(Q::one()),
Arg::InfinitePlus(InfDir::NegImag, _) => Folded::Imaginary(-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::InfinitePlus(d, q, r) => {
let i = arena.i_unit;
let mut terms: SmallVec<[ExprId; 3]> = SmallVec::new();
terms.push(match d {
InfDir::Pos => arena.infinity,
InfDir::Neg => arena.neg_infinity,
InfDir::PosImag => canon_mul(arena, &[i, arena.infinity]),
InfDir::NegImag => canon_mul(arena, &[arena.neg_one, i, arena.infinity]),
});
if !q.is_zero() {
let nid = arena.intern_num(q);
let c = arena.intern(ExprNode::Num(nid));
let pi = arena.pi;
terms.push(if matches!(d, InfDir::Pos | InfDir::Neg) {
canon_mul(arena, &[c, pi, i])
} else {
canon_mul(arena, &[c, pi])
});
}
if !r.is_zero() {
let nid = arena.intern_num(r);
terms.push(arena.intern(ExprNode::Num(nid)));
}
canon_add(arena, &terms)
}
Folded::Built(id) => id,
Folded::ComplexInfinity => arena.complex_infinity,
Folded::NaN => arena.nan,
})
}
fn fold_min_max(arena: &mut Arena, args: &[ExprId], is_min: bool) -> Option<ExprId> {
use crate::base::assumptions::{AssumptionCache, Props};
if args.is_empty() {
return None;
}
let (identity, absorbing) = if is_min {
(arena.infinity, arena.neg_infinity)
} else {
(arena.neg_infinity, arena.infinity)
};
let mut best: Option<(usize, Q)> = None;
let mut kept: SmallVec<[ExprId; 4]> = SmallVec::new();
let mut changed = false;
let mut has_absorbing = false;
for &a in args {
if a == arena.nan || a == arena.complex_infinity {
return Some(arena.nan);
}
if a == identity {
changed = true;
continue;
}
if a == absorbing {
has_absorbing = true;
continue;
}
if let Some(q) = arena.as_num(a) {
match &mut best {
Some((_, b)) => {
changed = true;
if (is_min && q < b) || (!is_min && q > b) {
*b = q.clone();
}
}
None => {
best = Some((kept.len(), q.clone()));
kept.push(a);
}
}
continue;
}
if kept.contains(&a) {
changed = true;
continue;
}
kept.push(a);
}
if has_absorbing {
let mut cache = AssumptionCache::new();
let all_real = kept
.iter()
.all(|&a| cache.query(arena, a, Props::REAL) == Some(true));
if all_real {
return Some(absorbing);
}
kept.push(absorbing);
}
if let Some((idx, q)) = best {
let nid = arena.intern_num(q);
kept[idx] = arena.intern(ExprNode::Num(nid));
}
match kept.len() {
0 => Some(identity),
1 => Some(kept[0]),
_ if changed => Some(arena.intern(if is_min {
ExprNode::Min(kept)
} else {
ExprNode::Max(kept)
})),
_ => None,
}
}
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(&q_add(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::BesselY
if is(1, 0) && num(0)?.is_negative() && num(0)?.denom() == &BigInt::from(2) =>
{
Folded::int(0)
}
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!(
matches!(a.node(result), ExprNode::Add(ch) if ch.contains(&a.infinity) && ch.contains(&x))
);
let three = a.int(3);
assert_eq!(a.add(&[a.infinity, three]), a.infinity);
let pi = a.pi;
assert_eq!(a.add(&[a.neg_infinity, pi]), a.neg_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}"
);
}
}
}
}