Skip to content

Commit

Permalink
Add math-parser library (#2033)
Browse files Browse the repository at this point in the history
* start of parser

* ops forgot

* reorder files and work on executer

* start of parser

* ops forgot

* reorder files and work on executer

* Cleanup and fix tests

* Integrate into the editor

* added unit checking at parse time

* fix tests

* fix issues

* fix editor intergration

* update pest grammer to support units

* units should be working, need to set up tests to know

* make unit type store exponants as i32

* remove scale, insted just multiply the literal by the scale

* unit now contains empty unit,remove options

* add more tests and implement almost all unary operators

* add evaluation context and variables

* function calling, api might be refined later

* add constants, change function call to not be as built into the parser
and add tests

* add function definitions

* remove meval

* remove raw-rs from workspace

* add support for numberless units

* fix unit handleing logic, add some "unit" tests(haha)

* make it so units cant do implcit mul with idents

* add bench and better tests

* fix editor api

* remove old test

* change hashmap context to use deref

* change constants to use hashmap instad of function

---------

Co-authored-by: hypercube <[email protected]>
Co-authored-by: Keavon Chambers <[email protected]>
  • Loading branch information
3 people authored Nov 21, 2024
1 parent 51ce51e commit 9fb4947
Show file tree
Hide file tree
Showing 14 changed files with 1,260 additions and 94 deletions.
75 changes: 65 additions & 10 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 2 additions & 4 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ members = [
"libraries/dyn-any",
"libraries/path-bool",
"libraries/bezier-rs",
"libraries/math-parser",
"website/other/bezier-rs-demos/wasm",
]
exclude = ["node-graph/gpu-compiler"]
Expand All @@ -31,6 +32,7 @@ graph-craft = { path = "node-graph/graph-craft", features = ["serde"] }
wgpu-executor = { path = "node-graph/wgpu-executor" }
bezier-rs = { path = "libraries/bezier-rs", features = ["dyn-any"] }
path-bool = { path = "libraries/path-bool", default-features = false }
math-parser = { path = "libraries/math-parser" }
node-macro = { path = "node-graph/node-macro" }

# Workspace dependencies
Expand Down Expand Up @@ -77,7 +79,6 @@ glam = { version = "0.28", default-features = false, features = ["serde"] }
base64 = "0.22"
image = { version = "0.25", default-features = false, features = ["png"] }
rustybuzz = "0.17"
meval = "0.2"
spirv = "0.3"
fern = { version = "0.6", features = ["colored"] }
num_enum = "0.7"
Expand All @@ -94,9 +95,6 @@ syn = { version = "2.0", default-features = false, features = [
] }
kurbo = { version = "0.11.0", features = ["serde"] }

[patch.crates-io]
meval = { git = "https://github.com/Titaniumtown/meval-rs" }

[profile.dev]
opt-level = 1

Expand Down
2 changes: 1 addition & 1 deletion frontend/wasm/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ js-sys = { workspace = true }
wasm-bindgen-futures = { workspace = true }
bezier-rs = { workspace = true }
glam = { workspace = true }
meval = { workspace = true }
math-parser = { workspace = true }
wgpu = { workspace = true, features = [
"fragile-send-sync-non-atomic-wasm",
] } # We don't have wgpu on multiple threads (yet) https://github.com/gfx-rs/wgpu/blob/trunk/CHANGELOG.md#wgpu-types-now-send-sync-on-wasm
Expand Down
90 changes: 11 additions & 79 deletions frontend/wasm/src/editor_api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -910,72 +910,17 @@ impl EditorHandle {

#[wasm_bindgen(js_name = evaluateMathExpression)]
pub fn evaluate_math_expression(expression: &str) -> Option<f64> {
// TODO: Rewrite our own purpose-built math expression parser that supports unit conversions.

let mut context = meval::Context::new();
context.var("tau", std::f64::consts::TAU);
context.func("log", f64::log10);
context.func("log10", f64::log10);
context.func("log2", f64::log2);

// Insert asterisks where implicit multiplication is used in the expression string
let expression = implicit_multiplication_preprocess(expression);

meval::eval_str_with_context(expression, &context).ok()
}

// Modified from this public domain snippet: <https://gist.github.com/Titaniumtown/c181be5d06505e003d8c4d1e372684ff>
// Discussion: <https://github.com/rekka/meval-rs/issues/28#issuecomment-1826381922>
pub fn implicit_multiplication_preprocess(expression: &str) -> String {
let function = expression.to_lowercase().replace("log10(", "log(").replace("log2(", "logtwo(").replace("pi", "π").replace("tau", "τ");
let valid_variables: Vec<char> = "eπτ".chars().collect();
let letters: Vec<char> = ('a'..='z').chain('A'..='Z').collect();
let numbers: Vec<char> = ('0'..='9').collect();
let function_chars: Vec<char> = function.chars().collect();
let mut output_string: String = String::new();
let mut prev_chars: Vec<char> = Vec::new();

for c in function_chars {
let mut add_asterisk: bool = false;
let prev_chars_len = prev_chars.len();

let prev_prev_char = if prev_chars_len >= 2 { *prev_chars.get(prev_chars_len - 2).unwrap() } else { ' ' };

let prev_char = if prev_chars_len >= 1 { *prev_chars.get(prev_chars_len - 1).unwrap() } else { ' ' };

let c_letters_var = letters.contains(&c) | valid_variables.contains(&c);
let prev_letters_var = valid_variables.contains(&prev_char) | letters.contains(&prev_char);

if prev_char == ')' {
if (c == '(') | numbers.contains(&c) | c_letters_var {
add_asterisk = true;
}
} else if c == '(' {
if (valid_variables.contains(&prev_char) | (')' == prev_char) | numbers.contains(&prev_char)) && !letters.contains(&prev_prev_char) {
add_asterisk = true;
}
} else if numbers.contains(&prev_char) {
if (c == '(') | c_letters_var {
add_asterisk = true;
}
} else if letters.contains(&c) {
if numbers.contains(&prev_char) | (valid_variables.contains(&prev_char) && valid_variables.contains(&c)) {
add_asterisk = true;
}
} else if (numbers.contains(&c) | c_letters_var) && prev_letters_var {
add_asterisk = true;
}

if add_asterisk {
output_string += "*";
}

prev_chars.push(c);
output_string += &c.to_string();
}

// We have to convert the Greek symbols back to ASCII because meval doesn't support unicode symbols as context constants
output_string.replace("logtwo(", "log2(").replace('π', "pi").replace('τ', "tau")
let value = math_parser::evaluate(expression)
.inspect_err(|err| error!("Math parser error on \"{expression}\": {err}"))
.ok()?
.0
.inspect_err(|err| error!("Math evaluate error on \"{expression}\": {err} "))
.ok()?;
let Some(real) = value.as_real() else {
error!("{value} was not a real; skipping.");
return None;
};
Some(real)
}

/// Helper function for calling JS's `requestAnimationFrame` with the given closure
Expand Down Expand Up @@ -1066,16 +1011,3 @@ fn auto_save_all_documents() {
}
});
}

#[test]
fn implicit_multiplication_preprocess_tests() {
assert_eq!(implicit_multiplication_preprocess("2pi"), "2*pi");
assert_eq!(implicit_multiplication_preprocess("sin(2pi)"), "sin(2*pi)");
assert_eq!(implicit_multiplication_preprocess("2sin(pi)"), "2*sin(pi)");
assert_eq!(implicit_multiplication_preprocess("2sin(3(4 + 5))"), "2*sin(3*(4 + 5))");
assert_eq!(implicit_multiplication_preprocess("3abs(-4)"), "3*abs(-4)");
assert_eq!(implicit_multiplication_preprocess("-1(4)"), "-1*(4)");
assert_eq!(implicit_multiplication_preprocess("(-1)4"), "(-1)*4");
assert_eq!(implicit_multiplication_preprocess("(((-1)))(4)"), "(((-1)))*(4)");
assert_eq!(implicit_multiplication_preprocess("2sin(pi) + 2cos(tau)"), "2*sin(pi) + 2*cos(tau)");
}
23 changes: 23 additions & 0 deletions libraries/math-parser/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
[package]
name = "math-parser"
version = "0.0.0"
rust-version = "1.79"
edition = "2021"
authors = ["Graphite Authors <[email protected]>"]
description = "Parser for Graphite style mathematics expressions"
license = "MIT OR Apache-2.0"

[dependencies]
pest = "2.7"
pest_derive = "2.7.11"
thiserror = "1"
lazy_static = "1.5"
num-complex = "0.4"
log = { workspace = true }

[dev-dependencies]
criterion = "0.5"

[[bench]]
name = "bench"
harness = false
50 changes: 50 additions & 0 deletions libraries/math-parser/benches/bench.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
use criterion::{black_box, criterion_group, criterion_main, Criterion};

use math_parser::ast;
use math_parser::context::EvalContext;

macro_rules! generate_benchmarks {
($( $input:expr ),* $(,)?) => {
fn parsing_bench(c: &mut Criterion) {
$(
c.bench_function(concat!("parse ", $input), |b| {
b.iter(|| {
let _ = black_box(ast::Node::from_str($input)).unwrap();
});
});
)*
}

fn evaluation_bench(c: &mut Criterion) {
$(
let expr = ast::Node::from_str($input).unwrap().0;
let context = EvalContext::default();

c.bench_function(concat!("eval ", $input), |b| {
b.iter(|| {
let _ = black_box(expr.eval(&context));
});
});
)*
}

criterion_group!(benches, parsing_bench, evaluation_bench);
criterion_main!(benches);
};
}

generate_benchmarks! {
"(3 * (4 + sqrt(25)) - cos(pi/3) * (2^3)) + 5 * e", // Mixed nested functions, constants, and operations
"((5 + 2 * (3 - sqrt(49)))^2) / (1 + sqrt(16)) + tau / 2", // Complex nested expression with constants
"log(100, 10) + (5 * sin(pi/4) + sqrt(81)) / (2 * phi)", // Logarithmic and trigonometric functions
"(sqrt(144) * 2 + 5) / (3 * (4 - sin(pi / 6))) + e^2", // Combined square root, trigonometric, and exponential operations
"cos(2 * pi) + tan(pi / 3) * log(32, 2) - sqrt(256)", // Multiple trigonometric and logarithmic functions
"(10 * (3 + 2) - 8 / 2)^2 + 7 * (2^4) - sqrt(225) + phi", // Mixed arithmetic with constants
"(5^2 + 3^3) * (sqrt(81) + sqrt(64)) - tau * log(1000, 10)", // Power and square root with constants
"((8 * sqrt(49) - 2 * e) + log(256, 2) / (2 + cos(pi))) * 1.5", // Nested functions and constants
"(tan(pi / 4) + 5) * (3 + sqrt(36)) / (log(1024, 2) - 4)", // Nested functions with trigonometry and logarithm
"((3 * e + 2 * sqrt(100)) - cos(tau / 4)) * log(27, 3) + phi", // Mixed constant usage and functions
"(sqrt(100) + 5 * sin(pi / 6) - 8 / log(64, 2)) + e^(1.5)", // Complex mix of square root, division, and exponentiation
"((sin(pi/2) + cos(0)) * (e^2 - 2 * sqrt(16))) / (log(100, 10) + pi)", // Nested trigonometric, exponential, and logarithmic functions
"(5 * (7 + sqrt(121)) - (log(243, 3) * phi)) + 3^5 / tau", //
}
Loading

0 comments on commit 9fb4947

Please sign in to comment.