1use serde_json::Value;
31
32pub const EXPRESSION_RUNTIME_VERSION: &str = "1";
34
35#[derive(Debug, Clone)]
38pub struct ExpressionError(pub String);
39
40impl std::fmt::Display for ExpressionError {
41 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
42 write!(f, "{}", self.0)
43 }
44}
45
46impl std::error::Error for ExpressionError {}
47
48fn err<T>(msg: impl Into<String>) -> Result<T, ExpressionError> {
49 Err(ExpressionError(msg.into()))
50}
51
52pub type Resolve<'a, C> =
58 &'a dyn Fn(&Value, &mut C, &mut dyn FnMut(&Value, &mut C) -> Result<f64, ExpressionError>)
59 -> Result<f64, ExpressionError>;
60
61pub type ResolveRef<'a, C> = &'a dyn Fn(&Value, &mut C) -> Result<String, ExpressionError>;
66
67pub type ClosureTable =
74 std::collections::HashMap<String, std::collections::HashMap<String, Vec<String>>>;
75
76pub struct EvalOptions<'a, C> {
80 pub closures: Option<&'a ClosureTable>,
81 pub resolve_ref: Option<ResolveRef<'a, C>>,
82}
83
84#[derive(Debug, Clone)]
92pub struct TraceNode {
93 pub typ: String,
94 pub value: f64,
95 pub children: Vec<TraceNode>,
96 pub left_ref: Option<String>,
97 pub right_ref: Option<String>,
98}
99
100enum Arity {
102 Unary { operand: &'static str },
103 Binary { left: &'static str, right: &'static str },
104 Nary { operands: &'static str },
105 Ternary { a: &'static str, b: &'static str, c: &'static str },
106}
107
108const ARITH: Arity = Arity::Binary { left: "arithLeft", right: "arithRight" };
109const COMPARE: Arity = Arity::Binary { left: "compareLeft", right: "compareRight" };
110const VALUE: Arity = Arity::Unary { operand: "value" };
111
112fn operator_arity(typ: &str) -> Option<Arity> {
116 match typ {
117 "kanonak.org/transformations/Add"
119 | "kanonak.org/transformations/Subtract"
120 | "kanonak.org/transformations/Multiply"
121 | "kanonak.org/transformations/Divide"
122 | "kanonak.org/math/Power"
123 | "kanonak.org/math/Modulo"
124 | "kanonak.org/math/Minimum"
125 | "kanonak.org/math/Maximum" => Some(ARITH),
126 "kanonak.org/transformations/Abs"
128 | "kanonak.org/transformations/Negate"
129 | "kanonak.org/math/Exp"
130 | "kanonak.org/math/Ln"
131 | "kanonak.org/math/Log10"
132 | "kanonak.org/math/Sqrt"
133 | "kanonak.org/math/Floor"
134 | "kanonak.org/math/Ceil"
135 | "kanonak.org/math/Round"
136 | "kanonak.org/math/Sign" => Some(VALUE),
137 "kanonak.org/transformations/Equals"
139 | "kanonak.org/transformations/GreaterThan"
140 | "kanonak.org/transformations/LessThan"
141 | "kanonak.org/transformations/GreaterThanOrEqual"
142 | "kanonak.org/transformations/LessThanOrEqual" => Some(COMPARE),
143 "kanonak.org/transformations/And" | "kanonak.org/transformations/Or" => {
145 Some(Arity::Nary { operands: "operands" })
146 }
147 "kanonak.org/math/Clip" => {
149 Some(Arity::Ternary { a: "clipValue", b: "clipLower", c: "clipUpper" })
150 }
151 _ => None,
152 }
153}
154
155fn floored_mod(a: f64, b: f64) -> Result<f64, ExpressionError> {
157 if b == 0.0 {
158 return err("Modulo by zero");
159 }
160 Ok(a - b * (a / b).floor())
161}
162
163fn round_half_away(a: f64) -> f64 {
165 if a < 0.0 {
167 -(((-a) + 0.5).floor())
168 } else {
169 (a + 0.5).floor()
170 }
171}
172
173fn sign(x: f64) -> f64 {
174 if x > 0.0 {
175 1.0
176 } else if x < 0.0 {
177 -1.0
178 } else {
179 0.0
180 }
181}
182
183fn truthy(n: f64) -> bool {
184 n != 0.0
185}
186
187fn boolnum(b: bool) -> f64 {
188 if b {
189 1.0
190 } else {
191 0.0
192 }
193}
194
195fn unary(typ: &str, x: f64) -> Result<f64, ExpressionError> {
198 match typ {
199 "kanonak.org/transformations/Abs" => Ok(x.abs()),
200 "kanonak.org/transformations/Negate" => Ok(-x),
201 "kanonak.org/math/Exp" => Ok(x.exp()),
202 "kanonak.org/math/Ln" => {
203 if x > 0.0 {
204 Ok(x.ln())
205 } else {
206 err("Ln of a non-positive number")
207 }
208 }
209 "kanonak.org/math/Log10" => {
210 if x > 0.0 {
211 Ok(x.log10())
212 } else {
213 err("Log10 of a non-positive number")
214 }
215 }
216 "kanonak.org/math/Sqrt" => {
217 if x >= 0.0 {
218 Ok(x.sqrt())
219 } else {
220 err("Sqrt of a negative number")
221 }
222 }
223 "kanonak.org/math/Floor" => Ok(x.floor()),
224 "kanonak.org/math/Ceil" => Ok(x.ceil()),
225 "kanonak.org/math/Round" => Ok(round_half_away(x)),
226 "kanonak.org/math/Sign" => Ok(sign(x)),
227 _ => err(format!("{typ} has no unary primitive")),
228 }
229}
230
231fn binary(typ: &str, a: f64, b: f64) -> Result<f64, ExpressionError> {
233 match typ {
234 "kanonak.org/transformations/Add" => Ok(a + b),
235 "kanonak.org/transformations/Subtract" => Ok(a - b),
236 "kanonak.org/transformations/Multiply" => Ok(a * b),
237 "kanonak.org/transformations/Divide" => {
238 if b == 0.0 {
239 err("Divide by zero")
240 } else {
241 Ok(a / b)
242 }
243 }
244 "kanonak.org/math/Power" => Ok(a.powf(b)),
245 "kanonak.org/math/Modulo" => floored_mod(a, b),
246 "kanonak.org/math/Minimum" => Ok(a.min(b)),
247 "kanonak.org/math/Maximum" => Ok(a.max(b)),
248 "kanonak.org/transformations/Equals" => Ok(boolnum(a == b)),
249 "kanonak.org/transformations/GreaterThan" => Ok(boolnum(a > b)),
250 "kanonak.org/transformations/LessThan" => Ok(boolnum(a < b)),
251 "kanonak.org/transformations/GreaterThanOrEqual" => Ok(boolnum(a >= b)),
252 "kanonak.org/transformations/LessThanOrEqual" => Ok(boolnum(a <= b)),
253 _ => err(format!("{typ} has no binary primitive")),
254 }
255}
256
257fn literal_value(node: &Value, typ: &str) -> Option<f64> {
259 match typ {
260 "kanonak.org/transformations/IntegerLiteral" => node.get("integerLiteral").and_then(as_number),
261 "kanonak.org/transformations/DecimalLiteral" => node.get("decimalLiteral").and_then(as_number),
262 "kanonak.org/transformations/BooleanLiteral" => {
263 let v = node.get("booleanLiteral");
264 let truthy = matches!(v, Some(Value::Bool(true)))
265 || matches!(v, Some(Value::String(s)) if s == "true");
266 Some(boolnum(truthy))
267 }
268 _ => None,
269 }
270}
271
272fn as_number(v: &Value) -> Option<f64> {
273 match v {
274 Value::Number(n) => n.as_f64(),
275 Value::String(s) => s.parse::<f64>().ok(),
276 Value::Bool(b) => Some(boolnum(*b)),
277 _ => None,
278 }
279}
280
281fn node_type(node: &Value) -> Result<&str, ExpressionError> {
283 match node.get("type").and_then(|t| t.as_str()) {
284 Some(t) => Ok(t),
285 None => err("node is missing a 'type'"),
286 }
287}
288
289fn operand<'a>(node: &'a Value, typ: &str, key: &str) -> Result<&'a Value, ExpressionError> {
290 match node.get(key) {
291 Some(v) if v.is_object() => Ok(v),
292 _ => err(format!("{typ} is missing operand '{key}'")),
293 }
294}
295
296fn identity_of<C>(
301 node: &Value,
302 ctx: &mut C,
303 options: Option<&EvalOptions<C>>,
304) -> Result<String, ExpressionError> {
305 let typ = node_type(node)?;
306 if typ == "kanonak.org/transformations/UriLiteral" {
307 return match node.get("refTo").and_then(|v| v.as_str()) {
308 Some(s) if !s.is_empty() => Ok(s.to_string()),
309 _ => err("UriLiteral is missing refTo"),
310 };
311 }
312 match options.and_then(|o| o.resolve_ref) {
313 Some(resolve_ref) => resolve_ref(node, ctx),
314 None => err(format!("No resolveRef supplied for identity leaf '{typ}'")),
315 }
316}
317
318fn fold_ordered<C>(
327 node: &Value,
328 typ: &str,
329 ctx: &mut C,
330 options: Option<&EvalOptions<C>>,
331) -> Result<(f64, String, String), ExpressionError> {
332 let via = match node.get("viaProperty").and_then(|v| v.as_str()) {
333 Some(s) if !s.is_empty() => s,
334 _ => return err(format!("{typ} is missing viaProperty")),
335 };
336 let left = identity_of(operand(node, typ, "compareLeft")?, ctx, options)?;
337 let right = identity_of(operand(node, typ, "compareRight")?, ctx, options)?;
338 let closure = match options.and_then(|o| o.closures).and_then(|c| c.get(via)) {
339 Some(c) => c,
340 None => return err(format!("No closure supplied for ordering property '{via}'")),
341 };
342 let value = if left == right {
343 boolnum(typ == "kanonak.org/transformations/IsAtLeast")
344 } else {
345 boolnum(closure.get(&left).map_or(false, |set| set.iter().any(|m| m == &right)))
346 };
347 Ok((value, left, right))
348}
349
350pub fn evaluate<C>(
354 node: &Value,
355 ctx: &mut C,
356 resolve: Resolve<C>,
357) -> Result<f64, ExpressionError> {
358 evaluate_with_options(node, ctx, resolve, None)
359}
360
361pub fn evaluate_with_options<C>(
365 node: &Value,
366 ctx: &mut C,
367 resolve: Resolve<C>,
368 options: Option<&EvalOptions<C>>,
369) -> Result<f64, ExpressionError> {
370 fn go<C>(
371 node: &Value,
372 ctx: &mut C,
373 resolve: Resolve<C>,
374 options: Option<&EvalOptions<C>>,
375 ) -> Result<f64, ExpressionError> {
376 let typ = node_type(node)?;
377
378 if let Some(arity) = operator_arity(typ) {
379 return match arity {
380 Arity::Unary { operand: key } => {
381 let x = go(operand(node, typ, key)?, ctx, resolve, options)?;
382 unary(typ, x)
383 }
384 Arity::Binary { left, right } => {
385 let a = go(operand(node, typ, left)?, ctx, resolve, options)?;
386 let b = go(operand(node, typ, right)?, ctx, resolve, options)?;
387 binary(typ, a, b)
388 }
389 Arity::Nary { operands } => {
390 let items = match node.get(operands).and_then(|v| v.as_array()) {
391 Some(arr) => arr,
392 None => return err(format!("{typ} expects an '{operands}' list")),
393 };
394 let is_and = typ == "kanonak.org/transformations/And";
395 for item in items {
397 let v = truthy(go(item, ctx, resolve, options)?);
398 if is_and && !v {
399 return Ok(0.0);
400 }
401 if !is_and && v {
402 return Ok(1.0);
403 }
404 }
405 Ok(boolnum(is_and))
406 }
407 Arity::Ternary { a, b, c } => {
408 let v = go(operand(node, typ, a)?, ctx, resolve, options)?;
410 let lo = go(operand(node, typ, b)?, ctx, resolve, options)?;
411 let hi = go(operand(node, typ, c)?, ctx, resolve, options)?;
412 Ok(v.max(lo).min(hi))
413 }
414 };
415 }
416
417 if typ == "kanonak.org/transformations/Not" {
418 let inner = go(operand(node, typ, "operand")?, ctx, resolve, options)?;
419 return Ok(boolnum(!truthy(inner)));
420 }
421
422 if typ == "kanonak.org/transformations/IsAtLeast"
423 || typ == "kanonak.org/transformations/Dominates"
424 {
425 return fold_ordered(node, typ, ctx, options).map(|(v, _, _)| v);
426 }
427
428 if let Some(lit) = literal_value(node, typ) {
429 return Ok(lit);
430 }
431
432 let mut recurse =
434 |n: &Value, c: &mut C| -> Result<f64, ExpressionError> { go(n, c, resolve, options) };
435 resolve(node, ctx, &mut recurse)
436 }
437
438 go(node, ctx, resolve, options)
439}
440
441pub fn explain<C>(
450 node: &Value,
451 ctx: &mut C,
452 resolve: Resolve<C>,
453 options: Option<&EvalOptions<C>>,
454) -> Result<TraceNode, ExpressionError> {
455 fn leaf(typ: &str, value: f64) -> TraceNode {
456 TraceNode { typ: typ.to_string(), value, children: Vec::new(), left_ref: None, right_ref: None }
457 }
458 fn parent(typ: &str, value: f64, children: Vec<TraceNode>) -> TraceNode {
459 TraceNode { typ: typ.to_string(), value, children, left_ref: None, right_ref: None }
460 }
461
462 fn go<C>(
463 node: &Value,
464 ctx: &mut C,
465 resolve: Resolve<C>,
466 options: Option<&EvalOptions<C>>,
467 ) -> Result<TraceNode, ExpressionError> {
468 let typ = node_type(node)?;
469
470 if let Some(arity) = operator_arity(typ) {
471 return match arity {
472 Arity::Unary { operand: key } => {
473 let x = go(operand(node, typ, key)?, ctx, resolve, options)?;
474 let value = unary(typ, x.value)?;
475 Ok(parent(typ, value, vec![x]))
476 }
477 Arity::Binary { left, right } => {
478 let a = go(operand(node, typ, left)?, ctx, resolve, options)?;
479 let b = go(operand(node, typ, right)?, ctx, resolve, options)?;
480 let value = binary(typ, a.value, b.value)?;
481 Ok(parent(typ, value, vec![a, b]))
482 }
483 Arity::Nary { operands } => {
484 let items = match node.get(operands).and_then(|v| v.as_array()) {
485 Some(arr) => arr,
486 None => return err(format!("{typ} expects an '{operands}' list")),
487 };
488 let is_and = typ == "kanonak.org/transformations/And";
489 let mut children = Vec::new();
490 for item in items {
491 let child = go(item, ctx, resolve, options)?;
492 let v = truthy(child.value);
493 children.push(child);
494 if is_and && !v {
497 return Ok(parent(typ, 0.0, children));
498 }
499 if !is_and && v {
500 return Ok(parent(typ, 1.0, children));
501 }
502 }
503 Ok(parent(typ, boolnum(is_and), children))
504 }
505 Arity::Ternary { a, b, c } => {
506 let v = go(operand(node, typ, a)?, ctx, resolve, options)?;
507 let lo = go(operand(node, typ, b)?, ctx, resolve, options)?;
508 let hi = go(operand(node, typ, c)?, ctx, resolve, options)?;
509 let value = v.value.max(lo.value).min(hi.value);
510 Ok(parent(typ, value, vec![v, lo, hi]))
511 }
512 };
513 }
514
515 if typ == "kanonak.org/transformations/Not" {
516 let x = go(operand(node, typ, "operand")?, ctx, resolve, options)?;
517 let value = boolnum(!truthy(x.value));
518 return Ok(parent(typ, value, vec![x]));
519 }
520
521 if typ == "kanonak.org/transformations/IsAtLeast"
522 || typ == "kanonak.org/transformations/Dominates"
523 {
524 let (value, left, right) = fold_ordered(node, typ, ctx, options)?;
525 return Ok(TraceNode {
526 typ: typ.to_string(),
527 value,
528 children: Vec::new(),
529 left_ref: Some(left),
530 right_ref: Some(right),
531 });
532 }
533
534 if let Some(lit) = literal_value(node, typ) {
535 return Ok(leaf(typ, lit));
536 }
537
538 let mut recurse = |n: &Value, c: &mut C| -> Result<f64, ExpressionError> {
542 evaluate_with_options(n, c, resolve, options)
543 };
544 let value = resolve(node, ctx, &mut recurse)?;
545 Ok(leaf(typ, value))
546 }
547
548 go(node, ctx, resolve, options)
549}