# nanoexpr: math expression evaluator for embedded Rust
[](https://crates.io/crates/nanoexpr)
[](https://docs.rs/nanoexpr)
[](https://github.com/AlixANNERAUD/nanoexpr/actions/workflows/rust.yml)
[](https://codecov.io/gh/AlixANNERAUD/nanoexpr)
[](https://docs.rs/nanoexpr)
[](https://github.com/AlixANNERAUD/nanoexpr/blob/main/Cargo.toml)
[](https://github.com/AlixANNERAUD/nanoexpr/blob/main/LICENSE)
**Parse and evaluate math formulas typed at run time, on a microcontroller, without a heap.** `nanoexpr` is a tiny
`no_std` math expression parser and evaluator for Rust and C: it computes `"2 * (3 + 4) ^ 2 / sqrt(16)"` in a single
pass, with **zero heap allocation**, bounded stack use and errors that point at the faulty part of the input.
```rust
assert_eq!(nanoexpr::evaluate::<f64>("2 * (3 + 4) ^ 2 / sqrt(16)"), Ok(24.5));
```
| Heap allocations | **0**, checked by a test |
| Speed | about **290 ns** to parse and evaluate a formula, at least 1.4 times faster than the other crates, or **40 ns** compiled once [measured](#performance) |
| Size | **6 KiB** of WebAssembly for integers, 46 KiB with `f64` and every math function |
| Targets | Any `no_std` target: Cortex-M, RISC-V (ESP32-C3), WebAssembly, desktops. Built in CI for each |
| Numbers | `f32`, `f64`, integers, calculator-exact `Decimal`, exact `Fraction`, both at once like a TI-84 CE, or your own type |
| Languages | Rust, and C or C++ through a CMake project |
| Dependencies | None, except the optional pure Rust `libm` |
## Why nanoexpr?
Letting users type a formula is common: a calculator app, a sensor conversion set in the field (`raw * 0.0125 - 40`),
an alarm threshold, a spreadsheet cell, a game stat. Most expression crates are built for desktops: they need `std`,
allocate a syntax tree and strings on every call, and turn bad input into `NaN` or a panic. On a microcontroller that
means heap fragmentation, unpredictable memory use and crashes. `nanoexpr` is built for those constraints instead,
and is faster than the others even on a desktop.
Use it for:
- **Calculator apps** on handhelds, watches and embedded GUIs, with TI-style decimal numbers and a percent key.
- **Sensor formulas and unit conversions** configured without reflashing the firmware.
- **Thresholds, scaling and business rules** stored as text in a configuration file or a database.
- **WebAssembly and plugins**, where a small binary and no allocator matter.
## Features
- **No heap, ever.** No `alloc`, no syntax tree, no token buffer: the expression is computed while it is parsed. A
test with a counting allocator checks it.
- **Bounded stack.** Nesting is limited (32 levels by default, configurable) so hostile input cannot overflow the
stack, and call arguments live in a fixed-size array.
- **Any number type.** `f32`, `f64`, every primitive integer, or your own type (fixed-point...) through the `Number`
trait. Integers report overflow instead of wrapping.
- **Calculator-exact decimals.** The optional `Decimal` type computes in decimal like TI and Casio calculators:
14 significant digits and exponents up to ±99 by default, so `0.1 + 0.2` is exactly `0.3`, and turns results
into fractions like the `►Frac` key: `1/3 + 1/6` is `1/2`.
- **Exact fractions.** The optional `Fraction` type computes with rational numbers exactly, so `1/3 + 1/6` is
exactly `1/2`, and `Exact` keeps results as fractions while they are exact and turns them into decimals when they
are not, like a TI-84 CE.
- **Variables and functions from your code.** The evaluator asks a `Context` for each name it meets: implement the
trait, pass `(name, value)` pairs, or wrap closures. Contexts compose as tuples.
- **Scientific functions included.** `Math` provides `pi`, `e`, `sqrt`, `sin`, `cos`, `tan`, `exp`, `ln`, `log`,
`min`, `max`, `clamp`, `fact`, rounding and more.
- **Errors you can show.** A `Copy` error with a kind to match on (and translate) and the byte span of the input
that caused it. Division by zero, domain errors and overflow are errors, not silent `NaN` or infinities.
- **Calculator-friendly syntax.** `^` and `**` are right-associative (`2^3^2 = 512`), `-2^2 = -4`, `5!` is a
factorial, `%` can be a percentage (`200 * 15% = 30`) instead of the remainder, and implicit multiplication
(`2pi`, `3(x + 1)`) can be turned on.
- **Comparisons and logic for rules.** `<`, `==`, `&&`, `||`, `!` and `c ? a : b` with C precedences, giving `1` or
`0`, and short-circuiting so `x != 0 && 1 / x > 2` is no division by zero.
- **Compile once, evaluate many times.** A formula can be compiled into a buffer you provide and evaluated again
with new variables in tens of nanoseconds, still without a heap.
- **C and C++ bindings** with a header and a CMake project, still allocation-free.
- **No dependencies** besides the optional `libm`, and no `unsafe` code in the evaluator.
## Usage
```toml
[dependencies]
nanoexpr = "0.1"
```
### Variables and functions
```rust
use nanoexpr::{ErrorKind, Functions, Math};
let variables = [("raw", 2150.0), ("offset", -40.0)];
("celsius", _) => Some(Err(ErrorKind::ArgumentCount)),
_ => None,
});
let context = (variables, (functions, Math));
assert_eq!(nanoexpr::evaluate_with("raw * 0.0125 + offset", &context), Ok(-13.125));
assert_eq!(nanoexpr::evaluate_with("round(celsius(300))", &context), Ok(27.0));
```
For full control, implement `Context` on your own type:
```rust
use nanoexpr::Context;
struct Sensors {
readings: [i32; 4],
}
impl Context<i32> for Sensors {
fn variable(&self, name: &str) -> Option<i32> {
let index: usize = name.strip_prefix("s")?.parse().ok()?;
self.readings.get(index).copied()
}
}
let sensors = Sensors { readings: [12, 30, 7, 1] };
assert_eq!(nanoexpr::evaluate_with("(s0 + s1 + s2) / 3", &sensors), Ok(16));
```
### Errors
```rust
use nanoexpr::{ErrorKind, Span};
let error = nanoexpr::evaluate::<f64>("1 + 4 / (2 - 2)").unwrap_err();
assert_eq!(error.kind, ErrorKind::DivisionByZero);
assert_eq!(error.span, Span::new(4, 15));
assert_eq!(error.to_string(), "division by zero at 4..15");
```
### Thresholds and rules
Comparisons give `1` or `0`, and `&&`, `||` and `? :` skip what they do not need, so a rule can guard its own
divisions:
```rust
let context = [("temperature", 35.0), ("humidity", 20.0), ("flow", 0.0)];
assert_eq!(nanoexpr::evaluate_with("temperature > 30 && humidity < 40", &context), Ok(1.0));
assert_eq!(nanoexpr::evaluate_with("flow != 0 && 100 / flow > 2", &context), Ok(0.0));
assert_eq!(nanoexpr::evaluate_with("flow > 0 ? 100 / flow : -1", &context), Ok(-1.0));
```
### Calculator mode and limits
```rust
use nanoexpr::{Evaluator, Math, PercentSign};
let calculator = Evaluator::<_, 4>::new(&Math)
.percent_sign(PercentSign::Percentage)
.implicit_multiplication(true)
.max_depth(16);
assert_eq!(calculator.evaluate::<f64>("200 * 15%"), Ok(30.0));
assert_eq!(calculator.evaluate::<f64>("10 mod 4"), Ok(2.0));
assert_eq!(calculator.evaluate::<f64>("2(3 + 4)sqrt(4)"), Ok(28.0));
```
The second generic parameter is the most arguments a call can take (8 by default).
### Compile once, evaluate many times
To evaluate the same formula again and again, such as a sensor conversion at each reading, compile it once into a
buffer of instructions you provide, then evaluate the program with each new context. Compiling checks the syntax,
reports the same errors as `evaluate` would for the text, and computes operations on constants once. Evaluating
only runs the instructions, with a stack of values of fixed size, so it never allocates either:
```rust
use nanoexpr::{Instruction, Math, Program};
let mut code = [Instruction::default(); 32];
let program: Program<f64> = nanoexpr::compile("raw * (5 / 400) - 40", &mut code).unwrap();
for (raw, celsius) in [(4000.0, 10.0), (4800.0, 20.0)] {
assert_eq!(program.evaluate(&([("raw", raw)], Math)), Ok(celsius));
}
```
Variables and functions are resolved by the context at each evaluation, and a program gives exactly the results
and errors, with the same spans, as `evaluate` on the same text. `Evaluator::compile` applies the settings of an
evaluator, such as `percent_sign` or `implicit_multiplication`. A buffer too small for the program, or a formula
that needs more pending values than the stack holds (`Program<f64, 32>` by default), is an `ErrorKind::TooLong`
error.
### Decimal numbers, like a pocket calculator
Binary floats cannot represent most decimal fractions, so `0.1 + 0.2` is `0.30000000000000004` as an `f64`.
Calculators compute in decimal instead. With the `decimal` feature, `Decimal` does the same: a decimal floating-point
number of 14 significant digits with exponents up to ±99 by default, as on TI-83 and TI-84 calculators. Each
operation rounds its exact result once, half to even, and checks against Python's `decimal` module show they match.
```toml
[dependencies]
nanoexpr = { version = "0.1", features = ["decimal"] }
```
```rust
# #[cfg(feature = "decimal")] {
use nanoexpr::Decimal;
let sum: Decimal = nanoexpr::evaluate("0.1 + 0.2").unwrap();
assert_eq!(sum.to_string(), "0.3");
let one: Decimal = nanoexpr::evaluate("1 / 3 * 3").unwrap();
assert_eq!(one.to_string(), "0.99999999999999");
assert_eq!(format!("{one:.10}"), "1");
let price: Decimal = nanoexpr::evaluate("19.99 * 3").unwrap();
assert_eq!(price.to_string(), "59.97");
# }
```
Like a calculator, keep more digits than you show: formatting with a precision such as `{:.10}` rounds to that many
significant digits, which hides the rounding of intermediate results. Choose other sizes with the generic
parameters, such as `Decimal<10, 99>` or `Decimal<18, 9999>`.
Like the `►Frac` key of TI calculators, `to_fraction` finds the simplest fraction a result is, up to a largest
denominator (9999 on TI-83 and TI-84), within one unit of its last digit. It gives a `Fraction` that displays as
`7/2`, or as the mixed number `3 1/2` with `{:#}`, and `None` for numbers such as `pi`:
```rust
# #[cfg(feature = "decimal")] {
use nanoexpr::Decimal;
let sum: Decimal = nanoexpr::evaluate("1/3 + 1/6").unwrap();
assert_eq!(sum.to_fraction(9999).unwrap().to_string(), "1/2");
let one: Decimal = nanoexpr::evaluate("1 / 3 * 3").unwrap();
assert_eq!(one.to_fraction(9999).unwrap().to_string(), "1");
let mixed: Decimal = nanoexpr::evaluate("2 + 5/4").unwrap();
assert_eq!(format!("{:#}", mixed.to_fraction(9999).unwrap()), "3 1/4");
# }
```
Every function of `Math` is computed in decimal too, without going through binary floats. Square roots, logarithms,
exponentials, trigonometric and hyperbolic functions and their inverses, and powers with any exponent work on 36
significant digits, then round once to the digits of the type. Arguments are taken as the exact decimals they are,
so results stay accurate next to the zeros of the functions and across the whole exponent range:
```rust
# #[cfg(feature = "decimal")] {
use nanoexpr::Decimal;
let sine: Decimal = nanoexpr::evaluate("sin(3.1415926535898)").unwrap();
assert_eq!(sine.to_string(), "-6.7615373566167e-15");
let large: Decimal<18, 9999> = nanoexpr::evaluate("exp(1000)").unwrap();
assert_eq!(large.to_string(), "1.97007111401704699e434");
# }
```
Checked against [mpmath](https://mpmath.org) on 3,806 results with 14 digits and 2,025 with 18 digits and exponents
up to ±9999, including arguments next to multiples of π, next to 1 and next to overflow, every result is correctly
rounded. Correct rounding is not guaranteed: in rare cases a result can be one unit in the last place away.
Trigonometric functions of arguments of `10^343` and beyond, where the digits of π kept run out, are `Domain`
errors.
### Exact fractions
With the `fraction` feature, `Fraction` computes with rational numbers exactly: `1/3 + 1/6` is `1/2`, not
`0.49999999999999`. Every value is kept in lowest terms, with a numerator and a denominator of up to 64 bits, and a
result that does not fit is an `Overflow` error rather than a rounded value. Decimal literals are exact too, so
`0.1 + 0.2` is `3/10`.
```toml
[dependencies]
nanoexpr = { version = "0.1", features = ["fraction"] }
```
```rust
# #[cfg(feature = "fraction")] {
use nanoexpr::{ErrorKind, Fraction};
let sum: Fraction = nanoexpr::evaluate("1/3 + 1/6").unwrap();
assert_eq!(sum.to_string(), "1/2");
let mixed: Fraction = nanoexpr::evaluate("0.75 + 5/2").unwrap();
assert_eq!(mixed.to_string(), "13/4");
assert_eq!(format!("{mixed:#}"), "3 1/4");
let root: Fraction = nanoexpr::evaluate("sqrt(4/9) + 8 ^ (1/3)").unwrap();
assert_eq!(root.to_string(), "8/3");
let irrational = nanoexpr::evaluate::<Fraction>("sqrt(2)").unwrap_err();
assert_eq!(irrational.kind, ErrorKind::Inexact);
# }
```
`Math` gives `Fraction` only the functions whose results are exact: `abs`, `sign`, rounding, `min`, `max`, `clamp`,
and `sqrt`, `cbrt`, `hypot` and `pow` when the result is rational. Other functions, `2 ^ (1/2)` and constants such
as `pi` are errors, as `Fraction` never approximates. `Fraction` parses from text such as `-5/4`, converts to `f64`,
and with the `decimal` feature to and from `Decimal`. Checks against Python's `fractions` module on 4,000 random
expressions show the results, overflows included, match.
### Fractions, like a TI-84 CE
Calculators such as the TI-84 Plus CE or the Casio ClassWiz keep results exact while they can: `1/3 + 1/6` shows
`1/2`, while `sqrt(2)` shows `1.4142135623731`. nanoexpr has the three pieces of that:
- `Decimal::to_fraction`, the `►Frac` key, finds the fraction a decimal result stands for.
- `Fraction` computes exactly, and reports what it cannot represent.
- With the `exact` feature, `Exact` is a fraction while the result is exact, and becomes a `Decimal` when it
overflows 64 bits or is irrational, such as `sqrt(2)`, `sin(1)` or `pi`. Once decimal, it stays decimal.
```toml
[dependencies]
nanoexpr = { version = "0.1", features = ["exact"] }
```
```rust
# #[cfg(feature = "exact")] {
use nanoexpr::Exact;
let sum: Exact = nanoexpr::evaluate("1/3 + 1/6").unwrap();
assert!(sum.is_exact());
assert_eq!(sum.to_string(), "1/2");
assert_eq!(sum.to_decimal().to_string(), "0.5");
let root: Exact = nanoexpr::evaluate("sqrt(4/9) + 0.1 + 0.2").unwrap();
assert_eq!(root.to_string(), "29/30");
assert_eq!(format!("{:#}", nanoexpr::evaluate::<Exact>("7/2").unwrap()), "3 1/2");
let mixed: Exact = nanoexpr::evaluate("1/3 + sqrt(2)").unwrap();
assert!(!mixed.is_exact());
assert_eq!(mixed.to_string(), "1.7475468957064");
assert_eq!(format!("{mixed:.10}"), "1.747546896");
# }
```
Typed decimals are exact when they fit a fraction, so `0.1 + 0.2` is `3/10`, as on a Casio ClassWiz; a TI-84 Plus
CE keeps them as decimals. Fractions display as `n/d`, or as mixed numbers with `{:#}`, and decimals round to the
precision asked for, such as `{:.10}`. `is_exact`, `as_fraction`, `as_decimal` and `to_decimal` tell the forms
apart. Errors and their spans are the same as with `Decimal`, and `Math` computes each function exactly when its
arguments are fractions and its result is exact, and in decimal otherwise.
## C and C++
The [`ffi`](https://github.com/AlixANNERAUD/nanoexpr/tree/main/ffi) directory of the repository holds C bindings
that evaluate `double` expressions, still without allocating. Variables and functions come from callbacks, and the
built-in math functions remain available:
```c
#include <string.h>
#include "nanoexpr.h"
static bool variable(void *user_data, const char *name, size_t name_length, double *value) {
if (name_length == 3 && memcmp(name, "raw", 3) == 0) {
*value = *(const double *)user_data;
return true;
}
return false;
}
double raw = 2150, result;
nanoexpr_error error;
nanoexpr_options options = nanoexpr_default_options();
options.variable = variable;
options.user_data = &raw;
const char *input = "raw * 0.0125 - 40";
if (nanoexpr_evaluate_with(input, strlen(input), &options, &result, &error) != NANOEXPR_OK) {
printf("%s at %zu..%zu\n", nanoexpr_status_description(error.kind), error.start, error.end);
}
```
The header is [`ffi/include/nanoexpr.h`](https://github.com/AlixANNERAUD/nanoexpr/blob/main/ffi/include/nanoexpr.h).
With CMake, add the directory and link the `nanoexpr` target, which builds the static library with `cargo`:
```cmake
add_subdirectory(nanoexpr/ffi)
target_link_libraries(app PRIVATE nanoexpr)
```
Without CMake, `cargo build --release -p nanoexpr-ffi` builds `target/release/libnanoexpr_c.a` (and a shared
library). `cargo rustc --release -p nanoexpr-ffi -- --print native-static-libs` lists the system libraries to link
with it. For a microcontroller, build without the standard library and link the archive into your firmware:
```sh
cargo rustc --release -p nanoexpr-ffi --target thumbv7em-none-eabihf --no-default-features --crate-type staticlib
```
Without `std` the library brings its own panic handler, which loops forever, so build it with `panic = "abort"`, the
default of bare-metal targets. The evaluator does not panic on any input.
## Syntax
From the loosest to the tightest binding:
| `c ? a : b` | Conditional: `a` if `c` is not zero, `b` otherwise | Right |
| `\|\|` | Logical or | Left |
| `&&` | Logical and | Left |
| `==` `!=` | Equal, not equal | Left |
| `<` `<=` `>` `>=` | Comparisons | Left |
| `+` `-` | Addition, subtraction | Left |
| `*` `/` `%` `mod` | Multiplication, division, remainder | Left |
| `-x` `+x` `!x` | Negation, unary plus, logical not | Prefix |
| `^` `**` | Power | Right |
| `x%` `n!` | Percentage, with `PercentSign::Percentage`, and factorial | Postfix |
| `( )` `name(a, b)` | Grouping, function call | |
Postfix operators bind tightest, as on TI calculators: `2^3!` is `2^(3!) = 64`, `-3!` is `-(3!) = -6` and `3!!` is
`(3!)! = 720`. The factorial `n!`, also `fact(n)` with `Math`, takes a non-negative integer, otherwise it is a
`Domain` error, and is an `Overflow` error when the result does not fit, such as above `170!` in `f64`, `20!` in
`i64` and `69!` in `Decimal`. A `!` after an operand is a factorial and before one a logical not, so `!3!` is
`!(3!) = 0`. `!=` is always read as not equal, so `5!=3` is `5 != 3`: write `5! == 120` or `(5!) == 120` to compare
a factorial.
Numbers are written `42`, `3.14`, `.5`, `5.` or `1.5e-3`. Names start with a letter or `_` and continue with
letters, digits or `_`.
With `implicit_multiplication(true)`, off by default, a name or a `(` directly after an operand multiplies it, as on
a calculator: `2pi`, `2x`, `3sqrt(4)`, `2(3 + 4)`, `(1 + 2)(3 + 4)` and `x y` are products. It has the precedence of
`*`, as on TI calculators, so `1/2x` is `(1/2) * x`. A name followed by `(` stays a call, so `x(2)` calls `x`, and a
number never multiplies implicitly, so `2 3` and `3!2` are still errors while `3!x` is `3! * x`. `2e3` is the number
`2000`, `2e` is `2 * e`.
Values are always numbers, as on TI calculators and in C: comparisons give `1` or `0`, and `&&`, `||`, `!` and
`? :` read zero as false and anything else as true. The precedences are those of C, so equality binds looser than
the other comparisons and `a < b == c < d` compares two comparisons. Comparisons chain like in C, not like in
Python: `3 > 2 > 1` is `(3 > 2) > 1`, which is `0`. Comparing a float `NaN`, or using it as a condition, is a
`Domain` error rather than a silent `false`.
`&&`, `||` and `? :` short-circuit: the operand they do not need is still parsed, to check its syntax, but it is
not computed, so its division by zero or unknown variable is no error. Errors in the text itself, such as a missing
parenthesis or an invalid number, are always reported, before any error that depends on values.
## Footprint
Sizes of a WebAssembly module exporting one evaluation function, built with `opt-level = "z"`, LTO and
`panic = "abort"`:
| `i32`, no `Math` | 5.7 KiB |
| `f64`, no `Math` | 28.7 KiB |
| `f64` with `Math` | 45.9 KiB |
Most of the `f64` size is `core`'s float parsing, which any crate reading float literals pays for.
## Comparison with other Rust expression evaluators
How `nanoexpr` compares with other expression evaluators on crates.io, as of their latest release in October 2026:
| **nanoexpr** | ✅ | **None** | `f32`, `f64`, all integers, your own | None (`libm` optional) | 2026 | MIT |
| [meval] 0.2.0 | ❌ | Yes | `f64` | `nom`, `fnv` | 2018 | Unlicense / MIT |
| [evalexpr] 13.1.0 | ❌ | Yes | Integers, floats, booleans, strings, tuples | None | 2025 | AGPL-3.0 |
| [fasteval] 0.2.4 | ❌ | Yes | `f64` | None | 2020 | MIT |
| [exmex] 0.21.0 | ❌ | Yes | Floats, custom operator sets | `regex`, `smallvec` | 2026 | MIT / Apache-2.0 |
| [exp-rs] 0.2.0 | ⚠️ ARM only, needs `alloc` | Yes, arena (`bumpalo`) | `f32` or `f64`, chosen at build time | `bitflags`, `bumpalo`, `critical-section`, `hash32`, `heapless`, `serde` | 2025 | MIT / Apache-2.0 |
| [maschen] 0.2.1 | ✅ | None without `std` | Not applicable: converts infix to postfix, does not evaluate | `thiserror` | 2024 | MIT |
And what each is best at:
| **nanoexpr** | Run-time math on a microcontroller or in WebAssembly, with predictable memory and errors to show users |
| [meval] | Quick `f64` evaluation on a desktop, binding expressions to closures |
| [evalexpr] | A small scripting language: booleans, strings, assignment, tuples (mind the AGPL license) |
| [fasteval] | Compiling an expression once and evaluating it many times, fast, on a desktop or server |
| [exmex] | Symbolic derivatives and custom operators |
| [exp-rs] | An embedded evaluator with a C API and an allocator you control |
| [maschen] | Building your own evaluator on top of a shunting-yard |
To evaluate the same formula many times, `nanoexpr`, `fasteval` and `exmex` can all compile it once: see the
[measurements](#performance).
[meval]: https://crates.io/crates/meval
[evalexpr]: https://crates.io/crates/evalexpr
[fasteval]: https://crates.io/crates/fasteval
[exmex]: https://crates.io/crates/exmex
[exp-rs]: https://crates.io/crates/exp-rs
[maschen]: https://crates.io/crates/maschen
## Performance
Each crate parses and evaluates three expressions from a string, once per call:
| Arithmetic | `((5.0 / 8.0) + 18.0 ^ 0.5 * 10.0 ^ 5.0) / 23.0` |
| Functions | `sin(0.5) * cos(0.5) + sin(1.5) / cos(1.5)` (`math::sin` and `math::cos` for `evalexpr`) |
| Variables | `x * x + 2.0 * x * y + y ^ 2.0` with `x = 1.5` and `y = 2.5` |
Median time per evaluation:
| **nanoexpr** | **290 ns** | **270 ns** | **160 ns** |
| meval 0.2.0 | 1340 ns | 1330 ns | 700 ns |
| evalexpr 13.1.0 | 2220 ns | 3050 ns | 2030 ns |
| fasteval 0.2.4 | 420 ns | 650 ns | 340 ns |
| exmex 0.21.0 | 3440 ns | 3420 ns | 6900 ns |
| exp-rs 0.2.0 | 2930 ns | 2800 ns | 2940 ns |
Compiled once, then evaluated with new values of `x` and `y` at each call, by `nanoexpr::compile`, `fasteval`'s
`Parser` and `compile`, and `exmex::parse`:
| **nanoexpr** | **6 ns** | **114 ns** | **38 ns** |
| fasteval 0.2.4 | 4 ns | 4 ns | 49 ns |
| exmex 0.21.0 | 10 ns | 10 ns | 37 ns |
All three compute operations on constants once, at compilation. `fasteval` and `exmex` also compute calls of
their built-in functions on constants, such as `sin(0.5)`, while `nanoexpr` asks the context for each call at each
evaluation, so a context can change what a function does between evaluations. None of them allocates while
evaluating a compiled expression.
Heap use per evaluation, as allocations / bytes allocated / peak live bytes:
| **nanoexpr** | **0 / 0 B / 0 B** | **0 / 0 B / 0 B** | **0 / 0 B / 0 B** |
| meval 0.2.0 | 36 / 6106 B / 3296 B | 43 / 4594 B / 2424 B | 18 / 2404 B / 1794 B |
| evalexpr 13.1.0 | 80 / 7440 B / 3264 B | 136 / 8826 B / 4008 B | 76 / 5416 B / 2146 B |
| fasteval 0.2.4 | 12 / 9166 B / 9166 B | 22 / 10191 B / 10027 B | 11 / 8259 B / 8259 B |
| exmex 0.21.0 | 2 / 2606 B / 2606 B | 2 / 2601 B / 2601 B | 4 / 2591 B / 2591 B |
| exp-rs 0.2.0 | Not measured | Not measured | Not measured |
Size of a WebAssembly module exporting one function that evaluates a string with each crate, built with
`opt-level = "z"`, LTO, `panic = "abort"` and symbols stripped. The baseline module only unwraps an `Option`, which
pulls in the standard library's panic and formatting code that every other module also contains:
| Baseline | 15.1 KiB | |
| **nanoexpr** | **79.8 KiB** | **64.6 KiB** |
| meval 0.2.0 | 140.0 KiB | 124.8 KiB |
| evalexpr 13.1.0 | 192.7 KiB | 177.5 KiB |
| fasteval 0.2.4 | 128.1 KiB | 113.0 KiB |
| exmex 0.21.0 | 1015.2 KiB | 1000.1 KiB |
| exp-rs 0.2.0 | 172.5 KiB | 157.3 KiB |
These modules use `std`, unlike the `no_std` builds in [Footprint](#footprint).
Measured on an Apple M2 Pro with rustc 1.99.0, in October 2026, as the best of six runs of the benchmark, while
other work kept the machine busy. In the first table every call parses the expression again. Times vary by a few
percent between runs. Heap use is counted by a global allocator wrapper; `exp-rs` installs its own global allocator, so its heap use cannot be
measured that way. To reproduce, from the
[`comparison`](https://github.com/AlixANNERAUD/nanoexpr/tree/main/comparison) directory of the repository:
```sh
cargo run --release --features all --bin bench
cargo run --release --features exp-rs --bin bench
./size.sh
```
## Feature flags
| `libm` | ✅ | `Number` for `f32` and `f64`, `Math` and `evaluate`, through the pure Rust [`libm`](https://crates.io/crates/libm) |
| `decimal` | | The `Decimal` type, and `Math` and `evaluate` for it, without `libm`. Enables `fraction` |
| `fraction` | | The exact `Fraction` type, and `Math` and `evaluate` for it, without `libm` |
| `exact` | | The `Exact` type, a fraction while exact and a `Decimal` otherwise. Enables `decimal` and `fraction` |
## Frequently asked questions
**How do I evaluate a math expression from a string in Rust?** Add `nanoexpr` and call
`nanoexpr::evaluate::<f64>("1 + 2 * 3")`. Use `evaluate_with` to provide variables and functions.
**Does it work without `std` or an allocator?** Yes. The crate is `#![no_std]`, never uses `alloc`, and needs no
global allocator, so it runs on bare-metal microcontrollers.
**Why is `0.1 + 0.2` not `0.3`?** With `f64`, no binary float can hold `0.1` exactly. Use the `decimal` feature and
`Decimal` to compute in decimal like a pocket calculator.
**Can a user crash the device with a malicious formula?** No: evaluation never allocates, nesting depth is bounded,
arithmetic is checked, and malformed, deep or overflowing input returns an error. The evaluator, compiled programs,
every number type and the C bindings are fuzzed with [`cargo-fuzz`](https://github.com/rust-fuzz/cargo-fuzz) on every
pull request and every week, looking for panics, overflows, hangs and wrong results. The stack is the one resource to
plan for: at the default depth of 32, the most deeply nested input needs about 15 KiB of stack in `f64`, 21 KiB in
`Decimal` and 27 KiB in `Fraction` and `Exact` in an optimized 64-bit build, and a test checks it fits in 64 KiB.
Builds without optimizations need several times more. Lower `max_depth` for a smaller stack.
**Is it a scripting language?** No: it evaluates numeric expressions only, though comparisons, `&&`, `||` and
`? :` are enough for thresholds and rules. For strings, booleans and assignments, see
the [comparison](#comparison-with-other-rust-expression-evaluators) below for crates that do more.
## License
[MIT](https://github.com/AlixANNERAUD/nanoexpr/blob/main/LICENSE)