296 lines
13 KiB
Markdown
296 lines
13 KiB
Markdown
# 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`:
|
||
|
||
```rust
|
||
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, so `x^2 + x^2` shares the same `x^2` node. Cloning is O(1).
|
||
- **Canonical ordering**: `Add` and `Mul` children 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`. Comparing `x == x` is a single integer comparison, not a string comparison.
|
||
- **Flattened n-ary operators**: `a + b + c` is `Add([a, b, c])`, not `Add(Add(a, b), c)`. This simplifies pattern matching and avoids arbitrary associativity choices.
|
||
- **Exact rational arithmetic**: Core operations use `Rational64` (from `num-rational`) to avoid floating-point error. `1/3 + 1/3 + 1/3` is exactly `1`, not `0.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](https://egraphs-good.github.io/) crate for equality saturation — a technique that explores all equivalent forms of an expression simultaneously, then extracts the "best" one.
|
||
|
||
### How It Works
|
||
|
||
1. **Insert** the expression into an e-graph
|
||
2. **Apply rewrite rules** (30+ rules covering algebra, trig, logarithms, exponents)
|
||
3. **Saturate** until no new equalities are discovered (or a node/time limit is hit)
|
||
4. **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:
|
||
|
||
1. **Linearity**: `∫(a·f + b·g) dx = a·∫f dx + b·∫g dx`
|
||
2. **Power rule**: `∫x^n dx = x^(n+1)/(n+1)` for n ≠ -1
|
||
3. **Known antiderivatives**: sin, cos, exp, ln, tan, sec², etc.
|
||
4. **U-substitution**: Detect `∫f(g(x))·g'(x) dx` patterns
|
||
5. **Integration by parts**: `∫u dv = u·v - ∫v du` with LIATE heuristic for choosing u
|
||
6. **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`:
|
||
|
||
1. **Polynomial detection**: Extract coefficients of `x^0, x^1, x^2, ...`
|
||
2. **Linear**: `ax + b = 0` → `x = -b/a`
|
||
3. **Quadratic**: `ax² + bx + c = 0` → Exact quadratic formula with rational simplification
|
||
4. **Higher polynomial**: Rational root theorem to find integer/rational roots, then synthetic division
|
||
5. **Transcendental**: Pattern matching for forms like `e^x = k`, `sin(x) = k`
|
||
6. **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) and `result.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:
|
||
|
||
1. User specifies an expression and parameter ranges (e.g., `a*sin(b*x)` with `a ∈ [0,5]`, `b ∈ [1,10]`)
|
||
2. Canvas renders sliders for each parameter
|
||
3. The WASM module evaluates the expression at 200+ points per slider change
|
||
4. 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 |
|