1use crate::context::entity::Entity;
23use crate::context::{connective, function, quantifier, verifier};
24use crate::error::{scope_error, NightjarLanguageError};
25use crate::language::grammar::{
26 BoolExpr, Literal, Predicate, Program, SpannedBoolExpr, SpannedValueExpr, SymbolRoot,
27 UnaryCheckOp, ValueExpr,
28};
29use crate::language::parser::{parse_with_config, ParserConfig};
30use crate::symbol_table::{resolve_in_entity, SymbolTable};
31
32#[derive(Debug, Clone)]
34pub struct ExecOptions {
35 pub float_epsilon: f64,
37 pub max_depth: usize,
39}
40
41impl Default for ExecOptions {
42 fn default() -> Self {
43 Self {
44 float_epsilon: 1e-10,
45 max_depth: 256,
46 }
47 }
48}
49
50#[derive(Debug, Clone, PartialEq)]
52pub enum ExecResult {
53 True,
55 False,
57 Error(NightjarLanguageError),
60}
61
62impl ExecResult {
63 pub fn is_true(&self) -> bool {
65 matches!(self, ExecResult::True)
66 }
67
68 pub fn is_false(&self) -> bool {
70 matches!(self, ExecResult::False)
71 }
72
73 pub fn is_error(&self) -> bool {
75 matches!(self, ExecResult::Error(_))
76 }
77}
78
79impl From<Result<bool, NightjarLanguageError>> for ExecResult {
80 fn from(r: Result<bool, NightjarLanguageError>) -> Self {
81 match r {
82 Ok(true) => ExecResult::True,
83 Ok(false) => ExecResult::False,
84 Err(e) => ExecResult::Error(e),
85 }
86 }
87}
88
89pub fn exec_entity(expression: &str, data: Entity, options: ExecOptions) -> ExecResult {
91 let cfg = ParserConfig {
92 max_depth: options.max_depth,
93 };
94 let program = match parse_with_config(expression, &cfg) {
95 Ok(p) => p,
96 Err(e) => return ExecResult::Error(e),
97 };
98 let symbols = SymbolTable::from_entity(data);
99 eval_program(&program, &symbols, &options).into()
100}
101
102#[cfg(feature = "json")]
104pub fn exec(expression: &str, data: serde_json::Value, options: ExecOptions) -> ExecResult {
105 exec_entity(expression, Entity::from(data), options)
106}
107
108fn eval_program(
128 p: &Program,
129 symbols: &SymbolTable,
130 opts: &ExecOptions,
131) -> Result<bool, NightjarLanguageError> {
132 eval_bool(&p.expr, symbols, opts, None)
133}
134
135fn eval_bool(
165 expr: &SpannedBoolExpr,
166 symbols: &SymbolTable,
167 opts: &ExecOptions,
168 scope: Option<&Entity>,
169) -> Result<bool, NightjarLanguageError> {
170 match &expr.node {
171 BoolExpr::Literal(b) => Ok(*b),
172 BoolExpr::Verifier { op, left, right } => {
173 let l = eval_value(left, symbols, opts, scope)?;
174 let r = eval_value(right, symbols, opts, scope)?;
175 verifier::apply_verifier(*op, &l, &r, opts.float_epsilon, expr.span)
176 }
177 BoolExpr::And(l, r) => {
178 let lv = eval_bool(l, symbols, opts, scope)?;
179 let rv = eval_bool(r, symbols, opts, scope)?;
180 Ok(connective::apply_and(lv, rv))
181 }
182 BoolExpr::Or(l, r) => {
183 let lv = eval_bool(l, symbols, opts, scope)?;
184 let rv = eval_bool(r, symbols, opts, scope)?;
185 Ok(connective::apply_or(lv, rv))
186 }
187 BoolExpr::Not(inner) => {
188 let v = eval_bool(inner, symbols, opts, scope)?;
189 Ok(connective::apply_not(v))
190 }
191 BoolExpr::UnaryCheck { op, operand } => {
192 let v = eval_value(operand, symbols, opts, scope)?;
193 match op {
194 UnaryCheckOp::NonEmpty => Ok(v.is_non_empty()),
195 }
196 }
197 BoolExpr::Quantifier {
198 op,
199 predicate,
200 operand,
201 } => {
202 let coll = eval_value(operand, symbols, opts, scope)?; match &predicate.node {
208 Predicate::Full(body) => {
209 quantifier::apply_quantifier_full(*op, &coll, expr.span, |element| {
211 eval_bool(body, symbols, opts, Some(element))
212 })
213 }
214 _ => {
215 let eval_pred = resolve_predicate(&predicate.node, symbols, opts, scope)?;
218 quantifier::apply_quantifier(
219 *op,
220 &eval_pred,
221 &coll,
222 opts.float_epsilon,
223 expr.span,
224 )
225 }
226 }
227 }
228 }
229}
230
231#[allow(clippy::only_used_in_recursion)]
266fn eval_value(
267 expr: &SpannedValueExpr,
268 symbols: &SymbolTable,
269 opts: &ExecOptions,
270 scope: Option<&Entity>,
271) -> Result<Entity, NightjarLanguageError> {
272 match &expr.node {
273 ValueExpr::Literal(lit) => Ok(literal_to_entity(lit)),
274 ValueExpr::Symbol { root, path } => match root {
275 SymbolRoot::Root => symbols.resolve_root_path(path, expr.span),
276 SymbolRoot::Element => match scope {
277 Some(elem) => resolve_in_entity(path, elem, expr.span),
278 None => Err(scope_error(
279 expr.span,
280 "`@` element-relative symbol evaluated without an enclosing quantifier",
281 )),
282 },
283 },
284 ValueExpr::FuncCall { op, args } => {
285 let mut evaluated = Vec::with_capacity(args.len());
286 for arg in args {
287 evaluated.push(eval_value(arg, symbols, opts, scope)?);
288 }
289 function::apply_function(*op, evaluated, expr.span)
290 }
291 }
292}
293
294fn resolve_predicate(
324 pred: &Predicate,
325 symbols: &SymbolTable,
326 opts: &ExecOptions,
327 scope: Option<&Entity>,
328) -> Result<quantifier::EvalPredicate, NightjarLanguageError> {
329 match pred {
330 Predicate::PartialVerifier { op, bound } => {
331 let bound_val = eval_value(bound, symbols, opts, scope)?;
332 Ok(quantifier::EvalPredicate::PartialVerifier {
333 op: *op,
334 bound: bound_val,
335 })
336 }
337 Predicate::UnaryCheck(check_op) => Ok(quantifier::EvalPredicate::UnaryCheck(*check_op)),
338 Predicate::Full(_) => unreachable!("Predicate::Full handled in eval_bool"),
342 }
343}
344
345fn literal_to_entity(lit: &Literal) -> Entity {
362 match lit {
363 Literal::Int(i) => Entity::Int(*i),
364 Literal::Float(f) => Entity::Float(*f),
365 Literal::String(s) => Entity::String(s.clone()),
366 Literal::Bool(b) => Entity::Bool(*b),
367 Literal::Null => Entity::Null,
368 }
369}
370
371#[cfg(test)]
372mod tests {
373 use super::*;
374 use crate::error::NightjarLanguageError;
375 use std::collections::HashMap;
376
377 fn run(expr: &str, data: Entity) -> ExecResult {
378 exec_entity(expr, data, ExecOptions::default())
379 }
380
381 fn empty() -> Entity {
382 Entity::Map(HashMap::new())
383 }
384
385 #[test]
388 fn top_level_true_and_false() {
389 assert_eq!(run("True", empty()), ExecResult::True);
390 assert_eq!(run("False", empty()), ExecResult::False);
391 }
392
393 #[test]
396 fn gt_simple() {
397 assert_eq!(run("(GT 1 2)", empty()), ExecResult::False);
398 assert_eq!(run("(GT 3 2)", empty()), ExecResult::True);
399 }
400
401 #[test]
402 fn eq_simple() {
403 assert_eq!(run("(EQ 1 1)", empty()), ExecResult::True);
404 assert_eq!(run("(EQ 1 2)", empty()), ExecResult::False);
405 }
406
407 #[test]
408 fn type_error_becomes_exec_error() {
409 let r = run("(GT GT 1)", empty());
410 assert!(matches!(r, ExecResult::Error(_)));
413 }
414
415 fn map_of(pairs: &[(&str, Entity)]) -> Entity {
418 let mut m = HashMap::new();
419 for (k, v) in pairs {
420 m.insert((*k).to_string(), v.clone());
421 }
422 Entity::Map(m)
423 }
424
425 #[test]
426 fn symbol_verifier() {
427 let data = map_of(&[("revenue", Entity::Int(100))]);
428 assert_eq!(run("(GE .revenue 100)", data), ExecResult::True);
429 }
430
431 #[test]
432 fn computed_verification_via_symbols() {
433 let data = map_of(&[
434 ("dept1", Entity::Int(100)),
435 ("dept2", Entity::Int(200)),
436 ("total", Entity::Int(300)),
437 ]);
438 assert_eq!(
439 run("(EQ (Add .dept1 .dept2) .total)", data),
440 ExecResult::True
441 );
442 }
443
444 #[test]
445 fn connective_and_nonempty() {
446 let data = map_of(&[
447 ("revenue", Entity::Int(50)),
448 ("name", Entity::String("Acme".into())),
449 ]);
450 assert_eq!(
451 run("(AND (GE .revenue 0) (NonEmpty .name))", data),
452 ExecResult::True
453 );
454 }
455
456 #[test]
459 fn forall_list_positive() {
460 let data = map_of(&[(
461 "scores",
462 Entity::List(vec![Entity::Int(1), Entity::Int(2), Entity::Int(3)]),
463 )]);
464 assert_eq!(run("(ForAll (GT 0) .scores)", data), ExecResult::True);
465 }
466
467 #[test]
468 fn forall_list_zero_fails() {
469 let data = map_of(&[(
470 "scores",
471 Entity::List(vec![Entity::Int(0), Entity::Int(1), Entity::Int(2)]),
472 )]);
473 assert_eq!(run("(ForAll (GT 0) .scores)", data), ExecResult::False);
474 }
475
476 #[test]
477 fn exists_admin_role() {
478 let data = map_of(&[(
479 "roles",
480 Entity::List(vec![
481 Entity::String("user".into()),
482 Entity::String("admin".into()),
483 ]),
484 )]);
485 assert_eq!(
486 run("(Exists (EQ \"admin\") .roles)", data),
487 ExecResult::True
488 );
489 }
490
491 #[test]
492 fn forall_scalar_fallback() {
493 let data = map_of(&[("count", Entity::Int(5))]);
494 assert_eq!(run("(ForAll (GT 0) .count)", data), ExecResult::True);
495 }
496
497 #[test]
498 fn forall_map_operand_is_type_error() {
499 let data = map_of(&[("data", map_of(&[("a", Entity::Int(1))]))]);
500 let r = run("(ForAll (GT 0) .data)", data);
501 assert!(matches!(
502 r,
503 ExecResult::Error(NightjarLanguageError::TypeError { .. })
504 ));
505 }
506
507 #[test]
508 fn forall_over_map_values_via_getvalues() {
509 let data = map_of(&[(
510 "revenue_by_dept",
511 map_of(&[("a", Entity::Int(10)), ("b", Entity::Int(20))]),
512 )]);
513 assert_eq!(
514 run("(ForAll (GE 0) (GetValues .revenue_by_dept))", data),
515 ExecResult::True
516 );
517 }
518
519 #[test]
522 fn missing_symbol_is_exec_error() {
523 let r = run("(GT .missing 0)", empty());
524 assert!(matches!(
525 r,
526 ExecResult::Error(NightjarLanguageError::SymbolNotFound { .. })
527 ));
528 }
529
530 #[test]
531 fn division_by_zero_is_exec_error() {
532 let r = run("(EQ (Div 1 0) 0)", empty());
533 assert!(matches!(
534 r,
535 ExecResult::Error(NightjarLanguageError::DivisionByZero { .. })
536 ));
537 }
538
539 #[test]
540 fn integer_overflow_is_exec_error() {
541 let r = run("(EQ (Add 9223372036854775807 1) 0)", empty());
543 assert!(matches!(
544 r,
545 ExecResult::Error(NightjarLanguageError::IntegerOverflow { .. })
546 ));
547 }
548
549 #[test]
552 fn nested_arithmetic_evaluates_inside_out() {
553 assert_eq!(
554 run("(EQ (Add (Mul 2 3) (Sub 10 4)) 12)", empty()),
555 ExecResult::True
556 );
557 }
558
559 #[test]
560 fn chained_quantifier_and_count() {
561 let data = map_of(&[(
562 "scores",
563 Entity::List(vec![
564 Entity::Int(1),
565 Entity::Int(2),
566 Entity::Int(3),
567 Entity::Int(4),
568 ]),
569 )]);
570 assert_eq!(
571 run("(AND (ForAll (GT 0) .scores) (GT (Count .scores) 3))", data),
572 ExecResult::True
573 );
574 }
575
576 #[test]
577 fn epsilon_equality_via_default_options() {
578 assert_eq!(run("(EQ (Add 0.1 0.2) 0.3)", empty()), ExecResult::True);
579 }
580
581 fn obj(pairs: &[(&str, Entity)]) -> Entity {
584 map_of(pairs)
585 }
586
587 #[test]
588 fn forall_equal_fields_all_true() {
589 let data = map_of(&[(
590 "items",
591 Entity::List(vec![
592 obj(&[("a", Entity::Int(1)), ("b", Entity::Int(1))]),
593 obj(&[("a", Entity::Int(2)), ("b", Entity::Int(2))]),
594 obj(&[("a", Entity::Int(3)), ("b", Entity::Int(3))]),
595 ]),
596 )]);
597 assert_eq!(run("(ForAll (EQ @.a @.b) .items)", data), ExecResult::True);
598 }
599
600 #[test]
601 fn forall_equal_fields_one_mismatch_is_false() {
602 let data = map_of(&[(
603 "items",
604 Entity::List(vec![
605 obj(&[("a", Entity::Int(1)), ("b", Entity::Int(1))]),
606 obj(&[("a", Entity::Int(2)), ("b", Entity::Int(9))]),
607 ]),
608 )]);
609 assert_eq!(run("(ForAll (EQ @.a @.b) .items)", data), ExecResult::False);
610 }
611
612 #[test]
613 fn forall_sum_of_fields_equals_third_field() {
614 let data = map_of(&[(
615 "items",
616 Entity::List(vec![
617 obj(&[
618 ("a", Entity::Int(1)),
619 ("b", Entity::Int(1)),
620 ("c", Entity::Int(2)),
621 ]),
622 obj(&[
623 ("a", Entity::Int(2)),
624 ("b", Entity::Int(2)),
625 ("c", Entity::Int(4)),
626 ]),
627 obj(&[
628 ("a", Entity::Int(3)),
629 ("b", Entity::Int(3)),
630 ("c", Entity::Int(6)),
631 ]),
632 ]),
633 )]);
634 assert_eq!(
635 run("(ForAll (EQ (Add @.a @.b) @.c) .items)", data),
636 ExecResult::True
637 );
638 }
639
640 #[test]
641 fn bare_at_refers_to_whole_element() {
642 let data = map_of(&[(
643 "scores",
644 Entity::List(vec![Entity::Int(1), Entity::Int(2), Entity::Int(3)]),
645 )]);
646 assert_eq!(run("(ForAll (GT @ 0) .scores)", data), ExecResult::True);
647 }
648
649 #[test]
650 fn at_field_missing_is_symbol_not_found() {
651 let data = map_of(&[(
652 "items",
653 Entity::List(vec![
654 obj(&[("a", Entity::Int(1)), ("b", Entity::Int(1))]),
655 obj(&[("a", Entity::Int(2))]), ]),
657 )]);
658 let r = run("(ForAll (EQ @.a @.b) .items)", data);
659 assert!(matches!(
660 r,
661 ExecResult::Error(NightjarLanguageError::SymbolNotFound { .. })
662 ));
663 }
664
665 #[test]
666 fn mixed_root_and_element_symbols_in_predicate() {
667 let data = map_of(&[
669 ("threshold", Entity::Int(100)),
670 (
671 "employees",
672 Entity::List(vec![
673 obj(&[("salary", Entity::Int(150))]),
674 obj(&[("salary", Entity::Int(200))]),
675 ]),
676 ),
677 ]);
678 assert_eq!(
679 run("(ForAll (GT @.salary .threshold) .employees)", data),
680 ExecResult::True
681 );
682 }
683
684 #[test]
685 fn nested_quantifier_inner_at_refers_to_inner_element() {
686 let data = map_of(&[(
688 "teams",
689 Entity::List(vec![
690 obj(&[("scores", Entity::List(vec![Entity::Int(1), Entity::Int(2)]))]),
691 obj(&[("scores", Entity::List(vec![Entity::Int(3), Entity::Int(4)]))]),
692 ]),
693 )]);
694 assert_eq!(
695 run("(ForAll (ForAll (GT @ 0) @.scores) .teams)", data),
696 ExecResult::True
697 );
698 }
699
700 #[test]
701 fn exists_with_full_predicate_short_circuits() {
702 let data = map_of(&[(
703 "items",
704 Entity::List(vec![
705 obj(&[("a", Entity::Int(1)), ("b", Entity::Int(2))]),
706 obj(&[("a", Entity::Int(5)), ("b", Entity::Int(5))]),
707 obj(&[("a", Entity::Int(9)), ("b", Entity::Int(8))]),
708 ]),
709 )]);
710 assert_eq!(run("(Exists (EQ @.a @.b) .items)", data), ExecResult::True);
711 }
712
713 #[test]
714 fn forall_full_predicate_on_empty_list_is_vacuously_true() {
715 let data = map_of(&[("items", Entity::List(vec![]))]);
716 assert_eq!(run("(ForAll (EQ @.a @.b) .items)", data), ExecResult::True);
717 }
718
719 #[test]
720 fn depth_limit_surfaces_as_exec_error() {
721 let mut s = String::new();
725 for _ in 0..20 {
726 s.push_str("(NOT ");
727 }
728 s.push_str("True");
729 for _ in 0..20 {
730 s.push(')');
731 }
732 let opts = ExecOptions {
733 max_depth: 10,
734 ..ExecOptions::default()
735 };
736 let r = exec_entity(&s, empty(), opts);
737 assert!(matches!(
738 r,
739 ExecResult::Error(NightjarLanguageError::RecursionError { .. })
740 ));
741 }
742}