13 KiB
SymClaw Architecture
This document describes the internal architecture of SymClaw for contributors and anyone curious about how a modern symbolic math engine works.
System Overview
User Input
│
┌────────────┼────────────┐
▼ ▼ ▼
┌────────┐ ┌─────────┐ ┌──────────┐
│ CLI │ │ WASM │ │ OpenClaw │
│ REPL │ │ Browser │ │ Skill │
└───┬────┘ └────┬────┘ └────┬─────┘
│ │ │
└────────────┼────────────┘
▼
┌─────────────────┐
│ Action Router │
│ (symclaw-skill)│
└────────┬────────┘
▼
┌────────────────────────────────┐
│ symclaw-core │
│ │
│ ┌────────┐ ┌───────────┐ │
│ │ Parser │───▶│ AST │ │
│ │ (nom) │ │ Arc<Expr> │ │
│ └────────┘ └─────┬─────┘ │
│ │ │
│ ┌────────────┼───────┐ │
│ ▼ ▼ ▼ │
│ ┌──────────┐ ┌────────┐ ┌──┴──────┐
│ │ Simplify │ │E-graph │ │ Diff │
│ │(pipeline)│ │ (egg) │ │ Integ │
│ └──────────┘ └────────┘ │ Solve │
│ │ Series │
│ │ Eval │
│ └─────────┘
│ │
│ ┌──────────────────────┘
│ ▼
│ ┌──────────┐
│ │ LaTeX │
│ │ Renderer │
│ └──────────┘
└────────────────────────────────┘
Expression AST
All symbolic expressions are represented as Arc<Expr>, defined in ast.rs:
pub enum Expr {
Num(Rational64), // Exact rational: 3/4, -7, 0
Float(f64), // IEEE 754 when exactness isn't needed
Sym(Symbol), // Interned symbol (fast equality)
Add(Vec<Arc<Expr>>), // Flattened sum: a + b + c
Mul(Vec<Arc<Expr>>), // Flattened product: a * b * c
Pow(Arc<Expr>, Arc<Expr>), // Exponentiation: base^exp
Func(Symbol, Vec<Arc<Expr>>), // Named function: sin(x), ln(x+1)
Neg(Arc<Expr>), // Unary negation
Inv(Arc<Expr>), // Multiplicative inverse: 1/x
}
Design Principles
Arc-based sharing: Subtrees are reference-counted, sox^2 + x^2shares the samex^2node. Cloning is O(1).- Canonical ordering:
AddandMulchildren are sorted by a deterministic ordering (symbols alphabetically, numbers first). This makes structural equality reliable. - Interned symbols: Variable names are stored as integer IDs via
string-interner. Comparingx == xis a single integer comparison, not a string comparison. - Flattened n-ary operators:
a + b + cisAdd([a, b, c]), notAdd(Add(a, b), c). This simplifies pattern matching and avoids arbitrary associativity choices. - Exact rational arithmetic: Core operations use
Rational64(fromnum-rational) to avoid floating-point error.1/3 + 1/3 + 1/3is exactly1, not0.9999....
Simplification Pipeline
Simplification runs in simplify.rs as a multi-pass pipeline. Each pass is a pure function Arc<Expr> → Arc<Expr>:
Input Expression
│
▼
┌──────────────┐
│ Constant Fold│ 2 + 3 → 5, sin(0) → 0
└──────┬───────┘
▼
┌──────────────┐
│ Identity │ x + 0 → x, x * 1 → x, x^1 → x
│ Elimination │
└──────┬───────┘
▼
┌──────────────┐
│ Like-Term │ 2x + 3x → 5x, x·x → x²
│ Collection │
└──────┬───────┘
▼
┌──────────────┐
│ Canonical │ Sort children, flatten nested Add/Mul
│ Form │
└──────┬───────┘
▼
┌──────────────┐
│ E-graph │ Optional: equality saturation for
│ Saturation │ deep algebraic identities
└──────┬───────┘
▼
Simplified Expression
The pipeline iterates until a fixed point (expression stops changing) or a maximum iteration count is reached. In practice, 2-3 passes suffice for most expressions.
E-graph Integration
SymClaw uses the egg crate for equality saturation — a technique that explores all equivalent forms of an expression simultaneously, then extracts the "best" one.
How It Works
- Insert the expression into an e-graph
- Apply rewrite rules (30+ rules covering algebra, trig, logarithms, exponents)
- Saturate until no new equalities are discovered (or a node/time limit is hit)
- Extract the smallest equivalent expression by AST cost
Rewrite Rules (Sample)
// Algebra
a + 0 ⟶ a
a * 1 ⟶ a
a * 0 ⟶ 0
a - a ⟶ 0
a / a ⟶ 1 (a ≠ 0)
a^0 ⟶ 1
a^1 ⟶ a
(a^m)^n ⟶ a^(m*n)
// Trig
sin(x)^2 + cos(x)^2 ⟶ 1
sin(0) ⟶ 0
cos(0) ⟶ 1
// Logarithms
ln(e^x) ⟶ x
e^(ln(x)) ⟶ x (x > 0)
ln(a*b) ⟶ ln(a) + ln(b) (a,b > 0)
ln(a/b) ⟶ ln(a) - ln(b) (a,b > 0)
Explain Mode
explain_equivalence(a, b) returns a step-by-step proof of why two expressions are equal, listing which rewrite rules were applied at each step. Used for educational output and debugging.
Differentiation
differentiate.rs implements symbolic differentiation via recursive structural rules:
| Expression | Derivative (w.r.t. x) |
|---|---|
c (constant) |
0 |
x |
1 |
f + g |
f' + g' |
f · g |
f'·g + f·g' (product rule) |
f^n (n const) |
n·f^(n-1)·f' (chain rule) |
f^g (general) |
f^g·(g'·ln(f) + g·f'/f) |
sin(f) |
cos(f)·f' |
cos(f) |
-sin(f)·f' |
exp(f) |
exp(f)·f' |
ln(f) |
f'/f |
Higher-order: differentiate_n(expr, var, n) applies differentiation n times.
Partial derivatives: Differentiate with respect to any variable; other variables are treated as constants.
The result is always passed through simplify() to reduce to canonical form.
Integration
integrate.rs implements symbolic integration via pattern matching with fallback strategies:
- Linearity:
∫(a·f + b·g) dx = a·∫f dx + b·∫g dx - Power rule:
∫x^n dx = x^(n+1)/(n+1)for n ≠ -1 - Known antiderivatives: sin, cos, exp, ln, tan, sec², etc.
- U-substitution: Detect
∫f(g(x))·g'(x) dxpatterns - Integration by parts:
∫u dv = u·v - ∫v duwith LIATE heuristic for choosing u - Definite integrals: Evaluate antiderivative at bounds (Fundamental Theorem)
Returns Option<Arc<Expr>> — None when no closed-form antiderivative is found.
Equation Solver
solve.rs solves equations of the form expr = 0:
- Polynomial detection: Extract coefficients of
x^0, x^1, x^2, ... - Linear:
ax + b = 0→x = -b/a - Quadratic:
ax² + bx + c = 0→ Exact quadratic formula with rational simplification - Higher polynomial: Rational root theorem to find integer/rational roots, then synthetic division
- Transcendental: Pattern matching for forms like
e^x = k,sin(x) = k - Systems (2×2): Gaussian elimination for pairs of linear equations
All solutions are returned as Vec<Arc<Expr>> in simplified form.
Taylor Series
series.rs computes Taylor expansions f(x) ≈ Σ f^(n)(a)/n! · (x-a)^n:
Fast Path
For known functions (sin, cos, exp, ln, sinh, cosh), series coefficients are computed directly from known formulas without repeated differentiation. This is O(n) instead of O(n²).
General Path
For arbitrary expressions, the engine differentiates n times at the expansion point, evaluating each derivative numerically or symbolically. The coefficients are assembled into a polynomial.
Maclaurin
When point = 0, this is a Maclaurin series. The fast path handles the most common cases.
WASM Compilation
symclaw-wasm compiles symclaw-core to WebAssembly via wasm-bindgen:
- No std dependencies: Core engine uses only
alloc, making WASM compilation clean - 12 exported functions:
parse,simplify,differentiate,integrate,solve,taylor,eval_expr,to_latex,simplify_egraph,explain,plot_data,version - String-based API: WASM boundary passes strings (JSON for complex types)
- Size: ~800 KB gzipped
The WASM module runs entirely client-side — no server roundtrips for computation.
OpenClaw Skill Protocol
symclaw-skill implements the OpenClaw skill protocol:
- Transport: JSON-RPC over stdin/stdout (one JSON object per line)
- Actions: 8 tool endpoints (
simplify,differentiate,integrate,solve,taylor,eval,latex,plot_data) - Canvas: Two HTML canvases —
manipulate.html(interactive plotting with sliders) andresult.html(formatted result display)
Request/Response Flow
OpenClaw Gateway
│
│ {"action": "differentiate", "expr": "sin(x^2)", "var": "x"}
▼
symclaw-skill (stdin)
│
│ parse → differentiate → simplify → latex
│
▼
symclaw-skill (stdout)
│
│ {"success": true, "result": "2*x*cos(x^2)", "latex": "2 x \\cos(x^{2})"}
▼
OpenClaw Gateway → User's channel
Canvas Manipulate
The manipulate.html canvas provides Mathematica-style interactive exploration:
- User specifies an expression and parameter ranges (e.g.,
a*sin(b*x)witha ∈ [0,5],b ∈ [1,10]) - Canvas renders sliders for each parameter
- The WASM module evaluates the expression at 200+ points per slider change
- A canvas-based plot updates in real time (~60 fps)
All computation happens client-side via the WASM module — no server calls during interaction.
Performance Characteristics
| Component | Complexity | Notes |
|---|---|---|
| Parse | O(n) | n = input length, nom zero-copy |
| Simplify (algebraic) | O(n log n) | Sorting + single-pass rewrites |
| E-graph saturation | O(rules × nodes) | Bounded by iteration/node limits |
| Differentiate | O(n) | n = AST nodes, single structural pass |
| Integrate | O(n²) worst | Pattern matching + recursive attempts |
| Solve (quadratic) | O(1) | Direct formula |
| Taylor (fast path) | O(order) | Known function coefficients |
| Taylor (general) | O(order²) | Repeated differentiation |
Memory: All expressions are Arc-shared. A deeply nested expression with shared subterms uses far less memory than its unfolded size.
Dependencies
| Crate | Purpose | Why |
|---|---|---|
egg |
E-graph equality saturation | Best-in-class Rust e-graph library |
nom |
Parser combinators | Zero-copy, composable, fast |
num-rational |
Exact rational arithmetic | Avoid floating-point errors |
num-bigint |
Big integer support | Overflow-safe coefficients |
string-interner |
Symbol interning | O(1) symbol comparison |
ordered-float |
Hashable f64 | For numeric expression keys |
serde / serde_json |
Serialization | Skill protocol + WASM boundary |
ahash |
Fast hashing | Expression caching |
lru |
LRU cache | Memoize expensive operations |