pub mod context;
pub mod coercion;
pub mod functions;
pub use context::Context;
pub use functions::{EvalCtx, FunctionMeta, Registry};
use crate::parser::ast::{BinaryOp, Expr, UnaryOp};
use crate::types::{ErrorKind, Value};
use coercion::{to_number, to_string_val};
use functions::FunctionKind;
pub fn evaluate_expr(expr: &Expr, ctx: &mut EvalCtx<'_>) -> Value {
match expr {
Expr::Number(n, _) => {
if n.is_finite() {
Value::Number(*n)
} else {
Value::Error(ErrorKind::Num)
}
}
Expr::Text(s, _) => Value::Text(s.clone()),
Expr::Bool(b, _) => Value::Bool(*b),
Expr::Variable(name, _) => ctx.ctx.get(name),
Expr::UnaryOp { op, operand, .. } => {
let val = evaluate_expr(operand, ctx);
match to_number(val) {
Err(e) => e,
Ok(n) => match op {
UnaryOp::Neg => Value::Number(-n),
UnaryOp::Percent => Value::Number(n / 100.0),
},
}
}
Expr::BinaryOp { op, left, right, .. } => {
let lv = evaluate_expr(left, ctx);
let rv = evaluate_expr(right, ctx);
eval_binary(op, lv, rv)
}
Expr::Array(elems, _) => {
let mut values = Vec::with_capacity(elems.len());
for elem in elems {
let v = evaluate_expr(elem, ctx);
values.push(v);
}
Value::Array(values)
}
Expr::FunctionCall { name, args, .. } => {
match ctx.registry.get(name) {
None => Value::Error(ErrorKind::Name),
Some(FunctionKind::Lazy(f)) => {
let f: functions::LazyFn = *f;
f(args, ctx)
}
Some(FunctionKind::Eager(f)) => {
let f: functions::EagerFn = *f;
let mut evaluated = Vec::with_capacity(args.len());
for arg in args {
let v = evaluate_expr(arg, ctx);
if matches!(v, Value::Error(_)) {
return v;
}
evaluated.push(v);
}
f(&evaluated)
}
}
}
}
}
fn type_rank(v: &Value) -> u8 {
match v {
Value::Number(_) | Value::Date(_) | Value::Empty => 0,
Value::Text(_) => 1,
Value::Bool(_) => 2,
Value::Error(_) | Value::Array(_) => 3,
}
}
fn eval_binary(op: &BinaryOp, lv: Value, rv: Value) -> Value {
match op {
BinaryOp::Add | BinaryOp::Sub | BinaryOp::Mul | BinaryOp::Div | BinaryOp::Pow => {
let ln = match to_number(lv) { Ok(n) => n, Err(e) => return e };
let rn = match to_number(rv) { Ok(n) => n, Err(e) => return e };
let result = match op {
BinaryOp::Add => ln + rn,
BinaryOp::Sub => ln - rn,
BinaryOp::Mul => ln * rn,
BinaryOp::Div => {
if rn == 0.0 {
return Value::Error(ErrorKind::DivByZero);
}
ln / rn
}
BinaryOp::Pow => ln.powf(rn),
_ => unreachable!(),
};
if !result.is_finite() {
return Value::Error(ErrorKind::Num);
}
Value::Number(result)
}
BinaryOp::Concat => {
let ls = match to_string_val(lv) { Ok(s) => s, Err(e) => return e };
let rs = match to_string_val(rv) { Ok(s) => s, Err(e) => return e };
Value::Text(ls + &rs)
}
BinaryOp::Eq | BinaryOp::Ne
| BinaryOp::Lt | BinaryOp::Gt
| BinaryOp::Le | BinaryOp::Ge => {
if let Value::Error(_) = &lv { return lv; }
if let Value::Error(_) = &rv { return rv; }
let result = compare_values(op, &lv, &rv);
Value::Bool(result)
}
}
}
fn compare_values(op: &BinaryOp, lv: &Value, rv: &Value) -> bool {
match (lv, rv) {
(Value::Number(a), Value::Number(b)) => apply_cmp(op, a.partial_cmp(b)),
(Value::Date(a), Value::Date(b)) => apply_cmp(op, a.partial_cmp(b)),
(Value::Date(a), Value::Number(b)) => apply_cmp(op, a.partial_cmp(b)),
(Value::Number(a), Value::Date(b)) => apply_cmp(op, a.partial_cmp(b)),
(Value::Text(a), Value::Text(b)) => apply_cmp(op, Some(a.cmp(b))),
(Value::Bool(a), Value::Bool(b)) => apply_cmp(op, Some(a.cmp(b))),
(Value::Empty, Value::Empty) => apply_cmp(op, Some(std::cmp::Ordering::Equal)),
(Value::Empty, Value::Number(b)) => apply_cmp(op, 0.0f64.partial_cmp(b)),
(Value::Number(a), Value::Empty) => apply_cmp(op, a.partial_cmp(&0.0f64)),
_ => {
let lr = type_rank(lv);
let rr = type_rank(rv);
match op {
BinaryOp::Eq => false,
BinaryOp::Ne => true,
BinaryOp::Lt => lr < rr,
BinaryOp::Gt => lr > rr,
BinaryOp::Le => lr <= rr,
BinaryOp::Ge => lr >= rr,
_ => unreachable!(),
}
}
}
}
fn apply_cmp(op: &BinaryOp, ord: Option<std::cmp::Ordering>) -> bool {
match ord {
None => false,
Some(o) => match op {
BinaryOp::Eq => o.is_eq(),
BinaryOp::Ne => o.is_ne(),
BinaryOp::Lt => o.is_lt(),
BinaryOp::Gt => o.is_gt(),
BinaryOp::Le => o.is_le(),
BinaryOp::Ge => o.is_ge(),
_ => unreachable!(),
},
}
}
#[cfg(test)]
mod tests;