use std::fmt;
use std::str::FromStr;
use bigdecimal::{BigDecimal, ToPrimitive, Zero};
use mumu::parser::interpreter::Interpreter;
use mumu::parser::types::Value;
#[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)*) => {};
}
#[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);
}
'-' => {
let mut look = chars.clone();
look.next(); 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)
}
#[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(); 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()
}
}
fn bigdecimal_mod(a: &BigDecimal, b: &BigDecimal) -> Result<BigDecimal, String> {
if b.is_zero() {
return Err("Modulo by zero".to_string());
}
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))
}
}
}
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())
}
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)
}
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),
}
}
}