Introduction

Ask a computer algebra system for the derivative of x3sin⁡(x)x^3 \sin(x) and it answers instantly with 3x2sin⁡(x)+x3cos⁡(x)3x^2\sin(x) + x^3\cos(x) — not a decimal approximation near some point, but the exact formula, valid everywhere. How?

The trick is to stop thinking of a formula as text and start thinking of it as a tree. The expression x3sin⁡(x)x^3 \sin(x) is really a multiplication node with two children: a power node (x3x^3) and a sine node (sin⁡(x)\sin(x)). Every formula, no matter how nested, decomposes this way down to leaves that are just variables and constants.

Once you have a tree, differentiation stops being an art. It becomes a handful of mechanical rewriting rules — one for addition, one for multiplication, one for composition — applied recursively from the root down to the leaves. No calculus insight is needed at the moment of computing; all the insight was baked into the rules centuries ago by Newton, Leibniz and their successors. The machine just follows them, perfectly, every time.

Differentiate a Tree

Pick one of the sample functions below and press Differentiate to watch the algorithm walk the expression tree and rewrite each node using the matching rule.

<p class="hint">Pick a function below. Symbolic differentiation walks its expression tree from the root down, applying one fixed rule per node type — sum, product, quotient, power, or chain — and rebuilds an exact derivative tree, never a numeric approximation.</p>
<div class="row">
  <label for="fnSelect" class="sr-only">Choose a function</label>
  <select id="fnSelect"></select>
  <button id="diff" type="button">Differentiate</button>
  <button id="reset" type="button" class="ghost">Reset</button>
</div>
<div class="panels">
  <div class="panel">
    <div class="panel-title">Original function</div>
    <div id="original" class="expr"></div>
  </div>
  <div class="panel">
    <div class="panel-title">Derivative</div>
    <div id="derivative" class="expr placeholder">Press "Differentiate" to see the exact result.</div>
  </div>
</div>
<div class="panel-title">Rule log</div>
<div id="log" class="log"></div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; }
.hint { font-size: .9rem; color: #444; margin: 0 0 .7rem; line-height: 1.45; }
.sr-only { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); }
.row { display: flex; gap: .5rem; flex-wrap: wrap; align-items: center; margin-bottom: .7rem; }
select { font: 600 14px system-ui, sans-serif; padding: .4rem .6rem; border-radius: 8px; border: 1px solid #adb1b8; }
button { font: 600 14px system-ui, sans-serif; padding: .45rem .9rem; border: 1px solid #1d3557;
         background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
.panels { display: grid; grid-template-columns: 1fr 1fr; gap: .6rem; margin-bottom: .7rem; }
@media (max-width: 520px) { .panels { grid-template-columns: 1fr; } }
.panel-title { font-size: .78rem; font-weight: 700; text-transform: uppercase; letter-spacing: .04em;
               color: #6b7280; margin: 0 0 .3rem; }
.expr { font: 700 18px ui-monospace, monospace; background: #e8eef3; border: 1px solid #cdd9e3;
        border-radius: 8px; padding: .7rem .8rem; min-height: 1.6em; color: #1d3557; word-break: break-word; }
.expr.placeholder { color: #8a97a6; font-weight: 500; font-family: system-ui, sans-serif; font-size: .9rem; }
.log { font: 13px ui-monospace, monospace; background: #16223a; color: #d7e2f0; border-radius: 8px;
       padding: .6rem .7rem; min-height: 2.4em; max-height: 220px; overflow-y: auto; line-height: 1.55; }
.log .step { color: #8fd3a3; }
.log .rule { color: #ffd166; }
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));

// The expression is represented as an abstract syntax tree (AST), never as a text string.
// Node constructors: const/var are leaves; add/mul/div/pow/sin/cos are internal nodes with children.
const CONST = (v) => ({ t: 'const', v });
const VAR = () => ({ t: 'var' });
const ADD = (a, b) => ({ t: 'add', a, b });
const MUL = (a, b) => ({ t: 'mul', a, b });
const DIV = (a, b) => ({ t: 'div', a, b });
const POW = (a, n) => ({ t: 'pow', a, n });
const SIN = (a) => ({ t: 'sin', a });
const COS = (a) => ({ t: 'cos', a });

// A handful of built-in sample functions users can pick from.
const SAMPLES = [
  { key: 'sample_poly', build: () => ADD(POW(VAR(), 3), MUL(CONST(2), VAR())) },
  { key: 'sample_product', build: () => MUL(POW(VAR(), 2), SIN(VAR())) },
  { key: 'sample_chain', build: () => SIN(POW(VAR(), 2)) },
  { key: 'sample_quotient', build: () => DIV(VAR(), ADD(VAR(), CONST(1))) }
];

let stepCount = 0;
let logLines = [];

// Turn a tree back into a readable formula string (for display only, not used by derive()) .
function print(node) {
  switch (node.t) {
    case 'const': return String(node.v);
    case 'var': return 'x';
    case 'add': return '(' + print(node.a) + ' + ' + print(node.b) + ')';
    case 'mul': return '(' + print(node.a) + ' * ' + print(node.b) + ')';
    case 'div': return '(' + print(node.a) + ' / ' + print(node.b) + ')';
    case 'pow': return print(node.a) + '^' + node.n;
    case 'sin': return 'sin(' + print(node.a) + ')';
    case 'cos': return 'cos(' + print(node.a) + ')';
    default: return '?';
  }
}

// Light post-pass: fold 0/1 identities and constant-constant arithmetic so the result stays readable.
function simplify(node) {
  if (node.t === 'add') {
    const a = simplify(node.a), b = simplify(node.b);
    if (a.t === 'const' && a.v === 0) return b;
    if (b.t === 'const' && b.v === 0) return a;
    if (a.t === 'const' && b.t === 'const') return CONST(a.v + b.v);
    return ADD(a, b);
  }
  if (node.t === 'mul') {
    const a = simplify(node.a), b = simplify(node.b);
    if ((a.t === 'const' && a.v === 0) || (b.t === 'const' && b.v === 0)) return CONST(0);
    if (a.t === 'const' && a.v === 1) return b;
    if (b.t === 'const' && b.v === 1) return a;
    if (a.t === 'const' && b.t === 'const') return CONST(a.v * b.v);
    return MUL(a, b);
  }
  if (node.t === 'div') {
    const a = simplify(node.a), b = simplify(node.b);
    if (a.t === 'const' && a.v === 0) return CONST(0);
    return DIV(a, b);
  }
  if (node.t === 'pow') {
    const a = simplify(node.a);
    if (node.n === 1) return a;
    if (node.n === 0) return CONST(1);
    return POW(a, node.n);
  }
  if (node.t === 'sin' || node.t === 'cos') return { t: node.t, a: simplify(node.a) };
  return node;
}

// The heart of the algorithm: one case per node type, each recursing into its children first.
// d/dx of `node`, logging the rule used at each recursive step.
function derive(node, depth) {
  stepCount++;
  switch (node.t) {
    case 'const':
      log(depth, 'rule_const', print(node));
      return CONST(0);
    case 'var':
      log(depth, 'rule_var', print(node));
      return CONST(1);
    case 'add': {
      log(depth, 'rule_sum', print(node));
      const da = derive(node.a, depth + 1);
      const db = derive(node.b, depth + 1);
      return ADD(da, db);
    }
    case 'mul': {
      log(depth, 'rule_product', print(node));
      const da = derive(node.a, depth + 1);
      const db = derive(node.b, depth + 1);
      return ADD(MUL(da, node.b), MUL(node.a, db));
    }
    case 'div': {
      log(depth, 'rule_quotient', print(node));
      const da = derive(node.a, depth + 1);
      const db = derive(node.b, depth + 1);
      return DIV(ADD(MUL(da, node.b), MUL(CONST(-1), MUL(node.a, db))), POW(node.b, 2));
    }
    case 'pow': {
      log(depth, 'rule_power', print(node));
      const da = derive(node.a, depth + 1);
      return MUL(MUL(CONST(node.n), POW(node.a, node.n - 1)), da);
    }
    case 'sin': {
      log(depth, 'rule_chain_sin', print(node));
      const da = derive(node.a, depth + 1);
      return MUL(COS(node.a), da);
    }
    case 'cos': {
      log(depth, 'rule_chain_cos', print(node));
      const da = derive(node.a, depth + 1);
      return MUL(MUL(CONST(-1), SIN(node.a)), da);
    }
    default:
      return CONST(0);
  }
}

// One human-readable label per differentiation rule, shown in the log panel.
// One human-readable label per differentiation rule, shown in the log panel.
const RULE_LABEL = {
  rule_const: 'constant rule: d/dx[c] = 0 for',
  rule_var: 'variable rule: d/dx[x] = 1 for',
  rule_sum: 'sum rule: d/dx[u + v] = u’ + v’ for',
  rule_product: 'product rule: d/dx[u*v] = u’v + uv’ for',
  rule_quotient: 'quotient rule: d/dx[u/v] = (u’v - uv’)/v^2 for',
  rule_power: 'power rule: d/dx[u^n] = n*u^(n-1)*u’ for',
  rule_chain_sin: 'chain rule: d/dx[sin(u)] = cos(u)*u’ for',
  rule_chain_cos: 'chain rule: d/dx[cos(u)] = -sin(u)*u’ for'
};

function log(depth, ruleKey, exprText) {
  const indent = '  '.repeat(depth);
  const line = i18nFmt('%indent%- %rule% %expr%', { indent, rule: RULE_LABEL[ruleKey], expr: exprText });
  logLines.push(line);
}

// One human-readable label per sample function, shown in the dropdown.
// One human-readable label per sample function, shown in the dropdown.
const SAMPLE_LABEL = {
  sample_poly: 'x^3 + 2x',
  sample_product: 'x^2 * sin(x)',
  sample_chain: 'sin(x^2)',
  sample_quotient: 'x / (x + 1)'
};

const select = document.getElementById('fnSelect');
SAMPLES.forEach((s, i) => {
  const opt = document.createElement('option');
  opt.value = String(i);
  opt.textContent = SAMPLE_LABEL[s.key];
  select.appendChild(opt);
});

function renderOriginal() {
  const idx = Number(select.value);
  const tree = SAMPLES[idx].build();
  document.getElementById('original').textContent = 'f(x) = ' + print(tree);
  const dEl = document.getElementById('derivative');
  dEl.textContent = 'Press "Differentiate" to see the exact result.';
  dEl.classList.add('placeholder');
  document.getElementById('log').innerHTML = '';
}

function runDifferentiate() {
  const idx = Number(select.value);
  const tree = SAMPLES[idx].build();
  stepCount = 0;
  logLines = [];
  const raw = derive(tree, 0);
  const clean = simplify(raw);
  const dEl = document.getElementById('derivative');
  dEl.textContent = "f'(x) = " + print(clean);
  dEl.classList.remove('placeholder');
  document.getElementById('log').innerHTML =
    logLines.map((l) => '<div class="step">' + l + '</div>').join('') +
    '<div class="rule">' + i18nFmt('Done — %n% recursive step(s), one rule application per tree node.', { n: stepCount }) + '</div>';
  console.log('Symbolic derivative computed:', print(tree), '->', print(clean));
}

document.getElementById('diff').addEventListener('click', runDifferentiate);
document.getElementById('reset').addEventListener('click', renderOriginal);
select.addEventListener('change', renderOriginal);
renderOriginal();
Result
Console

The log panel shows every rule firing in order: a product becomes u′v+uv′u'v + uv', a quotient becomes u′v−uv′v2\frac{u'v - uv'}{v^2}, and a composition f(g(x))f(g(x)) becomes f′(g(x))⋅g′(x)f'(g(x)) \cdot g'(x) — the chain rule. Nothing is evaluated at a numeric point; the output is a brand-new tree that represents the derivative as a formula for every xx at once.

The Real Complexity

Differentiating one node is O(1)O(1) work: look up which rule applies (sum, product, quotient, power, chain) and build the corresponding new nodes from the derivatives of the children. Since every node is visited exactly once, differentiating a tree with nn nodes takes O(n)O(n) time — differentiation itself is cheap, always, no matter how deep the nesting.

The catch is what comes out the other end. Each rule can more than double the size of a subtree: the product rule alone turns one node into a sum of two products, and if that pattern repeats at every level of a deeply nested expression, the un-simplified derivative can grow exponentially in the depth of the original formula. This is the well-known expression swell problem — the rules are simple, but naively applying them over and over produces monstrously large formulas full of redundant subexpressions.

In practice every computer algebra system fights this with simplification and common-subexpression sharing: rewriting x+0→xx + 0 \to x, 1⋅y→y1 \cdot y \to y, merging identical subtrees, and sometimes representing the expression as a directed acyclic graph instead of a tree so repeated pieces are stored once. Differentiation is the easy half of the job; taming the resulting formula so it stays human-readable — and so a later evaluation of it is fast — is the half that takes engineering. This is also why heavy numerical pipelines like neural network training prefer automatic differentiation: it accumulates derivative values through the same chain rule without ever materializing the blown-up symbolic formula.

Where It Matters

Whenever the formula itself is the deliverable — not just a number at a point — symbolic differentiation is the tool of choice:

  • Computer algebra systems: Mathematica, Maple, SymPy and Wolfram|Alpha all use expression-tree differentiation to solve calculus problems the same way a textbook would show its work.
  • Exact physics and engineering derivations: deriving equations of motion, Jacobians of robot kinematics, or sensitivity formulas in control theory often needs a closed-form expression, not just its value at one configuration.
  • Symbolic optimization and simplification: some solvers manipulate the derivative formula further (setting it to zero, factoring it) — something only possible if it exists as a real expression, not a black-box gradient evaluator.
  • Teaching and verification: symbolic differentiation is the natural way to check that a hand-derived formula or a numerical gradient approximation is actually correct.

Anywhere a human eventually needs to read the derivative — a textbook, a paper, a control law — this tree-rewriting approach is doing the work behind the scenes.

Conclusion

Symbolic differentiation turns one of the first hard things you learn in calculus into a small, mechanical, always-correct recursion: look at the root of the tree, apply the matching rule, recurse into the children. There is no approximation and no guesswork — just Newton and Leibniz's rules, executed with perfect discipline.

The cost of that perfection is size: applied naively, the exact answer can balloon far faster than the question that produced it. Every real computer algebra system is, underneath, this same tree-rewriting idea plus a constant fight against its own output — a reminder that being exact and being efficient are not always the same problem, a tension that also shapes how P vs NP separates easy answers from easy-to-check ones.

Share this article

Pick a channel — or use your device's native share sheet.

Comments

Loading comments...

https://www.kipuhub.com/en/article/symbolic-differentiation/Content licensed under CC BY-NC 4.0.