Evaluation & JIT

oCAS provides three paths for numeric evaluation of symbolic expressions: an interpreter (stack VM), a Cranelift JIT compiler, and a SIMD-vectorized batch evaluator. This chapter explains each path and when to use it.


Stack VM interpreter

The default evaluation path. ExpressionEvaluator compiles an Atom expression tree into a sequence of stack-machine instructions, then executes them on a flat operand stack.

#![allow(unused)]
fn main() {
let arena = Arena::new();
let ctx = AtomArena::new(&arena);

// Parse and compile
let e = parse(&ctx, "x^2 + y").unwrap();
let ev = ExpressionEvaluator::<f64>::compile(e).unwrap();

// Evaluate with parameter values
let result = ev.evaluate(&[3.0, 1.0]).unwrap();  // [10.0]
let result = ev.evaluate(&[2.0, 0.0]).unwrap();  // [4.0]
}

The compiler automatically detects free variables, assigns them parameter slots in sorted order, and optimizes the instruction sequence (constant folding, copy-chain removal).

Python:

ev = ocas.ExpressionEvaluator("x^2 + y", ["x", "y"])
print(ev.evaluate([3.0, 1.0]))  # [10.0]
print(ev.evaluate([2.0, 0.0]))  # [4.0]

Cranelift JIT

With the jit feature, oCAS compiles expressions to native machine code via Cranelift. This is ideal for repeatedly evaluating the same expression with different inputs — amortizing the compilation cost over many calls.

cargo build -p ocas --features jit
#![allow(unused)]
fn main() {
#[cfg(feature = "jit")]
{
    use ocas::prelude::*;

    let arena = Arena::new();
    let ctx = AtomArena::new(&arena);
    let e = parse(&ctx, "sin(x) * cos(y)").unwrap();

    let ev: ExpressionEvaluator<f64> = ExpressionEvaluator::compile(e).unwrap();
    let compiled = ev.compile_jit().unwrap();
    let result = compiled.call(&[0.5, 1.0]);  // ~0.2590
}
}

The JIT path translates the same IR used by the interpreter into native x86-64 or aarch64 code. For expressions evaluated thousands of times, this yields up to 97× speedup over the interpreter (see Benchmarks).

Multi-output JIT

compile_multi compiles several expressions into one evaluator, sharing common subexpressions across outputs; call_into writes results into a caller-provided buffer (zero allocation per call).

#![allow(unused)]
fn main() {
#[cfg(feature = "jit")]
{
    use ocas::prelude::*;

    let arena = Arena::new();
    let ctx = AtomArena::new(&arena);
    let e1 = parse(&ctx, "sin(x) + 1").unwrap();
    let e2 = parse(&ctx, "sin(x) * 2").unwrap();

    let ev: ExpressionEvaluator<f64> =
        ExpressionEvaluator::compile_multi(&[e1, e2]).unwrap();
    let compiled = ev.compile_jit().unwrap();
    let mut out = [0.0f64; 2];
    compiled.call_into(&[1.0], &mut out);
}
}

f32 mixed precision

compile_jit_f32 generates single-precision code (libm *f symbols); compile_vector_evaluator_f32 doubles the SIMD lane count on the same hardware. Use when f32 accuracy suffices.


SIMD batch evaluation

The simd feature enables vectorized evaluation using pulp, computing multiple inputs simultaneously with runtime-detected SIMD width (SSE2/AVX2/ AVX-512).

cargo build -p ocas --features simd
#![allow(unused)]
fn main() {
#[cfg(feature = "simd")]
{
    use ocas::prelude::*;

    let arena = Arena::new();
    let ctx = AtomArena::new(&arena);
    let e = parse(&ctx, "x^2 + y").unwrap();

    let ev: ExpressionEvaluator<f64> = ExpressionEvaluator::compile(e).unwrap();
    let vec = ev.compile_vector_evaluator().unwrap();
    // Each vec holds one parameter's values across all input rows
    let results = vec.evaluate(&[
        vec![1.0, 2.0, 3.0, 0.0],   // x: 1² + 0 = 1, 2² + 1 = 5, …
        vec![0.0, 1.0, 2.0, 5.0],   // y
    ]).unwrap();
    // results[0] = [1.0, 5.0, 11.0, 5.0] (one row per output; a single
    // output yields exactly one row)
}
}

SIMD evaluation processes many input rows in parallel (the batch lane count is the runtime-detected SIMD width), ideal for parameter sweeps, plotting, and Monte Carlo workloads.


Comparison

PathFeatureLatencyThroughputBest for
Interpreter(default)Low (no compile overhead)MediumSingle-shot, interactive use
JITjitHigh (compile) + Low (run)HighRepeated evaluation of same expression
SIMDsimdLow–MediumVery High (4×)Batch evaluation, parameter sweeps

When to use each

  • Interpreter: Interactive REPL sessions, one-off evaluations, Python API.
  • JIT: Repetitive numeric evaluation (root-finding iterations, optimization loops).
  • SIMD: Batching many inputs (plotting, sensitivity analysis, ML pipelines).

User-defined functions

FunctionMap allows registering custom Rust functions callable from evaluated expressions.

#![allow(unused)]
fn main() {
let mut fns = FunctionMap::<f64>::new();
fns.insert("f", |args: &[f64]| args[0].sin() * args[1].cos());

let e = parse(&ctx, "f(x, y)").unwrap();
let ev = ExpressionEvaluator::<f64>::compile_with(e, fns).unwrap();
let result = ev.evaluate(&[0.5, 1.0]).unwrap();
}

See also