use rustc_hash::FxHashMap;
use smallvec::SmallVec;
use crate::base::arena::Arena;
use crate::base::node::{ExprId, ExprNode, SymbolId};
use crate::base::walk::Binder;
pub(crate) fn subs(arena: &mut Arena, expr: ExprId, old: ExprId, new: ExprId) -> ExprId {
if old == new {
return expr;
}
if expr == old {
return new;
}
if arena.node(expr).is_atom() {
return expr;
}
let map: FxHashMap<ExprId, ExprId> = std::iter::once((old, new)).collect();
subs_scoped(arena, expr, &map, true, None)
}
type Rewrite<'a> = (
ExprId,
ExprId,
&'a dyn Fn(&mut Arena, ExprId) -> Option<ExprId>,
);
pub(crate) fn subs_with_rewrite(
arena: &mut Arena,
expr: ExprId,
old: ExprId,
new: ExprId,
rewrite: &dyn Fn(&mut Arena, ExprId) -> Option<ExprId>,
) -> ExprId {
if old == new {
return expr;
}
if expr == old {
return new;
}
let map: FxHashMap<ExprId, ExprId> = std::iter::once((old, new)).collect();
subs_scoped(arena, expr, &map, true, Some((old, new, rewrite)))
}
pub(crate) fn subs_map(
arena: &mut Arena,
expr: ExprId,
replacements: &[(ExprId, ExprId)],
) -> ExprId {
if replacements.is_empty() {
return expr;
}
let map: FxHashMap<ExprId, ExprId> = replacements.iter().copied().collect();
if let Some(&new) = map.get(&expr) {
return new;
}
if arena.node(expr).is_atom() {
return expr;
}
subs_scoped(arena, expr, &map, true, None)
}
struct Scopes {
maps: Vec<FxHashMap<ExprId, ExprId>>,
index: FxHashMap<Vec<(ExprId, ExprId)>, u32>,
algebraic: bool,
}
impl Scopes {
fn new(map: &FxHashMap<ExprId, ExprId>, algebraic: bool) -> (Self, u32) {
let mut scopes = Scopes {
maps: vec![FxHashMap::default()],
index: FxHashMap::default(),
algebraic,
};
scopes.index.insert(Vec::new(), 0);
let mut pairs: Vec<(ExprId, ExprId)> = map.iter().map(|(&k, &v)| (k, v)).collect();
pairs.sort_unstable();
let top = scopes.intern(pairs);
(scopes, top)
}
fn may_act(&self, arena: &Arena, c: ExprId, k: ExprId) -> bool {
crate::base::walk::contains(arena, c, k)
|| (self.algebraic
&& crate::base::walk::free_symbols(arena, k)
.into_iter()
.all(|s| crate::base::walk::contains(arena, c, s)))
}
fn intern(&mut self, pairs: Vec<(ExprId, ExprId)>) -> u32 {
if let Some(&sc) = self.index.get(&pairs) {
return sc;
}
let sc = self.maps.len() as u32;
self.maps.push(pairs.iter().copied().collect());
self.index.insert(pairs, sc);
sc
}
fn enter(&mut self, arena: &mut Arena, sc: u32, b: &mut Binder) -> (u32, ExprId) {
let ExprNode::Symbol(var_sym) = *arena.node(b.var) else {
return (0, b.var);
};
let mut inner: Vec<(ExprId, ExprId)> = self.maps[sc as usize]
.iter()
.map(|(&k, &v)| (k, v))
.filter(|&(k, v)| {
(!crate::base::walk::has_free_symbol(arena, k, var_sym)
|| function_pattern(arena, k, v, b.var, var_sym))
&& b.scoped.iter().any(|&c| self.may_act(arena, c, k))
})
.collect();
if inner.is_empty() {
return (0, b.var);
}
let var = if brings(arena, &inner, var_sym) {
let fresh = fresh_bound_variable(arena, var_sym, &b.scoped, &inner);
rename_bound(arena, &mut b.scoped, &mut inner, b.var, var_sym, fresh);
fresh
} else {
b.var
};
inner.sort_unstable();
(self.intern(inner), var)
}
fn enter_slot(&mut self, arena: &mut Arena, sc: u32, b: &mut Binder) -> SlotPlan {
let ExprNode::Symbol(var_sym) = *arena.node(b.var) else {
return SlotPlan::rebuild(0, b.var);
};
let map = &self.maps[sc as usize];
let point = map.get(&b.var).copied().unwrap_or(b.var);
let mut inner: Vec<(ExprId, ExprId)> = map
.iter()
.map(|(&k, &v)| (k, v))
.filter(|&(k, _)| k != b.var && b.scoped.iter().any(|&c| self.may_act(arena, c, k)))
.collect();
let captured = brings(arena, &inner, var_sym);
if point == b.var && !captured {
inner.sort_unstable();
return SlotPlan::rebuild(self.intern(inner), b.var);
}
let mentions_var = inner
.iter()
.any(|&(k, _)| crate::base::walk::has_free_symbol(arena, k, var_sym));
if let ExprNode::Symbol(to_sym) = *arena.node(point)
&& !captured
&& !b
.scoped
.iter()
.any(|&c| crate::base::walk::has_free_symbol(arena, c, to_sym))
&& !inner
.iter()
.any(|&(_, v)| crate::base::walk::has_free_symbol(arena, v, to_sym))
&& (!mentions_var
|| !b
.scoped
.iter()
.chain(inner.iter().flat_map(|(k, v)| [k, v]))
.any(|&e| crate::base::walk::contains(arena, e, point)))
{
rename_bound(arena, &mut b.scoped, &mut inner, b.var, var_sym, point);
inner.sort_unstable();
return SlotPlan::rebuild(self.intern(inner), point);
}
let outer_mentions_var = b.outer.iter().any(|&o| {
crate::base::walk::has_free_symbol(arena, o, var_sym)
|| map.iter().any(|(&k, &v)| {
crate::base::walk::has_free_symbol(arena, v, var_sym)
&& crate::base::walk::contains(arena, o, k)
})
});
let var = if captured || outer_mentions_var {
let fresh = fresh_bound_variable(arena, var_sym, &b.scoped, &inner);
rename_bound(arena, &mut b.scoped, &mut inner, b.var, var_sym, fresh);
fresh
} else {
b.var
};
inner.sort_unstable();
SlotPlan {
inner: self.intern(inner),
var,
point: Some(point),
}
}
}
fn function_pattern(
arena: &Arena,
old: ExprId,
new: ExprId,
var: ExprId,
var_sym: SymbolId,
) -> bool {
let ExprNode::Apply(head, args) = arena.node(old) else {
return false;
};
arena.lib_fn(*head).is_none()
&& (args.contains(&var) || !crate::base::walk::has_free_symbol(arena, new, var_sym))
}
fn brings(arena: &Arena, inner: &[(ExprId, ExprId)], sym: SymbolId) -> bool {
inner.iter().any(|&(k, v)| {
crate::base::walk::has_free_symbol(arena, v, sym)
&& !crate::base::walk::has_free_symbol(arena, k, sym)
})
}
fn rename_bound(
arena: &mut Arena,
scoped: &mut SmallVec<[ExprId; 2]>,
inner: &mut Vec<(ExprId, ExprId)>,
var: ExprId,
var_sym: SymbolId,
to: ExprId,
) {
let mentioning: SmallVec<[(ExprId, ExprId); 2]> = inner
.iter()
.copied()
.filter(|&(k, _)| crate::base::walk::has_free_symbol(arena, k, var_sym))
.collect();
if mentioning.is_empty() {
inner.push((var, to));
return;
}
for c in scoped.iter_mut() {
*c = subs(arena, *c, var, to);
}
for (k, v) in mentioning {
let renamed = (subs(arena, k, var, to), subs(arena, v, var, to));
inner.push(renamed);
}
}
#[derive(Clone, Copy)]
struct SlotPlan {
inner: u32,
var: ExprId,
point: Option<ExprId>,
}
impl SlotPlan {
fn rebuild(inner: u32, var: ExprId) -> Self {
SlotPlan {
inner,
var,
point: None,
}
}
}
enum Entered {
Binder(Binder, u32, ExprId),
Slot(Binder, SlotPlan),
}
pub(crate) fn fresh_symbol(arena: &mut Arena, base: &str, avoid: &[ExprId]) -> ExprId {
let mut n = 0usize;
loop {
let name = if n == 0 {
base.to_owned()
} else {
format!("{base}_{n}")
};
match arena.symbols.get(&name) {
None => return arena.symbol(&name),
Some(sid)
if arena.symbol_assumptions(sid)
== crate::base::assumptions::Assumptions::default() =>
{
let candidate = arena.intern(ExprNode::Symbol(sid));
if !avoid
.iter()
.any(|&e| crate::base::walk::contains(arena, e, candidate))
{
return candidate;
}
}
Some(_) => {}
}
n += 1;
}
}
fn fresh_bound_variable(
arena: &mut Arena,
var: SymbolId,
scoped: &[ExprId],
replacements: &[(ExprId, ExprId)],
) -> ExprId {
let base = arena.symbol_name(var).to_owned();
let assumptions = arena.symbol_assumptions(var);
let mut n = 1usize;
loop {
let name = format!("{base}_{n}");
match arena.symbols.get(&name) {
None => {
let fresh = arena.symbol(&name);
if let ExprNode::Symbol(sid) = *arena.node(fresh)
&& assumptions != crate::base::assumptions::Assumptions::default()
{
arena.set_symbol_assumptions(sid, assumptions);
}
return fresh;
}
Some(sid) if arena.symbol_assumptions(sid) == assumptions => {
let candidate = arena.intern(ExprNode::Symbol(sid));
let in_use = scoped
.iter()
.chain(replacements.iter().flat_map(|(k, v)| [k, v]))
.any(|&e| crate::base::walk::contains(arena, e, candidate));
if !in_use {
return candidate;
}
}
Some(_) => {}
}
n += 1;
}
}
fn subs_scoped(
arena: &mut Arena,
expr: ExprId,
map: &FxHashMap<ExprId, ExprId>,
differentiate: bool,
rewrite: Option<Rewrite<'_>>,
) -> ExprId {
let (mut scopes, top) = Scopes::new(map, rewrite.is_some());
let rewritten = |arena: &mut Arena, scopes: &Scopes, sc: u32, r: ExprId| -> ExprId {
match rewrite {
Some((old, new, f)) if scopes.maps[sc as usize].get(&old) == Some(&new) => {
if r == old {
new
} else {
f(arena, r).unwrap_or(r)
}
}
_ => r,
}
};
if top == 0 {
return expr;
}
let mut cache: FxHashMap<(ExprId, u32), ExprId> = FxHashMap::default();
let mut entered: FxHashMap<(ExprId, u32), Entered> = FxHashMap::default();
let mut stack: Vec<(ExprId, u32, bool)> = vec![(expr, top, false)];
let mut children: SmallVec<[(ExprId, u32); 6]> = SmallVec::new();
while let Some(&(id, sc, expanded)) = stack.last() {
if cache.contains_key(&(id, sc)) {
stack.pop();
continue;
}
if !expanded {
let leaf = if let Some(&new) = scopes.maps[sc as usize].get(&id) {
Some(new)
} else if arena.node(id).is_atom() {
Some(rewritten(arena, &scopes, sc, id))
} else {
None
};
if let Some(result) = leaf {
cache.insert((id, sc), result);
stack.pop();
continue;
}
if let Some(top) = stack.last_mut() {
top.2 = true;
}
children.clear();
if let Some(mut b) = crate::base::walk::binder(arena, id) {
let (inner, var) = scopes.enter(arena, sc, &mut b);
children.extend(b.outer.iter().map(|&c| (c, sc)));
if inner != 0 {
children.extend(b.scoped.iter().map(|&c| (c, inner)));
}
entered.insert((id, sc), Entered::Binder(b, inner, var));
} else if let Some(mut b) = crate::base::walk::var_slot(arena, id) {
let plan = scopes.enter_slot(arena, sc, &mut b);
children.extend(b.outer.iter().map(|&c| (c, sc)));
if plan.inner != 0 {
children.extend(b.scoped.iter().map(|&c| (c, plan.inner)));
}
entered.insert((id, sc), Entered::Slot(b, plan));
} else {
arena.node(id).for_each_child(|c| children.push((c, sc)));
}
for &(c, csc) in children.iter().rev() {
if !cache.contains_key(&(c, csc)) {
stack.push((c, csc, false));
}
}
} else {
stack.pop();
let get = |c: ExprId, s: u32| cache.get(&(c, s)).copied().unwrap_or(c);
let operands = |b: &Binder, inner: u32| {
let scoped: SmallVec<[ExprId; 2]> =
b.scoped.iter().map(|&c| get(c, inner)).collect();
let outer: SmallVec<[ExprId; 2]> = b.outer.iter().map(|&c| get(c, sc)).collect();
(scoped, outer)
};
let rebuilt = match entered.get(&(id, sc)) {
Some(Entered::Binder(b, inner, var)) => {
let (scoped, outer) = operands(b, *inner);
if *var == b.var && scoped == b.scoped && outer == b.outer {
id
} else if let (ExprNode::ImageSet(..), &[body]) = (arena.node(id), &scoped[..])
{
crate::transforms::sets::image_set(arena, *var, body)
} else {
crate::base::walk::rebuild_binder(arena, id, *var, &scoped, &outer)
}
}
Some(Entered::Slot(b, plan)) => {
let (scoped, outer) = operands(b, plan.inner);
let node = if plan.var == b.var && scoped == b.scoped && outer == b.outer {
id
} else {
crate::base::walk::rebuild_var_slot(arena, id, plan.var, &scoped, &outer)
};
match plan.point {
None => node,
Some(point) => at_point(arena, node, plan.var, point, differentiate),
}
}
None => crate::base::walk::rebuild_with(arena, id, &|c| get(c, sc)),
};
let rebuilt = rewritten(arena, &scopes, sc, rebuilt);
cache.insert((id, sc), rebuilt);
}
}
cache.get(&(expr, top)).copied().unwrap_or(expr)
}
fn at_point(
arena: &mut Arena,
node: ExprId,
var: ExprId,
point: ExprId,
differentiate: bool,
) -> ExprId {
if differentiate
&& let ExprNode::Derivative(body, dvar) = *arena.node(node)
&& crate::base::walk::free_symbols(arena, body)
.iter()
.all(|&s| s == dvar)
{
let d = crate::transforms::diff::diff(arena, body, dvar);
if d != node {
let map: FxHashMap<ExprId, ExprId> = std::iter::once((var, point)).collect();
return subs_scoped(arena, d, &map, false, None);
}
}
crate::base::walk::subs_node(arena, node, var, point)
}
pub(crate) fn eval_derivatives(arena: &mut Arena, expr: ExprId) -> ExprId {
let post_order = crate::base::walk::post_order_ids(arena, expr);
let mut cache: FxHashMap<ExprId, ExprId> = FxHashMap::default();
for &id in &post_order {
let node = arena.node(id).clone();
match node {
ExprNode::Derivative(inner, var) => {
let new_inner = cache.get(&inner).copied().unwrap_or(inner);
let new_var = cache.get(&var).copied().unwrap_or(var);
let result = crate::transforms::diff::diff(arena, new_inner, new_var);
cache.insert(id, result);
}
ExprNode::Subs(body, var, point) => {
let new_body = cache.get(&body).copied().unwrap_or(body);
let new_point = cache.get(&point).copied().unwrap_or(point);
let result = subs(arena, new_body, var, new_point);
cache.insert(id, result);
}
_ => {
let new_id = crate::base::walk::rebuild_with_cache(arena, id, &cache);
cache.insert(id, new_id);
}
}
}
cache.get(&expr).copied().unwrap_or(expr)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::base::arena::Arena;
fn sym(a: &mut Arena, name: &str) -> ExprId {
a.symbol(name)
}
fn display(a: &Arena, id: ExprId) -> String {
a.display(id).to_string()
}
#[test]
fn subs_symbol_for_number() {
let mut a = Arena::new();
let x = sym(&mut a, "x");
let three = a.int(3);
let expr = a.add(&[x, a.one]);
let result = subs(&mut a, expr, x, three);
assert_eq!(display(&a, result), "4");
}
#[test]
fn subs_symbol_in_product() {
let mut a = Arena::new();
let x = sym(&mut a, "x");
let y = sym(&mut a, "y");
let two = a.int(2);
let expr = a.mul(&[two, x]);
let result = subs(&mut a, expr, x, y);
assert_eq!(display(&a, result), "2*y");
}
#[test]
fn subs_in_pow() {
let mut a = Arena::new();
let x = sym(&mut a, "x");
let two = a.int(2);
let expr = a.pow(x, two);
let three = a.int(3);
let result = subs(&mut a, expr, x, three);
assert_eq!(display(&a, result), "9");
}
#[test]
fn subs_in_sin() {
let mut a = Arena::new();
let x = sym(&mut a, "x");
let y = sym(&mut a, "y");
let expr = a.sin(x);
let result = subs(&mut a, expr, x, y);
assert_eq!(display(&a, result), "sin(y)");
}
#[test]
fn subs_no_match_returns_same_id() {
let mut a = Arena::new();
let x = sym(&mut a, "x");
let y = sym(&mut a, "y");
let expr = a.add(&[x, a.one]);
let n99 = a.int(99);
let result = subs(&mut a, expr, y, n99);
assert_eq!(result, expr, "no match should return same ExprId");
}
#[test]
fn subs_entire_expression() {
let mut a = Arena::new();
let x = sym(&mut a, "x");
let y = sym(&mut a, "y");
let result = subs(&mut a, x, x, y);
assert_eq!(result, y);
}
#[test]
fn subs_old_equals_new_is_noop() {
let mut a = Arena::new();
let x = sym(&mut a, "x");
let expr = a.add(&[x, a.one]);
let result = subs(&mut a, expr, x, x);
assert_eq!(result, expr);
}
#[test]
fn subs_in_nested_add_mul() {
let mut a = Arena::new();
let x = sym(&mut a, "x");
let two = a.int(2);
let three = a.int(3);
let x_sq = a.pow(x, two);
let two_x = a.mul(&[two, x]);
let expr = a.add(&[x_sq, two_x, a.one]);
let result = subs(&mut a, expr, x, three);
assert_eq!(display(&a, result), "16");
}
#[test]
fn subs_in_function_of_pow() {
let mut a = Arena::new();
let x = sym(&mut a, "x");
let y = sym(&mut a, "y");
let two = a.int(2);
let xp = a.pow(x, two);
let expr = a.sin(xp);
let result = subs(&mut a, expr, x, y);
assert_eq!(display(&a, result), "sin(y^2)");
}
#[test]
fn subs_replaces_all_occurrences() {
let mut a = Arena::new();
let x = sym(&mut a, "x");
let y = sym(&mut a, "y");
let expr = a.add(&[x, x]);
let result = subs(&mut a, expr, x, y);
assert_eq!(display(&a, result), "2*y");
}
#[test]
fn subs_does_not_match_algebraic_subexpressions() {
let mut a = Arena::new();
let x = sym(&mut a, "x");
let two = a.int(2);
let one = a.one;
let x_inv = a.pow(x, a.neg_one);
let x_sq = a.pow(x, two);
let result = subs(&mut a, x_inv, x_sq, one);
assert_eq!(
result, x_inv,
"structural subs should not match x^2 in x^(-1)"
);
}
#[test]
fn subs_with_zero_evaluates() {
let mut a = Arena::new();
let x = sym(&mut a, "x");
let y = sym(&mut a, "y");
let zero = a.zero;
let expr = a.mul(&[x, y]);
let result = subs(&mut a, expr, y, zero);
assert_eq!(result, a.zero);
}
#[test]
fn subs_map_simultaneous() {
let mut a = Arena::new();
let x = sym(&mut a, "x");
let y = sym(&mut a, "y");
let expr = a.add(&[x, y]);
let result = subs_map(&mut a, expr, &[(x, y), (y, x)]);
assert_eq!(result, expr, "swapping x↔y in x+y should give x+y");
}
#[test]
fn subs_map_empty_is_noop() {
let mut a = Arena::new();
let x = sym(&mut a, "x");
let expr = a.add(&[x, a.one]);
let result = subs_map(&mut a, expr, &[]);
assert_eq!(result, expr);
}
#[test]
fn subs_map_multiple() {
let mut a = Arena::new();
let x = sym(&mut a, "x");
let y = sym(&mut a, "y");
let two = a.int(2);
let three = a.int(3);
let expr = a.add(&[x, y]);
let result = subs_map(&mut a, expr, &[(x, two), (y, three)]);
assert_eq!(display(&a, result), "5");
}
#[test]
fn subs_deep_expression_no_stack_overflow() {
let mut a = Arena::new();
let x = sym(&mut a, "x");
let y = sym(&mut a, "y");
let mut expr = x;
for _ in 0..10_000 {
expr = a.sin(expr);
}
let result = subs(&mut a, expr, x, y);
assert_ne!(
result, expr,
"substitution should have changed the deep expression"
);
}
#[test]
fn subs_moderate_depth_display_works() {
let mut a = Arena::new();
let x = sym(&mut a, "x");
let y = sym(&mut a, "y");
let mut expr = x;
for _ in 0..50 {
expr = a.sin(expr);
}
let result = subs(&mut a, expr, x, y);
let s = format!("{}", a.display(result));
assert!(s.starts_with("sin("), "should still start with sin(");
assert!(s.contains('y'), "should contain y after substitution");
assert!(!s.contains('x'), "should not contain x after substitution");
}
#[test]
fn subs_leaves_every_binder_kind_alone() {
let mut a = Arena::new();
let x = sym(&mut a, "x");
let y = sym(&mut a, "y");
let three = a.int(3);
let body = a.mul(&[x, y]);
let five = a.int(5);
let x5 = a.pow(x, five);
let neg_x = a.neg(x);
let poly = a.add(&[x5, neg_x, a.one]);
let binders = [
a.intern(ExprNode::Sum(body, x, a.zero, three)),
a.intern(ExprNode::Product_(body, x, a.one, three)),
a.definite_integral(body, x, a.zero, three),
a.intern(ExprNode::RootSum(poly, body, x)),
a.intern(ExprNode::ConditionSet(x, body)),
a.intern(ExprNode::RootOf(poly, x, a.zero)),
a.intern(ExprNode::Limit(body, x, a.zero)),
a.intern(ExprNode::Residue(body, x, a.zero)),
a.intern(ExprNode::LaplaceTransform(body, x, y)),
a.intern(ExprNode::InverseLaplaceTransform(body, x, y)),
];
let half = a.rational(1, 2);
for e in binders {
assert_eq!(subs(&mut a, e, x, half), e, "{}", display(&a, e));
}
let s = a.intern(ExprNode::Sum(body, x, a.zero, three));
let got = subs(&mut a, s, y, half);
assert_eq!(display(&a, got), "Sum(1/2*x, x=0..3)");
}
#[test]
fn subs_renames_a_binder_that_would_capture() {
let mut a = Arena::new();
let (x, k, n) = (sym(&mut a, "x"), sym(&mut a, "k"), sym(&mut a, "n"));
let body = a.mul(&[x, k]);
let s = a.intern(ExprNode::Sum(body, k, a.zero, n));
let k1 = a.add(&[k, a.one]);
let got = subs(&mut a, s, x, k1);
assert_eq!(display(&a, got), "Sum(k_1*(k + 1), k_1=0..n)");
let k_1 = sym(&mut a, "k_1");
let body = a.mul(&[x, k, k_1]);
let s = a.intern(ExprNode::Sum(body, k, a.zero, n));
let got = subs(&mut a, s, x, k);
assert_eq!(display(&a, got), "Sum(k*k_1*k_2, k_2=0..n)");
}
#[test]
fn subs_nested_binders_of_the_same_variable() {
let mut a = Arena::new();
let (x, y) = (sym(&mut a, "x"), sym(&mut a, "y"));
let xy = a.mul(&[x, y]);
let inner = a.intern(ExprNode::Sum(xy, x, a.zero, x));
let body = a.add(&[x, inner]);
let outer = a.intern(ExprNode::Sum(body, x, a.zero, y));
let got = subs(&mut a, outer, y, x);
let free: Vec<String> = crate::base::walk::free_symbols(&a, got)
.into_iter()
.map(|s| display(&a, s))
.collect();
assert_eq!(free, vec!["x".to_string()], "{}", display(&a, got));
assert_eq!(
display(&a, got),
"Sum(x_1 + Sum(x*x_1, x_1=0..x_1), x_1=0..x)"
);
}
#[test]
fn eval_derivatives_sin_x() {
let mut a = Arena::new();
let x = sym(&mut a, "x");
let sin_x = a.sin(x);
let formal = a.intern(crate::base::node::ExprNode::Derivative(sin_x, x));
let result = super::eval_derivatives(&mut a, formal);
assert_eq!(display(&a, result), "cos(x)");
}
#[test]
fn eval_derivatives_nested() {
let mut a = Arena::new();
let x = sym(&mut a, "x");
let two = a.int(2);
let x2 = a.pow(x, two);
let d1 = a.intern(crate::base::node::ExprNode::Derivative(x2, x));
let d2 = a.intern(crate::base::node::ExprNode::Derivative(d1, x));
let result = super::eval_derivatives(&mut a, d2);
assert_eq!(display(&a, result), "2");
}
#[test]
fn eval_derivatives_no_derivative_unchanged() {
let mut a = Arena::new();
let x = sym(&mut a, "x");
let two = a.int(2);
let x2 = a.pow(x, two);
let expr = a.add(&[x2, a.one]);
let result = super::eval_derivatives(&mut a, expr);
assert_eq!(result, expr);
}
#[test]
fn eval_derivatives_inside_add() {
let mut a = Arena::new();
let x = sym(&mut a, "x");
let two = a.int(2);
let x2 = a.pow(x, two);
let d = a.intern(crate::base::node::ExprNode::Derivative(x2, x));
let expr = a.add(&[x, d]);
let result = super::eval_derivatives(&mut a, expr);
assert_eq!(display(&a, result), "3*x");
}
}