use crate::deriv::log::{DerivationLog, DerivedExpr, RewriteStep};
use crate::kernel::{ExprData, ExprId, ExprPool, IdMap};
use crate::poly::UniPoly;
use crate::simplify::engine::simplify;
use std::cell::RefCell;
use std::fmt;
use std::rc::Rc;
pub(crate) fn const_node(pool: &ExprPool, r: rug::Rational) -> ExprId {
if *r.denom() == 1 {
pool.integer(r.numer().clone())
} else {
let (n, d) = r.into_numer_denom();
pool.rational(n, d)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum DiffError {
UnknownFunction(String),
NonIntegerExponent,
ForwardUnknownFunction(String),
ForwardNonIntegerExponent,
}
impl fmt::Display for DiffError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
DiffError::UnknownFunction(name) => {
write!(f, "cannot differentiate unknown function '{name}'")
}
DiffError::NonIntegerExponent => {
write!(f, "cannot differentiate power with non-integer exponent")
}
DiffError::ForwardUnknownFunction(name) => {
write!(f, "diff_forward: unknown function '{name}'")
}
DiffError::ForwardNonIntegerExponent => {
write!(f, "diff_forward: non-integer exponent")
}
}
}
}
impl std::error::Error for DiffError {}
impl crate::errors::AlkahestError for DiffError {
fn code(&self) -> &'static str {
match self {
DiffError::UnknownFunction(_) => "E-DIFF-001",
DiffError::NonIntegerExponent => "E-DIFF-002",
DiffError::ForwardUnknownFunction(_) => "E-DIFF-003",
DiffError::ForwardNonIntegerExponent => "E-DIFF-004",
}
}
fn remediation(&self) -> Option<&'static str> {
match self {
DiffError::UnknownFunction(_) => Some(
"register the function in PrimitiveRegistry, or use diff_forward with a custom rule",
),
DiffError::NonIntegerExponent => Some(
"symbolic exponents require the chain rule; use diff_forward for non-integer powers",
),
DiffError::ForwardUnknownFunction(_) => Some(
"register the function in PrimitiveRegistry with diff_forward implemented",
),
DiffError::ForwardNonIntegerExponent => Some(
"substitute concrete values first; diff_forward requires integer exponents",
),
}
}
}
pub fn diff(expr: ExprId, var: ExprId, pool: &ExprPool) -> Result<DerivedExpr<ExprId>, DiffError> {
if let Some(hit) = root_univariate_fastpath(expr, var, pool) {
return Ok(hit.and_then(|v| simplify(v, pool)));
}
let memo = DiffMemo::for_call(var, pool);
let _scope = memo.enter_scope();
let mut log = DerivationLog::new();
let value = diff_raw(expr, var, pool, &memo, &mut log)?;
Ok(DerivedExpr::with_log(value, log).and_then(|v| simplify(v, pool)))
}
type MemoTable = Rc<RefCell<IdMap<ExprId>>>;
struct ActiveDiff {
pool: usize,
var: ExprId,
memo: MemoTable,
}
thread_local! {
static ACTIVE_DIFFS: RefCell<Vec<ActiveDiff>> = const { RefCell::new(Vec::new()) };
}
struct DiffMemo {
own: MemoTable,
outer: Option<MemoTable>,
pool: usize,
var: ExprId,
}
struct ScopeGuard;
impl Drop for ScopeGuard {
fn drop(&mut self) {
ACTIVE_DIFFS.with(|s| {
s.borrow_mut().pop();
});
}
}
impl DiffMemo {
fn for_call(var: ExprId, pool: &ExprPool) -> Self {
let pool_addr = pool as *const ExprPool as usize;
let outer = ACTIVE_DIFFS.with(|s| {
s.borrow()
.iter()
.rev()
.find(|a| a.pool == pool_addr && a.var == var)
.map(|a| Rc::clone(&a.memo))
});
DiffMemo {
own: Rc::new(RefCell::new(IdMap::default())),
outer,
pool: pool_addr,
var,
}
}
fn enter_scope(&self) -> ScopeGuard {
ACTIVE_DIFFS.with(|s| {
s.borrow_mut().push(ActiveDiff {
pool: self.pool,
var: self.var,
memo: Rc::clone(&self.own),
})
});
ScopeGuard
}
fn get(&self, e: ExprId) -> Option<ExprId> {
if let Some(&v) = self.own.borrow().get(&e) {
return Some(v);
}
self.outer
.as_ref()
.and_then(|o| o.borrow().get(&e).copied())
}
fn insert(&self, e: ExprId, v: ExprId) {
self.own.borrow_mut().insert(e, v);
}
}
fn diff_registry() -> &'static crate::primitive::PrimitiveRegistry {
static REGISTRY: std::sync::OnceLock<crate::primitive::PrimitiveRegistry> =
std::sync::OnceLock::new();
REGISTRY.get_or_init(crate::primitive::PrimitiveRegistry::dispatch_registry)
}
fn is_monomial_in(expr: ExprId, var: ExprId, pool: &ExprPool) -> bool {
let is_var_power = |e: ExprId| {
e == var
|| pool.with(e, |d| match d {
ExprData::Pow { base, exp } => {
*base == var && pool.with(*exp, |x| matches!(x, ExprData::Integer(_)))
}
_ => false,
})
};
if is_var_power(expr) {
return true;
}
let args = match pool.get(expr) {
ExprData::Mul(args) => args,
_ => return false,
};
let mut powers = 0;
for a in args {
if is_var_power(a) {
powers += 1;
} else if !pool.with(a, |d| {
matches!(
d,
ExprData::Integer(_) | ExprData::Rational(_) | ExprData::Float(_)
)
}) {
return false;
}
}
powers == 1
}
fn zpoly_degree_bound(
expr: ExprId,
var: ExprId,
pool: &ExprPool,
memo: &mut IdMap<u64>,
) -> Option<u64> {
if let Some(&d) = memo.get(&expr) {
return Some(d);
}
enum Shape {
Leaf(Option<u64>),
Sum(Vec<ExprId>),
Prod(Vec<ExprId>),
Pow(ExprId, u32),
}
let shape = pool.with(expr, |data| match data {
ExprData::Symbol { .. } if expr == var => Shape::Leaf(Some(1)),
ExprData::Integer(_) => Shape::Leaf(Some(0)),
ExprData::Rational(r) if *r.0.denom() == 1 => Shape::Leaf(Some(0)),
ExprData::Add(args) => Shape::Sum(args.clone()),
ExprData::Mul(args) => Shape::Prod(args.clone()),
ExprData::Pow { base, exp } => match pool.with(*exp, |x| match x {
ExprData::Integer(n) if n.0 >= 0 => n.0.to_u32(),
_ => None,
}) {
Some(n) => Shape::Pow(*base, n),
None => Shape::Leaf(None),
},
_ => Shape::Leaf(None),
});
let d = match shape {
Shape::Leaf(d) => d?,
Shape::Sum(args) => {
let mut m = 0u64;
for a in args {
m = m.max(zpoly_degree_bound(a, var, pool, memo)?);
}
m
}
Shape::Prod(args) => {
let mut s = 0u64;
for a in args {
s = s.saturating_add(zpoly_degree_bound(a, var, pool, memo)?);
}
s
}
Shape::Pow(base, n) => {
zpoly_degree_bound(base, var, pool, memo)?.saturating_mul(u64::from(n))
}
};
memo.insert(expr, d);
Some(d)
}
fn build_zpoly(
expr: ExprId,
var: ExprId,
pool: &ExprPool,
memo: &mut IdMap<UniPoly>,
) -> Option<UniPoly> {
if let Some(p) = memo.get(&expr) {
return Some(p.clone());
}
let p = match pool.get(expr) {
ExprData::Add(args) => {
let mut acc = UniPoly::zero(var);
for a in args {
let t = build_zpoly(a, var, pool, memo)?;
acc = &acc + &t;
}
acc
}
ExprData::Mul(args) => {
let mut acc = UniPoly::constant(var, 1);
for a in args {
let t = build_zpoly(a, var, pool, memo)?;
acc = &acc * &t;
}
acc
}
ExprData::Pow { base, exp } => {
let n = pool.with(exp, |x| match x {
ExprData::Integer(n) => n.0.to_u32(),
_ => None,
})?;
build_zpoly(base, var, pool, memo)?.checked_pow(n).ok()?
}
_ => UniPoly::from_symbolic(expr, var, pool).ok()?,
};
memo.insert(expr, p.clone());
Some(p)
}
fn root_univariate_fastpath(
expr: ExprId,
var: ExprId,
pool: &ExprPool,
) -> Option<DerivedExpr<ExprId>> {
if matches!(
pool.get(expr),
ExprData::Symbol { .. } | ExprData::Integer(_) | ExprData::Rational(_) | ExprData::Float(_)
) || is_monomial_in(expr, var, pool)
{
return None;
}
let mut deg_memo: IdMap<u64> = IdMap::default();
let deg = zpoly_degree_bound(expr, var, pool, &mut deg_memo)?;
let nodes = deg_memo.len() as u64;
if deg.saturating_add(1) > nodes.saturating_mul(2) {
return None;
}
let poly = build_zpoly(expr, var, pool, &mut IdMap::default())?;
let der = poly.derivative();
let result = der.to_symbolic_expr(pool);
let mut log = DerivationLog::new();
log.push(RewriteStep::simple("diff_univariate_poly", expr, result));
Some(DerivedExpr::with_log(result, log))
}
fn diff_raw(
expr: ExprId,
var: ExprId,
pool: &ExprPool,
memo: &DiffMemo,
log: &mut DerivationLog,
) -> Result<ExprId, DiffError> {
if let Some(cached) = memo.get(expr) {
return Ok(cached);
}
enum Node {
IdentVar,
Const,
Add(Vec<ExprId>),
Mul(Vec<ExprId>),
Pow {
base: ExprId,
exp: ExprId,
},
Func {
name: String,
args: Vec<ExprId>,
},
Piecewise {
branches: Vec<(ExprId, ExprId)>,
default: ExprId,
},
RootSum {
poly: ExprId,
rvar: ExprId,
body: ExprId,
},
}
let node = pool.with(expr, |data| match data {
ExprData::Symbol { .. } if expr == var => Node::IdentVar,
ExprData::Symbol { .. }
| ExprData::Integer(_)
| ExprData::Rational(_)
| ExprData::Float(_) => Node::Const,
ExprData::Add(args) => Node::Add(args.clone()),
ExprData::Mul(args) => Node::Mul(args.clone()),
ExprData::Pow { base, exp } => Node::Pow {
base: *base,
exp: *exp,
},
ExprData::Func { name, args } => Node::Func {
name: name.clone(),
args: args.clone(),
},
ExprData::Piecewise { branches, default } => Node::Piecewise {
branches: branches.clone(),
default: *default,
},
ExprData::Predicate { .. } => Node::Const,
ExprData::Forall { .. } | ExprData::Exists { .. } => Node::Const,
ExprData::BigO(_) => Node::Const,
ExprData::RootSum { poly, var, body } => Node::RootSum {
poly: *poly,
rvar: *var,
body: *body,
},
});
let result = match node {
Node::IdentVar => {
let one = pool.integer(1_i32);
log.push(RewriteStep::simple("diff_identity", expr, one));
one
}
Node::Const => {
let zero = pool.integer(0_i32);
log.push(RewriteStep::simple("diff_const", expr, zero));
zero
}
Node::Add(args) => {
let mut dargs: Vec<ExprId> = Vec::with_capacity(args.len());
for a in args {
dargs.push(diff_raw(a, var, pool, memo, log)?);
}
let sum = pool.add(dargs);
log.push(RewriteStep::simple("sum_rule", expr, sum));
sum
}
Node::Mul(args) => {
let dargs: Vec<ExprId> = args
.iter()
.map(|&a| diff_raw(a, var, pool, memo, log))
.collect::<Result<_, _>>()?;
let mut terms: Vec<ExprId> = Vec::with_capacity(args.len());
for (i, &di) in dargs.iter().enumerate() {
let rest: Vec<ExprId> = args
.iter()
.enumerate()
.filter(|&(j, _)| j != i)
.map(|(_, &a)| a)
.collect();
let term = if rest.is_empty() {
di
} else if rest.len() == 1 {
pool.mul(vec![di, rest[0]])
} else {
let prod = pool.mul(rest);
pool.mul(vec![di, prod])
};
terms.push(term);
}
let result_id = match terms.len() {
0 => pool.integer(0_i32),
1 => terms[0],
_ => pool.add(terms),
};
log.push(RewriteStep::simple("product_rule", expr, result_id));
result_id
}
Node::Pow { base, exp } => {
let r = pool
.with(exp, |data| match data {
ExprData::Integer(n) => Some(rug::Rational::from(n.0.clone())),
ExprData::Rational(q) => Some(q.0.clone()),
_ => None,
})
.ok_or(DiffError::NonIntegerExponent)?;
if r == 0 {
let zero = pool.integer(0_i32);
log.push(RewriteStep::simple("power_rule_n0", expr, zero));
zero
} else if r == 1 {
let df = diff_raw(base, var, pool, memo, log)?;
log.push(RewriteStep::simple("power_rule_n1", expr, df));
df
} else {
let df = diff_raw(base, var, pool, memo, log)?;
let r_id = const_node(pool, r.clone());
let r_minus_1 = r - 1;
let base_pow = if r_minus_1 == 1 {
base
} else {
pool.pow(base, const_node(pool, r_minus_1))
};
let one = pool.integer(1_i32);
let result_id = if df == one {
pool.mul(vec![r_id, base_pow])
} else {
pool.mul(vec![r_id, base_pow, df])
};
log.push(RewriteStep::simple("power_rule", expr, result_id));
result_id
}
}
Node::Func { name, args } if args.len() == 1 => {
let f = args[0];
let df = diff_raw(f, var, pool, memo, log)?;
match name.as_str() {
"sin" => {
let cos_f = pool.func("cos", vec![f]);
let r = pool.mul(vec![cos_f, df]);
log.push(RewriteStep::simple("diff_sin", expr, r));
r
}
"cos" => {
let sin_f = pool.func("sin", vec![f]);
let neg_one = pool.integer(-1_i32);
let r = pool.mul(vec![neg_one, sin_f, df]);
log.push(RewriteStep::simple("diff_cos", expr, r));
r
}
"exp" => {
let exp_f = pool.func("exp", vec![f]);
let r = pool.mul(vec![exp_f, df]);
log.push(RewriteStep::simple("diff_exp", expr, r));
r
}
"log" => {
let f_inv = pool.pow(f, pool.integer(-1_i32));
let r = pool.mul(vec![df, f_inv]);
log.push(RewriteStep::simple("diff_log", expr, r));
r
}
"sqrt" => {
let sqrt_f = pool.func("sqrt", vec![f]);
let two_sqrt = pool.mul(vec![pool.integer(2_i32), sqrt_f]);
let denom_inv = pool.pow(two_sqrt, pool.integer(-1_i32));
let r = pool.mul(vec![df, denom_inv]);
log.push(RewriteStep::simple("diff_sqrt", expr, r));
r
}
other => {
let reg = diff_registry();
if let Some(d) = reg.diff_forward(other, &[f], var, pool) {
log.push(RewriteStep::simple("diff_primitive_registry", expr, d));
d
} else {
return Err(DiffError::UnknownFunction(other.to_string()));
}
}
}
}
Node::Func { name, args } => {
for &a in &args {
diff_raw(a, var, pool, memo, log)?;
}
let reg = diff_registry();
if let Some(d) = reg.diff_forward(&name, &args, var, pool) {
log.push(RewriteStep::simple("diff_primitive_registry", expr, d));
d
} else {
return Err(DiffError::UnknownFunction(name));
}
}
Node::Piecewise { branches, default } => {
let mut new_branches = Vec::with_capacity(branches.len());
for (cond, val) in branches {
let dval = diff_raw(val, var, pool, memo, log)?;
new_branches.push((cond, dval));
}
let ddefault = diff_raw(default, var, pool, memo, log)?;
let result = pool.piecewise(new_branches, ddefault);
log.push(RewriteStep::simple("diff_piecewise", expr, result));
result
}
Node::RootSum { poly, rvar, body } => {
let dbody = diff_raw(body, var, pool, memo, log)?;
let result = pool.root_sum(poly, rvar, dbody);
log.push(RewriteStep::simple("diff_root_sum", expr, result));
result
}
};
memo.insert(expr, result);
Ok(result)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::kernel::{Domain, ExprPool};
use crate::poly::UniPoly;
fn p() -> ExprPool {
ExprPool::new()
}
#[test]
fn diff_constant() {
let pool = p();
let x = pool.symbol("x", Domain::Real);
let r = diff(pool.integer(5_i32), x, &pool).unwrap();
assert_eq!(r.value, pool.integer(0_i32));
assert!(r.log.steps().iter().any(|s| s.rule_name == "diff_const"));
}
#[test]
fn diff_identity() {
let pool = p();
let x = pool.symbol("x", Domain::Real);
let r = diff(x, x, &pool).unwrap();
assert_eq!(r.value, pool.integer(1_i32));
assert!(r.log.steps().iter().any(|s| s.rule_name == "diff_identity"));
}
#[test]
fn diff_other_variable() {
let pool = p();
let x = pool.symbol("x", Domain::Real);
let y = pool.symbol("y", Domain::Real);
let r = diff(y, x, &pool).unwrap();
assert_eq!(r.value, pool.integer(0_i32));
}
#[test]
fn diff_linear() {
let pool = p();
let x = pool.symbol("x", Domain::Real);
let expr = pool.mul(vec![pool.integer(3_i32), x]);
let r = diff(expr, x, &pool).unwrap();
assert_eq!(r.value, pool.integer(3_i32));
}
#[test]
fn diff_quadratic() {
let pool = p();
let x = pool.symbol("x", Domain::Real);
let r = diff(pool.pow(x, pool.integer(2_i32)), x, &pool).unwrap();
let poly = UniPoly::from_symbolic(r.value, x, &pool).unwrap();
assert_eq!(poly.coefficients_i64(), vec![0, 2]);
}
#[test]
fn diff_cubic() {
let pool = p();
let x = pool.symbol("x", Domain::Real);
let r = diff(pool.pow(x, pool.integer(3_i32)), x, &pool).unwrap();
let poly = UniPoly::from_symbolic(r.value, x, &pool).unwrap();
assert_eq!(poly.coefficients_i64(), vec![0, 0, 3]);
}
#[test]
fn diff_polynomial() {
let pool = p();
let x = pool.symbol("x", Domain::Real);
let expr = pool.add(vec![
pool.pow(x, pool.integer(3_i32)),
pool.mul(vec![pool.integer(2_i32), pool.pow(x, pool.integer(2_i32))]),
x,
pool.integer(1_i32),
]);
let r = diff(expr, x, &pool).unwrap();
let poly = UniPoly::from_symbolic(r.value, x, &pool).unwrap();
assert_eq!(poly.coefficients_i64(), vec![1, 4, 3]);
}
#[test]
fn diff_sum_rule_logged() {
let pool = p();
let x = pool.symbol("x", Domain::Real);
let y = pool.symbol("y", Domain::Real);
let r = diff(pool.add(vec![x, y]), x, &pool).unwrap();
assert_eq!(r.value, pool.integer(1_i32));
assert!(r.log.steps().iter().any(|s| s.rule_name == "sum_rule"));
}
#[test]
fn diff_product_rule_logged() {
let pool = p();
let x = pool.symbol("x", Domain::Real);
let y = pool.symbol("y", Domain::Real);
let r = diff(pool.mul(vec![x, y]), x, &pool).unwrap();
assert_eq!(r.value, y);
assert!(r.log.steps().iter().any(|s| s.rule_name == "product_rule"));
}
#[test]
fn diff_sin() {
let pool = p();
let x = pool.symbol("x", Domain::Real);
let r = diff(pool.func("sin", vec![x]), x, &pool).unwrap();
assert_eq!(r.value, pool.func("cos", vec![x]));
assert!(r.log.steps().iter().any(|s| s.rule_name == "diff_sin"));
}
#[test]
fn diff_cos() {
let pool = p();
let x = pool.symbol("x", Domain::Real);
let r = diff(pool.func("cos", vec![x]), x, &pool).unwrap();
let sin_x = pool.func("sin", vec![x]);
let neg_one = pool.integer(-1_i32);
match pool.get(r.value) {
ExprData::Mul(ref args) => {
assert_eq!(args.len(), 2);
assert!(args.contains(&neg_one) && args.contains(&sin_x));
}
_ => panic!("expected Mul, got {:?}", pool.display(r.value)),
}
assert!(r.log.steps().iter().any(|s| s.rule_name == "diff_cos"));
}
#[test]
fn diff_exp() {
let pool = p();
let x = pool.symbol("x", Domain::Real);
let exp_x = pool.func("exp", vec![x]);
let r = diff(exp_x, x, &pool).unwrap();
assert_eq!(r.value, exp_x);
assert!(r.log.steps().iter().any(|s| s.rule_name == "diff_exp"));
}
#[test]
fn diff_log() {
let pool = p();
let x = pool.symbol("x", Domain::Real);
let r = diff(pool.func("log", vec![x]), x, &pool).unwrap();
assert_eq!(r.value, pool.pow(x, pool.integer(-1_i32)));
assert!(r.log.steps().iter().any(|s| s.rule_name == "diff_log"));
}
#[test]
fn diff_chain_rule_sin() {
let pool = p();
let x = pool.symbol("x", Domain::Real);
let r = diff(
pool.func("sin", vec![pool.pow(x, pool.integer(2_i32))]),
x,
&pool,
)
.unwrap();
assert_ne!(r.value, pool.integer(0_i32));
assert!(r.log.steps().iter().any(|s| s.rule_name == "diff_sin"));
assert!(r.log.steps().iter().any(|s| s.rule_name == "power_rule"));
assert!(!r
.log
.steps()
.iter()
.any(|s| s.rule_name == "diff_univariate_poly"));
let two_x = pool.mul(vec![pool.integer(2_i32), x]);
let expected = pool.mul(vec![
pool.func("cos", vec![pool.pow(x, pool.integer(2_i32))]),
two_x,
]);
assert_eq!(r.value, simplify(expected, &pool).value);
}
#[test]
fn diff_pow_n0() {
let pool = p();
let x = pool.symbol("x", Domain::Real);
let expr = pool.pow(x, pool.integer(0_i32));
let r = diff(expr, x, &pool).unwrap();
assert_eq!(r.value, pool.integer(0_i32));
assert!(r.log.steps().iter().any(|s| s.rule_name == "power_rule_n0"));
}
#[test]
fn diff_pow_n1() {
let pool = p();
let x = pool.symbol("x", Domain::Real);
let expr = pool.pow(x, pool.integer(1_i32));
let r = diff(expr, x, &pool).unwrap();
assert_eq!(r.value, pool.integer(1_i32));
assert!(r.log.steps().iter().any(|s| s.rule_name == "power_rule_n1"));
}
#[test]
fn diff_unknown_function_error() {
let pool = p();
let x = pool.symbol("x", Domain::Real);
let err = diff(pool.func("zeta", vec![x]), x, &pool);
assert!(matches!(err, Err(DiffError::UnknownFunction(_))));
}
#[test]
fn diff_non_integer_exponent_error() {
let pool = p();
let x = pool.symbol("x", Domain::Real);
let y = pool.symbol("y", Domain::Real);
let err = diff(pool.pow(x, y), x, &pool);
assert!(matches!(err, Err(DiffError::NonIntegerExponent)));
}
#[test]
fn diff_fractional_power() {
let pool = p();
let x = pool.symbol("x", Domain::Real);
let half = pool.pow(x, pool.rational(1_i32, 2_i32));
let d = diff(half, x, &pool).unwrap();
let expected = pool.mul(vec![
pool.rational(1_i32, 2_i32),
pool.pow(x, pool.rational(-1_i32, 2_i32)),
]);
assert_eq!(
simplify(d.value, &pool).value,
simplify(expected, &pool).value
);
let two_thirds = pool.pow(x, pool.rational(2_i32, 3_i32));
let d = diff(two_thirds, x, &pool).unwrap();
let expected = pool.mul(vec![
pool.rational(2_i32, 3_i32),
pool.pow(x, pool.rational(-1_i32, 3_i32)),
]);
assert_eq!(
simplify(d.value, &pool).value,
simplify(expected, &pool).value
);
}
#[test]
fn diff_fractional_power_chain_rule() {
let pool = p();
let x = pool.symbol("x", Domain::Real);
let base = pool.add(vec![pool.pow(x, pool.integer(2_i32)), pool.integer(1_i32)]);
let expr = pool.pow(base, pool.rational(3_i32, 2_i32));
let d = diff(expr, x, &pool).unwrap();
let expected = pool.mul(vec![
pool.integer(3_i32),
x,
pool.pow(base, pool.rational(1_i32, 2_i32)),
]);
assert_eq!(
simplify(d.value, &pool).value,
simplify(expected, &pool).value
);
}
#[test]
fn diff_balanced_geom_series_univariate_fastpath() {
fn balanced_sum(pool: &ExprPool, terms: &[ExprId]) -> ExprId {
match terms.len() {
0 => pool.integer(0_i32),
1 => terms[0],
_ => {
let mid = terms.len() / 2;
pool.add(vec![
balanced_sum(pool, &terms[..mid]),
balanced_sum(pool, &terms[mid..]),
])
}
}
}
let pool = p();
let x = pool.symbol("x", Domain::Real);
let n = 80i32;
let mut terms = vec![pool.integer(1_i32)];
for k in 1..=n {
terms.push(pool.pow(x, pool.integer(k)));
}
let expr = balanced_sum(&pool, &terms);
let r = diff(expr, x, &pool).unwrap();
assert!(
r.log
.steps()
.iter()
.any(|s| s.rule_name == "diff_univariate_poly"),
"expected dense ℤ-poly fast-path for balanced sum"
);
let poly = UniPoly::from_symbolic(r.value, x, &pool).unwrap();
assert_eq!(poly.degree(), i64::from(n) - 1);
let coeffs = poly.coefficients_i64();
assert_eq!(coeffs.first().copied(), Some(1));
assert_eq!(coeffs.last().copied(), Some(n as i64));
}
#[test]
fn diff_log_has_both_diff_and_simplify_steps() {
let pool = p();
let x = pool.symbol("x", Domain::Real);
let y = pool.symbol("y", Domain::Real);
let expr = pool.add(vec![
pool.pow(x, pool.integer(2_i32)),
y,
pool.integer(0_i32),
]);
let r = diff(expr, x, &pool).unwrap();
let rules: Vec<&str> = r.log.steps().iter().map(|s| s.rule_name).collect();
assert!(
rules.contains(&"sum_rule"),
"should have sum_rule: {rules:?}"
);
assert!(
rules.contains(&"power_rule"),
"x² term differentiates via the power rule: {rules:?}"
);
assert!(rules.len() > 1, "log should have multiple steps: {rules:?}");
}
fn elapsed_under(t0: std::time::Instant, secs: u64, what: &str) {
assert!(
t0.elapsed() < std::time::Duration::from_secs(secs),
"{what} took {:?}",
t0.elapsed()
);
}
#[test]
fn diff_nested_square_dag_is_linear() {
let pool = p();
let x = pool.symbol("x", Domain::Real);
let one = pool.integer(1_i32);
let build = |k: usize| {
let mut e = x;
for _ in 0..k {
e = pool.add(vec![pool.mul(vec![e, e]), one]);
}
e
};
let t0 = std::time::Instant::now();
let r = diff(build(40), x, &pool).unwrap();
elapsed_under(t0, 2, "diff of e ← e·e+1, k = 40");
let raw_steps = r
.log
.steps()
.iter()
.filter(|s| s.rule_name.starts_with("diff_") || s.rule_name.ends_with("_rule"))
.count();
assert!(
raw_steps <= 5 * 40 + 2,
"{raw_steps} diff steps for 81 nodes"
);
let e = build(5);
let r = diff(e, x, &pool).unwrap();
assert!(!r
.log
.steps()
.iter()
.any(|s| s.rule_name == "diff_univariate_poly"));
let got = UniPoly::from_symbolic(r.value, x, &pool).unwrap();
let want = UniPoly::from_symbolic(e, x, &pool).unwrap().derivative();
assert_eq!(got, want);
}
#[test]
fn diff_nested_registry_primitive_is_not_exponential() {
let pool = p();
let x = pool.symbol("x", Domain::Real);
let mut e = x;
for _ in 0..40 {
e = pool.func("tan", vec![e]);
}
let t0 = std::time::Instant::now();
let r = diff(e, x, &pool).unwrap();
elapsed_under(t0, 2, "diff of tan^40(x)");
let registry_steps = r
.log
.steps()
.iter()
.filter(|s| s.rule_name == "diff_primitive_registry")
.count();
assert_eq!(registry_steps, 40, "one step per distinct tan node");
let t1 = pool.func("tan", vec![x]);
let t2 = pool.func("tan", vec![t1]);
let d = diff(t2, x, &pool).unwrap().value;
let q = |u: ExprId| {
pool.mul(vec![
pool.func("sin", vec![u]),
pool.pow(pool.func("cos", vec![u]), pool.integer(-1_i32)),
])
};
let mut m = std::collections::HashMap::new();
m.insert(t1, q(x));
m.insert(t2, q(q(x)));
let d_sc = crate::kernel::subs::subs(d, &m, &pool);
let mut env = std::collections::HashMap::new();
env.insert(x, 0.3_f64);
let got = crate::eval_f64(d_sc, &pool, &env).unwrap();
let sec2 = |v: f64| 1.0 + v.tan() * v.tan();
let want = sec2(0.3_f64.tan()) * sec2(0.3);
assert!((got - want).abs() < 1e-12 * want.abs(), "{got} vs {want}");
}
#[test]
fn diff_power_of_sum_stays_factored() {
let pool = p();
let x = pool.symbol("x", Domain::Real);
let base = pool.add(vec![x, pool.integer(1_i32)]);
let r = diff(pool.pow(base, pool.integer(800_i32)), x, &pool).unwrap();
let expected = pool.mul(vec![
pool.integer(800_i32),
pool.pow(base, pool.integer(799_i32)),
]);
assert_eq!(r.value, simplify(expected, &pool).value);
let e20 = pool.pow(base, pool.integer(20_i32));
let d20 = diff(e20, x, &pool).unwrap().value;
assert_eq!(
UniPoly::from_symbolic(d20, x, &pool).unwrap(),
UniPoly::from_symbolic(e20, x, &pool).unwrap().derivative()
);
}
#[test]
fn diff_chebyshev_dag_is_polynomial() {
let pool = p();
let x = pool.symbol("x", Domain::Real);
let (two, m1) = (pool.integer(2_i32), pool.integer(-1_i32));
let (mut a, mut b) = (pool.integer(1_i32), x);
for _ in 1..40 {
let c = pool.add(vec![pool.mul(vec![two, x, b]), pool.mul(vec![m1, a])]);
a = b;
b = c;
}
let t0 = std::time::Instant::now();
let r = diff(b, x, &pool).unwrap();
elapsed_under(t0, 2, "diff of Chebyshev T_40");
assert!(r
.log
.steps()
.iter()
.any(|s| s.rule_name == "diff_univariate_poly"));
let d = UniPoly::from_symbolic(r.value, x, &pool).unwrap();
assert_eq!(d.degree(), 39);
assert_eq!(
d.eval_rational(&rug::Rational::from(1)),
rug::Rational::from(1600)
);
}
#[test]
fn diff_wide_sum_is_not_quadratic() {
let pool = p();
let x = pool.symbol("x", Domain::Real);
let n = 3000;
let terms: Vec<ExprId> = (0..n)
.map(|i| {
let a = pool.symbol(format!("a{i}"), Domain::Real);
pool.mul(vec![a, pool.pow(x, pool.integer(i as i64 + 1))])
})
.collect();
let t0 = std::time::Instant::now();
let r = diff(pool.add(terms), x, &pool).unwrap();
elapsed_under(t0, 10, "diff of Σ aᵢ·x^i, n = 3000");
let n_terms = pool.with(r.value, |d| match d {
ExprData::Add(a) => a.len(),
_ => 1,
});
assert_eq!(n_terms, n);
}
}