mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-19 02:48:12 +08:00
Move the math expression parser from Pest to Chumsky and add more features (#2685)
Rewrite the math-parser library using a chumsky-based lexer and parser, adding functions, comparisons, logic, and conditionals
This commit is contained in:
@@ -4,6 +4,7 @@ pub mod ast;
|
||||
mod constants;
|
||||
pub mod context;
|
||||
pub mod executer;
|
||||
pub mod lexer;
|
||||
pub mod parser;
|
||||
pub mod value;
|
||||
|
||||
@@ -13,10 +14,10 @@ use executer::EvalError;
|
||||
use parser::ParseError;
|
||||
use value::Value;
|
||||
|
||||
pub fn evaluate(expression: &str) -> Result<(Result<Value, EvalError>, Unit), ParseError> {
|
||||
pub fn evaluate(expression: &str) -> Result<Result<Value, EvalError>, ParseError> {
|
||||
let expr = ast::Node::try_parse_from_str(expression);
|
||||
let context = EvalContext::default();
|
||||
expr.map(|(node, unit)| (node.eval(&context), unit))
|
||||
expr.map(|node| node.eval(&context))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -27,136 +28,252 @@ mod tests {
|
||||
|
||||
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");
|
||||
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}"),
|
||||
};
|
||||
|
||||
// compare
|
||||
match (actual_value, expected_value) {
|
||||
(Value::Number(Number::Complex(a)), Value::Number(Number::Complex(e))) => {
|
||||
// real part
|
||||
if a.re.is_infinite() || e.re.is_infinite() {
|
||||
assert!(a.re == e.re, "`{}` → real part: expected {:?}, got {:?}", input, e.re, a.re);
|
||||
} else {
|
||||
assert!((a.re - e.re).abs() < EPSILON, "`{}` → real part: expected {}, got {}", input, e.re, a.re);
|
||||
}
|
||||
|
||||
// imag part
|
||||
if a.im.is_infinite() || e.im.is_infinite() {
|
||||
assert!(a.im == e.im, "`{}` → imag part: expected {:?}, got {:?}", input, e.im, a.im);
|
||||
} else {
|
||||
assert!((a.im - e.im).abs() < EPSILON, "`{}` → imag part: expected {}, got {}", input, e.im, a.im);
|
||||
}
|
||||
}
|
||||
|
||||
(Value::Number(Number::Real(a)), Value::Number(Number::Real(e))) => {
|
||||
if a.is_infinite() || e.is_infinite() {
|
||||
// both must be infinite and equal (i.e. both +∞ or both −∞)
|
||||
assert!(a == e, "`{input}` → expected infinite {e:?}, got {a:?}");
|
||||
} else if a.is_nan() || e.is_nan() {
|
||||
// both must be NaN
|
||||
assert!(a.is_nan() && e.is_nan(), "`{input}` → expected NaN, got {a:?}");
|
||||
} else {
|
||||
let diff = (a - e).abs();
|
||||
assert!(diff < EPSILON, "`{input}` → expected {e}, got {a}, Δ={diff}");
|
||||
}
|
||||
}
|
||||
|
||||
(got, expect) => {
|
||||
panic!("`{input}` → mismatched types: expected {expect:?}, got {got:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! test_end_to_end{
|
||||
($($name:ident: $input:expr_2021 => ($expected_value:expr_2021, $expected_unit:expr_2021)),* $(,)?) => {
|
||||
macro_rules! test_end_to_end {
|
||||
($($name:ident: $input:expr => $expected:expr),* $(,)?) => {
|
||||
$(
|
||||
#[test]
|
||||
fn $name() {
|
||||
let expected_value = $expected_value;
|
||||
let expected_unit = $expected_unit;
|
||||
|
||||
let expr = ast::Node::try_parse_from_str($input);
|
||||
let context = EvalContext::default();
|
||||
|
||||
let (actual_value, actual_unit) = expr.map(|(node, unit)| (node.eval(&context), unit)).unwrap();
|
||||
let actual_value = actual_value.unwrap();
|
||||
|
||||
|
||||
assert!(actual_unit == expected_unit, "Expected unit {:?} but found unit {:?}", expected_unit, actual_unit);
|
||||
|
||||
let expected_value = expected_value.into();
|
||||
|
||||
match (actual_value, expected_value) {
|
||||
(Value::Number(Number::Complex(actual_c)), Value::Number(Number::Complex(expected_c))) => {
|
||||
assert!(
|
||||
(actual_c.re.is_infinite() && expected_c.re.is_infinite()) || (actual_c.re - expected_c.re).abs() < EPSILON,
|
||||
"Expected real part {}, but got {}",
|
||||
expected_c.re,
|
||||
actual_c.re
|
||||
);
|
||||
assert!(
|
||||
(actual_c.im.is_infinite() && expected_c.im.is_infinite()) || (actual_c.im - expected_c.im).abs() < EPSILON,
|
||||
"Expected imaginary part {}, but got {}",
|
||||
expected_c.im,
|
||||
actual_c.im
|
||||
);
|
||||
}
|
||||
(Value::Number(Number::Real(actual_f)), Value::Number(Number::Real(expected_f))) => {
|
||||
if actual_f.is_infinite() || expected_f.is_infinite() {
|
||||
assert!(
|
||||
actual_f.is_infinite() && expected_f.is_infinite() && actual_f == expected_f,
|
||||
"Expected infinite value {}, but got {}",
|
||||
expected_f,
|
||||
actual_f
|
||||
);
|
||||
} else if actual_f.is_nan() || expected_f.is_nan() {
|
||||
assert!(actual_f.is_nan() && expected_f.is_nan(), "Expected NaN, but got {}", actual_f);
|
||||
} else {
|
||||
assert!((actual_f - expected_f).abs() < EPSILON, "Expected {}, but got {}", expected_f, actual_f);
|
||||
}
|
||||
}
|
||||
// Handle mismatched types
|
||||
_ => panic!("Mismatched types: expected {:?}, got {:?}", expected_value, actual_value),
|
||||
}
|
||||
|
||||
run_end_to_end_test($input, ($expected).into());
|
||||
}
|
||||
)*
|
||||
};
|
||||
}
|
||||
|
||||
test_end_to_end! {
|
||||
// Basic arithmetic and units
|
||||
infix_addition: "5 + 5" => (10., Unit::BASE_UNIT),
|
||||
infix_subtraction_units: "5m - 3m" => (2., Unit::LENGTH),
|
||||
infix_multiplication_units: "4s * 4s" => (16., Unit { length: 0, mass: 0, time: 2 }),
|
||||
infix_division_units: "8m/2s" => (4., Unit::VELOCITY),
|
||||
// Basic arithmetic
|
||||
infix_addition: "5 + 5" => 10.,
|
||||
infix_subtraction: "5 - 3" => 2.,
|
||||
infix_multiplication: "4 * 4" => 16.,
|
||||
infix_division: "8/2" => 4.,
|
||||
modulo_pos_pos: "3.2 % 2" => 1.2,
|
||||
modulo_pos_neg: "3.2 % -2" => 1.2,
|
||||
modulo_neg_neg: "(-3.2) % -2" => -1.2,
|
||||
modulo_neg_pos: "(-3.2) % 2" => -1.2,
|
||||
exp_pos_pos: "3.2 ^ 2" => 256. / 25.,
|
||||
exp_pos_neg: "3.2 ^ -2" => 25. / 256.,
|
||||
exp_neg_neg: "-3.2 ^ -2" => -25. / 256.,
|
||||
exp_neg_pos: "-3.2 ^ 2" => -256. / 25.,
|
||||
|
||||
// Order of operations
|
||||
order_of_operations_negative_prefix: "-10 + 5" => (-5., Unit::BASE_UNIT),
|
||||
order_of_operations_add_multiply: "5+1*1+5" => (11., Unit::BASE_UNIT),
|
||||
order_of_operations_add_negative_multiply: "5+(-1)*1+5" => (9., Unit::BASE_UNIT),
|
||||
order_of_operations_sqrt: "sqrt25 + 11" => (16., Unit::BASE_UNIT),
|
||||
order_of_operations_sqrt_expression: "sqrt(25+11)" => (6., Unit::BASE_UNIT),
|
||||
order_of_operations_negative_prefix: "-10 + 5" => -5.,
|
||||
order_of_operations_add_multiply: "5+1*1+5" => 11.,
|
||||
order_of_operations_add_negative_multiply: "5+(-1)*1+5" => 9.,
|
||||
order_of_operations_sqrt: "sqrt(25) + 11" => 16.,
|
||||
order_of_operations_sqrt_expression: "sqrt(25+11)" => 6.,
|
||||
|
||||
// Parentheses and nested expressions
|
||||
parentheses_nested_multiply: "(5 + 3) * (2 + 6)" => (64., Unit::BASE_UNIT),
|
||||
parentheses_mixed_operations: "2 * (3 + 5 * (2 + 1))" => (36., Unit::BASE_UNIT),
|
||||
parentheses_divide_add_multiply: "10 / (2 + 3) + (7 * 2)" => (16., Unit::BASE_UNIT),
|
||||
parentheses_nested_multiply: "(5 + 3) * (2 + 6)" => 64.,
|
||||
parentheses_mixed_operations: "2 * (3 + 5 * (2 + 1))" => 36.,
|
||||
parentheses_divide_add_multiply: "10 / (2 + 3) + (7 * 2)" => 16.,
|
||||
|
||||
// Square root and nested square root
|
||||
sqrt_chain_operations: "sqrt(16) + sqrt(9) * sqrt(4)" => (10., Unit::BASE_UNIT),
|
||||
sqrt_nested: "sqrt(sqrt(81))" => (3., Unit::BASE_UNIT),
|
||||
sqrt_divide_expression: "sqrt((25 + 11) / 9)" => (2., Unit::BASE_UNIT),
|
||||
sqrt_chain_operations: "sqrt(16) + sqrt(9) * sqrt(4)" => 10.,
|
||||
sqrt_nested: "sqrt(sqrt(81))" => 3.,
|
||||
sqrt_divide_expression: "sqrt((25 + 11) / 9)" => 2.,
|
||||
|
||||
// Mixed square root and units
|
||||
sqrt_multiply_units: "sqrt(16) * 2g + 5g" => (13., Unit::MASS),
|
||||
sqrt_add_multiply: "sqrt(49) - 1 + 2 * 3" => (12., Unit::BASE_UNIT),
|
||||
sqrt_addition_multiply: "(sqrt(36) + 2) * 2" => (16., Unit::BASE_UNIT),
|
||||
sqrt_add_multiply: "sqrt(49) - 1 + 2 * 3" => 12.,
|
||||
sqrt_addition_multiply: "(sqrt(36) + 2) * 2" => 16.,
|
||||
|
||||
// Exponentiation
|
||||
exponent_single: "2^3" => (8., Unit::BASE_UNIT),
|
||||
exponent_mixed_operations: "2^3 + 4^2" => (24., Unit::BASE_UNIT),
|
||||
exponent_nested: "2^(3+1)" => (16., Unit::BASE_UNIT),
|
||||
exponent_single: "2^3" => 8.,
|
||||
exponent_mixed_operations: "2^3 + 4^2" => 24.,
|
||||
exponent_nested: "2^(3+1)" => 16.,
|
||||
|
||||
// Factorial (postfix !)
|
||||
factorial_simple: "5!" => 120.,
|
||||
factorial_nested: "(3 + 2)!" => 120.,
|
||||
factorial_zero: "0!" => 1.,
|
||||
factorial_chain: "3!!" => 720., // (3!)! = 6! = 720
|
||||
|
||||
// Operations with negative values
|
||||
negative_units_add_multiply: "-5s + (-3 * 2)s" => (-11., Unit::TIME),
|
||||
negative_nested_parentheses: "-(5 + 3 * (2 - 1))" => (-8., Unit::BASE_UNIT),
|
||||
negative_sqrt_addition: "-(sqrt(16) + sqrt(9))" => (-7., Unit::BASE_UNIT),
|
||||
multiply_sqrt_subtract: "5 * 2 + sqrt(16) / 2 - 3" => (9., Unit::BASE_UNIT),
|
||||
add_multiply_subtract_sqrt: "4 + 3 * (2 + 1) - sqrt(25)" => (8., Unit::BASE_UNIT),
|
||||
add_sqrt_subtract_nested_multiply: "10 + sqrt(64) - (5 * (2 + 1))" => (3., Unit::BASE_UNIT),
|
||||
negative_nested_parentheses: "-(5 + 3 * (2 - 1))" => -8.,
|
||||
negative_sqrt_addition: "-(sqrt(16) + sqrt(9))" => -7.,
|
||||
multiply_sqrt_subtract: "5 * 2 + sqrt(16) / 2 - 3" => 9.,
|
||||
add_multiply_subtract_sqrt: "4 + 3 * (2 + 1) - sqrt(25)" => 8.,
|
||||
add_sqrt_subtract_nested_multiply: "10 + sqrt(64) - (5 * (2 + 1))" => 3.,
|
||||
|
||||
// Mathematical constants
|
||||
constant_pi: "pi" => (std::f64::consts::PI, Unit::BASE_UNIT),
|
||||
constant_e: "e" => (std::f64::consts::E, Unit::BASE_UNIT),
|
||||
constant_phi: "phi" => (1.61803398875, Unit::BASE_UNIT),
|
||||
constant_tau: "tau" => (2. * std::f64::consts::PI, Unit::BASE_UNIT),
|
||||
constant_infinity: "inf" => (f64::INFINITY, Unit::BASE_UNIT),
|
||||
constant_infinity_symbol: "∞" => (f64::INFINITY, Unit::BASE_UNIT),
|
||||
multiply_pi: "2 * pi" => (2. * std::f64::consts::PI, Unit::BASE_UNIT),
|
||||
add_e_constant: "e + 1" => (std::f64::consts::E + 1., Unit::BASE_UNIT),
|
||||
multiply_phi_constant: "phi * 2" => (1.61803398875 * 2., Unit::BASE_UNIT),
|
||||
exponent_tau: "2^tau" => (2f64.powf(2. * std::f64::consts::PI), Unit::BASE_UNIT),
|
||||
infinity_subtract_large_number: "inf - 1000" => (f64::INFINITY, Unit::BASE_UNIT),
|
||||
constant_pi: "pi" => std::f64::consts::PI,
|
||||
constant_e: "e" => std::f64::consts::E,
|
||||
constant_phi: "phi" => 1.61803398875,
|
||||
constant_tau: "tau" => 2. * std::f64::consts::PI,
|
||||
constant_infinity: "if(inf == ∞, inf, 0)" => f64::INFINITY,
|
||||
multiply_pi: "2 * pi" => 2. * std::f64::consts::PI,
|
||||
add_e_constant: "e + 1" => std::f64::consts::E + 1.,
|
||||
multiply_phi_constant: "phi * 2" => 1.61803398875 * 2.,
|
||||
exponent_tau: "2^tau" => 2f64.powf(2. * std::f64::consts::PI),
|
||||
infinity_subtract_large_number: "inf - 1000" => f64::INFINITY,
|
||||
|
||||
// Decimals with no leading digit before the point
|
||||
leading_dot_decimal: ".5" => (0.5, Unit::BASE_UNIT),
|
||||
leading_dot_in_expression: "1+.5" => (1.5, Unit::BASE_UNIT),
|
||||
leading_dot_exponent: ".5e3" => (500., Unit::BASE_UNIT),
|
||||
leading_dot_decimal: ".5" => 0.5,
|
||||
leading_dot_in_expression: "1+.5" => 1.5,
|
||||
leading_dot_exponent: ".5e3" => 500.,
|
||||
|
||||
// Trigonometric functions
|
||||
trig_sin_pi: "sin(pi)" => (0., Unit::BASE_UNIT),
|
||||
trig_cos_zero: "cos(0)" => (1., Unit::BASE_UNIT),
|
||||
trig_tan_pi_div_four: "tan(pi/4)" => (1., Unit::BASE_UNIT),
|
||||
trig_sin_tau: "sin(tau)" => (0., Unit::BASE_UNIT),
|
||||
trig_cos_tau_div_two: "cos(tau/2)" => (-1., Unit::BASE_UNIT),
|
||||
trig_sin_pi: "sin(pi)" => 0.,
|
||||
trig_cos_zero: "cos(0)" => 1.,
|
||||
trig_tan_pi_div_four: "tan(pi/4)" => 1.,
|
||||
trig_sin_tau: "sin(tau)" => 0.,
|
||||
trig_cos_tau_div_two: "cos(tau/2)" => -1.,
|
||||
trig_csc: "csc(pi/2)" => 1.,
|
||||
trig_sec: "sec(0)" => 1.,
|
||||
trig_cot: "cot(pi/4)" => 1.,
|
||||
|
||||
// Inverse trig aliases
|
||||
inverse_trig_asin: "asin(1)" => std::f64::consts::FRAC_PI_2,
|
||||
inverse_trig_acos: "acos(1)" => 0.,
|
||||
inverse_trig_atan: "atan(1)" => std::f64::consts::FRAC_PI_4,
|
||||
inverse_trig_acsc: "acsc(1)" => std::f64::consts::FRAC_PI_2,
|
||||
inverse_trig_asec: "asec(1)" => 0.,
|
||||
inverse_trig_acot: "acot(1)" => std::f64::consts::FRAC_PI_4,
|
||||
|
||||
// Hyperbolic and reciprocal hyperbolic
|
||||
hyperbolic_sinh: "sinh(0)" => 0.,
|
||||
hyperbolic_cosh: "cosh(0)" => 1.,
|
||||
hyperbolic_tanh: "tanh(0)" => 0.,
|
||||
hyperbolic_csch: "csch(1)" => 1f64.sinh().recip(),
|
||||
hyperbolic_sech: "sech(0)" => 1.,
|
||||
hyperbolic_coth: "coth(1)" => 1f64.tanh().recip(),
|
||||
|
||||
// Inverse hyperbolic
|
||||
inverse_hyperbolic_asinh: "asinh(0)" => 0.,
|
||||
inverse_hyperbolic_acosh: "acosh(1)" => 0.,
|
||||
inverse_hyperbolic_atanh: "atanh(0)" => 0.,
|
||||
inverse_hyperbolic_acsch: "acsch(1)" => 1f64.asinh(),
|
||||
inverse_hyperbolic_asech: "asech(1)" => 1f64.acosh(),
|
||||
inverse_hyperbolic_acoth: "acoth(2)" => 0.5f64.atanh(),
|
||||
|
||||
// Basic if statements
|
||||
if_true_condition: "if(1,5,3)" => 5.,
|
||||
if_false_condition: "if(0, 5, 3)" => 3.,
|
||||
|
||||
// Arithmetic conditions
|
||||
if_arithmetic_true: "if(2+2-4, 1 , 0)" => 0.,
|
||||
if_arithmetic_false: "if(3*2-5, 1, 0)" => 1.,
|
||||
|
||||
// Nested arithmetic
|
||||
if_complex_arithmetic: "if((5+3)*(2-1), 10, 20)" => 10.,
|
||||
if_with_division: "if(8/4-2 == 0, 15, 25)" => 15.,
|
||||
if_with_division_ne: "if(8/4-2 ≠ 0, 15, 25)" => 25.,
|
||||
|
||||
// Constants in conditions
|
||||
if_with_pi: "if(pi > 3, 1, 0)" => 1.,
|
||||
if_with_e: "if(e < 3, 1, 0)" => 1.,
|
||||
|
||||
// Functions in conditions
|
||||
if_with_sqrt: "if(sqrt(16) == 4, 1, 0)" => 1.,
|
||||
if_with_sin: "if(sin(pi) == 0.0, 1, 0)" => 0.,
|
||||
|
||||
// Logical NOT (prefix !)
|
||||
logical_not_zero: "!0" => 1.,
|
||||
logical_not_nonzero: "!5" => 0.,
|
||||
logical_not_expression: "!(2 - 2)" => 1.,
|
||||
|
||||
// Logical helpers as functions
|
||||
logical_isnan: "isnan(0/0)" => 1.,
|
||||
logical_eq: "eq(2, 2)" => 1.,
|
||||
logical_greater: "greater(3, 2)" => 1.,
|
||||
|
||||
// Log / exp / pow / root
|
||||
log_ln: "ln(e)" => 1.,
|
||||
log_log10: "log(100)" => 2.,
|
||||
log_log2: "log2(8)" => 3.,
|
||||
log_change_of_base: "log(8, 2)" => 3.,
|
||||
exp_function: "exp(1)" => std::f64::consts::E,
|
||||
pow_real: "pow(2, 3)" => 8.,
|
||||
root_square: "root(9, 2)" => 3.,
|
||||
root_cube: "root(8, 3)" => 2.,
|
||||
|
||||
// Nested if statements
|
||||
nested_if: "if(1, if(0, 1, 2), 3)" => 2.,
|
||||
nested_if_complex: "if(2-2 == 0, if(1, 5, 6), if(1, 7, 8))" => 5.,
|
||||
|
||||
// Mixed operations in conditions and blocks
|
||||
if_complex_condition: "if(sqrt(16) + sin(pi) < 5, 2*pi, 3*e)" => 2. * std::f64::consts::PI,
|
||||
if_complex_blocks: "if(1, 2*sqrt(16) + sin(pi/2), 3*cos(0) + 4)" => 9.,
|
||||
|
||||
// Mapping helpers
|
||||
mapping_trunc: "trunc(3.7)" => 3.,
|
||||
mapping_fract: "fract(3.25)" => 0.25,
|
||||
mapping_sign_pos: "sign(5)" => 1.,
|
||||
mapping_sign_neg: "sign(-5)" => -1.,
|
||||
|
||||
// Geometry / mapping extras
|
||||
geometry_hypot: "hypot(3, 4)" => 5.,
|
||||
mapping_remap: "remap(5, 0, 10, 0, 100)" => 50.,
|
||||
|
||||
// GCD / LCM
|
||||
gcd_simple: "gcd(24, 18)" => 6.,
|
||||
lcm_simple: "lcm(4, 6)" => 12.,
|
||||
|
||||
// atan2
|
||||
trig_atan2_axis: "atan2(1, 0)" => std::f64::consts::FRAC_PI_2,
|
||||
|
||||
// Comparison operators combined with logical AND
|
||||
comparison_operators: "if(1 <= 2 && 1 ≤ 2 && 2 >= 1 && 2 ≥ 1, 1., 0.)" => 1.,
|
||||
|
||||
// Logical AND / OR
|
||||
logical_and_true: "if(1 <= 2 && 2 < 3, 1., 0.)" => 1.,
|
||||
logical_and_false: "if(1 <= 2 && 3 < 2, 1., 0.)" => 0.,
|
||||
logical_or_true_left: "if(1 > 2 || 2 < 3, 1., 0.)" => 1.,
|
||||
logical_or_true_right: "if(2 < 1 || 2 < 3, 1., 0.)" => 1.,
|
||||
logical_or_false: "if(1 > 2 || 3 < 2, 1., 0.)" => 0.,
|
||||
logical_precedence_and_over_or: "if(0 == 1 || 1 == 1 && 0 == 0, 1., 0.)" => 1.,
|
||||
|
||||
// Edge cases
|
||||
if_zero: "if(0.0, 1, 2)" => 2.,
|
||||
|
||||
// Complex nested expressions
|
||||
if_nested_expr: "if((sqrt(16) + 2) * (sin(pi) + 1), 3 + 4 * 2, 5 - 2 / 1)" => 11.,
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user