ccalc-engine
Core computation engine for ccalc — a terminal calculator with Octave/MATLAB syntax.
This crate provides the complete language pipeline: tokenizer → parser → AST → evaluator, plus the block executor that handles control flow, user-defined functions, and the session search path. It is designed to be embedded in any host application that needs a scriptable MATLAB-dialect expression engine.
input string
└─► tokenizer (parser::tokenize)
└─► recursive-descent parser (parser::parse) → Stmt / Expr AST
└─► evaluator (eval::eval) → Value
└─► block executor (exec::exec_stmts)
Features
| Feature | Description |
|---|---|
| Numeric types | f64 scalars, 2-D real matrices (via ndarray), complex numbers |
| MATLAB-compatible syntax | Operators, ranges 1:5, matrix literals [1 2; 3 4], element-wise .* |
| Control flow | if/elseif/else, for, while, do/until, switch/case, break/continue, return |
| User functions | Named function definitions, multiple return values, @(x) lambdas, closures |
| Scoping | global and persistent variables, private/ directory isolation, +pkg/ namespaces |
| String types | Single-quoted char arrays and double-quoted string objects |
| Cell arrays | {1, 'hi', [1 2 3]} heterogeneous containers, varargin/varargout |
| Structs | Scalar structs, struct arrays, nested field access s.a.b |
| File I/O | fopen/fclose/fgetl/fgets, dlmread/dlmwrite, isfile/pwd |
| Formatted output | fprintf/sprintf with full C printf specifier support |
| Error handling | try/catch, error(), warning(), pcall(), lasterr() |
| Autoload | Calling an unknown name searches <name>.calc / <name>.m on the path |
| Packages | +pkg/ namespace directories; pkg.func(args) call syntax |
| Number bases | Decimal, hex, binary, octal output; 0xFF / 0b1010 / 0o17 literals |
| 160+ built-ins | Math, matrix, string, I/O, filesystem, bitwise, statistics |
Installation
Add to your Cargo.toml:
[]
= "0.21"
Optional: BLAS acceleration
Matrix multiplication (A * B) can be accelerated via OpenBLAS. inv/det
always use pure Rust (no BLAS required).
# Dynamic linkage — requires system-installed OpenBLAS
= { = "0.21", = ["blas"] }
# Static linkage — compiles OpenBLAS from source (requires gfortran + cmake)
= { = "0.21", = ["blas-static"] }
Quick start
use ;
Single expression
use ;
init;
let env = new_env;
let expr = parse.unwrap;
// parser::parse returns a Stmt; extract the Expr from Stmt::Expr
let val = match expr ;
println!; // Scalar(1.0)
User-defined functions
use ;
init;
let mut env = new_env;
let mut io = new;
let src = "
function y = square(x)
y = x ^ 2;
end
result = square(7)
";
let stmts = parse_stmts.unwrap;
exec_stmts.unwrap;
// env["result"] == Scalar(49.0)
API overview
env module
parser module
// Parse a single statement (expression or assignment).
eval module
// Evaluate a parsed expression.
exec module
// Must be called once at startup before any user function can be invoked.
// Search path management (mirrors MATLAB addpath/rmpath).
Architecture
ccalc-engine/src/
├── lib.rs crate root, module re-exports
├── env.rs Value enum, Env type, workspace save/load
├── eval.rs Expr/Op AST, eval_inner, call_builtin (160+ built-ins),
│ global/persistent variable stores, display thread-locals,
│ autoload hook, format_* helpers
├── parser.rs tokenize → recursive-descent parse → Stmt/Expr AST
├── exec.rs exec_stmts, call_user_function, try_autoload,
│ session path, script dir stack, body parse cache
└── io.rs IoContext — file descriptor table for fopen/fclose/fgetl/fgets
Thread model
All state is thread-local. Each OS thread has its own independent environment, global/persistent stores, and search path. This makes the engine safe to embed in multi-threaded applications as long as each thread manages its own environment.
Hook pattern
eval.rs and exec.rs are decoupled via function-pointer hooks to avoid a
circular dependency:
FnCallHook— called byeval_innerwhen aValue::Functionis invoked; implemented byexec::call_user_function.AutoloadHook— called byeval_innerwhen a name is unknown; implemented byexec::try_autoload.
Both hooks are registered by exec::init() which must be called before any
user function or autoload can work.
Scoping
| Mechanism | Description |
|---|---|
global x |
Shared across all functions and the base workspace that declare it |
persistent x |
Per-function value that survives between calls; [] on first call |
private/ |
Functions in private/ are visible only to the parent directory |
+pkg/ |
Package namespace; pkg.func(args) searches +pkg/func.calc |
Persistent variables use write-through semantics: Stmt::IndexSet and
Stmt::Assign write to PERSISTENT_STORE immediately so recursive callers
see updates without waiting for the current frame to return. This is what
makes memoization (e.g. Fibonacci with a persistent cache) correct and O(n)
rather than O(2ⁿ).
Embedding example — minimal REPL
use ;
Running the benchmarks
Benchmarks live in benches/engine.rs and use Criterion:
| Benchmark | What it measures |
|---|---|
scalar_ops_sum_1M |
sum(1:1000000) — range + reduction |
fib/fib_30 |
Recursive Fibonacci(30) — deep function call overhead |
loop_10k |
for k=1:10000; s+=k; end — loop + compound assignment |
matmul/100 … matmul/1000 |
ones(N,N) * ones(N,N) — matrix multiply |
fn_calls_1000 |
1000 calls to a trivial named function |
License
MIT — same as the parent ccalc workspace.