1 unstable release
Uses new Rust 2024
new 0.1.0 | Apr 21, 2025 |
---|
#166 in Math
375KB
7.5K
SLoC
exp-rs
exp-rs (github.com/cosmikwolf/exp-rs) is a tiny recursive descent expression parser, compiler, and evaluation engine for math expressions.
A C header is generated automatically for FFI usage via cbindgen
.
This project was inspired by tinyexpr-rs by Krzysztof Kondrak, which is itself a port of TinyExpr by codeplea.
The function grammar of tinyexpr-plusplus was used to make it a compatible replacement.
exp-rs is a no_std
crate and is designed to be compatible with embedded systems and environments where the Rust standard library is not available.
Features
- Parse and evaluate mathematical expressions
- Support for variables, constants, and functions
- Array access with
array[index]
syntax - Attribute access with
object.attribute
syntax - Custom function registration (both native Rust functions and expression-based functions)
- No external dependencies for core functionality
- No-std compatible
- Configurable precision with f32 (single-precision) or f64 (double-precision) modes
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:
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 |
Grammar (EBNF-like)
<expr> = <term> { ("," | ";") <term> }
<term> = <factor> { ("+" | "-") <factor> }
<factor> = <power> { ("*" | "/" | "%") <power> }
<power> = <unary> { ("^" | "**") <unary> }
<unary> = { ("-" | "+" | "~") } <postfix>
<postfix> = <primary> { ("(" <args> ")" | "[" <expr> "]" | "." <variable>) }
<primary> = <constant>
| <variable>
| <function> "(" <args> ")"
| <function> <primary> // Juxtaposition
| <variable> "[" <expr> "]"
| <variable> "." <variable>
| "(" <expr> ")"
<args> = [ <expr> { ("," | ";") <expr> } ]
- Function application by juxtaposition is supported (e.g.,
sin x
is equivalent tosin(x)
). - Both
,
and;
are accepted as list separators. - Multi-character operators are tokenized and parsed correctly.
Supported Operators
- Arithmetic:
+
,-
,*
,/
,%
,^
,**
- Comparison:
<
,>
,<=
,>=
,==
,!=
,<>
- Logical:
&&
,||
- Bitwise:
&
,|
,~
,<<
,>>
,<<<
,>>>
- Comma/semicolon:
,
,;
(list separator, returns last value) - Unary:
-
,+
,~
(bitwise not)
Limitations
- Ternary conditional expressions (
condition ? true_expr : false_expr
) are not supported. - Locale-dependent separator (comma/semicolon) is always accepted; locale configuration is not yet implemented.
- Feature flags for optional grammar features are not yet available.
Usage
Add to your Cargo.toml
:
[dependencies]
exp-rs = "0.1"
Floating-Point Precision
By default, exp-rs
uses 64-bit floating point (double precision) for calculations. You can configure the precision using feature flags:
# Use default 64-bit precision (double)
exp-rs = "0.1"
# Or explicitly enable 64-bit precision
exp-rs = { version = "0.1", features = ["f64"] }
# Use 32-bit precision (float)
exp-rs = { version = "0.1", default-features = false, features = ["f32"] }
Note that only one precision mode (f32
or f64
) can be enabled at a time.
Custom Math Implementations (CMSIS-DSP Support)
For embedded systems, you can disable built-in math functions and provide your own implementations (e.g., using CMSIS-DSP):
# Disable built-in math functions to use custom implementations
exp-rs = { version = "0.1", default-features = false, features = ["f32", "no-builtin-math"] }
The QEMU tests include examples of integrating with CMSIS-DSP for optimized math functions on ARM Cortex-M processors.
Basic Example
use exp_rs::engine::interp;
fn main() {
// Simple expression evaluation
let result = interp("2 + 3 * 4", None).unwrap();
println!("2 + 3 * 4 = {}", result); // Outputs: 2 + 3 * 4 = 14
// Using built-in functions
let result = interp("sin(pi/4) + cos(pi/4)", None).unwrap();
println!("sin(pi/4) + cos(pi/4) = {}", result); // Approximately 1.414
}
Using Variables and Constants
use exp_rs::engine::interp;
use exp_rs::context::EvalContext;
fn main() {
let mut ctx = EvalContext::new();
// Add variables
ctx.variables.insert("x".to_string(), 5.0);
ctx.variables.insert("y".to_string(), 10.0);
// Add constants
ctx.constants.insert("FACTOR".to_string(), 2.5);
// Evaluate expression with variables and constants
let result = interp("x + y * FACTOR", Some(&mut ctx)).unwrap();
println!("x + y * FACTOR = {}", result); // Outputs: x + y * FACTOR = 30
}
Custom Functions
use exp_rs::engine::interp;
use exp_rs::context::EvalContext;
fn main() {
let mut ctx = EvalContext::new();
// Register a native function
ctx.register_native_function("sum", 3, |args| {
args.iter().sum()
});
// Register an expression function
ctx.register_expression_function(
"hypotenuse",
&["a", "b"],
"sqrt(a^2 + b^2)"
).unwrap();
// Use the custom functions
let result1 = interp("sum(1, 2, 3)", Some(&mut ctx)).unwrap();
println!("sum(1, 2, 3) = {}", result1); // Outputs: sum(1, 2, 3) = 6
let result2 = interp("hypotenuse(3, 4)", Some(&mut ctx)).unwrap();
println!("hypotenuse(3, 4) = {}", result2); // Outputs: hypotenuse(3, 4) = 5
}
Arrays and Attributes
use exp_rs::engine::interp;
use exp_rs::context::EvalContext;
use std::collections::BTreeMap;
fn main() {
let mut ctx = EvalContext::new();
// 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 = BTreeMap::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
let result1 = interp("data[2]", Some(&mut ctx)).unwrap();
println!("data[2] = {}", result1); // Outputs: data[2] = 30
// Access attributes
let result2 = interp("point.x + point.y", Some(&mut ctx)).unwrap();
println!("point.x + point.y = {}", result2); // Outputs: point.x + point.y = 7
// Combine array and attribute access in expressions
let result3 = interp("sqrt(point.x^2 + point.y^2) + data[0]", Some(&mut ctx)).unwrap();
println!("sqrt(point.x^2 + point.y^2) + data[0] = {}", result3); // Outputs: 15
}
Supported Operators
- Addition:
+
- Subtraction:
-
- Multiplication:
*
- Division:
/
- Modulo:
%
- Power:
^
- Unary minus:
-
- Comma operator:
,
(returns the last value)
Built-in 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
Disabling Built-in Functions
You can disable the built-in math functions with the no-builtin-math
feature flag to provide your own implementations, for example when using CMSIS-DSP on embedded systems:
[dependencies]
exp-rs = { version = "0.1", default-features = false, features = ["f32", "no-builtin-math"] }
Constants
pi
: 3.14159... (π)e
: 2.71828... (Euler's number)
C FFI and Header Generation
If you want to use exp-rs
from C or other languages, a C header file is automatically generated during the build process using cbindgen
.
This header exposes a simple C API for evaluating expressions.
After running cargo build
, the generated header file can be found at:
include/exp_rs.h
You can copy this header to your C project and link against the generated static or dynamic library.
Example C usage
#include "exp_rs.h"
int main() {
double result = exp_rs_eval("2+2*2");
printf("%f\n", result); // prints "6.000000"
return 0;
}
Code Coverage
To check test coverage, install cargo-tarpaulin:
cargo install cargo-tarpaulin
Then run:
cargo tarpaulin --workspace --all-features
Build instructions
The build script (build.rs
) will automatically generate a C header file for FFI usage.
Cargo Build
cargo build
cargo test
cargo run --example basic
Meson Build
The project can also be integrated into Meson build systems using the provided meson.build
file:
# Configure with default options
meson setup build
# Configure for QEMU testing
meson setup build-qemu --cross-file qemu_test/qemu_harness/arm-cortex-m7-qemu.ini -Dbuild_target=qemu_tests
# Build the project
meson compile -C build
# Run tests (when using QEMU configuration)
meson test -C build-qemu
QEMU Tests
The repository includes a script to run tests in QEMU emulation:
# Run all QEMU tests with default settings (f32 mode)
./run_qemu_tests.sh
# Run with verbose output
./run_qemu_tests.sh --verbose
# Run a specific test
./run_qemu_tests.sh --test test_name
# Run with f64 (double precision) support
./run_qemu_tests.sh --mode f64
# Clean build before running tests
./run_qemu_tests.sh --clean
# Show help
./run_qemu_tests.sh --help
Project History & 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 by Blake Madden.
The project has grown to include:
- Support for a wider range of operators
- Array and attribute access
- Juxtaposition support for function calls
- Support for ARM Cortex-M with CMSIS-DSP integration
- Configurable precision with f32/f64 modes
- Comprehensive cbindgen generated FFI for C integration
License
Licensed under either of
- Apache License, Version 2.0 (LICENSE-APACHE or http://www.apache.org/licenses/LICENSE-2.0)
- MIT license (LICENSE-MIT or http://opensource.org/licenses/MIT)
Dependencies
~1.5–3.5MB
~60K SLoC