1use intent_ir::{ArithOp, CmpOp, IrExpr, IrLiteral};
2use serde_json::Value;
3
4use crate::error::RuntimeError;
5use crate::value::EvalContext;
6
7pub fn evaluate(expr: &IrExpr, ctx: &EvalContext) -> Result<Value, RuntimeError> {
12 match expr {
13 IrExpr::Literal(lit) => eval_literal(lit),
14 IrExpr::Var(name) => eval_var(name, ctx),
15 IrExpr::FieldAccess { root, field } => eval_field_access(root, field, ctx),
16 IrExpr::Compare { left, op, right } => eval_compare(left, *op, right, ctx),
17 IrExpr::Arithmetic { left, op, right } => eval_arithmetic(left, *op, right, ctx),
18 IrExpr::And(left, right) => eval_and(left, right, ctx),
19 IrExpr::Or(left, right) => eval_or(left, right, ctx),
20 IrExpr::Not(inner) => eval_not(inner, ctx),
21 IrExpr::Implies(left, right) => eval_implies(left, right, ctx),
22 IrExpr::Old(inner) => eval_old(inner, ctx),
23 IrExpr::Forall { binding, ty, body } => eval_forall(binding, ty, body, ctx),
24 IrExpr::Exists { binding, ty, body } => eval_exists(binding, ty, body, ctx),
25 IrExpr::Call { name, args } => eval_call(name, args, ctx),
26 IrExpr::List(items) => eval_list(items, ctx),
27 }
28}
29
30fn eval_literal(lit: &IrLiteral) -> Result<Value, RuntimeError> {
31 match lit {
32 IrLiteral::Null => Ok(Value::Null),
33 IrLiteral::Bool(b) => Ok(Value::Bool(*b)),
34 IrLiteral::Int(n) => Ok(json_number_i64(*n)),
35 IrLiteral::Decimal(s) => parse_decimal_value(s),
36 IrLiteral::String(s) => Ok(Value::String(s.clone())),
37 }
38}
39
40fn eval_var(name: &str, ctx: &EvalContext) -> Result<Value, RuntimeError> {
41 if let Some(val) = ctx.bindings.get(name) {
43 return Ok(val.clone());
44 }
45 if name.starts_with(|c: char| c.is_uppercase()) {
47 return Ok(Value::String(name.to_string()));
48 }
49 Err(RuntimeError::UnboundVariable(name.to_string()))
50}
51
52fn eval_field_access(root: &IrExpr, field: &str, ctx: &EvalContext) -> Result<Value, RuntimeError> {
53 let root_val = evaluate(root, ctx)?;
54 match &root_val {
55 Value::Object(map) => map.get(field).cloned().ok_or(RuntimeError::FieldNotFound {
56 field: field.to_string(),
57 }),
58 Value::Null => Err(RuntimeError::FieldNotFound {
59 field: field.to_string(),
60 }),
61 _ => Err(RuntimeError::NotAnObject),
62 }
63}
64
65fn eval_compare(
66 left: &IrExpr,
67 op: CmpOp,
68 right: &IrExpr,
69 ctx: &EvalContext,
70) -> Result<Value, RuntimeError> {
71 let lhs = evaluate(left, ctx)?;
72 let rhs = evaluate(right, ctx)?;
73
74 let result = match op {
75 CmpOp::Eq => values_equal(&lhs, &rhs),
76 CmpOp::Ne => !values_equal(&lhs, &rhs),
77 CmpOp::Lt => values_compare(&lhs, &rhs)?.is_some_and(|o| o.is_lt()),
78 CmpOp::Gt => values_compare(&lhs, &rhs)?.is_some_and(|o| o.is_gt()),
79 CmpOp::Le => values_compare(&lhs, &rhs)?.is_some_and(|o| o.is_le()),
80 CmpOp::Ge => values_compare(&lhs, &rhs)?.is_some_and(|o| o.is_ge()),
81 };
82 Ok(Value::Bool(result))
83}
84
85fn eval_arithmetic(
86 left: &IrExpr,
87 op: ArithOp,
88 right: &IrExpr,
89 ctx: &EvalContext,
90) -> Result<Value, RuntimeError> {
91 let lhs = evaluate(left, ctx)?;
92 let rhs = evaluate(right, ctx)?;
93
94 match (&lhs, &rhs) {
95 (Value::Number(a), Value::Number(b)) => {
96 let af = as_f64(a);
97 let bf = as_f64(b);
98 let result = match op {
99 ArithOp::Add => af + bf,
100 ArithOp::Sub => af - bf,
101 };
102 Ok(json_number_f64(result))
103 }
104 _ => Err(RuntimeError::TypeError {
105 expected: "number".into(),
106 got: type_name(&lhs),
107 }),
108 }
109}
110
111fn eval_and(left: &IrExpr, right: &IrExpr, ctx: &EvalContext) -> Result<Value, RuntimeError> {
112 let lhs = as_bool(&evaluate(left, ctx)?)?;
113 if !lhs {
114 return Ok(Value::Bool(false)); }
116 let rhs = as_bool(&evaluate(right, ctx)?)?;
117 Ok(Value::Bool(rhs))
118}
119
120fn eval_or(left: &IrExpr, right: &IrExpr, ctx: &EvalContext) -> Result<Value, RuntimeError> {
121 let lhs = as_bool(&evaluate(left, ctx)?)?;
122 if lhs {
123 return Ok(Value::Bool(true)); }
125 let rhs = as_bool(&evaluate(right, ctx)?)?;
126 Ok(Value::Bool(rhs))
127}
128
129fn eval_not(inner: &IrExpr, ctx: &EvalContext) -> Result<Value, RuntimeError> {
130 let val = as_bool(&evaluate(inner, ctx)?)?;
131 Ok(Value::Bool(!val))
132}
133
134fn eval_implies(left: &IrExpr, right: &IrExpr, ctx: &EvalContext) -> Result<Value, RuntimeError> {
135 let lhs = as_bool(&evaluate(left, ctx)?)?;
137 if !lhs {
138 return Ok(Value::Bool(true));
139 }
140 let rhs = as_bool(&evaluate(right, ctx)?)?;
141 Ok(Value::Bool(rhs))
142}
143
144fn eval_old(inner: &IrExpr, ctx: &EvalContext) -> Result<Value, RuntimeError> {
145 let old_bindings = ctx
146 .old_bindings
147 .as_ref()
148 .ok_or(RuntimeError::OldWithoutContext)?;
149
150 let old_ctx = EvalContext {
151 bindings: old_bindings.clone(),
152 old_bindings: None,
153 instances: ctx.instances.clone(),
154 };
155 evaluate(inner, &old_ctx)
156}
157
158fn eval_forall(
159 binding: &str,
160 ty: &str,
161 body: &IrExpr,
162 ctx: &EvalContext,
163) -> Result<Value, RuntimeError> {
164 let instances = ctx
165 .instances
166 .get(ty)
167 .ok_or_else(|| RuntimeError::NoInstances(ty.to_string()))?;
168
169 for instance in instances {
170 let child_ctx = ctx.with_binding(binding.to_string(), instance.clone());
171 let result = as_bool(&evaluate(body, &child_ctx)?)?;
172 if !result {
173 return Ok(Value::Bool(false));
174 }
175 }
176 Ok(Value::Bool(true))
177}
178
179fn eval_exists(
180 binding: &str,
181 ty: &str,
182 body: &IrExpr,
183 ctx: &EvalContext,
184) -> Result<Value, RuntimeError> {
185 let instances = ctx
186 .instances
187 .get(ty)
188 .ok_or_else(|| RuntimeError::NoInstances(ty.to_string()))?;
189
190 for instance in instances {
191 let child_ctx = ctx.with_binding(binding.to_string(), instance.clone());
192 let result = as_bool(&evaluate(body, &child_ctx)?)?;
193 if result {
194 return Ok(Value::Bool(true));
195 }
196 }
197 Ok(Value::Bool(false))
198}
199
200fn eval_call(name: &str, args: &[IrExpr], ctx: &EvalContext) -> Result<Value, RuntimeError> {
201 let evaluated_args: Vec<Value> = args
202 .iter()
203 .map(|a| evaluate(a, ctx))
204 .collect::<Result<_, _>>()?;
205
206 match name {
208 "len" => {
209 if let Some(val) = evaluated_args.first() {
210 match val {
211 Value::Array(arr) => Ok(json_number_i64(arr.len() as i64)),
212 Value::String(s) => Ok(json_number_i64(s.len() as i64)),
213 _ => Err(RuntimeError::TypeError {
214 expected: "array or string".into(),
215 got: type_name(val),
216 }),
217 }
218 } else {
219 Err(RuntimeError::TypeError {
220 expected: "1 argument".into(),
221 got: "0 arguments".into(),
222 })
223 }
224 }
225 "now" => Ok(Value::String("now()".to_string())),
226 _ => Err(RuntimeError::UnknownFunction(name.to_string())),
227 }
228}
229
230fn eval_list(items: &[IrExpr], ctx: &EvalContext) -> Result<Value, RuntimeError> {
231 let values: Vec<Value> = items
232 .iter()
233 .map(|i| evaluate(i, ctx))
234 .collect::<Result<_, _>>()?;
235 Ok(Value::Array(values))
236}
237
238fn as_bool(val: &Value) -> Result<bool, RuntimeError> {
241 match val {
242 Value::Bool(b) => Ok(*b),
243 _ => Err(RuntimeError::TypeError {
244 expected: "bool".into(),
245 got: type_name(val),
246 }),
247 }
248}
249
250fn as_f64(n: &serde_json::Number) -> f64 {
251 n.as_f64().unwrap_or(0.0)
252}
253
254fn json_number_i64(n: i64) -> Value {
255 Value::Number(serde_json::Number::from(n))
256}
257
258fn json_number_f64(n: f64) -> Value {
259 serde_json::Number::from_f64(n).map_or(Value::Null, Value::Number)
260}
261
262fn parse_decimal_value(s: &str) -> Result<Value, RuntimeError> {
263 s.parse::<f64>()
264 .map(json_number_f64)
265 .map_err(|_| RuntimeError::DecimalError(format!("invalid decimal: {s}")))
266}
267
268fn type_name(val: &Value) -> String {
269 match val {
270 Value::Null => "null".into(),
271 Value::Bool(_) => "bool".into(),
272 Value::Number(_) => "number".into(),
273 Value::String(_) => "string".into(),
274 Value::Array(_) => "array".into(),
275 Value::Object(_) => "object".into(),
276 }
277}
278
279fn values_equal(a: &Value, b: &Value) -> bool {
280 match (a, b) {
281 (Value::Number(a), Value::Number(b)) => as_f64(a) == as_f64(b),
282 _ => a == b,
283 }
284}
285
286fn values_compare(a: &Value, b: &Value) -> Result<Option<std::cmp::Ordering>, RuntimeError> {
287 match (a, b) {
288 (Value::Number(a), Value::Number(b)) => Ok(as_f64(a).partial_cmp(&as_f64(b))),
289 (Value::String(a), Value::String(b)) => Ok(Some(a.cmp(b))),
290 _ => Err(RuntimeError::TypeError {
291 expected: "comparable types (number or string)".into(),
292 got: format!("{} vs {}", type_name(a), type_name(b)),
293 }),
294 }
295}
296
297#[cfg(test)]
298mod tests {
299 use super::*;
300 use serde_json::json;
301 use std::collections::HashMap;
302
303 fn empty_ctx() -> EvalContext {
304 EvalContext::new()
305 }
306
307 fn ctx_with(bindings: Vec<(&str, Value)>) -> EvalContext {
308 let mut ctx = EvalContext::new();
309 for (k, v) in bindings {
310 ctx.bindings.insert(k.to_string(), v);
311 }
312 ctx
313 }
314
315 #[test]
318 fn eval_literal_null() {
319 let expr = IrExpr::Literal(IrLiteral::Null);
320 assert_eq!(evaluate(&expr, &empty_ctx()).unwrap(), Value::Null);
321 }
322
323 #[test]
324 fn eval_literal_bool() {
325 let expr = IrExpr::Literal(IrLiteral::Bool(true));
326 assert_eq!(evaluate(&expr, &empty_ctx()).unwrap(), json!(true));
327 }
328
329 #[test]
330 fn eval_literal_int() {
331 let expr = IrExpr::Literal(IrLiteral::Int(42));
332 assert_eq!(evaluate(&expr, &empty_ctx()).unwrap(), json!(42));
333 }
334
335 #[test]
336 fn eval_literal_decimal() {
337 let expr = IrExpr::Literal(IrLiteral::Decimal("10.50".into()));
338 assert_eq!(evaluate(&expr, &empty_ctx()).unwrap(), json!(10.50));
339 }
340
341 #[test]
342 fn eval_literal_string() {
343 let expr = IrExpr::Literal(IrLiteral::String("hello".into()));
344 assert_eq!(evaluate(&expr, &empty_ctx()).unwrap(), json!("hello"));
345 }
346
347 #[test]
350 fn eval_bound_variable() {
351 let ctx = ctx_with(vec![("amount", json!(100))]);
352 let expr = IrExpr::Var("amount".into());
353 assert_eq!(evaluate(&expr, &ctx).unwrap(), json!(100));
354 }
355
356 #[test]
357 fn eval_unbound_variable_error() {
358 let expr = IrExpr::Var("unknown".into());
359 assert_eq!(
360 evaluate(&expr, &empty_ctx()),
361 Err(RuntimeError::UnboundVariable("unknown".into()))
362 );
363 }
364
365 #[test]
366 fn eval_union_variant_as_string() {
367 let expr = IrExpr::Var("Active".into());
368 assert_eq!(evaluate(&expr, &empty_ctx()).unwrap(), json!("Active"));
369 }
370
371 #[test]
374 fn eval_field_access() {
375 let ctx = ctx_with(vec![("account", json!({"balance": 500, "name": "Alice"}))]);
376 let expr = IrExpr::FieldAccess {
377 root: Box::new(IrExpr::Var("account".into())),
378 field: "balance".into(),
379 };
380 assert_eq!(evaluate(&expr, &ctx).unwrap(), json!(500));
381 }
382
383 #[test]
384 fn eval_nested_field_access() {
385 let ctx = ctx_with(vec![("user", json!({"profile": {"email": "a@b.com"}}))]);
386 let expr = IrExpr::FieldAccess {
387 root: Box::new(IrExpr::FieldAccess {
388 root: Box::new(IrExpr::Var("user".into())),
389 field: "profile".into(),
390 }),
391 field: "email".into(),
392 };
393 assert_eq!(evaluate(&expr, &ctx).unwrap(), json!("a@b.com"));
394 }
395
396 #[test]
397 fn eval_field_not_found() {
398 let ctx = ctx_with(vec![("obj", json!({"a": 1}))]);
399 let expr = IrExpr::FieldAccess {
400 root: Box::new(IrExpr::Var("obj".into())),
401 field: "missing".into(),
402 };
403 assert_eq!(
404 evaluate(&expr, &ctx),
405 Err(RuntimeError::FieldNotFound {
406 field: "missing".into()
407 })
408 );
409 }
410
411 #[test]
412 fn eval_field_access_on_non_object() {
413 let ctx = ctx_with(vec![("x", json!(42))]);
414 let expr = IrExpr::FieldAccess {
415 root: Box::new(IrExpr::Var("x".into())),
416 field: "f".into(),
417 };
418 assert_eq!(evaluate(&expr, &ctx), Err(RuntimeError::NotAnObject));
419 }
420
421 #[test]
424 fn eval_compare_eq() {
425 let ctx = ctx_with(vec![("a", json!(5)), ("b", json!(5))]);
426 let expr = IrExpr::Compare {
427 left: Box::new(IrExpr::Var("a".into())),
428 op: CmpOp::Eq,
429 right: Box::new(IrExpr::Var("b".into())),
430 };
431 assert_eq!(evaluate(&expr, &ctx).unwrap(), json!(true));
432 }
433
434 #[test]
435 fn eval_compare_ne() {
436 let ctx = ctx_with(vec![("a", json!(5)), ("b", json!(3))]);
437 let expr = IrExpr::Compare {
438 left: Box::new(IrExpr::Var("a".into())),
439 op: CmpOp::Ne,
440 right: Box::new(IrExpr::Var("b".into())),
441 };
442 assert_eq!(evaluate(&expr, &ctx).unwrap(), json!(true));
443 }
444
445 #[test]
446 fn eval_compare_gt() {
447 let ctx = ctx_with(vec![("a", json!(10)), ("b", json!(5))]);
448 let expr = IrExpr::Compare {
449 left: Box::new(IrExpr::Var("a".into())),
450 op: CmpOp::Gt,
451 right: Box::new(IrExpr::Var("b".into())),
452 };
453 assert_eq!(evaluate(&expr, &ctx).unwrap(), json!(true));
454 }
455
456 #[test]
457 fn eval_compare_le() {
458 let expr = IrExpr::Compare {
459 left: Box::new(IrExpr::Literal(IrLiteral::Int(3))),
460 op: CmpOp::Le,
461 right: Box::new(IrExpr::Literal(IrLiteral::Int(3))),
462 };
463 assert_eq!(evaluate(&expr, &empty_ctx()).unwrap(), json!(true));
464 }
465
466 #[test]
467 fn eval_compare_strings() {
468 let expr = IrExpr::Compare {
469 left: Box::new(IrExpr::Literal(IrLiteral::String("apple".into()))),
470 op: CmpOp::Lt,
471 right: Box::new(IrExpr::Literal(IrLiteral::String("banana".into()))),
472 };
473 assert_eq!(evaluate(&expr, &empty_ctx()).unwrap(), json!(true));
474 }
475
476 #[test]
477 fn eval_compare_union_variant() {
478 let ctx = ctx_with(vec![("account", json!({"status": "Active"}))]);
480 let expr = IrExpr::Compare {
481 left: Box::new(IrExpr::FieldAccess {
482 root: Box::new(IrExpr::Var("account".into())),
483 field: "status".into(),
484 }),
485 op: CmpOp::Eq,
486 right: Box::new(IrExpr::Var("Active".into())),
487 };
488 assert_eq!(evaluate(&expr, &ctx).unwrap(), json!(true));
489 }
490
491 #[test]
494 fn eval_arithmetic_add() {
495 let expr = IrExpr::Arithmetic {
496 left: Box::new(IrExpr::Literal(IrLiteral::Int(3))),
497 op: ArithOp::Add,
498 right: Box::new(IrExpr::Literal(IrLiteral::Int(4))),
499 };
500 assert_eq!(evaluate(&expr, &empty_ctx()).unwrap(), json!(7.0));
501 }
502
503 #[test]
504 fn eval_arithmetic_sub() {
505 let expr = IrExpr::Arithmetic {
506 left: Box::new(IrExpr::Literal(IrLiteral::Int(10))),
507 op: ArithOp::Sub,
508 right: Box::new(IrExpr::Literal(IrLiteral::Int(3))),
509 };
510 assert_eq!(evaluate(&expr, &empty_ctx()).unwrap(), json!(7.0));
511 }
512
513 #[test]
514 fn eval_arithmetic_type_error() {
515 let expr = IrExpr::Arithmetic {
516 left: Box::new(IrExpr::Literal(IrLiteral::String("x".into()))),
517 op: ArithOp::Add,
518 right: Box::new(IrExpr::Literal(IrLiteral::Int(1))),
519 };
520 assert!(matches!(
521 evaluate(&expr, &empty_ctx()),
522 Err(RuntimeError::TypeError { .. })
523 ));
524 }
525
526 #[test]
529 fn eval_and_true() {
530 let expr = IrExpr::And(
531 Box::new(IrExpr::Literal(IrLiteral::Bool(true))),
532 Box::new(IrExpr::Literal(IrLiteral::Bool(true))),
533 );
534 assert_eq!(evaluate(&expr, &empty_ctx()).unwrap(), json!(true));
535 }
536
537 #[test]
538 fn eval_and_short_circuit() {
539 let expr = IrExpr::And(
541 Box::new(IrExpr::Literal(IrLiteral::Bool(false))),
542 Box::new(IrExpr::Var("nonexistent".into())),
543 );
544 assert_eq!(evaluate(&expr, &empty_ctx()).unwrap(), json!(false));
545 }
546
547 #[test]
548 fn eval_or_short_circuit() {
549 let expr = IrExpr::Or(
551 Box::new(IrExpr::Literal(IrLiteral::Bool(true))),
552 Box::new(IrExpr::Var("nonexistent".into())),
553 );
554 assert_eq!(evaluate(&expr, &empty_ctx()).unwrap(), json!(true));
555 }
556
557 #[test]
558 fn eval_not() {
559 let expr = IrExpr::Not(Box::new(IrExpr::Literal(IrLiteral::Bool(false))));
560 assert_eq!(evaluate(&expr, &empty_ctx()).unwrap(), json!(true));
561 }
562
563 #[test]
564 fn eval_implies_true_antecedent() {
565 let expr = IrExpr::Implies(
567 Box::new(IrExpr::Literal(IrLiteral::Bool(true))),
568 Box::new(IrExpr::Literal(IrLiteral::Bool(true))),
569 );
570 assert_eq!(evaluate(&expr, &empty_ctx()).unwrap(), json!(true));
571 }
572
573 #[test]
574 fn eval_implies_false_antecedent() {
575 let expr = IrExpr::Implies(
577 Box::new(IrExpr::Literal(IrLiteral::Bool(false))),
578 Box::new(IrExpr::Var("nonexistent".into())),
579 );
580 assert_eq!(evaluate(&expr, &empty_ctx()).unwrap(), json!(true));
581 }
582
583 #[test]
586 fn eval_old_with_context() {
587 let mut ctx = ctx_with(vec![("balance", json!(500))]);
588 let mut old = HashMap::new();
589 old.insert("balance".to_string(), json!(1000));
590 ctx.old_bindings = Some(old);
591
592 let expr = IrExpr::Old(Box::new(IrExpr::Var("balance".into())));
593 assert_eq!(evaluate(&expr, &ctx).unwrap(), json!(1000));
594 }
595
596 #[test]
597 fn eval_old_without_context() {
598 let expr = IrExpr::Old(Box::new(IrExpr::Var("x".into())));
599 assert_eq!(
600 evaluate(&expr, &empty_ctx()),
601 Err(RuntimeError::OldWithoutContext)
602 );
603 }
604
605 #[test]
606 fn eval_old_field_access() {
607 let mut ctx = ctx_with(vec![("account", json!({"balance": 200}))]);
609 let mut old = HashMap::new();
610 old.insert("account".to_string(), json!({"balance": 500}));
611 ctx.old_bindings = Some(old);
612
613 let expr = IrExpr::Old(Box::new(IrExpr::FieldAccess {
614 root: Box::new(IrExpr::Var("account".into())),
615 field: "balance".into(),
616 }));
617 assert_eq!(evaluate(&expr, &ctx).unwrap(), json!(500));
618 }
619
620 #[test]
623 fn eval_forall_true() {
624 let mut ctx = EvalContext::new();
626 ctx.instances.insert(
627 "Account".into(),
628 vec![
629 json!({"balance": 100}),
630 json!({"balance": 0}),
631 json!({"balance": 50}),
632 ],
633 );
634
635 let expr = IrExpr::Forall {
636 binding: "a".into(),
637 ty: "Account".into(),
638 body: Box::new(IrExpr::Compare {
639 left: Box::new(IrExpr::FieldAccess {
640 root: Box::new(IrExpr::Var("a".into())),
641 field: "balance".into(),
642 }),
643 op: CmpOp::Ge,
644 right: Box::new(IrExpr::Literal(IrLiteral::Int(0))),
645 }),
646 };
647 assert_eq!(evaluate(&expr, &ctx).unwrap(), json!(true));
648 }
649
650 #[test]
651 fn eval_forall_false() {
652 let mut ctx = EvalContext::new();
653 ctx.instances.insert(
654 "Account".into(),
655 vec![
656 json!({"balance": 100}),
657 json!({"balance": -5}), ],
659 );
660
661 let expr = IrExpr::Forall {
662 binding: "a".into(),
663 ty: "Account".into(),
664 body: Box::new(IrExpr::Compare {
665 left: Box::new(IrExpr::FieldAccess {
666 root: Box::new(IrExpr::Var("a".into())),
667 field: "balance".into(),
668 }),
669 op: CmpOp::Ge,
670 right: Box::new(IrExpr::Literal(IrLiteral::Int(0))),
671 }),
672 };
673 assert_eq!(evaluate(&expr, &ctx).unwrap(), json!(false));
674 }
675
676 #[test]
677 fn eval_exists_true() {
678 let mut ctx = EvalContext::new();
679 ctx.instances.insert(
680 "Record".into(),
681 vec![json!({"status": "Pending"}), json!({"status": "Completed"})],
682 );
683
684 let expr = IrExpr::Exists {
685 binding: "r".into(),
686 ty: "Record".into(),
687 body: Box::new(IrExpr::Compare {
688 left: Box::new(IrExpr::FieldAccess {
689 root: Box::new(IrExpr::Var("r".into())),
690 field: "status".into(),
691 }),
692 op: CmpOp::Eq,
693 right: Box::new(IrExpr::Var("Completed".into())),
694 }),
695 };
696 assert_eq!(evaluate(&expr, &ctx).unwrap(), json!(true));
697 }
698
699 #[test]
700 fn eval_exists_false() {
701 let mut ctx = EvalContext::new();
702 ctx.instances
703 .insert("Record".into(), vec![json!({"status": "Pending"})]);
704
705 let expr = IrExpr::Exists {
706 binding: "r".into(),
707 ty: "Record".into(),
708 body: Box::new(IrExpr::Compare {
709 left: Box::new(IrExpr::FieldAccess {
710 root: Box::new(IrExpr::Var("r".into())),
711 field: "status".into(),
712 }),
713 op: CmpOp::Eq,
714 right: Box::new(IrExpr::Var("Completed".into())),
715 }),
716 };
717 assert_eq!(evaluate(&expr, &ctx).unwrap(), json!(false));
718 }
719
720 #[test]
721 fn eval_forall_no_instances() {
722 let expr = IrExpr::Forall {
723 binding: "x".into(),
724 ty: "Missing".into(),
725 body: Box::new(IrExpr::Literal(IrLiteral::Bool(true))),
726 };
727 assert_eq!(
728 evaluate(&expr, &empty_ctx()),
729 Err(RuntimeError::NoInstances("Missing".into()))
730 );
731 }
732
733 #[test]
736 fn eval_list_literal() {
737 let expr = IrExpr::List(vec![
738 IrExpr::Literal(IrLiteral::Int(1)),
739 IrExpr::Literal(IrLiteral::Int(2)),
740 IrExpr::Literal(IrLiteral::Int(3)),
741 ]);
742 assert_eq!(evaluate(&expr, &empty_ctx()).unwrap(), json!([1, 2, 3]));
743 }
744
745 #[test]
748 fn eval_call_len_array() {
749 let ctx = ctx_with(vec![("items", json!([1, 2, 3]))]);
750 let expr = IrExpr::Call {
751 name: "len".into(),
752 args: vec![IrExpr::Var("items".into())],
753 };
754 assert_eq!(evaluate(&expr, &ctx).unwrap(), json!(3));
755 }
756
757 #[test]
758 fn eval_call_len_string() {
759 let expr = IrExpr::Call {
760 name: "len".into(),
761 args: vec![IrExpr::Literal(IrLiteral::String("hello".into()))],
762 };
763 assert_eq!(evaluate(&expr, &empty_ctx()).unwrap(), json!(5));
764 }
765
766 #[test]
767 fn eval_call_unknown() {
768 let expr = IrExpr::Call {
769 name: "bogus".into(),
770 args: vec![],
771 };
772 assert_eq!(
773 evaluate(&expr, &empty_ctx()),
774 Err(RuntimeError::UnknownFunction("bogus".into()))
775 );
776 }
777
778 #[test]
781 fn eval_postcondition_with_old() {
782 let mut ctx = ctx_with(vec![
784 ("from", json!({"balance": 800})),
785 ("amount", json!(200)),
786 ]);
787 let mut old = HashMap::new();
788 old.insert("from".to_string(), json!({"balance": 1000}));
789 ctx.old_bindings = Some(old);
790
791 let expr = IrExpr::Compare {
792 left: Box::new(IrExpr::FieldAccess {
793 root: Box::new(IrExpr::Var("from".into())),
794 field: "balance".into(),
795 }),
796 op: CmpOp::Eq,
797 right: Box::new(IrExpr::Arithmetic {
798 left: Box::new(IrExpr::Old(Box::new(IrExpr::FieldAccess {
799 root: Box::new(IrExpr::Var("from".into())),
800 field: "balance".into(),
801 }))),
802 op: ArithOp::Sub,
803 right: Box::new(IrExpr::Var("amount".into())),
804 }),
805 };
806 assert_eq!(evaluate(&expr, &ctx).unwrap(), json!(true));
807 }
808
809 #[test]
810 fn eval_nested_forall_implies() {
811 let mut ctx = EvalContext::new();
814 ctx.instances.insert(
815 "Account".into(),
816 vec![
817 json!({"id": "1", "name": "Alice"}),
818 json!({"id": "2", "name": "Bob"}),
819 ],
820 );
821
822 let expr = IrExpr::Forall {
823 binding: "a".into(),
824 ty: "Account".into(),
825 body: Box::new(IrExpr::Forall {
826 binding: "b".into(),
827 ty: "Account".into(),
828 body: Box::new(IrExpr::Implies(
829 Box::new(IrExpr::Compare {
830 left: Box::new(IrExpr::FieldAccess {
831 root: Box::new(IrExpr::Var("a".into())),
832 field: "id".into(),
833 }),
834 op: CmpOp::Ne,
835 right: Box::new(IrExpr::FieldAccess {
836 root: Box::new(IrExpr::Var("b".into())),
837 field: "id".into(),
838 }),
839 }),
840 Box::new(IrExpr::Compare {
841 left: Box::new(IrExpr::FieldAccess {
842 root: Box::new(IrExpr::Var("a".into())),
843 field: "name".into(),
844 }),
845 op: CmpOp::Ne,
846 right: Box::new(IrExpr::FieldAccess {
847 root: Box::new(IrExpr::Var("b".into())),
848 field: "name".into(),
849 }),
850 }),
851 )),
852 }),
853 };
854 assert_eq!(evaluate(&expr, &ctx).unwrap(), json!(true));
855 }
856}