Expand description
§nanoexpr: math expression evaluator for embedded Rust
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.
assert_eq!(nanoexpr::evaluate::<f64>("2 * (3 + 4) ^ 2 / sqrt(16)"), Ok(24.5));| At a glance | |
|---|---|
| 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 |
| 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 theNumbertrait. Integers report overflow instead of wrapping. - Calculator-exact decimals. The optional
Decimaltype computes in decimal like TI and Casio calculators: 14 significant digits and exponents up to ±99 by default, so0.1 + 0.2is exactly0.3, and turns results into fractions like the►Frackey:1/3 + 1/6is1/2. - Exact fractions. The optional
Fractiontype computes with rational numbers exactly, so1/3 + 1/6is exactly1/2, andExactkeeps 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
Contextfor each name it meets: implement the trait, pass(name, value)pairs, or wrap closures. Contexts compose as tuples. - Scientific functions included.
Mathprovidespi,e,sqrt,sin,cos,tan,exp,ln,log,min,max,clamp,fact, rounding and more. - Errors you can show. A
Copyerror 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 silentNaNor 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.
<,==,&&,||,!andc ? a : bwith C precedences, giving1or0, and short-circuiting sox != 0 && 1 / x > 2is 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 nounsafecode in the evaluator.
§Usage
[dependencies]
nanoexpr = "0.1"§Variables and functions
use nanoexpr::{ErrorKind, Functions, Math};
let variables = [("raw", 2150.0), ("offset", -40.0)];
let functions = Functions(|name: &str, arguments: &[f64]| match (name, arguments) {
("celsius", [kelvin]) => Some(Ok(kelvin - 273.15)),
("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:
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
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:
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
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:
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.
[dependencies]
nanoexpr = { version = "0.1", features = ["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:
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:
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 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.
[dependencies]
nanoexpr = { version = "0.1", features = ["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►Frackey, finds the fraction a decimal result stands for.Fractioncomputes exactly, and reports what it cannot represent.- With the
exactfeature,Exactis a fraction while the result is exact, and becomes aDecimalwhen it overflows 64 bits or is irrational, such assqrt(2),sin(1)orpi. Once decimal, it stays decimal.
[dependencies]
nanoexpr = { version = "0.1", features = ["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 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:
#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.
With CMake, add the directory and link the nanoexpr target, which builds the static library with cargo:
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:
cargo rustc --release -p nanoexpr-ffi --target thumbv7em-none-eabihf --no-default-features --crate-type staticlibWithout 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:
| Operators | Meaning | Associativity |
|---|---|---|
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":
| Configuration | Size |
|---|---|
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:
| Crate | no_std | Heap allocation | Number types | Required dependencies | Latest release | License |
|---|---|---|---|---|---|---|
| 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:
| Crate | Pick it when you need |
|---|---|
| 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
Each crate parses and evaluates three expressions from a string, once per call:
| Expression | Input |
|---|---|
| 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:
| Crate | Arithmetic | Functions | Variables |
|---|---|---|---|
| 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:
| Crate | Arithmetic | Functions | Variables |
|---|---|---|---|
| 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:
| Crate | Arithmetic | Functions | Variables |
|---|---|---|---|
| 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:
| Crate | Size | Over baseline |
|---|---|---|
| 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.
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 directory of the repository:
cargo run --release --features all --bin bench
cargo run --release --features exp-rs --bin bench
./size.sh§Feature flags
| Feature | Default | Effect |
|---|---|---|
libm | ✅ | Number for f32 and f64, Math and evaluate, through the pure Rust 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 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 below for crates that do more.
§License
Structs§
- Decimal
- A decimal floating-point number, as pocket calculators compute with.
- Error
- An evaluation error and the part of the input that caused it.
- Evaluator
- Evaluates expressions against a
Context, with configurable limits and syntax. - Exact
- A number that stays an exact
Fractionwhile it can, and becomes aDecimalotherwise, like the results of a TI-84 Plus CE or a Casio ClassWiz. - Fraction
- An exact rational number, such as
-7/2, always in lowest terms. - Functions
- A context of functions resolved by a closure.
- Instruction
- One instruction of a
Program, borrowing the names of variables and functions from the expression. - Math
- The usual constants and functions of a scientific calculator, for
f32andf64with thelibmfeature, forDecimalwith thedecimalfeature, the exact ones forFractionwith thefractionfeature, and both forExactwith theexactfeature. - Parse
Decimal Error - The error of parsing a
Decimalfrom text. - Parse
Fraction Error - The error of parsing a
Fractionfrom text. - Program
- An expression compiled by
Evaluator::compile, to evaluate many times with different variables. - Span
- Byte range of the input an
Errorpoints to. - Variables
- A context of variables resolved by a closure.
Enums§
- Error
Kind - What went wrong while evaluating an expression.
- Percent
Sign - How the
%sign is read.
Traits§
- Context
- Resolves the variables and functions an expression refers to.
- Number
- A value type expressions are evaluated in.
Functions§
- compile
- Compiles
inputintocodewith the default settings, to evaluate it many times withProgram::evaluate. - evaluate
- Evaluates
inputwith the constants and functions ofMath. - evaluate_
with - Evaluates
input, resolving names withcontext.