use serde_json::Value;
pub const EXPRESSION_RUNTIME_VERSION: &str = "1";
#[derive(Debug, Clone)]
pub struct ExpressionError(pub String);
impl std::fmt::Display for ExpressionError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
impl std::error::Error for ExpressionError {}
fn err<T>(msg: impl Into<String>) -> Result<T, ExpressionError> {
Err(ExpressionError(msg.into()))
}
pub type Resolve<'a, C> =
&'a dyn Fn(&Value, &mut C, &mut dyn FnMut(&Value, &mut C) -> Result<f64, ExpressionError>)
-> Result<f64, ExpressionError>;
enum Arity {
Unary { operand: &'static str },
Binary { left: &'static str, right: &'static str },
Nary { operands: &'static str },
Ternary { a: &'static str, b: &'static str, c: &'static str },
}
const ARITH: Arity = Arity::Binary { left: "arithLeft", right: "arithRight" };
const COMPARE: Arity = Arity::Binary { left: "compareLeft", right: "compareRight" };
const VALUE: Arity = Arity::Unary { operand: "value" };
fn operator_arity(typ: &str) -> Option<Arity> {
match typ {
"kanonak.org/transformations/Add"
| "kanonak.org/transformations/Subtract"
| "kanonak.org/transformations/Multiply"
| "kanonak.org/transformations/Divide"
| "kanonak.org/math/Power"
| "kanonak.org/math/Modulo"
| "kanonak.org/math/Minimum"
| "kanonak.org/math/Maximum" => Some(ARITH),
"kanonak.org/transformations/Abs"
| "kanonak.org/transformations/Negate"
| "kanonak.org/math/Exp"
| "kanonak.org/math/Ln"
| "kanonak.org/math/Log10"
| "kanonak.org/math/Sqrt"
| "kanonak.org/math/Floor"
| "kanonak.org/math/Ceil"
| "kanonak.org/math/Round"
| "kanonak.org/math/Sign" => Some(VALUE),
"kanonak.org/transformations/Equals"
| "kanonak.org/transformations/GreaterThan"
| "kanonak.org/transformations/LessThan"
| "kanonak.org/transformations/GreaterThanOrEqual"
| "kanonak.org/transformations/LessThanOrEqual" => Some(COMPARE),
"kanonak.org/transformations/And" | "kanonak.org/transformations/Or" => {
Some(Arity::Nary { operands: "operands" })
}
"kanonak.org/math/Clip" => {
Some(Arity::Ternary { a: "clipValue", b: "clipLower", c: "clipUpper" })
}
_ => None,
}
}
fn floored_mod(a: f64, b: f64) -> Result<f64, ExpressionError> {
if b == 0.0 {
return err("Modulo by zero");
}
Ok(a - b * (a / b).floor())
}
fn round_half_away(a: f64) -> f64 {
if a < 0.0 {
-(((-a) + 0.5).floor())
} else {
(a + 0.5).floor()
}
}
fn sign(x: f64) -> f64 {
if x > 0.0 {
1.0
} else if x < 0.0 {
-1.0
} else {
0.0
}
}
fn truthy(n: f64) -> bool {
n != 0.0
}
fn boolnum(b: bool) -> f64 {
if b {
1.0
} else {
0.0
}
}
fn unary(typ: &str, x: f64) -> Result<f64, ExpressionError> {
match typ {
"kanonak.org/transformations/Abs" => Ok(x.abs()),
"kanonak.org/transformations/Negate" => Ok(-x),
"kanonak.org/math/Exp" => Ok(x.exp()),
"kanonak.org/math/Ln" => {
if x > 0.0 {
Ok(x.ln())
} else {
err("Ln of a non-positive number")
}
}
"kanonak.org/math/Log10" => {
if x > 0.0 {
Ok(x.log10())
} else {
err("Log10 of a non-positive number")
}
}
"kanonak.org/math/Sqrt" => {
if x >= 0.0 {
Ok(x.sqrt())
} else {
err("Sqrt of a negative number")
}
}
"kanonak.org/math/Floor" => Ok(x.floor()),
"kanonak.org/math/Ceil" => Ok(x.ceil()),
"kanonak.org/math/Round" => Ok(round_half_away(x)),
"kanonak.org/math/Sign" => Ok(sign(x)),
_ => err(format!("{typ} has no unary primitive")),
}
}
fn binary(typ: &str, a: f64, b: f64) -> Result<f64, ExpressionError> {
match typ {
"kanonak.org/transformations/Add" => Ok(a + b),
"kanonak.org/transformations/Subtract" => Ok(a - b),
"kanonak.org/transformations/Multiply" => Ok(a * b),
"kanonak.org/transformations/Divide" => {
if b == 0.0 {
err("Divide by zero")
} else {
Ok(a / b)
}
}
"kanonak.org/math/Power" => Ok(a.powf(b)),
"kanonak.org/math/Modulo" => floored_mod(a, b),
"kanonak.org/math/Minimum" => Ok(a.min(b)),
"kanonak.org/math/Maximum" => Ok(a.max(b)),
"kanonak.org/transformations/Equals" => Ok(boolnum(a == b)),
"kanonak.org/transformations/GreaterThan" => Ok(boolnum(a > b)),
"kanonak.org/transformations/LessThan" => Ok(boolnum(a < b)),
"kanonak.org/transformations/GreaterThanOrEqual" => Ok(boolnum(a >= b)),
"kanonak.org/transformations/LessThanOrEqual" => Ok(boolnum(a <= b)),
_ => err(format!("{typ} has no binary primitive")),
}
}
fn literal_value(node: &Value, typ: &str) -> Option<f64> {
match typ {
"kanonak.org/transformations/IntegerLiteral" => node.get("integerLiteral").and_then(as_number),
"kanonak.org/transformations/DecimalLiteral" => node.get("decimalLiteral").and_then(as_number),
"kanonak.org/transformations/BooleanLiteral" => {
let v = node.get("booleanLiteral");
let truthy = matches!(v, Some(Value::Bool(true)))
|| matches!(v, Some(Value::String(s)) if s == "true");
Some(boolnum(truthy))
}
_ => None,
}
}
fn as_number(v: &Value) -> Option<f64> {
match v {
Value::Number(n) => n.as_f64(),
Value::String(s) => s.parse::<f64>().ok(),
Value::Bool(b) => Some(boolnum(*b)),
_ => None,
}
}
fn node_type(node: &Value) -> Result<&str, ExpressionError> {
match node.get("type").and_then(|t| t.as_str()) {
Some(t) => Ok(t),
None => err("node is missing a 'type'"),
}
}
fn operand<'a>(node: &'a Value, typ: &str, key: &str) -> Result<&'a Value, ExpressionError> {
match node.get(key) {
Some(v) if v.is_object() => Ok(v),
_ => err(format!("{typ} is missing operand '{key}'")),
}
}
pub fn evaluate<C>(
node: &Value,
ctx: &mut C,
resolve: Resolve<C>,
) -> Result<f64, ExpressionError> {
fn go<C>(
node: &Value,
ctx: &mut C,
resolve: Resolve<C>,
) -> Result<f64, ExpressionError> {
let typ = node_type(node)?;
if let Some(arity) = operator_arity(typ) {
return match arity {
Arity::Unary { operand: key } => {
let x = go(operand(node, typ, key)?, ctx, resolve)?;
unary(typ, x)
}
Arity::Binary { left, right } => {
let a = go(operand(node, typ, left)?, ctx, resolve)?;
let b = go(operand(node, typ, right)?, ctx, resolve)?;
binary(typ, a, b)
}
Arity::Nary { operands } => {
let items = match node.get(operands).and_then(|v| v.as_array()) {
Some(arr) => arr,
None => return err(format!("{typ} expects an '{operands}' list")),
};
let is_and = typ == "kanonak.org/transformations/And";
for item in items {
let v = truthy(go(item, ctx, resolve)?);
if is_and && !v {
return Ok(0.0);
}
if !is_and && v {
return Ok(1.0);
}
}
Ok(boolnum(is_and))
}
Arity::Ternary { a, b, c } => {
let v = go(operand(node, typ, a)?, ctx, resolve)?;
let lo = go(operand(node, typ, b)?, ctx, resolve)?;
let hi = go(operand(node, typ, c)?, ctx, resolve)?;
Ok(v.max(lo).min(hi))
}
};
}
if typ == "kanonak.org/transformations/Not" {
let inner = go(operand(node, typ, "operand")?, ctx, resolve)?;
return Ok(boolnum(!truthy(inner)));
}
if let Some(lit) = literal_value(node, typ) {
return Ok(lit);
}
let mut recurse =
|n: &Value, c: &mut C| -> Result<f64, ExpressionError> { go(n, c, resolve) };
resolve(node, ctx, &mut recurse)
}
go(node, ctx, resolve)
}