nanoexpr 0.2.1

Fast no_std math expression parser and evaluator with zero heap allocation: formulas with variables and functions for microcontrollers, WebAssembly and C, in f32, f64, integers or calculator-exact decimals
Documentation
use nanoexpr::{
    Context, Error, ErrorKind, Evaluator, Functions, Instruction, Number, PercentSign, Program,
    Span,
};

struct Random(u64);

impl Random {
    fn next(&mut self) -> u64 {
        self.0 ^= self.0 << 13;
        self.0 ^= self.0 >> 7;
        self.0 ^= self.0 << 17;
        self.0
    }

    fn below(&mut self, bound: usize) -> usize {
        (self.next() % bound as u64) as usize
    }

    fn pick<'a>(&mut self, items: &[&'a str]) -> &'a str {
        items[self.below(items.len())]
    }
}

const NUMBERS: &[&str] = &[
    "0",
    "1",
    "2",
    "3",
    "10",
    "100",
    "0.5",
    "1e3",
    "2.5e-1",
    ".5",
    "256",
    "9223372036854775807",
];
const NAMES: &[&str] = &["x", "y", "z", "big", "unknown", "e", "mod"];
const BINARY: &[&str] = &[
    "+", "-", "*", "/", "%", "mod", "^", "**", "<", "<=", ">", ">=", "==", "!=", "&&", "||",
];
const NOISE: &[&str] = &[
    "(", ")", "?", ":", ",", "+", "*", "!", "&&", "=", "<", "1", "x", "f(",
];

fn space(random: &mut Random, output: &mut String) {
    match random.below(4) {
        0 => {}
        1 | 2 => output.push(' '),
        _ => output.push_str("  "),
    }
}

fn expression(random: &mut Random, depth: usize, output: &mut String) {
    let leaf = depth == 0 || random.below(3) == 0;
    if leaf {
        match random.below(3) {
            0 => output.push_str(random.pick(NAMES)),
            _ => output.push_str(random.pick(NUMBERS)),
        }
        return;
    }
    match random.below(11) {
        0..=2 => {
            expression(random, depth - 1, output);
            space(random, output);
            output.push_str(random.pick(BINARY));
            space(random, output);
            expression(random, depth - 1, output);
        }
        3 => {
            output.push_str(random.pick(&["-", "+", "!"]));
            expression(random, depth - 1, output);
        }
        4 => {
            output.push('(');
            expression(random, depth - 1, output);
            output.push(')');
        }
        5 => {
            output.push_str(random.pick(&["f", "g", "h", "fail", "missing", "x"]));
            output.push('(');
            let count = random.below(5);
            for index in 0..count {
                if index > 0 {
                    output.push(',');
                    space(random, output);
                }
                expression(random, depth - 1, output);
            }
            output.push(')');
        }
        6 | 7 => {
            expression(random, depth - 1, output);
            space(random, output);
            output.push('?');
            space(random, output);
            expression(random, depth - 1, output);
            space(random, output);
            output.push(':');
            space(random, output);
            expression(random, depth - 1, output);
        }
        8 => {
            expression(random, depth - 1, output);
            space(random, output);
            expression(random, depth - 1, output);
        }
        9 => {
            expression(random, depth - 1, output);
            space(random, output);
            output.push_str(random.pick(&["%", "!", "!!"]));
        }
        _ => {
            expression(random, depth - 1, output);
            space(random, output);
            output.push_str(random.pick(&["&&", "||"]));
            space(random, output);
            expression(random, depth - 1, output);
        }
    }
}

fn input(random: &mut Random) -> String {
    let mut output = String::new();
    let depth = random.below(7);
    expression(random, depth, &mut output);
    if random.below(5) == 0 {
        let mut position = random.below(output.len() + 1);
        while !output.is_char_boundary(position) {
            position -= 1;
        }
        if random.below(2) == 0 && position < output.len() {
            output.remove(position);
        } else {
            output.insert_str(position, random.pick(NOISE));
        }
    }
    output
}

fn functions<N: Number + PartialOrd>(name: &str, arguments: &[N]) -> Option<Result<N, ErrorKind>> {
    let zero = N::default();
    match (name, arguments) {
        ("f", [first, rest @ ..]) => {
            Some(rest.iter().try_fold(*first, |sum, value| sum.add(*value)))
        }
        ("f", []) => Some(Err(ErrorKind::ArgumentCount)),
        ("g", [value]) if *value < zero => Some(Err(ErrorKind::Domain)),
        ("g", [value]) => Some(Ok(*value)),
        ("h", []) => Some(N::from_bool(true)),
        ("fail", _) => Some(Err(ErrorKind::Overflow)),
        _ => None,
    }
}

fn compare<N, C, const ARGUMENTS: usize>(
    evaluator: &Evaluator<'_, C, ARGUMENTS>,
    context: &C,
    input: &str,
    same: impl Fn(&N, &N) -> bool,
) -> bool
where
    N: Number + core::fmt::Debug,
    C: Context<N>,
{
    let direct = evaluator.evaluate::<N>(input);
    let mut code = [Instruction::default(); 1024];
    let compiled = evaluator
        .compile::<N, 512>(input, &mut code)
        .and_then(|program| program.evaluate(context));
    let equal = match (&direct, &compiled) {
        (Ok(left), Ok(right)) => same(left, right),
        (Err(left), Err(right)) => left == right,
        _ => false,
    };
    assert!(
        equal,
        "{input:?}: evaluated {direct:?}, compiled {compiled:?}"
    );
    direct.is_ok()
}

fn differential<N, const ARGUMENTS: usize>(
    variables: [(&str, N); 4],
    seed: u64,
    same: impl Fn(&N, &N) -> bool + Copy,
) where
    N: Number + PartialOrd + core::fmt::Debug,
{
    let context = (variables, Functions(functions::<N>));
    let mut random = Random(seed);
    let mut successes = 0;
    let mut total = 0;
    for percent_sign in [PercentSign::Remainder, PercentSign::Percentage] {
        for implicit_multiplication in [false, true] {
            for max_depth in [4, 32] {
                let evaluator = Evaluator::<_, ARGUMENTS>::new(&context)
                    .percent_sign(percent_sign)
                    .implicit_multiplication(implicit_multiplication)
                    .max_depth(max_depth);
                for _ in 0..3_000 {
                    let input = input(&mut random);
                    successes += usize::from(compare(&evaluator, &context, &input, same));
                    total += 1;
                }
            }
        }
    }
    assert!(successes * 10 > total, "{successes} of {total} succeeded");
}

#[test]
fn compiled_integers_match_evaluation() {
    let variables = [("x", 3_i64), ("y", 0), ("z", -2), ("big", i64::MAX)];
    differential::<i64, 8>(variables, 0x9e37_79b9_7f4a_7c15, i64::eq);
    differential::<i64, 2>(variables, 0x2545_f491_4f6c_dd1d, i64::eq);
}

#[cfg(feature = "libm")]
#[test]
fn compiled_floats_match_evaluation() {
    let variables = [("x", 3.0), ("y", 0.0), ("z", -2.5), ("big", f64::NAN)];
    differential::<f64, 8>(variables, 0xdead_beef_cafe_f00d, |left, right| {
        left.to_bits() == right.to_bits()
    });
}

#[cfg(feature = "decimal")]
#[test]
fn compiled_decimals_match_evaluation() {
    use nanoexpr::Decimal;
    let number = |text: &str| text.parse::<Decimal>().unwrap();
    let variables = [
        ("x", number("3")),
        ("y", number("0")),
        ("z", number("-2.5")),
        ("big", number("9e99")),
    ];
    differential::<Decimal, 8>(variables, 0x0123_4567_89ab_cdef, |left, right| {
        left == right
    });
}

#[cfg(feature = "fraction")]
#[test]
fn compiled_fractions_match_evaluation() {
    use nanoexpr::Fraction;
    let number = |text: &str| text.parse::<Fraction>().unwrap();
    let variables = [
        ("x", number("3")),
        ("y", number("0")),
        ("z", number("-5/2")),
        ("big", number("18446744073709551615")),
    ];
    differential::<Fraction, 8>(variables, 0x0f1e_2d3c_4b5a_6978, Fraction::eq);
}

#[cfg(feature = "exact")]
#[test]
fn compiled_exact_numbers_match_evaluation() {
    use nanoexpr::{Exact, Number};
    let number = |text: &str| Exact::parse(text).unwrap();
    let variables = [
        ("x", number("3")),
        ("y", number("0")),
        ("z", number("2.5")),
        ("big", number("9e99")),
    ];
    differential::<Exact, 8>(variables, 0x5851_f42d_4c95_7f2d, |left, right| {
        left == right
    });
}

#[test]
fn programs_are_reusable() {
    let mut code = [Instruction::default(); 16];
    let program: Program<i64> = nanoexpr::compile("x != 0 && 100 / x > 2", &mut code).unwrap();
    assert_eq!(program.evaluate(&[("x", 0)]), Ok(0));
    assert_eq!(program.evaluate(&[("x", 10)]), Ok(1));
    assert_eq!(program.evaluate(&[("x", 50)]), Ok(0));
    assert_eq!(
        program.evaluate(&[("y", 1)]),
        Err(Error::new(ErrorKind::UnknownVariable, Span::new(0, 1)))
    );
    assert_eq!(program.len(), 10);
    assert!(!program.is_empty());
    assert_eq!(program.stack(), 2);
}

#[test]
fn compilation_reports_errors_of_the_text() {
    let mut code = [Instruction::<i64>::default(); 16];
    let error = |input| {
        nanoexpr::compile::<i64, 8>(input, &mut [Instruction::default(); 16])
            .map(|program| program.len())
            .unwrap_err()
    };
    assert_eq!(
        error("1 +"),
        Error::new(ErrorKind::UnexpectedEnd, Span::new(3, 3))
    );
    assert_eq!(
        error("0 && 1.5"),
        Error::new(ErrorKind::InvalidNumber, Span::new(5, 8))
    );
    assert_eq!(
        error("1 / 0 + )"),
        Error::new(ErrorKind::UnexpectedToken, Span::new(8, 9))
    );
    assert!(nanoexpr::compile::<i64, 8>("1 / 0", &mut code).is_ok());
}

#[test]
fn small_buffers_are_too_long() {
    let mut code = [Instruction::default(); 3];
    assert_eq!(
        nanoexpr::compile::<i64, 8>("x + y", &mut code).map(|program| program.len()),
        Ok(3)
    );
    assert_eq!(
        nanoexpr::compile::<i64, 8>("x + y + z", &mut code).map(|program| program.len()),
        Err(Error::new(ErrorKind::TooLong, Span::new(8, 9)))
    );
    let mut code = [Instruction::default(); 32];
    assert_eq!(
        nanoexpr::compile::<i64, 2>("x + y * z", &mut code).map(|program| program.len()),
        Err(Error::new(ErrorKind::TooLong, Span::new(8, 9)))
    );
    assert_eq!(
        nanoexpr::compile::<i64, 3>("x + y * z", &mut code).map(|program| program.stack()),
        Ok(3)
    );
    assert_eq!(
        nanoexpr::compile::<i64, 0>("1", &mut code).map(|program| program.len()),
        Err(Error::new(ErrorKind::TooLong, Span::new(0, 1)))
    );
}

#[test]
fn constants_are_folded() {
    let mut code = [Instruction::default(); 32];
    let length = |input, code: &mut [Instruction<'_, i64>]| {
        nanoexpr::compile::<i64, 8>(input, code).map(|program| program.len())
    };
    assert_eq!(length("1 + 2 * -3 ^ 2", &mut code), Ok(1));
    assert_eq!(length("x * (60 * 60)", &mut code), Ok(3));
    assert_eq!(length("1 / 0", &mut code), Ok(3));
    assert_eq!(length("(x ? 1 : 2) + 3", &mut code), Ok(7));
    assert_eq!(length("x && 1 + 2", &mut code), Ok(4));
    let program: Program<i64> = nanoexpr::compile("2 * 3 + (x ? 1 : 2) * 10", &mut code).unwrap();
    assert_eq!(program.evaluate(&[("x", 1)]), Ok(16));
    assert_eq!(program.evaluate(&[("x", 0)]), Ok(26));
    let program: Program<i64> = nanoexpr::compile("x + 1 / 0", &mut code).unwrap();
    assert_eq!(
        program.evaluate(&[("x", 1)]),
        Err(Error::new(ErrorKind::DivisionByZero, Span::new(4, 9)))
    );
}

#[test]
fn conditionals_count_one_pending_value() {
    let mut code = [Instruction::default(); 32];
    let program = nanoexpr::compile::<i64, 1>("1 ? 2 : 3 ? 4 : 5", &mut code).unwrap();
    assert_eq!(program.evaluate(&()), Ok(2));
    let program = nanoexpr::compile::<i64, 1>("0 || 1 && 0 || 2", &mut code).unwrap();
    assert_eq!(program.stack(), 1);
    assert_eq!(program.evaluate(&()), Ok(1));
}