exp_rs/lib.rs
1// #![cfg_attr(all(not(test), target_arch = "arm"), no_std)]
2#![cfg_attr(all(not(test), target_arch = "arm"), no_std)]
3//! exp-rs
4//!
5//! A minimal, extensible, no_std-friendly math expression parser and evaluator for Rust.
6//!
7//! # Overview
8//!
9//! 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.
10//!
11//! Key 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//! - Standard function call syntax with parentheses (`sin(x)`, `cos(y)`, etc.)
19//! - Comprehensive error handling
20//! - No_std compatibility for embedded systems
21//!
22//! # Quick Start
23//!
24//! Here's a basic example of evaluating a math expression:
25//!
26//! ```rust
27//! use exp_rs::interp;
28//!
29//! fn main() {
30//! // Simple expression evaluation
31//! let result = interp("2 + 3 * 4", None).unwrap();
32//! assert_eq!(result, 14.0); // 2 + (3 * 4) = 14
33//!
34//! #[cfg(feature = "libm")]
35//! {
36//! // Using built-in functions and constants
37//! let result = interp("sin(pi/4) + cos(pi/4)", None).unwrap();
38//! assert!(result - 1.414 < 0.001); // Approximately √2
39//! }
40//! }
41//! ```
42//!
43//! # Expression API - The Primary Interface
44//!
45//! The `Expression` struct provides the most efficient way to evaluate expressions,
46//! especially when you need to evaluate the same expression multiple times with
47//! different parameter values. It uses arena allocation for zero-allocation
48//! evaluation after parsing.
49//!
50//! ## Simple Expression Evaluation
51//!
52//! ```rust
53//! use exp_rs::Expression;
54//! use bumpalo::Bump;
55//!
56//! // Create an arena for memory allocation
57//! let arena = Bump::new();
58//!
59//! // Evaluate a simple expression without variables
60//! let result = Expression::eval_simple("2 + 3 * 4", &arena).unwrap();
61//! assert_eq!(result, 14.0);
62//! ```
63//!
64//! ## Expressions with Parameters
65//!
66//! ```rust
67//! use exp_rs::{Expression, EvalContext};
68//! use bumpalo::Bump;
69//! use std::rc::Rc;
70//!
71//! let arena = Bump::new();
72//!
73//! // Method 1: Using batch builder
74//! let mut builder = Expression::new(&arena);
75//! builder.add_parameter("x", 3.0).unwrap();
76//! builder.add_parameter("y", 4.0).unwrap();
77//! builder.add_expression("x^2 + y").unwrap();
78//! builder.eval(&Rc::new(EvalContext::new())).unwrap();
79//! let result = builder.get_result(0).unwrap();
80//! assert_eq!(result, 13.0); // 3^2 + 4 = 13
81//!
82//! // Method 2: Using eval_with_params for one-shot evaluation
83//! let params = [("x", 3.0), ("y", 4.0)];
84//! let result = Expression::eval_with_params(
85//! "x^2 + y",
86//! ¶ms,
87//! &Rc::new(EvalContext::new()),
88//! &arena
89//! ).unwrap();
90//! assert_eq!(result, 13.0);
91//! ```
92//!
93//! ## Efficient Repeated Evaluation
94//!
95//! The Expression API excels when evaluating the same expression multiple times:
96//!
97//! ```rust
98//! use exp_rs::{Expression, EvalContext};
99//! use bumpalo::Bump;
100//! use std::rc::Rc;
101//!
102//! let arena = Bump::new();
103//! let ctx = Rc::new(EvalContext::new());
104//!
105//! // Parse once, evaluate many times
106//! let mut builder = Expression::new(&arena);
107//! builder.add_parameter("a", 1.0).unwrap();
108//! builder.add_parameter("b", -3.0).unwrap();
109//! builder.add_parameter("c", 2.0).unwrap();
110//! builder.add_parameter("x", 0.0).unwrap();
111//! builder.add_expression("a * x^2 + b * x + c").unwrap();
112//!
113//! // Evaluate for different x values
114//! for x in [0.0, 1.0, 2.0, 3.0] {
115//! builder.set("x", x).unwrap();
116//! builder.eval(&ctx).unwrap();
117//! let y = builder.get_result(0).unwrap();
118//! println!("f({}) = {}", x, y);
119//! }
120//! ```
121//!
122//! ## Batch Expression Evaluation
123//!
124//! Evaluate multiple expressions with shared parameters:
125//!
126//! ```rust
127//! use exp_rs::{Expression, EvalContext};
128//! use bumpalo::Bump;
129//! use std::rc::Rc;
130//!
131//! let arena = Bump::new();
132//! let ctx = Rc::new(EvalContext::new());
133//!
134//! let mut batch = Expression::new(&arena);
135//!
136//! // Add shared parameters
137//! batch.add_parameter("radius", 5.0).unwrap();
138//!
139//! // Add multiple expressions
140//! let area_idx = batch.add_expression("pi * radius^2").unwrap();
141//! let circumference_idx = batch.add_expression("2 * pi * radius").unwrap();
142//!
143//! // Evaluate all expressions
144//! batch.eval(&ctx).unwrap();
145//!
146//! println!("Area: {}", batch.get_result(area_idx).unwrap());
147//! println!("Circumference: {}", batch.get_result(circumference_idx).unwrap());
148//!
149//! // Update parameter and re-evaluate
150//! batch.set("radius", 10.0).unwrap();
151//! batch.eval(&ctx).unwrap();
152//!
153//! println!("New area: {}", batch.get_result(area_idx).unwrap());
154//! println!("New circumference: {}", batch.get_result(circumference_idx).unwrap());
155//! ```
156//!
157//! ## Relationship to interp()
158//!
159//! The `interp()` function remains available for backward compatibility and simple
160//! one-shot evaluations. Internally, it uses the Expression API:
161//!
162//! ```rust
163//! use exp_rs::interp;
164//!
165//! // These are equivalent:
166//! let result1 = interp("2 + 3", None).unwrap();
167//!
168//! use exp_rs::Expression;
169//! use bumpalo::Bump;
170//! let arena = Bump::new();
171//! let result2 = Expression::eval_simple("2 + 3", &arena).unwrap();
172//!
173//! assert_eq!(result1, result2);
174//! ```
175//!
176//! For new code, especially when evaluating expressions multiple times or when
177//! performance is critical, prefer using the Expression API directly.
178//!
179//! # Supported Grammar
180//!
181//! exp-rs supports a superset of the original TinyExpr grammar, closely matching the tinyexpr++ grammar, including:
182//!
183//! - Multi-character operators: `&&`, `||`, `==`, `!=`, `<=`, `>=`, `<<`, `>>`, `<<<`, `>>>`, `**`, `<>`
184//! - Logical operators (`&&`, `||`) with short-circuit evaluation
185//! - Logical, comparison, bitwise, and exponentiation operators with correct precedence and associativity
186//! - List expressions and both comma and semicolon as separators
187//! - Standard function call syntax with parentheses
188//! - Array and attribute access
189//! - Right-associative exponentiation
190//!
191//! ## Operator Precedence and Associativity
192//!
193//! From lowest to highest precedence:
194//!
195//! | Precedence | Operators | Associativity |
196//! |------------|-------------------------------------|--------------------|
197//! | 1 | `,` `;` | Left |
198//! | 2 | `||` | Left |
199//! | 3 | `&&` | Left |
200//! | 4 | `|` | Left (bitwise OR) |
201//! | 6 | `&` | Left (bitwise AND) |
202//! | 7 | `==` `!=` `<` `>` `<=` `>=` `<>` | Left (comparison) |
203//! | 8 | `<<` `>>` `<<<` `>>>` | Left (bit shifts) |
204//! | 9 | `+` `-` | Left |
205//! | 10 | `*` `/` `%` | Left |
206//! | 14 | unary `+` `-` `~` | Right (unary) |
207//! | 15 | `^` | Right |
208//! | 16 | `**` | Right |
209//!
210//! ## Built-in Functions
211//!
212//! The following functions are available by default when the `libm` feature is enabled. Without the `libm` feature,
213//! these functions will not be automatically registered and must be defined by the user with native or expression functions:
214//!
215//! - Trigonometric: `sin`, `cos`, `tan`, `asin`, `acos`, `atan`, `atan2`
216//! - Hyperbolic: `sinh`, `cosh`, `tanh`
217//! - Exponential/Logarithmic: `exp`, `log`, `log10`, `ln`
218//! - Power/Root: `sqrt`, `pow`
219//! - Rounding: `ceil`, `floor`
220//! - Comparison: `max`, `min`
221//! - Misc: `abs`, `sign`
222//!
223//! ## Built-in Constants
224//!
225//! - `pi`: 3.14159... (π)
226//! - `e`: 2.71828... (Euler's number)
227//!
228//! # Feature Flags
229//!
230//! - `libm`: Enables built-in math functions using the libm library. Without this feature, you must register your own math functions.
231//! - `f32`: Use 32-bit floating point (single precision) for calculations
232//!
233//! When `f32` is not specified, 64-bit floating point (double precision) is used by default.
234//!
235//! # Embedded Systems Support
236//!
237//! exp-rs provides extensive support for embedded systems:
238//!
239//! - `no_std` compatible with the `alloc` crate
240//! - Configurable precision with `f32`/`f64` options
241//! - Option to disable built-in math functions and provide custom implementations
242//! - Tested example using qemu CMSIS-DSP math functions (test in repo)
243//! - Meson build system integration for cross-compilation
244//! - QEMU test harness for validating on ARM hardware
245//! - Optional C FFI for calling from non-Rust code
246//!
247//! # Using Variables and Constants
248//!
249//! ```rust
250//! extern crate alloc;
251//! use exp_rs::context::EvalContext;
252//! use exp_rs::interp;
253//! use alloc::rc::Rc;
254//!
255//! // Create an evaluation context
256//! let mut ctx = EvalContext::new();
257//!
258//! // Add variables
259//! ctx.set_parameter("x", 5.0);
260//! ctx.set_parameter("y", 10.0);
261//!
262//! // Add constants - these won't change once set
263//! ctx.constants.insert("FACTOR".try_into().unwrap(), 2.5).unwrap();
264//!
265//! // Evaluate expression with variables and constants
266//! let result = interp("x + y * FACTOR", Some(Rc::new(ctx))).unwrap();
267//! // Result: 30.0 (5 + (10 * 2.5) = 30)
268//! ```
269//!
270//! # Arrays and Object Attributes
271//!
272//! ```rust
273//! extern crate alloc;
274//! use exp_rs::interp;
275//! use exp_rs::context::EvalContext;
276//! use heapless::FnvIndexMap;
277//! use alloc::rc::Rc;
278//!
279//! // Create an evaluation context
280//! let mut ctx = EvalContext::new();
281//! // Add an array
282//! ctx.arrays.insert("data".try_into().unwrap(), vec![10.0, 20.0, 30.0, 40.0, 50.0]).unwrap();
283//!
284//! // Add an object with attributes
285//! let mut point = FnvIndexMap::new();
286//! point.insert("x".try_into().unwrap(), 3.0).unwrap();
287//! point.insert("y".try_into().unwrap(), 4.0).unwrap();
288//! ctx.attributes.insert("point".try_into().unwrap(), point).unwrap();
289//! let ctx_rc = Rc::new(ctx);
290//!
291//! // Access array elements in expressions
292//! interp("data[2]", Some(Rc::clone(&ctx_rc))).unwrap(); // Returns 30.0
293//!
294//! // Access attributes in expressions
295//! interp("point.x + point.y", Some(Rc::clone(&ctx_rc))).unwrap(); // Returns 7.0
296//!
297//! # #[cfg(feature = "libm")]
298//! # {
299//! // Combine array and attribute access in expressions
300//! interp("sqrt(point.x^2 + point.y^2) + data[0]", Some(Rc::clone(&ctx_rc))).unwrap();
301//! // Result: sqrt(3^2 + 4^2) + 10 = 5 + 10 = 15
302//! # }
303//! ```
304//!
305//! # Custom Functions
306//!
307//! exp-rs allows you to define custom functions in two ways:
308//!
309//! ## Native Functions
310//!
311//! Native functions can be defined at compile time:
312//!
313//! ```rust
314//! extern crate alloc;
315//! use exp_rs::context::EvalContext;
316//! use exp_rs::engine::interp;
317//! use alloc::rc::Rc;
318//!
319//! fn main() {
320//! let mut ctx = EvalContext::new();
321//!
322//! // Register a native function that sums all arguments
323//! ctx.register_native_function("sum", 3, |args| {
324//! args.iter().sum()
325//! });
326//!
327//! // Use the custom function
328//! let result = interp("sum(1, 2, 3)", Some(Rc::new(ctx))).unwrap();
329//! assert_eq!(result, 6.0);
330//! }
331//! ```
332//!
333//! ## Expression Functions
334//!
335//! Expression functions can be registered and passed into the library at runtime:
336//!
337//! ```rust
338//! # #[cfg(feature = "libm")]
339//! # {
340//! extern crate alloc;
341//! use exp_rs::context::EvalContext;
342//! use exp_rs::expression::Expression;
343//! use alloc::rc::Rc;
344//! use bumpalo::Bump;
345//!
346//! fn main() {
347//! let arena = Bump::new();
348//! let mut builder = Expression::new(&arena);
349//! let ctx = EvalContext::new();
350//!
351//! // Register an expression function in the batch
352//! builder.register_expression_function(
353//! "hypotenuse",
354//! &["a", "b"],
355//! "sqrt(a^2 + b^2)"
356//! ).unwrap();
357//!
358//! // Add expression that uses the custom function
359//! builder.add_expression("hypotenuse(3, 4)").unwrap();
360//!
361//! // Evaluate the batch
362//! builder.eval(&Rc::new(ctx)).unwrap();
363//! let result = builder.get_result(0).unwrap();
364//! assert_eq!(result, 5.0);
365//! }
366//! # }
367//! ```
368//!
369//! # Performance Optimization with AST Caching
370//!
371//! For repeated evaluations of the same expression with different variables:
372//!
373//! ```rust
374//! extern crate alloc;
375//! use exp_rs::context::EvalContext;
376//! use exp_rs::engine::interp;
377//! use alloc::rc::Rc;
378//!
379//! fn main() {
380//! let mut ctx = EvalContext::new();
381//!
382//! // Evaluate expression with different parameter values
383//! ctx.set_parameter("x", 1.0).unwrap();
384//! let result1 = interp("x^2 + 2*x + 1", Some(Rc::new(ctx.clone()))).unwrap();
385//! assert_eq!(result1, 4.0); // 1^2 + 2*1 + 1 = 4
386//!
387//! // Update parameter and evaluate again
388//! ctx.set_parameter("x", 2.0).unwrap();
389//! let result2 = interp("x^2 + 2*x + 1", Some(Rc::new(ctx.clone()))).unwrap();
390//! assert_eq!(result2, 9.0); // 2^2 + 2*2 + 1 = 9
391//!
392//! // The arena-based implementation provides efficient evaluation
393//! ctx.set_parameter("x", 3.0).unwrap();
394//! let result3 = interp("x^2 + 2*x + 1", Some(Rc::new(ctx))).unwrap();
395//! assert_eq!(result3, 16.0); // 3^2 + 2*3 + 1 = 16
396//! }
397//! ```
398//!
399//! # Using on Embedded Systems (no_std)
400//!
401//! exp-rs is designed to work in no_std environments with the alloc crate.
402//! A C header is automatically generated at compile time using Cbindgen.
403//!
404//! ```rust
405//! extern crate alloc;
406//! use exp_rs::interp;
407//! use exp_rs::EvalContext;
408//! use exp_rs::Real;
409//! use alloc::rc::Rc;
410//!
411//! // This defines an FFI function that can be called from C code
412//! pub extern "C" fn evaluate_expression(x: f32, y: f32) -> f32 {
413//! // Note: Real is either f32 or f64 depending on feature flags
414//! // Create an evaluation context
415//! let mut ctx = EvalContext::new();
416//!
417//! // Set parameters
418//! ctx.set_parameter("x", x as Real);
419//! ctx.set_parameter("y", y as Real);
420//!
421//! // Evaluate the expression
422//! let result = interp("sqrt(x^2 + y^2)", Some(Rc::new(ctx))).unwrap();
423//!
424//! // Convert back to f32 for C compatibility
425//! result as f32
426//! }
427//! ```
428//!
429//! # Disabling Built-in Math Functions
430//!
431//! For embedded systems where you want to provide your own math implementations:
432//!
433//! ```rust
434//! extern crate alloc;
435//! use exp_rs::context::EvalContext;
436//! use exp_rs::engine::interp;
437//! use alloc::rc::Rc;
438//!
439//! fn main() {
440//! let mut ctx = EvalContext::new();
441//!
442//! // Register custom math functions
443//! ctx.register_native_function("sin", 1, |args| args[0].sin());
444//! ctx.register_native_function("cos", 1, |args| args[0].cos());
445//! ctx.register_native_function("sqrt", 1, |args| args[0].sqrt());
446//!
447//! // Use the functions
448//! let result = interp("sin(0.5) + cos(0.5)", Some(Rc::new(ctx))).unwrap();
449//! println!("Result: {}", result);
450//! }
451//! ```
452//!
453//! # Error Handling
454//!
455//! Comprehensive error handling is provided:
456//!
457//! ```rust
458//! extern crate alloc;
459//! use exp_rs::context::EvalContext;
460//! use exp_rs::engine::interp;
461//! use exp_rs::error::ExprError;
462//! use alloc::rc::Rc;
463//!
464//! fn main() {
465//! let ctx = EvalContext::new();
466//!
467//! // Handle syntax errors
468//! match interp("2 + * 3", Some(Rc::new(ctx.clone()))) {
469//! Ok(_) => println!("Unexpected success"),
470//! Err(ExprError::Syntax(msg)) => println!("Syntax error: {}", msg),
471//! Err(e) => println!("Unexpected error: {:?}", e),
472//! }
473//!
474//! // Handle unknown variables
475//! match interp("x + 5", Some(Rc::new(ctx.clone()))) {
476//! Ok(_) => println!("Unexpected success"),
477//! Err(ExprError::UnknownVariable { name }) => println!("Unknown variable: {}", name),
478//! Err(e) => println!("Unexpected error: {:?}", e),
479//! }
480//!
481//! // Handle division by zero
482//! match interp("1 / 0", Some(Rc::new(ctx))) {
483//! Ok(result) => {
484//! if result.is_infinite() {
485//! println!("Division by zero correctly returned infinity")
486//! } else {
487//! println!("Unexpected result: {}", result)
488//! }
489//! },
490//! Err(e) => println!("Unexpected error: {:?}", e),
491//! }
492//! }
493//! ```
494//!
495//! # Attribution
496//!
497//! exp-rs began as a fork of tinyexpr-rs by Krzysztof Kondrak, which itself was a port of the TinyExpr C library
498//! by Lewis Van Winkle (codeplea). As the functionality expanded beyond the scope of the original TinyExpr,
499//! it evolved into a new project with additional features inspired by tinyexpr-plusplus.
500
501// Re-export alloc for no_std compatibility
502#[cfg(all(not(test), target_arch = "arm"))]
503extern crate alloc;
504
505// For tests, use std
506#[cfg(test)]
507extern crate std as alloc;
508
509#[cfg(test)]
510pub use std::string::{String, ToString};
511
512// Export common types regardless of mode
513#[cfg(all(not(test), target_arch = "arm"))]
514pub use alloc::boxed::Box;
515#[cfg(all(not(test), target_arch = "arm"))]
516pub use alloc::string::{String, ToString};
517#[cfg(all(not(test), target_arch = "arm"))]
518pub use alloc::vec::Vec;
519
520// For non-ARM targets, keep the original behavior
521#[cfg(not(all(not(test), target_arch = "arm")))]
522#[cfg(not(test))]
523extern crate alloc;
524#[cfg(not(all(not(test), target_arch = "arm")))]
525#[cfg(not(test))]
526pub use alloc::boxed::Box;
527#[cfg(not(all(not(test), target_arch = "arm")))]
528#[cfg(not(test))]
529pub use alloc::string::{String, ToString};
530#[cfg(not(all(not(test), target_arch = "arm")))]
531#[cfg(not(test))]
532pub use alloc::vec::Vec;
533
534// Ensure core::result::Result, core::result::Result::Ok, and core::result::Result::Err are in scope for no_std/serde
535
536pub mod context;
537pub mod engine;
538pub mod error;
539pub mod eval;
540pub mod evaluator;
541pub mod expression;
542pub mod expression_functions;
543pub mod ffi;
544pub mod functions;
545pub mod lexer;
546pub mod types;
547
548pub use context::*;
549pub use engine::*;
550pub use expression::{Expression, Param};
551pub use functions::*;
552pub use types::*;
553
554pub use ffi::*;
555
556// Re-export recursion depth tracking functions for testing
557#[cfg(test)]
558pub use eval::recursion::{get_recursion_depth, reset_recursion_depth, set_max_recursion_depth};
559
560// Re-export iterative evaluation components for batch processing
561pub use eval::iterative::{EvalEngine, eval_with_engine};
562
563// Compile-time check: only one of f32 or f64 can be enabled
564/// Define the floating-point type based on feature flags
565#[cfg(feature = "f32")]
566pub type Real = f32;
567
568#[cfg(not(feature = "f32"))]
569pub type Real = f64;
570
571pub mod constants {
572 use super::Real;
573
574 #[cfg(feature = "f32")]
575 pub const PI: Real = core::f32::consts::PI;
576 #[cfg(feature = "f32")]
577 pub const E: Real = core::f32::consts::E;
578 #[cfg(feature = "f32")]
579 pub const TEST_PRECISION: Real = 1e-6;
580
581 #[cfg(not(feature = "f32"))]
582 pub const PI: Real = core::f64::consts::PI;
583 #[cfg(not(feature = "f32"))]
584 pub const E: Real = core::f64::consts::E;
585 #[cfg(not(feature = "f32"))]
586 pub const TEST_PRECISION: Real = 1e-10;
587}
588
589/// Utility macro to check if two floating point values are approximately equal
590/// within a specified epsilon. Supports optional format arguments like assert_eq!.
591#[macro_export]
592macro_rules! assert_approx_eq {
593 // Case 1: assert_approx_eq!(left, right) -> use default epsilon
594 ($left:expr, $right:expr $(,)?) => {
595 $crate::assert_approx_eq!($left, $right, $crate::constants::TEST_PRECISION)
596 };
597 // Case 2: assert_approx_eq!(left, right, epsilon) -> use specified epsilon
598 ($left:expr, $right:expr, $epsilon:expr $(,)?) => {{
599 let left_val = $left;
600 let right_val = $right;
601 let eps = $epsilon;
602
603 // Use a default message if none is provided
604 let message = format!(
605 "assertion failed: `(left ≈ right)` \
606 (left: `{}`, right: `{}`, epsilon: `{}`)",
607 left_val, right_val, eps
608 );
609
610 if left_val.is_nan() && right_val.is_nan() {
611 // NaN == NaN for our purposes
612 } else if left_val.is_infinite()
613 && right_val.is_infinite()
614 && left_val.signum() == right_val.signum()
615 {
616 // Same-signed infinities are equal
617 } else {
618 assert!((left_val - right_val).abs() < eps, "{}", message);
619 }
620 }};
621 // Case 3: assert_approx_eq!(left, right, epsilon, "format message") -> use specified epsilon and message
622 ($left:expr, $right:expr, $epsilon:expr, $msg:literal $(,)?) => {{
623 let left_val = $left;
624 let right_val = $right;
625 let eps = $epsilon;
626
627 if left_val.is_nan() && right_val.is_nan() {
628 // NaN == NaN for our purposes
629 } else if left_val.is_infinite()
630 && right_val.is_infinite()
631 && left_val.signum() == right_val.signum()
632 {
633 // Same-signed infinities are equal
634 } else {
635 assert!((left_val - right_val).abs() < eps, $msg);
636 }
637 }};
638 // Case 4: assert_approx_eq!(left, right, epsilon, "format message with args", args...) -> use specified epsilon and formatted message
639 ($left:expr, $right:expr, $epsilon:expr, $fmt:expr, $($arg:tt)+) => {{
640 let left_val = $left;
641 let right_val = $right;
642 let eps = $epsilon;
643
644 if left_val.is_nan() && right_val.is_nan() {
645 // NaN == NaN for our purposes
646 } else if left_val.is_infinite()
647 && right_val.is_infinite()
648 && left_val.signum() == right_val.signum()
649 {
650 // Same-signed infinities are equal
651 } else {
652 assert!((left_val - right_val).abs() < eps, $fmt, $($arg)+);
653 }
654 }};
655}