exp_rs/
lib.rs

1#![cfg_attr(all(not(test), target_arch = "arm"), no_std)]
2#![doc = r#"
3# exp-rs
4
5A minimal, extensible, no_std-friendly math expression parser and evaluator for Rust.
6
7## Overview
8
9exp-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](https://github.com/codeplea/tinyexpr) and [TinyExpr++](https://github.com/Blake-Madden/tinyexpr-plusplus), but with additional features and Rust-native design.
10
11Key features:
12- Configurable floating-point precision (f32/f64)
13- Support for user-defined variables, constants, arrays, attributes, and functions
14- Built-in math functions (sin, cos, pow, etc.) that can be enabled/disabled
15- Ability to override any built-in function at runtime
16- Array access with `array[index]` syntax
17- Object attributes with `object.attribute` syntax
18- Function application by juxtaposition (`sin x` is equivalent to `sin(x)`)
19- Comprehensive error handling
20- No_std compatibility for embedded systems
21- Integration with CMSIS-DSP for ARM Cortex-M
22
23## Quick Start
24
25Here's a basic example of evaluating a math expression:
26
27```rust
28use exp_rs::engine::interp;
29
30fn main() {
31    // Simple expression evaluation
32    let result = interp("2 + 3 * 4", None).unwrap();
33    assert_eq!(result, 14.0); // 2 + (3 * 4) = 14
34
35    // Using built-in functions and constants
36    let result = interp("sin(pi/4) + cos(pi/4)", None).unwrap();
37    assert!(result - 1.414 < 0.001); // Approximately √2
38}
39```
40
41## Using Variables and Constants
42
43```text
44// Example of using variables and constants:
45
46// Create an evaluation context
47let mut ctx = EvalContext::new();
48
49// Add variables
50ctx.set_parameter("x", 5.0);
51ctx.set_parameter("y", 10.0);
52
53// Add constants - these won't change once set
54ctx.constants.insert("FACTOR".to_string(), 2.5);
55
56// Evaluate expression with variables and constants
57let result = interp("x + y * FACTOR", Some(Rc::new(ctx))).unwrap();
58// Result: 30.0 (5 + (10 * 2.5) = 30)
59```
60
61## Arrays and Object Attributes
62
63```text
64// Example for arrays and object attributes:
65
66// Add an array
67ctx.arrays.insert("data".to_string(), vec![10.0, 20.0, 30.0, 40.0, 50.0]);
68
69// Add an object with attributes
70let mut point = HashMap::new();
71point.insert("x".to_string(), 3.0);
72point.insert("y".to_string(), 4.0);
73ctx.attributes.insert("point".to_string(), point);
74
75// Access array elements in expressions
76interp("data[2]", Some(Rc::new(ctx))).unwrap(); // Returns 30.0
77
78// Access attributes in expressions
79interp("point.x + point.y", Some(Rc::new(ctx))).unwrap(); // Returns 7.0
80
81// Combine array and attribute access in expressions
82interp("sqrt(point.x^2 + point.y^2) + data[0]", Some(Rc::new(ctx))).unwrap();
83// Result: sqrt(3^2 + 4^2) + 10 = 5 + 10 = 15
84```
85
86## Custom Functions
87
88### Native Functions
89
90```rust,no_run
91extern crate alloc;
92use exp_rs::context::EvalContext;
93use exp_rs::engine::interp;
94use alloc::rc::Rc;
95
96fn main() {
97    let mut ctx = EvalContext::new();
98
99    // Register a native function that sums all arguments
100    ctx.register_native_function("sum", 3, |args| {
101        args.iter().sum()
102    });
103
104    // Use the custom function
105    let result = interp("sum(1, 2, 3)", Some(Rc::new(ctx))).unwrap();
106    assert_eq!(result, 6.0);
107}
108```
109
110### Expression Functions
111
112```rust,no_run
113extern crate alloc;
114use exp_rs::context::EvalContext;
115use exp_rs::engine::interp;
116use alloc::rc::Rc;
117
118fn main() {
119    let mut ctx = EvalContext::new();
120
121    // Register an expression function
122    ctx.register_expression_function(
123        "hypotenuse",
124        &["a", "b"],
125        "sqrt(a^2 + b^2)"
126    ).unwrap();
127
128    // Use the custom function
129    let result = interp("hypotenuse(3, 4)", Some(Rc::new(ctx))).unwrap();
130    assert_eq!(result, 5.0);
131}
132```
133
134## Performance Optimization with AST Caching
135
136For repeated evaluations of the same expression with different variables:
137
138```rust,no_run
139extern crate alloc;
140use exp_rs::context::EvalContext;
141use exp_rs::engine::interp;
142use alloc::rc::Rc;
143
144fn main() {
145    let mut ctx = EvalContext::new();
146    ctx.enable_ast_cache(); // Enable AST caching
147
148    // First evaluation will parse and cache the AST
149    ctx.set_parameter("x", 1.0);
150    let result1 = interp("x^2 + 2*x + 1", Some(Rc::new(ctx.clone()))).unwrap();
151    assert_eq!(result1, 4.0); // 1^2 + 2*1 + 1 = 4
152
153    // Subsequent evaluations with the same expression will reuse the cached AST
154    ctx.set_parameter("x", 2.0);
155    let result2 = interp("x^2 + 2*x + 1", Some(Rc::new(ctx.clone()))).unwrap();
156    assert_eq!(result2, 9.0); // 2^2 + 2*2 + 1 = 9
157
158    // This is much faster for repeated evaluations
159    ctx.set_parameter("x", 3.0);
160    let result3 = interp("x^2 + 2*x + 1", Some(Rc::new(ctx))).unwrap();
161    assert_eq!(result3, 16.0); // 3^2 + 2*3 + 1 = 16
162}
163```
164
165## Using on Embedded Systems (no_std)
166
167exp-rs is designed to work in no_std environments with the alloc crate:
168
169```rust
170extern crate alloc;
171use exp_rs::interp;
172use exp_rs::EvalContext;
173use alloc::rc::Rc;
174// When using in a no_std environment with alloc:
175
176// This defines an FFI function that can be called from C code
177pub extern "C" fn evaluate_expression(x: f32, y: f32) -> f32 {
178    // Create an evaluation context
179    let mut ctx = EvalContext::new();
180
181    // Set parameters
182    ctx.set_parameter("x", x as f64);
183    ctx.set_parameter("y", y as f64);
184
185    // Evaluate the expression
186    let result = interp("sqrt(x^2 + y^2)", Some(Rc::new(ctx))).unwrap();
187
188    // Convert back to f32 for C compatibility
189    result as f32
190}
191```
192
193## Disabling Built-in Math Functions
194
195For embedded systems where you want to provide your own math implementations:
196
197```rust
198// In Cargo.toml:
199// exp-rs = { version = "0.1", default-features = false, features = ["f32", "no-builtin-math"] }
200extern crate alloc;
201use exp_rs::context::EvalContext;
202use exp_rs::engine::interp;
203use alloc::rc::Rc;
204use libm::{sin, cos};
205
206fn main() {
207    let mut ctx = EvalContext::new();
208
209    // Register custom math functions using standard Rust methods
210    ctx.register_native_function("sin", 1, |args| args[0].sin());
211    ctx.register_native_function("cos", 1, |args| args[0].cos());
212    ctx.register_native_function("sqrt", 1, |args| args[0].sqrt());
213
214    // Now you can use these functions
215    let result = interp("sin(0.5) + cos(0.5)", Some(Rc::new(ctx))).unwrap();
216    assert_eq!(result, sin(0.5) + cos(0.5)); // sin(0.5) + cos(0.5)
217    println!("Result: {}", result);
218}
219```
220
221## Error Handling
222
223Comprehensive error handling is provided:
224
225```rust
226extern crate alloc;
227use exp_rs::context::EvalContext;
228use exp_rs::engine::interp;
229use exp_rs::error::ExprError;
230use alloc::rc::Rc;
231
232fn main() {
233    let ctx = EvalContext::new();
234
235    // Handle syntax errors
236    match interp("2 + * 3", Some(Rc::new(ctx.clone()))) {
237        Ok(_) => println!("Unexpected success"),
238        Err(ExprError::Syntax(msg)) => println!("Syntax error: {}", msg),
239        Err(e) => println!("Unexpected error: {:?}", e),
240    }
241
242    // Handle unknown variables
243    match interp("x + 5", Some(Rc::new(ctx.clone()))) {
244        Ok(_) => println!("Unexpected success"),
245        Err(ExprError::UnknownVariable { name }) => println!("Unknown variable: {}", name),
246        Err(e) => println!("Unexpected error: {:?}", e),
247    }
248
249    // Handle division by zero
250    match interp("1 / 0", Some(Rc::new(ctx))) {
251        Ok(result) => {
252            if result.is_infinite() {
253                println!("Division by zero correctly returned infinity")
254            } else {
255                println!("Unexpected result: {}", result)
256            }
257        },
258        Err(e) => println!("Unexpected error: {:?}", e),
259    }
260}
261```
262
263## Supported Grammar
264
265exp-rs supports a superset of the original TinyExpr grammar, closely matching the [tinyexpr++](https://github.com/Blake-Madden/tinyexpr-plusplus) grammar, including:
266
267- Multi-character operators: `&&`, `||`, `==`, `!=`, `<=`, `>=`, `<<`, `>>`, `<<<`, `>>>`, `**`, `<>`
268- Logical, comparison, bitwise, and exponentiation operators with correct precedence and associativity
269- List expressions and both comma and semicolon as separators
270- Function call syntax supporting both parentheses and juxtaposition
271- Array and attribute access
272- Right-associative exponentiation
273
274### Operator Precedence and Associativity
275
276From lowest to highest precedence:
277
278| Precedence | Operators                                 | Associativity      |
279|------------|-------------------------------------------|--------------------|
280| 1          | `,` `;`                                   | Left               |
281| 2          | `||`                                      | Left               |
282| 3          | `&&`                                      | Left               |
283| 4          | `|`                                       | Left (bitwise OR)  |
284| 6          | `&`                                       | Left (bitwise AND) |
285| 7          | `==` `!=` `<` `>` `<=` `>=` `<>`          | Left (comparison)  |
286| 8          | `<<` `>>` `<<<` `>>>`                     | Left (bit shifts)  |
287| 9          | `+` `-`                                   | Left               |
288| 10         | `*` `/` `%`                               | Left               |
289| 14         | unary `+` `-` `~`                         | Right (unary)      |
290| 15         | `^`                                       | Right              |
291| 16         | `**`                                      | Right              |
292
293### Built-in Functions
294
295The following functions are available by default (unless `no-builtin-math` is enabled):
296
297- Trigonometric: `sin`, `cos`, `tan`, `asin`, `acos`, `atan`, `atan2`
298- Hyperbolic: `sinh`, `cosh`, `tanh`
299- Exponential/Logarithmic: `exp`, `log`, `log10`, `ln`
300- Power/Root: `sqrt`, `pow`
301- Rounding: `ceil`, `floor`
302- Comparison: `max`, `min`
303- Misc: `abs`, `sign`
304
305### Built-in Constants
306
307- `pi`: 3.14159... (π)
308- `e`: 2.71828... (Euler's number)
309
310## Feature Flags
311
312- `no-builtin-math`: Disables all built-in math functions. You must register your own.
313- `f32`: Use 32-bit floating point (single precision) for calculations
314- `f64`: Use 64-bit floating point (double precision) for calculations (default)
315
316Only one of `f32` or `f64` can be enabled at a time.
317
318## Embedded Systems Support
319
320exp-rs provides extensive support for embedded systems:
321
322- `no_std` compatible with the `alloc` crate
323- Configurable precision with `f32`/`f64` options
324- Option to disable built-in math functions and provide custom implementations
325- Integration with CMSIS-DSP for ARM Cortex-M processors
326- Meson build system integration for cross-compilation
327- QEMU test harness for validating on ARM hardware
328- Optional C FFI for calling from non-Rust code
329
330## Attribution
331
332exp-rs began as a fork of [tinyexpr-rs](https://github.com/kondrak/tinyexpr-rs) by Krzysztof Kondrak, which itself was a port of the [TinyExpr](https://github.com/codeplea/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](https://github.com/Blake-Madden/tinyexpr-plusplus).
333
334"#]
335
336// Re-export alloc for no_std compatibility
337#[cfg(all(not(test), target_arch = "arm"))]
338extern crate alloc;
339#[cfg(all(not(test), target_arch = "arm"))]
340pub use alloc::boxed::Box;
341#[cfg(all(not(test), target_arch = "arm"))]
342pub use alloc::string::{String, ToString};
343#[cfg(all(not(test), target_arch = "arm"))]
344pub use alloc::vec::Vec;
345
346// For non-ARM targets, keep the original behavior
347#[cfg(not(all(not(test), target_arch = "arm")))]
348#[cfg(not(test))]
349extern crate alloc;
350#[cfg(not(all(not(test), target_arch = "arm")))]
351#[cfg(not(test))]
352pub use alloc::boxed::Box;
353#[cfg(not(all(not(test), target_arch = "arm")))]
354#[cfg(not(test))]
355pub use alloc::string::{String, ToString};
356#[cfg(not(all(not(test), target_arch = "arm")))]
357#[cfg(not(test))]
358pub use alloc::vec::Vec;
359
360// Ensure core::result::Result, core::result::Result::Ok, and core::result::Result::Err are in scope for no_std/serde
361
362pub mod context;
363pub mod engine;
364pub mod error;
365pub mod eval;
366pub mod expression_functions;
367pub mod ffi;
368pub mod functions;
369pub mod lexer;
370pub mod types;
371
372pub use context::*;
373pub use engine::*;
374pub use functions::*;
375pub use types::*;
376
377pub use ffi::*;
378
379// Compile-time check: only one of f32 or f64 can be enabled
380#[cfg(all(feature = "f32", feature = "f64"))]
381compile_error!("You must enable only one of the features: 'f32' or 'f64', not both.");
382
383/// Define the floating-point type based on feature flags
384#[cfg(feature = "f32")]
385pub type Real = f32;
386
387#[cfg(feature = "f64")]
388pub type Real = f64;
389
390pub mod constants {
391    use super::Real;
392
393    #[cfg(feature = "f32")]
394    pub const PI: Real = core::f32::consts::PI;
395    #[cfg(feature = "f32")]
396    pub const E: Real = core::f32::consts::E;
397    #[cfg(feature = "f32")]
398    pub const TEST_PRECISION: Real = 1e-6;
399
400    #[cfg(feature = "f64")]
401    pub const PI: Real = core::f64::consts::PI;
402    #[cfg(feature = "f64")]
403    pub const E: Real = core::f64::consts::E;
404    #[cfg(feature = "f64")]
405    pub const TEST_PRECISION: Real = 1e-10;
406}
407
408/// Utility macro to check if two floating point values are approximately equal
409/// within a specified epsilon. Supports optional format arguments like assert_eq!.
410#[macro_export]
411macro_rules! assert_approx_eq {
412    // Case 1: assert_approx_eq!(left, right) -> use default epsilon
413    ($left:expr, $right:expr $(,)?) => {
414        $crate::assert_approx_eq!($left, $right, $crate::constants::TEST_PRECISION)
415    };
416    // Case 2: assert_approx_eq!(left, right, epsilon) -> use specified epsilon
417    ($left:expr, $right:expr, $epsilon:expr $(,)?) => {{
418        let left_val = $left;
419        let right_val = $right;
420        let eps = $epsilon;
421
422        // Use a default message if none is provided
423        let message = format!(
424            "assertion failed: `(left ≈ right)` \
425             (left: `{}`, right: `{}`, epsilon: `{}`)",
426            left_val, right_val, eps
427        );
428
429        if left_val.is_nan() && right_val.is_nan() {
430            // NaN == NaN for our purposes
431        } else if left_val.is_infinite()
432            && right_val.is_infinite()
433            && left_val.signum() == right_val.signum()
434        {
435            // Same-signed infinities are equal
436        } else {
437            assert!((left_val - right_val).abs() < eps, "{}", message);
438        }
439    }};
440    // Case 3: assert_approx_eq!(left, right, epsilon, "format message") -> use specified epsilon and message
441    ($left:expr, $right:expr, $epsilon:expr, $msg:literal $(,)?) => {{
442        let left_val = $left;
443        let right_val = $right;
444        let eps = $epsilon;
445
446        if left_val.is_nan() && right_val.is_nan() {
447            // NaN == NaN for our purposes
448        } else if left_val.is_infinite()
449            && right_val.is_infinite()
450            && left_val.signum() == right_val.signum()
451        {
452            // Same-signed infinities are equal
453        } else {
454            assert!((left_val - right_val).abs() < eps, $msg);
455        }
456    }};
457    // Case 4: assert_approx_eq!(left, right, epsilon, "format message with args", args...) -> use specified epsilon and formatted message
458    ($left:expr, $right:expr, $epsilon:expr, $fmt:expr, $($arg:tt)+) => {{
459        let left_val = $left;
460        let right_val = $right;
461        let eps = $epsilon;
462
463        if left_val.is_nan() && right_val.is_nan() {
464            // NaN == NaN for our purposes
465        } else if left_val.is_infinite()
466            && right_val.is_infinite()
467            && left_val.signum() == right_val.signum()
468        {
469            // Same-signed infinities are equal
470        } else {
471            assert!((left_val - right_val).abs() < eps, $fmt, $($arg)+);
472        }
473    }};
474}