Fix the math parser's implicit multiplication precedence and other regressions from the rewrite (#4383)

* Fix parsing regressions, make parsing 2.5x faster than the old pest parser, and clean up the math-parser rewrite

* Fix review findings: whitespace-juxtaposed numbers, mixed real/complex logic, correctly rounded literals, unified NaN truthiness, and gcd/lcm range checks
This commit is contained in:
Keavon Chambers
2026-07-26 16:41:51 -07:00
committed by GitHub
parent 0952933ede
commit 949022cee0
8 changed files with 655 additions and 750 deletions
+76 -8
View File
@@ -1,5 +1,3 @@
#![allow(unused)]
pub mod ast;
mod constants;
pub mod context;
@@ -8,8 +6,7 @@ pub mod lexer;
pub mod parser;
pub mod value;
use ast::Unit;
use context::{EvalContext, ValueMap};
use context::EvalContext;
use executer::EvalError;
use parser::ParseError;
use value::Value;
@@ -23,25 +20,53 @@ pub fn evaluate(expression: &str) -> Result<Result<Value, EvalError>, ParseError
#[cfg(test)]
mod tests {
use super::*;
use ast::Unit;
use value::Number;
const EPSILON: f64 = 1e-10_f64;
#[test]
fn malformed_juxtaposed_numbers_fail_to_parse() {
// Two numbers cannot be glued together by a stray decimal point (they must not parse as implicit multiplication)
for input in ["1..5", "1.5.5", "1..", ".5.5"] {
assert!(evaluate(input).is_err(), "expected `{input}` to be a parse error");
}
}
#[test]
fn unrecognized_characters_fail_to_parse() {
// Unrecognized trailing input must be rejected rather than silently dropped after a valid prefix
for input in ["2@", "5#", "2 $ 3", "sqrt(4)@", "5 & 3", "5 | 3", "2 = 3"] {
assert!(evaluate(input).is_err(), "expected `{input}` to be a parse error");
}
}
#[test]
fn juxtaposed_numbers_fail_to_parse() {
// Adjacent number literals like digit-grouped `10 000` must not silently multiply
for input in ["2 3", "10 000", "1 .5", "sqrt(4).5", "2 3 + 1"] {
assert!(evaluate(input).is_err(), "expected `{input}` to be a parse error");
}
}
#[test]
fn extremely_long_fraction_parses() {
let input = format!("0.{}", "1".repeat(320));
let value = evaluate(&input).unwrap().unwrap();
assert_eq!(value.as_real(), Some(1. / 9.));
}
fn run_end_to_end_test(input: &str, expected_value: Value) {
let expr = match ast::Node::try_parse_from_str(input) {
Ok(expr) => expr,
Err(err) => panic!("failed to parse `{input}`: {err}"),
};
dbg!(&expr);
let context = EvalContext::default();
let actual_value = match expr.eval(&context) {
Ok(v) => v,
Err(err) => panic!("failed to evaluate {input} becuase of error {err}"),
Err(err) => panic!("failed to evaluate `{input}` because of error {err}"),
};
// compare
match (actual_value, expected_value) {
(Value::Number(Number::Complex(a)), Value::Number(Number::Complex(e))) => {
// real part
@@ -128,6 +153,17 @@ mod tests {
exponent_single: "2^3" => 8.,
exponent_mixed_operations: "2^3 + 4^2" => 24.,
exponent_nested: "2^(3+1)" => 16.,
exponent_right_associative: "2^2^3" => 256.,
exponent_unary_operand: "2^-1" => 0.5,
// Implicit multiplication binds like `*`/`/`: tighter than `+`, looser than `^`, left to right
implicit_multiplication_constant: "2pi" => 2. * std::f64::consts::PI,
implicit_multiplication_before_addition: "2pi + 1" => 2. * std::f64::consts::PI + 1.,
implicit_multiplication_shares_division: "1/2pi" => std::f64::consts::PI / 2.,
implicit_multiplication_left_to_right: "6/2pi" => 3. * std::f64::consts::PI,
implicit_multiplication_power_operand: "2pi^2" => 2. * std::f64::consts::PI.powi(2),
implicit_multiplication_function: "2sqrt(4)" => 4.,
implicit_multiplication_excludes_unary_minus: "2 -3" => -1.,
// Factorial (postfix !)
factorial_simple: "5!" => 120.,
@@ -275,5 +311,37 @@ mod tests {
// Complex nested expressions
if_nested_expr: "if((sqrt(16) + 2) * (sin(pi) + 1), 3 + 4 * 2, 5 - 2 / 1)" => 11.,
// Overflow-safe evaluation
factorial_overflows_to_infinity: "171!" => f64::INFINITY,
factorial_huge_input: "10000000000000000000000!" => f64::INFINITY,
lcm_huge_no_overflow: "lcm(1099511627776, 1099511627775)" => 1099511627776. * 1099511627775.,
gcd_non_finite: "gcd(inf, 6)" => f64::NAN,
long_literal: "10000000000000000000000" => 1e22,
huge_exponent_saturates: "1e4294967296" => f64::INFINITY,
// Odd integer roots of negative values are real
root_negative_odd: "root(-8, 3)" => -2.,
root_negative_odd_reciprocal: "root(-8, -3)" => -0.5,
root_negative_even: "root(-4, 2)" => f64::NAN,
// NaN poisons conditions and logic instead of acting as a boolean
if_nan_condition: "if(sqrt(-1), 1, 2)" => f64::NAN,
nan_and: "sqrt(-1) && 1" => f64::NAN,
nan_or: "sqrt(-1) || 1" => f64::NAN,
nan_not: "!sqrt(-1)" => f64::NAN,
// Logic and equality span real and complex operands
mixed_equality: "1 == i" => 0.,
complex_equality: "i == i" => 1.,
mixed_and: "1 && i" => 1.,
mixed_nan_and: "sqrt(-1) && i" => f64::NAN,
// Correctly rounded literals via std parsing
seventeen_digit_literal: "999999999999999999" => 1e18,
long_fraction_literal: "0.1111111111111111111111111111111111111111" => 1. / 9.,
// Integer functions reject inputs beyond f64's exact integer range
gcd_beyond_exact_integers: "gcd(10000000000000000000, 2)" => f64::NAN,
}
}