mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-15 14:18:04 +08:00
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:
committed by
Dennis Kobert
parent
e569c56c97
commit
647f05532b
1
Cargo.lock
generated
1
Cargo.lock
generated
@@ -3394,7 +3394,6 @@ version = "0.0.0"
|
||||
dependencies = [
|
||||
"chumsky",
|
||||
"criterion",
|
||||
"lazy_static",
|
||||
"num-complex",
|
||||
"thiserror 2.0.18",
|
||||
]
|
||||
|
||||
@@ -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"] }
|
||||
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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),* $(,)?) => {
|
||||
$(
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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),* $(,)?) => {
|
||||
$(
|
||||
|
||||
@@ -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"),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user