1#![cfg_attr(all(not(test), target_arch = "arm"), no_std)]
2# 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#[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#[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
360pub 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#[cfg(all(feature = "f32", feature = "f64"))]
381compile_error!("You must enable only one of the features: 'f32' or 'f64', not both.");
382
383#[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#[macro_export]
411macro_rules! assert_approx_eq {
412 ($left:expr, $right:expr $(,)?) => {
414 $crate::assert_approx_eq!($left, $right, $crate::constants::TEST_PRECISION)
415 };
416 ($left:expr, $right:expr, $epsilon:expr $(,)?) => {{
418 let left_val = $left;
419 let right_val = $right;
420 let eps = $epsilon;
421
422 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 } else if left_val.is_infinite()
432 && right_val.is_infinite()
433 && left_val.signum() == right_val.signum()
434 {
435 } else {
437 assert!((left_val - right_val).abs() < eps, "{}", message);
438 }
439 }};
440 ($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 } else if left_val.is_infinite()
449 && right_val.is_infinite()
450 && left_val.signum() == right_val.signum()
451 {
452 } else {
454 assert!((left_val - right_val).abs() < eps, $msg);
455 }
456 }};
457 ($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 } else if left_val.is_infinite()
466 && right_val.is_infinite()
467 && left_val.signum() == right_val.signum()
468 {
469 } else {
471 assert!((left_val - right_val).abs() < eps, $fmt, $($arg)+);
472 }
473 }};
474}