Expressions
Topic:
core.expressions
The Expression type is the foundation of MathHook. Expressions are represented as an enum with variants for different mathematical constructs including numbers, variables, operations, functions, constants, matrices, and relations.
Expressions
The Expression type is the foundation of MathHook. This chapter explains expression structure, creation, and manipulation.
Expression Structure
Expressions in MathHook are represented as an enum with variants for different mathematical constructs:
#![allow(unused)] fn main() { pub enum Expression { // Numbers Integer(i64), Rational(Box<RationalData>), Float(f64), Complex(Box<ComplexData>), // Variables Symbol(Symbol), // Operations Add(Vec<Expression>), Mul(Vec<Expression>), Pow(Box<Expression>, Box<Expression>), // Functions Function(String, Vec<Expression>), // Constants Constant(ConstantType), // Matrices Matrix(Vec<Vec<Expression>>), // Relations Equation(Box<Expression>, Box<Expression>), // Other variants... } }
Creating Expressions
Using Macros (Recommended)
The expr!() macro provides full mathematical syntax support:
#![allow(unused)] fn main() { use mathhook::prelude::*; let x = symbol!(x); // Basic arithmetic let sum = expr!(x + y); let product = expr!(x * y); let difference = expr!(x - y); // Becomes x + (-1)*y let quotient = expr!(x / y); // Becomes x * y^(-1) // Power operations - three equivalent syntaxes let power1 = expr!(x ^ 2); // Caret notation (math-style) let power2 = expr!(x ** 2); // Double-star (Python-style) let power3 = expr!(x.pow(2)); // Method call // Mathematical precedence (^ binds tighter than * and /) let quadratic = expr!(a * x ^ 2 + b * x + c); // Correctly parsed // Comparison operators let eq = expr!(x == y); // Equality let lt = expr!(x < y); // Less than let gt = expr!(x > y); // Greater than let le = expr!(x <= y); // Less or equal let ge = expr!(x >= y); // Greater or equal // Method calls let abs_val = expr!(x.abs()); // Absolute value let sqrt_val = expr!(x.sqrt()); // Square root let simplified = expr!(x.simplify()); // Simplify expression // Function calls let sin_val = expr!(sin(x)); // Unary function let log_val = expr!(log(x, y)); // Binary function // Complex nested expressions let complex = expr!(sin(x ^ 2) + cos(y ^ 2)); let expanded = expr!((x + 1) * (x - 1)); }
Using Constructors
For runtime values or when macros aren't suitable:
#![allow(unused)] fn main() { use mathhook::prelude::*; let x = symbol!(x); // Direct constructors let sum = Expression::add(vec![x.clone(), Expression::integer(1)]); let product = Expression::mul(vec![x.clone(), Expression::integer(2)]); let power = Expression::pow(x.clone(), Expression::integer(2)); // Use for runtime variables (NOT expr! macro) for i in 0..10 { let term = Expression::integer(i); // CORRECT // NOT: expr!(i) - this creates symbol "i" } }
Expression Properties
Immutability
Expressions are immutable after creation. All operations return new expressions:
#![allow(unused)] fn main() { let expr = expr!(x + 1); let doubled = expr.mul(&expr!(2)); // Returns new expression // `expr` is unchanged }
Memory Efficiency
Expressions are designed to be 32 bytes to fit in CPU cache lines for optimal performance.
Why This Design?
Why 32-Byte Expression Size?
Design Decision: MathHook's Expression type is constrained to exactly 32 bytes.
Why?
- Modern CPUs have 64-byte cache lines (standard on x86-64, ARM64)
- Two expressions fit perfectly in one cache line
- Cache-friendly data structures yield 3-5x faster operations in hot loops
- This is critical for CAS workloads with millions of expression traversals
Trade-off: Must use Box<T> for large nested structures
- Recursive types (like
Pow(Box<Expression>, Box<Expression>)) use heap allocation - Pointer indirection has small overhead, but cache benefits far outweigh it
- For typical expression trees (depth < 50), the trade-off is heavily positive
Alternative Considered: Variable-size expressions (like Python objects)
- Pros: Simpler implementation, no size constraints
- Cons: Poor cache locality, unpredictable performance, frequent cache misses
- Decision: Performance predictability > implementation simplicity for CAS workload
When This Matters:
- Hot loops processing millions of expressions (simplification, pattern matching)
- Recursive algorithms (symbolic differentiation, integration)
- Less important: One-time parsing, display formatting, or educational explanations
Verification:
#![allow(unused)] fn main() { use std::mem::size_of; use mathhook::Expression; assert_eq!(size_of::<Expression>(), 32); }
Performance Impact: Benchmarks show 3-5x speedup on simplification and 2-3x on derivative computation compared to variable-size design.
Why Immutable Expressions?
Design Decision: Expressions cannot be modified after creation. All operations return new expressions.
Why?
- Thread Safety: Safe to share across threads without locks
- Correctness: No hidden mutation surprises
- Optimization: Compiler can optimize knowing values never change
- Debugging: Expression history is traceable
Trade-off: More allocations
- Each operation creates new expressions
- Mitigated by: reference counting (cheap clones), arena allocation for bulk operations
- Benchmark: <100ns overhead per operation (negligible in practice)
Alternative Considered: Mutable expressions with copy-on-write
- Pros: Fewer allocations in some cases
- Cons: Complex lifetime management, thread-safety issues, hard to reason about
- Decision: Simplicity and safety > micro-optimization
Example:
#![allow(unused)] fn main() { let expr = expr!(x + 1); let doubled = expr.mul(&expr!(2)); // `expr` is unchanged, `doubled` is new expression // Safe to use both in parallel }
Why Canonical Forms?
Design Decision: MathHook automatically normalizes expressions to canonical form.
What is Canonical Form?
y + xbecomesx + y(sorted)(a + b) + cbecomesAdd(a, b, c)(flattened)x + 0becomesx(identity removed)6/4becomes3/2(rationals reduced)
Why?
- Equality checking: Structurally equal expressions are always equal
- Simplification: Canonical form is prerequisite for many simplification rules
- Consistency: Same mathematical expression always has same representation
- Performance: Pattern matching is faster on normalized expressions
Trade-off: Small overhead on construction
- Every
add(),mul(),pow()normalizes - Typically <50ns per operation
- Benefit: Avoid expensive normalization later during pattern matching
Example:
#![allow(unused)] fn main() { let expr1 = expr!(x + y); let expr2 = expr!(y + x); assert_eq!(expr1, expr2); // True - both normalized to x + y }
When This Matters:
- Expression equality checking (hash tables, caches)
- Pattern matching in simplification rules
- Zero detection (is expression mathematically zero?)
Thread Safety
Expressions are Send + Sync, making them safe to share across threads:
#![allow(unused)] fn main() { use std::sync::Arc; let expr = Arc::new(expr!(x ^ 2)); let expr_clone = Arc::clone(&expr); // Use in multiple threads safely }
Pattern Matching
Work with expression structure using Rust's pattern matching:
#![allow(unused)] fn main() { match expr { Expression::Add(terms) => { println!("Sum with {} terms", terms.len()); } Expression::Mul(factors) => { println!("Product with {} factors", factors.len()); } Expression::Pow(base, exp) => { println!("Power: {} ^ {}", base, exp); } Expression::Function(name, args) => { println!("Function {} with {} args", name, args.len()); } _ => {} } }
Canonical Forms
Expressions automatically maintain canonical forms:
- Commutative operations sorted:
- Associativity flattened:
- Identity elimination: ,
- Rationals reduced:
Common Operations
Simplification
#![allow(unused)] fn main() { let expr = expr!(x + x); let simplified = expr.simplify(); // Result: 2*x }
Evaluation
#![allow(unused)] fn main() { let x = symbol!(x); let expr = expr!(x ^ 2); let result = expr.substitute(&x, &expr!(3)); // Result: 9 }
Formatting
#![allow(unused)] fn main() { let expr = expr!(x ^ 2); println!("Standard: {}", expr); // x^2 println!("LaTeX: {}", expr.to_latex()); // x^{2} println!("Wolfram: {}", expr.to_wolfram()); // Power[x, 2] }
Next Steps
Examples
Basic Expression Creation with Macros
Using expr! and symbol! macros for ergonomic expression creation
Rust
#![allow(unused)] fn main() { use mathhook::prelude::*; let x = symbol!(x); let y = symbol!(y); // Basic arithmetic let sum = expr!(x + y); let product = expr!(x * y); let power = expr!(x ^ 2); // Complex expressions let quadratic = expr!(a * x ^ 2 + b * x + c); }
Python
from mathhook import symbol, expr
x = symbol('x')
y = symbol('y')
# Basic arithmetic
sum_expr = expr('x + y')
product = expr('x * y')
power = expr('x^2')
# Complex expressions
quadratic = expr('a*x^2 + b*x + c')
JavaScript
const { symbol, expr } = require('mathhook-node');
const x = symbol('x');
const y = symbol('y');
// Basic arithmetic
const sum = expr('x + y');
const product = expr('x * y');
const power = expr('x^2');
// Complex expressions
const quadratic = expr('a*x^2 + b*x + c');
Canonical Form Normalization
Expressions are automatically normalized to canonical form
Rust
#![allow(unused)] fn main() { use mathhook::prelude::*; let expr1 = expr!(x + y); let expr2 = expr!(y + x); // Both normalized to same form assert_eq!(expr1, expr2); // Rationals reduced let frac = Expression::rational(6, 4); assert_eq!(frac, Expression::rational(3, 2)); }
Python
from mathhook import expr, Expression
expr1 = expr('x + y')
expr2 = expr('y + x')
# Both normalized to same form
assert expr1 == expr2
# Rationals reduced
frac = Expression.rational(6, 4)
assert frac == Expression.rational(3, 2)
JavaScript
const { expr, Expression } = require('mathhook-node');
const expr1 = expr('x + y');
const expr2 = expr('y + x');
// Both normalized to same form
console.assert(expr1.equals(expr2));
// Rationals reduced
const frac = Expression.rational(6, 4);
console.assert(frac.equals(Expression.rational(3, 2)));
Immutable Operations
All expression operations return new expressions without modifying originals
Rust
#![allow(unused)] fn main() { use mathhook::prelude::*; let expr = expr!(x + 1); let doubled = expr.mul(&expr!(2)); // Original unchanged println!("Original: {}", expr); // x + 1 println!("Doubled: {}", doubled); // 2*(x + 1) }
Python
from mathhook import expr
original = expr('x + 1')
doubled = original * 2
# Original unchanged
print(f"Original: {original}") # x + 1
print(f"Doubled: {doubled}") # 2*(x + 1)
JavaScript
const { expr } = require('mathhook-node');
const original = expr('x + 1');
const doubled = original.mul(2);
// Original unchanged
console.log(`Original: ${original}`); // x + 1
console.log(`Doubled: ${doubled}`); // 2*(x + 1)
Performance
Time Complexity: O(1) for construction, O(n) for traversal
API Reference
- Rust:
mathhook_core::expression::Expression - Python:
mathhook.Expression - JavaScript:
mathhook-node.Expression