Crate exp_rs

Crate exp_rs 

Source
Expand description

exp-rs

A minimal, extensible, no_std-friendly math expression parser and evaluator for Rust.

§Overview

exp-rs is a math expression parser and evaluator library designed to be simple, extensible, and compatible with no_std environments, designed for use on embedded targets.

Key features:

  • Configurable floating-point precision (f32/f64)
  • Support for user-defined variables, constants, arrays, attributes, and functions
  • Built-in math functions (sin, cos, pow, etc.) that can be enabled/disabled
  • Ability to override any built-in function at runtime
  • Array access with array[index] syntax
  • Object attributes with object.attribute syntax
  • Standard function call syntax with parentheses (sin(x), cos(y), etc.)
  • Comprehensive error handling
  • No_std compatibility for embedded systems

§Quick Start

Here’s a basic example of evaluating a math expression:

use exp_rs::interp;

fn main() {
    // Simple expression evaluation
    let result = interp("2 + 3 * 4", None).unwrap();
    assert_eq!(result, 14.0); // 2 + (3 * 4) = 14

    #[cfg(feature = "libm")]
    {
        // Using built-in functions and constants
        let result = interp("sin(pi/4) + cos(pi/4)", None).unwrap();
        assert!(result - 1.414 < 0.001); // Approximately √2
    }
}

§Expression API - The Primary Interface

The Expression struct provides the most efficient way to evaluate expressions, especially when you need to evaluate the same expression multiple times with different parameter values. It uses arena allocation for zero-allocation evaluation after parsing.

§Simple Expression Evaluation

use exp_rs::Expression;
use bumpalo::Bump;

// Create an arena for memory allocation
let arena = Bump::new();

// Evaluate a simple expression without variables
let result = Expression::eval_simple("2 + 3 * 4", &arena).unwrap();
assert_eq!(result, 14.0);

§Expressions with Parameters

use exp_rs::{Expression, EvalContext};
use bumpalo::Bump;
use std::rc::Rc;

let arena = Bump::new();

// Method 1: Using batch builder
let mut builder = Expression::new(&arena);
builder.add_parameter("x", 3.0).unwrap();
builder.add_parameter("y", 4.0).unwrap();
builder.add_expression("x^2 + y").unwrap();
builder.eval(&Rc::new(EvalContext::new())).unwrap();
let result = builder.get_result(0).unwrap();
assert_eq!(result, 13.0); // 3^2 + 4 = 13

// Method 2: Using eval_with_params for one-shot evaluation
let params = [("x", 3.0), ("y", 4.0)];
let result = Expression::eval_with_params(
    "x^2 + y",
    &params,
    &Rc::new(EvalContext::new()),
    &arena
).unwrap();
assert_eq!(result, 13.0);

§Efficient Repeated Evaluation

The Expression API excels when evaluating the same expression multiple times:

use exp_rs::{Expression, EvalContext};
use bumpalo::Bump;
use std::rc::Rc;

let arena = Bump::new();
let ctx = Rc::new(EvalContext::new());

// Parse once, evaluate many times
let mut builder = Expression::new(&arena);
builder.add_parameter("a", 1.0).unwrap();
builder.add_parameter("b", -3.0).unwrap();
builder.add_parameter("c", 2.0).unwrap();
builder.add_parameter("x", 0.0).unwrap();
builder.add_expression("a * x^2 + b * x + c").unwrap();

// Evaluate for different x values
for x in [0.0, 1.0, 2.0, 3.0] {
    builder.set("x", x).unwrap();
    builder.eval(&ctx).unwrap();
    let y = builder.get_result(0).unwrap();
    println!("f({}) = {}", x, y);
}

§Batch Expression Evaluation

Evaluate multiple expressions with shared parameters:

use exp_rs::{Expression, EvalContext};
use bumpalo::Bump;
use std::rc::Rc;

let arena = Bump::new();
let ctx = Rc::new(EvalContext::new());

let mut batch = Expression::new(&arena);

// Add shared parameters
batch.add_parameter("radius", 5.0).unwrap();

// Add multiple expressions
let area_idx = batch.add_expression("pi * radius^2").unwrap();
let circumference_idx = batch.add_expression("2 * pi * radius").unwrap();

// Evaluate all expressions
batch.eval(&ctx).unwrap();

println!("Area: {}", batch.get_result(area_idx).unwrap());
println!("Circumference: {}", batch.get_result(circumference_idx).unwrap());

// Update parameter and re-evaluate
batch.set("radius", 10.0).unwrap();
batch.eval(&ctx).unwrap();

println!("New area: {}", batch.get_result(area_idx).unwrap());
println!("New circumference: {}", batch.get_result(circumference_idx).unwrap());

§Relationship to interp()

The interp() function remains available for backward compatibility and simple one-shot evaluations. Internally, it uses the Expression API:

use exp_rs::interp;

// These are equivalent:
let result1 = interp("2 + 3", None).unwrap();

use exp_rs::Expression;
use bumpalo::Bump;
let arena = Bump::new();
let result2 = Expression::eval_simple("2 + 3", &arena).unwrap();

assert_eq!(result1, result2);

For new code, especially when evaluating expressions multiple times or when performance is critical, prefer using the Expression API directly.

§Supported Grammar

exp-rs supports a superset of the original TinyExpr grammar, closely matching the tinyexpr++ grammar, including:

  • Multi-character operators: &&, ||, ==, !=, <=, >=, <<, >>, <<<, >>>, **, <>
  • Logical operators (&&, ||) with short-circuit evaluation
  • Logical, comparison, bitwise, and exponentiation operators with correct precedence and associativity
  • List expressions and both comma and semicolon as separators
  • Standard function call syntax with parentheses
  • Array and attribute access
  • Right-associative exponentiation

§Operator Precedence and Associativity

From lowest to highest precedence:

PrecedenceOperatorsAssociativity
1, ;Left
2`
3&&Left
4``
6&Left (bitwise AND)
7== != < > <= >= <>Left (comparison)
8<< >> <<< >>>Left (bit shifts)
9+ -Left
10* / %Left
14unary + - ~Right (unary)
15^Right
16**Right

§Built-in Functions

The following functions are available by default when the libm feature is enabled. Without the libm feature, these functions will not be automatically registered and must be defined by the user with native or expression functions:

  • Trigonometric: sin, cos, tan, asin, acos, atan, atan2
  • Hyperbolic: sinh, cosh, tanh
  • Exponential/Logarithmic: exp, log, log10, ln
  • Power/Root: sqrt, pow
  • Rounding: ceil, floor
  • Comparison: max, min
  • Misc: abs, sign

§Built-in Constants

  • pi: 3.14159… (π)
  • e: 2.71828… (Euler’s number)

§Feature Flags

  • libm: Enables built-in math functions using the libm library. Without this feature, you must register your own math functions.
  • f32: Use 32-bit floating point (single precision) for calculations

When f32 is not specified, 64-bit floating point (double precision) is used by default.

§Embedded Systems Support

exp-rs provides extensive support for embedded systems:

  • no_std compatible with the alloc crate
  • Configurable precision with f32/f64 options
  • Option to disable built-in math functions and provide custom implementations
  • Tested example using qemu CMSIS-DSP math functions (test in repo)
  • Meson build system integration for cross-compilation
  • QEMU test harness for validating on ARM hardware
  • Optional C FFI for calling from non-Rust code

§Using Variables and Constants

extern crate alloc;
use exp_rs::context::EvalContext;
use exp_rs::interp;
use alloc::rc::Rc;

// Create an evaluation context
let mut ctx = EvalContext::new();

// Add variables
ctx.set_parameter("x", 5.0);
ctx.set_parameter("y", 10.0);

// Add constants - these won't change once set
ctx.constants.insert("FACTOR".try_into().unwrap(), 2.5).unwrap();

// Evaluate expression with variables and constants
let result = interp("x + y * FACTOR", Some(Rc::new(ctx))).unwrap();
// Result: 30.0 (5 + (10 * 2.5) = 30)

§Arrays and Object Attributes

extern crate alloc;
use exp_rs::interp;
use exp_rs::context::EvalContext;
use heapless::FnvIndexMap;
use alloc::rc::Rc;

// Create an evaluation context
let mut ctx = EvalContext::new();
// Add an array
ctx.arrays.insert("data".try_into().unwrap(), vec![10.0, 20.0, 30.0, 40.0, 50.0]).unwrap();

// Add an object with attributes
let mut point = FnvIndexMap::new();
point.insert("x".try_into().unwrap(), 3.0).unwrap();
point.insert("y".try_into().unwrap(), 4.0).unwrap();
ctx.attributes.insert("point".try_into().unwrap(), point).unwrap();
let ctx_rc = Rc::new(ctx);

// Access array elements in expressions
interp("data[2]", Some(Rc::clone(&ctx_rc))).unwrap(); // Returns 30.0

// Access attributes in expressions
interp("point.x + point.y", Some(Rc::clone(&ctx_rc))).unwrap(); // Returns 7.0

// Combine array and attribute access in expressions
interp("sqrt(point.x^2 + point.y^2) + data[0]", Some(Rc::clone(&ctx_rc))).unwrap();
// Result: sqrt(3^2 + 4^2) + 10 = 5 + 10 = 15

§Custom Functions

exp-rs allows you to define custom functions in two ways:

§Native Functions

Native functions can be defined at compile time:

extern crate alloc;
use exp_rs::context::EvalContext;
use exp_rs::engine::interp;
use alloc::rc::Rc;

fn main() {
    let mut ctx = EvalContext::new();

    // Register a native function that sums all arguments
    ctx.register_native_function("sum", 3, |args| {
        args.iter().sum()
    });

    // Use the custom function
    let result = interp("sum(1, 2, 3)", Some(Rc::new(ctx))).unwrap();
    assert_eq!(result, 6.0);
}

§Expression Functions

Expression functions can be registered and passed into the library at runtime:

extern crate alloc;
use exp_rs::context::EvalContext;
use exp_rs::expression::Expression;
use alloc::rc::Rc;
use bumpalo::Bump;

fn main() {
    let arena = Bump::new();
    let mut builder = Expression::new(&arena);
    let ctx = EvalContext::new();

    // Register an expression function in the batch
    builder.register_expression_function(
        "hypotenuse",
        &["a", "b"],
        "sqrt(a^2 + b^2)"
    ).unwrap();

    // Add expression that uses the custom function
    builder.add_expression("hypotenuse(3, 4)").unwrap();

    // Evaluate the batch
    builder.eval(&Rc::new(ctx)).unwrap();
    let result = builder.get_result(0).unwrap();
    assert_eq!(result, 5.0);
}

§Performance Optimization with AST Caching

For repeated evaluations of the same expression with different variables:

extern crate alloc;
use exp_rs::context::EvalContext;
use exp_rs::engine::interp;
use alloc::rc::Rc;

fn main() {
    let mut ctx = EvalContext::new();

    // Evaluate expression with different parameter values
    ctx.set_parameter("x", 1.0).unwrap();
    let result1 = interp("x^2 + 2*x + 1", Some(Rc::new(ctx.clone()))).unwrap();
    assert_eq!(result1, 4.0); // 1^2 + 2*1 + 1 = 4

    // Update parameter and evaluate again
    ctx.set_parameter("x", 2.0).unwrap();
    let result2 = interp("x^2 + 2*x + 1", Some(Rc::new(ctx.clone()))).unwrap();
    assert_eq!(result2, 9.0); // 2^2 + 2*2 + 1 = 9

    // The arena-based implementation provides efficient evaluation
    ctx.set_parameter("x", 3.0).unwrap();
    let result3 = interp("x^2 + 2*x + 1", Some(Rc::new(ctx))).unwrap();
    assert_eq!(result3, 16.0); // 3^2 + 2*3 + 1 = 16
}

§Using on Embedded Systems (no_std)

exp-rs is designed to work in no_std environments with the alloc crate. A C header is automatically generated at compile time using Cbindgen.

extern crate alloc;
use exp_rs::interp;
use exp_rs::EvalContext;
use exp_rs::Real;
use alloc::rc::Rc;

// This defines an FFI function that can be called from C code
pub extern "C" fn evaluate_expression(x: f32, y: f32) -> f32 {
    // Note: Real is either f32 or f64 depending on feature flags
    // Create an evaluation context
    let mut ctx = EvalContext::new();

    // Set parameters
    ctx.set_parameter("x", x as Real);
    ctx.set_parameter("y", y as Real);

    // Evaluate the expression
    let result = interp("sqrt(x^2 + y^2)", Some(Rc::new(ctx))).unwrap();

    // Convert back to f32 for C compatibility
    result as f32
}

§Disabling Built-in Math Functions

For embedded systems where you want to provide your own math implementations:

extern crate alloc;
use exp_rs::context::EvalContext;
use exp_rs::engine::interp;
use alloc::rc::Rc;

fn main() {
    let mut ctx = EvalContext::new();

    // Register custom math functions
    ctx.register_native_function("sin", 1, |args| args[0].sin());
    ctx.register_native_function("cos", 1, |args| args[0].cos());
    ctx.register_native_function("sqrt", 1, |args| args[0].sqrt());

    // Use the functions
    let result = interp("sin(0.5) + cos(0.5)", Some(Rc::new(ctx))).unwrap();
    println!("Result: {}", result);
}

§Error Handling

Comprehensive error handling is provided:

extern crate alloc;
use exp_rs::context::EvalContext;
use exp_rs::engine::interp;
use exp_rs::error::ExprError;
use alloc::rc::Rc;

fn main() {
    let ctx = EvalContext::new();

    // Handle syntax errors
    match interp("2 + * 3", Some(Rc::new(ctx.clone()))) {
        Ok(_) => println!("Unexpected success"),
        Err(ExprError::Syntax(msg)) => println!("Syntax error: {}", msg),
        Err(e) => println!("Unexpected error: {:?}", e),
    }

    // Handle unknown variables
    match interp("x + 5", Some(Rc::new(ctx.clone()))) {
        Ok(_) => println!("Unexpected success"),
        Err(ExprError::UnknownVariable { name }) => println!("Unknown variable: {}", name),
        Err(e) => println!("Unexpected error: {:?}", e),
    }

    // Handle division by zero
    match interp("1 / 0", Some(Rc::new(ctx))) {
        Ok(result) => {
            if result.is_infinite() {
                println!("Division by zero correctly returned infinity")
            } else {
                println!("Unexpected result: {}", result)
            }
        },
        Err(e) => println!("Unexpected error: {:?}", e),
    }
}

§Attribution

exp-rs began as a fork of tinyexpr-rs by Krzysztof Kondrak, which itself was a port of the TinyExpr C library by Lewis Van Winkle (codeplea). As the functionality expanded beyond the scope of the original TinyExpr, it evolved into a new project with additional features inspired by tinyexpr-plusplus.

Re-exports§

pub use expression::Expression;
pub use expression::Param;
pub use eval::iterative::EvalEngine;
pub use eval::iterative::eval_with_engine;
pub use context::*;
pub use engine::*;
pub use functions::*;
pub use types::*;
pub use ffi::*;

Modules§

constants
context
engine
error
Error types and handling for the exp-rs crate.
eval
Expression evaluation module for exp-rs
evaluator
Arena-managed expression evaluator
expression
Batch expression evaluation builder for efficient real-time evaluation
expression_functions
Expression functions implementation for the exp-rs library.
ffi
Foreign Function Interface (FFI) for C/C++ interoperability
functions
Built-in mathematical functions for expression evaluation.
lexer
types
Type definitions for the expression parser and evaluator.

Macros§

assert_approx_eq
Utility macro to check if two floating point values are approximately equal within a specified epsilon. Supports optional format arguments like assert_eq!.

Structs§

Box
A pointer type that uniquely owns a heap allocation of type T.
String
A UTF-8–encoded, growable string.
Vec
A contiguous growable array type, written as Vec<T>, short for ‘vector’.

Traits§

ToString
A trait for converting a value to a String.

Type Aliases§

Real