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. It was inspired by TinyExpr and TinyExpr++, but with additional features and Rust-native design.

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
  • Function application by juxtaposition (sin x is equivalent to sin(x))
  • Comprehensive error handling
  • No_std compatibility for embedded systems
  • Integration with CMSIS-DSP for ARM Cortex-M

§Quick Start

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

use exp_rs::engine::interp;

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

    // 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
}

§Using Variables and Constants

// Example of using variables and constants:

// 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".to_string(), 2.5);

// 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

// Example for arrays and object attributes:

// Add an array
ctx.arrays.insert("data".to_string(), vec![10.0, 20.0, 30.0, 40.0, 50.0]);

// Add an object with attributes
let mut point = HashMap::new();
point.insert("x".to_string(), 3.0);
point.insert("y".to_string(), 4.0);
ctx.attributes.insert("point".to_string(), point);

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

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

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

§Custom Functions

§Native Functions

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

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 an expression function
    ctx.register_expression_function(
        "hypotenuse",
        &["a", "b"],
        "sqrt(a^2 + b^2)"
    ).unwrap();

    // Use the custom function
    let result = interp("hypotenuse(3, 4)", Some(Rc::new(ctx))).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();
    ctx.enable_ast_cache(); // Enable AST caching

    // First evaluation will parse and cache the AST
    ctx.set_parameter("x", 1.0);
    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

    // Subsequent evaluations with the same expression will reuse the cached AST
    ctx.set_parameter("x", 2.0);
    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

    // This is much faster for repeated evaluations
    ctx.set_parameter("x", 3.0);
    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:

extern crate alloc;
use exp_rs::interp;
use exp_rs::EvalContext;
use alloc::rc::Rc;
// When using in a no_std environment with alloc:

// This defines an FFI function that can be called from C code
pub extern "C" fn evaluate_expression(x: f32, y: f32) -> f32 {
    // Create an evaluation context
    let mut ctx = EvalContext::new();

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

    // 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:

// In Cargo.toml:
// exp-rs = { version = "0.1", default-features = false, features = ["f32", "no-builtin-math"] }
extern crate alloc;
use exp_rs::context::EvalContext;
use exp_rs::engine::interp;
use alloc::rc::Rc;
use libm::{sin, cos};

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

    // Register custom math functions using standard Rust methods
    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());

    // Now you can use these functions
    let result = interp("sin(0.5) + cos(0.5)", Some(Rc::new(ctx))).unwrap();
    assert_eq!(result, sin(0.5) + cos(0.5)); // sin(0.5) + cos(0.5)
    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),
    }
}

§Supported Grammar

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

  • Multi-character operators: &&, ||, ==, !=, <=, >=, <<, >>, <<<, >>>, **, <>
  • Logical, comparison, bitwise, and exponentiation operators with correct precedence and associativity
  • List expressions and both comma and semicolon as separators
  • Function call syntax supporting both parentheses and juxtaposition
  • 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 (unless no-builtin-math is enabled):

  • 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

  • no-builtin-math: Disables all built-in math functions. You must register your own.
  • f32: Use 32-bit floating point (single precision) for calculations
  • f64: Use 64-bit floating point (double precision) for calculations (default)

Only one of f32 or f64 can be enabled at a time.

§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
  • Integration with CMSIS-DSP for ARM Cortex-M processors
  • Meson build system integration for cross-compilation
  • QEMU test harness for validating on ARM hardware
  • Optional C FFI for calling from non-Rust code

§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 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_functions
Expression functions implementation for the exp-rs library.
ffi
C FFI interface for exp-rs.
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