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 Dennis Kobert
parent e569c56c97
commit 647f05532b
8 changed files with 655 additions and 750 deletions

1
Cargo.lock generated
View File

@@ -3394,7 +3394,6 @@ version = "0.0.0"
dependencies = [
"chumsky",
"criterion",
"lazy_static",
"num-complex",
"thiserror 2.0.18",
]

View File

@@ -9,7 +9,6 @@ license = "MIT OR Apache-2.0"
[dependencies]
thiserror = "2.0"
lazy_static = "1.5"
num-complex = "0.4"
chumsky = { version = "0.10", default-features = false, features = ["std"] }

View File

@@ -1,637 +1,421 @@
use crate::value::{Number, Value};
use lazy_static::lazy_static;
use num_complex::{Complex, ComplexFloat};
use std::collections::HashMap;
use num_complex::ComplexFloat;
use std::f64::consts::{LN_2, PI};
type FunctionImplementation = Box<dyn Fn(&[Value]) -> Option<Value> + Send + Sync>;
lazy_static! {
pub static ref DEFAULT_FUNCTIONS: HashMap<&'static str, FunctionImplementation> = {
let mut map: HashMap<&'static str, FunctionImplementation> = HashMap::new();
pub type BuiltinFunction = fn(&[Value]) -> Option<Value>;
map.insert(
"sqrt",
Box::new(|values| match values{
[Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(real.sqrt()))),
[Value::Number(Number::Complex(complex))] => Some(Value::Number(Number::Complex(complex.sqrt()))),
_ => None,
})
);
map.insert(
"sin",
Box::new(|values| match values {
[Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(real.sin()))),
[Value::Number(Number::Complex(complex))] => Some(Value::Number(Number::Complex(complex.sin()))),
_ => None,
}),
);
/// Truncates both operands to nonnegative integers for `gcd`/`lcm`, or `None` when either is non-finite or beyond f64's exactly-representable integer range.
fn integer_operands(a: f64, b: f64) -> Option<(u64, u64)> {
// The largest magnitude below which every integer is exactly representable in f64
const EXACT_INTEGER_LIMIT: f64 = (1_u64 << f64::MANTISSA_DIGITS) as f64;
map.insert(
"cos",
Box::new(|values| match values {
[Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(real.cos()))),
[Value::Number(Number::Complex(complex))] => Some(Value::Number(Number::Complex(complex.cos()))),
_ => None,
}),
);
let (a, b) = (a.trunc(), b.trunc());
if !a.is_finite() || !b.is_finite() || a.abs() > EXACT_INTEGER_LIMIT || b.abs() > EXACT_INTEGER_LIMIT {
return None;
}
Some(((a as i64).unsigned_abs(), (b as i64).unsigned_abs()))
}
map.insert(
"tan",
Box::new(|values| match values {
[Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(real.tan()))),
[Value::Number(Number::Complex(complex))] => Some(Value::Number(Number::Complex(complex.tan()))),
_ => None,
}),
);
fn euclidean_gcd(mut x: u64, mut y: u64) -> u64 {
while y != 0 {
(x, y) = (y, x % y);
}
x
}
map.insert(
"csc",
Box::new(|values| match values {
[Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(real.sin().recip()))),
[Value::Number(Number::Complex(complex))] => Some(Value::Number(Number::Complex(complex.sin().recip()))),
_ => None,
}),
);
/// Looks up a built-in math function by name, returning a plain function pointer so dispatch avoids hashing and dynamic allocation.
pub fn builtin_function(name: &str) -> Option<BuiltinFunction> {
Some(match name {
"sin" => |values| match values {
[Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(real.sin()))),
[Value::Number(Number::Complex(complex))] => Some(Value::Number(Number::Complex(complex.sin()))),
_ => None,
},
map.insert(
"sec",
Box::new(|values| match values {
[Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(real.cos().recip()))),
[Value::Number(Number::Complex(complex))] => Some(Value::Number(Number::Complex(complex.cos().recip()))),
_ => None,
}),
);
"cos" => |values| match values {
[Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(real.cos()))),
[Value::Number(Number::Complex(complex))] => Some(Value::Number(Number::Complex(complex.cos()))),
_ => None,
},
map.insert(
"cot",
Box::new(|values| match values {
[Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(real.tan().recip()))),
[Value::Number(Number::Complex(complex))] => Some(Value::Number(Number::Complex(complex.tan().recip()))),
_ => None,
}),
);
"tan" => |values| match values {
[Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(real.tan()))),
[Value::Number(Number::Complex(complex))] => Some(Value::Number(Number::Complex(complex.tan()))),
_ => None,
},
"csc" => |values| match values {
[Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(real.sin().recip()))),
[Value::Number(Number::Complex(complex))] => Some(Value::Number(Number::Complex(complex.sin().recip()))),
_ => None,
},
"sec" => |values| match values {
[Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(real.cos().recip()))),
[Value::Number(Number::Complex(complex))] => Some(Value::Number(Number::Complex(complex.cos().recip()))),
_ => None,
},
"cot" => |values| match values {
[Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(real.tan().recip()))),
[Value::Number(Number::Complex(complex))] => Some(Value::Number(Number::Complex(complex.tan().recip()))),
_ => None,
},
// Inverse trig with legacy names and standard aliases
map.insert(
"invsin",
Box::new(|values| match values {
[Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(real.asin()))),
[Value::Number(Number::Complex(complex))] => Some(Value::Number(Number::Complex(complex.asin()))),
_ => None,
}),
);
map.insert(
"asin",
Box::new(|values| match values {
[Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(real.asin()))),
[Value::Number(Number::Complex(complex))] => Some(Value::Number(Number::Complex(complex.asin()))),
_ => None,
}),
);
"invsin" => |values| match values {
[Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(real.asin()))),
[Value::Number(Number::Complex(complex))] => Some(Value::Number(Number::Complex(complex.asin()))),
_ => None,
},
"asin" => |values| match values {
[Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(real.asin()))),
[Value::Number(Number::Complex(complex))] => Some(Value::Number(Number::Complex(complex.asin()))),
_ => None,
},
map.insert(
"invcos",
Box::new(|values| match values {
[Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(real.acos()))),
[Value::Number(Number::Complex(complex))] => Some(Value::Number(Number::Complex(complex.acos()))),
_ => None,
}),
);
map.insert(
"acos",
Box::new(|values| match values {
[Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(real.acos()))),
[Value::Number(Number::Complex(complex))] => Some(Value::Number(Number::Complex(complex.acos()))),
_ => None,
}),
);
"invcos" => |values| match values {
[Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(real.acos()))),
[Value::Number(Number::Complex(complex))] => Some(Value::Number(Number::Complex(complex.acos()))),
_ => None,
},
"acos" => |values| match values {
[Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(real.acos()))),
[Value::Number(Number::Complex(complex))] => Some(Value::Number(Number::Complex(complex.acos()))),
_ => None,
},
map.insert(
"invtan",
Box::new(|values| match values {
[Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(real.atan()))),
[Value::Number(Number::Complex(complex))] => Some(Value::Number(Number::Complex(complex.atan()))),
_ => None,
}),
);
map.insert(
"atan",
Box::new(|values| match values {
[Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(real.atan()))),
[Value::Number(Number::Complex(complex))] => Some(Value::Number(Number::Complex(complex.atan()))),
_ => None,
}),
);
"invtan" => |values| match values {
[Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(real.atan()))),
[Value::Number(Number::Complex(complex))] => Some(Value::Number(Number::Complex(complex.atan()))),
_ => None,
},
"atan" => |values| match values {
[Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(real.atan()))),
[Value::Number(Number::Complex(complex))] => Some(Value::Number(Number::Complex(complex.atan()))),
_ => None,
},
map.insert(
"invcsc",
Box::new(|values| match values {
[Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(real.recip().asin()))),
[Value::Number(Number::Complex(complex))] => Some(Value::Number(Number::Complex(complex.recip().asin()))),
_ => None,
}),
);
map.insert(
"acsc",
Box::new(|values| match values {
[Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(real.recip().asin()))),
[Value::Number(Number::Complex(complex))] => Some(Value::Number(Number::Complex(complex.recip().asin()))),
_ => None,
}),
);
"invcsc" => |values| match values {
[Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(real.recip().asin()))),
[Value::Number(Number::Complex(complex))] => Some(Value::Number(Number::Complex(complex.recip().asin()))),
_ => None,
},
"acsc" => |values| match values {
[Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(real.recip().asin()))),
[Value::Number(Number::Complex(complex))] => Some(Value::Number(Number::Complex(complex.recip().asin()))),
_ => None,
},
map.insert(
"invsec",
Box::new(|values| match values {
[Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(real.recip().acos()))),
[Value::Number(Number::Complex(complex))] => Some(Value::Number(Number::Complex(complex.recip().acos()))),
_ => None,
}),
);
map.insert(
"asec",
Box::new(|values| match values {
[Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(real.recip().acos()))),
[Value::Number(Number::Complex(complex))] => Some(Value::Number(Number::Complex(complex.recip().acos()))),
_ => None,
}),
);
"invsec" => |values| match values {
[Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(real.recip().acos()))),
[Value::Number(Number::Complex(complex))] => Some(Value::Number(Number::Complex(complex.recip().acos()))),
_ => None,
},
"asec" => |values| match values {
[Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(real.recip().acos()))),
[Value::Number(Number::Complex(complex))] => Some(Value::Number(Number::Complex(complex.recip().acos()))),
_ => None,
},
map.insert(
"invcot",
Box::new(|values| match values {
[Value::Number(Number::Real(real))] => {
Some(Value::Number(Number::Real(real.recip().atan())))
}
[Value::Number(Number::Complex(complex))] => {
Some(Value::Number(Number::Complex(complex.recip().atan())))
}
_ => None,
}),
);
map.insert(
"acot",
Box::new(|values| match values {
[Value::Number(Number::Real(real))] => {
Some(Value::Number(Number::Real(real.recip().atan())))
}
[Value::Number(Number::Complex(complex))] => {
Some(Value::Number(Number::Complex(complex.recip().atan())))
}
_ => None,
}),
);
"invcot" => |values| match values {
[Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(real.recip().atan()))),
[Value::Number(Number::Complex(complex))] => Some(Value::Number(Number::Complex(complex.recip().atan()))),
_ => None,
},
"acot" => |values| match values {
[Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(real.recip().atan()))),
[Value::Number(Number::Complex(complex))] => Some(Value::Number(Number::Complex(complex.recip().atan()))),
_ => None,
},
// Hyperbolic Functions
map.insert(
"sinh",
Box::new(|values| match values {
[Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(real.sinh()))),
[Value::Number(Number::Complex(complex))] => Some(Value::Number(Number::Complex(complex.sinh()))),
_ => None,
}),
);
"sinh" => |values| match values {
[Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(real.sinh()))),
[Value::Number(Number::Complex(complex))] => Some(Value::Number(Number::Complex(complex.sinh()))),
_ => None,
},
map.insert(
"cosh",
Box::new(|values| match values {
[Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(real.cosh()))),
[Value::Number(Number::Complex(complex))] => Some(Value::Number(Number::Complex(complex.cosh()))),
_ => None,
}),
);
"cosh" => |values| match values {
[Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(real.cosh()))),
[Value::Number(Number::Complex(complex))] => Some(Value::Number(Number::Complex(complex.cosh()))),
_ => None,
},
map.insert(
"tanh",
Box::new(|values| match values {
[Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(real.tanh()))),
[Value::Number(Number::Complex(complex))] => Some(Value::Number(Number::Complex(complex.tanh()))),
_ => None,
}),
);
"tanh" => |values| match values {
[Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(real.tanh()))),
[Value::Number(Number::Complex(complex))] => Some(Value::Number(Number::Complex(complex.tanh()))),
_ => None,
},
// Reciprocal hyperbolic functions
map.insert(
"csch",
Box::new(|values| match values {
[Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(real.sinh().recip()))),
[Value::Number(Number::Complex(complex))] => Some(Value::Number(Number::Complex(complex.sinh().recip()))),
_ => None,
}),
);
"csch" => |values| match values {
[Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(real.sinh().recip()))),
[Value::Number(Number::Complex(complex))] => Some(Value::Number(Number::Complex(complex.sinh().recip()))),
_ => None,
},
map.insert(
"sech",
Box::new(|values| match values {
[Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(real.cosh().recip()))),
[Value::Number(Number::Complex(complex))] => Some(Value::Number(Number::Complex(complex.cosh().recip()))),
_ => None,
}),
);
"sech" => |values| match values {
[Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(real.cosh().recip()))),
[Value::Number(Number::Complex(complex))] => Some(Value::Number(Number::Complex(complex.cosh().recip()))),
_ => None,
},
map.insert(
"coth",
Box::new(|values| match values {
[Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(real.tanh().recip()))),
[Value::Number(Number::Complex(complex))] => Some(Value::Number(Number::Complex(complex.tanh().recip()))),
_ => None,
}),
);
"coth" => |values| match values {
[Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(real.tanh().recip()))),
[Value::Number(Number::Complex(complex))] => Some(Value::Number(Number::Complex(complex.tanh().recip()))),
_ => None,
},
// Inverse Hyperbolic Functions
map.insert(
"asinh",
Box::new(|values| match values {
[Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(real.asinh()))),
[Value::Number(Number::Complex(complex))] => Some(Value::Number(Number::Complex(complex.asinh()))),
_ => None,
}),
);
"asinh" => |values| match values {
[Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(real.asinh()))),
[Value::Number(Number::Complex(complex))] => Some(Value::Number(Number::Complex(complex.asinh()))),
_ => None,
},
map.insert(
"acosh",
Box::new(|values| match values {
[Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(real.acosh()))),
[Value::Number(Number::Complex(complex))] => Some(Value::Number(Number::Complex(complex.acosh()))),
_ => None,
}),
);
"acosh" => |values| match values {
[Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(real.acosh()))),
[Value::Number(Number::Complex(complex))] => Some(Value::Number(Number::Complex(complex.acosh()))),
_ => None,
},
map.insert(
"atanh",
Box::new(|values| match values {
[Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(real.atanh()))),
[Value::Number(Number::Complex(complex))] => Some(Value::Number(Number::Complex(complex.atanh()))),
_ => None,
}),
);
"atanh" => |values| match values {
[Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(real.atanh()))),
[Value::Number(Number::Complex(complex))] => Some(Value::Number(Number::Complex(complex.atanh()))),
_ => None,
},
// Inverse reciprocal hyperbolic functions
map.insert(
"acsch",
Box::new(|values| match values {
[Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(real.recip().asinh()))),
[Value::Number(Number::Complex(complex))] => Some(Value::Number(Number::Complex(complex.recip().asinh()))),
_ => None,
}),
);
"acsch" => |values| match values {
[Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(real.recip().asinh()))),
[Value::Number(Number::Complex(complex))] => Some(Value::Number(Number::Complex(complex.recip().asinh()))),
_ => None,
},
map.insert(
"asech",
Box::new(|values| match values {
[Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(real.recip().acosh()))),
[Value::Number(Number::Complex(complex))] => Some(Value::Number(Number::Complex(complex.recip().acosh()))),
_ => None,
}),
);
"asech" => |values| match values {
[Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(real.recip().acosh()))),
[Value::Number(Number::Complex(complex))] => Some(Value::Number(Number::Complex(complex.recip().acosh()))),
_ => None,
},
map.insert(
"acoth",
Box::new(|values| match values {
[Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(real.recip().atanh()))),
[Value::Number(Number::Complex(complex))] => Some(Value::Number(Number::Complex(complex.recip().atanh()))),
_ => None,
}),
);
"acoth" => |values| match values {
[Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(real.recip().atanh()))),
[Value::Number(Number::Complex(complex))] => Some(Value::Number(Number::Complex(complex.recip().atanh()))),
_ => None,
},
// Logarithm Functions
map.insert(
"ln",
Box::new(|values| match values {
[Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(real.ln()))),
[Value::Number(Number::Complex(complex))] => Some(Value::Number(Number::Complex(complex.ln()))),
_ => None,
}),
);
"ln" => |values| match values {
[Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(real.ln()))),
[Value::Number(Number::Complex(complex))] => Some(Value::Number(Number::Complex(complex.ln()))),
_ => None,
},
// Exponential / power helpers
map.insert(
"exp",
Box::new(|values| match values {
[Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(real.exp()))),
[Value::Number(Number::Complex(complex))] => Some(Value::Number(Number::Complex(complex.exp()))),
_ => None,
}),
);
"exp" => |values| match values {
[Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(real.exp()))),
[Value::Number(Number::Complex(complex))] => Some(Value::Number(Number::Complex(complex.exp()))),
_ => None,
},
map.insert(
"pow",
Box::new(|values| match values {
[Value::Number(Number::Real(x)), Value::Number(Number::Real(n))] => {
Some(Value::Number(Number::Real(x.powf(*n))))
}
[Value::Number(Number::Complex(x)), Value::Number(Number::Real(n))] => {
Some(Value::Number(Number::Complex(x.powf(*n))))
}
[Value::Number(Number::Complex(x)), Value::Number(Number::Complex(n))] => {
Some(Value::Number(Number::Complex(x.powc(*n))))
}
_ => None,
}),
);
"pow" => |values| match values {
[Value::Number(Number::Real(x)), Value::Number(Number::Real(n))] => Some(Value::Number(Number::Real(x.powf(*n)))),
[Value::Number(Number::Complex(x)), Value::Number(Number::Real(n))] => Some(Value::Number(Number::Complex(x.powf(*n)))),
[Value::Number(Number::Complex(x)), Value::Number(Number::Complex(n))] => Some(Value::Number(Number::Complex(x.powc(*n)))),
_ => None,
},
map.insert(
"root",
Box::new(|values| match values {
[Value::Number(Number::Real(x)), Value::Number(Number::Real(n))] => {
Some(Value::Number(Number::Real(x.powf(1. / *n))))
}
[Value::Number(Number::Complex(x)), Value::Number(Number::Real(n))] => {
Some(Value::Number(Number::Complex(x.powf(1. / *n))))
}
_ => None,
}),
);
"root" => |values| match values {
[Value::Number(Number::Real(x)), Value::Number(Number::Real(n))] => {
// Odd integer roots of negative reals are real, which powf alone would report as NaN
let root = if *x < 0. && n.rem_euclid(2.) == 1. { -(-x).powf(1. / *n) } else { x.powf(1. / *n) };
Some(Value::Number(Number::Real(root)))
}
[Value::Number(Number::Complex(x)), Value::Number(Number::Real(n))] => Some(Value::Number(Number::Complex(x.powf(1. / *n)))),
_ => None,
},
map.insert(
"log",
Box::new(|values| match values {
[Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(real.log10()))),
[Value::Number(Number::Complex(complex))] => Some(Value::Number(Number::Complex(complex.log10()))),
[Value::Number(n), Value::Number(base)] => {
// Custom base logarithm using change of base formula
let compute_log = |x: f64, b: f64| -> f64 { x.ln() / b.ln() };
match (n, base) {
(Number::Real(x), Number::Real(b)) => Some(Value::Number(Number::Real(compute_log(*x, *b)))),
_ => None,
}
"log" => |values| match values {
[Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(real.log10()))),
[Value::Number(Number::Complex(complex))] => Some(Value::Number(Number::Complex(complex.log10()))),
[Value::Number(n), Value::Number(base)] => {
// Custom base logarithm using change of base formula
let compute_log = |x: f64, b: f64| -> f64 { x.ln() / b.ln() };
match (n, base) {
(Number::Real(x), Number::Real(b)) => Some(Value::Number(Number::Real(compute_log(*x, *b)))),
_ => None,
}
_ => None,
}),
);
}
_ => None,
},
map.insert(
"log2",
Box::new(|values| match values {
[Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(real.log2()))),
[Value::Number(Number::Complex(complex))] => Some(Value::Number(Number::Complex(complex / LN_2))),
_ => None,
}),
);
"log2" => |values| match values {
[Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(real.log2()))),
[Value::Number(Number::Complex(complex))] => Some(Value::Number(Number::Complex(complex.ln() / LN_2))),
_ => None,
},
// Root Functions
map.insert(
"sqrt",
Box::new(|values| match values {
[Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(real.sqrt()))),
[Value::Number(Number::Complex(complex))] => Some(Value::Number(Number::Complex(complex.sqrt()))),
_ => None,
}),
);
"sqrt" => |values| match values {
[Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(real.sqrt()))),
[Value::Number(Number::Complex(complex))] => Some(Value::Number(Number::Complex(complex.sqrt()))),
_ => None,
},
map.insert(
"cbrt",
Box::new(|values| match values {
[Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(real.cbrt()))),
[Value::Number(Number::Complex(complex))] => Some(Value::Number(Number::Complex(complex.powf(1./3.)))),
_ => None,
}),
);
"cbrt" => |values| match values {
[Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(real.cbrt()))),
[Value::Number(Number::Complex(complex))] => Some(Value::Number(Number::Complex(complex.powf(1. / 3.)))),
_ => None,
},
// Geometry Functions
map.insert(
"hypot",
Box::new(|values| match values {
[Value::Number(Number::Real(a)), Value::Number(Number::Real(b))] => {
Some(Value::Number(Number::Real(a.hypot(*b))))
},
_ => None,
}),
);
"hypot" => |values| match values {
[Value::Number(Number::Real(a)), Value::Number(Number::Real(b))] => Some(Value::Number(Number::Real(a.hypot(*b)))),
_ => None,
},
map.insert(
"atan2",
Box::new(|values| match values {
[Value::Number(Number::Real(y)), Value::Number(Number::Real(x))] => {
Some(Value::Number(Number::Real(y.atan2(*x))))
}
_ => None,
}),
);
"atan2" => |values| match values {
[Value::Number(Number::Real(y)), Value::Number(Number::Real(x))] => Some(Value::Number(Number::Real(y.atan2(*x)))),
_ => None,
},
// Mapping Functions
map.insert(
"abs",
Box::new(|values| match values {
[Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(real.abs()))),
[Value::Number(Number::Complex(complex))] => Some(Value::Number(Number::Real(complex.abs()))),
_ => None,
}),
);
"abs" => |values| match values {
[Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(real.abs()))),
[Value::Number(Number::Complex(complex))] => Some(Value::Number(Number::Real(complex.abs()))),
_ => None,
},
map.insert(
"floor",
Box::new(|values| match values {
[Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(real.floor()))),
_ => None,
}),
);
"floor" => |values| match values {
[Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(real.floor()))),
_ => None,
},
map.insert(
"ceil",
Box::new(|values| match values {
[Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(real.ceil()))),
_ => None,
}),
);
"ceil" => |values| match values {
[Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(real.ceil()))),
_ => None,
},
map.insert(
"round",
Box::new(|values| match values {
[Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(real.round()))),
_ => None,
}),
);
"round" => |values| match values {
[Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(real.round()))),
_ => None,
},
map.insert(
"clamp",
Box::new(|values| match values {
[Value::Number(Number::Real(x)), Value::Number(Number::Real(min)), Value::Number(Number::Real(max))] => {
Some(Value::Number(Number::Real(x.clamp(*min, *max))))
},
_ => None,
}),
);
"clamp" => |values| match values {
[Value::Number(Number::Real(x)), Value::Number(Number::Real(min)), Value::Number(Number::Real(max))] => Some(Value::Number(Number::Real(x.clamp(*min, *max)))),
_ => None,
},
map.insert(
"lerp",
Box::new(|values| match values {
[Value::Number(Number::Real(a)), Value::Number(Number::Real(b)), Value::Number(Number::Real(t))] => {
Some(Value::Number(Number::Real(a + (b - a) * t)))
},
_ => None,
}),
);
"lerp" => |values| match values {
[Value::Number(Number::Real(a)), Value::Number(Number::Real(b)), Value::Number(Number::Real(t))] => Some(Value::Number(Number::Real(a + (b - a) * t))),
_ => None,
},
map.insert(
"remap",
Box::new(|values| match values {
[
Value::Number(Number::Real(value)),
Value::Number(Number::Real(in_a)),
Value::Number(Number::Real(in_b)),
Value::Number(Number::Real(out_a)),
Value::Number(Number::Real(out_b)),
] => {
let t = (*value - *in_a) / (*in_b - *in_a);
Some(Value::Number(Number::Real(out_a + t * (out_b - out_a))))
"remap" => |values| match values {
[
Value::Number(Number::Real(value)),
Value::Number(Number::Real(in_a)),
Value::Number(Number::Real(in_b)),
Value::Number(Number::Real(out_a)),
Value::Number(Number::Real(out_b)),
] => {
let t = (*value - *in_a) / (*in_b - *in_a);
Some(Value::Number(Number::Real(out_a + t * (out_b - out_a))))
}
_ => None,
},
"trunc" => |values| match values {
[Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(real.trunc()))),
_ => None,
},
"fract" => |values| match values {
[Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(real.fract()))),
_ => None,
},
"sign" => |values| match values {
[Value::Number(Number::Real(real))] => {
let s = if *real > 0. {
1.
} else if *real < 0. {
-1.
} else {
0.
};
Some(Value::Number(Number::Real(s)))
}
_ => None,
},
"gcd" => |values| match values {
[Value::Number(Number::Real(a)), Value::Number(Number::Real(b))] => {
let gcd = integer_operands(*a, *b).map_or(f64::NAN, |(x, y)| euclidean_gcd(x, y) as f64);
Some(Value::Number(Number::Real(gcd)))
}
_ => None,
},
"lcm" => |values| match values {
[Value::Number(Number::Real(a)), Value::Number(Number::Real(b))] => {
let Some((x, y)) = integer_operands(*a, *b) else {
return Some(Value::Number(Number::Real(f64::NAN)));
};
if x == 0 || y == 0 {
return Some(Value::Number(Number::Real(0.)));
}
_ => None,
}),
);
map.insert(
"trunc",
Box::new(|values| match values {
[Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(real.trunc()))),
_ => None,
}),
);
map.insert(
"fract",
Box::new(|values| match values {
[Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(real.fract()))),
_ => None,
}),
);
map.insert(
"sign",
Box::new(|values| match values {
[Value::Number(Number::Real(real))] => {
let s = if *real > 0. {
1.
} else if *real < 0. {
-1.
} else {
0.
};
Some(Value::Number(Number::Real(s)))
}
_ => None,
}),
);
map.insert(
"gcd",
Box::new(|values| match values {
[Value::Number(Number::Real(a)), Value::Number(Number::Real(b))] => {
let mut x = a.trunc() as i64;
let mut y = b.trunc() as i64;
if x == 0 && y == 0 {
return Some(Value::Number(Number::Real(0.)));
}
x = x.abs();
y = y.abs();
while y != 0 {
let r = x % y;
x = y;
y = r;
}
Some(Value::Number(Number::Real(x as f64)))
}
_ => None,
}),
);
map.insert(
"lcm",
Box::new(|values| match values {
[Value::Number(Number::Real(a)), Value::Number(Number::Real(b))] => {
let mut x = a.trunc() as i64;
let mut y = b.trunc() as i64;
x = x.abs();
y = y.abs();
if x == 0 || y == 0 {
return Some(Value::Number(Number::Real(0.)));
}
// gcd
let mut gx = x;
let mut gy = y;
while gy != 0 {
let r = gx % gy;
gx = gy;
gy = r;
}
let lcm = (x / gx) * y;
Some(Value::Number(Number::Real(lcm as f64)))
}
_ => None,
}),
);
// Multiply in f64 so huge results can't overflow the integer range
let lcm = (x / euclidean_gcd(x, y)) as f64 * y as f64;
Some(Value::Number(Number::Real(lcm)))
}
_ => None,
},
// Complex Number Functions
map.insert(
"real",
Box::new(|values| match values {
[Value::Number(Number::Complex(complex))] => Some(Value::Number(Number::Real(complex.re))),
[Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(*real))),
_ => None,
}),
);
"real" => |values| match values {
[Value::Number(Number::Complex(complex))] => Some(Value::Number(Number::Real(complex.re))),
[Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(*real))),
_ => None,
},
map.insert(
"imag",
Box::new(|values| match values {
[Value::Number(Number::Complex(complex))] => Some(Value::Number(Number::Real(complex.im))),
[Value::Number(Number::Real(_))] => Some(Value::Number(Number::Real(0.))),
_ => None,
}),
);
"imag" => |values| match values {
[Value::Number(Number::Complex(complex))] => Some(Value::Number(Number::Real(complex.im))),
[Value::Number(Number::Real(_))] => Some(Value::Number(Number::Real(0.))),
_ => None,
},
map.insert(
"conj",
Box::new(|values| match values {
[Value::Number(Number::Complex(complex))] => {
Some(Value::Number(Number::Complex(complex.conj())))
}
[Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(*real))),
_ => None,
}),
);
"conj" => |values| match values {
[Value::Number(Number::Complex(complex))] => Some(Value::Number(Number::Complex(complex.conj()))),
[Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(*real))),
_ => None,
},
map.insert(
"arg",
Box::new(|values| match values {
[Value::Number(Number::Complex(complex))] => {
Some(Value::Number(Number::Real(complex.arg())))
}
[Value::Number(Number::Real(real))] => {
let angle = if *real >= 0. { 0. } else { PI };
Some(Value::Number(Number::Real(angle)))
}
_ => None,
}),
);
"arg" => |values| match values {
[Value::Number(Number::Complex(complex))] => Some(Value::Number(Number::Real(complex.arg()))),
[Value::Number(Number::Real(real))] => {
let angle = if *real >= 0. { 0. } else { PI };
Some(Value::Number(Number::Real(angle)))
}
_ => None,
},
// Logical Functions
map.insert(
"isnan",
Box::new(|values| match values {
[Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(if real.is_nan() { 1. } else { 0. }))),
_ => None,
}),
);
"isnan" => |values| match values {
[Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(if real.is_nan() { 1. } else { 0. }))),
_ => None,
},
map.insert(
"eq",
Box::new(|values| match values {
[Value::Number(a), Value::Number(b)] => Some(Value::Number(Number::Real(if a == b { 1. } else { 0. }))),
_ => None,
}),
);
"eq" => |values| match values {
[Value::Number(a), Value::Number(b)] => Some(Value::Number(Number::Real(if a == b { 1. } else { 0. }))),
_ => None,
},
map.insert(
"greater",
Box::new(|values| match values {
[Value::Number(Number::Real(a)), Value::Number(Number::Real(b))] => {
Some(Value::Number(Number::Real(if a > b { 1. } else { 0. })))
},
_ => None,
}),
);
map
};
"greater" => |values| match values {
[Value::Number(Number::Real(a)), Value::Number(Number::Real(b))] => Some(Value::Number(Number::Real(if a > b { 1. } else { 0. }))),
_ => None,
},
_ => return None,
})
}

View File

@@ -1,8 +1,7 @@
use crate::ast::{Literal, Node};
use crate::constants::DEFAULT_FUNCTIONS;
use crate::ast::{BinaryOp, Literal, Node};
use crate::constants::builtin_function;
use crate::context::{EvalContext, FunctionProvider, ValueProvider};
use crate::value::{Number, Value};
use num_complex::Complex;
use thiserror::Error;
#[derive(Debug, Error)]
@@ -12,8 +11,12 @@ pub enum EvalError {
#[error("Missing function: {0}")]
MissingFunction(String),
#[error("Wrong type for function call")]
#[error("Wrong argument types for function call")]
TypeError,
#[error("Unsupported operand types for operator")]
OperatorTypeError,
}
impl Node {
@@ -25,28 +28,43 @@ impl Node {
},
Node::BinOp { lhs, op, rhs } => match (lhs.eval(context)?, rhs.eval(context)?) {
(Value::Number(lhs), Value::Number(rhs)) => Ok(Value::Number(lhs.binary_op(*op, rhs).ok_or(EvalError::TypeError)?)),
(Value::Number(lhs), Value::Number(rhs)) => Ok(Value::Number(lhs.binary_op(*op, rhs).ok_or(EvalError::OperatorTypeError)?)),
},
Node::UnaryOp { expr, op } => match expr.eval(context)? {
Value::Number(num) => Ok(Value::Number(num.unary_op(*op))),
},
Node::Var(name) => context.get_value(name).ok_or_else(|| EvalError::MissingValue(name.clone())),
Node::FnCall { name, expr } => {
let values = expr.iter().map(|expr| expr.eval(context)).collect::<Result<Vec<Value>, EvalError>>()?;
if let Some(function) = DEFAULT_FUNCTIONS.get(&name.as_str()) {
function(&values).ok_or(EvalError::TypeError)
} else if let Some(val) = context.run_function(name, &values) {
Ok(val)
// Arguments land in a stack buffer when they fit (builtins take at most 5), avoiding a heap allocation per call
let mut stack_values = [Value::from_f64(0.); 5];
let heap_values: Vec<Value>;
let values: &[Value] = if expr.len() <= stack_values.len() {
for (slot, argument) in stack_values.iter_mut().zip(expr) {
*slot = argument.eval(context)?;
}
&stack_values[..expr.len()]
} else {
context.get_value(name).ok_or_else(|| EvalError::MissingFunction(name.to_string()))
heap_values = expr.iter().map(|argument| argument.eval(context)).collect::<Result<Vec<Value>, EvalError>>()?;
&heap_values
};
if let Some(function) = builtin_function(name) {
function(values).ok_or(EvalError::TypeError)
} else if let Some(val) = context.run_function(name, values) {
Ok(val)
} else if let Some(Value::Number(value)) = context.get_value(name)
&& let [Value::Number(argument)] = values
{
// A known value applied to one argument is implicit multiplication, so `x(2)` matches `2(3)` and `i(16)`
Ok(Value::Number(value.binary_op(BinaryOp::Mul, *argument).ok_or(EvalError::OperatorTypeError)?))
} else {
Err(EvalError::MissingFunction(name.to_string()))
}
}
Node::Conditional { condition, if_block, else_block } => {
let condition = match condition.eval(context)? {
Value::Number(Number::Real(number)) => number != 0.,
Value::Number(Number::Complex(number)) => number != Complex::ZERO,
};
// A NaN condition yields NaN rather than arbitrarily picking a branch
let Value::Number(number) = condition.eval(context)?;
let Some(condition) = number.as_bool() else { return Ok(Value::from_f64(f64::NAN)) };
if condition { if_block.eval(context) } else { else_block.eval(context) }
}
@@ -57,9 +75,37 @@ impl Node {
#[cfg(test)]
mod tests {
use crate::ast::{BinaryOp, Literal, Node, UnaryOp};
use crate::context::{EvalContext, ValueMap};
use crate::context::{EvalContext, NothingMap, ValueProvider};
use crate::value::Value;
struct SingleValue(f64);
impl ValueProvider for SingleValue {
fn get_value(&self, name: &str) -> Option<Value> {
(name == "x").then(|| Value::from_f64(self.0))
}
}
#[test]
fn known_value_with_one_argument_multiplies() {
// `x(2)` juxtaposes like `2(3)` and `i(16)` instead of silently discarding the argument
let call = Node::FnCall {
name: "x".to_string(),
expr: vec![Node::Lit(Literal::Float(2.))],
};
let result = call.eval(&EvalContext::new(SingleValue(5.), NothingMap)).unwrap();
assert_eq!(result, Value::from_f64(10.));
}
#[test]
fn known_value_with_multiple_arguments_is_an_error() {
let call = Node::FnCall {
name: "x".to_string(),
expr: vec![Node::Lit(Literal::Float(1.)), Node::Lit(Literal::Float(2.))],
};
assert!(call.eval(&EvalContext::new(SingleValue(5.), NothingMap)).is_err());
}
macro_rules! eval_tests {
($($name:ident: $expected:expr_2021 => $expr:expr_2021),* $(,)?) => {
$(

View File

@@ -1,14 +1,9 @@
use crate::ast::Literal;
use chumsky::input::{Input, ValueInput};
use chumsky::prelude::*;
use chumsky::span::SimpleSpan;
use chumsky::text::{ident, int};
use core::f64;
use num_complex::Complex64;
use std::fmt;
use std::iter::Peekable;
use std::ops::Range;
use std::str::Chars;
pub type Span = SimpleSpan;
@@ -40,6 +35,9 @@ pub enum Token<'src> {
EqEq,
If,
/// An unrecognized character; the parser never matches this, forcing a parse error rather than silently truncating the input.
Error,
}
impl<'src> fmt::Display for Token<'src> {
@@ -71,6 +69,8 @@ impl<'src> fmt::Display for Token<'src> {
Token::EqEq => f.write_str("=="),
Token::If => f.write_str("if"),
Token::Error => f.write_str("<error>"),
}
}
}
@@ -162,57 +162,64 @@ impl<'a> Lexer<'a> {
&self.input[start..self.pos]
}
fn lex_ident(&mut self) -> &'a str {
self.consume_while(|c| c.is_alphanumeric() || c == '_')
}
fn lex_uint(&mut self) -> Option<(u64, usize)> {
let mut v = 0u64;
fn consume_digits(&mut self) -> (usize, f64) {
let mut value = 0_f64;
let mut digits = 0;
while let Some(d) = self.peek().and_then(|c| c.to_digit(10)) {
v = v * 10 + d as u64;
value = value * 10. + d as f64;
digits += 1;
self.bump();
}
(digits > 0).then_some((v, digits))
(digits, value)
}
// A numeric literal cannot follow another operand across whitespace (`10 000`, `sqrt(4).5`), only constants/calls/parens may juxtapose
fn juxtaposes_with_preceding_operand(&self, literal_start: usize) -> bool {
let mut preceding = self.input[..literal_start].trim_end();
// A `!` run is postfix factorial only when an operand precedes it, otherwise it's a prefix logical not
while let Some(rest) = preceding.strip_suffix('!') {
preceding = rest.trim_end();
}
preceding.chars().next_back().is_some_and(|c| c.is_alphanumeric() || c == '.' || c == ')' || c == '∞')
}
fn lex_number(&mut self) -> Option<f64> {
let start_pos = self.pos;
let (int_val, int_digits) = self.lex_uint().unwrap_or((0, 0));
let (int_digits, int_value) = self.consume_digits();
let mut got_digit = int_digits > 0;
let mut num = int_val as f64;
let mut plain_integer = true;
if self.peek() == Some('.') {
self.bump();
if let Some((frac_val, frac_digits)) = self.lex_uint() {
num += (frac_val as f64) / 10f64.powi(frac_digits as i32);
got_digit = true;
}
plain_integer = false;
got_digit |= self.consume_digits().0 > 0;
}
if matches!(self.peek(), Some('e' | 'E')) {
if got_digit && matches!(self.peek(), Some('e' | 'E')) {
self.bump();
let sign = match self.peek() {
Some('+') => {
self.bump();
1
}
Some('-') => {
self.bump();
-1
}
_ => 1,
};
if let Some((exp_val, _)) = self.lex_uint() {
num *= 10f64.powi(sign * exp_val as i32);
} else {
plain_integer = false;
if matches!(self.peek(), Some('+' | '-')) {
self.bump();
}
if self.consume_digits().0 == 0 {
self.pos = start_pos;
return None;
}
}
got_digit.then_some(num)
// A numeric literal cannot be glued directly to another by a stray decimal point or digit (e.g. `1..5`, `1.5.5`), so reject rather than letting it parse as implicit multiplication
if !got_digit || self.peek().is_some_and(|c| c == '.' || c.is_ascii_digit()) || self.juxtaposes_with_preceding_operand(start_pos) {
self.pos = start_pos;
return None;
}
// Accumulation is exact up to 15 digits; longer or fractional literals get std's correctly-rounded parsing
if plain_integer && int_digits <= 15 {
return Some(int_value);
}
self.input[start_pos..self.pos].parse::<f64>().ok()
}
fn skip_ws(&mut self) {
@@ -231,7 +238,7 @@ impl<'a> Lexer<'a> {
self.bump();
AndAnd
} else {
return None;
Error
}
}
'|' => {
@@ -239,7 +246,7 @@ impl<'a> Lexer<'a> {
self.bump();
OrOr
} else {
return None;
Error
}
}
@@ -287,13 +294,29 @@ impl<'a> Lexer<'a> {
self.bump();
EqEq
} else {
return None;
Error
}
}
c if c.is_ascii_digit() || (c == '.' && self.peek().is_some_and(|c| c.is_ascii_digit())) => {
self.pos = start;
Float(self.lex_number()?)
match self.lex_number() {
Some(number) => Float(number),
// Consume the whole malformed numeric run so the error span covers it and lexing makes forward progress
None => {
self.pos = start;
let mut prev = '\0';
while let Some(c) = self.peek() {
let part_of_number = c.is_ascii_digit() || c == '.' || c == 'e' || c == 'E' || ((c == '+' || c == '-') && matches!(prev, 'e' | 'E'));
if !part_of_number {
break;
}
prev = c;
self.bump();
}
Error
}
}
}
_ => {
@@ -307,7 +330,7 @@ impl<'a> Lexer<'a> {
} else if ch.is_alphanumeric() {
Ident(ident)
} else {
return None;
Error
}
}
};

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,
}
}

View File

@@ -1,15 +1,9 @@
use crate::ast::{BinaryOp, Literal, Node, UnaryOp, Unit};
use crate::context::EvalContext;
use crate::ast::{BinaryOp, Literal, Node, UnaryOp};
use crate::lexer::{Lexer, Span, Token};
use crate::value::{Complex, Number, Value};
use chumsky::container::Seq;
use chumsky::input::{BorrowInput, ValueInput};
use chumsky::error::LabelError;
use chumsky::input::ValueInput;
use chumsky::{Parser, prelude::*};
use lazy_static::lazy_static;
use num_complex::ComplexFloat;
use std::fmt;
use std::num::{ParseFloatError, ParseIntError};
use thiserror::Error;
/// One message per parse failure, each tagged with its byte range in the source expression.
#[derive(Debug)]
@@ -31,18 +25,23 @@ impl std::error::Error for ParseError {}
impl Node {
pub fn try_parse_from_str(src: &str) -> Result<Node, ParseError> {
let tokens = Lexer::new(src);
// Parse with zero-cost errors first (several times faster), then re-parse invalid input with rich errors to build the messages
if let Ok(ast) = parser::<Lexer, extra::Default>().parse(Lexer::new(src)).into_result() {
return Ok(ast);
}
match parser().parse(tokens).into_result() {
match parser::<Lexer, extra::Err<Rich<Token, Span>>>().parse(Lexer::new(src)).into_result() {
Ok(ast) => Ok(ast),
Err(parse_errs) => Err(ParseError(parse_errs.into_iter().map(|e| format!("{e} at {}", e.span())).collect())),
}
}
}
pub fn parser<'src, I>() -> impl Parser<'src, I, Node, extra::Err<Rich<'src, Token<'src>, Span>>>
pub fn parser<'src, I, E>() -> impl Parser<'src, I, Node, E>
where
I: ValueInput<'src, Token = Token<'src>, Span = Span>,
E: extra::ParserExtra<'src, I>,
E::Error: LabelError<'src, I, &'static str>,
{
recursive(|expr| {
let constant = select! {
@@ -52,31 +51,27 @@ where
let args = expr.clone().separated_by(just(Token::Comma)).collect::<Vec<_>>().delimited_by(just(Token::LParen), just(Token::RParen));
let if_expr = just(Token::If)
.ignore_then(args.clone()) // Parses (cond, a, b)
.try_map(|args: Vec<Node>, span| {
if args.len() != 3 {
return Err(Rich::custom(span, "Expected 3 arguments in if(cond, a, b)"));
}
let mut iter = args.into_iter();
let cond = iter.next().unwrap();
let if_b = iter.next().unwrap();
let else_b = iter.next().unwrap();
Ok(Node::Conditional {
condition: Box::new(cond),
if_block: Box::new(if_b),
else_block: Box::new(else_b),
})
});
let if_expr = just(Token::If).ignore_then(args.clone()).try_map(|args: Vec<Node>, span| {
let [condition, if_block, else_block] = <[Node; 3]>::try_from(args).map_err(|_| LabelError::<I, _>::expected_found(["3 arguments in if(condition, a, b)"], None, span))?;
Ok(Node::Conditional {
condition: Box::new(condition),
if_block: Box::new(if_block),
else_block: Box::new(else_block),
})
});
let ident = select! {Token::Ident(s) => s}.labelled("ident");
let call = ident.then(args).map(|(name, args): (&str, Vec<Node>)| Node::FnCall { name: name.to_string(), expr: args });
// An ident followed by parenthesized args is a function call, otherwise a variable
let call_or_var = ident.then(args.or_not()).map(|(name, args): (&str, Option<Vec<Node>>)| match args {
Some(args) => Node::FnCall { name: name.to_string(), expr: args },
None => Node::Var(name.to_string()),
});
let parens = expr.clone().delimited_by(just(Token::LParen), just(Token::RParen));
let var = ident.map(|s| Node::Var(s.to_string()));
let atom = choice((constant, if_expr, call, parens, var)).labelled("atom").boxed();
let atom = choice((constant, if_expr, call_or_var, parens)).labelled("atom");
let add_op = choice((just(Token::Plus).to(BinaryOp::Add), just(Token::Minus).to(BinaryOp::Sub)));
let mul_op = choice((just(Token::Star).to(BinaryOp::Mul), just(Token::Slash).to(BinaryOp::Div), just(Token::Modulo).to(BinaryOp::Modulo)));
@@ -94,35 +89,34 @@ where
));
// Postfix factorial: expr! → UnaryOp::Fac
let postfix = atom
.clone()
.foldl(just(Token::Bang).repeated(), |expr, _| Node::UnaryOp {
op: UnaryOp::Fac,
expr: Box::new(expr),
let postfix = atom.clone().foldl(just(Token::Bang).repeated(), |expr, _| Node::UnaryOp {
op: UnaryOp::Fac,
expr: Box::new(expr),
});
// Exponentiation is right-associative (`2^2^3` is `2^(2^3)`) and the exponent may carry unary signs like `2^-3`
let pow = recursive(|pow| {
let exponent = unary_op.clone().repeated().foldr(pow, |op, expr| Node::UnaryOp { op, expr: Box::new(expr) });
postfix.clone().then(pow_op.ignore_then(exponent).or_not()).map(|(base, exponent)| match exponent {
Some(exponent) => Node::BinOp {
lhs: Box::new(base),
op: BinaryOp::Pow,
rhs: Box::new(exponent),
},
None => base,
})
.boxed();
});
let pow = postfix.clone().foldl(
pow_op
.then(unary_op.clone().repeated().foldr(postfix, |op, expr| Node::UnaryOp { op, expr: Box::new(expr) }).boxed())
.repeated(),
|lhs, (op, rhs)| Node::BinOp {
lhs: Box::new(lhs),
op,
rhs: Box::new(rhs),
},
);
let unary = unary_op.clone().repeated().foldr(pow.clone(), |op, expr| Node::UnaryOp { op, expr: Box::new(expr) });
let unary = unary_op.repeated().foldr(pow, |op, expr| Node::UnaryOp { op, expr: Box::new(expr) }).boxed();
let product = unary
.clone()
.foldl(mul_op.then(unary).repeated(), |lhs, (op, rhs)| Node::BinOp {
lhs: Box::new(lhs),
op,
rhs: Box::new(rhs),
})
.boxed();
// Juxtaposed factors like `2pi` or `2sqrt(4)` multiply implicitly at the same precedence as `*` and `/`.
// The implicit operand is a `pow`, not a full unary, so `2 -3` stays a subtraction; the lexer rejects a bare number as the right operand (`10 000` is not `10*000`).
let implicit_mul = pow.map(|rhs| (BinaryOp::Mul, rhs));
let product = unary.clone().foldl(choice((mul_op.then(unary), implicit_mul)).repeated(), |lhs, (op, rhs)| Node::BinOp {
lhs: Box::new(lhs),
op,
rhs: Box::new(rhs),
});
let add = product.clone().foldl(add_op.then(product).repeated(), |lhs, (op, rhs)| Node::BinOp {
lhs: Box::new(lhs),
@@ -136,15 +130,7 @@ where
rhs: Box::new(rhs),
});
// Chain comparisons like `a < b < c` by multiplying the boolean
// (1. / 0.) results, preserving the existing semantics.
let chained_cmp = cmp.clone().foldl(cmp.repeated(), |lhs, rhs| Node::BinOp {
lhs: Box::new(lhs),
op: BinaryOp::Mul,
rhs: Box::new(rhs),
});
let and = chained_cmp.clone().foldl(and_op.then(chained_cmp).repeated(), |lhs, (op, rhs)| Node::BinOp {
let and = cmp.clone().foldl(and_op.then(cmp).repeated(), |lhs, (op, rhs)| Node::BinOp {
lhs: Box::new(lhs),
op,
rhs: Box::new(rhs),
@@ -161,6 +147,8 @@ where
#[cfg(test)]
mod tests {
use super::*;
use crate::value::Complex;
macro_rules! test_parser {
($($name:ident: $input:expr_2021 => $expected:expr_2021),* $(,)?) => {
$(

View File

@@ -1,6 +1,4 @@
use crate::ast::{BinaryOp, UnaryOp};
use num_complex::ComplexFloat;
use std::f64::consts::PI;
pub type Complex = num_complex::Complex<f64>;
@@ -52,20 +50,38 @@ impl std::fmt::Display for Number {
}
impl Number {
/// The value's truthiness for conditions and logic operators, or `None` for NaN values, which poison the result rather than acting as a boolean.
pub fn as_bool(self) -> Option<bool> {
match self {
Number::Real(real) => (!real.is_nan()).then_some(real != 0.),
Number::Complex(complex) => (!complex.re.is_nan() && !complex.im.is_nan()).then_some(complex != Complex::ZERO),
}
}
pub fn binary_op(self, op: BinaryOp, other: Number) -> Option<Number> {
// Logic and equality work uniformly across real and complex operands
match op {
BinaryOp::And | BinaryOp::Or => {
let (Some(lhs), Some(rhs)) = (self.as_bool(), other.as_bool()) else {
return Some(Number::Real(f64::NAN));
};
let result = if matches!(op, BinaryOp::And) { lhs && rhs } else { lhs || rhs };
return Some(Number::Real(result as u8 as f64));
}
BinaryOp::Eq | BinaryOp::Neq => {
let equal = match (self, other) {
(Number::Real(lhs), Number::Real(rhs)) => lhs == rhs,
(Number::Complex(lhs), Number::Complex(rhs)) => lhs == rhs,
(Number::Real(real), Number::Complex(complex)) | (Number::Complex(complex), Number::Real(real)) => complex == Complex::new(real, 0.),
};
return Some(Number::Real((equal != matches!(op, BinaryOp::Neq)) as u8 as f64));
}
_ => {}
}
match (self, other) {
(Number::Real(lhs), Number::Real(rhs)) => {
let result = match op {
BinaryOp::And => {
let l = lhs != 0.;
let r = rhs != 0.;
if l && r { 1. } else { 0. }
}
BinaryOp::Or => {
let l = lhs != 0.;
let r = rhs != 0.;
if l || r { 1. } else { 0. }
}
BinaryOp::Add => lhs + rhs,
BinaryOp::Sub => lhs - rhs,
BinaryOp::Mul => lhs * rhs,
@@ -76,8 +92,7 @@ impl Number {
BinaryOp::Lt => (lhs < rhs) as u8 as f64,
BinaryOp::Geq => (lhs >= rhs) as u8 as f64,
BinaryOp::Gt => (lhs > rhs) as u8 as f64,
BinaryOp::Neq => (lhs != rhs) as u8 as f64,
BinaryOp::Eq => (lhs == rhs) as u8 as f64,
BinaryOp::And | BinaryOp::Or | BinaryOp::Eq | BinaryOp::Neq => unreachable!("handled above"),
};
Some(Number::Real(result))
@@ -85,16 +100,6 @@ impl Number {
(Number::Complex(lhs), Number::Complex(rhs)) => {
let result = match op {
BinaryOp::And => {
let l = lhs != Complex::new(0., 0.);
let r = rhs != Complex::new(0., 0.);
return Some(Number::Real(if l && r { 1. } else { 0. }));
}
BinaryOp::Or => {
let l = lhs != Complex::new(0., 0.);
let r = rhs != Complex::new(0., 0.);
return Some(Number::Real(if l || r { 1. } else { 0. }));
}
BinaryOp::Add => lhs + rhs,
BinaryOp::Sub => lhs - rhs,
BinaryOp::Mul => lhs * rhs,
@@ -104,20 +109,7 @@ impl Number {
BinaryOp::Leq | BinaryOp::Lt | BinaryOp::Geq | BinaryOp::Gt => {
return None;
}
BinaryOp::Neq => {
if lhs != rhs {
return Some(Number::Real(1.));
} else {
return Some(Number::Real(0.));
}
}
BinaryOp::Eq => {
if lhs == rhs {
return Some(Number::Real(1.));
} else {
return Some(Number::Real(0.));
}
}
BinaryOp::And | BinaryOp::Or | BinaryOp::Eq | BinaryOp::Neq => unreachable!("handled above"),
};
Some(Number::Complex(result))
}
@@ -151,6 +143,13 @@ impl Number {
}
pub fn unary_op(self, op: UnaryOp) -> Number {
if matches!(op, UnaryOp::Not) {
return match self.as_bool() {
Some(boolean) => Number::Real(!boolean as u8 as f64),
None => Number::Real(f64::NAN),
};
}
match self {
Number::Real(real) => match op {
UnaryOp::Neg => Number::Real(-real),
@@ -165,27 +164,26 @@ impl Number {
if truncated < 0. || (real - truncated).abs() > f64::EPSILON {
return Number::Real(f64::NAN);
}
// Return infinity above 170! since that overflows f64, which also keeps huge inputs from spinning the loop
let n = truncated as u64;
if n > 170 {
return Number::Real(f64::INFINITY);
}
let mut acc = 1_f64;
for k in 1..=n {
acc *= k as f64;
}
Number::Real(acc)
}
UnaryOp::Not => {
let is_zero = real == 0.;
Number::Real(if is_zero { 1. } else { 0. })
}
UnaryOp::Not => unreachable!("handled above"),
},
Number::Complex(complex) => match op {
UnaryOp::Neg => Number::Complex(-complex),
UnaryOp::Sqrt => Number::Complex(complex.sqrt()),
UnaryOp::Fac => Number::Complex(Complex::new(f64::NAN, f64::NAN)),
UnaryOp::Not => {
let is_zero = complex == Complex::new(0., 0.);
Number::Real(if is_zero { 1. } else { 0. })
}
UnaryOp::Not => unreachable!("handled above"),
},
}
}