mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-15 14:18:04 +08:00
Move the math expression parser from Pest to Chumsky and add more features (#2685)
Rewrite the math-parser library using a chumsky-based lexer and parser, adding functions, comparisons, logic, and conditionals
This commit is contained in:
97
Cargo.lock
generated
97
Cargo.lock
generated
@@ -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]]
|
||||
@@ -3358,7 +3371,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]]
|
||||
@@ -3379,11 +3392,10 @@ dependencies = [
|
||||
name = "math-parser"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"chumsky",
|
||||
"criterion",
|
||||
"lazy_static",
|
||||
"num-complex",
|
||||
"pest",
|
||||
"pest_derive",
|
||||
"thiserror 2.0.18",
|
||||
]
|
||||
|
||||
@@ -4148,50 +4160,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"
|
||||
@@ -4834,8 +4802,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]]
|
||||
@@ -4846,9 +4825,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"
|
||||
@@ -6319,7 +6304,7 @@ dependencies = [
|
||||
"matchers",
|
||||
"nu-ansi-term",
|
||||
"once_cell",
|
||||
"regex-automata",
|
||||
"regex-automata 0.4.14",
|
||||
"sharded-slab",
|
||||
"smallvec",
|
||||
"thread_local",
|
||||
@@ -6398,12 +6383,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"
|
||||
|
||||
@@ -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()?;
|
||||
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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| {
|
||||
|
||||
@@ -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> },
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -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) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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" }
|
||||
372
libraries/math-parser/src/lexer.rs
Normal file
372
libraries/math-parser/src/lexer.rs
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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.,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.))),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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. })
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,10 +48,10 @@ fn math<T: num_traits::float::Float>(
|
||||
#[default(1.)]
|
||||
operand_b: T,
|
||||
) -> T {
|
||||
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 T::from(0.).unwrap();
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user