Skip to main content

nightjar_lang/
executor.rs

1// Copyright 2026 Wayne Hong (h-alice) <contact@halice.art>
2// Nightjar Language Project
3//
4// Licensed under the Apache License, Version 2.0 (the "License");
5// you may not use this file except in compliance with the License.
6// You may obtain a copy of the License at
7//
8//     http://www.apache.org/licenses/LICENSE-2.0
9//
10// Unless required by applicable law or agreed to in writing, software
11// distributed under the License is distributed on an "AS IS" BASIS,
12// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13// See the License for the specific language governing permissions and
14// limitations under the License.
15
16//! Core executor.
17//!
18//! Parses a Nightjar language expression and evaluates the resulting AST
19//! against a flattened symbol table, yielding a three-valued ExecResult
20//! (True / False / Error).
21
22use 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/// Execution options, configurable per invocation.
33#[derive(Debug, Clone)]
34pub struct ExecOptions {
35    /// Tolerance for epsilon-based `EQ`/`NE` on floats.
36    pub float_epsilon: f64,
37    /// Max nesting depth enforced by the parser.
38    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/// Three-valued execution result.
51#[derive(Debug, Clone, PartialEq)]
52pub enum ExecResult {
53    /// The assertion holds.
54    True,
55    /// The assertion does not hold.
56    False,
57    /// The assertion could not be evaluated; the wrapped
58    /// [`NightjarLanguageError`] carries the diagnostic.
59    Error(NightjarLanguageError),
60}
61
62impl ExecResult {
63    /// Return `true` when the result is [`ExecResult::True`].
64    pub fn is_true(&self) -> bool {
65        matches!(self, ExecResult::True)
66    }
67
68    /// Return `true` when the result is [`ExecResult::False`].
69    pub fn is_false(&self) -> bool {
70        matches!(self, ExecResult::False)
71    }
72
73    /// Return `true` when the result is [`ExecResult::Error`].
74    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
89/// Main entry point against an already-built Entity.
90pub 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/// Convenience entry point to ingest JSON directly.
103#[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
108/// Drive evaluation of the top-level `Program`.
109///
110/// Since our program is a boolean expression, we use this thin wrapper that
111/// delegates to [`eval_bool`] with an initially empty scope (no element binding
112/// only the root symbol table is visible).
113///
114/// Example:
115///
116/// ```ignore
117/// use crate::executor::{eval_program, ExecOptions};
118/// use crate::language::parser::parse;
119/// use crate::symbol_table::SymbolTable;
120/// use crate::context::entity::Entity;
121///
122/// let program  = parse("(EQ 1 1)").unwrap();
123/// let symbols  = SymbolTable::from_entity(Entity::Null);
124/// let opts     = ExecOptions::default();
125/// assert!(eval_program(&program, &symbols, &opts).unwrap());
126/// ```
127fn 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
135/// Recursive evaluator for boolean-producing AST nodes.
136///
137/// Evaluates on the `BoolExpr` variant, forwards `symbols` for root-rooted
138/// lookups, and forwards `scope` (the current iteration element, if any), so
139/// that element-relative `@` symbols inside a quantifier predicate resolve
140/// against the element rather than the root.
141///
142/// The `Quantifier` arm branches on the predicate kind: `PartialVerifier` /
143/// `UnaryCheck` reuse the pre-resolution path via [`resolve_predicate`] &
144/// [`quantifier::apply_quantifier`], while `Predicate::Full(body)` takes the
145/// delayed-evaluation path that invokes `eval_bool` per element with the
146/// element bound in `scope`.
147///
148/// Example (internal):
149///
150/// ```ignore
151/// // Evaluate `(GT .x 0)` against `{ x: 5 }`.
152/// use crate::executor::{eval_bool, ExecOptions};
153/// use crate::language::parser::parse;
154/// use crate::symbol_table::SymbolTable;
155/// use crate::context::entity::Entity;
156/// use std::collections::HashMap;
157///
158/// let mut m = HashMap::new();
159/// m.insert("x".to_string(), Entity::Int(5));
160/// let st   = SymbolTable::from_entity(Entity::Map(m));
161/// let prog = parse("(GT .x 0)").unwrap();
162/// assert!(eval_bool(&prog.expr, &st, &ExecOptions::default(), None).unwrap());
163/// ```
164fn 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            // Here's the only place we will actually use `scope`.
203            //
204            // The operand (the list being iterated) resolves in the current
205            // scope, so keep `scope` here and do not shadow with an element yet.
206            let coll = eval_value(operand, symbols, opts, scope)?; // Eval into collection in current scope.
207            match &predicate.node {
208                Predicate::Full(body) => {
209                    // For full predicates, we evaluate the body once per element.
210                    quantifier::apply_quantifier_full(*op, &coll, expr.span, |element| {
211                        eval_bool(body, symbols, opts, Some(element))
212                    })
213                }
214                _ => {
215                    // For partial predicates, we resolve the predicate once in the current scope.
216                    // So there's no need to evaluate the operand every time.
217                    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/// Recursive evaluator for value-producing AST nodes.
232///
233/// Converts literals, resolves symbol references (root-rooted against
234/// `symbols`, element-rooted against `scope`), and reduces function calls by
235/// evaluating every argument and dispatching to [`function::apply_function`].
236///
237/// Invariants worth remembering:
238/// - `ValueExpr::Symbol { root: Element, .. }` with `scope == None` is a
239///   runtime `ScopeError` — the static validator normally catches this at
240///   parse time, but the check here is a defensive fallback.
241/// - `scope` is preserved through recursive calls unchanged; the only
242///   function that swaps it is the quantifier dispatch in [`eval_bool`].
243///
244/// Example (internal):
245///
246/// ```ignore
247/// // Resolve `.x` against a root map.
248/// use crate::executor::{eval_value, ExecOptions};
249/// use crate::language::grammar::{Spanned, SymbolRoot, ValueExpr};
250/// use crate::symbol_table::SymbolTable;
251/// use crate::context::entity::Entity;
252/// use crate::error::Span;
253/// use std::collections::HashMap;
254///
255/// let mut m = HashMap::new();
256/// m.insert("x".to_string(), Entity::Int(42));
257/// let st = SymbolTable::from_entity(Entity::Map(m));
258/// let expr = Spanned::new(
259///     ValueExpr::Symbol { root: SymbolRoot::Root, path: "x".into() },
260///     Span::new(0, 2),
261/// );
262/// let v = eval_value(&expr, &st, &ExecOptions::default(), None).unwrap();
263/// assert_eq!(v, Entity::Int(42));
264/// ```
265#[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
294/// Pre-resolve the `PartialVerifier` and `UnaryCheck` into an [`EvalPredicate`]
295///
296/// [`EvalPredicate`] bound operand is already reduced to an `Entity`. This happens
297/// once, before the quantifier loop, so scalar bounds like `(GT 0)` aren't
298/// re-evaluated per element.
299///
300/// `Predicate::Full` is intentionally unreachable here, since full predicates
301/// need per-element scope binding and are handled directly inside
302/// [`eval_bool`]'s `Quantifier` arm.
303///
304/// Example:
305///
306/// ```ignore
307/// use crate::executor::{resolve_predicate, ExecOptions};
308/// use crate::language::grammar::{Predicate, Spanned, ValueExpr, Literal, VerifierOp};
309/// use crate::context::quantifier::EvalPredicate;
310/// use crate::context::entity::Entity;
311/// use crate::symbol_table::SymbolTable;
312/// use crate::error::Span;
313///
314/// // Resolve `(GT 0)`, the bound operand `0` is reduced to Entity::Int(0).
315/// let pred = Predicate::PartialVerifier {
316///     op: VerifierOp::GT,
317///     bound: Box::new(Spanned::new(ValueExpr::Literal(Literal::Int(0)), Span::new(0, 1))),
318/// };
319/// let st = SymbolTable::from_entity(Entity::Null);
320/// let out = resolve_predicate(&pred, &st, &ExecOptions::default(), None).unwrap();
321/// assert!(matches!(out, EvalPredicate::PartialVerifier { bound: Entity::Int(0), .. }));
322/// ```
323fn 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        // `Full` is handled directly in `eval_bool`'s Quantifier arm because
339        // it requires per-element scope binding that `EvalPredicate` can't
340        // represent.
341        Predicate::Full(_) => unreachable!("Predicate::Full handled in eval_bool"),
342    }
343}
344
345/// Map an AST `Literal` to its runtime `Entity` counterpart.
346///
347/// This is a trivial, one-to-one conversion that exists so the evaluator
348/// doesn't need to repeat the match arms inline.
349///
350/// Example (internal):
351///
352/// ```ignore
353/// use crate::executor::literal_to_entity;
354/// use crate::language::grammar::Literal;
355/// use crate::context::entity::Entity;
356///
357/// assert_eq!(literal_to_entity(&Literal::Int(7)),       Entity::Int(7));
358/// assert_eq!(literal_to_entity(&Literal::Bool(true)),   Entity::Bool(true));
359/// assert_eq!(literal_to_entity(&Literal::Null),         Entity::Null);
360/// ```
361fn 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    // ── Boolean literals ─────────────────────────────────────
386
387    #[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    // ── Basic verifiers ──────────────────────────────────────
394
395    #[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        // `GT GT 1` is actually a parse error (GT isn't a value expression).
411        // Verify the result is an error variant regardless of which.
412        assert!(matches!(r, ExecResult::Error(_)));
413    }
414
415    // ── Symbol resolution ────────────────────────────────────
416
417    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    // ── Quantifiers ──────────────────────────────────────────
457
458    #[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    // ── Errors propagate ─────────────────────────────────────
520
521    #[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        // i64::MAX = 9223372036854775807
542        let r = run("(EQ (Add 9223372036854775807 1) 0)", empty());
543        assert!(matches!(
544            r,
545            ExecResult::Error(NightjarLanguageError::IntegerOverflow { .. })
546        ));
547    }
548
549    // ── Misc ─────────────────────────────────────────────────
550
551    #[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    // ── Element-relative symbols (`@`) inside quantifier predicates ──
582
583    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))]), // missing `b`
656            ]),
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        // Every employee's salary is above the root-level threshold.
668        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        // Two teams, each with a list of scores. Every score in every team > 0.
687        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        // Exercise the depth guard at a low limit so we don't have to push
722        // the real Rust call stack close to its ceiling. 20 nested NOTs
723        // against a max_depth of 10 must surface DepthLimitExceeded.
724        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}