Skip to main content

Crate nanoexpr

Crate nanoexpr 

Source
Expand description

§nanoexpr: math expression evaluator for embedded Rust

crates.io docs.rs CI codecov no_std MSRV 1.85 License: MIT

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 allocations0, checked by a test
Speedabout 290 ns to parse and evaluate a formula, at least 1.4 times faster than the other crates, or 40 ns compiled once measured
Size6 KiB of WebAssembly for integers, 46 KiB with f64 and every math function
TargetsAny no_std target: Cortex-M, RISC-V (ESP32-C3), WebAssembly, desktops. Built in CI for each
Numbersf32, f64, integers, calculator-exact Decimal, exact Fraction, both at once like a TI-84 CE, or your own type
LanguagesRust, and C or C++ through a CMake project
DependenciesNone, 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

[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 ►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.
[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 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:

OperatorsMeaningAssociativity
c ? a : bConditional: a if c is not zero, b otherwiseRight
||Logical orLeft
&&Logical andLeft
== !=Equal, not equalLeft
< <= > >=ComparisonsLeft
+ -Addition, subtractionLeft
* / % modMultiplication, division, remainderLeft
-x +x !xNegation, unary plus, logical notPrefix
^ **PowerRight
x% n!Percentage, with PercentSign::Percentage, and factorialPostfix
( ) 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":

ConfigurationSize
i32, no Math5.7 KiB
f64, no Math28.7 KiB
f64 with Math45.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:

Crateno_stdHeap allocationNumber typesRequired dependenciesLatest releaseLicense
nanoexpr✅Nonef32, f64, all integers, your ownNone (libm optional)2026MIT
meval 0.2.0❌Yesf64nom, fnv2018Unlicense / MIT
evalexpr 13.1.0❌YesIntegers, floats, booleans, strings, tuplesNone2025AGPL-3.0
fasteval 0.2.4❌Yesf64None2020MIT
exmex 0.21.0❌YesFloats, custom operator setsregex, smallvec2026MIT / Apache-2.0
exp-rs 0.2.0⚠️ ARM only, needs allocYes, arena (bumpalo)f32 or f64, chosen at build timebitflags, bumpalo, critical-section, hash32, heapless, serde2025MIT / Apache-2.0
maschen 0.2.1✅None without stdNot applicable: converts infix to postfix, does not evaluatethiserror2024MIT

And what each is best at:

CratePick it when you need
nanoexprRun-time math on a microcontroller or in WebAssembly, with predictable memory and errors to show users
mevalQuick f64 evaluation on a desktop, binding expressions to closures
evalexprA small scripting language: booleans, strings, assignment, tuples (mind the AGPL license)
fastevalCompiling an expression once and evaluating it many times, fast, on a desktop or server
exmexSymbolic derivatives and custom operators
exp-rsAn embedded evaluator with a C API and an allocator you control
maschenBuilding 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:

ExpressionInput
Arithmetic((5.0 / 8.0) + 18.0 ^ 0.5 * 10.0 ^ 5.0) / 23.0
Functionssin(0.5) * cos(0.5) + sin(1.5) / cos(1.5) (math::sin and math::cos for evalexpr)
Variablesx * x + 2.0 * x * y + y ^ 2.0 with x = 1.5 and y = 2.5

Median time per evaluation:

CrateArithmeticFunctionsVariables
nanoexpr290 ns270 ns160 ns
meval 0.2.01340 ns1330 ns700 ns
evalexpr 13.1.02220 ns3050 ns2030 ns
fasteval 0.2.4420 ns650 ns340 ns
exmex 0.21.03440 ns3420 ns6900 ns
exp-rs 0.2.02930 ns2800 ns2940 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:

CrateArithmeticFunctionsVariables
nanoexpr6 ns114 ns38 ns
fasteval 0.2.44 ns4 ns49 ns
exmex 0.21.010 ns10 ns37 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:

CrateArithmeticFunctionsVariables
nanoexpr0 / 0 B / 0 B0 / 0 B / 0 B0 / 0 B / 0 B
meval 0.2.036 / 6106 B / 3296 B43 / 4594 B / 2424 B18 / 2404 B / 1794 B
evalexpr 13.1.080 / 7440 B / 3264 B136 / 8826 B / 4008 B76 / 5416 B / 2146 B
fasteval 0.2.412 / 9166 B / 9166 B22 / 10191 B / 10027 B11 / 8259 B / 8259 B
exmex 0.21.02 / 2606 B / 2606 B2 / 2601 B / 2601 B4 / 2591 B / 2591 B
exp-rs 0.2.0Not measuredNot measuredNot 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:

CrateSizeOver baseline
Baseline15.1 KiB
nanoexpr79.8 KiB64.6 KiB
meval 0.2.0140.0 KiB124.8 KiB
evalexpr 13.1.0192.7 KiB177.5 KiB
fasteval 0.2.4128.1 KiB113.0 KiB
exmex 0.21.01015.2 KiB1000.1 KiB
exp-rs 0.2.0172.5 KiB157.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

FeatureDefaultEffect
libm✅Number for f32 and f64, Math and evaluate, through the pure Rust libm
decimalThe Decimal type, and Math and evaluate for it, without libm. Enables fraction
fractionThe exact Fraction type, and Math and evaluate for it, without libm
exactThe 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

MIT

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 Fraction while it can, and becomes a Decimal otherwise, 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 f32 and f64 with the libm feature, for Decimal with the decimal feature, the exact ones for Fraction with the fraction feature, and both for Exact with the exact feature.
ParseDecimalError
The error of parsing a Decimal from text.
ParseFractionError
The error of parsing a Fraction from text.
Program
An expression compiled by Evaluator::compile, to evaluate many times with different variables.
Span
Byte range of the input an Error points to.
Variables
A context of variables resolved by a closure.

Enums§

ErrorKind
What went wrong while evaluating an expression.
PercentSign
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 input into code with the default settings, to evaluate it many times with Program::evaluate.
evaluate
Evaluates input with the constants and functions of Math.
evaluate_with
Evaluates input, resolving names with context.