mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-19 19:08:05 +08:00
Merge origin/master into the async record refactor
Scaffolding merge for the reconcile; the final series to master is authored fresh. Rank plumbing resolves to our axis-IR model, the node macro and the LaneSource render walk stay ours, master's vector restructure and gradient vocabulary are adopted, and the paint and appearance adoption is deliberately deferred behind our fill and stroke markers.
This commit is contained in:
@@ -1,3 +1,5 @@
|
||||
Copyright (c) Graphite contributors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
|
||||
@@ -8,11 +8,9 @@ description = "Parser for Graphite style mathematics expressions"
|
||||
license = "MIT OR Apache-2.0"
|
||||
|
||||
[dependencies]
|
||||
pest = "2.7"
|
||||
pest_derive = "2.7"
|
||||
thiserror = "2.0"
|
||||
lazy_static = "1.5"
|
||||
num-complex = "0.4"
|
||||
chumsky = { version = "0.10", default-features = false, features = ["std"] }
|
||||
|
||||
[dev-dependencies]
|
||||
criterion = { workspace = true }
|
||||
|
||||
@@ -9,7 +9,7 @@ macro_rules! generate_benchmarks {
|
||||
$(
|
||||
c.bench_function(concat!("parse ", $input), |b| {
|
||||
b.iter(|| {
|
||||
let _ = black_box(ast::Node::try_parse_from_str($input)).unwrap();
|
||||
let _ = black_box(ast::Node::try_parse_from_str($input));
|
||||
});
|
||||
});
|
||||
)*
|
||||
@@ -17,7 +17,10 @@ macro_rules! generate_benchmarks {
|
||||
|
||||
fn evaluation_bench(c: &mut Criterion) {
|
||||
$(
|
||||
let expr = ast::Node::try_parse_from_str($input).unwrap().0;
|
||||
let expr = match ast::Node::try_parse_from_str($input) {
|
||||
Ok(expr) => expr,
|
||||
Err(err) => panic!("failed to parse `{}`: {err}", $input),
|
||||
};
|
||||
let context = EvalContext::default();
|
||||
|
||||
c.bench_function(concat!("eval ", $input), |b| {
|
||||
|
||||
@@ -37,7 +37,7 @@ impl Unit {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum Literal {
|
||||
Float(f64),
|
||||
Complex(Complex),
|
||||
@@ -54,8 +54,19 @@ pub enum BinaryOp {
|
||||
Add,
|
||||
Sub,
|
||||
Mul,
|
||||
/// Logical AND (nonzero treated as true, returns 1. or 0.)
|
||||
And,
|
||||
Div,
|
||||
/// Logical OR (nonzero treated as true, returns 1. or 0.)
|
||||
Or,
|
||||
Modulo,
|
||||
Pow,
|
||||
Leq,
|
||||
Lt,
|
||||
Geq,
|
||||
Gt,
|
||||
Neq,
|
||||
Eq,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Clone, Copy)]
|
||||
@@ -63,6 +74,7 @@ pub enum UnaryOp {
|
||||
Neg,
|
||||
Sqrt,
|
||||
Fac,
|
||||
Not,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
@@ -72,4 +84,5 @@ pub enum Node {
|
||||
FnCall { name: String, expr: Vec<Node> },
|
||||
BinOp { lhs: Box<Node>, op: BinaryOp, rhs: Box<Node> },
|
||||
UnaryOp { expr: Box<Node>, op: UnaryOp },
|
||||
Conditional { condition: Box<Node>, if_block: Box<Node>, else_block: Box<Node> },
|
||||
}
|
||||
|
||||
@@ -1,122 +1,421 @@
|
||||
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 num_complex::ComplexFloat;
|
||||
use std::f64::consts::{LN_2, PI};
|
||||
|
||||
type FunctionImplementation = Box<dyn Fn(&[Value]) -> Option<Value> + Send + Sync>;
|
||||
lazy_static! {
|
||||
pub static ref DEFAULT_FUNCTIONS: HashMap<&'static str, FunctionImplementation> = {
|
||||
let mut map: HashMap<&'static str, FunctionImplementation> = HashMap::new();
|
||||
pub type BuiltinFunction = fn(&[Value]) -> Option<Value>;
|
||||
|
||||
map.insert(
|
||||
"sin",
|
||||
Box::new(|values| match values {
|
||||
[Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(real.sin()))),
|
||||
[Value::Number(Number::Complex(complex))] => Some(Value::Number(Number::Complex(complex.sin()))),
|
||||
_ => None,
|
||||
}),
|
||||
);
|
||||
/// Truncates both operands to nonnegative integers for `gcd`/`lcm`, or `None` when either is non-finite or beyond f64's exactly-representable integer range.
|
||||
fn integer_operands(a: f64, b: f64) -> Option<(u64, u64)> {
|
||||
// The largest magnitude below which every integer is exactly representable in f64
|
||||
const EXACT_INTEGER_LIMIT: f64 = (1_u64 << f64::MANTISSA_DIGITS) as f64;
|
||||
|
||||
map.insert(
|
||||
"cos",
|
||||
Box::new(|values| match values {
|
||||
[Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(real.cos()))),
|
||||
[Value::Number(Number::Complex(complex))] => Some(Value::Number(Number::Complex(complex.cos()))),
|
||||
_ => None,
|
||||
}),
|
||||
);
|
||||
|
||||
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,
|
||||
}),
|
||||
);
|
||||
|
||||
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(
|
||||
"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(
|
||||
"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(
|
||||
"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(
|
||||
"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(
|
||||
"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()))),
|
||||
_ => None,
|
||||
}),
|
||||
);
|
||||
|
||||
map
|
||||
};
|
||||
let (a, b) = (a.trunc(), b.trunc());
|
||||
if !a.is_finite() || !b.is_finite() || a.abs() > EXACT_INTEGER_LIMIT || b.abs() > EXACT_INTEGER_LIMIT {
|
||||
return None;
|
||||
}
|
||||
Some(((a as i64).unsigned_abs(), (b as i64).unsigned_abs()))
|
||||
}
|
||||
|
||||
fn euclidean_gcd(mut x: u64, mut y: u64) -> u64 {
|
||||
while y != 0 {
|
||||
(x, y) = (y, x % y);
|
||||
}
|
||||
x
|
||||
}
|
||||
|
||||
/// Looks up a built-in math function by name, returning a plain function pointer so dispatch avoids hashing and dynamic allocation.
|
||||
pub fn builtin_function(name: &str) -> Option<BuiltinFunction> {
|
||||
Some(match name {
|
||||
"sin" => |values| match values {
|
||||
[Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(real.sin()))),
|
||||
[Value::Number(Number::Complex(complex))] => Some(Value::Number(Number::Complex(complex.sin()))),
|
||||
_ => None,
|
||||
},
|
||||
|
||||
"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
|
||||
"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
|
||||
"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
|
||||
"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
|
||||
"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
|
||||
"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
|
||||
"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
|
||||
"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))] => {
|
||||
// Odd integer roots of negative reals are real, which powf alone would report as NaN
|
||||
let root = if *x < 0. && n.rem_euclid(2.) == 1. { -(-x).powf(1. / *n) } else { x.powf(1. / *n) };
|
||||
Some(Value::Number(Number::Real(root)))
|
||||
}
|
||||
[Value::Number(Number::Complex(x)), Value::Number(Number::Real(n))] => Some(Value::Number(Number::Complex(x.powf(1. / *n)))),
|
||||
_ => None,
|
||||
},
|
||||
|
||||
"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,
|
||||
},
|
||||
|
||||
"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
|
||||
"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. / 3.)))),
|
||||
_ => None,
|
||||
},
|
||||
|
||||
// Geometry Functions
|
||||
"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
|
||||
"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. {
|
||||
1.
|
||||
} else if *real < 0. {
|
||||
-1.
|
||||
} else {
|
||||
0.
|
||||
};
|
||||
Some(Value::Number(Number::Real(s)))
|
||||
}
|
||||
_ => None,
|
||||
},
|
||||
|
||||
"gcd" => |values| match values {
|
||||
[Value::Number(Number::Real(a)), Value::Number(Number::Real(b))] => {
|
||||
let gcd = integer_operands(*a, *b).map_or(f64::NAN, |(x, y)| euclidean_gcd(x, y) as f64);
|
||||
Some(Value::Number(Number::Real(gcd)))
|
||||
}
|
||||
_ => None,
|
||||
},
|
||||
|
||||
"lcm" => |values| match values {
|
||||
[Value::Number(Number::Real(a)), Value::Number(Number::Real(b))] => {
|
||||
let Some((x, y)) = integer_operands(*a, *b) else {
|
||||
return Some(Value::Number(Number::Real(f64::NAN)));
|
||||
};
|
||||
if x == 0 || y == 0 {
|
||||
return Some(Value::Number(Number::Real(0.)));
|
||||
}
|
||||
|
||||
// Multiply in f64 so huge results can't overflow the integer range
|
||||
let lcm = (x / euclidean_gcd(x, y)) as f64 * y as f64;
|
||||
Some(Value::Number(Number::Real(lcm)))
|
||||
}
|
||||
_ => None,
|
||||
},
|
||||
|
||||
// Complex Number Functions
|
||||
"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.))),
|
||||
_ => 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. } else { PI };
|
||||
Some(Value::Number(Number::Real(angle)))
|
||||
}
|
||||
_ => None,
|
||||
},
|
||||
|
||||
// Logical Functions
|
||||
"isnan" => |values| match values {
|
||||
[Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(if real.is_nan() { 1. } else { 0. }))),
|
||||
_ => None,
|
||||
},
|
||||
|
||||
"eq" => |values| match values {
|
||||
[Value::Number(a), Value::Number(b)] => Some(Value::Number(Number::Real(if a == b { 1. } else { 0. }))),
|
||||
_ => None,
|
||||
},
|
||||
|
||||
"greater" => |values| match values {
|
||||
[Value::Number(Number::Real(a)), Value::Number(Number::Real(b))] => Some(Value::Number(Number::Real(if a > b { 1. } else { 0. }))),
|
||||
_ => None,
|
||||
},
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use crate::ast::{Literal, Node};
|
||||
use crate::constants::DEFAULT_FUNCTIONS;
|
||||
use crate::ast::{BinaryOp, Literal, Node};
|
||||
use crate::constants::builtin_function;
|
||||
use crate::context::{EvalContext, FunctionProvider, ValueProvider};
|
||||
use crate::value::{Number, Value};
|
||||
use thiserror::Error;
|
||||
@@ -11,8 +11,12 @@ pub enum EvalError {
|
||||
|
||||
#[error("Missing function: {0}")]
|
||||
MissingFunction(String),
|
||||
#[error("Wrong type for function call")]
|
||||
|
||||
#[error("Wrong argument types for function call")]
|
||||
TypeError,
|
||||
|
||||
#[error("Unsupported operand types for operator")]
|
||||
OperatorTypeError,
|
||||
}
|
||||
|
||||
impl Node {
|
||||
@@ -24,22 +28,46 @@ 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::OperatorTypeError)?)),
|
||||
},
|
||||
Node::UnaryOp { expr, op } => match expr.eval(context)? {
|
||||
Value::Number(num) => Ok(Value::Number(num.unary_op(*op))),
|
||||
},
|
||||
Node::Var(name) => context.get_value(name).ok_or_else(|| EvalError::MissingValue(name.clone())),
|
||||
Node::FnCall { name, expr } => {
|
||||
let values = expr.iter().map(|expr| expr.eval(context)).collect::<Result<Vec<Value>, EvalError>>()?;
|
||||
if let Some(function) = DEFAULT_FUNCTIONS.get(&name.as_str()) {
|
||||
function(&values).ok_or(EvalError::TypeError)
|
||||
} else if let Some(val) = context.run_function(name, &values) {
|
||||
Ok(val)
|
||||
// Arguments land in a stack buffer when they fit (builtins take at most 5), avoiding a heap allocation per call
|
||||
let mut stack_values = [Value::from_f64(0.); 5];
|
||||
let heap_values: Vec<Value>;
|
||||
let values: &[Value] = if expr.len() <= stack_values.len() {
|
||||
for (slot, argument) in stack_values.iter_mut().zip(expr) {
|
||||
*slot = argument.eval(context)?;
|
||||
}
|
||||
&stack_values[..expr.len()]
|
||||
} else {
|
||||
context.get_value(name).ok_or_else(|| EvalError::MissingFunction(name.to_string()))
|
||||
heap_values = expr.iter().map(|argument| argument.eval(context)).collect::<Result<Vec<Value>, EvalError>>()?;
|
||||
&heap_values
|
||||
};
|
||||
|
||||
if let Some(function) = builtin_function(name) {
|
||||
function(values).ok_or(EvalError::TypeError)
|
||||
} else if let Some(val) = context.run_function(name, values) {
|
||||
Ok(val)
|
||||
} else if let Some(Value::Number(value)) = context.get_value(name)
|
||||
&& let [Value::Number(argument)] = values
|
||||
{
|
||||
// A known value applied to one argument is implicit multiplication, so `x(2)` matches `2(3)` and `i(16)`
|
||||
Ok(Value::Number(value.binary_op(BinaryOp::Mul, *argument).ok_or(EvalError::OperatorTypeError)?))
|
||||
} else {
|
||||
Err(EvalError::MissingFunction(name.to_string()))
|
||||
}
|
||||
}
|
||||
Node::Conditional { condition, if_block, else_block } => {
|
||||
// A NaN condition yields NaN rather than arbitrarily picking a branch
|
||||
let Value::Number(number) = condition.eval(context)?;
|
||||
let Some(condition) = number.as_bool() else { return Ok(Value::from_f64(f64::NAN)) };
|
||||
|
||||
if condition { if_block.eval(context) } else { else_block.eval(context) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -47,9 +75,37 @@ impl Node {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::ast::{BinaryOp, Literal, Node, UnaryOp};
|
||||
use crate::context::{EvalContext, ValueMap};
|
||||
use crate::context::{EvalContext, NothingMap, ValueProvider};
|
||||
use crate::value::Value;
|
||||
|
||||
struct SingleValue(f64);
|
||||
|
||||
impl ValueProvider for SingleValue {
|
||||
fn get_value(&self, name: &str) -> Option<Value> {
|
||||
(name == "x").then(|| Value::from_f64(self.0))
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn known_value_with_one_argument_multiplies() {
|
||||
// `x(2)` juxtaposes like `2(3)` and `i(16)` instead of silently discarding the argument
|
||||
let call = Node::FnCall {
|
||||
name: "x".to_string(),
|
||||
expr: vec![Node::Lit(Literal::Float(2.))],
|
||||
};
|
||||
let result = call.eval(&EvalContext::new(SingleValue(5.), NothingMap)).unwrap();
|
||||
assert_eq!(result, Value::from_f64(10.));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn known_value_with_multiple_arguments_is_an_error() {
|
||||
let call = Node::FnCall {
|
||||
name: "x".to_string(),
|
||||
expr: vec![Node::Lit(Literal::Float(1.)), Node::Lit(Literal::Float(2.))],
|
||||
};
|
||||
assert!(call.eval(&EvalContext::new(SingleValue(5.), NothingMap)).is_err());
|
||||
}
|
||||
|
||||
macro_rules! eval_tests {
|
||||
($($name:ident: $expected:expr_2021 => $expr:expr_2021),* $(,)?) => {
|
||||
$(
|
||||
|
||||
@@ -1,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" }
|
||||
395
libraries/math-parser/src/lexer.rs
Normal file
395
libraries/math-parser/src/lexer.rs
Normal file
@@ -0,0 +1,395 @@
|
||||
use crate::ast::Literal;
|
||||
use chumsky::input::{Input, ValueInput};
|
||||
use chumsky::span::SimpleSpan;
|
||||
use num_complex::Complex64;
|
||||
use std::fmt;
|
||||
use std::ops::Range;
|
||||
|
||||
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,
|
||||
|
||||
/// 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> {
|
||||
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"),
|
||||
|
||||
Token::Error => f.write_str("<error>"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum Constant {
|
||||
Pi,
|
||||
Tau,
|
||||
E,
|
||||
Phi,
|
||||
Inf,
|
||||
I,
|
||||
G,
|
||||
}
|
||||
|
||||
impl Constant {
|
||||
pub fn value(self) -> Literal {
|
||||
use Constant::*;
|
||||
use std::f64::consts;
|
||||
match self {
|
||||
Pi => Literal::Float(consts::PI),
|
||||
Tau => Literal::Float(consts::TAU),
|
||||
E => Literal::Float(consts::E),
|
||||
Phi => Literal::Float(1.618_033_988_75),
|
||||
Inf => Literal::Float(f64::INFINITY),
|
||||
I => Literal::Complex(Complex64::new(0., 1.)),
|
||||
G => Literal::Float(9.80665),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_name(name: &str) -> Option<Constant> {
|
||||
use Constant::*;
|
||||
Some(match name {
|
||||
"pi" | "π" => Pi,
|
||||
"tau" | "τ" => Tau,
|
||||
"e" => E,
|
||||
"phi" | "φ" => Phi,
|
||||
"inf" | "∞" => Inf,
|
||||
"i" => I,
|
||||
"G" => G,
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Constant {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
use Constant::*;
|
||||
f.write_str(match self {
|
||||
Pi => "pi",
|
||||
Tau => "tau",
|
||||
E => "e",
|
||||
Phi => "phi",
|
||||
Inf => "inf",
|
||||
I => "i",
|
||||
G => "G",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Lexer<'a> {
|
||||
input: &'a str,
|
||||
pos: usize,
|
||||
}
|
||||
|
||||
impl<'a> Lexer<'a> {
|
||||
pub fn new(input: &'a str) -> Self {
|
||||
Self { input, pos: 0 }
|
||||
}
|
||||
|
||||
fn peek(&self) -> Option<char> {
|
||||
self.input[self.pos..].chars().next()
|
||||
}
|
||||
|
||||
fn bump(&mut self) -> Option<char> {
|
||||
let c = self.peek()?;
|
||||
self.pos += c.len_utf8();
|
||||
Some(c)
|
||||
}
|
||||
|
||||
fn consume_while<F>(&mut self, cond: F) -> &'a str
|
||||
where
|
||||
F: Fn(char) -> bool,
|
||||
{
|
||||
let start = self.pos;
|
||||
while self.peek().is_some_and(&cond) {
|
||||
self.bump();
|
||||
}
|
||||
&self.input[start..self.pos]
|
||||
}
|
||||
|
||||
fn consume_digits(&mut self) -> (usize, f64) {
|
||||
let mut value = 0_f64;
|
||||
let mut digits = 0;
|
||||
while let Some(d) = self.peek().and_then(|c| c.to_digit(10)) {
|
||||
value = value * 10. + d as f64;
|
||||
digits += 1;
|
||||
self.bump();
|
||||
}
|
||||
(digits, value)
|
||||
}
|
||||
|
||||
// A numeric literal cannot follow another operand across whitespace (`10 000`, `sqrt(4).5`), only constants/calls/parens may juxtapose
|
||||
fn juxtaposes_with_preceding_operand(&self, literal_start: usize) -> bool {
|
||||
let mut preceding = self.input[..literal_start].trim_end();
|
||||
|
||||
// A `!` run is postfix factorial only when an operand precedes it, otherwise it's a prefix logical not
|
||||
while let Some(rest) = preceding.strip_suffix('!') {
|
||||
preceding = rest.trim_end();
|
||||
}
|
||||
|
||||
preceding.chars().next_back().is_some_and(|c| c.is_alphanumeric() || c == '.' || c == ')' || c == '∞')
|
||||
}
|
||||
|
||||
fn lex_number(&mut self) -> Option<f64> {
|
||||
let start_pos = self.pos;
|
||||
let (int_digits, int_value) = self.consume_digits();
|
||||
let mut got_digit = int_digits > 0;
|
||||
let mut plain_integer = true;
|
||||
|
||||
if self.peek() == Some('.') {
|
||||
self.bump();
|
||||
plain_integer = false;
|
||||
got_digit |= self.consume_digits().0 > 0;
|
||||
}
|
||||
|
||||
if got_digit && matches!(self.peek(), Some('e' | 'E')) {
|
||||
self.bump();
|
||||
plain_integer = false;
|
||||
if matches!(self.peek(), Some('+' | '-')) {
|
||||
self.bump();
|
||||
}
|
||||
if self.consume_digits().0 == 0 {
|
||||
self.pos = start_pos;
|
||||
return None;
|
||||
}
|
||||
}
|
||||
|
||||
// A numeric literal cannot be glued directly to another by a stray decimal point or digit (e.g. `1..5`, `1.5.5`), so reject rather than letting it parse as implicit multiplication
|
||||
if !got_digit || self.peek().is_some_and(|c| c == '.' || c.is_ascii_digit()) || self.juxtaposes_with_preceding_operand(start_pos) {
|
||||
self.pos = start_pos;
|
||||
return None;
|
||||
}
|
||||
|
||||
// Accumulation is exact up to 15 digits; longer or fractional literals get std's correctly-rounded parsing
|
||||
if plain_integer && int_digits <= 15 {
|
||||
return Some(int_value);
|
||||
}
|
||||
self.input[start_pos..self.pos].parse::<f64>().ok()
|
||||
}
|
||||
|
||||
fn skip_ws(&mut self) {
|
||||
self.consume_while(char::is_whitespace);
|
||||
}
|
||||
|
||||
pub fn next_token(&mut self) -> Option<Token<'a>> {
|
||||
self.skip_ws();
|
||||
let start = self.pos;
|
||||
let ch = self.bump()?;
|
||||
|
||||
use Token::*;
|
||||
let tok = match ch {
|
||||
'&' => {
|
||||
if self.peek() == Some('&') {
|
||||
self.bump();
|
||||
AndAnd
|
||||
} else {
|
||||
Error
|
||||
}
|
||||
}
|
||||
'|' => {
|
||||
if self.peek() == Some('|') {
|
||||
self.bump();
|
||||
OrOr
|
||||
} else {
|
||||
Error
|
||||
}
|
||||
}
|
||||
|
||||
'(' => 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 {
|
||||
Error
|
||||
}
|
||||
}
|
||||
|
||||
c if c.is_ascii_digit() || (c == '.' && self.peek().is_some_and(|c| c.is_ascii_digit())) => {
|
||||
self.pos = start;
|
||||
match self.lex_number() {
|
||||
Some(number) => Float(number),
|
||||
// Consume the whole malformed numeric run so the error span covers it and lexing makes forward progress
|
||||
None => {
|
||||
self.pos = start;
|
||||
let mut prev = '\0';
|
||||
while let Some(c) = self.peek() {
|
||||
let part_of_number = c.is_ascii_digit() || c == '.' || c == 'e' || c == 'E' || ((c == '+' || c == '-') && matches!(prev, 'e' | 'E'));
|
||||
if !part_of_number {
|
||||
break;
|
||||
}
|
||||
prev = c;
|
||||
self.bump();
|
||||
}
|
||||
Error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_ => {
|
||||
self.consume_while(|c| c.is_alphanumeric() || c == '_');
|
||||
let ident = &self.input[start..self.pos];
|
||||
|
||||
if ident == "if" {
|
||||
If
|
||||
} else if let Some(lit) = Constant::from_name(ident) {
|
||||
Const(lit)
|
||||
} else if ch.is_alphanumeric() {
|
||||
Ident(ident)
|
||||
} else {
|
||||
Error
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Some(tok)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Iterator for Lexer<'a> {
|
||||
type Item = Token<'a>;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
self.next_token()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'src> Input<'src> for Lexer<'src> {
|
||||
type Token = Token<'src>;
|
||||
type Span = Span;
|
||||
type Cursor = usize; // byte offset inside `input`
|
||||
type MaybeToken = Token<'src>;
|
||||
type Cache = Self;
|
||||
|
||||
#[inline]
|
||||
fn begin(self) -> (Self::Cursor, Self::Cache) {
|
||||
(0, self)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn cursor_location(cursor: &Self::Cursor) -> usize {
|
||||
*cursor
|
||||
}
|
||||
|
||||
#[inline]
|
||||
unsafe fn next_maybe(this: &mut Self::Cache, cursor: &mut Self::Cursor) -> Option<Self::MaybeToken> {
|
||||
this.pos = *cursor;
|
||||
if let Some(tok) = this.next_token() {
|
||||
*cursor = this.pos;
|
||||
Some(tok)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
unsafe fn span(_this: &mut Self::Cache, range: Range<&Self::Cursor>) -> Self::Span {
|
||||
(*range.start..*range.end).into()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'src> ValueInput<'src> for Lexer<'src> {
|
||||
#[inline]
|
||||
unsafe fn next(this: &mut Self::Cache, cursor: &mut Self::Cursor) -> Option<Self::Token> {
|
||||
this.pos = *cursor;
|
||||
if let Some(tok) = this.next_token() {
|
||||
*cursor = this.pos;
|
||||
Some(tok)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,162 +1,347 @@
|
||||
#![allow(unused)]
|
||||
|
||||
pub mod ast;
|
||||
mod constants;
|
||||
pub mod context;
|
||||
pub mod executer;
|
||||
pub mod lexer;
|
||||
pub mod parser;
|
||||
pub mod value;
|
||||
|
||||
use ast::Unit;
|
||||
use context::{EvalContext, ValueMap};
|
||||
use context::EvalContext;
|
||||
use executer::EvalError;
|
||||
use parser::ParseError;
|
||||
use value::Value;
|
||||
|
||||
pub fn evaluate(expression: &str) -> Result<(Result<Value, EvalError>, Unit), ParseError> {
|
||||
pub fn evaluate(expression: &str) -> Result<Result<Value, EvalError>, ParseError> {
|
||||
let expr = ast::Node::try_parse_from_str(expression);
|
||||
let context = EvalContext::default();
|
||||
expr.map(|(node, unit)| (node.eval(&context), unit))
|
||||
expr.map(|node| node.eval(&context))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use ast::Unit;
|
||||
use value::Number;
|
||||
|
||||
const EPSILON: f64 = 1e-10_f64;
|
||||
|
||||
#[test]
|
||||
fn malformed_juxtaposed_numbers_fail_to_parse() {
|
||||
// Two numbers cannot be glued together by a stray decimal point (they must not parse as implicit multiplication).
|
||||
// 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");
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! test_end_to_end{
|
||||
($($name:ident: $input:expr_2021 => ($expected_value:expr_2021, $expected_unit:expr_2021)),* $(,)?) => {
|
||||
#[test]
|
||||
fn unrecognized_characters_fail_to_parse() {
|
||||
// Unrecognized trailing input must be rejected rather than silently dropped after a valid prefix
|
||||
for input in ["2@", "5#", "2 $ 3", "sqrt(4)@", "5 & 3", "5 | 3", "2 = 3"] {
|
||||
assert!(evaluate(input).is_err(), "expected `{input}` to be a parse error");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn juxtaposed_numbers_fail_to_parse() {
|
||||
// Adjacent number literals like digit-grouped `10 000` must not silently multiply
|
||||
for input in ["2 3", "10 000", "1 .5", "sqrt(4).5", "2 3 + 1"] {
|
||||
assert!(evaluate(input).is_err(), "expected `{input}` to be a parse error");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extremely_long_fraction_parses() {
|
||||
let input = format!("0.{}", "1".repeat(320));
|
||||
let value = evaluate(&input).unwrap().unwrap();
|
||||
assert_eq!(value.as_real(), Some(1. / 9.));
|
||||
}
|
||||
|
||||
fn run_end_to_end_test(input: &str, expected_value: Value) {
|
||||
let expr = match ast::Node::try_parse_from_str(input) {
|
||||
Ok(expr) => expr,
|
||||
Err(err) => panic!("failed to parse `{input}`: {err}"),
|
||||
};
|
||||
let context = EvalContext::default();
|
||||
|
||||
let actual_value = match expr.eval(&context) {
|
||||
Ok(v) => v,
|
||||
Err(err) => panic!("failed to evaluate `{input}` because of error {err}"),
|
||||
};
|
||||
|
||||
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() {
|
||||
let expected_value = $expected_value;
|
||||
let expected_unit = $expected_unit;
|
||||
|
||||
let expr = ast::Node::try_parse_from_str($input);
|
||||
let context = EvalContext::default();
|
||||
|
||||
let (actual_value, actual_unit) = expr.map(|(node, unit)| (node.eval(&context), unit)).unwrap();
|
||||
let actual_value = actual_value.unwrap();
|
||||
|
||||
|
||||
assert!(actual_unit == expected_unit, "Expected unit {:?} but found unit {:?}", expected_unit, actual_unit);
|
||||
|
||||
let expected_value = expected_value.into();
|
||||
|
||||
match (actual_value, expected_value) {
|
||||
(Value::Number(Number::Complex(actual_c)), Value::Number(Number::Complex(expected_c))) => {
|
||||
assert!(
|
||||
(actual_c.re.is_infinite() && expected_c.re.is_infinite()) || (actual_c.re - expected_c.re).abs() < EPSILON,
|
||||
"Expected real part {}, but got {}",
|
||||
expected_c.re,
|
||||
actual_c.re
|
||||
);
|
||||
assert!(
|
||||
(actual_c.im.is_infinite() && expected_c.im.is_infinite()) || (actual_c.im - expected_c.im).abs() < EPSILON,
|
||||
"Expected imaginary part {}, but got {}",
|
||||
expected_c.im,
|
||||
actual_c.im
|
||||
);
|
||||
}
|
||||
(Value::Number(Number::Real(actual_f)), Value::Number(Number::Real(expected_f))) => {
|
||||
if actual_f.is_infinite() || expected_f.is_infinite() {
|
||||
assert!(
|
||||
actual_f.is_infinite() && expected_f.is_infinite() && actual_f == expected_f,
|
||||
"Expected infinite value {}, but got {}",
|
||||
expected_f,
|
||||
actual_f
|
||||
);
|
||||
} else if actual_f.is_nan() || expected_f.is_nan() {
|
||||
assert!(actual_f.is_nan() && expected_f.is_nan(), "Expected NaN, but got {}", actual_f);
|
||||
} else {
|
||||
assert!((actual_f - expected_f).abs() < EPSILON, "Expected {}, but got {}", expected_f, actual_f);
|
||||
}
|
||||
}
|
||||
// Handle mismatched types
|
||||
_ => panic!("Mismatched types: expected {:?}, got {:?}", expected_value, actual_value),
|
||||
}
|
||||
|
||||
run_end_to_end_test($input, ($expected).into());
|
||||
}
|
||||
)*
|
||||
};
|
||||
}
|
||||
|
||||
test_end_to_end! {
|
||||
// Basic arithmetic and units
|
||||
infix_addition: "5 + 5" => (10., Unit::BASE_UNIT),
|
||||
infix_subtraction_units: "5m - 3m" => (2., Unit::LENGTH),
|
||||
infix_multiplication_units: "4s * 4s" => (16., Unit { length: 0, mass: 0, time: 2 }),
|
||||
infix_division_units: "8m/2s" => (4., Unit::VELOCITY),
|
||||
// Basic arithmetic
|
||||
infix_addition: "5 + 5" => 10.,
|
||||
infix_subtraction: "5 - 3" => 2.,
|
||||
infix_multiplication: "4 * 4" => 16.,
|
||||
infix_division: "8/2" => 4.,
|
||||
modulo_pos_pos: "3.2 % 2" => 1.2,
|
||||
modulo_pos_neg: "3.2 % -2" => 1.2,
|
||||
modulo_neg_neg: "(-3.2) % -2" => -1.2,
|
||||
modulo_neg_pos: "(-3.2) % 2" => -1.2,
|
||||
exp_pos_pos: "3.2 ^ 2" => 256. / 25.,
|
||||
exp_pos_neg: "3.2 ^ -2" => 25. / 256.,
|
||||
exp_neg_neg: "-3.2 ^ -2" => -25. / 256.,
|
||||
exp_neg_pos: "-3.2 ^ 2" => -256. / 25.,
|
||||
|
||||
// Order of operations
|
||||
order_of_operations_negative_prefix: "-10 + 5" => (-5., Unit::BASE_UNIT),
|
||||
order_of_operations_add_multiply: "5+1*1+5" => (11., Unit::BASE_UNIT),
|
||||
order_of_operations_add_negative_multiply: "5+(-1)*1+5" => (9., Unit::BASE_UNIT),
|
||||
order_of_operations_sqrt: "sqrt25 + 11" => (16., Unit::BASE_UNIT),
|
||||
order_of_operations_sqrt_expression: "sqrt(25+11)" => (6., Unit::BASE_UNIT),
|
||||
order_of_operations_negative_prefix: "-10 + 5" => -5.,
|
||||
order_of_operations_add_multiply: "5+1*1+5" => 11.,
|
||||
order_of_operations_add_negative_multiply: "5+(-1)*1+5" => 9.,
|
||||
order_of_operations_sqrt: "sqrt(25) + 11" => 16.,
|
||||
order_of_operations_sqrt_expression: "sqrt(25+11)" => 6.,
|
||||
|
||||
// Parentheses and nested expressions
|
||||
parentheses_nested_multiply: "(5 + 3) * (2 + 6)" => (64., Unit::BASE_UNIT),
|
||||
parentheses_mixed_operations: "2 * (3 + 5 * (2 + 1))" => (36., Unit::BASE_UNIT),
|
||||
parentheses_divide_add_multiply: "10 / (2 + 3) + (7 * 2)" => (16., Unit::BASE_UNIT),
|
||||
parentheses_nested_multiply: "(5 + 3) * (2 + 6)" => 64.,
|
||||
parentheses_mixed_operations: "2 * (3 + 5 * (2 + 1))" => 36.,
|
||||
parentheses_divide_add_multiply: "10 / (2 + 3) + (7 * 2)" => 16.,
|
||||
|
||||
// Square root and nested square root
|
||||
sqrt_chain_operations: "sqrt(16) + sqrt(9) * sqrt(4)" => (10., Unit::BASE_UNIT),
|
||||
sqrt_nested: "sqrt(sqrt(81))" => (3., Unit::BASE_UNIT),
|
||||
sqrt_divide_expression: "sqrt((25 + 11) / 9)" => (2., Unit::BASE_UNIT),
|
||||
sqrt_chain_operations: "sqrt(16) + sqrt(9) * sqrt(4)" => 10.,
|
||||
sqrt_nested: "sqrt(sqrt(81))" => 3.,
|
||||
sqrt_divide_expression: "sqrt((25 + 11) / 9)" => 2.,
|
||||
|
||||
// Mixed square root and units
|
||||
sqrt_multiply_units: "sqrt(16) * 2g + 5g" => (13., Unit::MASS),
|
||||
sqrt_add_multiply: "sqrt(49) - 1 + 2 * 3" => (12., Unit::BASE_UNIT),
|
||||
sqrt_addition_multiply: "(sqrt(36) + 2) * 2" => (16., Unit::BASE_UNIT),
|
||||
sqrt_add_multiply: "sqrt(49) - 1 + 2 * 3" => 12.,
|
||||
sqrt_addition_multiply: "(sqrt(36) + 2) * 2" => 16.,
|
||||
|
||||
// Exponentiation
|
||||
exponent_single: "2^3" => (8., Unit::BASE_UNIT),
|
||||
exponent_mixed_operations: "2^3 + 4^2" => (24., Unit::BASE_UNIT),
|
||||
exponent_nested: "2^(3+1)" => (16., Unit::BASE_UNIT),
|
||||
exponent_single: "2^3" => 8.,
|
||||
exponent_mixed_operations: "2^3 + 4^2" => 24.,
|
||||
exponent_nested: "2^(3+1)" => 16.,
|
||||
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.,
|
||||
factorial_nested: "(3 + 2)!" => 120.,
|
||||
factorial_zero: "0!" => 1.,
|
||||
factorial_chain: "3!!" => 720., // (3!)! = 6! = 720
|
||||
|
||||
// Operations with negative values
|
||||
negative_units_add_multiply: "-5s + (-3 * 2)s" => (-11., Unit::TIME),
|
||||
negative_nested_parentheses: "-(5 + 3 * (2 - 1))" => (-8., Unit::BASE_UNIT),
|
||||
negative_sqrt_addition: "-(sqrt(16) + sqrt(9))" => (-7., Unit::BASE_UNIT),
|
||||
multiply_sqrt_subtract: "5 * 2 + sqrt(16) / 2 - 3" => (9., Unit::BASE_UNIT),
|
||||
add_multiply_subtract_sqrt: "4 + 3 * (2 + 1) - sqrt(25)" => (8., Unit::BASE_UNIT),
|
||||
add_sqrt_subtract_nested_multiply: "10 + sqrt(64) - (5 * (2 + 1))" => (3., Unit::BASE_UNIT),
|
||||
negative_nested_parentheses: "-(5 + 3 * (2 - 1))" => -8.,
|
||||
negative_sqrt_addition: "-(sqrt(16) + sqrt(9))" => -7.,
|
||||
multiply_sqrt_subtract: "5 * 2 + sqrt(16) / 2 - 3" => 9.,
|
||||
add_multiply_subtract_sqrt: "4 + 3 * (2 + 1) - sqrt(25)" => 8.,
|
||||
add_sqrt_subtract_nested_multiply: "10 + sqrt(64) - (5 * (2 + 1))" => 3.,
|
||||
|
||||
// Mathematical constants
|
||||
constant_pi: "pi" => (std::f64::consts::PI, Unit::BASE_UNIT),
|
||||
constant_e: "e" => (std::f64::consts::E, Unit::BASE_UNIT),
|
||||
constant_phi: "phi" => (1.61803398875, Unit::BASE_UNIT),
|
||||
constant_tau: "tau" => (2. * std::f64::consts::PI, Unit::BASE_UNIT),
|
||||
constant_infinity: "inf" => (f64::INFINITY, Unit::BASE_UNIT),
|
||||
constant_infinity_symbol: "∞" => (f64::INFINITY, Unit::BASE_UNIT),
|
||||
multiply_pi: "2 * pi" => (2. * std::f64::consts::PI, Unit::BASE_UNIT),
|
||||
add_e_constant: "e + 1" => (std::f64::consts::E + 1., Unit::BASE_UNIT),
|
||||
multiply_phi_constant: "phi * 2" => (1.61803398875 * 2., Unit::BASE_UNIT),
|
||||
exponent_tau: "2^tau" => (2f64.powf(2. * std::f64::consts::PI), Unit::BASE_UNIT),
|
||||
infinity_subtract_large_number: "inf - 1000" => (f64::INFINITY, Unit::BASE_UNIT),
|
||||
constant_pi: "pi" => std::f64::consts::PI,
|
||||
constant_e: "e" => std::f64::consts::E,
|
||||
constant_phi: "phi" => 1.61803398875,
|
||||
constant_tau: "tau" => 2. * std::f64::consts::PI,
|
||||
constant_infinity: "if(inf == ∞, inf, 0)" => f64::INFINITY,
|
||||
multiply_pi: "2 * pi" => 2. * std::f64::consts::PI,
|
||||
add_e_constant: "e + 1" => std::f64::consts::E + 1.,
|
||||
multiply_phi_constant: "phi * 2" => 1.61803398875 * 2.,
|
||||
exponent_tau: "2^tau" => 2f64.powf(2. * std::f64::consts::PI),
|
||||
infinity_subtract_large_number: "inf - 1000" => f64::INFINITY,
|
||||
|
||||
// Decimals with no leading digit before the point
|
||||
leading_dot_decimal: ".5" => (0.5, Unit::BASE_UNIT),
|
||||
leading_dot_in_expression: "1+.5" => (1.5, Unit::BASE_UNIT),
|
||||
leading_dot_exponent: ".5e3" => (500., Unit::BASE_UNIT),
|
||||
leading_dot_decimal: ".5" => 0.5,
|
||||
leading_dot_in_expression: "1+.5" => 1.5,
|
||||
leading_dot_exponent: ".5e3" => 500.,
|
||||
|
||||
// Trigonometric functions
|
||||
trig_sin_pi: "sin(pi)" => (0., Unit::BASE_UNIT),
|
||||
trig_cos_zero: "cos(0)" => (1., Unit::BASE_UNIT),
|
||||
trig_tan_pi_div_four: "tan(pi/4)" => (1., Unit::BASE_UNIT),
|
||||
trig_sin_tau: "sin(tau)" => (0., Unit::BASE_UNIT),
|
||||
trig_cos_tau_div_two: "cos(tau/2)" => (-1., Unit::BASE_UNIT),
|
||||
trig_sin_pi: "sin(pi)" => 0.,
|
||||
trig_cos_zero: "cos(0)" => 1.,
|
||||
trig_tan_pi_div_four: "tan(pi/4)" => 1.,
|
||||
trig_sin_tau: "sin(tau)" => 0.,
|
||||
trig_cos_tau_div_two: "cos(tau/2)" => -1.,
|
||||
trig_csc: "csc(pi/2)" => 1.,
|
||||
trig_sec: "sec(0)" => 1.,
|
||||
trig_cot: "cot(pi/4)" => 1.,
|
||||
|
||||
// Inverse trig aliases
|
||||
inverse_trig_asin: "asin(1)" => std::f64::consts::FRAC_PI_2,
|
||||
inverse_trig_acos: "acos(1)" => 0.,
|
||||
inverse_trig_atan: "atan(1)" => std::f64::consts::FRAC_PI_4,
|
||||
inverse_trig_acsc: "acsc(1)" => std::f64::consts::FRAC_PI_2,
|
||||
inverse_trig_asec: "asec(1)" => 0.,
|
||||
inverse_trig_acot: "acot(1)" => std::f64::consts::FRAC_PI_4,
|
||||
|
||||
// Hyperbolic and reciprocal hyperbolic
|
||||
hyperbolic_sinh: "sinh(0)" => 0.,
|
||||
hyperbolic_cosh: "cosh(0)" => 1.,
|
||||
hyperbolic_tanh: "tanh(0)" => 0.,
|
||||
hyperbolic_csch: "csch(1)" => 1f64.sinh().recip(),
|
||||
hyperbolic_sech: "sech(0)" => 1.,
|
||||
hyperbolic_coth: "coth(1)" => 1f64.tanh().recip(),
|
||||
|
||||
// Inverse hyperbolic
|
||||
inverse_hyperbolic_asinh: "asinh(0)" => 0.,
|
||||
inverse_hyperbolic_acosh: "acosh(1)" => 0.,
|
||||
inverse_hyperbolic_atanh: "atanh(0)" => 0.,
|
||||
inverse_hyperbolic_acsch: "acsch(1)" => 1f64.asinh(),
|
||||
inverse_hyperbolic_asech: "asech(1)" => 1f64.acosh(),
|
||||
inverse_hyperbolic_acoth: "acoth(2)" => 0.5f64.atanh(),
|
||||
|
||||
// Basic if statements
|
||||
if_true_condition: "if(1,5,3)" => 5.,
|
||||
if_false_condition: "if(0, 5, 3)" => 3.,
|
||||
|
||||
// Arithmetic conditions
|
||||
if_arithmetic_true: "if(2+2-4, 1 , 0)" => 0.,
|
||||
if_arithmetic_false: "if(3*2-5, 1, 0)" => 1.,
|
||||
|
||||
// Nested arithmetic
|
||||
if_complex_arithmetic: "if((5+3)*(2-1), 10, 20)" => 10.,
|
||||
if_with_division: "if(8/4-2 == 0, 15, 25)" => 15.,
|
||||
if_with_division_ne: "if(8/4-2 ≠ 0, 15, 25)" => 25.,
|
||||
|
||||
// Constants in conditions
|
||||
if_with_pi: "if(pi > 3, 1, 0)" => 1.,
|
||||
if_with_e: "if(e < 3, 1, 0)" => 1.,
|
||||
|
||||
// Functions in conditions
|
||||
if_with_sqrt: "if(sqrt(16) == 4, 1, 0)" => 1.,
|
||||
if_with_sin: "if(sin(pi) == 0.0, 1, 0)" => 0.,
|
||||
|
||||
// Logical NOT (prefix !)
|
||||
logical_not_zero: "!0" => 1.,
|
||||
logical_not_nonzero: "!5" => 0.,
|
||||
logical_not_expression: "!(2 - 2)" => 1.,
|
||||
|
||||
// Logical helpers as functions
|
||||
logical_isnan: "isnan(0/0)" => 1.,
|
||||
logical_eq: "eq(2, 2)" => 1.,
|
||||
logical_greater: "greater(3, 2)" => 1.,
|
||||
|
||||
// Log / exp / pow / root
|
||||
log_ln: "ln(e)" => 1.,
|
||||
log_log10: "log(100)" => 2.,
|
||||
log_log2: "log2(8)" => 3.,
|
||||
log_change_of_base: "log(8, 2)" => 3.,
|
||||
exp_function: "exp(1)" => std::f64::consts::E,
|
||||
pow_real: "pow(2, 3)" => 8.,
|
||||
root_square: "root(9, 2)" => 3.,
|
||||
root_cube: "root(8, 3)" => 2.,
|
||||
|
||||
// Nested if statements
|
||||
nested_if: "if(1, if(0, 1, 2), 3)" => 2.,
|
||||
nested_if_complex: "if(2-2 == 0, if(1, 5, 6), if(1, 7, 8))" => 5.,
|
||||
|
||||
// Mixed operations in conditions and blocks
|
||||
if_complex_condition: "if(sqrt(16) + sin(pi) < 5, 2*pi, 3*e)" => 2. * std::f64::consts::PI,
|
||||
if_complex_blocks: "if(1, 2*sqrt(16) + sin(pi/2), 3*cos(0) + 4)" => 9.,
|
||||
|
||||
// Mapping helpers
|
||||
mapping_trunc: "trunc(3.7)" => 3.,
|
||||
mapping_fract: "fract(3.25)" => 0.25,
|
||||
mapping_sign_pos: "sign(5)" => 1.,
|
||||
mapping_sign_neg: "sign(-5)" => -1.,
|
||||
|
||||
// Geometry / mapping extras
|
||||
geometry_hypot: "hypot(3, 4)" => 5.,
|
||||
mapping_remap: "remap(5, 0, 10, 0, 100)" => 50.,
|
||||
|
||||
// GCD / LCM
|
||||
gcd_simple: "gcd(24, 18)" => 6.,
|
||||
lcm_simple: "lcm(4, 6)" => 12.,
|
||||
|
||||
// atan2
|
||||
trig_atan2_axis: "atan2(1, 0)" => std::f64::consts::FRAC_PI_2,
|
||||
|
||||
// Comparison operators combined with logical AND
|
||||
comparison_operators: "if(1 <= 2 && 1 ≤ 2 && 2 >= 1 && 2 ≥ 1, 1., 0.)" => 1.,
|
||||
|
||||
// Logical AND / OR
|
||||
logical_and_true: "if(1 <= 2 && 2 < 3, 1., 0.)" => 1.,
|
||||
logical_and_false: "if(1 <= 2 && 3 < 2, 1., 0.)" => 0.,
|
||||
logical_or_true_left: "if(1 > 2 || 2 < 3, 1., 0.)" => 1.,
|
||||
logical_or_true_right: "if(2 < 1 || 2 < 3, 1., 0.)" => 1.,
|
||||
logical_or_false: "if(1 > 2 || 3 < 2, 1., 0.)" => 0.,
|
||||
logical_precedence_and_over_or: "if(0 == 1 || 1 == 1 && 0 == 0, 1., 0.)" => 1.,
|
||||
|
||||
// Edge cases
|
||||
if_zero: "if(0.0, 1, 2)" => 2.,
|
||||
|
||||
// Complex nested expressions
|
||||
if_nested_expr: "if((sqrt(16) + 2) * (sin(pi) + 1), 3 + 4 * 2, 5 - 2 / 1)" => 11.,
|
||||
|
||||
// Overflow-safe evaluation
|
||||
factorial_overflows_to_infinity: "171!" => f64::INFINITY,
|
||||
factorial_huge_input: "10000000000000000000000!" => f64::INFINITY,
|
||||
lcm_huge_no_overflow: "lcm(1099511627776, 1099511627775)" => 1099511627776. * 1099511627775.,
|
||||
gcd_non_finite: "gcd(inf, 6)" => f64::NAN,
|
||||
long_literal: "10000000000000000000000" => 1e22,
|
||||
huge_exponent_saturates: "1e4294967296" => f64::INFINITY,
|
||||
|
||||
// Odd integer roots of negative values are real
|
||||
root_negative_odd: "root(-8, 3)" => -2.,
|
||||
root_negative_odd_reciprocal: "root(-8, -3)" => -0.5,
|
||||
root_negative_even: "root(-4, 2)" => f64::NAN,
|
||||
|
||||
// NaN poisons conditions and logic instead of acting as a boolean
|
||||
if_nan_condition: "if(sqrt(-1), 1, 2)" => f64::NAN,
|
||||
nan_and: "sqrt(-1) && 1" => f64::NAN,
|
||||
nan_or: "sqrt(-1) || 1" => f64::NAN,
|
||||
nan_not: "!sqrt(-1)" => f64::NAN,
|
||||
|
||||
// Logic and equality span real and complex operands
|
||||
mixed_equality: "1 == i" => 0.,
|
||||
complex_equality: "i == i" => 1.,
|
||||
mixed_and: "1 && i" => 1.,
|
||||
mixed_nan_and: "sqrt(-1) && i" => f64::NAN,
|
||||
|
||||
// Correctly rounded literals via std parsing
|
||||
seventeen_digit_literal: "999999999999999999" => 1e18,
|
||||
long_fraction_literal: "0.1111111111111111111111111111111111111111" => 1. / 9.,
|
||||
|
||||
// Integer functions reject inputs beyond f64's exact integer range
|
||||
gcd_beyond_exact_integers: "gcd(10000000000000000000, 2)" => f64::NAN,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,326 +1,165 @@
|
||||
use crate::ast::{BinaryOp, Literal, Node, UnaryOp, Unit};
|
||||
use crate::context::EvalContext;
|
||||
use crate::value::{Complex, Number, Value};
|
||||
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;
|
||||
use crate::ast::{BinaryOp, Literal, Node, UnaryOp};
|
||||
use crate::lexer::{Lexer, Span, Token};
|
||||
use chumsky::error::LabelError;
|
||||
use chumsky::input::ValueInput;
|
||||
use chumsky::{Parser, prelude::*};
|
||||
use std::fmt;
|
||||
|
||||
#[derive(Parser)]
|
||||
#[grammar = "./grammer.pest"] // Point to the grammar file
|
||||
struct ExprParser;
|
||||
/// One message per parse failure, each tagged with its byte range in the source expression.
|
||||
#[derive(Debug)]
|
||||
pub struct ParseError(Vec<String>);
|
||||
|
||||
lazy_static! {
|
||||
static ref PRATT_PARSER: PrattParser<Rule> = {
|
||||
PrattParser::new()
|
||||
.op(Op::infix(Rule::add, Assoc::Left) | Op::infix(Rule::sub, Assoc::Left))
|
||||
.op(Op::infix(Rule::mul, Assoc::Left) | Op::infix(Rule::div, Assoc::Left) | Op::infix(Rule::paren, Assoc::Left))
|
||||
.op(Op::infix(Rule::pow, Assoc::Right))
|
||||
.op(Op::postfix(Rule::fac) | Op::postfix(Rule::EOI))
|
||||
.op(Op::prefix(Rule::sqrt))
|
||||
.op(Op::prefix(Rule::neg))
|
||||
};
|
||||
impl fmt::Display for ParseError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
for (index, error) in self.0.iter().enumerate() {
|
||||
if index > 0 {
|
||||
writeln!(f)?;
|
||||
}
|
||||
write!(f, "{error}")?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum TypeError {
|
||||
#[error("Invalid BinOp: {0:?} {1:?} {2:?}")]
|
||||
InvalidBinaryOp(Unit, BinaryOp, Unit),
|
||||
|
||||
#[error("Invalid UnaryOp: {0:?}")]
|
||||
InvalidUnaryOp(Unit, UnaryOp),
|
||||
}
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum ParseError {
|
||||
#[error("ParseIntError: {0}")]
|
||||
ParseInt(#[from] ParseIntError),
|
||||
#[error("ParseFloatError: {0}")]
|
||||
ParseFloat(#[from] ParseFloatError),
|
||||
|
||||
#[error("TypeError: {0}")]
|
||||
Type(#[from] TypeError),
|
||||
|
||||
#[error("PestError: {0}")]
|
||||
Pest(#[from] Box<pest::error::Error<Rule>>),
|
||||
}
|
||||
impl std::error::Error for ParseError {}
|
||||
|
||||
impl Node {
|
||||
pub fn try_parse_from_str(s: &str) -> Result<(Node, Unit), ParseError> {
|
||||
let pairs = ExprParser::parse(Rule::program, s).map_err(Box::new)?;
|
||||
let (node, metadata) = parse_expr(pairs)?;
|
||||
Ok((node, metadata.unit))
|
||||
}
|
||||
}
|
||||
pub fn try_parse_from_str(src: &str) -> Result<Node, ParseError> {
|
||||
// Parse with zero-cost errors first (several times faster), then re-parse invalid input with rich errors to build the messages
|
||||
if let Ok(ast) = parser::<Lexer, extra::Default>().parse(Lexer::new(src)).into_result() {
|
||||
return Ok(ast);
|
||||
}
|
||||
|
||||
struct NodeMetadata {
|
||||
pub unit: Unit,
|
||||
}
|
||||
|
||||
impl NodeMetadata {
|
||||
pub fn new(unit: Unit) -> Self {
|
||||
Self { unit }
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_unit(pairs: Pairs<Rule>) -> Result<(Unit, f64), ParseError> {
|
||||
let mut scale = 1.;
|
||||
let mut length = 0;
|
||||
let mut mass = 0;
|
||||
let mut time = 0;
|
||||
|
||||
for pair in pairs {
|
||||
println!("found rule: {:?}", pair.as_rule());
|
||||
match pair.as_rule() {
|
||||
Rule::nano => scale *= 1e-9,
|
||||
Rule::micro => scale *= 1e-6,
|
||||
Rule::milli => scale *= 1e-3,
|
||||
Rule::centi => scale *= 1e-2,
|
||||
Rule::deci => scale *= 1e-1,
|
||||
Rule::deca => scale *= 1e1,
|
||||
Rule::hecto => scale *= 1e2,
|
||||
Rule::kilo => scale *= 1e3,
|
||||
Rule::mega => scale *= 1e6,
|
||||
Rule::giga => scale *= 1e9,
|
||||
Rule::tera => scale *= 1e12,
|
||||
|
||||
Rule::meter => length = 1,
|
||||
Rule::gram => mass = 1,
|
||||
Rule::second => time = 1,
|
||||
|
||||
_ => unreachable!(), // All possible rules should be covered
|
||||
match parser::<Lexer, extra::Err<Rich<Token, Span>>>().parse(Lexer::new(src)).into_result() {
|
||||
Ok(ast) => Ok(ast),
|
||||
Err(parse_errs) => Err(ParseError(parse_errs.into_iter().map(|e| format!("{e} at {}", e.span())).collect())),
|
||||
}
|
||||
}
|
||||
|
||||
Ok((Unit { length, mass, time }, scale))
|
||||
}
|
||||
|
||||
fn parse_const(pair: Pair<Rule>) -> Literal {
|
||||
match pair.as_rule() {
|
||||
Rule::infinity => Literal::Float(f64::INFINITY),
|
||||
Rule::imaginary_unit => Literal::Complex(Complex::new(0., 1.)),
|
||||
Rule::pi => Literal::Float(std::f64::consts::PI),
|
||||
Rule::tau => Literal::Float(2. * std::f64::consts::PI),
|
||||
Rule::euler_number => Literal::Float(std::f64::consts::E),
|
||||
Rule::golden_ratio => Literal::Float(1.61803398875),
|
||||
_ => unreachable!("Unexpected constant: {:?}", pair),
|
||||
}
|
||||
}
|
||||
pub fn parser<'src, I, 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! {
|
||||
Token::Float(f) => Node::Lit(Literal::Float(f)),
|
||||
Token::Const(c) => Node::Lit(c.value())
|
||||
};
|
||||
|
||||
fn parse_lit(mut pairs: Pairs<Rule>) -> Result<(Literal, Unit), ParseError> {
|
||||
let literal = match pairs.next() {
|
||||
Some(lit) => match lit.as_rule() {
|
||||
Rule::int => {
|
||||
let value = lit.as_str().parse::<i32>()? as f64;
|
||||
Literal::Float(value)
|
||||
}
|
||||
Rule::float => {
|
||||
let value = lit.as_str().parse::<f64>()?;
|
||||
Literal::Float(value)
|
||||
}
|
||||
Rule::unit => {
|
||||
let (unit, scale) = parse_unit(lit.into_inner())?;
|
||||
return Ok((Literal::Float(scale), unit));
|
||||
}
|
||||
rule => unreachable!("unexpected rule: {:?}", rule),
|
||||
},
|
||||
None => unreachable!("expected rule"), // No literal found
|
||||
};
|
||||
let args = expr.clone().separated_by(just(Token::Comma)).collect::<Vec<_>>().delimited_by(just(Token::LParen), just(Token::RParen));
|
||||
|
||||
if let Some(unit_pair) = pairs.next() {
|
||||
let unit_pairs = unit_pair.into_inner(); // Get the inner pairs for the unit
|
||||
let (unit, scale) = parse_unit(unit_pairs)?;
|
||||
let if_expr = just(Token::If).ignore_then(args.clone()).try_map(|args: Vec<Node>, span| {
|
||||
let [condition, if_block, else_block] = <[Node; 3]>::try_from(args).map_err(|_| LabelError::<I, _>::expected_found(["3 arguments in if(condition, a, b)"], None, span))?;
|
||||
|
||||
println!("found unit: {unit:?}");
|
||||
|
||||
Ok((
|
||||
match literal {
|
||||
Literal::Float(num) => Literal::Float(num * scale),
|
||||
Literal::Complex(num) => Literal::Complex(num * scale),
|
||||
},
|
||||
unit,
|
||||
))
|
||||
} else {
|
||||
Ok((literal, Unit::BASE_UNIT))
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_expr(pairs: Pairs<Rule>) -> Result<(Node, NodeMetadata), ParseError> {
|
||||
PRATT_PARSER
|
||||
.map_primary(|primary| {
|
||||
Ok(match primary.as_rule() {
|
||||
Rule::lit => {
|
||||
let (lit, unit) = parse_lit(primary.into_inner())?;
|
||||
|
||||
(Node::Lit(lit), NodeMetadata { unit })
|
||||
}
|
||||
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::<Result<Vec<Node>, ParseError>>()?,
|
||||
},
|
||||
NodeMetadata::new(Unit::BASE_UNIT),
|
||||
)
|
||||
}
|
||||
Rule::constant => {
|
||||
let lit = parse_const(primary.into_inner().next().expect("constant should have atleast 1 child"));
|
||||
|
||||
(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::<f64>()?;
|
||||
(Node::Lit(Literal::Float(value)), NodeMetadata::new(Unit::BASE_UNIT))
|
||||
}
|
||||
rule => unreachable!("unexpected rule: {:?}", rule),
|
||||
Ok(Node::Conditional {
|
||||
condition: Box::new(condition),
|
||||
if_block: Box::new(if_block),
|
||||
else_block: Box::new(else_block),
|
||||
})
|
||||
})
|
||||
.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 ident = select! {Token::Ident(s) => s}.labelled("ident");
|
||||
|
||||
let node = Node::UnaryOp { expr: Box::new(rhs), op };
|
||||
let unit = rhs_metadata.unit;
|
||||
// An ident followed by parenthesized args is a function call, otherwise a variable
|
||||
let call_or_var = ident.then(args.or_not()).map(|(name, args): (&str, Option<Vec<Node>>)| match args {
|
||||
Some(args) => Node::FnCall { name: name.to_string(), expr: args },
|
||||
None => Node::Var(name.to_string()),
|
||||
});
|
||||
|
||||
let 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
|
||||
};
|
||||
let parens = expr.clone().delimited_by(just(Token::LParen), just(Token::RParen));
|
||||
|
||||
Ok((node, NodeMetadata::new(unit)))
|
||||
})
|
||||
.map_postfix(|lhs, op| {
|
||||
let (lhs_node, lhs_metadata) = lhs?;
|
||||
let atom = choice((constant, if_expr, call_or_var, parens)).labelled("atom");
|
||||
|
||||
let op = match op.as_rule() {
|
||||
Rule::EOI => return Ok((lhs_node, lhs_metadata)),
|
||||
Rule::fac => UnaryOp::Fac,
|
||||
rule => unreachable!("unexpected rule: {:?}", rule),
|
||||
};
|
||||
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),
|
||||
));
|
||||
|
||||
if !lhs_metadata.unit.is_base() {
|
||||
return Err(ParseError::Type(TypeError::InvalidUnaryOp(lhs_metadata.unit, op)));
|
||||
}
|
||||
// Postfix factorial: expr! → UnaryOp::Fac
|
||||
let postfix = atom.clone().foldl(just(Token::Bang).repeated(), |expr, _| Node::UnaryOp {
|
||||
op: UnaryOp::Fac,
|
||||
expr: Box::new(expr),
|
||||
});
|
||||
|
||||
Ok((Node::UnaryOp { expr: Box::new(lhs_node), op }, lhs_metadata))
|
||||
})
|
||||
.map_infix(|lhs, op, rhs| {
|
||||
let (lhs, lhs_metadata) = lhs?;
|
||||
let (rhs, rhs_metadata) = rhs?;
|
||||
|
||||
let op = match op.as_rule() {
|
||||
Rule::add => BinaryOp::Add,
|
||||
Rule::sub => BinaryOp::Sub,
|
||||
Rule::mul => BinaryOp::Mul,
|
||||
Rule::div => BinaryOp::Div,
|
||||
Rule::pow => BinaryOp::Pow,
|
||||
Rule::paren => BinaryOp::Mul,
|
||||
rule => unreachable!("unexpected rule: {:?}", rule),
|
||||
};
|
||||
|
||||
let (lhs_unit, rhs_unit) = (lhs_metadata.unit, rhs_metadata.unit);
|
||||
|
||||
let unit = match (!lhs_unit.is_base(), !rhs_unit.is_base()) {
|
||||
(true, true) => match op {
|
||||
BinaryOp::Mul => Unit {
|
||||
length: lhs_unit.length + rhs_unit.length,
|
||||
mass: lhs_unit.mass + rhs_unit.mass,
|
||||
time: lhs_unit.time + rhs_unit.time,
|
||||
},
|
||||
BinaryOp::Div => Unit {
|
||||
length: lhs_unit.length - rhs_unit.length,
|
||||
mass: lhs_unit.mass - rhs_unit.mass,
|
||||
time: lhs_unit.time - rhs_unit.time,
|
||||
},
|
||||
BinaryOp::Add | BinaryOp::Sub => {
|
||||
if lhs_unit == rhs_unit {
|
||||
lhs_unit
|
||||
} else {
|
||||
return Err(ParseError::Type(TypeError::InvalidBinaryOp(lhs_unit, op, rhs_unit)));
|
||||
}
|
||||
}
|
||||
BinaryOp::Pow => {
|
||||
return Err(ParseError::Type(TypeError::InvalidBinaryOp(lhs_unit, op, rhs_unit)));
|
||||
}
|
||||
// Exponentiation is right-associative (`2^2^3` is `2^(2^3)`) and the exponent may carry unary signs like `2^-3`
|
||||
let pow = recursive(|pow| {
|
||||
let exponent = unary_op.clone().repeated().foldr(pow, |op, expr| Node::UnaryOp { op, expr: Box::new(expr) });
|
||||
postfix.clone().then(pow_op.ignore_then(exponent).or_not()).map(|(base, exponent)| match exponent {
|
||||
Some(exponent) => Node::BinOp {
|
||||
lhs: Box::new(base),
|
||||
op: BinaryOp::Pow,
|
||||
rhs: Box::new(exponent),
|
||||
},
|
||||
None => base,
|
||||
})
|
||||
});
|
||||
|
||||
(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.clone().repeated().foldr(pow.clone(), |op, expr| Node::UnaryOp { op, expr: Box::new(expr) });
|
||||
|
||||
let node = Node::BinOp {
|
||||
lhs: Box::new(lhs),
|
||||
op,
|
||||
rhs: Box::new(rhs),
|
||||
};
|
||||
// Juxtaposed factors like `2pi` or `2sqrt(4)` multiply implicitly at the same precedence as `*` and `/`.
|
||||
// The implicit operand is a `pow`, not a full unary, so `2 -3` stays a subtraction; the lexer rejects a bare number as the right operand (`10 000` is not `10*000`).
|
||||
let implicit_mul = pow.map(|rhs| (BinaryOp::Mul, rhs));
|
||||
let product = unary.clone().foldl(choice((mul_op.then(unary), implicit_mul)).repeated(), |lhs, (op, rhs)| Node::BinOp {
|
||||
lhs: Box::new(lhs),
|
||||
op,
|
||||
rhs: Box::new(rhs),
|
||||
});
|
||||
|
||||
Ok((node, NodeMetadata::new(unit)))
|
||||
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),
|
||||
});
|
||||
|
||||
let and = cmp.clone().foldl(and_op.then(cmp).repeated(), |lhs, (op, rhs)| Node::BinOp {
|
||||
lhs: Box::new(lhs),
|
||||
op,
|
||||
rhs: Box::new(rhs),
|
||||
});
|
||||
|
||||
and.clone().foldl(or_op.then(and).repeated(), |lhs, (op, rhs)| Node::BinOp {
|
||||
lhs: Box::new(lhs),
|
||||
op,
|
||||
rhs: Box::new(rhs),
|
||||
})
|
||||
.parse(pairs)
|
||||
})
|
||||
}
|
||||
|
||||
//TODO: set up Unit test for Units
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::value::Complex;
|
||||
|
||||
macro_rules! test_parser {
|
||||
($($name:ident: $input:expr_2021 => $expected:expr_2021),* $(,)?) => {
|
||||
$(
|
||||
#[test]
|
||||
fn $name() {
|
||||
let result = Node::try_parse_from_str($input).unwrap();
|
||||
assert_eq!(result.0, $expected);
|
||||
|
||||
let result = match Node::try_parse_from_str($input) {
|
||||
Ok(expr) => expr,
|
||||
Err(err) => panic!("failed to parse `{}`: {err}", $input),
|
||||
};
|
||||
assert_eq!(result, $expected);
|
||||
}
|
||||
)*
|
||||
};
|
||||
@@ -349,16 +188,20 @@ mod tests {
|
||||
op: BinaryOp::Pow,
|
||||
rhs: Box::new(Node::Lit(Literal::Float(3.))),
|
||||
},
|
||||
test_parse_unary_sqrt: "sqrt(16)" => Node::UnaryOp {
|
||||
expr: Box::new(Node::Lit(Literal::Float(16.))),
|
||||
op: UnaryOp::Sqrt,
|
||||
test_parse_unary_sqrt: "sqrt(16)" => Node::FnCall {
|
||||
name: "sqrt".to_string(),
|
||||
expr: vec![Node::Lit(Literal::Float(16.))],
|
||||
},
|
||||
test_parse_sqr_ident: "sqr(16)" => Node::FnCall {
|
||||
name:"sqr".to_string(),
|
||||
expr: vec![Node::Lit(Literal::Float(16.))]
|
||||
test_parse_ii_call: "ii(16)" => Node::FnCall {
|
||||
name: "ii".to_string(),
|
||||
expr: vec![Node::Lit(Literal::Float(16.))]
|
||||
},
|
||||
|
||||
test_parse_complex_expr: "(1 + 2) 3 - 4 ^ 2" => Node::BinOp {
|
||||
test_parse_i_mul: "i(16)" => Node::BinOp {
|
||||
lhs: Box::new(Node::Lit(Literal::Complex(Complex::new(0., 1.)))),
|
||||
op: BinaryOp::Mul,
|
||||
rhs: Box::new(Node::Lit(Literal::Float(16.))),
|
||||
},
|
||||
test_parse_complex_expr: "(1 + 2) * 3 - 4 ^ 2" => Node::BinOp {
|
||||
lhs: Box::new(Node::BinOp {
|
||||
lhs: Box::new(Node::BinOp {
|
||||
lhs: Box::new(Node::Lit(Literal::Float(1.))),
|
||||
@@ -374,6 +217,15 @@ mod tests {
|
||||
op: BinaryOp::Pow,
|
||||
rhs: Box::new(Node::Lit(Literal::Float(2.))),
|
||||
}),
|
||||
},
|
||||
test_conditional_expr: "if (x+3, 0, 1)" => Node::Conditional{
|
||||
condition: Box::new(Node::BinOp{
|
||||
lhs: Box::new(Node::Var("x".to_string())),
|
||||
op: BinaryOp::Add,
|
||||
rhs: Box::new(Node::Lit(Literal::Float(3.))),
|
||||
}),
|
||||
if_block: Box::new(Node::Lit(Literal::Float(0.))),
|
||||
else_block: Box::new(Node::Lit(Literal::Float(1.))),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
use crate::ast::{BinaryOp, UnaryOp};
|
||||
use num_complex::ComplexFloat;
|
||||
use std::f64::consts::PI;
|
||||
|
||||
pub type Complex = num_complex::Complex<f64>;
|
||||
|
||||
@@ -52,7 +50,35 @@ impl std::fmt::Display for Number {
|
||||
}
|
||||
|
||||
impl Number {
|
||||
pub fn binary_op(self, op: BinaryOp, other: Number) -> Number {
|
||||
/// The value's truthiness for conditions and logic operators, or `None` for NaN values, which poison the result rather than acting as a boolean.
|
||||
pub fn as_bool(self) -> Option<bool> {
|
||||
match self {
|
||||
Number::Real(real) => (!real.is_nan()).then_some(real != 0.),
|
||||
Number::Complex(complex) => (!complex.re.is_nan() && !complex.im.is_nan()).then_some(complex != Complex::ZERO),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn binary_op(self, op: BinaryOp, other: Number) -> Option<Number> {
|
||||
// Logic and equality work uniformly across real and complex operands
|
||||
match op {
|
||||
BinaryOp::And | BinaryOp::Or => {
|
||||
let (Some(lhs), Some(rhs)) = (self.as_bool(), other.as_bool()) else {
|
||||
return Some(Number::Real(f64::NAN));
|
||||
};
|
||||
let result = if matches!(op, BinaryOp::And) { lhs && rhs } else { lhs || rhs };
|
||||
return Some(Number::Real(result as u8 as f64));
|
||||
}
|
||||
BinaryOp::Eq | BinaryOp::Neq => {
|
||||
let equal = match (self, other) {
|
||||
(Number::Real(lhs), Number::Real(rhs)) => lhs == rhs,
|
||||
(Number::Complex(lhs), Number::Complex(rhs)) => lhs == rhs,
|
||||
(Number::Real(real), Number::Complex(complex)) | (Number::Complex(complex), Number::Real(real)) => complex == Complex::new(real, 0.),
|
||||
};
|
||||
return Some(Number::Real((equal != matches!(op, BinaryOp::Neq)) as u8 as f64));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
match (self, other) {
|
||||
(Number::Real(lhs), Number::Real(rhs)) => {
|
||||
let result = match op {
|
||||
@@ -60,9 +86,16 @@ impl Number {
|
||||
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::And | BinaryOp::Or | BinaryOp::Eq | BinaryOp::Neq => unreachable!("handled above"),
|
||||
};
|
||||
Number::Real(result)
|
||||
|
||||
Some(Number::Real(result))
|
||||
}
|
||||
|
||||
(Number::Complex(lhs), Number::Complex(rhs)) => {
|
||||
@@ -71,9 +104,14 @@ impl Number {
|
||||
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::And | BinaryOp::Or | BinaryOp::Eq | BinaryOp::Neq => unreachable!("handled above"),
|
||||
};
|
||||
Number::Complex(result)
|
||||
Some(Number::Complex(result))
|
||||
}
|
||||
|
||||
(Number::Real(lhs), Number::Complex(rhs)) => {
|
||||
@@ -84,8 +122,9 @@ impl Number {
|
||||
BinaryOp::Mul => lhs_complex * rhs,
|
||||
BinaryOp::Div => lhs_complex / rhs,
|
||||
BinaryOp::Pow => lhs_complex.powc(rhs),
|
||||
_ => return None,
|
||||
};
|
||||
Number::Complex(result)
|
||||
Some(Number::Complex(result))
|
||||
}
|
||||
|
||||
(Number::Complex(lhs), Number::Real(rhs)) => {
|
||||
@@ -96,26 +135,55 @@ impl Number {
|
||||
BinaryOp::Mul => lhs * rhs_complex,
|
||||
BinaryOp::Div => lhs / rhs_complex,
|
||||
BinaryOp::Pow => lhs.powf(rhs),
|
||||
_ => return None,
|
||||
};
|
||||
Number::Complex(result)
|
||||
Some(Number::Complex(result))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn unary_op(self, op: UnaryOp) -> Number {
|
||||
if matches!(op, UnaryOp::Not) {
|
||||
return match self.as_bool() {
|
||||
Some(boolean) => Number::Real(!boolean as u8 as f64),
|
||||
None => Number::Real(f64::NAN),
|
||||
};
|
||||
}
|
||||
|
||||
match self {
|
||||
Number::Real(real) => match op {
|
||||
UnaryOp::Neg => Number::Real(-real),
|
||||
UnaryOp::Sqrt => Number::Real(real.sqrt()),
|
||||
UnaryOp::Fac => {
|
||||
// n! for real n: use integer semantics when n is a
|
||||
// non-negative integer, otherwise return NaN.
|
||||
if !real.is_finite() {
|
||||
return Number::Real(f64::NAN);
|
||||
}
|
||||
let truncated = real.trunc();
|
||||
if truncated < 0. || (real - truncated).abs() > f64::EPSILON {
|
||||
return Number::Real(f64::NAN);
|
||||
}
|
||||
|
||||
UnaryOp::Fac => todo!("Implement factorial"),
|
||||
// Return infinity above 170! since that overflows f64, which also keeps huge inputs from spinning the loop
|
||||
let n = truncated as u64;
|
||||
if n > 170 {
|
||||
return Number::Real(f64::INFINITY);
|
||||
}
|
||||
let mut acc = 1_f64;
|
||||
for k in 1..=n {
|
||||
acc *= k as f64;
|
||||
}
|
||||
Number::Real(acc)
|
||||
}
|
||||
UnaryOp::Not => unreachable!("handled above"),
|
||||
},
|
||||
|
||||
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 => unreachable!("handled above"),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
Copyright (c) Graphite contributors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
Copyright (c) Graphite contributors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
|
||||
Reference in New Issue
Block a user