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:
Dennis Kobert
2026-09-08 15:03:57 +00:00
385 changed files with 34669 additions and 20078 deletions

View File

@@ -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

View File

@@ -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 }

View File

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

View File

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

View File

@@ -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,
})
}

View File

@@ -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),* $(,)?) => {
$(

View File

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

View File

@@ -0,0 +1,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
}
}
}

View File

@@ -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,
}
}

View File

@@ -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.))),
}
}
}

View File

@@ -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"),
},
}
}

View File

@@ -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

View File

@@ -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