Expression Trees
Simplification
Mathematical Expressions
Computer Science
Algorithm Optimization

Simplifying expression trees

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

An expression tree is a binary tree where internal nodes are operators (+, -, *, /) and leaf nodes are operands (numbers or variables). Simplifying an expression tree means applying algebraic rules to reduce it to a simpler equivalent form — constant folding (computing 3 + 58), identity removal (eliminating x * 1x), and algebraic rewrites (reducing x - x0). This is a core optimization in compilers, computer algebra systems, and symbolic math libraries.

Expression Tree Representation

python
1class Expr:
2    pass
3
4class Num(Expr):
5    def __init__(self, value):
6        self.value = value
7    def __repr__(self):
8        return str(self.value)
9
10class Var(Expr):
11    def __init__(self, name):
12        self.name = name
13    def __repr__(self):
14        return self.name
15
16class BinOp(Expr):
17    def __init__(self, op, left, right):
18        self.op = op
19        self.left = left
20        self.right = right
21    def __repr__(self):
22        return f"({self.left} {self.op} {self.right})"

The expression (3 + x) * 2 is represented as:

 
1       *
2      / \
3     +   2
4    / \
5   3   x
python
tree = BinOp('*', BinOp('+', Num(3), Var('x')), Num(2))
print(tree)  # ((3 + x) * 2)

Simplification Rules

The core algebraic identities to apply:

RuleBeforeAfter
Constant folding3 + 58
Additive identityx + 0 or 0 + xx
Multiplicative identityx * 1 or 1 * xx
Multiplication by zerox * 0 or 0 * x0
Self-subtractionx - x0
Self-divisionx / x1
Division identityx / 1x
Double negation--xx
Subtraction of zerox - 0x

Simplification Algorithm

Apply rules recursively, bottom-up (simplify children first, then apply rules to the result):

python
1def simplify(node):
2    if isinstance(node, (Num, Var)):
3        return node
4
5    # Recursively simplify children first
6    left = simplify(node.left)
7    right = simplify(node.right)
8
9    # Constant folding — both children are numbers
10    if isinstance(left, Num) and isinstance(right, Num):
11        if node.op == '+': return Num(left.value + right.value)
12        if node.op == '-': return Num(left.value - right.value)
13        if node.op == '*': return Num(left.value * right.value)
14        if node.op == '/' and right.value != 0:
15            return Num(left.value / right.value)
16
17    # Identity rules for addition
18    if node.op == '+':
19        if isinstance(left, Num) and left.value == 0: return right    # 0 + x → x
20        if isinstance(right, Num) and right.value == 0: return left   # x + 0 → x
21
22    # Identity rules for subtraction
23    if node.op == '-':
24        if isinstance(right, Num) and right.value == 0: return left   # x - 0 → x
25        if equal(left, right): return Num(0)                          # x - x → 0
26
27    # Identity rules for multiplication
28    if node.op == '*':
29        if isinstance(left, Num) and left.value == 0: return Num(0)   # 0 * x → 0
30        if isinstance(right, Num) and right.value == 0: return Num(0) # x * 0 → 0
31        if isinstance(left, Num) and left.value == 1: return right    # 1 * x → x
32        if isinstance(right, Num) and right.value == 1: return left   # x * 1 → x
33
34    # Identity rules for division
35    if node.op == '/':
36        if isinstance(right, Num) and right.value == 1: return left   # x / 1 → x
37        if equal(left, right): return Num(1)                          # x / x → 1
38
39    return BinOp(node.op, left, right)
40
41def equal(a, b):
42    """Structural equality check."""
43    if isinstance(a, Num) and isinstance(b, Num):
44        return a.value == b.value
45    if isinstance(a, Var) and isinstance(b, Var):
46        return a.name == b.name
47    if isinstance(a, BinOp) and isinstance(b, BinOp):
48        return a.op == b.op and equal(a.left, b.left) and equal(a.right, b.right)
49    return False

Examples

python
1# Constant folding: (3 + 5) * 2 → 16
2tree = BinOp('*', BinOp('+', Num(3), Num(5)), Num(2))
3print(simplify(tree))  # 16
4
5# Identity removal: x * 1 + 0 → x
6tree = BinOp('+', BinOp('*', Var('x'), Num(1)), Num(0))
7print(simplify(tree))  # x
8
9# Self-subtraction: (a + b) - (a + b) → 0
10tree = BinOp('-',
11    BinOp('+', Var('a'), Var('b')),
12    BinOp('+', Var('a'), Var('b')))
13print(simplify(tree))  # 0
14
15# Mixed: (x * 0) + (y * 1) → y
16tree = BinOp('+', BinOp('*', Var('x'), Num(0)), BinOp('*', Var('y'), Num(1)))
17print(simplify(tree))  # y

Multi-Pass Simplification

One pass may not fully simplify an expression. Apply simplification repeatedly until the tree stops changing:

python
1def fully_simplify(node):
2    prev = None
3    current = node
4    while not equal(prev, current) if prev else True:
5        prev = current
6        current = simplify(current)
7    return current
8
9# (x + 0) * 1 → first pass: x * 1 → second pass: x
10tree = BinOp('*', BinOp('+', Var('x'), Num(0)), Num(1))
11print(fully_simplify(tree))  # x

Compiler Optimization Context

Compilers apply expression tree simplification as part of their optimization passes:

 
1Source code: y = (x * 1) + (0 * z) - 0
2Parse tree:  Sub(Add(Mul(x, 1), Mul(0, z)), 0)
3
4After simplification:
5  Mul(x, 1) → x
6  Mul(0, z)0
7  Add(x, 0) → x
8  Sub(x, 0) → x
9
10Optimized: y = x    (single register copy instead of 3 operations)

GCC and LLVM perform these optimizations on their intermediate representations (IR), eliminating redundant arithmetic instructions.

C# / LINQ Expression Trees

In C#, System.Linq.Expressions provides expression trees for runtime code generation:

csharp
1using System.Linq.Expressions;
2
3// Build: x => x * 1 + 0
4var param = Expression.Parameter(typeof(double), "x");
5var body = Expression.Add(
6    Expression.Multiply(param, Expression.Constant(1.0)),
7    Expression.Constant(0.0));
8var expr = Expression.Lambda<Func<double, double>>(body, param);
9
10// The C# compiler does not simplify expression trees automatically.
11// Libraries like MetaLinq or custom visitors can simplify them.

Common Pitfalls

  • Floating-point precision: x - x is not always exactly 0 for floats. 0.1 + 0.2 - 0.3 ≈ 5.5e-17, not 0. Use tolerance-based comparison for constant folding with floats.
  • Division by zero: x / x → 1 is only valid when x ≠ 0. A robust simplifier must track constraints or leave x / x unsimplified when x could be zero.
  • Non-commutative operations: Subtraction and division are not commutative. a - b ≠ b - a. Do not apply commutativity rules to these operators.
  • Infinite loops: Without a convergence check, simplification can oscillate between equivalent forms. The fully_simplify approach with structural equality prevents this.
  • Missing rules: A minimal simplifier only handles basic identities. Real computer algebra systems (SymPy, Mathematica) apply hundreds of rules including distribution, factoring, trigonometric identities, and logarithm laws.

Summary

  • Expression trees represent mathematical expressions as binary trees (operators at internal nodes, operands at leaves)
  • Simplification applies algebraic rules bottom-up: constant folding, identity removal, zero multiplication
  • Use recursive simplification on children first, then apply rules to the parent node
  • Multiple passes may be needed — repeat until the tree stops changing
  • Structural equality checks determine when two subtrees are identical
  • Compilers use these techniques to eliminate redundant arithmetic in generated code

Course illustration
Course illustration

All Rights Reserved.