/// Evaluate a self-contained mathematical expression without executing code or /// resolving external variables. pub(super) fn evaluate(input: &str) -> Result { const MAX_EXPRESSION_BYTES: usize = 4096; if input.len() > MAX_EXPRESSION_BYTES { return Err(format!( "expression exceeds the {MAX_EXPRESSION_BYTES}-byte limit" )); } let mut parser = Parser { input, position: 0, depth: 0, }; let value = parser.parse_expression()?; parser.skip_whitespace(); if parser.position != input.len() { return Err(parser.error("unexpected trailing input")); } Ok(value) } struct Parser<'a> { input: &'a str, position: usize, depth: usize, } impl Parser<'_> { fn parse_expression(&mut self) -> Result { let mut value = self.parse_term()?; loop { if self.consume(b'+') { value += self.parse_term()?; } else if self.consume(b'-') { value -= self.parse_term()?; } else { return Ok(value); } } } fn parse_term(&mut self) -> Result { let mut value = self.parse_unary()?; loop { if self.consume(b'*') { value *= self.parse_unary()?; } else if self.consume(b'/') { value /= self.parse_unary()?; } else if self.consume(b'%') { value %= self.parse_unary()?; } else { return Ok(value); } } } fn parse_unary(&mut self) -> Result { if self.consume(b'+') { self.nested(Self::parse_unary) } else if self.consume(b'-') { Ok(-self.nested(Self::parse_unary)?) } else { self.parse_power() } } fn parse_power(&mut self) -> Result { let base = self.parse_primary()?; if self.consume(b'^') { Ok(base.powf(self.nested(Self::parse_unary)?)) } else { Ok(base) } } fn parse_primary(&mut self) -> Result { self.skip_whitespace(); match self.peek() { Some(b'(') => { self.position += 1; let value = self.nested(Self::parse_expression)?; if !self.consume(b')') { return Err(self.error("expected ')'")); } Ok(value) } Some(byte) if byte.is_ascii_digit() || byte == b'.' => self.parse_number(), Some(byte) if byte.is_ascii_alphabetic() || byte == b'_' => self.parse_identifier(), Some(_) => Err(self.error("expected a number, constant, function, or '('")), None => Err(self.error("unexpected end of expression")), } } fn parse_number(&mut self) -> Result { self.skip_whitespace(); let start = self.position; let mut digits = 0; while self.peek().is_some_and(|byte| byte.is_ascii_digit()) { self.position += 1; digits += 1; } if self.peek() == Some(b'.') { self.position += 1; while self.peek().is_some_and(|byte| byte.is_ascii_digit()) { self.position += 1; digits += 1; } } if digits == 0 { return Err(self.error("invalid number")); } if matches!(self.peek(), Some(b'e' | b'E')) { self.position += 1; if matches!(self.peek(), Some(b'+' | b'-')) { self.position += 1; } let exponent_start = self.position; while self.peek().is_some_and(|byte| byte.is_ascii_digit()) { self.position += 1; } if self.position == exponent_start { return Err(self.error("invalid numeric exponent")); } } self.input[start..self.position] .parse::() .map_err(|_| self.error("invalid number")) } fn parse_identifier(&mut self) -> Result { self.skip_whitespace(); let start = self.position; while self .peek() .is_some_and(|byte| byte.is_ascii_alphanumeric() || byte == b'_') { self.position += 1; } let name = self.input[start..self.position].to_ascii_lowercase(); self.skip_whitespace(); if self.peek() != Some(b'(') { return match name.as_str() { "pi" => Ok(std::f64::consts::PI), "e" => Ok(std::f64::consts::E), _ => Err(self.error(&format!("unknown constant or variable '{name}'"))), }; } self.position += 1; let mut arguments = Vec::new(); self.skip_whitespace(); if self.peek() != Some(b')') { loop { arguments.push(self.nested(Self::parse_expression)?); if self.consume(b',') { continue; } break; } } if !self.consume(b')') { return Err(self.error("expected ')' after function arguments")); } apply_function(&name, &arguments).map_err(|message| self.error(&message)) } fn consume(&mut self, expected: u8) -> bool { self.skip_whitespace(); if self.peek() == Some(expected) { self.position += 1; true } else { false } } fn skip_whitespace(&mut self) { while self.peek().is_some_and(|byte| byte.is_ascii_whitespace()) { self.position += 1; } } fn peek(&self) -> Option { self.input.as_bytes().get(self.position).copied() } fn nested(&mut self, parse: fn(&mut Self) -> Result) -> Result { const MAX_PARSE_DEPTH: usize = 128; if self.depth >= MAX_PARSE_DEPTH { return Err(self.error("expression nesting limit exceeded")); } self.depth += 1; let result = parse(self); self.depth -= 1; result } fn error(&self, message: &str) -> String { format!("{message} at byte {}", self.position) } } fn apply_function(name: &str, arguments: &[f64]) -> Result { let unary = |function: fn(f64) -> f64| match arguments { [value] => Ok(function(*value)), _ => Err(format!("function '{name}' expects one argument")), }; match name { "sqrt" => unary(f64::sqrt), "abs" => unary(f64::abs), "exp" => unary(f64::exp), "ln" => unary(f64::ln), "log2" => unary(f64::log2), "log10" => unary(f64::log10), "sin" => unary(f64::sin), "cos" => unary(f64::cos), "tan" => unary(f64::tan), "asin" => unary(f64::asin), "acos" => unary(f64::acos), "atan" => unary(f64::atan), "sinh" => unary(f64::sinh), "cosh" => unary(f64::cosh), "tanh" => unary(f64::tanh), "asinh" => unary(f64::asinh), "acosh" => unary(f64::acosh), "atanh" => unary(f64::atanh), "floor" => unary(f64::floor), "ceil" => unary(f64::ceil), "round" => unary(f64::round), "signum" => unary(f64::signum), "atan2" => match arguments { [y, x] => Ok(y.atan2(*x)), _ => Err("function 'atan2' expects two arguments".to_string()), }, "min" => arguments .iter() .copied() .reduce(f64::min) .ok_or_else(|| "function 'min' expects at least one argument".to_string()), "max" => arguments .iter() .copied() .reduce(f64::max) .ok_or_else(|| "function 'max' expects at least one argument".to_string()), _ => Err(format!("unknown function '{name}'")), } } #[cfg(test)] mod tests { use super::*; #[test] fn respects_precedence_and_right_associative_power() { assert_eq!(evaluate("15*3+5^(2+1)").unwrap(), 170.0); assert_eq!(evaluate("2^3^2").unwrap(), 512.0); assert_eq!(evaluate("-2^2").unwrap(), -4.0); } #[test] fn supports_constants_functions_and_scientific_notation() { assert_eq!(evaluate("sqrt(1.44e2)").unwrap(), 12.0); assert_eq!(evaluate("max(1, 2, 3) + min(4, 5)").unwrap(), 7.0); assert!((evaluate("sin(pi / 2)").unwrap() - 1.0).abs() < f64::EPSILON); } #[test] fn rejects_unknown_names_and_trailing_input() { assert!(evaluate("unknown").is_err()); assert!(evaluate("1 + 2 garbage").is_err()); assert!(evaluate("sqrt() ").is_err()); assert!(evaluate(&"(".repeat(129)).is_err()); assert!(evaluate(&"1+".repeat(3000)).is_err()); } }