math-mumu 0.1.3

Math functions plugin for the Mumu / Lava language
Documentation
// math/src/arb.rs
//
// Arbitrary‑precision arithmetic for the MuMu “math:arb” bridge.
//
// Supported operators: +  -  *  /  %
// Supported function : round(expr, places)
//
// --------------------------------------------------------------------------

use std::fmt;
use std::str::FromStr;

use bigdecimal::{BigDecimal, ToPrimitive, Zero};
use mumu::parser::interpreter::Interpreter;
use mumu::parser::types::Value;

// ===== debug_log! =========================================================

#[cfg(debug_assertions)]
macro_rules! debug_log {
    ($verbose:expr, $($arg:tt)*) => {
        if $verbose {
            eprintln!($($arg)*);
        }
    };
}

#[cfg(not(debug_assertions))]
macro_rules! debug_log {
    ($verbose:expr, $($arg:tt)*) => {};
}

// ===== lexer ==============================================================

#[derive(Clone, Debug, PartialEq)]
enum Token {
    LParen,
    RParen,
    Comma,
    Plus,
    Minus,
    Star,
    Slash,
    Percent,
    Identifier(String),
    Number(BigDecimal),
}

fn tokenize(src: &str, verbose: bool) -> Result<Vec<Token>, String> {
    debug_log!(verbose, "[tokenize] raw='{}'", src);
    let mut chars = src.chars().peekable();
    let mut tokens = Vec::<Token>::new();

    while let Some(&ch) = chars.peek() {
        match ch {
            ' ' | '\t' | '\r' | '\n' => {
                chars.next();
            }
            '(' => {
                chars.next();
                tokens.push(Token::LParen);
            }
            ')' => {
                chars.next();
                tokens.push(Token::RParen);
            }
            ',' => {
                chars.next();
                tokens.push(Token::Comma);
            }
            '+' => {
                chars.next();
                tokens.push(Token::Plus);
            }
            '-' => {
                // Could be unary minus or start of a number.
                let mut look = chars.clone();
                look.next(); // skip '-'
                if look.peek().map_or(false, |c| c.is_ascii_digit() || *c == '.') {
                    let mut s = String::from("-");
                    chars.next();
                    while let Some(&c2) = chars.peek() {
                        if c2.is_ascii_digit() || c2 == '.' {
                            s.push(c2);
                            chars.next();
                        } else {
                            break;
                        }
                    }
                    let bd = BigDecimal::from_str(&s)
                        .map_err(|e| format!("Invalid number '{}': {}", s, e))?;
                    tokens.push(Token::Number(bd));
                } else {
                    chars.next();
                    tokens.push(Token::Minus);
                }
            }
            '*' => {
                chars.next();
                tokens.push(Token::Star);
            }
            '/' => {
                chars.next();
                tokens.push(Token::Slash);
            }
            '%' => {
                chars.next();
                tokens.push(Token::Percent);
            }
            '0'..='9' | '.' => {
                let mut s = String::new();
                while let Some(&c2) = chars.peek() {
                    if c2.is_ascii_digit() || c2 == '.' {
                        s.push(c2);
                        chars.next();
                    } else {
                        break;
                    }
                }
                let bd = BigDecimal::from_str(&s)
                    .map_err(|e| format!("Invalid number '{}': {}", s, e))?;
                tokens.push(Token::Number(bd));
            }
            'a'..='z' | 'A'..='Z' | '_' => {
                let mut ident = String::new();
                while let Some(&c2) = chars.peek() {
                    if c2.is_alphanumeric() || c2 == '_' {
                        ident.push(c2);
                        chars.next();
                    } else {
                        break;
                    }
                }
                tokens.push(Token::Identifier(ident));
            }
            _ => return Err(format!("Unexpected character '{}'", ch)),
        }
    }

    if tokens.is_empty() {
        return Err("No tokens found".to_string());
    }

    debug_log!(verbose, "[tokenize] => {:?}", tokens);
    Ok(tokens)
}

// ===== Pratt parser =======================================================

#[derive(Copy, Clone, Debug, Eq, PartialEq)]
enum Op {
    Add,
    Sub,
    Mul,
    Div,
    Mod,
}

#[derive(Clone, Debug)]
enum Expr {
    Number(BigDecimal),
    Infix(Op, Box<Expr>, Box<Expr>),
    Round(Box<Expr>, i64),
}

struct Parser {
    tokens: Vec<Token>,
    pos: usize,
    verbose: bool,
}

impl Parser {
    fn new(tokens: Vec<Token>, verbose: bool) -> Self {
        Self { tokens, pos: 0, verbose }
    }

    fn peek(&self) -> Option<&Token> {
        self.tokens.get(self.pos)
    }

    fn next(&mut self) -> Option<Token> {
        let tok = self.tokens.get(self.pos).cloned();
        if tok.is_some() {
            self.pos += 1;
        }
        tok
    }

    fn parse_expression(&mut self, min_bp: u8) -> Result<Expr, String> {
        let mut lhs = self.parse_prefix()?;

        loop {
            let op = match self.peek() {
                Some(Token::Plus)    => Op::Add,
                Some(Token::Minus)   => Op::Sub,
                Some(Token::Star)    => Op::Mul,
                Some(Token::Slash)   => Op::Div,
                Some(Token::Percent) => Op::Mod,
                _ => break,
            };

            let (lbp, rbp) = Self::binding_power(op);
            if lbp < min_bp {
                break;
            }
            self.next(); // consume operator
            let rhs = self.parse_expression(rbp)?;
            lhs = Expr::Infix(op, Box::new(lhs), Box::new(rhs));
        }

        Ok(lhs)
    }

    fn parse_prefix(&mut self) -> Result<Expr, String> {
        match self.next() {
            Some(Token::Number(n)) => Ok(Expr::Number(n)),
            Some(Token::Minus) => {
                let inner = self.parse_expression(255)?;
                Ok(Expr::Infix(
                    Op::Sub,
                    Box::new(Expr::Number(BigDecimal::zero())),
                    Box::new(inner),
                ))
            }
            Some(Token::LParen) => {
                let inner = self.parse_expression(0)?;
                match self.next() {
                    Some(Token::RParen) => Ok(inner),
                    _ => Err("Expected ')'".to_string()),
                }
            }
            Some(Token::Identifier(name)) if name == "round" => {
                match self.next() {
                    Some(Token::LParen) => {
                        let val = self.parse_expression(0)?;
                        match self.next() {
                            Some(Token::Comma) => {}
                            _ => return Err("Expected ',' in round()".to_string()),
                        }
                        let places_tok = self.next().ok_or("Missing places in round()")?;
                        let places = match places_tok {
                            Token::Number(bd) if bd.is_integer() =>
                                bd.to_i64().ok_or("round() places out of range")?,
                            _ => return Err("round() places must be an integer".to_string()),
                        };
                        match self.next() {
                            Some(Token::RParen) => Ok(Expr::Round(Box::new(val), places)),
                            _ => Err("Expected ')' after round()".to_string()),
                        }
                    }
                    _ => Err("Expected '(' after 'round'".to_string()),
                }
            }
            Some(tok) => Err(format!("Unexpected token {:?}", tok)),
            None => Err("Unexpected end of input".to_string()),
        }
    }

    fn binding_power(op: Op) -> (u8, u8) {
        match op {
            Op::Add | Op::Sub => (10, 11),
            Op::Mul | Op::Div | Op::Mod => (20, 21),
        }
    }

    fn is_done(&self) -> bool {
        self.pos >= self.tokens.len()
    }
}

// ===== evaluation =========================================================

fn bigdecimal_mod(a: &BigDecimal, b: &BigDecimal) -> Result<BigDecimal, String> {
    if b.is_zero() {
        return Err("Modulo by zero".to_string());
    }
    // r = a − trunc(a / b) ⋅ b
    let div = (a / b).with_scale(0);
    Ok(a - &(div * b))
}

fn eval(expr: &Expr) -> Result<BigDecimal, String> {
    match expr {
        Expr::Number(n) => Ok(n.clone()),
        Expr::Infix(op, lhs, rhs) => {
            let l = eval(lhs)?;
            let r = eval(rhs)?;
            match op {
                Op::Add => Ok(l + r),
                Op::Sub => Ok(l - r),
                Op::Mul => Ok(l * r),
                Op::Div => {
                    if r.is_zero() {
                        Err("Division by zero".to_string())
                    } else {
                        Ok(l / r)
                    }
                }
                Op::Mod => bigdecimal_mod(&l, &r),
            }
        }
        Expr::Round(inner, places) => {
            let v = eval(inner)?;
            Ok(v.round(*places))
        }
    }
}

// ===== public façade ======================================================

pub fn eval_arb_expression(src: &str, verbose: bool) -> Result<String, String> {
    let tokens = tokenize(src, verbose)?;
    let mut parser = Parser::new(tokens, verbose);
    let ast = parser.parse_expression(0)?;
    if !parser.is_done() {
        return Err("Unexpected tokens after complete parse".to_string());
    }
    debug_log!(verbose, "[eval] AST = {:?}", ast);
    let result = eval(&ast)?;
    Ok(result.normalized().to_string())
}

// ===== MuMu bridge ========================================================

pub fn math_arb_bridge(
    interp: &mut Interpreter,
    args: Vec<Value>,
) -> Result<Value, String> {
    if args.len() != 1 {
        return Err(format!("math:arb => expected 1 argument, got {}", args.len()));
    }

    let raw = match &args[0] {
        Value::SingleString(s) => s.clone(),
        Value::StrArray(a) if a.len() == 1 => a[0].clone(),
        _ => return Err("math:arb => argument must be a single string".to_string()),
    };

    let verbose = interp.is_verbose();
    eval_arb_expression(&raw, verbose).map(Value::SingleString)
}

// ===== Display implementation (debug convenience) =========================

impl fmt::Display for Expr {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Expr::Number(n) => write!(f, "{}", n),
            Expr::Infix(op, l, r) => {
                let op_str = match op {
                    Op::Add => "+",
                    Op::Sub => "-",
                    Op::Mul => "*",
                    Op::Div => "/",
                    Op::Mod => "%",
                };
                write!(f, "({} {} {})", l, op_str, r)
            }
            Expr::Round(e, p) => write!(f, "round({}, {})", e, p),
        }
    }
}