Move the math expression parser from Pest to Chumsky and add more features (#2685)

Rewrite the math-parser library using a chumsky-based lexer and parser, adding functions, comparisons, logic, and conditionals
This commit is contained in:
urisinger
2026-07-27 00:14:39 +03:00
committed by GitHub
parent b2e5b6645c
commit 0952933ede
13 changed files with 1415 additions and 534 deletions

97
Cargo.lock generated
View File

@@ -654,6 +654,19 @@ dependencies = [
"windows-link 0.1.3",
]
[[package]]
name = "chumsky"
version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "14377e276b2c8300513dff55ba4cc4142b44e5d6de6d00eb5b2307d650bb4ec1"
dependencies = [
"hashbrown 0.15.5",
"regex-automata 0.3.9",
"serde",
"unicode-ident",
"unicode-segmentation",
]
[[package]]
name = "ciborium"
version = "0.2.2"
@@ -1536,8 +1549,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e1e1dacd0d2082dfcf1351c4bdd566bbe89a2b263235a2b50058f1e130a47277"
dependencies = [
"bit-set 0.8.0",
"regex-automata",
"regex-syntax",
"regex-automata 0.4.14",
"regex-syntax 0.8.6",
]
[[package]]
@@ -3357,7 +3370,7 @@ version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9"
dependencies = [
"regex-automata",
"regex-automata 0.4.14",
]
[[package]]
@@ -3378,11 +3391,10 @@ dependencies = [
name = "math-parser"
version = "0.0.0"
dependencies = [
"chumsky",
"criterion",
"lazy_static",
"num-complex",
"pest",
"pest_derive",
"thiserror 2.0.18",
]
@@ -4147,50 +4159,6 @@ version = "2.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
[[package]]
name = "pest"
version = "2.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1db05f56d34358a8b1066f67cbb203ee3e7ed2ba674a6263a1d5ec6db2204323"
dependencies = [
"memchr",
"thiserror 2.0.18",
"ucd-trie",
]
[[package]]
name = "pest_derive"
version = "2.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bb056d9e8ea77922845ec74a1c4e8fb17e7c218cc4fc11a15c5d25e189aa40bc"
dependencies = [
"pest",
"pest_generator",
]
[[package]]
name = "pest_generator"
version = "2.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "87e404e638f781eb3202dc82db6760c8ae8a1eeef7fb3fa8264b2ef280504966"
dependencies = [
"pest",
"pest_meta",
"proc-macro2",
"quote",
"syn 2.0.106",
]
[[package]]
name = "pest_meta"
version = "2.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "edd1101f170f5903fde0914f899bb503d9ff5271d7ba76bbb70bea63690cc0d5"
dependencies = [
"pest",
"sha2",
]
[[package]]
name = "petgraph"
version = "0.7.1"
@@ -4833,8 +4801,19 @@ checksum = "23d7fd106d8c02486a8d64e778353d1cffe08ce79ac2e82f540c86d0facf6912"
dependencies = [
"aho-corasick",
"memchr",
"regex-automata",
"regex-syntax",
"regex-automata 0.4.14",
"regex-syntax 0.8.6",
]
[[package]]
name = "regex-automata"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "59b23e92ee4318893fa3fe3e6fb365258efbfe6ac6ab30f090cdcbb7aa37efa9"
dependencies = [
"aho-corasick",
"memchr",
"regex-syntax 0.7.5",
]
[[package]]
@@ -4845,9 +4824,15 @@ checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f"
dependencies = [
"aho-corasick",
"memchr",
"regex-syntax",
"regex-syntax 0.8.6",
]
[[package]]
name = "regex-syntax"
version = "0.7.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dbb5fb1acd8a1a18b3dd5be62d25485eb770e05afb408a9627d14d451bae12da"
[[package]]
name = "regex-syntax"
version = "0.8.6"
@@ -6322,7 +6307,7 @@ dependencies = [
"matchers",
"nu-ansi-term",
"once_cell",
"regex-automata",
"regex-automata 0.4.14",
"sharded-slab",
"smallvec",
"thread_local",
@@ -6401,12 +6386,6 @@ version = "1.18.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f"
[[package]]
name = "ucd-trie"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971"
[[package]]
name = "unicode-bidi"
version = "0.3.18"

View File

@@ -550,7 +550,6 @@ fn evaluate_and_validate_number_input(expression: &str, number_input: &NumberInp
let value = math_parser::evaluate(expression)
.inspect_err(|err| error!("Math parser error on \"{expression}\": {err}"))
.ok()?
.0
.inspect_err(|err| error!("Math evaluate error on \"{expression}\": {err}"))
.ok()?;

View File

@@ -8,11 +8,10 @@ description = "Parser for Graphite style mathematics expressions"
license = "MIT OR Apache-2.0"
[dependencies]
pest = "2.7"
pest_derive = "2.7"
thiserror = "2.0"
lazy_static = "1.5"
num-complex = "0.4"
chumsky = { version = "0.10", default-features = false, features = ["std"] }
[dev-dependencies]
criterion = { workspace = true }

View File

@@ -9,7 +9,7 @@ macro_rules! generate_benchmarks {
$(
c.bench_function(concat!("parse ", $input), |b| {
b.iter(|| {
let _ = black_box(ast::Node::try_parse_from_str($input)).unwrap();
let _ = black_box(ast::Node::try_parse_from_str($input));
});
});
)*
@@ -17,7 +17,10 @@ macro_rules! generate_benchmarks {
fn evaluation_bench(c: &mut Criterion) {
$(
let expr = ast::Node::try_parse_from_str($input).unwrap().0;
let expr = match ast::Node::try_parse_from_str($input) {
Ok(expr) => expr,
Err(err) => panic!("failed to parse `{}`: {err}", $input),
};
let context = EvalContext::default();
c.bench_function(concat!("eval ", $input), |b| {

View File

@@ -37,7 +37,7 @@ impl Unit {
}
}
#[derive(Debug, PartialEq)]
#[derive(Debug, Clone, PartialEq)]
pub enum Literal {
Float(f64),
Complex(Complex),
@@ -54,8 +54,19 @@ pub enum BinaryOp {
Add,
Sub,
Mul,
/// Logical AND (nonzero treated as true, returns 1. or 0.)
And,
Div,
/// Logical OR (nonzero treated as true, returns 1. or 0.)
Or,
Modulo,
Pow,
Leq,
Lt,
Geq,
Gt,
Neq,
Eq,
}
#[derive(Debug, PartialEq, Clone, Copy)]
@@ -63,6 +74,7 @@ pub enum UnaryOp {
Neg,
Sqrt,
Fac,
Not,
}
#[derive(Debug, PartialEq)]
@@ -72,4 +84,5 @@ pub enum Node {
FnCall { name: String, expr: Vec<Node> },
BinOp { lhs: Box<Node>, op: BinaryOp, rhs: Box<Node> },
UnaryOp { expr: Box<Node>, op: UnaryOp },
Conditional { condition: Box<Node>, if_block: Box<Node>, else_block: Box<Node> },
}

View File

@@ -2,13 +2,21 @@ use crate::value::{Number, Value};
use lazy_static::lazy_static;
use num_complex::{Complex, ComplexFloat};
use std::collections::HashMap;
use std::f64::consts::PI;
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();
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 {
@@ -63,6 +71,7 @@ lazy_static! {
}),
);
// Inverse trig with legacy names and standard aliases
map.insert(
"invsin",
Box::new(|values| match values {
@@ -71,6 +80,14 @@ lazy_static! {
_ => 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,
}),
);
map.insert(
"invcos",
@@ -80,6 +97,14 @@ lazy_static! {
_ => 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,
}),
);
map.insert(
"invtan",
@@ -89,6 +114,14 @@ lazy_static! {
_ => 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,
}),
);
map.insert(
"invcsc",
@@ -98,6 +131,14 @@ lazy_static! {
_ => 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,
}),
);
map.insert(
"invsec",
@@ -107,12 +148,486 @@ lazy_static! {
_ => 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,
}),
);
map.insert(
"invcot",
Box::new(|values| match values {
[Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real((PI / 2. - real).atan()))),
[Value::Number(Number::Complex(complex))] => Some(Value::Number(Number::Complex((Complex::new(PI / 2., 0.) - complex).atan()))),
[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,
}),
);
// 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,
}),
);
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,
}),
);
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,
}),
);
// 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,
}),
);
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,
}),
);
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,
}),
);
// 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,
}),
);
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,
}),
);
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,
}),
);
// 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,
}),
);
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,
}),
);
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,
}),
);
// 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,
}),
);
// 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,
}),
);
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,
}),
);
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,
}),
);
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,
}
}
_ => 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,
}),
);
// 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,
}),
);
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,
}),
);
// 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,
}),
);
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,
}),
);
// 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,
}),
);
map.insert(
"floor",
Box::new(|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,
}),
);
map.insert(
"round",
Box::new(|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,
}),
);
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,
}),
);
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))))
}
_ => 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,
}),
);
// 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,
}),
);
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,
}),
);
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,
}),
);
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,
}),
);
// 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,
}),
);
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,
}),
);
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,
}),
);

View File

@@ -2,6 +2,7 @@ use crate::ast::{Literal, Node};
use crate::constants::DEFAULT_FUNCTIONS;
use crate::context::{EvalContext, FunctionProvider, ValueProvider};
use crate::value::{Number, Value};
use num_complex::Complex;
use thiserror::Error;
#[derive(Debug, Error)]
@@ -24,7 +25,7 @@ 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))),
(Value::Number(lhs), Value::Number(rhs)) => Ok(Value::Number(lhs.binary_op(*op, rhs).ok_or(EvalError::TypeError)?)),
},
Node::UnaryOp { expr, op } => match expr.eval(context)? {
Value::Number(num) => Ok(Value::Number(num.unary_op(*op))),
@@ -32,6 +33,7 @@ impl Node {
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) {
@@ -40,6 +42,14 @@ impl Node {
context.get_value(name).ok_or_else(|| 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,
};
if condition { if_block.eval(context) } else { else_block.eval(context) }
}
}
}
}

View File

@@ -1,60 +0,0 @@
WHITESPACE = _{ " " | "\t" }
// TODO: Proper indentation and formatting
program = _{ SOI ~ expr ~ EOI }
expr = { atom ~ (infix ~ atom)* }
atom = _{ prefix? ~ primary ~ postfix? }
infix = _{ add | sub | mul | div | pow | paren }
add = { "+" } // Addition
sub = { "-" } // Subtraction
mul = { "*" } // Multiplication
div = { "/" } // Division
mod = { "%" } // Modulo
pow = { "^" } // Exponentiation
paren = { "" } // Implicit multiplication operator
prefix = _{ neg | sqrt }
neg = { "-" } // Negation
sqrt = { "sqrt" }
postfix = _{ fac }
fac = { "!" } // Factorial
primary = _{ ("(" ~ expr ~ ")") | lit | constant | fn_call | ident }
fn_call = { ident ~ "(" ~ expr ~ ("," ~ expr)* ~ ")" }
ident = @{ (ASCII_ALPHA | "_") ~ (ASCII_ALPHANUMERIC | "_")* }
lit = { unit | ((float | int) ~ unit?) }
float = @{ (int ~ "." ~ int? ~ exp? | "." ~ int ~ exp? | int ~ exp) ~ !("." | ASCII_DIGIT) }
exp = _{ ^"e" ~ ("+" | "-")? ~ int }
int = @{ ASCII_DIGIT+ }
unit = ${ (scale ~ base_unit) | base_unit ~ !ident}
base_unit = _{ meter | second | gram }
meter = { "m" }
second = { "s" }
gram = { "g" }
scale = _{ nano | micro | milli | centi | deci | deca | hecto | kilo | mega | giga | tera }
nano = { "n" }
micro = { "µ" | "u" }
milli = { "m" }
centi = { "c" }
deci = { "d" }
deca = { "da" }
hecto = { "h" }
kilo = { "k" }
mega = { "M" }
giga = { "G" }
tera = { "T" }
// Constants
constant = { infinity | imaginary_unit | pi | tau | euler_number | golden_ratio | gravity_acceleration }
infinity = { "inf" | "INF" | "infinity" | "INFINITY" | "∞" }
imaginary_unit = { "i" | "I" }
pi = { "pi" | "PI" | "π" }
tau = { "tau" | "TAU" | "τ" }
euler_number = { "e" }
golden_ratio = { "phi" | "PHI" | "φ" }
gravity_acceleration = { "G" }

View File

@@ -0,0 +1,372 @@
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;
#[derive(Clone, Debug, PartialEq)]
pub enum Token<'src> {
Float(f64),
Const(Constant),
Ident(&'src str),
AndAnd,
OrOr,
Bang,
LParen,
RParen,
Comma,
Plus,
Minus,
Modulo,
Star,
Slash,
Caret,
Lt,
Le,
Gt,
Ge,
Neq,
EqEq,
If,
}
impl<'src> fmt::Display for Token<'src> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Token::Float(x) => write!(f, "{x}"),
Token::Const(c) => write!(f, "{c}"),
Token::Ident(name) => write!(f, "{name}"),
Token::AndAnd => f.write_str("&&"),
Token::OrOr => f.write_str("||"),
Token::Bang => f.write_str("!"),
Token::LParen => f.write_str("("),
Token::RParen => f.write_str(")"),
Token::Comma => f.write_str(","),
Token::Plus => f.write_str("+"),
Token::Minus => f.write_str("-"),
Token::Modulo => f.write_str("%"),
Token::Star => f.write_str("*"),
Token::Slash => f.write_str("/"),
Token::Caret => f.write_str("^"),
Token::Lt => f.write_str("<"),
Token::Le => f.write_str("<="),
Token::Gt => f.write_str(">"),
Token::Ge => f.write_str(">="),
Token::Neq => f.write_str("!="),
Token::EqEq => f.write_str("=="),
Token::If => f.write_str("if"),
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Constant {
Pi,
Tau,
E,
Phi,
Inf,
I,
G,
}
impl Constant {
pub fn value(self) -> Literal {
use Constant::*;
use std::f64::consts;
match self {
Pi => Literal::Float(consts::PI),
Tau => Literal::Float(consts::TAU),
E => Literal::Float(consts::E),
Phi => Literal::Float(1.618_033_988_75),
Inf => Literal::Float(f64::INFINITY),
I => Literal::Complex(Complex64::new(0., 1.)),
G => Literal::Float(9.80665),
}
}
pub fn from_name(name: &str) -> Option<Constant> {
use Constant::*;
Some(match name {
"pi" | "π" => Pi,
"tau" | "τ" => Tau,
"e" => E,
"phi" | "φ" => Phi,
"inf" | "" => Inf,
"i" => I,
"G" => G,
_ => return None,
})
}
}
impl fmt::Display for Constant {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
use Constant::*;
f.write_str(match self {
Pi => "pi",
Tau => "tau",
E => "e",
Phi => "phi",
Inf => "inf",
I => "i",
G => "G",
})
}
}
pub struct Lexer<'a> {
input: &'a str,
pos: usize,
}
impl<'a> Lexer<'a> {
pub fn new(input: &'a str) -> Self {
Self { input, pos: 0 }
}
fn peek(&self) -> Option<char> {
self.input[self.pos..].chars().next()
}
fn bump(&mut self) -> Option<char> {
let c = self.peek()?;
self.pos += c.len_utf8();
Some(c)
}
fn consume_while<F>(&mut self, cond: F) -> &'a str
where
F: Fn(char) -> bool,
{
let start = self.pos;
while self.peek().is_some_and(&cond) {
self.bump();
}
&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;
let mut digits = 0;
while let Some(d) = self.peek().and_then(|c| c.to_digit(10)) {
v = v * 10 + d as u64;
digits += 1;
self.bump();
}
(digits > 0).then_some((v, digits))
}
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 mut got_digit = int_digits > 0;
let mut num = int_val as f64;
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;
}
}
if 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 {
self.pos = start_pos;
return None;
}
}
got_digit.then_some(num)
}
fn skip_ws(&mut self) {
self.consume_while(char::is_whitespace);
}
pub fn next_token(&mut self) -> Option<Token<'a>> {
self.skip_ws();
let start = self.pos;
let ch = self.bump()?;
use Token::*;
let tok = match ch {
'&' => {
if self.peek() == Some('&') {
self.bump();
AndAnd
} else {
return None;
}
}
'|' => {
if self.peek() == Some('|') {
self.bump();
OrOr
} else {
return None;
}
}
'(' => LParen,
')' => RParen,
',' => Comma,
'+' => Plus,
'-' => Minus,
'*' => Star,
'%' => Modulo,
'/' => Slash,
'^' => Caret,
'≠' => Neq,
'!' => {
if self.peek() == Some('=') {
self.bump();
Neq
} else {
Bang
}
}
'≤' => Le,
'<' => {
if self.peek() == Some('=') {
self.bump();
Le
} else {
Lt
}
}
'≥' => Ge,
'>' => {
if self.peek() == Some('=') {
self.bump();
Ge
} else {
Gt
}
}
'=' => {
if self.peek() == Some('=') {
self.bump();
EqEq
} else {
return None;
}
}
c if c.is_ascii_digit() || (c == '.' && self.peek().is_some_and(|c| c.is_ascii_digit())) => {
self.pos = start;
Float(self.lex_number()?)
}
_ => {
self.consume_while(|c| c.is_alphanumeric() || c == '_');
let ident = &self.input[start..self.pos];
if ident == "if" {
If
} else if let Some(lit) = Constant::from_name(ident) {
Const(lit)
} else if ch.is_alphanumeric() {
Ident(ident)
} else {
return None;
}
}
};
Some(tok)
}
}
impl<'a> Iterator for Lexer<'a> {
type Item = Token<'a>;
fn next(&mut self) -> Option<Self::Item> {
self.next_token()
}
}
impl<'src> Input<'src> for Lexer<'src> {
type Token = Token<'src>;
type Span = Span;
type Cursor = usize; // byte offset inside `input`
type MaybeToken = Token<'src>;
type Cache = Self;
#[inline]
fn begin(self) -> (Self::Cursor, Self::Cache) {
(0, self)
}
#[inline]
fn cursor_location(cursor: &Self::Cursor) -> usize {
*cursor
}
#[inline]
unsafe fn next_maybe(this: &mut Self::Cache, cursor: &mut Self::Cursor) -> Option<Self::MaybeToken> {
this.pos = *cursor;
if let Some(tok) = this.next_token() {
*cursor = this.pos;
Some(tok)
} else {
None
}
}
#[inline]
unsafe fn span(_this: &mut Self::Cache, range: Range<&Self::Cursor>) -> Self::Span {
(*range.start..*range.end).into()
}
}
impl<'src> ValueInput<'src> for Lexer<'src> {
#[inline]
unsafe fn next(this: &mut Self::Cache, cursor: &mut Self::Cursor) -> Option<Self::Token> {
this.pos = *cursor;
if let Some(tok) = this.next_token() {
*cursor = this.pos;
Some(tok)
} else {
None
}
}
}

View File

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

View File

@@ -1,316 +1,163 @@
use crate::ast::{BinaryOp, Literal, Node, UnaryOp, Unit};
use crate::context::EvalContext;
use crate::lexer::{Lexer, Span, Token};
use crate::value::{Complex, Number, Value};
use chumsky::container::Seq;
use chumsky::input::{BorrowInput, ValueInput};
use chumsky::{Parser, prelude::*};
use lazy_static::lazy_static;
use num_complex::ComplexFloat;
use pest::Parser;
use pest::iterators::{Pair, Pairs};
use pest::pratt_parser::{Assoc, Op, PrattParser};
use pest_derive::Parser;
use std::fmt;
use std::num::{ParseFloatError, ParseIntError};
use thiserror::Error;
#[derive(Parser)]
#[grammar = "./grammer.pest"] // Point to the grammar file
struct ExprParser;
/// One message per parse failure, each tagged with its byte range in the source expression.
#[derive(Debug)]
pub struct ParseError(Vec<String>);
lazy_static! {
static ref PRATT_PARSER: PrattParser<Rule> = {
PrattParser::new()
.op(Op::infix(Rule::add, Assoc::Left) | Op::infix(Rule::sub, Assoc::Left))
.op(Op::infix(Rule::mul, Assoc::Left) | Op::infix(Rule::div, Assoc::Left) | Op::infix(Rule::paren, Assoc::Left))
.op(Op::infix(Rule::pow, Assoc::Right))
.op(Op::postfix(Rule::fac) | Op::postfix(Rule::EOI))
.op(Op::prefix(Rule::sqrt))
.op(Op::prefix(Rule::neg))
};
impl fmt::Display for ParseError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
for (index, error) in self.0.iter().enumerate() {
if index > 0 {
writeln!(f)?;
}
write!(f, "{error}")?;
}
Ok(())
}
}
#[derive(Error, Debug)]
pub enum TypeError {
#[error("Invalid BinOp: {0:?} {1:?} {2:?}")]
InvalidBinaryOp(Unit, BinaryOp, Unit),
#[error("Invalid UnaryOp: {0:?}")]
InvalidUnaryOp(Unit, UnaryOp),
}
#[derive(Error, Debug)]
pub enum ParseError {
#[error("ParseIntError: {0}")]
ParseInt(#[from] ParseIntError),
#[error("ParseFloatError: {0}")]
ParseFloat(#[from] ParseFloatError),
#[error("TypeError: {0}")]
Type(#[from] TypeError),
#[error("PestError: {0}")]
Pest(#[from] Box<pest::error::Error<Rule>>),
}
impl std::error::Error for ParseError {}
impl Node {
pub fn try_parse_from_str(s: &str) -> Result<(Node, Unit), ParseError> {
let pairs = ExprParser::parse(Rule::program, s).map_err(Box::new)?;
let (node, metadata) = parse_expr(pairs)?;
Ok((node, metadata.unit))
}
}
pub fn try_parse_from_str(src: &str) -> Result<Node, ParseError> {
let tokens = Lexer::new(src);
struct NodeMetadata {
pub unit: Unit,
}
impl NodeMetadata {
pub fn new(unit: Unit) -> Self {
Self { unit }
}
}
fn parse_unit(pairs: Pairs<Rule>) -> Result<(Unit, f64), ParseError> {
let mut scale = 1.;
let mut length = 0;
let mut mass = 0;
let mut time = 0;
for pair in pairs {
println!("found rule: {:?}", pair.as_rule());
match pair.as_rule() {
Rule::nano => scale *= 1e-9,
Rule::micro => scale *= 1e-6,
Rule::milli => scale *= 1e-3,
Rule::centi => scale *= 1e-2,
Rule::deci => scale *= 1e-1,
Rule::deca => scale *= 1e1,
Rule::hecto => scale *= 1e2,
Rule::kilo => scale *= 1e3,
Rule::mega => scale *= 1e6,
Rule::giga => scale *= 1e9,
Rule::tera => scale *= 1e12,
Rule::meter => length = 1,
Rule::gram => mass = 1,
Rule::second => time = 1,
_ => unreachable!(), // All possible rules should be covered
match parser().parse(tokens).into_result() {
Ok(ast) => Ok(ast),
Err(parse_errs) => Err(ParseError(parse_errs.into_iter().map(|e| format!("{e} at {}", e.span())).collect())),
}
}
Ok((Unit { length, mass, time }, scale))
}
fn parse_const(pair: Pair<Rule>) -> Literal {
match pair.as_rule() {
Rule::infinity => Literal::Float(f64::INFINITY),
Rule::imaginary_unit => Literal::Complex(Complex::new(0., 1.)),
Rule::pi => Literal::Float(std::f64::consts::PI),
Rule::tau => Literal::Float(2. * std::f64::consts::PI),
Rule::euler_number => Literal::Float(std::f64::consts::E),
Rule::golden_ratio => Literal::Float(1.61803398875),
_ => unreachable!("Unexpected constant: {:?}", pair),
}
}
pub fn parser<'src, I>() -> impl Parser<'src, I, Node, extra::Err<Rich<'src, Token<'src>, Span>>>
where
I: ValueInput<'src, Token = Token<'src>, Span = Span>,
{
recursive(|expr| {
let constant = select! {
Token::Float(f) => Node::Lit(Literal::Float(f)),
Token::Const(c) => Node::Lit(c.value())
};
fn parse_lit(mut pairs: Pairs<Rule>) -> Result<(Literal, Unit), ParseError> {
let literal = match pairs.next() {
Some(lit) => match lit.as_rule() {
Rule::int => {
let value = lit.as_str().parse::<i32>()? as f64;
Literal::Float(value)
}
Rule::float => {
let value = lit.as_str().parse::<f64>()?;
Literal::Float(value)
}
Rule::unit => {
let (unit, scale) = parse_unit(lit.into_inner())?;
return Ok((Literal::Float(scale), unit));
}
rule => unreachable!("unexpected rule: {:?}", rule),
},
None => unreachable!("expected rule"), // No literal found
};
let args = expr.clone().separated_by(just(Token::Comma)).collect::<Vec<_>>().delimited_by(just(Token::LParen), just(Token::RParen));
if let Some(unit_pair) = pairs.next() {
let unit_pairs = unit_pair.into_inner(); // Get the inner pairs for the unit
let (unit, scale) = parse_unit(unit_pairs)?;
println!("found unit: {unit:?}");
Ok((
match literal {
Literal::Float(num) => Literal::Float(num * scale),
Literal::Complex(num) => Literal::Complex(num * scale),
},
unit,
))
} else {
Ok((literal, Unit::BASE_UNIT))
}
}
fn parse_expr(pairs: Pairs<Rule>) -> Result<(Node, NodeMetadata), ParseError> {
PRATT_PARSER
.map_primary(|primary| {
Ok(match primary.as_rule() {
Rule::lit => {
let (lit, unit) = parse_lit(primary.into_inner())?;
(Node::Lit(lit), NodeMetadata { unit })
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)"));
}
Rule::fn_call => {
let mut pairs = primary.into_inner();
let name = pairs.next().expect("fn_call always has 2 children").as_str().to_string();
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),
})
});
(
Node::FnCall {
name,
expr: pairs.map(|p| parse_expr(p.into_inner()).map(|expr| expr.0)).collect::<Result<Vec<Node>, ParseError>>()?,
},
NodeMetadata::new(Unit::BASE_UNIT),
)
}
Rule::constant => {
let lit = parse_const(primary.into_inner().next().expect("constant should have atleast 1 child"));
let ident = select! {Token::Ident(s) => s}.labelled("ident");
(Node::Lit(lit), NodeMetadata::new(Unit::BASE_UNIT))
}
Rule::ident => {
let name = primary.as_str().to_string();
let call = ident.then(args).map(|(name, args): (&str, Vec<Node>)| Node::FnCall { name: name.to_string(), expr: args });
(Node::Var(name), NodeMetadata::new(Unit::BASE_UNIT))
}
Rule::expr => parse_expr(primary.into_inner())?,
Rule::float => {
let value = primary.as_str().parse::<f64>()?;
(Node::Lit(Literal::Float(value)), NodeMetadata::new(Unit::BASE_UNIT))
}
rule => unreachable!("unexpected rule: {:?}", rule),
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 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 pow_op = just(Token::Caret).to(BinaryOp::Pow);
let unary_op = choice((just(Token::Minus).to(UnaryOp::Neg), just(Token::Bang).to(UnaryOp::Not)));
let and_op = just(Token::AndAnd).to(BinaryOp::And);
let or_op = just(Token::OrOr).to(BinaryOp::Or);
let cmp_op = choice((
just(Token::Lt).to(BinaryOp::Lt),
just(Token::Le).to(BinaryOp::Leq),
just(Token::Gt).to(BinaryOp::Gt),
just(Token::Ge).to(BinaryOp::Geq),
just(Token::Neq).to(BinaryOp::Neq),
just(Token::EqEq).to(BinaryOp::Eq),
));
// Postfix factorial: expr! → UnaryOp::Fac
let postfix = atom
.clone()
.foldl(just(Token::Bang).repeated(), |expr, _| Node::UnaryOp {
op: UnaryOp::Fac,
expr: Box::new(expr),
})
})
.map_prefix(|op, rhs| {
let (rhs, rhs_metadata) = rhs?;
let op = match op.as_rule() {
Rule::neg => UnaryOp::Neg,
Rule::sqrt => UnaryOp::Sqrt,
.boxed();
rule => unreachable!("unexpected rule: {:?}", rule),
};
let node = Node::UnaryOp { expr: Box::new(rhs), op };
let unit = rhs_metadata.unit;
let unit = if !unit.is_base() {
match op {
UnaryOp::Sqrt if unit.length % 2 == 0 && unit.mass % 2 == 0 && unit.time % 2 == 0 => Unit {
length: unit.length / 2,
mass: unit.mass / 2,
time: unit.time / 2,
},
UnaryOp::Neg => unit,
op => return Err(ParseError::Type(TypeError::InvalidUnaryOp(unit, op))),
}
} else {
Unit::BASE_UNIT
};
Ok((node, NodeMetadata::new(unit)))
})
.map_postfix(|lhs, op| {
let (lhs_node, lhs_metadata) = lhs?;
let op = match op.as_rule() {
Rule::EOI => return Ok((lhs_node, lhs_metadata)),
Rule::fac => UnaryOp::Fac,
rule => unreachable!("unexpected rule: {:?}", rule),
};
if !lhs_metadata.unit.is_base() {
return Err(ParseError::Type(TypeError::InvalidUnaryOp(lhs_metadata.unit, op)));
}
Ok((Node::UnaryOp { expr: Box::new(lhs_node), op }, lhs_metadata))
})
.map_infix(|lhs, op, rhs| {
let (lhs, lhs_metadata) = lhs?;
let (rhs, rhs_metadata) = rhs?;
let op = match op.as_rule() {
Rule::add => BinaryOp::Add,
Rule::sub => BinaryOp::Sub,
Rule::mul => BinaryOp::Mul,
Rule::div => BinaryOp::Div,
Rule::pow => BinaryOp::Pow,
Rule::paren => BinaryOp::Mul,
rule => unreachable!("unexpected rule: {:?}", rule),
};
let (lhs_unit, rhs_unit) = (lhs_metadata.unit, rhs_metadata.unit);
let unit = match (!lhs_unit.is_base(), !rhs_unit.is_base()) {
(true, true) => match op {
BinaryOp::Mul => Unit {
length: lhs_unit.length + rhs_unit.length,
mass: lhs_unit.mass + rhs_unit.mass,
time: lhs_unit.time + rhs_unit.time,
},
BinaryOp::Div => Unit {
length: lhs_unit.length - rhs_unit.length,
mass: lhs_unit.mass - rhs_unit.mass,
time: lhs_unit.time - rhs_unit.time,
},
BinaryOp::Add | BinaryOp::Sub => {
if lhs_unit == rhs_unit {
lhs_unit
} else {
return Err(ParseError::Type(TypeError::InvalidBinaryOp(lhs_unit, op, rhs_unit)));
}
}
BinaryOp::Pow => {
return Err(ParseError::Type(TypeError::InvalidBinaryOp(lhs_unit, op, rhs_unit)));
}
},
(true, false) => match op {
BinaryOp::Add | BinaryOp::Sub => return Err(ParseError::Type(TypeError::InvalidBinaryOp(lhs_unit, op, Unit::BASE_UNIT))),
BinaryOp::Pow => {
//TODO: improve error type
//TODO: support 1 / int
if let Ok(Value::Number(Number::Real(val))) = rhs.eval(&EvalContext::default()) {
if (val - val as i32 as f64).abs() <= f64::EPSILON {
Unit {
length: lhs_unit.length * val as i32,
mass: lhs_unit.mass * val as i32,
time: lhs_unit.time * val as i32,
}
} else {
return Err(ParseError::Type(TypeError::InvalidBinaryOp(lhs_unit, op, Unit::BASE_UNIT)));
}
} else {
return Err(ParseError::Type(TypeError::InvalidBinaryOp(lhs_unit, op, Unit::BASE_UNIT)));
}
}
_ => lhs_unit,
},
(false, true) => match op {
BinaryOp::Add | BinaryOp::Sub | BinaryOp::Pow => return Err(ParseError::Type(TypeError::InvalidBinaryOp(Unit::BASE_UNIT, op, rhs_unit))),
_ => rhs_unit,
},
(false, false) => Unit::BASE_UNIT,
};
let node = Node::BinOp {
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),
};
},
);
Ok((node, NodeMetadata::new(unit)))
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();
let add = product.clone().foldl(add_op.then(product).repeated(), |lhs, (op, rhs)| Node::BinOp {
lhs: Box::new(lhs),
op,
rhs: Box::new(rhs),
});
let cmp = add.clone().foldl(cmp_op.then(add).repeated(), |lhs: Node, (op, rhs)| Node::BinOp {
lhs: Box::new(lhs),
op,
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 {
lhs: Box::new(lhs),
op,
rhs: Box::new(rhs),
});
and.clone().foldl(or_op.then(and).repeated(), |lhs, (op, rhs)| Node::BinOp {
lhs: Box::new(lhs),
op,
rhs: Box::new(rhs),
})
.parse(pairs)
})
}
//TODO: set up Unit test for Units
#[cfg(test)]
mod tests {
use super::*;
@@ -319,8 +166,12 @@ mod tests {
$(
#[test]
fn $name() {
let result = Node::try_parse_from_str($input).unwrap();
assert_eq!(result.0, $expected);
let result = match Node::try_parse_from_str($input) {
Ok(expr) => expr,
Err(err) => panic!("failed to parse `{}`: {err}", $input),
};
assert_eq!(result, $expected);
}
)*
};
@@ -349,16 +200,20 @@ mod tests {
op: BinaryOp::Pow,
rhs: Box::new(Node::Lit(Literal::Float(3.))),
},
test_parse_unary_sqrt: "sqrt(16)" => Node::UnaryOp {
expr: Box::new(Node::Lit(Literal::Float(16.))),
op: UnaryOp::Sqrt,
test_parse_unary_sqrt: "sqrt(16)" => Node::FnCall {
name: "sqrt".to_string(),
expr: vec![Node::Lit(Literal::Float(16.))],
},
test_parse_sqr_ident: "sqr(16)" => Node::FnCall {
name:"sqr".to_string(),
expr: vec![Node::Lit(Literal::Float(16.))]
test_parse_ii_call: "ii(16)" => Node::FnCall {
name: "ii".to_string(),
expr: vec![Node::Lit(Literal::Float(16.))]
},
test_parse_complex_expr: "(1 + 2) 3 - 4 ^ 2" => Node::BinOp {
test_parse_i_mul: "i(16)" => Node::BinOp {
lhs: Box::new(Node::Lit(Literal::Complex(Complex::new(0., 1.)))),
op: BinaryOp::Mul,
rhs: Box::new(Node::Lit(Literal::Float(16.))),
},
test_parse_complex_expr: "(1 + 2) * 3 - 4 ^ 2" => Node::BinOp {
lhs: Box::new(Node::BinOp {
lhs: Box::new(Node::BinOp {
lhs: Box::new(Node::Lit(Literal::Float(1.))),
@@ -374,6 +229,15 @@ mod tests {
op: BinaryOp::Pow,
rhs: Box::new(Node::Lit(Literal::Float(2.))),
}),
},
test_conditional_expr: "if (x+3, 0, 1)" => Node::Conditional{
condition: Box::new(Node::BinOp{
lhs: Box::new(Node::Var("x".to_string())),
op: BinaryOp::Add,
rhs: Box::new(Node::Lit(Literal::Float(3.))),
}),
if_block: Box::new(Node::Lit(Literal::Float(0.))),
else_block: Box::new(Node::Lit(Literal::Float(1.))),
}
}
}

View File

@@ -52,28 +52,74 @@ impl std::fmt::Display for Number {
}
impl Number {
pub fn binary_op(self, op: BinaryOp, other: Number) -> Number {
pub fn binary_op(self, op: BinaryOp, other: Number) -> Option<Number> {
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,
BinaryOp::Div => lhs / rhs,
BinaryOp::Modulo => lhs % rhs,
BinaryOp::Pow => lhs.powf(rhs),
BinaryOp::Leq => (lhs <= rhs) as u8 as f64,
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,
};
Number::Real(result)
Some(Number::Real(result))
}
(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,
BinaryOp::Div => lhs / rhs,
BinaryOp::Modulo => lhs % rhs,
BinaryOp::Pow => lhs.powc(rhs),
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.));
}
}
};
Number::Complex(result)
Some(Number::Complex(result))
}
(Number::Real(lhs), Number::Complex(rhs)) => {
@@ -84,8 +130,9 @@ impl Number {
BinaryOp::Mul => lhs_complex * rhs,
BinaryOp::Div => lhs_complex / rhs,
BinaryOp::Pow => lhs_complex.powc(rhs),
_ => return None,
};
Number::Complex(result)
Some(Number::Complex(result))
}
(Number::Complex(lhs), Number::Real(rhs)) => {
@@ -96,8 +143,9 @@ impl Number {
BinaryOp::Mul => lhs * rhs_complex,
BinaryOp::Div => lhs / rhs_complex,
BinaryOp::Pow => lhs.powf(rhs),
_ => return None,
};
Number::Complex(result)
Some(Number::Complex(result))
}
}
}
@@ -107,15 +155,37 @@ impl Number {
Number::Real(real) => match op {
UnaryOp::Neg => Number::Real(-real),
UnaryOp::Sqrt => Number::Real(real.sqrt()),
UnaryOp::Fac => todo!("Implement factorial"),
UnaryOp::Fac => {
// n! for real n: use integer semantics when n is a
// non-negative integer, otherwise return NaN.
if !real.is_finite() {
return Number::Real(f64::NAN);
}
let truncated = real.trunc();
if truncated < 0. || (real - truncated).abs() > f64::EPSILON {
return Number::Real(f64::NAN);
}
let n = truncated as u64;
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. })
}
},
Number::Complex(complex) => match op {
UnaryOp::Neg => Number::Complex(-complex),
UnaryOp::Sqrt => Number::Complex(complex.sqrt()),
UnaryOp::Fac => todo!("Implement factorial"),
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. })
}
},
}
}

View File

@@ -52,10 +52,10 @@ fn math<T: num_traits::float::Float>(
let (operand_a, attributes) = operand_a.into_parts();
let (expression, operand_b) = (expression.element(), *operand_b.element());
let (node, _unit) = match ast::Node::try_parse_from_str(expression) {
let node = match ast::Node::try_parse_from_str(expression) {
Ok(expr) => expr,
Err(e) => {
warn!("Invalid expression: `{expression}`\n{e:?}");
warn!("Invalid expression: `{expression}`\n{e}");
return Item::from_parts(T::from(0.).unwrap(), attributes);
}
};