From 5ccb4f6fca4d8efddfdfa2315f7bb4f47490dcb4 Mon Sep 17 00:00:00 2001 From: uri singer Date: Mon, 13 Jul 2026 17:26:34 -0700 Subject: [PATCH 1/2] Rewrite the math-parser library using a chumsky-based lexer and parser, adding functions, comparisons, logic, and conditionals --- Cargo.lock | 110 ++-- .../messages/layout/layout_message_handler.rs | 3 +- libraries/math-parser/Cargo.toml | 4 +- libraries/math-parser/benches/bench.rs | 10 +- libraries/math-parser/src/ast.rs | 15 +- libraries/math-parser/src/constants.rs | 521 +++++++++++++++++- libraries/math-parser/src/diagnostic.rs | 159 ++++++ libraries/math-parser/src/executer.rs | 48 +- libraries/math-parser/src/grammer.pest | 60 -- libraries/math-parser/src/lexer.rs | 372 +++++++++++++ libraries/math-parser/src/lib.rs | 345 ++++++++---- libraries/math-parser/src/parser.rs | 486 ++++++---------- libraries/math-parser/src/value.rs | 92 +++- node-graph/nodes/math/src/lib.rs | 6 +- 14 files changed, 1646 insertions(+), 585 deletions(-) create mode 100644 libraries/math-parser/src/diagnostic.rs delete mode 100644 libraries/math-parser/src/grammer.pest create mode 100644 libraries/math-parser/src/lexer.rs diff --git a/Cargo.lock b/Cargo.lock index d594ec4e31..713713c76f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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" @@ -778,6 +791,16 @@ dependencies = [ "thiserror 2.0.18", ] +[[package]] +name = "codespan-reporting" +version = "0.12.0" +source = "git+https://github.com/urisinger/codespan-style-writer.git#7ff3cb9013bf8320c151388cd76dfe254b325f0c" +dependencies = [ + "serde", + "termcolor", + "unicode-width", +] + [[package]] name = "codespan-reporting" version = "0.13.1" @@ -1527,8 +1550,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]] @@ -3345,7 +3368,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]] @@ -3366,11 +3389,11 @@ dependencies = [ name = "math-parser" version = "0.0.0" dependencies = [ + "chumsky", + "codespan-reporting 0.12.0", "criterion", "lazy_static", "num-complex", - "pest", - "pest_derive", "thiserror 2.0.18", ] @@ -3480,7 +3503,7 @@ dependencies = [ "bitflags 2.11.0", "cfg-if", "cfg_aliases", - "codespan-reporting", + "codespan-reporting 0.13.1", "half", "hashbrown 0.16.0", "hexf-parse", @@ -4135,50 +4158,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" @@ -4821,8 +4800,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]] @@ -4833,9 +4823,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" @@ -6304,7 +6300,7 @@ dependencies = [ "matchers", "nu-ansi-term", "once_cell", - "regex-automata", + "regex-automata 0.4.14", "sharded-slab", "smallvec", "thread_local", @@ -6383,12 +6379,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" diff --git a/editor/src/messages/layout/layout_message_handler.rs b/editor/src/messages/layout/layout_message_handler.rs index 6dc225d0d8..0ee57036f9 100644 --- a/editor/src/messages/layout/layout_message_handler.rs +++ b/editor/src/messages/layout/layout_message_handler.rs @@ -543,9 +543,8 @@ fn populate_computed_display_fields(layout: &mut Layout) { /// Returns `None` if the expression fails to parse, fails to evaluate, or yields a non-real number (such as `sqrt(-1)`). fn evaluate_and_validate_number_input(expression: &str, number_input: &NumberInput) -> Option { let value = math_parser::evaluate(expression) - .inspect_err(|err| error!("Math parser error on \"{expression}\": {err}")) + .inspect_err(|_| error!("Math parser error on \"{expression}\"")) .ok()? - .0 .inspect_err(|err| error!("Math evaluate error on \"{expression}\": {err}")) .ok()?; diff --git a/libraries/math-parser/Cargo.toml b/libraries/math-parser/Cargo.toml index 3a0424b9ac..200c89e8da 100644 --- a/libraries/math-parser/Cargo.toml +++ b/libraries/math-parser/Cargo.toml @@ -8,11 +8,11 @@ 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"] } +codespan-reporting = { git = "https://github.com/urisinger/codespan-style-writer.git"} [dev-dependencies] criterion = { workspace = true } diff --git a/libraries/math-parser/benches/bench.rs b/libraries/math-parser/benches/bench.rs index 68c158b13f..668cf3ce4e 100644 --- a/libraries/math-parser/benches/bench.rs +++ b/libraries/math-parser/benches/bench.rs @@ -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,13 @@ 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) => { + err.print(); + panic!(concat!("failed to parse `", $input, "`")); + } + }; let context = EvalContext::default(); c.bench_function(concat!("eval ", $input), |b| { diff --git a/libraries/math-parser/src/ast.rs b/libraries/math-parser/src/ast.rs index 4c42fc4b1d..4b4c0be625 100644 --- a/libraries/math-parser/src/ast.rs +++ b/libraries/math-parser/src/ast.rs @@ -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.0 or 0.0) + And, Div, + /// Logical OR (nonzero treated as true, returns 1.0 or 0.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 }, BinOp { lhs: Box, op: BinaryOp, rhs: Box }, UnaryOp { expr: Box, op: UnaryOp }, + Conditional { condition: Box, if_block: Box, else_block: Box }, } diff --git a/libraries/math-parser/src/constants.rs b/libraries/math-parser/src/constants.rs index f2ca33c646..d2ea380f3f 100644 --- a/libraries/math-parser/src/constants.rs +++ b/libraries/math-parser/src/constants.rs @@ -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 Option + 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.0 / *n)))) + } + [Value::Number(Number::Complex(x)), Value::Number(Number::Real(n))] => { + Some(Value::Number(Number::Complex(x.powf(1.0 / *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.0/3.0)))), + _ => 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.0 { + 1.0 + } else if *real < 0.0 { + -1.0 + } else { + 0.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.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.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.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 { 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.0 } else { 0.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.0 } else { 0.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.0 } else { 0.0 }))) + }, _ => None, }), ); diff --git a/libraries/math-parser/src/diagnostic.rs b/libraries/math-parser/src/diagnostic.rs new file mode 100644 index 0000000000..2d7d4874d6 --- /dev/null +++ b/libraries/math-parser/src/diagnostic.rs @@ -0,0 +1,159 @@ +use std::io::{self, Write}; + +use codespan_reporting::{ + diagnostic::{Diagnostic, Label, LabelStyle, Severity}, + files::{self, Files, SimpleFile, SimpleFiles}, + term::{ + self, Config, Renderer, RichDiagnostic, emit, + termcolor::{ColorChoice, StandardStream}, + }, +}; + +use crate::lexer::Span; + +pub struct CompileError { + pub file: SimpleFile, + pub diagnostics: Vec>, +} + +impl CompileError { + pub fn print(&self) { + let mut writer = StandardStream::stderr(ColorChoice::Auto); + let config = term::Config::default(); + for diag in &self.diagnostics { + term::emit(&mut writer.lock(), &config, &self.file, diag).unwrap(); + } + writer.flush(); + } + + pub fn render_html(&self, config: &Config) -> Result, files::Error> { + let mut buf = Vec::new(); + { + let mut html_writer = HtmlWriter::new(&mut buf); + + let mut renderer = Renderer::new(&mut html_writer, config); + for diag in &self.diagnostics { + RichDiagnostic::new(diag, config).render(&self.file, &mut renderer)?; + } + html_writer.close_span().expect("buffer writer cant fail"); + } + + Ok(buf) + } +} + +pub(crate) fn make_compile_error(filename: impl Into, src: &str, errs: impl IntoIterator)>) -> CompileError { + let file = SimpleFile::new(filename.into(), src.to_string()); + + let diagnostics = errs.into_iter().map(|(msg, primary, secondaries)| make_diagnostic(msg, primary, &secondaries)).collect(); + + CompileError { file, diagnostics } +} + +fn make_diagnostic(msg: impl Into, primary: Span, secondaries: &[(String, Span)]) -> Diagnostic<()> { + let msg_str = msg.into(); + let mut labels = vec![Label::primary((), primary).with_message(msg_str.clone())]; + for (smsg, span) in secondaries { + labels.push(Label::secondary((), *span).with_message(smsg.clone())); + } + Diagnostic::error().with_message(msg_str).with_labels(labels) +} + +struct HtmlWriter { + upstream: W, + span_open: bool, +} + +impl HtmlWriter { + pub fn new(upstream: W) -> Self { + HtmlWriter { upstream, span_open: false } + } + + /// Close any open span + fn close_span(&mut self) -> io::Result<()> { + if self.span_open { + write!(self.upstream, "")?; + self.span_open = false; + } + Ok(()) + } + + /// Open a new span with the given CSS class + fn open_span(&mut self, class: &str) -> io::Result<()> { + // close existing first + self.close_span()?; + write!(self.upstream, "", class)?; + self.span_open = true; + Ok(()) + } +} + +impl Write for HtmlWriter { + fn write(&mut self, buf: &[u8]) -> io::Result { + let mut last = 0; + for (i, &b) in buf.iter().enumerate() { + let escape = match b { + b'<' => b"<"[..].as_ref(), + b'>' => b">"[..].as_ref(), + b'&' => b"&"[..].as_ref(), + _ => continue, + }; + self.upstream.write_all(&buf[last..i])?; + self.upstream.write_all(escape)?; + last = i + 1; + } + self.upstream.write_all(&buf[last..])?; + Ok(buf.len()) + } + fn flush(&mut self) -> io::Result<()> { + self.upstream.flush() + } +} + +impl codespan_reporting::term::WriteStyle for HtmlWriter { + fn set_header(&mut self, severity: Severity) -> io::Result<()> { + let class = match severity { + Severity::Bug => "header-bug", + Severity::Error => "header-error", + Severity::Warning => "header-warning", + Severity::Note => "header-note", + Severity::Help => "header-help", + }; + self.open_span(class) + } + + fn set_header_message(&mut self) -> io::Result<()> { + self.open_span("header-message") + } + + fn set_line_number(&mut self) -> io::Result<()> { + self.open_span("line-number") + } + + fn set_note_bullet(&mut self) -> io::Result<()> { + self.open_span("note-bullet") + } + + fn set_source_border(&mut self) -> io::Result<()> { + self.open_span("source-border") + } + + fn set_label(&mut self, severity: Severity, label_style: LabelStyle) -> io::Result<()> { + let sev = match severity { + Severity::Bug => "bug", + Severity::Error => "error", + Severity::Warning => "warning", + Severity::Note => "note", + Severity::Help => "help", + }; + let typ = match label_style { + LabelStyle::Primary => "primary", + LabelStyle::Secondary => "secondary", + }; + self.open_span(&format!("label-{}-{}", typ, sev)) + } + + fn reset(&mut self) -> io::Result<()> { + self.close_span() + } +} diff --git a/libraries/math-parser/src/executer.rs b/libraries/math-parser/src/executer.rs index 3d5394dd4a..f795735093 100644 --- a/libraries/math-parser/src/executer.rs +++ b/libraries/math-parser/src/executer.rs @@ -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::, 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.0, + Value::Number(Number::Complex(number)) => number != Complex::ZERO, + }; + + if condition { if_block.eval(context) } else { else_block.eval(context) } + } } } } @@ -63,38 +73,38 @@ mod tests { } eval_tests! { - test_addition: Value::from_f64(7.) => Node::BinOp { - lhs: Box::new(Node::Lit(Literal::Float(3.))), + test_addition: Value::from_f64(7.0) => Node::BinOp { + lhs: Box::new(Node::Lit(Literal::Float(3.0))), op: BinaryOp::Add, - rhs: Box::new(Node::Lit(Literal::Float(4.))), + rhs: Box::new(Node::Lit(Literal::Float(4.0))), }, - test_subtraction: Value::from_f64(1.) => Node::BinOp { - lhs: Box::new(Node::Lit(Literal::Float(5.))), + test_subtraction: Value::from_f64(1.0) => Node::BinOp { + lhs: Box::new(Node::Lit(Literal::Float(5.0))), op: BinaryOp::Sub, - rhs: Box::new(Node::Lit(Literal::Float(4.))), + rhs: Box::new(Node::Lit(Literal::Float(4.0))), }, - test_multiplication: Value::from_f64(12.) => Node::BinOp { - lhs: Box::new(Node::Lit(Literal::Float(3.))), + test_multiplication: Value::from_f64(12.0) => Node::BinOp { + lhs: Box::new(Node::Lit(Literal::Float(3.0))), op: BinaryOp::Mul, - rhs: Box::new(Node::Lit(Literal::Float(4.))), + rhs: Box::new(Node::Lit(Literal::Float(4.0))), }, test_division: Value::from_f64(2.5) => Node::BinOp { - lhs: Box::new(Node::Lit(Literal::Float(5.))), + lhs: Box::new(Node::Lit(Literal::Float(5.0))), op: BinaryOp::Div, - rhs: Box::new(Node::Lit(Literal::Float(2.))), + rhs: Box::new(Node::Lit(Literal::Float(2.0))), }, - test_negation: Value::from_f64(-3.) => Node::UnaryOp { - expr: Box::new(Node::Lit(Literal::Float(3.))), + test_negation: Value::from_f64(-3.0) => Node::UnaryOp { + expr: Box::new(Node::Lit(Literal::Float(3.0))), op: UnaryOp::Neg, }, - test_sqrt: Value::from_f64(2.) => Node::UnaryOp { - expr: Box::new(Node::Lit(Literal::Float(4.))), + test_sqrt: Value::from_f64(2.0) => Node::UnaryOp { + expr: Box::new(Node::Lit(Literal::Float(4.0))), op: UnaryOp::Sqrt, }, - test_power: Value::from_f64(8.) => Node::BinOp { - lhs: Box::new(Node::Lit(Literal::Float(2.))), + test_power: Value::from_f64(8.0) => Node::BinOp { + lhs: Box::new(Node::Lit(Literal::Float(2.0))), op: BinaryOp::Pow, - rhs: Box::new(Node::Lit(Literal::Float(3.))), + rhs: Box::new(Node::Lit(Literal::Float(3.0))), }, } } diff --git a/libraries/math-parser/src/grammer.pest b/libraries/math-parser/src/grammer.pest deleted file mode 100644 index befa01cbc9..0000000000 --- a/libraries/math-parser/src/grammer.pest +++ /dev/null @@ -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" } diff --git a/libraries/math-parser/src/lexer.rs b/libraries/math-parser/src/lexer.rs new file mode 100644 index 0000000000..4b1e4c7a1b --- /dev/null +++ b/libraries/math-parser/src/lexer.rs @@ -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.0, 1.0)), + G => Literal::Float(9.80665), + } + } + + pub fn from_str(name: &str) -> Option { + 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 { + self.input[self.pos..].chars().next() + } + + fn bump(&mut self) -> Option { + let c = self.peek()?; + self.pos += c.len_utf8(); + Some(c) + } + + fn consume_while(&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 { + 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> { + 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_str(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.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 { + 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 { + this.pos = *cursor; + if let Some(tok) = this.next_token() { + *cursor = this.pos; + Some(tok) + } else { + None + } + } +} diff --git a/libraries/math-parser/src/lib.rs b/libraries/math-parser/src/lib.rs index fb926e8ff1..777cb03674 100644 --- a/libraries/math-parser/src/lib.rs +++ b/libraries/math-parser/src/lib.rs @@ -3,160 +3,285 @@ pub mod ast; mod constants; pub mod context; +pub mod diagnostic; pub mod executer; +pub mod lexer; pub mod parser; pub mod value; use ast::Unit; use context::{EvalContext, ValueMap}; +use diagnostic::CompileError; use executer::EvalError; -use parser::ParseError; use value::Value; -pub fn evaluate(expression: &str) -> Result<(Result, Unit), ParseError> { +pub fn evaluate(expression: &str) -> Result, CompileError> { 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)] mod tests { use super::*; use ast::Unit; + use codespan_reporting::term::{ + self, + termcolor::{ColorChoice, StandardStream}, + }; use value::Number; const EPSILON: f64 = 1e-10_f64; - #[test] - fn malformed_juxtaposed_numbers_fail_to_parse() { - // Two numbers cannot be glued together by a stray decimal point (they must not parse as implicit multiplication). - for input in ["1..5", "1.5.5", "1..", ".5.5"] { - assert!(evaluate(input).is_err(), "expected `{input}` to be a parse error"); - } - } + 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) => { + err.print(); + panic!("failed to parse `{input}`"); + } + }; + dbg!(&expr); + let context = EvalContext::default(); - macro_rules! test_end_to_end{ - ($($name:ident: $input:expr_2021 => ($expected_value:expr_2021, $expected_unit:expr_2021)),* $(,)?) => { - $( - #[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), - } + 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 => $expected:expr),* $(,)?) => { + $( + #[test] + fn $name() { + 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.0 * std::f64::consts::PI, + constant_infinity: "if(inf == ∞, inf, 0)" => f64::INFINITY, + multiply_pi: "2 * pi" => 2.0 * std::f64::consts::PI, + add_e_constant: "e + 1" => std::f64::consts::E + 1.0, + multiply_phi_constant: "phi * 2" => 1.61803398875 * 2.0, + exponent_tau: "2^tau" => 2f64.powf(2.0 * 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.0, + trig_cos_zero: "cos(0)" => 1.0, + trig_tan_pi_div_four: "tan(pi/4)" => 1.0, + trig_sin_tau: "sin(tau)" => 0.0, + trig_cos_tau_div_two: "cos(tau/2)" => -1.0, + trig_csc: "csc(pi/2)" => 1.0, + trig_sec: "sec(0)" => 1.0, + trig_cot: "cot(pi/4)" => 1.0, + + // Inverse trig aliases + inverse_trig_asin: "asin(1)" => std::f64::consts::FRAC_PI_2, + inverse_trig_acos: "acos(1)" => 0.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.0, + inverse_trig_acot: "acot(1)" => std::f64::consts::FRAC_PI_4, + + // Hyperbolic and reciprocal hyperbolic + hyperbolic_sinh: "sinh(0)" => 0.0, + hyperbolic_cosh: "cosh(0)" => 1.0, + hyperbolic_tanh: "tanh(0)" => 0.0, + hyperbolic_csch: "csch(1)" => 1f64.sinh().recip(), + hyperbolic_sech: "sech(0)" => 1.0, + hyperbolic_coth: "coth(1)" => 1f64.tanh().recip(), + + // Inverse hyperbolic + inverse_hyperbolic_asinh: "asinh(0)" => 0.0, + inverse_hyperbolic_acosh: "acosh(1)" => 0.0, + inverse_hyperbolic_atanh: "atanh(0)" => 0.0, + inverse_hyperbolic_acsch: "acsch(1)" => 1.0f64.asinh(), + inverse_hyperbolic_asech: "asech(1)" => 1.0f64.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.0, + logical_not_nonzero: "!5" => 0.0, + logical_not_expression: "!(2 - 2)" => 1.0, + + // Logical helpers as functions + logical_isnan: "isnan(0/0)" => 1.0, + logical_eq: "eq(2, 2)" => 1.0, + logical_greater: "greater(3, 2)" => 1.0, + + // Log / exp / pow / root + log_ln: "ln(e)" => 1.0, + log_log10: "log(100)" => 2.0, + log_log2: "log2(8)" => 3.0, + log_change_of_base: "log(8, 2)" => 3.0, + exp_function: "exp(1)" => std::f64::consts::E, + pow_real: "pow(2, 3)" => 8.0, + root_square: "root(9, 2)" => 3.0, + root_cube: "root(8, 3)" => 2.0, + + // 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.0, + mapping_fract: "fract(3.25)" => 0.25, + mapping_sign_pos: "sign(5)" => 1.0, + mapping_sign_neg: "sign(-5)" => -1.0, + + // Geometry / mapping extras + geometry_hypot: "hypot(3, 4)" => 5.0, + mapping_remap: "remap(5, 0, 10, 0, 100)" => 50.0, + + // GCD / LCM + gcd_simple: "gcd(24, 18)" => 6.0, + lcm_simple: "lcm(4, 6)" => 12.0, + + // 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., } } diff --git a/libraries/math-parser/src/parser.rs b/libraries/math-parser/src/parser.rs index 9389915b92..792c3be46a 100644 --- a/libraries/math-parser/src/parser.rs +++ b/libraries/math-parser/src/parser.rs @@ -1,316 +1,158 @@ use crate::ast::{BinaryOp, Literal, Node, UnaryOp, Unit}; use crate::context::EvalContext; +use crate::diagnostic::{CompileError, make_compile_error}; +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::num::{ParseFloatError, ParseIntError}; use thiserror::Error; -#[derive(Parser)] -#[grammar = "./grammer.pest"] // Point to the grammar file -struct ExprParser; - -lazy_static! { - static ref PRATT_PARSER: PrattParser = { - 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)) - }; -} - -#[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>), -} - 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)) - } -} - -struct NodeMetadata { - pub unit: Unit, -} - -impl NodeMetadata { - pub fn new(unit: Unit) -> Self { - Self { unit } - } -} - -fn parse_unit(pairs: Pairs) -> 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 + pub fn try_parse_from_str(src: &str) -> Result { + let tokens = Lexer::new(src); + + match parser().parse(tokens).into_result() { + Ok(ast) => Ok(ast), + Err(parse_errs) => { + let errs = parse_errs.into_iter().map(|e| { + let primary = e.span(); + let mut secondary = Vec::new(); + for (msg, ctx_span) in e.contexts() { + secondary.push((msg.to_string(), *ctx_span)); + } + (e.to_string(), *primary, secondary) + }); + Err(make_compile_error("expression", src, errs)) + } } } - - Ok((Unit { length, mass, time }, scale)) } -fn parse_const(pair: Pair) -> 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, 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) -> Result<(Literal, Unit), ParseError> { - let literal = match pairs.next() { - Some(lit) => match lit.as_rule() { - Rule::int => { - let value = lit.as_str().parse::()? as f64; - Literal::Float(value) - } - Rule::float => { - let value = lit.as_str().parse::()?; - Literal::Float(value) + let args = expr.clone().separated_by(just(Token::Comma)).collect::>().delimited_by(just(Token::LParen), just(Token::RParen)); + + let if_expr = just(Token::If) + .ignore_then(args.clone()) // Parses (cond, a, b) + .try_map(|args: Vec, span| { + if args.len() != 3 { + return Err(Rich::custom(span, "Expected 3 arguments in if(cond, a, b)")); + } + let mut iter = args.into_iter(); + let cond = iter.next().unwrap(); + let if_b = iter.next().unwrap(); + let else_b = iter.next().unwrap(); + Ok(Node::Conditional { + condition: Box::new(cond), + if_block: Box::new(if_b), + else_block: Box::new(else_b), + }) } - 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 - }; - - 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) -> 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 }) - } - Rule::fn_call => { - let mut pairs = primary.into_inner(); - let name = pairs.next().expect("fn_call always has 2 children").as_str().to_string(); - - ( - Node::FnCall { - name, - expr: pairs.map(|p| parse_expr(p.into_inner()).map(|expr| expr.0)).collect::, ParseError>>()?, - }, - NodeMetadata::new(Unit::BASE_UNIT), - ) - } - Rule::constant => { - let lit = parse_const(primary.into_inner().next().expect("constant should have atleast 1 child")); - - (Node::Lit(lit), NodeMetadata::new(Unit::BASE_UNIT)) - } - Rule::ident => { - let name = primary.as_str().to_string(); - - (Node::Var(name), NodeMetadata::new(Unit::BASE_UNIT)) - } - Rule::expr => parse_expr(primary.into_inner())?, - Rule::float => { - let value = primary.as_str().parse::()?; - (Node::Lit(Literal::Float(value)), NodeMetadata::new(Unit::BASE_UNIT)) - } - rule => unreachable!("unexpected rule: {:?}", rule), + ); + + let ident = select! {Token::Ident(s) => s}.labelled("ident"); + + let call = ident.then(args).map(|(name, args): (&str, Vec)| Node::FnCall { name: name.to_string(), expr: args }); + + 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, - - 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), - }; + .boxed(); - 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))); - } - }, + 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), + }, + ); - (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 unary = unary_op.repeated().foldr(pow, |op, expr| Node::UnaryOp { op, expr: Box::new(expr) }).boxed(); - let node = Node::BinOp { + let product = unary + .clone() + .foldl(mul_op.then(unary).repeated(), |lhs, (op, rhs)| Node::BinOp { lhs: Box::new(lhs), op, rhs: Box::new(rhs), - }; - - Ok((node, NodeMetadata::new(unit))) - }) - .parse(pairs) + }) + .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 / 0.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), + }); + + let or = and.clone().foldl(or_op.then(and).repeated(), |lhs, (op, rhs)| Node::BinOp { + lhs: Box::new(lhs), + op, + rhs: Box::new(rhs), + }); + + or + }) } -//TODO: set up Unit test for Units #[cfg(test)] mod tests { use super::*; @@ -319,61 +161,81 @@ 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) => { + err.print(); + panic!(concat!("failed to parse `", $input, "`")); + } + }; + assert_eq!(result, $expected); } )* }; } test_parser! { - test_parse_int_literal: "42" => Node::Lit(Literal::Float(42.)), + test_parse_int_literal: "42" => Node::Lit(Literal::Float(42.0)), test_parse_float_literal: "3.14" => Node::Lit(Literal::Float(#[allow(clippy::approx_constant)] 3.14)), test_parse_ident: "x" => Node::Var("x".to_string()), test_parse_unary_neg: "-42" => Node::UnaryOp { - expr: Box::new(Node::Lit(Literal::Float(42.))), + expr: Box::new(Node::Lit(Literal::Float(42.0))), op: UnaryOp::Neg, }, test_parse_binary_add: "1 + 2" => Node::BinOp { - lhs: Box::new(Node::Lit(Literal::Float(1.))), + lhs: Box::new(Node::Lit(Literal::Float(1.0))), op: BinaryOp::Add, - rhs: Box::new(Node::Lit(Literal::Float(2.))), + rhs: Box::new(Node::Lit(Literal::Float(2.0))), }, test_parse_binary_mul: "3 * 4" => Node::BinOp { - lhs: Box::new(Node::Lit(Literal::Float(3.))), + lhs: Box::new(Node::Lit(Literal::Float(3.0))), op: BinaryOp::Mul, - rhs: Box::new(Node::Lit(Literal::Float(4.))), + rhs: Box::new(Node::Lit(Literal::Float(4.0))), }, test_parse_binary_pow: "2 ^ 3" => Node::BinOp { - lhs: Box::new(Node::Lit(Literal::Float(2.))), + lhs: Box::new(Node::Lit(Literal::Float(2.0))), op: BinaryOp::Pow, - rhs: Box::new(Node::Lit(Literal::Float(3.))), + rhs: Box::new(Node::Lit(Literal::Float(3.0))), }, - 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.0))], }, - 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.0))] }, - - 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.0, 1.0)))), + op: BinaryOp::Mul, + rhs: Box::new(Node::Lit(Literal::Float(16.0))), + }, + 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.))), + lhs: Box::new(Node::Lit(Literal::Float(1.0))), op: BinaryOp::Add, - rhs: Box::new(Node::Lit(Literal::Float(2.))), + rhs: Box::new(Node::Lit(Literal::Float(2.0))), }), op: BinaryOp::Mul, - rhs: Box::new(Node::Lit(Literal::Float(3.))), + rhs: Box::new(Node::Lit(Literal::Float(3.0))), }), op: BinaryOp::Sub, rhs: Box::new(Node::BinOp { - lhs: Box::new(Node::Lit(Literal::Float(4.))), + lhs: Box::new(Node::Lit(Literal::Float(4.0))), op: BinaryOp::Pow, - rhs: Box::new(Node::Lit(Literal::Float(2.))), + rhs: Box::new(Node::Lit(Literal::Float(2.0))), + }), + }, + 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.0))), }), + if_block: Box::new(Node::Lit(Literal::Float(0.0))), + else_block: Box::new(Node::Lit(Literal::Float(1.0))), } } } diff --git a/libraries/math-parser/src/value.rs b/libraries/math-parser/src/value.rs index 1e56070a9c..74184c0575 100644 --- a/libraries/math-parser/src/value.rs +++ b/libraries/math-parser/src/value.rs @@ -52,52 +52,100 @@ 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 { match (self, other) { (Number::Real(lhs), Number::Real(rhs)) => { let result = match op { + BinaryOp::And => { + let l = lhs != 0.0; + let r = rhs != 0.0; + if l && r { 1.0 } else { 0.0 } + } + BinaryOp::Or => { + let l = lhs != 0.0; + let r = rhs != 0.0; + if l || r { 1.0 } else { 0.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, 0.0); + let r = rhs != Complex::new(0.0, 0.0); + return Some(Number::Real(if l && r { 1.0 } else { 0.0 })); + } + BinaryOp::Or => { + let l = lhs != Complex::new(0.0, 0.0); + let r = rhs != Complex::new(0.0, 0.0); + return Some(Number::Real(if l || r { 1.0 } else { 0.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.0)); + } else { + return Some(Number::Real(0.0)); + } + } + BinaryOp::Eq => { + if lhs == rhs { + return Some(Number::Real(1.0)); + } else { + return Some(Number::Real(0.0)); + } + } }; - Number::Complex(result) + Some(Number::Complex(result)) } (Number::Real(lhs), Number::Complex(rhs)) => { - let lhs_complex = Complex::new(lhs, 0.); + let lhs_complex = Complex::new(lhs, 0.0); let result = match op { BinaryOp::Add => lhs_complex + rhs, BinaryOp::Sub => lhs_complex - rhs, 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)) => { - let rhs_complex = Complex::new(rhs, 0.); + let rhs_complex = Complex::new(rhs, 0.0); let result = match op { BinaryOp::Add => lhs + rhs_complex, BinaryOp::Sub => lhs - rhs_complex, 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.0 || (real - truncated).abs() > f64::EPSILON { + return Number::Real(f64::NAN); + } + let n = truncated as u64; + let mut acc = 1.0_f64; + for k in 1..=n { + acc *= k as f64; + } + Number::Real(acc) + } + UnaryOp::Not => { + let is_zero = real == 0.0; + Number::Real(if is_zero { 1.0 } else { 0.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, 0.0); + Number::Real(if is_zero { 1.0 } else { 0.0 }) + } }, } } diff --git a/node-graph/nodes/math/src/lib.rs b/node-graph/nodes/math/src/lib.rs index 1474aa2be3..9a0ebdd3bf 100644 --- a/node-graph/nodes/math/src/lib.rs +++ b/node-graph/nodes/math/src/lib.rs @@ -53,10 +53,10 @@ fn math( 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:?}"); + Err(_) => { + warn!("Invalid expression: `{expression}`"); return Item::from_parts(T::from(0.).unwrap(), attributes); } }; From b1c49fedeb2c8029d98694df67ac31bff74de0dc Mon Sep 17 00:00:00 2001 From: Keavon Chambers Date: Mon, 13 Jul 2026 17:27:08 -0700 Subject: [PATCH 2/2] Fix parsing regressions, make parsing 2.5x faster than the old pest parser, and clean up the math-parser rewrite --- Cargo.lock | 14 +- .../messages/layout/layout_message_handler.rs | 2 +- libraries/math-parser/Cargo.toml | 2 - libraries/math-parser/benches/bench.rs | 11 +- libraries/math-parser/src/constants.rs | 990 +++++++----------- libraries/math-parser/src/diagnostic.rs | 159 --- libraries/math-parser/src/executer.rs | 59 +- libraries/math-parser/src/lexer.rs | 42 +- libraries/math-parser/src/lib.rs | 51 +- libraries/math-parser/src/parser.rs | 168 ++- libraries/math-parser/src/value.rs | 6 +- node-graph/nodes/math/src/lib.rs | 4 +- 12 files changed, 567 insertions(+), 941 deletions(-) delete mode 100644 libraries/math-parser/src/diagnostic.rs diff --git a/Cargo.lock b/Cargo.lock index 713713c76f..644f673911 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -791,16 +791,6 @@ dependencies = [ "thiserror 2.0.18", ] -[[package]] -name = "codespan-reporting" -version = "0.12.0" -source = "git+https://github.com/urisinger/codespan-style-writer.git#7ff3cb9013bf8320c151388cd76dfe254b325f0c" -dependencies = [ - "serde", - "termcolor", - "unicode-width", -] - [[package]] name = "codespan-reporting" version = "0.13.1" @@ -3390,9 +3380,7 @@ name = "math-parser" version = "0.0.0" dependencies = [ "chumsky", - "codespan-reporting 0.12.0", "criterion", - "lazy_static", "num-complex", "thiserror 2.0.18", ] @@ -3503,7 +3491,7 @@ dependencies = [ "bitflags 2.11.0", "cfg-if", "cfg_aliases", - "codespan-reporting 0.13.1", + "codespan-reporting", "half", "hashbrown 0.16.0", "hexf-parse", diff --git a/editor/src/messages/layout/layout_message_handler.rs b/editor/src/messages/layout/layout_message_handler.rs index 0ee57036f9..8465a831c9 100644 --- a/editor/src/messages/layout/layout_message_handler.rs +++ b/editor/src/messages/layout/layout_message_handler.rs @@ -543,7 +543,7 @@ fn populate_computed_display_fields(layout: &mut Layout) { /// Returns `None` if the expression fails to parse, fails to evaluate, or yields a non-real number (such as `sqrt(-1)`). fn evaluate_and_validate_number_input(expression: &str, number_input: &NumberInput) -> Option { let value = math_parser::evaluate(expression) - .inspect_err(|_| error!("Math parser error on \"{expression}\"")) + .inspect_err(|err| error!("Math parser error on \"{expression}\": {err}")) .ok()? .inspect_err(|err| error!("Math evaluate error on \"{expression}\": {err}")) .ok()?; diff --git a/libraries/math-parser/Cargo.toml b/libraries/math-parser/Cargo.toml index 200c89e8da..acdfa7e3dc 100644 --- a/libraries/math-parser/Cargo.toml +++ b/libraries/math-parser/Cargo.toml @@ -9,10 +9,8 @@ license = "MIT OR Apache-2.0" [dependencies] thiserror = "2.0" -lazy_static = "1.5" num-complex = "0.4" chumsky = { version = "0.10", default-features = false, features = ["std"] } -codespan-reporting = { git = "https://github.com/urisinger/codespan-style-writer.git"} [dev-dependencies] criterion = { workspace = true } diff --git a/libraries/math-parser/benches/bench.rs b/libraries/math-parser/benches/bench.rs index 668cf3ce4e..92cc8ec81e 100644 --- a/libraries/math-parser/benches/bench.rs +++ b/libraries/math-parser/benches/bench.rs @@ -17,13 +17,10 @@ macro_rules! generate_benchmarks { fn evaluation_bench(c: &mut Criterion) { $( - let expr = match ast::Node::try_parse_from_str($input) { - Ok(expr) => expr, - Err(err) => { - err.print(); - panic!(concat!("failed to parse `", $input, "`")); - } - }; + 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| { diff --git a/libraries/math-parser/src/constants.rs b/libraries/math-parser/src/constants.rs index d2ea380f3f..4f4ea874af 100644 --- a/libraries/math-parser/src/constants.rs +++ b/libraries/math-parser/src/constants.rs @@ -1,637 +1,417 @@ use crate::value::{Number, Value}; -use lazy_static::lazy_static; -use num_complex::{Complex, ComplexFloat}; -use std::collections::HashMap; +use num_complex::ComplexFloat; use std::f64::consts::{LN_2, PI}; -type FunctionImplementation = Box Option + 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 { - [Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(real.sin()))), - [Value::Number(Number::Complex(complex))] => Some(Value::Number(Number::Complex(complex.sin()))), - _ => None, - }), - ); - - map.insert( - "cos", - Box::new(|values| match values { - [Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(real.cos()))), - [Value::Number(Number::Complex(complex))] => Some(Value::Number(Number::Complex(complex.cos()))), - _ => None, - }), - ); - - map.insert( - "tan", - Box::new(|values| match values { - [Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(real.tan()))), - [Value::Number(Number::Complex(complex))] => Some(Value::Number(Number::Complex(complex.tan()))), - _ => None, - }), - ); - - map.insert( - "csc", - Box::new(|values| match values { - [Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(real.sin().recip()))), - [Value::Number(Number::Complex(complex))] => Some(Value::Number(Number::Complex(complex.sin().recip()))), - _ => None, - }), - ); - - map.insert( - "sec", - Box::new(|values| match values { - [Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(real.cos().recip()))), - [Value::Number(Number::Complex(complex))] => Some(Value::Number(Number::Complex(complex.cos().recip()))), - _ => None, - }), - ); - - map.insert( - "cot", - Box::new(|values| match values { - [Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(real.tan().recip()))), - [Value::Number(Number::Complex(complex))] => Some(Value::Number(Number::Complex(complex.tan().recip()))), - _ => None, - }), - ); +pub type BuiltinFunction = fn(&[Value]) -> Option; + +/// Looks up a built-in math function by name, returning a plain function pointer so dispatch avoids hashing and dynamic allocation. +pub fn builtin_function(name: &str) -> Option { + Some(match name { + "sin" => |values| match values { + [Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(real.sin()))), + [Value::Number(Number::Complex(complex))] => Some(Value::Number(Number::Complex(complex.sin()))), + _ => None, + }, + + "cos" => |values| match values { + [Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(real.cos()))), + [Value::Number(Number::Complex(complex))] => Some(Value::Number(Number::Complex(complex.cos()))), + _ => None, + }, + + "tan" => |values| match values { + [Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(real.tan()))), + [Value::Number(Number::Complex(complex))] => Some(Value::Number(Number::Complex(complex.tan()))), + _ => None, + }, + + "csc" => |values| match values { + [Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(real.sin().recip()))), + [Value::Number(Number::Complex(complex))] => Some(Value::Number(Number::Complex(complex.sin().recip()))), + _ => None, + }, + + "sec" => |values| match values { + [Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(real.cos().recip()))), + [Value::Number(Number::Complex(complex))] => Some(Value::Number(Number::Complex(complex.cos().recip()))), + _ => None, + }, + + "cot" => |values| match values { + [Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(real.tan().recip()))), + [Value::Number(Number::Complex(complex))] => Some(Value::Number(Number::Complex(complex.tan().recip()))), + _ => None, + }, // Inverse trig with legacy names and standard aliases - map.insert( - "invsin", - Box::new(|values| match values { - [Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(real.asin()))), - [Value::Number(Number::Complex(complex))] => Some(Value::Number(Number::Complex(complex.asin()))), - _ => None, - }), - ); - map.insert( - "asin", - Box::new(|values| match values { - [Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(real.asin()))), - [Value::Number(Number::Complex(complex))] => Some(Value::Number(Number::Complex(complex.asin()))), - _ => None, - }), - ); - - map.insert( - "invcos", - Box::new(|values| match values { - [Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(real.acos()))), - [Value::Number(Number::Complex(complex))] => Some(Value::Number(Number::Complex(complex.acos()))), - _ => None, - }), - ); - map.insert( - "acos", - Box::new(|values| match values { - [Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(real.acos()))), - [Value::Number(Number::Complex(complex))] => Some(Value::Number(Number::Complex(complex.acos()))), - _ => None, - }), - ); - - map.insert( - "invtan", - Box::new(|values| match values { - [Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(real.atan()))), - [Value::Number(Number::Complex(complex))] => Some(Value::Number(Number::Complex(complex.atan()))), - _ => None, - }), - ); - map.insert( - "atan", - Box::new(|values| match values { - [Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(real.atan()))), - [Value::Number(Number::Complex(complex))] => Some(Value::Number(Number::Complex(complex.atan()))), - _ => None, - }), - ); - - map.insert( - "invcsc", - Box::new(|values| match values { - [Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(real.recip().asin()))), - [Value::Number(Number::Complex(complex))] => Some(Value::Number(Number::Complex(complex.recip().asin()))), - _ => None, - }), - ); - map.insert( - "acsc", - Box::new(|values| match values { - [Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(real.recip().asin()))), - [Value::Number(Number::Complex(complex))] => Some(Value::Number(Number::Complex(complex.recip().asin()))), - _ => None, - }), - ); - - map.insert( - "invsec", - Box::new(|values| match values { - [Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(real.recip().acos()))), - [Value::Number(Number::Complex(complex))] => Some(Value::Number(Number::Complex(complex.recip().acos()))), - _ => None, - }), - ); - map.insert( - "asec", - Box::new(|values| match values { - [Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(real.recip().acos()))), - [Value::Number(Number::Complex(complex))] => Some(Value::Number(Number::Complex(complex.recip().acos()))), - _ => None, - }), - ); - - map.insert( - "invcot", - Box::new(|values| match values { - [Value::Number(Number::Real(real))] => { - Some(Value::Number(Number::Real(real.recip().atan()))) - } - [Value::Number(Number::Complex(complex))] => { - Some(Value::Number(Number::Complex(complex.recip().atan()))) - } - _ => None, - }), - ); - map.insert( - "acot", - Box::new(|values| match values { - [Value::Number(Number::Real(real))] => { - Some(Value::Number(Number::Real(real.recip().atan()))) - } - [Value::Number(Number::Complex(complex))] => { - Some(Value::Number(Number::Complex(complex.recip().atan()))) - } - _ => None, - }), - ); + "invsin" => |values| match values { + [Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(real.asin()))), + [Value::Number(Number::Complex(complex))] => Some(Value::Number(Number::Complex(complex.asin()))), + _ => None, + }, + "asin" => |values| match values { + [Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(real.asin()))), + [Value::Number(Number::Complex(complex))] => Some(Value::Number(Number::Complex(complex.asin()))), + _ => None, + }, + + "invcos" => |values| match values { + [Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(real.acos()))), + [Value::Number(Number::Complex(complex))] => Some(Value::Number(Number::Complex(complex.acos()))), + _ => None, + }, + "acos" => |values| match values { + [Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(real.acos()))), + [Value::Number(Number::Complex(complex))] => Some(Value::Number(Number::Complex(complex.acos()))), + _ => None, + }, + + "invtan" => |values| match values { + [Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(real.atan()))), + [Value::Number(Number::Complex(complex))] => Some(Value::Number(Number::Complex(complex.atan()))), + _ => None, + }, + "atan" => |values| match values { + [Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(real.atan()))), + [Value::Number(Number::Complex(complex))] => Some(Value::Number(Number::Complex(complex.atan()))), + _ => None, + }, + + "invcsc" => |values| match values { + [Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(real.recip().asin()))), + [Value::Number(Number::Complex(complex))] => Some(Value::Number(Number::Complex(complex.recip().asin()))), + _ => None, + }, + "acsc" => |values| match values { + [Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(real.recip().asin()))), + [Value::Number(Number::Complex(complex))] => Some(Value::Number(Number::Complex(complex.recip().asin()))), + _ => None, + }, + + "invsec" => |values| match values { + [Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(real.recip().acos()))), + [Value::Number(Number::Complex(complex))] => Some(Value::Number(Number::Complex(complex.recip().acos()))), + _ => None, + }, + "asec" => |values| match values { + [Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(real.recip().acos()))), + [Value::Number(Number::Complex(complex))] => Some(Value::Number(Number::Complex(complex.recip().acos()))), + _ => None, + }, + + "invcot" => |values| match values { + [Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(real.recip().atan()))), + [Value::Number(Number::Complex(complex))] => Some(Value::Number(Number::Complex(complex.recip().atan()))), + _ => None, + }, + "acot" => |values| match values { + [Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(real.recip().atan()))), + [Value::Number(Number::Complex(complex))] => Some(Value::Number(Number::Complex(complex.recip().atan()))), + _ => None, + }, // Hyperbolic Functions - map.insert( - "sinh", - Box::new(|values| match values { - [Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(real.sinh()))), - [Value::Number(Number::Complex(complex))] => Some(Value::Number(Number::Complex(complex.sinh()))), - _ => None, - }), - ); - - 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, - }), - ); + "sinh" => |values| match values { + [Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(real.sinh()))), + [Value::Number(Number::Complex(complex))] => Some(Value::Number(Number::Complex(complex.sinh()))), + _ => None, + }, + + "cosh" => |values| match values { + [Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(real.cosh()))), + [Value::Number(Number::Complex(complex))] => Some(Value::Number(Number::Complex(complex.cosh()))), + _ => None, + }, + + "tanh" => |values| match values { + [Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(real.tanh()))), + [Value::Number(Number::Complex(complex))] => Some(Value::Number(Number::Complex(complex.tanh()))), + _ => None, + }, // Reciprocal hyperbolic functions - map.insert( - "csch", - Box::new(|values| match values { - [Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(real.sinh().recip()))), - [Value::Number(Number::Complex(complex))] => Some(Value::Number(Number::Complex(complex.sinh().recip()))), - _ => None, - }), - ); - - 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, - }), - ); + "csch" => |values| match values { + [Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(real.sinh().recip()))), + [Value::Number(Number::Complex(complex))] => Some(Value::Number(Number::Complex(complex.sinh().recip()))), + _ => None, + }, + + "sech" => |values| match values { + [Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(real.cosh().recip()))), + [Value::Number(Number::Complex(complex))] => Some(Value::Number(Number::Complex(complex.cosh().recip()))), + _ => None, + }, + + "coth" => |values| match values { + [Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(real.tanh().recip()))), + [Value::Number(Number::Complex(complex))] => Some(Value::Number(Number::Complex(complex.tanh().recip()))), + _ => None, + }, // Inverse Hyperbolic Functions - map.insert( - "asinh", - Box::new(|values| match values { - [Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(real.asinh()))), - [Value::Number(Number::Complex(complex))] => Some(Value::Number(Number::Complex(complex.asinh()))), - _ => None, - }), - ); - - 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, - }), - ); + "asinh" => |values| match values { + [Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(real.asinh()))), + [Value::Number(Number::Complex(complex))] => Some(Value::Number(Number::Complex(complex.asinh()))), + _ => None, + }, + + "acosh" => |values| match values { + [Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(real.acosh()))), + [Value::Number(Number::Complex(complex))] => Some(Value::Number(Number::Complex(complex.acosh()))), + _ => None, + }, + + "atanh" => |values| match values { + [Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(real.atanh()))), + [Value::Number(Number::Complex(complex))] => Some(Value::Number(Number::Complex(complex.atanh()))), + _ => None, + }, // Inverse reciprocal hyperbolic functions - map.insert( - "acsch", - Box::new(|values| match values { - [Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(real.recip().asinh()))), - [Value::Number(Number::Complex(complex))] => Some(Value::Number(Number::Complex(complex.recip().asinh()))), - _ => None, - }), - ); - - 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, - }), - ); + "acsch" => |values| match values { + [Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(real.recip().asinh()))), + [Value::Number(Number::Complex(complex))] => Some(Value::Number(Number::Complex(complex.recip().asinh()))), + _ => None, + }, + + "asech" => |values| match values { + [Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(real.recip().acosh()))), + [Value::Number(Number::Complex(complex))] => Some(Value::Number(Number::Complex(complex.recip().acosh()))), + _ => None, + }, + + "acoth" => |values| match values { + [Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(real.recip().atanh()))), + [Value::Number(Number::Complex(complex))] => Some(Value::Number(Number::Complex(complex.recip().atanh()))), + _ => None, + }, // Logarithm Functions - map.insert( - "ln", - Box::new(|values| match values { - [Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(real.ln()))), - [Value::Number(Number::Complex(complex))] => Some(Value::Number(Number::Complex(complex.ln()))), - _ => None, - }), - ); + "ln" => |values| match values { + [Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(real.ln()))), + [Value::Number(Number::Complex(complex))] => Some(Value::Number(Number::Complex(complex.ln()))), + _ => None, + }, // Exponential / power helpers - map.insert( - "exp", - Box::new(|values| match values { - [Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(real.exp()))), - [Value::Number(Number::Complex(complex))] => Some(Value::Number(Number::Complex(complex.exp()))), - _ => None, - }), - ); - - 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.0 / *n)))) - } - [Value::Number(Number::Complex(x)), Value::Number(Number::Real(n))] => { - Some(Value::Number(Number::Complex(x.powf(1.0 / *n)))) + "exp" => |values| match values { + [Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(real.exp()))), + [Value::Number(Number::Complex(complex))] => Some(Value::Number(Number::Complex(complex.exp()))), + _ => None, + }, + + "pow" => |values| match values { + [Value::Number(Number::Real(x)), Value::Number(Number::Real(n))] => Some(Value::Number(Number::Real(x.powf(*n)))), + [Value::Number(Number::Complex(x)), Value::Number(Number::Real(n))] => Some(Value::Number(Number::Complex(x.powf(*n)))), + [Value::Number(Number::Complex(x)), Value::Number(Number::Complex(n))] => Some(Value::Number(Number::Complex(x.powc(*n)))), + _ => None, + }, + + "root" => |values| match values { + [Value::Number(Number::Real(x)), Value::Number(Number::Real(n))] => Some(Value::Number(Number::Real(x.powf(1.0 / *n)))), + [Value::Number(Number::Complex(x)), Value::Number(Number::Real(n))] => Some(Value::Number(Number::Complex(x.powf(1.0 / *n)))), + _ => None, + }, + + "log" => |values| match values { + [Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(real.log10()))), + [Value::Number(Number::Complex(complex))] => Some(Value::Number(Number::Complex(complex.log10()))), + [Value::Number(n), Value::Number(base)] => { + // Custom base logarithm using change of base formula + let compute_log = |x: f64, b: f64| -> f64 { x.ln() / b.ln() }; + match (n, base) { + (Number::Real(x), Number::Real(b)) => Some(Value::Number(Number::Real(compute_log(*x, *b)))), + _ => None, } - _ => None, - }), - ); - - 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, - }), - ); + } + _ => None, + }, + + "log2" => |values| match values { + [Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(real.log2()))), + [Value::Number(Number::Complex(complex))] => Some(Value::Number(Number::Complex(complex.ln() / LN_2))), + _ => None, + }, // Root Functions - map.insert( - "sqrt", - Box::new(|values| match values { - [Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(real.sqrt()))), - [Value::Number(Number::Complex(complex))] => Some(Value::Number(Number::Complex(complex.sqrt()))), - _ => None, - }), - ); - - 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.0/3.0)))), - _ => None, - }), - ); + "sqrt" => |values| match values { + [Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(real.sqrt()))), + [Value::Number(Number::Complex(complex))] => Some(Value::Number(Number::Complex(complex.sqrt()))), + _ => None, + }, + + "cbrt" => |values| match values { + [Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(real.cbrt()))), + [Value::Number(Number::Complex(complex))] => Some(Value::Number(Number::Complex(complex.powf(1.0 / 3.0)))), + _ => 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, - }), - ); + "hypot" => |values| match values { + [Value::Number(Number::Real(a)), Value::Number(Number::Real(b))] => Some(Value::Number(Number::Real(a.hypot(*b)))), + _ => None, + }, + + "atan2" => |values| match values { + [Value::Number(Number::Real(y)), Value::Number(Number::Real(x))] => Some(Value::Number(Number::Real(y.atan2(*x)))), + _ => None, + }, // Mapping Functions - map.insert( - "abs", - Box::new(|values| match values { - [Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(real.abs()))), - [Value::Number(Number::Complex(complex))] => Some(Value::Number(Number::Real(complex.abs()))), - _ => None, - }), - ); - - 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)))) + "abs" => |values| match values { + [Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(real.abs()))), + [Value::Number(Number::Complex(complex))] => Some(Value::Number(Number::Real(complex.abs()))), + _ => None, + }, + + "floor" => |values| match values { + [Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(real.floor()))), + _ => None, + }, + + "ceil" => |values| match values { + [Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(real.ceil()))), + _ => None, + }, + + "round" => |values| match values { + [Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(real.round()))), + _ => None, + }, + + "clamp" => |values| match values { + [Value::Number(Number::Real(x)), Value::Number(Number::Real(min)), Value::Number(Number::Real(max))] => Some(Value::Number(Number::Real(x.clamp(*min, *max)))), + _ => None, + }, + + "lerp" => |values| match values { + [Value::Number(Number::Real(a)), Value::Number(Number::Real(b)), Value::Number(Number::Real(t))] => Some(Value::Number(Number::Real(a + (b - a) * t))), + _ => None, + }, + + "remap" => |values| match values { + [ + Value::Number(Number::Real(value)), + Value::Number(Number::Real(in_a)), + Value::Number(Number::Real(in_b)), + Value::Number(Number::Real(out_a)), + Value::Number(Number::Real(out_b)), + ] => { + let t = (*value - *in_a) / (*in_b - *in_a); + Some(Value::Number(Number::Real(out_a + t * (out_b - out_a)))) + } + _ => None, + }, + + "trunc" => |values| match values { + [Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(real.trunc()))), + _ => None, + }, + + "fract" => |values| match values { + [Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(real.fract()))), + _ => None, + }, + + "sign" => |values| match values { + [Value::Number(Number::Real(real))] => { + let s = if *real > 0.0 { + 1.0 + } else if *real < 0.0 { + -1.0 + } else { + 0.0 + }; + Some(Value::Number(Number::Real(s))) + } + _ => None, + }, + + "gcd" => |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.0))); } - _ => None, - }), - ); - - map.insert( - "trunc", - Box::new(|values| match values { - [Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(real.trunc()))), - _ => None, - }), - ); - - map.insert( - "fract", - Box::new(|values| match values { - [Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(real.fract()))), - _ => None, - }), - ); - - map.insert( - "sign", - Box::new(|values| match values { - [Value::Number(Number::Real(real))] => { - let s = if *real > 0.0 { - 1.0 - } else if *real < 0.0 { - -1.0 - } else { - 0.0 - }; - Some(Value::Number(Number::Real(s))) + x = x.abs(); + y = y.abs(); + while y != 0 { + let r = x % y; + x = y; + y = r; } - _ => 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.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))) + Some(Value::Number(Number::Real(x as f64))) + } + _ => None, + }, + + "lcm" => |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.0))); } - _ => 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.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))) + + // gcd + let mut gx = x; + let mut gy = y; + while gy != 0 { + let r = gx % gy; + gx = gy; + gy = r; } - _ => None, - }), - ); + 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.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 { 0.0 } else { PI }; - Some(Value::Number(Number::Real(angle))) - } - _ => None, - }), - ); + "real" => |values| match values { + [Value::Number(Number::Complex(complex))] => Some(Value::Number(Number::Real(complex.re))), + [Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(*real))), + _ => None, + }, + + "imag" => |values| match values { + [Value::Number(Number::Complex(complex))] => Some(Value::Number(Number::Real(complex.im))), + [Value::Number(Number::Real(_))] => Some(Value::Number(Number::Real(0.0))), + _ => None, + }, + + "conj" => |values| match values { + [Value::Number(Number::Complex(complex))] => Some(Value::Number(Number::Complex(complex.conj()))), + [Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(*real))), + _ => None, + }, + + "arg" => |values| match values { + [Value::Number(Number::Complex(complex))] => Some(Value::Number(Number::Real(complex.arg()))), + [Value::Number(Number::Real(real))] => { + let angle = if *real >= 0.0 { 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.0 } else { 0.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.0 } else { 0.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.0 } else { 0.0 }))) - }, - _ => None, - }), - ); - - map - }; + "isnan" => |values| match values { + [Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(if real.is_nan() { 1.0 } else { 0.0 }))), + _ => None, + }, + + "eq" => |values| match values { + [Value::Number(a), Value::Number(b)] => Some(Value::Number(Number::Real(if a == b { 1.0 } else { 0.0 }))), + _ => None, + }, + + "greater" => |values| match values { + [Value::Number(Number::Real(a)), Value::Number(Number::Real(b))] => Some(Value::Number(Number::Real(if a > b { 1.0 } else { 0.0 }))), + _ => None, + }, + _ => return None, + }) } diff --git a/libraries/math-parser/src/diagnostic.rs b/libraries/math-parser/src/diagnostic.rs deleted file mode 100644 index 2d7d4874d6..0000000000 --- a/libraries/math-parser/src/diagnostic.rs +++ /dev/null @@ -1,159 +0,0 @@ -use std::io::{self, Write}; - -use codespan_reporting::{ - diagnostic::{Diagnostic, Label, LabelStyle, Severity}, - files::{self, Files, SimpleFile, SimpleFiles}, - term::{ - self, Config, Renderer, RichDiagnostic, emit, - termcolor::{ColorChoice, StandardStream}, - }, -}; - -use crate::lexer::Span; - -pub struct CompileError { - pub file: SimpleFile, - pub diagnostics: Vec>, -} - -impl CompileError { - pub fn print(&self) { - let mut writer = StandardStream::stderr(ColorChoice::Auto); - let config = term::Config::default(); - for diag in &self.diagnostics { - term::emit(&mut writer.lock(), &config, &self.file, diag).unwrap(); - } - writer.flush(); - } - - pub fn render_html(&self, config: &Config) -> Result, files::Error> { - let mut buf = Vec::new(); - { - let mut html_writer = HtmlWriter::new(&mut buf); - - let mut renderer = Renderer::new(&mut html_writer, config); - for diag in &self.diagnostics { - RichDiagnostic::new(diag, config).render(&self.file, &mut renderer)?; - } - html_writer.close_span().expect("buffer writer cant fail"); - } - - Ok(buf) - } -} - -pub(crate) fn make_compile_error(filename: impl Into, src: &str, errs: impl IntoIterator)>) -> CompileError { - let file = SimpleFile::new(filename.into(), src.to_string()); - - let diagnostics = errs.into_iter().map(|(msg, primary, secondaries)| make_diagnostic(msg, primary, &secondaries)).collect(); - - CompileError { file, diagnostics } -} - -fn make_diagnostic(msg: impl Into, primary: Span, secondaries: &[(String, Span)]) -> Diagnostic<()> { - let msg_str = msg.into(); - let mut labels = vec![Label::primary((), primary).with_message(msg_str.clone())]; - for (smsg, span) in secondaries { - labels.push(Label::secondary((), *span).with_message(smsg.clone())); - } - Diagnostic::error().with_message(msg_str).with_labels(labels) -} - -struct HtmlWriter { - upstream: W, - span_open: bool, -} - -impl HtmlWriter { - pub fn new(upstream: W) -> Self { - HtmlWriter { upstream, span_open: false } - } - - /// Close any open span - fn close_span(&mut self) -> io::Result<()> { - if self.span_open { - write!(self.upstream, "")?; - self.span_open = false; - } - Ok(()) - } - - /// Open a new span with the given CSS class - fn open_span(&mut self, class: &str) -> io::Result<()> { - // close existing first - self.close_span()?; - write!(self.upstream, "", class)?; - self.span_open = true; - Ok(()) - } -} - -impl Write for HtmlWriter { - fn write(&mut self, buf: &[u8]) -> io::Result { - let mut last = 0; - for (i, &b) in buf.iter().enumerate() { - let escape = match b { - b'<' => b"<"[..].as_ref(), - b'>' => b">"[..].as_ref(), - b'&' => b"&"[..].as_ref(), - _ => continue, - }; - self.upstream.write_all(&buf[last..i])?; - self.upstream.write_all(escape)?; - last = i + 1; - } - self.upstream.write_all(&buf[last..])?; - Ok(buf.len()) - } - fn flush(&mut self) -> io::Result<()> { - self.upstream.flush() - } -} - -impl codespan_reporting::term::WriteStyle for HtmlWriter { - fn set_header(&mut self, severity: Severity) -> io::Result<()> { - let class = match severity { - Severity::Bug => "header-bug", - Severity::Error => "header-error", - Severity::Warning => "header-warning", - Severity::Note => "header-note", - Severity::Help => "header-help", - }; - self.open_span(class) - } - - fn set_header_message(&mut self) -> io::Result<()> { - self.open_span("header-message") - } - - fn set_line_number(&mut self) -> io::Result<()> { - self.open_span("line-number") - } - - fn set_note_bullet(&mut self) -> io::Result<()> { - self.open_span("note-bullet") - } - - fn set_source_border(&mut self) -> io::Result<()> { - self.open_span("source-border") - } - - fn set_label(&mut self, severity: Severity, label_style: LabelStyle) -> io::Result<()> { - let sev = match severity { - Severity::Bug => "bug", - Severity::Error => "error", - Severity::Warning => "warning", - Severity::Note => "note", - Severity::Help => "help", - }; - let typ = match label_style { - LabelStyle::Primary => "primary", - LabelStyle::Secondary => "secondary", - }; - self.open_span(&format!("label-{}-{}", typ, sev)) - } - - fn reset(&mut self) -> io::Result<()> { - self.close_span() - } -} diff --git a/libraries/math-parser/src/executer.rs b/libraries/math-parser/src/executer.rs index f795735093..ad3cb8328f 100644 --- a/libraries/math-parser/src/executer.rs +++ b/libraries/math-parser/src/executer.rs @@ -1,5 +1,5 @@ use crate::ast::{Literal, Node}; -use crate::constants::DEFAULT_FUNCTIONS; +use crate::constants::builtin_function; use crate::context::{EvalContext, FunctionProvider, ValueProvider}; use crate::value::{Number, Value}; use num_complex::Complex; @@ -32,11 +32,22 @@ 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::, EvalError>>()?; + // Arguments land in a stack buffer when they fit (builtins take at most 5), avoiding a heap allocation per call + let mut stack_values = [Value::from_f64(0.); 5]; + let heap_values: Vec; + let values: &[Value] = if expr.len() <= stack_values.len() { + for (slot, argument) in stack_values.iter_mut().zip(expr) { + *slot = argument.eval(context)?; + } + &stack_values[..expr.len()] + } else { + heap_values = expr.iter().map(|argument| argument.eval(context)).collect::, EvalError>>()?; + &heap_values + }; - 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) { + if let Some(function) = builtin_function(name) { + function(values).ok_or(EvalError::TypeError) + } else if let Some(val) = context.run_function(name, values) { Ok(val) } else { context.get_value(name).ok_or_else(|| EvalError::MissingFunction(name.to_string())) @@ -57,7 +68,7 @@ impl Node { #[cfg(test)] mod tests { use crate::ast::{BinaryOp, Literal, Node, UnaryOp}; - use crate::context::{EvalContext, ValueMap}; + use crate::context::EvalContext; use crate::value::Value; macro_rules! eval_tests { @@ -73,38 +84,38 @@ mod tests { } eval_tests! { - test_addition: Value::from_f64(7.0) => Node::BinOp { - lhs: Box::new(Node::Lit(Literal::Float(3.0))), + test_addition: Value::from_f64(7.) => Node::BinOp { + lhs: Box::new(Node::Lit(Literal::Float(3.))), op: BinaryOp::Add, - rhs: Box::new(Node::Lit(Literal::Float(4.0))), + rhs: Box::new(Node::Lit(Literal::Float(4.))), }, - test_subtraction: Value::from_f64(1.0) => Node::BinOp { - lhs: Box::new(Node::Lit(Literal::Float(5.0))), + test_subtraction: Value::from_f64(1.) => Node::BinOp { + lhs: Box::new(Node::Lit(Literal::Float(5.))), op: BinaryOp::Sub, - rhs: Box::new(Node::Lit(Literal::Float(4.0))), + rhs: Box::new(Node::Lit(Literal::Float(4.))), }, - test_multiplication: Value::from_f64(12.0) => Node::BinOp { - lhs: Box::new(Node::Lit(Literal::Float(3.0))), + test_multiplication: Value::from_f64(12.) => Node::BinOp { + lhs: Box::new(Node::Lit(Literal::Float(3.))), op: BinaryOp::Mul, - rhs: Box::new(Node::Lit(Literal::Float(4.0))), + rhs: Box::new(Node::Lit(Literal::Float(4.))), }, test_division: Value::from_f64(2.5) => Node::BinOp { - lhs: Box::new(Node::Lit(Literal::Float(5.0))), + lhs: Box::new(Node::Lit(Literal::Float(5.))), op: BinaryOp::Div, - rhs: Box::new(Node::Lit(Literal::Float(2.0))), + rhs: Box::new(Node::Lit(Literal::Float(2.))), }, - test_negation: Value::from_f64(-3.0) => Node::UnaryOp { - expr: Box::new(Node::Lit(Literal::Float(3.0))), + test_negation: Value::from_f64(-3.) => Node::UnaryOp { + expr: Box::new(Node::Lit(Literal::Float(3.))), op: UnaryOp::Neg, }, - test_sqrt: Value::from_f64(2.0) => Node::UnaryOp { - expr: Box::new(Node::Lit(Literal::Float(4.0))), + test_sqrt: Value::from_f64(2.) => Node::UnaryOp { + expr: Box::new(Node::Lit(Literal::Float(4.))), op: UnaryOp::Sqrt, }, - test_power: Value::from_f64(8.0) => Node::BinOp { - lhs: Box::new(Node::Lit(Literal::Float(2.0))), + test_power: Value::from_f64(8.) => Node::BinOp { + lhs: Box::new(Node::Lit(Literal::Float(2.))), op: BinaryOp::Pow, - rhs: Box::new(Node::Lit(Literal::Float(3.0))), + rhs: Box::new(Node::Lit(Literal::Float(3.))), }, } } diff --git a/libraries/math-parser/src/lexer.rs b/libraries/math-parser/src/lexer.rs index 4b1e4c7a1b..cf98c7c80a 100644 --- a/libraries/math-parser/src/lexer.rs +++ b/libraries/math-parser/src/lexer.rs @@ -1,14 +1,9 @@ use crate::ast::Literal; use chumsky::input::{Input, ValueInput}; -use chumsky::prelude::*; use chumsky::span::SimpleSpan; -use chumsky::text::{ident, int}; -use core::f64; use num_complex::Complex64; use std::fmt; -use std::iter::Peekable; use std::ops::Range; -use std::str::Chars; pub type Span = SimpleSpan; @@ -40,6 +35,9 @@ pub enum Token<'src> { EqEq, If, + + /// An unrecognized character; the parser never matches this, forcing a parse error rather than silently truncating the input. + Error, } impl<'src> fmt::Display for Token<'src> { @@ -71,6 +69,8 @@ impl<'src> fmt::Display for Token<'src> { Token::EqEq => f.write_str("=="), Token::If => f.write_str("if"), + + Token::Error => f.write_str(""), } } } @@ -101,7 +101,7 @@ impl Constant { } } - pub fn from_str(name: &str) -> Option { + pub fn from_name(name: &str) -> Option { use Constant::*; Some(match name { "pi" | "π" => Pi, @@ -162,10 +162,6 @@ impl<'a> Lexer<'a> { &self.input[start..self.pos] } - fn lex_ident(&mut self) -> &'a str { - self.consume_while(|c| c.is_alphanumeric() || c == '_') - } - fn lex_uint(&mut self) -> Option<(u64, usize)> { let mut v = 0u64; let mut digits = 0; @@ -212,6 +208,12 @@ impl<'a> Lexer<'a> { } } + // A numeric literal cannot be glued directly to another by a stray decimal point or digit (e.g. `1..5`, `1.5.5`), so reject rather than letting it parse as implicit multiplication. + if got_digit && self.peek().is_some_and(|c| c == '.' || c.is_ascii_digit()) { + self.pos = start_pos; + return None; + } + got_digit.then_some(num) } @@ -231,7 +233,7 @@ impl<'a> Lexer<'a> { self.bump(); AndAnd } else { - return None; + Error } } '|' => { @@ -239,7 +241,7 @@ impl<'a> Lexer<'a> { self.bump(); OrOr } else { - return None; + Error } } @@ -287,13 +289,21 @@ impl<'a> Lexer<'a> { self.bump(); EqEq } else { - return None; + Error } } c if c.is_ascii_digit() || (c == '.' && self.peek().is_some_and(|c| c.is_ascii_digit())) => { self.pos = start; - Float(self.lex_number()?) + match self.lex_number() { + Some(number) => Float(number), + // `lex_number` resets `pos` on failure, so advance past one character to guarantee forward progress. + None => { + self.pos = start; + self.bump(); + Error + } + } } _ => { @@ -302,12 +312,12 @@ impl<'a> Lexer<'a> { if ident == "if" { If - } else if let Some(lit) = Constant::from_str(ident) { + } else if let Some(lit) = Constant::from_name(ident) { Const(lit) } else if ch.is_alphanumeric() { Ident(ident) } else { - return None; + Error } } }; diff --git a/libraries/math-parser/src/lib.rs b/libraries/math-parser/src/lib.rs index 777cb03674..b0164a038e 100644 --- a/libraries/math-parser/src/lib.rs +++ b/libraries/math-parser/src/lib.rs @@ -1,21 +1,17 @@ -#![allow(unused)] - pub mod ast; mod constants; pub mod context; -pub mod diagnostic; pub mod executer; pub mod lexer; pub mod parser; pub mod value; -use ast::Unit; -use context::{EvalContext, ValueMap}; -use diagnostic::CompileError; +use context::EvalContext; use executer::EvalError; +use parser::ParseError; use value::Value; -pub fn evaluate(expression: &str) -> Result, CompileError> { +pub fn evaluate(expression: &str) -> Result, ParseError> { let expr = ast::Node::try_parse_from_str(expression); let context = EvalContext::default(); expr.map(|node| node.eval(&context)) @@ -24,32 +20,38 @@ pub fn evaluate(expression: &str) -> Result, CompileErr #[cfg(test)] mod tests { use super::*; - use ast::Unit; - use codespan_reporting::term::{ - self, - termcolor::{ColorChoice, StandardStream}, - }; use value::Number; const EPSILON: f64 = 1e-10_f64; + #[test] + fn malformed_juxtaposed_numbers_fail_to_parse() { + // Two numbers cannot be glued together by a stray decimal point (they must not parse as implicit multiplication). + for input in ["1..5", "1.5.5", "1..", ".5.5"] { + assert!(evaluate(input).is_err(), "expected `{input}` to be a parse error"); + } + } + + #[test] + fn unrecognized_characters_fail_to_parse() { + // Unrecognized trailing input must be rejected rather than silently dropped after a valid prefix. + for input in ["2@", "5#", "2 $ 3", "sqrt(4)@", "5 & 3", "5 | 3", "2 = 3"] { + assert!(evaluate(input).is_err(), "expected `{input}` to be a parse error"); + } + } + 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) => { - err.print(); - panic!("failed to parse `{input}`"); - } + Err(err) => panic!("failed to parse `{input}`: {err}"), }; - dbg!(&expr); let context = EvalContext::default(); let actual_value = match expr.eval(&context) { Ok(v) => v, - Err(err) => panic!("failed to evaluate {input} becuase of error {err}"), + Err(err) => panic!("failed to evaluate `{input}` because of error {err}"), }; - // compare match (actual_value, expected_value) { (Value::Number(Number::Complex(a)), Value::Number(Number::Complex(e))) => { // real part @@ -136,6 +138,17 @@ mod tests { exponent_single: "2^3" => 8., exponent_mixed_operations: "2^3 + 4^2" => 24., exponent_nested: "2^(3+1)" => 16., + exponent_right_associative: "2^2^3" => 256., + exponent_unary_operand: "2^-1" => 0.5, + + // Implicit multiplication binds like `*`/`/`: tighter than `+`, looser than `^`, left to right + implicit_multiplication_constant: "2pi" => 2. * std::f64::consts::PI, + implicit_multiplication_before_addition: "2pi + 1" => 2. * std::f64::consts::PI + 1., + implicit_multiplication_shares_division: "1/2pi" => std::f64::consts::PI / 2., + implicit_multiplication_left_to_right: "6/2pi" => 3. * std::f64::consts::PI, + implicit_multiplication_power_operand: "2pi^2" => 2. * std::f64::consts::PI.powi(2), + implicit_multiplication_function: "2sqrt(4)" => 4., + implicit_multiplication_excludes_unary_minus: "2 -3" => -1., // Factorial (postfix !) factorial_simple: "5!" => 120., diff --git a/libraries/math-parser/src/parser.rs b/libraries/math-parser/src/parser.rs index 792c3be46a..e44464cfa3 100644 --- a/libraries/math-parser/src/parser.rs +++ b/libraries/math-parser/src/parser.rs @@ -1,40 +1,47 @@ -use crate::ast::{BinaryOp, Literal, Node, UnaryOp, Unit}; -use crate::context::EvalContext; -use crate::diagnostic::{CompileError, make_compile_error}; +use crate::ast::{BinaryOp, Literal, Node, UnaryOp}; use crate::lexer::{Lexer, Span, Token}; -use crate::value::{Complex, Number, Value}; -use chumsky::container::Seq; -use chumsky::input::{BorrowInput, ValueInput}; +use chumsky::error::LabelError; +use chumsky::input::ValueInput; use chumsky::{Parser, prelude::*}; -use lazy_static::lazy_static; -use num_complex::ComplexFloat; -use std::num::{ParseFloatError, ParseIntError}; -use thiserror::Error; +use std::fmt; + +/// One message per parse failure, each tagged with its byte range in the source expression. +#[derive(Debug)] +pub struct ParseError(Vec); + +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(()) + } +} + +impl std::error::Error for ParseError {} impl Node { - pub fn try_parse_from_str(src: &str) -> Result { - let tokens = Lexer::new(src); + pub fn try_parse_from_str(src: &str) -> Result { + // Parse with zero-cost errors first (several times faster), then re-parse invalid input with rich errors to build the messages + if let Ok(ast) = parser::().parse(Lexer::new(src)).into_result() { + return Ok(ast); + } - match parser().parse(tokens).into_result() { + match parser::>>().parse(Lexer::new(src)).into_result() { Ok(ast) => Ok(ast), - Err(parse_errs) => { - let errs = parse_errs.into_iter().map(|e| { - let primary = e.span(); - let mut secondary = Vec::new(); - for (msg, ctx_span) in e.contexts() { - secondary.push((msg.to_string(), *ctx_span)); - } - (e.to_string(), *primary, secondary) - }); - Err(make_compile_error("expression", src, errs)) - } + Err(parse_errs) => Err(ParseError(parse_errs.into_iter().map(|e| format!("{e} at {}", e.span())).collect())), } } } -pub fn parser<'src, I>() -> impl Parser<'src, I, Node, extra::Err, Span>>> +pub fn parser<'src, I, E>() -> impl Parser<'src, I, Node, E> where I: ValueInput<'src, Token = Token<'src>, Span = Span>, + E: extra::ParserExtra<'src, I>, + E::Error: LabelError<'src, I, &'static str>, { recursive(|expr| { let constant = select! { @@ -44,32 +51,27 @@ where let args = expr.clone().separated_by(just(Token::Comma)).collect::>().delimited_by(just(Token::LParen), just(Token::RParen)); - let if_expr = just(Token::If) - .ignore_then(args.clone()) // Parses (cond, a, b) - .try_map(|args: Vec, span| { - if args.len() != 3 { - return Err(Rich::custom(span, "Expected 3 arguments in if(cond, a, b)")); - } - let mut iter = args.into_iter(); - let cond = iter.next().unwrap(); - let if_b = iter.next().unwrap(); - let else_b = iter.next().unwrap(); - Ok(Node::Conditional { - condition: Box::new(cond), - if_block: Box::new(if_b), - else_block: Box::new(else_b), - }) - } - ); + let if_expr = just(Token::If).ignore_then(args.clone()).try_map(|args: Vec, span| { + let [condition, if_block, else_block] = <[Node; 3]>::try_from(args).map_err(|_| LabelError::::expected_found(["3 arguments in if(condition, a, b)"], None, span))?; + + Ok(Node::Conditional { + condition: Box::new(condition), + if_block: Box::new(if_block), + else_block: Box::new(else_block), + }) + }); let ident = select! {Token::Ident(s) => s}.labelled("ident"); - let call = ident.then(args).map(|(name, args): (&str, Vec)| Node::FnCall { name: name.to_string(), expr: args }); + // An ident followed by parenthesized args is a function call, otherwise a variable + let call_or_var = ident.then(args.or_not()).map(|(name, args): (&str, Option>)| match args { + Some(args) => Node::FnCall { name: name.to_string(), expr: args }, + None => Node::Var(name.to_string()), + }); let parens = expr.clone().delimited_by(just(Token::LParen), just(Token::RParen)); - let var = ident.map(|s| Node::Var(s.to_string())); - let atom = choice((constant, if_expr, call, parens, var)).labelled("atom").boxed(); + let atom = choice((constant, if_expr, call_or_var, parens)).labelled("atom"); let add_op = choice((just(Token::Plus).to(BinaryOp::Add), just(Token::Minus).to(BinaryOp::Sub))); let mul_op = choice((just(Token::Star).to(BinaryOp::Mul), just(Token::Slash).to(BinaryOp::Div), just(Token::Modulo).to(BinaryOp::Modulo))); @@ -87,87 +89,75 @@ where )); // Postfix factorial: expr! → UnaryOp::Fac - let postfix = atom - .clone() - .foldl(just(Token::Bang).repeated(), |expr, _| Node::UnaryOp { - op: UnaryOp::Fac, - expr: Box::new(expr), - }) - .boxed(); - - let pow = postfix.clone().foldl( - pow_op - .then(unary_op.clone().repeated().foldr(postfix, |op, expr| Node::UnaryOp { op, expr: Box::new(expr) }).boxed()) - .repeated(), - |lhs, (op, rhs)| Node::BinOp { - lhs: Box::new(lhs), - op, - rhs: Box::new(rhs), - }, - ); - - let unary = unary_op.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), + let postfix = atom.clone().foldl(just(Token::Bang).repeated(), |expr, _| Node::UnaryOp { + op: UnaryOp::Fac, + expr: Box::new(expr), + }); + + // Exponentiation is right-associative (`2^2^3` is `2^(2^3)`); the exponent may carry unary signs like `2^-3`. + let pow = recursive(|pow| { + let exponent = unary_op.clone().repeated().foldr(pow, |op, expr| Node::UnaryOp { op, expr: Box::new(expr) }); + postfix.clone().then(pow_op.ignore_then(exponent).or_not()).map(|(base, exponent)| match exponent { + Some(exponent) => Node::BinOp { + lhs: Box::new(base), + op: BinaryOp::Pow, + rhs: Box::new(exponent), + }, + None => base, }) - .boxed(); + }); - let add = product.clone().foldl(add_op.then(product).repeated(), |lhs, (op, rhs)| Node::BinOp { + let unary = unary_op.repeated().foldr(pow.clone(), |op, expr| Node::UnaryOp { op, expr: Box::new(expr) }); + + // Juxtaposed factors like `2pi` or `2sqrt(4)` multiply implicitly at the same precedence as `*` and `/`. + // The implicit right operand is a `pow`, not a full unary, so a following `-` stays a subtraction (`2 -3` means `2 - 3`). + let implicit_mul = pow.map(|rhs| (BinaryOp::Mul, rhs)); + let product = unary.clone().foldl(choice((mul_op.then(unary), implicit_mul)).repeated(), |lhs, (op, rhs)| Node::BinOp { lhs: Box::new(lhs), op, rhs: Box::new(rhs), }); - let cmp = add.clone().foldl(cmp_op.then(add).repeated(), |lhs: Node, (op, rhs)| Node::BinOp { + let add = product.clone().foldl(add_op.then(product).repeated(), |lhs, (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 / 0.0) results, preserving the existing semantics. - let chained_cmp = cmp.clone().foldl(cmp.repeated(), |lhs, rhs| Node::BinOp { + let cmp = add.clone().foldl(cmp_op.then(add).repeated(), |lhs: Node, (op, rhs)| Node::BinOp { lhs: Box::new(lhs), - op: BinaryOp::Mul, + op, rhs: Box::new(rhs), }); - let and = chained_cmp.clone().foldl(and_op.then(chained_cmp).repeated(), |lhs, (op, rhs)| Node::BinOp { + let and = cmp.clone().foldl(and_op.then(cmp).repeated(), |lhs, (op, rhs)| Node::BinOp { lhs: Box::new(lhs), op, rhs: Box::new(rhs), }); - let or = and.clone().foldl(or_op.then(and).repeated(), |lhs, (op, rhs)| Node::BinOp { + and.clone().foldl(or_op.then(and).repeated(), |lhs, (op, rhs)| Node::BinOp { lhs: Box::new(lhs), op, rhs: Box::new(rhs), - }); - - or + }) }) } #[cfg(test)] mod tests { use super::*; + use crate::value::Complex; + macro_rules! test_parser { ($($name:ident: $input:expr_2021 => $expected:expr_2021),* $(,)?) => { $( #[test] fn $name() { - let result = match Node::try_parse_from_str($input){ + let result = match Node::try_parse_from_str($input) { Ok(expr) => expr, - Err(err) => { - err.print(); - panic!(concat!("failed to parse `", $input, "`")); - } + Err(err) => panic!("failed to parse `{}`: {err}", $input), }; assert_eq!(result, $expected); } diff --git a/libraries/math-parser/src/value.rs b/libraries/math-parser/src/value.rs index 74184c0575..e10833ec38 100644 --- a/libraries/math-parser/src/value.rs +++ b/libraries/math-parser/src/value.rs @@ -1,6 +1,4 @@ use crate::ast::{BinaryOp, UnaryOp}; -use num_complex::ComplexFloat; -use std::f64::consts::PI; pub type Complex = num_complex::Complex; @@ -123,7 +121,7 @@ impl Number { } (Number::Real(lhs), Number::Complex(rhs)) => { - let lhs_complex = Complex::new(lhs, 0.0); + let lhs_complex = Complex::new(lhs, 0.); let result = match op { BinaryOp::Add => lhs_complex + rhs, BinaryOp::Sub => lhs_complex - rhs, @@ -136,7 +134,7 @@ impl Number { } (Number::Complex(lhs), Number::Real(rhs)) => { - let rhs_complex = Complex::new(rhs, 0.0); + let rhs_complex = Complex::new(rhs, 0.); let result = match op { BinaryOp::Add => lhs + rhs_complex, BinaryOp::Sub => lhs - rhs_complex, diff --git a/node-graph/nodes/math/src/lib.rs b/node-graph/nodes/math/src/lib.rs index 9a0ebdd3bf..0b0e922d3c 100644 --- a/node-graph/nodes/math/src/lib.rs +++ b/node-graph/nodes/math/src/lib.rs @@ -55,8 +55,8 @@ fn math( let node = match ast::Node::try_parse_from_str(expression) { Ok(expr) => expr, - Err(_) => { - warn!("Invalid expression: `{expression}`"); + Err(e) => { + warn!("Invalid expression: `{expression}`\n{e}"); return Item::from_parts(T::from(0.).unwrap(), attributes); } };