PicoBot/src/tools/expression.rs
xiaoxixi 63d20d1eb8 refactor: eliminate build warnings and legacy evaluator
Resolve strict Clippy findings across all targets, preserve public API compatibility with scoped lint exceptions, and fix sourced messages retaining media references. Replace meval and its future-incompatible nom dependency with a bounded internal expression parser and regression tests.
2026-07-14 11:00:39 +08:00

278 lines
8.8 KiB
Rust

/// Evaluate a self-contained mathematical expression without executing code or
/// resolving external variables.
pub(super) fn evaluate(input: &str) -> Result<f64, String> {
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<f64, String> {
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<f64, String> {
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<f64, String> {
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<f64, String> {
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<f64, String> {
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<f64, String> {
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::<f64>()
.map_err(|_| self.error("invalid number"))
}
fn parse_identifier(&mut self) -> Result<f64, String> {
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<u8> {
self.input.as_bytes().get(self.position).copied()
}
fn nested<T>(&mut self, parse: fn(&mut Self) -> Result<T, String>) -> Result<T, String> {
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<f64, String> {
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());
}
}