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.attributesyntax - 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",
¶ms,
&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:
| Precedence | Operators | Associativity |
|---|---|---|
| 1 | , ; | Left |
| 2 | ` | |
| 3 | && | Left |
| 4 | ` | ` |
| 6 | & | Left (bitwise AND) |
| 7 | == != < > <= >= <> | Left (comparison) |
| 8 | << >> <<< >>> | Left (bit shifts) |
| 9 | + - | Left |
| 10 | * / % | Left |
| 14 | unary + - ~ | 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_stdcompatible with thealloccrate- Configurable precision with
f32/f64options - 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.