ccalc-engine 0.25.0

Core computation engine for ccalc: tokenizer, parser, AST evaluator, and memory cells
Documentation

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:

[dependencies]
ccalc-engine = "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
ccalc-engine = { version = "0.21", features = ["blas"] }

# Static linkage — compiles OpenBLAS from source (requires gfortran + cmake)
ccalc-engine = { version = "0.21", features = ["blas-static"] }

Quick start

use ccalc_engine::{env, eval, exec, parser, io};

fn main() {
    // One-time initialisation — registers the function call and autoload hooks.
    exec::init();

    // Build the variable environment.
    let mut env = env::new_env();
    let mut io  = io::IoContext::new();
    let     fmt = eval::FormatMode::Short;
    let     base = eval::Base::Dec;

    // Parse a multi-statement block and execute it.
    let src = "
        x = 3;
        y = 4;
        hyp = sqrt(x^2 + y^2)
    ";
    let stmts = parser::parse_stmts(src).unwrap();
    exec::exec_stmts(&stmts, &mut env, &mut io, &fmt, base, false).unwrap();

    // Read a result from the environment.
    if let Some(val) = env.get("hyp") {
        println!("hyp = {}", eval::format_value(val, base, &fmt));
        // hyp = 5
    }
}

Single expression

use ccalc_engine::{env, eval, exec, parser};

exec::init();
let env = env::new_env();

let expr = parser::parse("sin(pi/6)^2 + cos(pi/6)^2").unwrap();
// parser::parse returns a Stmt; extract the Expr from Stmt::Expr
let val = match expr {
    parser::Stmt::Expr(e) => eval::eval(&e, &env).unwrap(),
    _ => panic!("expected expression"),
};
println!("{val:?}"); // Scalar(1.0)

User-defined functions

use ccalc_engine::{env, eval, exec, parser, io};

exec::init();
let mut env = env::new_env();
let mut io  = io::IoContext::new();

let src = "
    function y = square(x)
      y = x ^ 2;
    end

    result = square(7)
";
let stmts = parser::parse_stmts(src).unwrap();
exec::exec_stmts(&stmts, &mut env, &mut io,
    &eval::FormatMode::Short, eval::Base::Dec, false).unwrap();

// env["result"] == Scalar(49.0)

API overview

env module

pub fn new_env() -> Env   // creates a fresh environment seeded with i, j = 0+1i

pub enum Value {
    Void,
    Scalar(f64),
    Matrix(Array2<f64>),
    Complex(f64, f64),
    Str(String),           // single-quoted char array
    StringObj(String),     // double-quoted string object
    Lambda(LambdaFn),      // @(x) expr closure
    Function { outputs, params, body_source, locals },
    Tuple(Vec<Value>),     // internal multi-return (consumed by MultiAssign)
    Cell(Vec<Value>),      // {1, 'hi', [1 2 3]}
    Struct(IndexMap<String, Value>),
    StructArray(Vec<IndexMap<String, Value>>),
}

parser module

// Parse a single statement (expression or assignment).
pub fn parse(input: &str) -> Result<Stmt, String>

// Parse a full multi-line script into a statement list.
// Each tuple is (statement, is_silent) where is_silent == true means
// the statement ended with ';' and should not print output.
pub fn parse_stmts(input: &str) -> Result<Vec<(Stmt, bool)>, String>

// REPL helper: returns the net block depth delta for one line.
// +1 for 'if'/'for'/'while'/'function'/..., -1 for 'end'/'until'.
pub fn block_depth_delta(line: &str) -> i32

// Returns true if a line is a self-contained single-line block:
// 'if cond; body; end' — no buffering needed in the REPL.
pub fn is_single_line_block(line: &str) -> bool

eval module

// Evaluate a parsed expression.
pub fn eval(expr: &Expr, env: &Env) -> Result<Value, String>

// Evaluate with file I/O support (required for fprintf, fopen, etc.).
pub fn eval_with_io(expr: &Expr, env: &Env, io: &mut IoContext) -> Result<Value, String>

// Number formatting.
pub fn format_value(v: &Value, base: Base, mode: &FormatMode) -> String
pub fn format_value_full(v: &Value, mode: &FormatMode) -> Option<String>
pub fn format_scalar(n: f64, base: Base, mode: &FormatMode) -> String
pub fn format_complex(re: f64, im: f64, mode: &FormatMode) -> String

// C printf-style formatting engine used by fprintf/sprintf.
pub fn format_printf(fmt: &str, args: &[Value]) -> Result<String, String>

#[derive(Clone, Copy, PartialEq)]
pub enum Base { Dec, Hex, Bin, Oct }

#[derive(Clone)]
pub enum FormatMode {
    Short, Long, ShortE, LongE, ShortG, Bank, Rat, Hex, Sign,
    Compact, Loose,
    Decimals(usize),   // format N
}

exec module

// Must be called once at startup before any user function can be invoked.
pub fn exec_stmts(
    stmts: &[(Stmt, bool)],
    env: &mut Env,
    io: &mut IoContext,
    fmt: &FormatMode,
    base: Base,
    compact: bool,
) -> Result<Option<Signal>, String>

pub enum Signal { Break, Continue, Return }

// Search path management (mirrors MATLAB addpath/rmpath).
pub fn session_path_init(paths: Vec<PathBuf>)
pub fn session_path_add(path: PathBuf, append: bool)
pub fn session_path_remove(path: &Path)
pub fn session_path_list() -> Vec<PathBuf>

// Script directory stack — push before run(), pop after.
pub fn script_dir_push(dir: &Path)
pub fn script_dir_pop()

// Resolve a script filename to an existing path (CWD → script dir → session path).
pub fn resolve_script_path(name: &str) -> Option<PathBuf>

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 by eval_inner when a Value::Function is invoked; implemented by exec::call_user_function.
  • AutoloadHook — called by eval_inner when a name is unknown; implemented by exec::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 ccalc_engine::{env, eval, exec, parser, io};

fn main() {
    exec::init();
    let mut env  = env::new_env();
    let mut io   = io::IoContext::new();
    let     fmt  = eval::FormatMode::Short;
    let     base = eval::Base::Dec;

    let mut input = String::new();
    loop {
        input.clear();
        std::io::stdin().read_line(&mut input).unwrap();
        let line = input.trim();
        if line == "exit" { break; }

        match parser::parse_stmts(line) {
            Ok(stmts) => {
                match exec::exec_stmts(&stmts, &mut env, &mut io, &fmt, base, false) {
                    Ok(_) => {}
                    Err(e) => eprintln!("Error: {e}"),
                }
            }
            Err(e) => eprintln!("Parse error: {e}"),
        }
    }
}

Running the benchmarks

cargo bench

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/100matmul/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.