mathexpr 0.1.1

A fast, safe mathematical expression parser and evaluator with bytecode compilation
Documentation
//! Parser for mathematical expressions.
//!
//! This module uses nom to parse expression strings into an AST.

#[cfg(not(feature = "std"))]
use alloc::{boxed::Box, string::String, vec::Vec};

#[cfg(not(feature = "std"))]
use alloc::format;

use nom::{
    branch::alt,
    bytes::complete::tag,
    character::complete::{alpha1, alphanumeric1, char, multispace0},
    combinator::{map, opt, recognize},
    multi::{many0, separated_list0},
    number::complete::double,
    sequence::{delimited, pair},
    IResult, Parser,
};

use crate::ast::{BinOp, Expr};
use crate::error::ParseError;

/// Parse a number literal.
fn number(input: &str) -> IResult<&str, Expr> {
    map(double, Expr::Number).parse(input)
}

/// Parse an identifier (variable name or underscore).
fn ident(input: &str) -> IResult<&str, String> {
    map(
        recognize(pair(
            alt((alpha1, tag("_"))),
            many0(alt((alphanumeric1, tag("_")))),
        )),
        String::from,
    )
    .parse(input)
}

/// Parse a function call: name(arg1, arg2, ...)
fn function_call(input: &str) -> IResult<&str, Expr> {
    let (input, name) = ident(input)?;
    let (input, _) = multispace0.parse(input)?;
    let (input, _) = char('(').parse(input)?;
    let (input, _) = multispace0.parse(input)?;
    let (input, args) =
        separated_list0(delimited(multispace0, char(','), multispace0), expr).parse(input)?;
    let (input, _) = multispace0.parse(input)?;
    let (input, _) = char(')').parse(input)?;

    Ok((input, Expr::FunctionCall { name, args }))
}

/// Parse a variable reference, the current value (_), or a constant (pi, e).
fn variable(input: &str) -> IResult<&str, Expr> {
    map(ident, |name| {
        if name == "_" {
            Expr::CurrentValue
        } else if name == "pi" {
            // Support bare `pi` as a constant (equivalent to `pi()`)
            Expr::FunctionCall {
                name: String::from("pi"),
                args: Vec::new(),
            }
        } else if name == "e" {
            // Support bare `e` as a constant (equivalent to `e()`)
            Expr::FunctionCall {
                name: String::from("e"),
                args: Vec::new(),
            }
        } else {
            Expr::Variable(name)
        }
    })
    .parse(input)
}

/// Parse primary expressions: numbers, function calls, variables, or parenthesized expressions.
fn primary(input: &str) -> IResult<&str, Expr> {
    alt((
        delimited((multispace0, char('(')), expr, (multispace0, char(')'))),
        number,
        function_call,
        variable,
    ))
    .parse(input)
}

/// Parse unary minus.
fn unary(input: &str) -> IResult<&str, Expr> {
    let (input, _) = multispace0.parse(input)?;
    let (input, neg) = opt(char('-')).parse(input)?;
    let (input, _) = multispace0.parse(input)?;
    let (input, e) = primary(input)?;

    Ok((
        input,
        match neg {
            Some(_) => Expr::UnaryMinus(Box::new(e)),
            None => e,
        },
    ))
}

/// Parse exponentiation (right-associative, highest precedence after unary).
fn power(input: &str) -> IResult<&str, Expr> {
    let (input, base) = unary(input)?;
    let (input, _) = multispace0.parse(input)?;
    let (input, exp) = opt(|i| {
        let (i, _) = char('^').parse(i)?;
        let (i, _) = multispace0.parse(i)?;
        power(i) // recursive for right-associativity
    })
    .parse(input)?;

    Ok((
        input,
        match exp {
            Some(e) => Expr::BinaryOp {
                op: BinOp::Pow,
                left: Box::new(base),
                right: Box::new(e),
            },
            None => base,
        },
    ))
}

/// Parse multiplication, division, and modulo (higher precedence than +/-).
fn term(input: &str) -> IResult<&str, Expr> {
    let (input, first) = power(input)?;
    let (input, rest) = many0((
        delimited(
            multispace0,
            alt((char('*'), char('/'), char('%'))),
            multispace0,
        ),
        power,
    ))
    .parse(input)?;

    Ok((
        input,
        rest.into_iter().fold(first, |acc, (op_char, val)| {
            let op = match op_char {
                '*' => BinOp::Mul,
                '/' => BinOp::Div,
                '%' => BinOp::Mod,
                _ => unreachable!(),
            };
            Expr::BinaryOp {
                op,
                left: Box::new(acc),
                right: Box::new(val),
            }
        }),
    ))
}

/// Parse addition and subtraction (lowest precedence).
fn expr(input: &str) -> IResult<&str, Expr> {
    let (input, first) = term(input)?;
    let (input, rest) = many0((
        delimited(multispace0, alt((char('+'), char('-'))), multispace0),
        term,
    ))
    .parse(input)?;

    Ok((
        input,
        rest.into_iter().fold(first, |acc, (op_char, val)| {
            let op = match op_char {
                '+' => BinOp::Add,
                '-' => BinOp::Sub,
                _ => unreachable!(),
            };
            Expr::BinaryOp {
                op,
                left: Box::new(acc),
                right: Box::new(val),
            }
        }),
    ))
}

/// Parse a mathematical expression string into an AST.
///
/// # Arguments
///
/// * `input` - The expression string to parse
///
/// # Returns
///
/// * `Ok(Expr)` - The parsed AST
/// * `Err(ParseError)` - If parsing fails
///
/// # Example
///
/// ```
/// use mathexpr::parse;
///
/// let ast = parse("sqrt(x^2 + y^2)").unwrap();
/// ```
pub fn parse(input: &str) -> Result<Expr, ParseError> {
    match expr(input.trim()) {
        Ok(("", expr)) => Ok(expr),
        Ok((remaining, _)) => Err(ParseError::UnexpectedTrailingInput(String::from(remaining))),
        Err(e) => Err(ParseError::InvalidSyntax(format!("{:?}", e))),
    }
}