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
Generated
-1
View File
@@ -3393,7 +3393,6 @@ version = "0.0.0"
dependencies = [ dependencies = [
"chumsky", "chumsky",
"criterion", "criterion",
"lazy_static",
"num-complex", "num-complex",
"thiserror 2.0.18", "thiserror 2.0.18",
] ]
-1
View File
@@ -9,7 +9,6 @@ license = "MIT OR Apache-2.0"
[dependencies] [dependencies]
thiserror = "2.0" thiserror = "2.0"
lazy_static = "1.5"
num-complex = "0.4" num-complex = "0.4"
chumsky = { version = "0.10", default-features = false, features = ["std"] } chumsky = { version = "0.10", default-features = false, features = ["std"] }
+355 -571
View File
@@ -1,637 +1,421 @@
use crate::value::{Number, Value}; use crate::value::{Number, Value};
use lazy_static::lazy_static; use num_complex::ComplexFloat;
use num_complex::{Complex, ComplexFloat};
use std::collections::HashMap;
use std::f64::consts::{LN_2, PI}; use std::f64::consts::{LN_2, PI};
type FunctionImplementation = Box<dyn Fn(&[Value]) -> Option<Value> + Send + Sync>; pub type BuiltinFunction = fn(&[Value]) -> Option<Value>;
lazy_static! {
pub static ref DEFAULT_FUNCTIONS: HashMap<&'static str, FunctionImplementation> = {
let mut map: HashMap<&'static str, FunctionImplementation> = HashMap::new();
map.insert( /// Truncates both operands to nonnegative integers for `gcd`/`lcm`, or `None` when either is non-finite or beyond f64's exactly-representable integer range.
"sqrt", fn integer_operands(a: f64, b: f64) -> Option<(u64, u64)> {
Box::new(|values| match values{ // The largest magnitude below which every integer is exactly representable in f64
[Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(real.sqrt()))), const EXACT_INTEGER_LIMIT: f64 = (1_u64 << f64::MANTISSA_DIGITS) as f64;
[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,
}),
);
map.insert( let (a, b) = (a.trunc(), b.trunc());
"cos", if !a.is_finite() || !b.is_finite() || a.abs() > EXACT_INTEGER_LIMIT || b.abs() > EXACT_INTEGER_LIMIT {
Box::new(|values| match values { return None;
[Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(real.cos()))), }
[Value::Number(Number::Complex(complex))] => Some(Value::Number(Number::Complex(complex.cos()))), Some(((a as i64).unsigned_abs(), (b as i64).unsigned_abs()))
_ => None, }
}),
);
map.insert( fn euclidean_gcd(mut x: u64, mut y: u64) -> u64 {
"tan", while y != 0 {
Box::new(|values| match values { (x, y) = (y, x % y);
[Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(real.tan()))), }
[Value::Number(Number::Complex(complex))] => Some(Value::Number(Number::Complex(complex.tan()))), x
_ => None, }
}),
);
map.insert( /// Looks up a built-in math function by name, returning a plain function pointer so dispatch avoids hashing and dynamic allocation.
"csc", pub fn builtin_function(name: &str) -> Option<BuiltinFunction> {
Box::new(|values| match values { Some(match name {
[Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(real.sin().recip()))), "sin" => |values| match values {
[Value::Number(Number::Complex(complex))] => Some(Value::Number(Number::Complex(complex.sin().recip()))), [Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(real.sin()))),
_ => None, [Value::Number(Number::Complex(complex))] => Some(Value::Number(Number::Complex(complex.sin()))),
}), _ => None,
); },
map.insert( "cos" => |values| match values {
"sec", [Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(real.cos()))),
Box::new(|values| match values { [Value::Number(Number::Complex(complex))] => Some(Value::Number(Number::Complex(complex.cos()))),
[Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(real.cos().recip()))), _ => None,
[Value::Number(Number::Complex(complex))] => Some(Value::Number(Number::Complex(complex.cos().recip()))), },
_ => None,
}),
);
map.insert( "tan" => |values| match values {
"cot", [Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(real.tan()))),
Box::new(|values| match values { [Value::Number(Number::Complex(complex))] => Some(Value::Number(Number::Complex(complex.tan()))),
[Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(real.tan().recip()))), _ => None,
[Value::Number(Number::Complex(complex))] => Some(Value::Number(Number::Complex(complex.tan().recip()))), },
_ => 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 // Inverse trig with legacy names and standard aliases
map.insert( "invsin" => |values| match values {
"invsin", [Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(real.asin()))),
Box::new(|values| match values { [Value::Number(Number::Complex(complex))] => Some(Value::Number(Number::Complex(complex.asin()))),
[Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(real.asin()))), _ => None,
[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()))),
map.insert( _ => None,
"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,
}),
);
map.insert( "invcos" => |values| match values {
"invcos", [Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(real.acos()))),
Box::new(|values| match values { [Value::Number(Number::Complex(complex))] => Some(Value::Number(Number::Complex(complex.acos()))),
[Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(real.acos()))), _ => None,
[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()))),
map.insert( _ => None,
"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,
}),
);
map.insert( "invtan" => |values| match values {
"invtan", [Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(real.atan()))),
Box::new(|values| match values { [Value::Number(Number::Complex(complex))] => Some(Value::Number(Number::Complex(complex.atan()))),
[Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(real.atan()))), _ => None,
[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()))),
map.insert( _ => None,
"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,
}),
);
map.insert( "invcsc" => |values| match values {
"invcsc", [Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(real.recip().asin()))),
Box::new(|values| match values { [Value::Number(Number::Complex(complex))] => Some(Value::Number(Number::Complex(complex.recip().asin()))),
[Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(real.recip().asin()))), _ => None,
[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()))),
map.insert( _ => None,
"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,
}),
);
map.insert( "invsec" => |values| match values {
"invsec", [Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(real.recip().acos()))),
Box::new(|values| match values { [Value::Number(Number::Complex(complex))] => Some(Value::Number(Number::Complex(complex.recip().acos()))),
[Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(real.recip().acos()))), _ => None,
[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()))),
map.insert( _ => None,
"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,
}),
);
map.insert( "invcot" => |values| match values {
"invcot", [Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(real.recip().atan()))),
Box::new(|values| match values { [Value::Number(Number::Complex(complex))] => Some(Value::Number(Number::Complex(complex.recip().atan()))),
[Value::Number(Number::Real(real))] => { _ => None,
Some(Value::Number(Number::Real(real.recip().atan()))) },
} "acot" => |values| match values {
[Value::Number(Number::Complex(complex))] => { [Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(real.recip().atan()))),
Some(Value::Number(Number::Complex(complex.recip().atan()))) [Value::Number(Number::Complex(complex))] => Some(Value::Number(Number::Complex(complex.recip().atan()))),
} _ => None,
_ => 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,
}),
);
// Hyperbolic Functions // Hyperbolic Functions
map.insert( "sinh" => |values| match values {
"sinh", [Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(real.sinh()))),
Box::new(|values| match values { [Value::Number(Number::Complex(complex))] => Some(Value::Number(Number::Complex(complex.sinh()))),
[Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(real.sinh()))), _ => None,
[Value::Number(Number::Complex(complex))] => Some(Value::Number(Number::Complex(complex.sinh()))), },
_ => None,
}),
);
map.insert( "cosh" => |values| match values {
"cosh", [Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(real.cosh()))),
Box::new(|values| match values { [Value::Number(Number::Complex(complex))] => Some(Value::Number(Number::Complex(complex.cosh()))),
[Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(real.cosh()))), _ => None,
[Value::Number(Number::Complex(complex))] => Some(Value::Number(Number::Complex(complex.cosh()))), },
_ => None,
}),
);
map.insert( "tanh" => |values| match values {
"tanh", [Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(real.tanh()))),
Box::new(|values| match values { [Value::Number(Number::Complex(complex))] => Some(Value::Number(Number::Complex(complex.tanh()))),
[Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(real.tanh()))), _ => None,
[Value::Number(Number::Complex(complex))] => Some(Value::Number(Number::Complex(complex.tanh()))), },
_ => None,
}),
);
// Reciprocal hyperbolic functions // Reciprocal hyperbolic functions
map.insert( "csch" => |values| match values {
"csch", [Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(real.sinh().recip()))),
Box::new(|values| match values { [Value::Number(Number::Complex(complex))] => Some(Value::Number(Number::Complex(complex.sinh().recip()))),
[Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(real.sinh().recip()))), _ => None,
[Value::Number(Number::Complex(complex))] => Some(Value::Number(Number::Complex(complex.sinh().recip()))), },
_ => None,
}),
);
map.insert( "sech" => |values| match values {
"sech", [Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(real.cosh().recip()))),
Box::new(|values| match values { [Value::Number(Number::Complex(complex))] => Some(Value::Number(Number::Complex(complex.cosh().recip()))),
[Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(real.cosh().recip()))), _ => None,
[Value::Number(Number::Complex(complex))] => Some(Value::Number(Number::Complex(complex.cosh().recip()))), },
_ => None,
}),
);
map.insert( "coth" => |values| match values {
"coth", [Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(real.tanh().recip()))),
Box::new(|values| match values { [Value::Number(Number::Complex(complex))] => Some(Value::Number(Number::Complex(complex.tanh().recip()))),
[Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(real.tanh().recip()))), _ => None,
[Value::Number(Number::Complex(complex))] => Some(Value::Number(Number::Complex(complex.tanh().recip()))), },
_ => None,
}),
);
// Inverse Hyperbolic Functions // Inverse Hyperbolic Functions
map.insert( "asinh" => |values| match values {
"asinh", [Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(real.asinh()))),
Box::new(|values| match values { [Value::Number(Number::Complex(complex))] => Some(Value::Number(Number::Complex(complex.asinh()))),
[Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(real.asinh()))), _ => None,
[Value::Number(Number::Complex(complex))] => Some(Value::Number(Number::Complex(complex.asinh()))), },
_ => None,
}),
);
map.insert( "acosh" => |values| match values {
"acosh", [Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(real.acosh()))),
Box::new(|values| match values { [Value::Number(Number::Complex(complex))] => Some(Value::Number(Number::Complex(complex.acosh()))),
[Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(real.acosh()))), _ => None,
[Value::Number(Number::Complex(complex))] => Some(Value::Number(Number::Complex(complex.acosh()))), },
_ => None,
}),
);
map.insert( "atanh" => |values| match values {
"atanh", [Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(real.atanh()))),
Box::new(|values| match values { [Value::Number(Number::Complex(complex))] => Some(Value::Number(Number::Complex(complex.atanh()))),
[Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(real.atanh()))), _ => None,
[Value::Number(Number::Complex(complex))] => Some(Value::Number(Number::Complex(complex.atanh()))), },
_ => None,
}),
);
// Inverse reciprocal hyperbolic functions // Inverse reciprocal hyperbolic functions
map.insert( "acsch" => |values| match values {
"acsch", [Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(real.recip().asinh()))),
Box::new(|values| match values { [Value::Number(Number::Complex(complex))] => Some(Value::Number(Number::Complex(complex.recip().asinh()))),
[Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(real.recip().asinh()))), _ => None,
[Value::Number(Number::Complex(complex))] => Some(Value::Number(Number::Complex(complex.recip().asinh()))), },
_ => None,
}),
);
map.insert( "asech" => |values| match values {
"asech", [Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(real.recip().acosh()))),
Box::new(|values| match values { [Value::Number(Number::Complex(complex))] => Some(Value::Number(Number::Complex(complex.recip().acosh()))),
[Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(real.recip().acosh()))), _ => None,
[Value::Number(Number::Complex(complex))] => Some(Value::Number(Number::Complex(complex.recip().acosh()))), },
_ => None,
}),
);
map.insert( "acoth" => |values| match values {
"acoth", [Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(real.recip().atanh()))),
Box::new(|values| match values { [Value::Number(Number::Complex(complex))] => Some(Value::Number(Number::Complex(complex.recip().atanh()))),
[Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(real.recip().atanh()))), _ => None,
[Value::Number(Number::Complex(complex))] => Some(Value::Number(Number::Complex(complex.recip().atanh()))), },
_ => None,
}),
);
// Logarithm Functions // Logarithm Functions
map.insert( "ln" => |values| match values {
"ln", [Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(real.ln()))),
Box::new(|values| match values { [Value::Number(Number::Complex(complex))] => Some(Value::Number(Number::Complex(complex.ln()))),
[Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(real.ln()))), _ => None,
[Value::Number(Number::Complex(complex))] => Some(Value::Number(Number::Complex(complex.ln()))), },
_ => None,
}),
);
// Exponential / power helpers // Exponential / power helpers
map.insert( "exp" => |values| match values {
"exp", [Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(real.exp()))),
Box::new(|values| match values { [Value::Number(Number::Complex(complex))] => Some(Value::Number(Number::Complex(complex.exp()))),
[Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(real.exp()))), _ => None,
[Value::Number(Number::Complex(complex))] => Some(Value::Number(Number::Complex(complex.exp()))), },
_ => None,
}),
);
map.insert( "pow" => |values| match values {
"pow", [Value::Number(Number::Real(x)), Value::Number(Number::Real(n))] => Some(Value::Number(Number::Real(x.powf(*n)))),
Box::new(|values| match values { [Value::Number(Number::Complex(x)), Value::Number(Number::Real(n))] => Some(Value::Number(Number::Complex(x.powf(*n)))),
[Value::Number(Number::Real(x)), Value::Number(Number::Real(n))] => { [Value::Number(Number::Complex(x)), Value::Number(Number::Complex(n))] => Some(Value::Number(Number::Complex(x.powc(*n)))),
Some(Value::Number(Number::Real(x.powf(*n)))) _ => None,
} },
[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" => |values| match values {
"root", [Value::Number(Number::Real(x)), Value::Number(Number::Real(n))] => {
Box::new(|values| match values { // Odd integer roots of negative reals are real, which powf alone would report as NaN
[Value::Number(Number::Real(x)), Value::Number(Number::Real(n))] => { let root = if *x < 0. && n.rem_euclid(2.) == 1. { -(-x).powf(1. / *n) } else { x.powf(1. / *n) };
Some(Value::Number(Number::Real(x.powf(1. / *n)))) Some(Value::Number(Number::Real(root)))
} }
[Value::Number(Number::Complex(x)), Value::Number(Number::Real(n))] => { [Value::Number(Number::Complex(x)), Value::Number(Number::Real(n))] => Some(Value::Number(Number::Complex(x.powf(1. / *n)))),
Some(Value::Number(Number::Complex(x.powf(1. / *n)))) _ => None,
} },
_ => None,
}),
);
map.insert( "log" => |values| match values {
"log", [Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(real.log10()))),
Box::new(|values| match values { [Value::Number(Number::Complex(complex))] => Some(Value::Number(Number::Complex(complex.log10()))),
[Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(real.log10()))), [Value::Number(n), Value::Number(base)] => {
[Value::Number(Number::Complex(complex))] => Some(Value::Number(Number::Complex(complex.log10()))), // Custom base logarithm using change of base formula
[Value::Number(n), Value::Number(base)] => { let compute_log = |x: f64, b: f64| -> f64 { x.ln() / b.ln() };
// Custom base logarithm using change of base formula match (n, base) {
let compute_log = |x: f64, b: f64| -> f64 { x.ln() / b.ln() }; (Number::Real(x), Number::Real(b)) => Some(Value::Number(Number::Real(compute_log(*x, *b)))),
match (n, base) { _ => None,
(Number::Real(x), Number::Real(b)) => Some(Value::Number(Number::Real(compute_log(*x, *b)))),
_ => None,
}
} }
_ => None, }
}), _ => None,
); },
map.insert( "log2" => |values| match values {
"log2", [Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(real.log2()))),
Box::new(|values| match values { [Value::Number(Number::Complex(complex))] => Some(Value::Number(Number::Complex(complex.ln() / LN_2))),
[Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(real.log2()))), _ => None,
[Value::Number(Number::Complex(complex))] => Some(Value::Number(Number::Complex(complex / LN_2))), },
_ => None,
}),
);
// Root Functions // Root Functions
map.insert( "sqrt" => |values| match values {
"sqrt", [Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(real.sqrt()))),
Box::new(|values| match values { [Value::Number(Number::Complex(complex))] => Some(Value::Number(Number::Complex(complex.sqrt()))),
[Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(real.sqrt()))), _ => None,
[Value::Number(Number::Complex(complex))] => Some(Value::Number(Number::Complex(complex.sqrt()))), },
_ => None,
}),
);
map.insert( "cbrt" => |values| match values {
"cbrt", [Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(real.cbrt()))),
Box::new(|values| match values { [Value::Number(Number::Complex(complex))] => Some(Value::Number(Number::Complex(complex.powf(1. / 3.)))),
[Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(real.cbrt()))), _ => None,
[Value::Number(Number::Complex(complex))] => Some(Value::Number(Number::Complex(complex.powf(1./3.)))), },
_ => None,
}),
);
// Geometry Functions // Geometry Functions
map.insert( "hypot" => |values| match values {
"hypot", [Value::Number(Number::Real(a)), Value::Number(Number::Real(b))] => Some(Value::Number(Number::Real(a.hypot(*b)))),
Box::new(|values| match values { _ => None,
[Value::Number(Number::Real(a)), Value::Number(Number::Real(b))] => { },
Some(Value::Number(Number::Real(a.hypot(*b))))
},
_ => None,
}),
);
map.insert( "atan2" => |values| match values {
"atan2", [Value::Number(Number::Real(y)), Value::Number(Number::Real(x))] => Some(Value::Number(Number::Real(y.atan2(*x)))),
Box::new(|values| match values { _ => None,
[Value::Number(Number::Real(y)), Value::Number(Number::Real(x))] => { },
Some(Value::Number(Number::Real(y.atan2(*x))))
}
_ => None,
}),
);
// Mapping Functions // Mapping Functions
map.insert( "abs" => |values| match values {
"abs", [Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(real.abs()))),
Box::new(|values| match values { [Value::Number(Number::Complex(complex))] => Some(Value::Number(Number::Real(complex.abs()))),
[Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(real.abs()))), _ => None,
[Value::Number(Number::Complex(complex))] => Some(Value::Number(Number::Real(complex.abs()))), },
_ => None,
}),
);
map.insert( "floor" => |values| match values {
"floor", [Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(real.floor()))),
Box::new(|values| match values { _ => None,
[Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(real.floor()))), },
_ => None,
}),
);
map.insert( "ceil" => |values| match values {
"ceil", [Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(real.ceil()))),
Box::new(|values| match values { _ => None,
[Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(real.ceil()))), },
_ => None,
}),
);
map.insert( "round" => |values| match values {
"round", [Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(real.round()))),
Box::new(|values| match values { _ => None,
[Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(real.round()))), },
_ => None,
}),
);
map.insert( "clamp" => |values| match values {
"clamp", [Value::Number(Number::Real(x)), Value::Number(Number::Real(min)), Value::Number(Number::Real(max))] => Some(Value::Number(Number::Real(x.clamp(*min, *max)))),
Box::new(|values| match values { _ => None,
[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" => |values| match values {
"lerp", [Value::Number(Number::Real(a)), Value::Number(Number::Real(b)), Value::Number(Number::Real(t))] => Some(Value::Number(Number::Real(a + (b - a) * t))),
Box::new(|values| match values { _ => None,
[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" => |values| match values {
"remap", [
Box::new(|values| match values { Value::Number(Number::Real(value)),
[ Value::Number(Number::Real(in_a)),
Value::Number(Number::Real(value)), Value::Number(Number::Real(in_b)),
Value::Number(Number::Real(in_a)), Value::Number(Number::Real(out_a)),
Value::Number(Number::Real(in_b)), Value::Number(Number::Real(out_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))))
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( // Multiply in f64 so huge results can't overflow the integer range
"trunc", let lcm = (x / euclidean_gcd(x, y)) as f64 * y as f64;
Box::new(|values| match values { Some(Value::Number(Number::Real(lcm)))
[Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(real.trunc()))), }
_ => None, _ => 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,
}),
);
// Complex Number Functions // Complex Number Functions
map.insert( "real" => |values| match values {
"real", [Value::Number(Number::Complex(complex))] => Some(Value::Number(Number::Real(complex.re))),
Box::new(|values| match values { [Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(*real))),
[Value::Number(Number::Complex(complex))] => Some(Value::Number(Number::Real(complex.re))), _ => None,
[Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(*real))), },
_ => None,
}),
);
map.insert( "imag" => |values| match values {
"imag", [Value::Number(Number::Complex(complex))] => Some(Value::Number(Number::Real(complex.im))),
Box::new(|values| match values { [Value::Number(Number::Real(_))] => Some(Value::Number(Number::Real(0.))),
[Value::Number(Number::Complex(complex))] => Some(Value::Number(Number::Real(complex.im))), _ => None,
[Value::Number(Number::Real(_))] => Some(Value::Number(Number::Real(0.))), },
_ => None,
}),
);
map.insert( "conj" => |values| match values {
"conj", [Value::Number(Number::Complex(complex))] => Some(Value::Number(Number::Complex(complex.conj()))),
Box::new(|values| match values { [Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(*real))),
[Value::Number(Number::Complex(complex))] => { _ => None,
Some(Value::Number(Number::Complex(complex.conj()))) },
}
[Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(*real))),
_ => None,
}),
);
map.insert( "arg" => |values| match values {
"arg", [Value::Number(Number::Complex(complex))] => Some(Value::Number(Number::Real(complex.arg()))),
Box::new(|values| match values { [Value::Number(Number::Real(real))] => {
[Value::Number(Number::Complex(complex))] => { let angle = if *real >= 0. { 0. } else { PI };
Some(Value::Number(Number::Real(complex.arg()))) Some(Value::Number(Number::Real(angle)))
} }
[Value::Number(Number::Real(real))] => { _ => None,
let angle = if *real >= 0. { 0. } else { PI }; },
Some(Value::Number(Number::Real(angle)))
}
_ => None,
}),
);
// Logical Functions // Logical Functions
map.insert( "isnan" => |values| match values {
"isnan", [Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(if real.is_nan() { 1. } else { 0. }))),
Box::new(|values| match values { _ => None,
[Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(if real.is_nan() { 1. } else { 0. }))), },
_ => None,
}),
);
map.insert( "eq" => |values| match values {
"eq", [Value::Number(a), Value::Number(b)] => Some(Value::Number(Number::Real(if a == b { 1. } else { 0. }))),
Box::new(|values| match values { _ => None,
[Value::Number(a), Value::Number(b)] => Some(Value::Number(Number::Real(if a == b { 1. } else { 0. }))), },
_ => None,
}),
);
map.insert( "greater" => |values| match values {
"greater", [Value::Number(Number::Real(a)), Value::Number(Number::Real(b))] => Some(Value::Number(Number::Real(if a > b { 1. } else { 0. }))),
Box::new(|values| match values { _ => None,
[Value::Number(Number::Real(a)), Value::Number(Number::Real(b))] => { },
Some(Value::Number(Number::Real(if a > b { 1. } else { 0. }))) _ => return None,
}, })
_ => None,
}),
);
map
};
} }
+63 -17
View File
@@ -1,8 +1,7 @@
use crate::ast::{Literal, Node}; use crate::ast::{BinaryOp, Literal, Node};
use crate::constants::DEFAULT_FUNCTIONS; use crate::constants::builtin_function;
use crate::context::{EvalContext, FunctionProvider, ValueProvider}; use crate::context::{EvalContext, FunctionProvider, ValueProvider};
use crate::value::{Number, Value}; use crate::value::{Number, Value};
use num_complex::Complex;
use thiserror::Error; use thiserror::Error;
#[derive(Debug, Error)] #[derive(Debug, Error)]
@@ -12,8 +11,12 @@ pub enum EvalError {
#[error("Missing function: {0}")] #[error("Missing function: {0}")]
MissingFunction(String), MissingFunction(String),
#[error("Wrong type for function call")]
#[error("Wrong argument types for function call")]
TypeError, TypeError,
#[error("Unsupported operand types for operator")]
OperatorTypeError,
} }
impl Node { impl Node {
@@ -25,28 +28,43 @@ impl Node {
}, },
Node::BinOp { lhs, op, rhs } => match (lhs.eval(context)?, rhs.eval(context)?) { 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)? { Node::UnaryOp { expr, op } => match expr.eval(context)? {
Value::Number(num) => Ok(Value::Number(num.unary_op(*op))), 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::Var(name) => context.get_value(name).ok_or_else(|| EvalError::MissingValue(name.clone())),
Node::FnCall { name, expr } => { Node::FnCall { name, expr } => {
let values = expr.iter().map(|expr| expr.eval(context)).collect::<Result<Vec<Value>, EvalError>>()?; // 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];
if let Some(function) = DEFAULT_FUNCTIONS.get(&name.as_str()) { let heap_values: Vec<Value>;
function(&values).ok_or(EvalError::TypeError) let values: &[Value] = if expr.len() <= stack_values.len() {
} else if let Some(val) = context.run_function(name, &values) { for (slot, argument) in stack_values.iter_mut().zip(expr) {
Ok(val) *slot = argument.eval(context)?;
}
&stack_values[..expr.len()]
} else { } 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 } => { Node::Conditional { condition, if_block, else_block } => {
let condition = match condition.eval(context)? { // A NaN condition yields NaN rather than arbitrarily picking a branch
Value::Number(Number::Real(number)) => number != 0., let Value::Number(number) = condition.eval(context)?;
Value::Number(Number::Complex(number)) => number != Complex::ZERO, 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) } if condition { if_block.eval(context) } else { else_block.eval(context) }
} }
@@ -57,9 +75,37 @@ impl Node {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use crate::ast::{BinaryOp, Literal, Node, UnaryOp}; use crate::ast::{BinaryOp, Literal, Node, UnaryOp};
use crate::context::{EvalContext, ValueMap}; use crate::context::{EvalContext, NothingMap, ValueProvider};
use crate::value::Value; 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 { macro_rules! eval_tests {
($($name:ident: $expected:expr_2021 => $expr:expr_2021),* $(,)?) => { ($($name:ident: $expected:expr_2021 => $expr:expr_2021),* $(,)?) => {
$( $(
+63 -40
View File
@@ -1,14 +1,9 @@
use crate::ast::Literal; use crate::ast::Literal;
use chumsky::input::{Input, ValueInput}; use chumsky::input::{Input, ValueInput};
use chumsky::prelude::*;
use chumsky::span::SimpleSpan; use chumsky::span::SimpleSpan;
use chumsky::text::{ident, int};
use core::f64;
use num_complex::Complex64; use num_complex::Complex64;
use std::fmt; use std::fmt;
use std::iter::Peekable;
use std::ops::Range; use std::ops::Range;
use std::str::Chars;
pub type Span = SimpleSpan; pub type Span = SimpleSpan;
@@ -40,6 +35,9 @@ pub enum Token<'src> {
EqEq, EqEq,
If, 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> { impl<'src> fmt::Display for Token<'src> {
@@ -71,6 +69,8 @@ impl<'src> fmt::Display for Token<'src> {
Token::EqEq => f.write_str("=="), Token::EqEq => f.write_str("=="),
Token::If => f.write_str("if"), 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] &self.input[start..self.pos]
} }
fn lex_ident(&mut self) -> &'a str { fn consume_digits(&mut self) -> (usize, f64) {
self.consume_while(|c| c.is_alphanumeric() || c == '_') let mut value = 0_f64;
}
fn lex_uint(&mut self) -> Option<(u64, usize)> {
let mut v = 0u64;
let mut digits = 0; let mut digits = 0;
while let Some(d) = self.peek().and_then(|c| c.to_digit(10)) { 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; digits += 1;
self.bump(); 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> { fn lex_number(&mut self) -> Option<f64> {
let start_pos = self.pos; 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 got_digit = int_digits > 0;
let mut num = int_val as f64; let mut plain_integer = true;
if self.peek() == Some('.') { if self.peek() == Some('.') {
self.bump(); self.bump();
if let Some((frac_val, frac_digits)) = self.lex_uint() { plain_integer = false;
num += (frac_val as f64) / 10f64.powi(frac_digits as i32); got_digit |= self.consume_digits().0 > 0;
got_digit = true;
}
} }
if matches!(self.peek(), Some('e' | 'E')) { if got_digit && matches!(self.peek(), Some('e' | 'E')) {
self.bump(); self.bump();
let sign = match self.peek() { plain_integer = false;
Some('+') => { if matches!(self.peek(), Some('+' | '-')) {
self.bump(); self.bump();
1 }
} if self.consume_digits().0 == 0 {
Some('-') => {
self.bump();
-1
}
_ => 1,
};
if let Some((exp_val, _)) = self.lex_uint() {
num *= 10f64.powi(sign * exp_val as i32);
} else {
self.pos = start_pos; self.pos = start_pos;
return None; 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) { fn skip_ws(&mut self) {
@@ -231,7 +238,7 @@ impl<'a> Lexer<'a> {
self.bump(); self.bump();
AndAnd AndAnd
} else { } else {
return None; Error
} }
} }
'|' => { '|' => {
@@ -239,7 +246,7 @@ impl<'a> Lexer<'a> {
self.bump(); self.bump();
OrOr OrOr
} else { } else {
return None; Error
} }
} }
@@ -287,13 +294,29 @@ impl<'a> Lexer<'a> {
self.bump(); self.bump();
EqEq EqEq
} else { } else {
return None; Error
} }
} }
c if c.is_ascii_digit() || (c == '.' && self.peek().is_some_and(|c| c.is_ascii_digit())) => { c if c.is_ascii_digit() || (c == '.' && self.peek().is_some_and(|c| c.is_ascii_digit())) => {
self.pos = start; 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() { } else if ch.is_alphanumeric() {
Ident(ident) Ident(ident)
} else { } else {
return None; Error
} }
} }
}; };
+76 -8
View File
@@ -1,5 +1,3 @@
#![allow(unused)]
pub mod ast; pub mod ast;
mod constants; mod constants;
pub mod context; pub mod context;
@@ -8,8 +6,7 @@ pub mod lexer;
pub mod parser; pub mod parser;
pub mod value; pub mod value;
use ast::Unit; use context::EvalContext;
use context::{EvalContext, ValueMap};
use executer::EvalError; use executer::EvalError;
use parser::ParseError; use parser::ParseError;
use value::Value; use value::Value;
@@ -23,25 +20,53 @@ pub fn evaluate(expression: &str) -> Result<Result<Value, EvalError>, ParseError
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use ast::Unit;
use value::Number; use value::Number;
const EPSILON: f64 = 1e-10_f64; 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) { fn run_end_to_end_test(input: &str, expected_value: Value) {
let expr = match ast::Node::try_parse_from_str(input) { let expr = match ast::Node::try_parse_from_str(input) {
Ok(expr) => expr, Ok(expr) => expr,
Err(err) => panic!("failed to parse `{input}`: {err}"), Err(err) => panic!("failed to parse `{input}`: {err}"),
}; };
dbg!(&expr);
let context = EvalContext::default(); let context = EvalContext::default();
let actual_value = match expr.eval(&context) { let actual_value = match expr.eval(&context) {
Ok(v) => v, 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) { match (actual_value, expected_value) {
(Value::Number(Number::Complex(a)), Value::Number(Number::Complex(e))) => { (Value::Number(Number::Complex(a)), Value::Number(Number::Complex(e))) => {
// real part // real part
@@ -128,6 +153,17 @@ mod tests {
exponent_single: "2^3" => 8., exponent_single: "2^3" => 8.,
exponent_mixed_operations: "2^3 + 4^2" => 24., exponent_mixed_operations: "2^3 + 4^2" => 24.,
exponent_nested: "2^(3+1)" => 16., 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 (postfix !)
factorial_simple: "5!" => 120., factorial_simple: "5!" => 120.,
@@ -275,5 +311,37 @@ mod tests {
// Complex nested expressions // Complex nested expressions
if_nested_expr: "if((sqrt(16) + 2) * (sin(pi) + 1), 3 + 4 * 2, 5 - 2 / 1)" => 11., 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,
} }
} }
+54 -66
View File
@@ -1,15 +1,9 @@
use crate::ast::{BinaryOp, Literal, Node, UnaryOp, Unit}; use crate::ast::{BinaryOp, Literal, Node, UnaryOp};
use crate::context::EvalContext;
use crate::lexer::{Lexer, Span, Token}; use crate::lexer::{Lexer, Span, Token};
use crate::value::{Complex, Number, Value}; use chumsky::error::LabelError;
use chumsky::container::Seq; use chumsky::input::ValueInput;
use chumsky::input::{BorrowInput, ValueInput};
use chumsky::{Parser, prelude::*}; use chumsky::{Parser, prelude::*};
use lazy_static::lazy_static;
use num_complex::ComplexFloat;
use std::fmt; 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. /// One message per parse failure, each tagged with its byte range in the source expression.
#[derive(Debug)] #[derive(Debug)]
@@ -31,18 +25,23 @@ impl std::error::Error for ParseError {}
impl Node { impl Node {
pub fn try_parse_from_str(src: &str) -> Result<Node, ParseError> { 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), Ok(ast) => Ok(ast),
Err(parse_errs) => Err(ParseError(parse_errs.into_iter().map(|e| format!("{e} at {}", e.span())).collect())), 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 where
I: ValueInput<'src, Token = Token<'src>, Span = Span>, I: ValueInput<'src, Token = Token<'src>, Span = Span>,
E: extra::ParserExtra<'src, I>,
E::Error: LabelError<'src, I, &'static str>,
{ {
recursive(|expr| { recursive(|expr| {
let constant = select! { 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 args = expr.clone().separated_by(just(Token::Comma)).collect::<Vec<_>>().delimited_by(just(Token::LParen), just(Token::RParen));
let if_expr = just(Token::If) let if_expr = just(Token::If).ignore_then(args.clone()).try_map(|args: Vec<Node>, span| {
.ignore_then(args.clone()) // Parses (cond, a, b) 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))?;
.try_map(|args: Vec<Node>, span| {
if args.len() != 3 { Ok(Node::Conditional {
return Err(Rich::custom(span, "Expected 3 arguments in if(cond, a, b)")); condition: Box::new(condition),
} if_block: Box::new(if_block),
let mut iter = args.into_iter(); else_block: Box::new(else_block),
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 ident = select! {Token::Ident(s) => s}.labelled("ident"); 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 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 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))); 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 // Postfix factorial: expr! → UnaryOp::Fac
let postfix = atom let postfix = atom.clone().foldl(just(Token::Bang).repeated(), |expr, _| Node::UnaryOp {
.clone() op: UnaryOp::Fac,
.foldl(just(Token::Bang).repeated(), |expr, _| Node::UnaryOp { expr: Box::new(expr),
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( let unary = unary_op.clone().repeated().foldr(pow.clone(), |op, expr| Node::UnaryOp { op, expr: Box::new(expr) });
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.repeated().foldr(pow, |op, expr| Node::UnaryOp { op, expr: Box::new(expr) }).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 product = unary let implicit_mul = pow.map(|rhs| (BinaryOp::Mul, rhs));
.clone() let product = unary.clone().foldl(choice((mul_op.then(unary), implicit_mul)).repeated(), |lhs, (op, rhs)| Node::BinOp {
.foldl(mul_op.then(unary).repeated(), |lhs, (op, rhs)| Node::BinOp { lhs: Box::new(lhs),
lhs: Box::new(lhs), op,
op, rhs: Box::new(rhs),
rhs: Box::new(rhs), });
})
.boxed();
let add = product.clone().foldl(add_op.then(product).repeated(), |lhs, (op, rhs)| Node::BinOp { let add = product.clone().foldl(add_op.then(product).repeated(), |lhs, (op, rhs)| Node::BinOp {
lhs: Box::new(lhs), lhs: Box::new(lhs),
@@ -136,15 +130,7 @@ where
rhs: Box::new(rhs), rhs: Box::new(rhs),
}); });
// Chain comparisons like `a < b < c` by multiplying the boolean let and = cmp.clone().foldl(and_op.then(cmp).repeated(), |lhs, (op, rhs)| Node::BinOp {
// (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 {
lhs: Box::new(lhs), lhs: Box::new(lhs),
op, op,
rhs: Box::new(rhs), rhs: Box::new(rhs),
@@ -161,6 +147,8 @@ where
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use crate::value::Complex;
macro_rules! test_parser { macro_rules! test_parser {
($($name:ident: $input:expr_2021 => $expected:expr_2021),* $(,)?) => { ($($name:ident: $input:expr_2021 => $expected:expr_2021),* $(,)?) => {
$( $(
+44 -46
View File
@@ -1,6 +1,4 @@
use crate::ast::{BinaryOp, UnaryOp}; use crate::ast::{BinaryOp, UnaryOp};
use num_complex::ComplexFloat;
use std::f64::consts::PI;
pub type Complex = num_complex::Complex<f64>; pub type Complex = num_complex::Complex<f64>;
@@ -52,20 +50,38 @@ impl std::fmt::Display for Number {
} }
impl 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> { 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) { match (self, other) {
(Number::Real(lhs), Number::Real(rhs)) => { (Number::Real(lhs), Number::Real(rhs)) => {
let result = match op { 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::Add => lhs + rhs,
BinaryOp::Sub => lhs - rhs, BinaryOp::Sub => lhs - rhs,
BinaryOp::Mul => lhs * rhs, BinaryOp::Mul => lhs * rhs,
@@ -76,8 +92,7 @@ impl Number {
BinaryOp::Lt => (lhs < rhs) as u8 as f64, BinaryOp::Lt => (lhs < rhs) as u8 as f64,
BinaryOp::Geq => (lhs >= rhs) as u8 as f64, BinaryOp::Geq => (lhs >= rhs) as u8 as f64,
BinaryOp::Gt => (lhs > rhs) as u8 as f64, BinaryOp::Gt => (lhs > rhs) as u8 as f64,
BinaryOp::Neq => (lhs != rhs) as u8 as f64, BinaryOp::And | BinaryOp::Or | BinaryOp::Eq | BinaryOp::Neq => unreachable!("handled above"),
BinaryOp::Eq => (lhs == rhs) as u8 as f64,
}; };
Some(Number::Real(result)) Some(Number::Real(result))
@@ -85,16 +100,6 @@ impl Number {
(Number::Complex(lhs), Number::Complex(rhs)) => { (Number::Complex(lhs), Number::Complex(rhs)) => {
let result = match op { 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::Add => lhs + rhs,
BinaryOp::Sub => lhs - rhs, BinaryOp::Sub => lhs - rhs,
BinaryOp::Mul => lhs * rhs, BinaryOp::Mul => lhs * rhs,
@@ -104,20 +109,7 @@ impl Number {
BinaryOp::Leq | BinaryOp::Lt | BinaryOp::Geq | BinaryOp::Gt => { BinaryOp::Leq | BinaryOp::Lt | BinaryOp::Geq | BinaryOp::Gt => {
return None; return None;
} }
BinaryOp::Neq => { BinaryOp::And | BinaryOp::Or | BinaryOp::Eq | BinaryOp::Neq => unreachable!("handled above"),
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.));
}
}
}; };
Some(Number::Complex(result)) Some(Number::Complex(result))
} }
@@ -151,6 +143,13 @@ impl Number {
} }
pub fn unary_op(self, op: UnaryOp) -> 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 { match self {
Number::Real(real) => match op { Number::Real(real) => match op {
UnaryOp::Neg => Number::Real(-real), UnaryOp::Neg => Number::Real(-real),
@@ -165,27 +164,26 @@ impl Number {
if truncated < 0. || (real - truncated).abs() > f64::EPSILON { if truncated < 0. || (real - truncated).abs() > f64::EPSILON {
return Number::Real(f64::NAN); 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; let n = truncated as u64;
if n > 170 {
return Number::Real(f64::INFINITY);
}
let mut acc = 1_f64; let mut acc = 1_f64;
for k in 1..=n { for k in 1..=n {
acc *= k as f64; acc *= k as f64;
} }
Number::Real(acc) Number::Real(acc)
} }
UnaryOp::Not => { UnaryOp::Not => unreachable!("handled above"),
let is_zero = real == 0.;
Number::Real(if is_zero { 1. } else { 0. })
}
}, },
Number::Complex(complex) => match op { Number::Complex(complex) => match op {
UnaryOp::Neg => Number::Complex(-complex), UnaryOp::Neg => Number::Complex(-complex),
UnaryOp::Sqrt => Number::Complex(complex.sqrt()), UnaryOp::Sqrt => Number::Complex(complex.sqrt()),
UnaryOp::Fac => Number::Complex(Complex::new(f64::NAN, f64::NAN)), UnaryOp::Fac => Number::Complex(Complex::new(f64::NAN, f64::NAN)),
UnaryOp::Not => { UnaryOp::Not => unreachable!("handled above"),
let is_zero = complex == Complex::new(0., 0.);
Number::Real(if is_zero { 1. } else { 0. })
}
}, },
} }
} }