Skip to main content

salvor_graph/
expr.rs

1//! The branch-condition expression language: a small, TOTAL, non-Turing-complete
2//! language that gives meaning to the opaque string inside a
3//! [`crate::document::BranchCondition::Expression`].
4//!
5//! # Why deliberately weak
6//!
7//! A branch condition is evaluated inside a durable, replayed run: the same
8//! expression is re-evaluated against the same routed value during replay and
9//! again during a fork, so its result must be identical every time and forever.
10//! Two properties follow, and they shape every decision here:
11//!
12//! - **Total.** Every syntactically valid expression produces a `bool` for
13//!   *every* possible JSON value, with no panic, no error, and no undefined
14//!   case. There is no runtime failure mode, so a valid condition can never
15//!   turn a replay into an error.
16//! - **Non-Turing-complete.** No loops, no function calls, no arithmetic, no
17//!   regex. Evaluation is a single linear walk of an abstract syntax tree whose
18//!   size is bounded by the [`MAX_EXPRESSION_LEN`]-character source cap, so eval
19//!   time is bounded. A Turing-complete language in a durable log would make
20//!   replay time unbounded.
21//!
22//! # The grammar
23//!
24//! ```text
25//! or         := and ( "||" and )*
26//! and        := unary ( "&&" unary )*
27//! unary      := "!" unary | atom
28//! atom       := "(" or ")" | comparison
29//! comparison := operand ( cmp_op operand )?
30//! cmp_op     := "==" | "!=" | "<" | "<=" | ">" | ">="
31//! operand    := literal | path
32//! literal    := string | number | "true" | "false" | "null"
33//! path       := ident ( "." segment )*
34//! segment    := ident | integer
35//! ```
36//!
37//! Precedence, loosest to tightest: `||`, then `&&`, then `!`, then a
38//! comparison. A comparison therefore binds tighter than any boolean operator
39//! (`!score > 0.8` parses as `!(score > 0.8)`), and comparisons do not chain
40//! (`a < b < c` is a parse error). Parentheses group a whole boolean
41//! sub-expression; they are not part of a comparison operand, so `(a > b) > c`
42//! is a parse error rather than a comparison of a boolean.
43//!
44//! A bare operand used where a boolean is expected (`ready`, or `ready && ok`)
45//! is true only when it resolves to the JSON boolean `true`; every other value,
46//! and a missing path, is false.
47//!
48//! # Paths and array indexing
49//!
50//! A path is dot-separated segments walked from the root of the routed value:
51//! `score`, `output.score`, `items.0.score`. Array indexing IS supported
52//! because routed values routinely carry lists (a tool that returns an array, a
53//! map join), and without it a branch could not reach into one at all. The
54//! container type decides how a segment is read: on a JSON object a segment is a
55//! key, and on a JSON array a purely numeric segment is a zero-based index. A
56//! numeric segment therefore never indexes an object, so an object key spelled
57//! with digits (`{"0": ...}`) is not reachable; that ambiguity is traded away
58//! deliberately for a single, deterministic rule.
59//!
60//! # Total semantics (these are forever: replay and fork re-run them verbatim)
61//!
62//! - **Missing path.** A path that does not resolve (an absent key, an index
63//!   past the end, or a descent into a non-container) is MISSING, which is
64//!   distinct from JSON `null`. Any comparison with a missing operand is
65//!   `false`, and a missing operand in boolean position is `false`. Rationale:
66//!   JSON `null` is a value an author chose, so conflating it with "absent"
67//!   would let `x == null` fire on a field that simply is not there; keeping
68//!   them distinct means a branch never silently fires on absent data. `!` still
69//!   lets an author test for absence deliberately (`!(score > 0.8)` is true when
70//!   `score` is missing).
71//! - **Type mismatch.** Equality (`==`, `!=`) is defined across all types:
72//!   values of different types are never equal, so `"5" == 5` is false. Ordering
73//!   (`<`, `<=`, `>`, `>=`) is defined only for two numbers or two strings; any
74//!   other ordered comparison (a number against a string, anything against a
75//!   bool/null/object/array) is `false`. Rationale: ordering has an obvious
76//!   total meaning only for numbers and strings, and yielding `false` everywhere
77//!   else means a mismatched type can never satisfy an ordering branch.
78//! - **Number comparison.** Two numbers compare by mathematical value. When both
79//!   are integers they compare exactly through `i128`, so two distinct large
80//!   integers never collide; when either is a floating-point number both are
81//!   compared as `f64` (so `1 == 1.0` is true). Rationale: exact integer
82//!   comparison is the safe default, and dropping to `f64` only when a fractional
83//!   value is actually involved confines `f64`'s precision limit to the case
84//!   that inherently needs it. JSON numbers are always finite (JSON cannot encode
85//!   `NaN` or infinity), so an unordered `f64` result cannot arise; the code
86//!   treats it as `false` regardless, keeping eval total for any constructed
87//!   value.
88
89use std::cmp::Ordering;
90
91use serde_json::{Number, Value};
92
93/// The maximum length, in Unicode scalar values, of a condition expression.
94///
95/// Enforced at [`parse`] before any work, so an over-long string is rejected
96/// with a precise error and never lexed. The cap is what bounds the AST size,
97/// which in turn bounds eval time.
98pub const MAX_EXPRESSION_LEN: usize = 512;
99
100/// A failure to parse an expression. Its [`Display`](std::fmt::Display) message
101/// is the human-readable diagnostic, and names the offending character position
102/// where one is known.
103///
104/// `Clone`, `PartialEq`, and `Eq` are derived so a caller (the graph validator)
105/// can carry it inside its own comparable error type.
106#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
107#[error("{message}")]
108pub struct ExprError {
109    message: String,
110}
111
112impl ExprError {
113    fn new(message: impl Into<String>) -> Self {
114        Self {
115            message: message.into(),
116        }
117    }
118
119    /// The human-readable diagnostic message.
120    #[must_use]
121    pub fn message(&self) -> &str {
122        &self.message
123    }
124}
125
126/// A parsed, ready-to-evaluate condition expression.
127///
128/// Produced by [`parse`] and evaluated by [`Expr::eval`]. It owns its whole AST,
129/// borrows nothing, and holds no IO, clock, or randomness, so it is safe to keep
130/// and re-evaluate for the life of a run.
131#[derive(Clone, Debug, PartialEq)]
132pub struct Expr {
133    root: Bool,
134}
135
136impl Expr {
137    /// Evaluates the expression against a routed value, always returning a
138    /// `bool`.
139    ///
140    /// This is TOTAL: it never panics and never errors for any `value`. The
141    /// missing-path, type-mismatch, and number-comparison semantics are those
142    /// documented on the [module](self). Eval is a single linear walk of the
143    /// AST, whose size is bounded by [`MAX_EXPRESSION_LEN`].
144    #[must_use]
145    pub fn eval(&self, value: &Value) -> bool {
146        eval_bool(&self.root, value)
147    }
148}
149
150/// A parsed reference into a routed value: a dot-separated path with array
151/// indexing, the same `path` grammar the expression language uses for an
152/// operand, standing alone as a value reference rather than inside a boolean.
153///
154/// This is how a `map` node's `over` field names the list it fans out over: the
155/// reference is resolved against the node's routed value at fan-out time, and the
156/// engine reuses the identical missing-path semantics the branch expressions use,
157/// so references resolve one consistent way across the whole document. It owns
158/// its whole path, borrows nothing, and holds no IO, clock, or randomness, so it
159/// is safe to keep and re-resolve for the life of a run.
160#[derive(Clone, Debug, PartialEq)]
161pub struct Reference {
162    segments: Vec<Segment>,
163}
164
165impl Reference {
166    /// Resolves the reference against a routed value, returning the value it
167    /// names or `None` when the path is missing (an absent key, an index past the
168    /// end, or a descent into a non-container).
169    ///
170    /// This is TOTAL: it never panics for any `value`, and its missing-path
171    /// semantics are exactly those [`Expr::eval`] uses for a path operand.
172    #[must_use]
173    pub fn resolve<'a>(&self, value: &'a Value) -> Option<&'a Value> {
174        resolve_path(&self.segments, value)
175    }
176}
177
178/// Parses a bare reference path (`items`, `output.items`, `results.0.items`), the
179/// standalone counterpart of a `path` operand in the expression grammar.
180///
181/// The [`MAX_EXPRESSION_LEN`]-character cap is enforced first, exactly as [`parse`]
182/// does. Only a path is accepted: a literal (`5`, `"x"`, `true`) is rejected, so a
183/// reference always names a location in the routed value rather than a constant.
184///
185/// # Errors
186///
187/// Returns an [`ExprError`] when the input exceeds the length cap, is not a
188/// well-formed path, or is a bare literal rather than a path.
189pub fn parse_reference(input: &str) -> Result<Reference, ExprError> {
190    let chars: Vec<char> = input.chars().collect();
191    if chars.len() > MAX_EXPRESSION_LEN {
192        return Err(ExprError::new(format!(
193            "reference is {} characters long, which exceeds the {MAX_EXPRESSION_LEN}-character limit",
194            chars.len()
195        )));
196    }
197
198    let tokens = lex(&chars)?;
199    let mut parser = Parser {
200        tokens: &tokens,
201        pos: 0,
202        end: chars.len(),
203    };
204    let operand = parser.parse_operand()?;
205    parser.expect_end()?;
206    match operand {
207        Operand::Path(segments) => Ok(Reference { segments }),
208        Operand::Literal(_) => Err(ExprError::new(
209            "a reference must be a path into the routed value, not a literal",
210        )),
211    }
212}
213
214/// A boolean-valued node of the AST.
215#[derive(Clone, Debug, PartialEq)]
216enum Bool {
217    Or(Box<Bool>, Box<Bool>),
218    And(Box<Bool>, Box<Bool>),
219    Not(Box<Bool>),
220    Compare {
221        left: Operand,
222        op: CmpOp,
223        right: Operand,
224    },
225    /// A bare operand used in boolean position: true only if it resolves to the
226    /// JSON boolean `true`.
227    Truthy(Operand),
228}
229
230/// One side of a comparison, or a bare boolean operand: a literal value or a
231/// path into the routed value.
232#[derive(Clone, Debug, PartialEq)]
233enum Operand {
234    Literal(Value),
235    Path(Vec<Segment>),
236}
237
238/// One step of a path: an object key or an array index.
239#[derive(Clone, Debug, PartialEq)]
240enum Segment {
241    Key(String),
242    Index(usize),
243}
244
245/// A comparison operator.
246#[derive(Clone, Copy, Debug, PartialEq)]
247enum CmpOp {
248    Eq,
249    Ne,
250    Lt,
251    Le,
252    Gt,
253    Ge,
254}
255
256// ---------------------------------------------------------------------------
257// Parsing
258// ---------------------------------------------------------------------------
259
260/// Parses a condition expression, or reports why it is not well-formed.
261///
262/// The [`MAX_EXPRESSION_LEN`]-character cap is enforced first, so an over-long
263/// input is rejected before any lexing. On success the returned [`Expr`] is
264/// guaranteed to evaluate totally against any JSON value.
265///
266/// # Errors
267///
268/// Returns an [`ExprError`] whose message names the problem (and, where known,
269/// the character position) when the input exceeds the length cap or does not
270/// match the grammar.
271pub fn parse(input: &str) -> Result<Expr, ExprError> {
272    let chars: Vec<char> = input.chars().collect();
273    if chars.len() > MAX_EXPRESSION_LEN {
274        return Err(ExprError::new(format!(
275            "expression is {} characters long, which exceeds the {MAX_EXPRESSION_LEN}-character limit",
276            chars.len()
277        )));
278    }
279
280    let tokens = lex(&chars)?;
281    let mut parser = Parser {
282        tokens: &tokens,
283        pos: 0,
284        end: chars.len(),
285    };
286    let root = parser.parse_or()?;
287    parser.expect_end()?;
288    Ok(Expr { root })
289}
290
291/// A lexed token together with the character position it started at.
292#[derive(Clone, Debug, PartialEq)]
293struct Spanned {
294    token: Token,
295    at: usize,
296}
297
298/// A lexical token.
299#[derive(Clone, Debug, PartialEq)]
300enum Token {
301    Ident(String),
302    Number(Number),
303    Str(String),
304    True,
305    False,
306    Null,
307    Dot,
308    LParen,
309    RParen,
310    Bang,
311    AndAnd,
312    OrOr,
313    EqEq,
314    NotEq,
315    Lt,
316    Le,
317    Gt,
318    Ge,
319}
320
321fn is_ident_start(c: char) -> bool {
322    c.is_ascii_alphabetic() || c == '_'
323}
324
325fn is_ident_continue(c: char) -> bool {
326    c.is_ascii_alphanumeric() || c == '_'
327}
328
329/// Turns the character slice into a token stream, or reports the first bad
330/// character.
331fn lex(chars: &[char]) -> Result<Vec<Spanned>, ExprError> {
332    let mut tokens = Vec::new();
333    let mut i = 0;
334    let len = chars.len();
335
336    while i < len {
337        let c = chars[i];
338        let at = i;
339
340        if c.is_whitespace() {
341            i += 1;
342            continue;
343        }
344
345        match c {
346            '(' => {
347                tokens.push(Spanned {
348                    token: Token::LParen,
349                    at,
350                });
351                i += 1;
352            }
353            ')' => {
354                tokens.push(Spanned {
355                    token: Token::RParen,
356                    at,
357                });
358                i += 1;
359            }
360            '.' => {
361                tokens.push(Spanned {
362                    token: Token::Dot,
363                    at,
364                });
365                i += 1;
366            }
367            '!' => {
368                if i + 1 < len && chars[i + 1] == '=' {
369                    tokens.push(Spanned {
370                        token: Token::NotEq,
371                        at,
372                    });
373                    i += 2;
374                } else {
375                    tokens.push(Spanned {
376                        token: Token::Bang,
377                        at,
378                    });
379                    i += 1;
380                }
381            }
382            '=' => {
383                if i + 1 < len && chars[i + 1] == '=' {
384                    tokens.push(Spanned {
385                        token: Token::EqEq,
386                        at,
387                    });
388                    i += 2;
389                } else {
390                    return Err(at_char(at, "a lone `=`; did you mean `==`?"));
391                }
392            }
393            '<' => {
394                if i + 1 < len && chars[i + 1] == '=' {
395                    tokens.push(Spanned {
396                        token: Token::Le,
397                        at,
398                    });
399                    i += 2;
400                } else {
401                    tokens.push(Spanned {
402                        token: Token::Lt,
403                        at,
404                    });
405                    i += 1;
406                }
407            }
408            '>' => {
409                if i + 1 < len && chars[i + 1] == '=' {
410                    tokens.push(Spanned {
411                        token: Token::Ge,
412                        at,
413                    });
414                    i += 2;
415                } else {
416                    tokens.push(Spanned {
417                        token: Token::Gt,
418                        at,
419                    });
420                    i += 1;
421                }
422            }
423            '&' => {
424                if i + 1 < len && chars[i + 1] == '&' {
425                    tokens.push(Spanned {
426                        token: Token::AndAnd,
427                        at,
428                    });
429                    i += 2;
430                } else {
431                    return Err(at_char(at, "a lone `&`; did you mean `&&`?"));
432                }
433            }
434            '|' => {
435                if i + 1 < len && chars[i + 1] == '|' {
436                    tokens.push(Spanned {
437                        token: Token::OrOr,
438                        at,
439                    });
440                    i += 2;
441                } else {
442                    return Err(at_char(at, "a lone `|`; did you mean `||`?"));
443                }
444            }
445            '"' => {
446                let (token, next) = lex_string(chars, i)?;
447                tokens.push(Spanned { token, at });
448                i = next;
449            }
450            _ if c.is_ascii_digit()
451                || (c == '-' && i + 1 < len && chars[i + 1].is_ascii_digit()) =>
452            {
453                let (token, next) = lex_number(chars, i)?;
454                tokens.push(Spanned { token, at });
455                i = next;
456            }
457            _ if is_ident_start(c) => {
458                let (token, next) = lex_ident(chars, i);
459                tokens.push(Spanned { token, at });
460                i = next;
461            }
462            _ => {
463                return Err(at_char(at, format!("an unexpected character `{c}`")));
464            }
465        }
466    }
467
468    Ok(tokens)
469}
470
471/// Lexes a double-quoted string starting at the opening quote. Supports the
472/// escapes `\"`, `\\`, `\/`, `\n`, `\t`, and `\r`; any other escape, or an
473/// unterminated string, is an error.
474fn lex_string(chars: &[char], start: usize) -> Result<(Token, usize), ExprError> {
475    let len = chars.len();
476    let mut i = start + 1;
477    let mut value = String::new();
478
479    while i < len {
480        let c = chars[i];
481        if c == '"' {
482            return Ok((Token::Str(value), i + 1));
483        }
484        if c == '\\' {
485            i += 1;
486            if i >= len {
487                break;
488            }
489            match chars[i] {
490                '"' => value.push('"'),
491                '\\' => value.push('\\'),
492                '/' => value.push('/'),
493                'n' => value.push('\n'),
494                't' => value.push('\t'),
495                'r' => value.push('\r'),
496                other => {
497                    return Err(at_char(
498                        i,
499                        format!("an unsupported string escape `\\{other}`"),
500                    ));
501                }
502            }
503            i += 1;
504        } else {
505            value.push(c);
506            i += 1;
507        }
508    }
509
510    Err(at_char(start, "an unterminated string literal"))
511}
512
513/// Lexes an integer or decimal number (optionally negative). Exponents are not
514/// supported.
515fn lex_number(chars: &[char], start: usize) -> Result<(Token, usize), ExprError> {
516    let len = chars.len();
517    let mut i = start;
518    if chars[i] == '-' {
519        i += 1;
520    }
521    while i < len && chars[i].is_ascii_digit() {
522        i += 1;
523    }
524    // A fractional part is consumed only when a digit actually follows the dot,
525    // so `items.0` lexes as the number `0` then a `.`, not as `0.` waiting for a
526    // fraction.
527    if i + 1 < len && chars[i] == '.' && chars[i + 1].is_ascii_digit() {
528        i += 1;
529        while i < len && chars[i].is_ascii_digit() {
530            i += 1;
531        }
532    }
533
534    let text: String = chars[start..i].iter().collect();
535    let number: Number = serde_json::from_str(&text)
536        .map_err(|_| at_char(start, format!("a malformed number `{text}`")))?;
537    Ok((Token::Number(number), i))
538}
539
540/// Lexes an identifier, promoting the three reserved words to their keyword
541/// tokens.
542fn lex_ident(chars: &[char], start: usize) -> (Token, usize) {
543    let len = chars.len();
544    let mut i = start;
545    while i < len && is_ident_continue(chars[i]) {
546        i += 1;
547    }
548    let text: String = chars[start..i].iter().collect();
549    let token = match text.as_str() {
550        "true" => Token::True,
551        "false" => Token::False,
552        "null" => Token::Null,
553        _ => Token::Ident(text),
554    };
555    (token, i)
556}
557
558fn at_char(position: usize, what: impl std::fmt::Display) -> ExprError {
559    ExprError::new(format!("found {what} at character {position}"))
560}
561
562/// A recursive-descent parser over the token stream. Each grammar rule is one
563/// method, and they call one another top-down, so the source reads in the same
564/// order as the grammar in the module doc.
565struct Parser<'a> {
566    tokens: &'a [Spanned],
567    pos: usize,
568    /// The character length of the source, used to position an
569    /// unexpected-end-of-input error.
570    end: usize,
571}
572
573impl Parser<'_> {
574    fn peek(&self) -> Option<&Token> {
575        self.tokens.get(self.pos).map(|s| &s.token)
576    }
577
578    fn position(&self) -> usize {
579        self.tokens.get(self.pos).map_or(self.end, |s| s.at)
580    }
581
582    fn advance(&mut self) {
583        self.pos += 1;
584    }
585
586    fn expect_end(&self) -> Result<(), ExprError> {
587        match self.peek() {
588            None => Ok(()),
589            Some(_) => Err(at_char(
590                self.position(),
591                "an unexpected trailing token; the expression already ended",
592            )),
593        }
594    }
595
596    fn parse_or(&mut self) -> Result<Bool, ExprError> {
597        let mut left = self.parse_and()?;
598        while matches!(self.peek(), Some(Token::OrOr)) {
599            self.advance();
600            let right = self.parse_and()?;
601            left = Bool::Or(Box::new(left), Box::new(right));
602        }
603        Ok(left)
604    }
605
606    fn parse_and(&mut self) -> Result<Bool, ExprError> {
607        let mut left = self.parse_unary()?;
608        while matches!(self.peek(), Some(Token::AndAnd)) {
609            self.advance();
610            let right = self.parse_unary()?;
611            left = Bool::And(Box::new(left), Box::new(right));
612        }
613        Ok(left)
614    }
615
616    fn parse_unary(&mut self) -> Result<Bool, ExprError> {
617        if matches!(self.peek(), Some(Token::Bang)) {
618            self.advance();
619            let inner = self.parse_unary()?;
620            Ok(Bool::Not(Box::new(inner)))
621        } else {
622            self.parse_atom()
623        }
624    }
625
626    fn parse_atom(&mut self) -> Result<Bool, ExprError> {
627        if matches!(self.peek(), Some(Token::LParen)) {
628            self.advance();
629            let inner = self.parse_or()?;
630            match self.peek() {
631                Some(Token::RParen) => {
632                    self.advance();
633                    Ok(inner)
634                }
635                _ => Err(at_char(self.position(), "a missing closing `)`")),
636            }
637        } else {
638            self.parse_comparison()
639        }
640    }
641
642    fn parse_comparison(&mut self) -> Result<Bool, ExprError> {
643        let left = self.parse_operand()?;
644        let op = match self.peek() {
645            Some(Token::EqEq) => CmpOp::Eq,
646            Some(Token::NotEq) => CmpOp::Ne,
647            Some(Token::Lt) => CmpOp::Lt,
648            Some(Token::Le) => CmpOp::Le,
649            Some(Token::Gt) => CmpOp::Gt,
650            Some(Token::Ge) => CmpOp::Ge,
651            _ => return Ok(Bool::Truthy(left)),
652        };
653        self.advance();
654        let right = self.parse_operand()?;
655        Ok(Bool::Compare { left, op, right })
656    }
657
658    fn parse_operand(&mut self) -> Result<Operand, ExprError> {
659        match self.peek() {
660            Some(Token::Number(n)) => {
661                let value = Operand::Literal(Value::Number(n.clone()));
662                self.advance();
663                Ok(value)
664            }
665            Some(Token::Str(s)) => {
666                let value = Operand::Literal(Value::String(s.clone()));
667                self.advance();
668                Ok(value)
669            }
670            Some(Token::True) => {
671                self.advance();
672                Ok(Operand::Literal(Value::Bool(true)))
673            }
674            Some(Token::False) => {
675                self.advance();
676                Ok(Operand::Literal(Value::Bool(false)))
677            }
678            Some(Token::Null) => {
679                self.advance();
680                Ok(Operand::Literal(Value::Null))
681            }
682            Some(Token::Ident(name)) => {
683                let name = name.clone();
684                self.advance();
685                self.parse_path(name)
686            }
687            _ => Err(at_char(
688                self.position(),
689                "a value or path where one was required",
690            )),
691        }
692    }
693
694    fn parse_path(&mut self, first: String) -> Result<Operand, ExprError> {
695        let mut segments = vec![Segment::Key(first)];
696        while matches!(self.peek(), Some(Token::Dot)) {
697            self.advance();
698            match self.peek() {
699                Some(Token::Ident(name)) => {
700                    segments.push(Segment::Key(name.clone()));
701                    self.advance();
702                }
703                Some(Token::Number(n)) => {
704                    let index = n
705                        .as_u64()
706                        .and_then(|v| usize::try_from(v).ok())
707                        .ok_or_else(|| {
708                            at_char(
709                                self.position(),
710                                "a path segment that is not a non-negative integer index",
711                            )
712                        })?;
713                    segments.push(Segment::Index(index));
714                    self.advance();
715                }
716                _ => {
717                    return Err(at_char(self.position(), "a missing path segment after `.`"));
718                }
719            }
720        }
721        Ok(Operand::Path(segments))
722    }
723}
724
725// ---------------------------------------------------------------------------
726// Evaluation
727// ---------------------------------------------------------------------------
728
729fn eval_bool(node: &Bool, root: &Value) -> bool {
730    match node {
731        Bool::Or(a, b) => eval_bool(a, root) || eval_bool(b, root),
732        Bool::And(a, b) => eval_bool(a, root) && eval_bool(b, root),
733        Bool::Not(a) => !eval_bool(a, root),
734        Bool::Compare { left, op, right } => eval_compare(left, *op, right, root),
735        Bool::Truthy(operand) => matches!(resolve(operand, root), Some(Value::Bool(true))),
736    }
737}
738
739/// Resolves an operand to the value it names, or `None` when a path is missing.
740/// A literal always resolves (a literal `null` resolves to `Some(Null)`, which
741/// is what keeps `null` distinct from a missing path).
742fn resolve<'a>(operand: &'a Operand, root: &'a Value) -> Option<&'a Value> {
743    match operand {
744        Operand::Literal(value) => Some(value),
745        Operand::Path(segments) => resolve_path(segments, root),
746    }
747}
748
749/// Walks a path of segments from the root of a value, returning the value it
750/// names or `None` when the path is missing (an absent key, an index past the
751/// end, or a descent into a non-container). Shared by the expression evaluator
752/// and by [`Reference::resolve`], so a `map` node's `over` reference and a branch
753/// path resolve a routed value the identical way.
754fn resolve_path<'a>(segments: &[Segment], root: &'a Value) -> Option<&'a Value> {
755    let mut current = root;
756    for segment in segments {
757        current = match (current, segment) {
758            (Value::Object(map), Segment::Key(key)) => map.get(key)?,
759            (Value::Array(items), Segment::Index(index)) => items.get(*index)?,
760            _ => return None,
761        };
762    }
763    Some(current)
764}
765
766fn eval_compare(left: &Operand, op: CmpOp, right: &Operand, root: &Value) -> bool {
767    // A missing operand makes every comparison false, so a branch never fires on
768    // absent data.
769    let (Some(l), Some(r)) = (resolve(left, root), resolve(right, root)) else {
770        return false;
771    };
772
773    match op {
774        CmpOp::Eq => values_equal(l, r),
775        CmpOp::Ne => !values_equal(l, r),
776        CmpOp::Lt => matches!(order(l, r), Some(Ordering::Less)),
777        CmpOp::Le => matches!(order(l, r), Some(Ordering::Less | Ordering::Equal)),
778        CmpOp::Gt => matches!(order(l, r), Some(Ordering::Greater)),
779        CmpOp::Ge => matches!(order(l, r), Some(Ordering::Greater | Ordering::Equal)),
780    }
781}
782
783/// Equality across all types: numbers compare by mathematical value, and any two
784/// values of different types are unequal.
785fn values_equal(l: &Value, r: &Value) -> bool {
786    match (l, r) {
787        (Value::Number(a), Value::Number(b)) => number_cmp(a, b) == Some(Ordering::Equal),
788        _ => l == r,
789    }
790}
791
792/// Ordering, defined only for two numbers or two strings; every other pairing is
793/// unordered.
794fn order(l: &Value, r: &Value) -> Option<Ordering> {
795    match (l, r) {
796        (Value::Number(a), Value::Number(b)) => number_cmp(a, b),
797        (Value::String(a), Value::String(b)) => Some(a.cmp(b)),
798        _ => None,
799    }
800}
801
802/// Compares two JSON numbers by mathematical value. Two integers compare exactly
803/// through `i128`; when either is floating-point both are compared as `f64`.
804fn number_cmp(a: &Number, b: &Number) -> Option<Ordering> {
805    if let (Some(x), Some(y)) = (as_i128(a), as_i128(b)) {
806        return Some(x.cmp(&y));
807    }
808    match (a.as_f64(), b.as_f64()) {
809        (Some(x), Some(y)) => x.partial_cmp(&y),
810        _ => None,
811    }
812}
813
814/// The exact integer value of a number, or `None` when it is floating-point.
815fn as_i128(n: &Number) -> Option<i128> {
816    n.as_u64()
817        .map(i128::from)
818        .or_else(|| n.as_i64().map(i128::from))
819}
820
821#[cfg(test)]
822mod tests {
823    use super::*;
824    use proptest::prelude::*;
825    use serde_json::json;
826
827    fn eval(expr: &str, value: &Value) -> bool {
828        parse(expr)
829            .unwrap_or_else(|e| panic!("`{expr}` should parse: {e}"))
830            .eval(value)
831    }
832
833    // --- The sample from the validator's own test must parse and evaluate. ---
834
835    #[test]
836    fn sample_score_expression() {
837        assert!(eval("score > 0.8", &json!({"score": 0.9})));
838        assert!(!eval("score > 0.8", &json!({"score": 0.5})));
839    }
840
841    // --- Each operator. ---
842
843    #[test]
844    fn every_comparison_operator() {
845        let v = json!({"n": 5});
846        assert!(eval("n == 5", &v));
847        assert!(!eval("n == 4", &v));
848        assert!(eval("n != 4", &v));
849        assert!(!eval("n != 5", &v));
850        assert!(eval("n < 6", &v));
851        assert!(!eval("n < 5", &v));
852        assert!(eval("n <= 5", &v));
853        assert!(eval("n > 4", &v));
854        assert!(!eval("n > 5", &v));
855        assert!(eval("n >= 5", &v));
856    }
857
858    #[test]
859    fn boolean_operators() {
860        let v = json!({"a": true, "b": false});
861        assert!(eval("a && !b", &v));
862        assert!(eval("a || b", &v));
863        assert!(!eval("!a", &v));
864        assert!(eval("!b", &v));
865        assert!(!eval("a && b", &v));
866    }
867
868    // --- Precedence and parenthesization. ---
869
870    #[test]
871    fn and_binds_tighter_than_or() {
872        // false || (true && true) == true; if || bound tighter it would be
873        // (false || true) && true == true too, so use a distinguishing case:
874        // true || (false && false) == true, vs (true || false) && false == false.
875        let v = json!({});
876        assert!(eval("true || false && false", &v));
877        assert!(!eval("(true || false) && false", &v));
878    }
879
880    #[test]
881    fn not_binds_tighter_than_and() {
882        let v = json!({"a": false, "b": true});
883        // !a && b parses as (!a) && b == true && true == true.
884        assert!(eval("!a && b", &v));
885    }
886
887    #[test]
888    fn comparison_binds_tighter_than_not() {
889        // !score > 0.8 parses as !(score > 0.8).
890        assert!(eval("!score > 0.8", &json!({"score": 0.5})));
891        assert!(!eval("!score > 0.8", &json!({"score": 0.9})));
892    }
893
894    #[test]
895    fn parentheses_group_boolean_expressions() {
896        let v = json!({"a": true, "b": false, "c": true});
897        assert!(eval("a && (b || c)", &v));
898        assert!(!eval("(a && b) || (b && c)", &v));
899    }
900
901    #[test]
902    fn comparisons_do_not_chain() {
903        assert!(parse("1 < 2 < 3").is_err());
904    }
905
906    #[test]
907    fn a_boolean_group_is_not_a_comparison_operand() {
908        assert!(parse("(a > b) > c").is_err());
909    }
910
911    // --- Paths, including array indexing. ---
912
913    #[test]
914    fn nested_and_indexed_paths() {
915        let v = json!({"output": {"score": 0.9}, "items": [{"score": 1}, {"score": 2}]});
916        assert!(eval("output.score > 0.8", &v));
917        assert!(eval("items.0.score == 1", &v));
918        assert!(eval("items.1.score == 2", &v));
919    }
920
921    #[test]
922    fn bare_path_is_truthy_only_for_boolean_true() {
923        assert!(eval("flag", &json!({"flag": true})));
924        assert!(!eval("flag", &json!({"flag": false})));
925        assert!(!eval("flag", &json!({"flag": 1})));
926        assert!(!eval("flag", &json!({"flag": "true"})));
927        assert!(!eval("flag", &json!({})));
928    }
929
930    // --- Missing-path semantics. ---
931
932    #[test]
933    fn missing_path_makes_every_comparison_false() {
934        let v = json!({});
935        assert!(!eval("missing == 1", &v));
936        assert!(!eval("missing != 1", &v));
937        assert!(!eval("missing < 1", &v));
938        assert!(!eval("missing > 1", &v));
939        // But negation gives a deliberate handle on absence.
940        assert!(eval("!(missing > 1)", &v));
941    }
942
943    #[test]
944    fn missing_is_distinct_from_null() {
945        // A present null equals a null literal; a missing path does not.
946        assert!(eval("x == null", &json!({"x": null})));
947        assert!(!eval("missing == null", &json!({})));
948    }
949
950    #[test]
951    fn descending_into_a_non_container_is_missing() {
952        let v = json!({"x": 5});
953        assert!(!eval("x.y == 1", &v));
954        assert!(!eval("x.0 == 1", &v));
955    }
956
957    // --- Type-mismatch semantics. ---
958
959    #[test]
960    fn cross_type_equality_is_never_equal() {
961        assert!(!eval("x == 5", &json!({"x": "5"})));
962        assert!(eval("x != 5", &json!({"x": "5"})));
963        assert!(!eval("x == 1", &json!({"x": true})));
964    }
965
966    #[test]
967    fn cross_type_ordering_is_false() {
968        assert!(!eval("x < 5", &json!({"x": "5"})));
969        assert!(!eval("x > 5", &json!({"x": "5"})));
970        assert!(!eval("x < 5", &json!({"x": true})));
971        assert!(!eval("x < 5", &json!({"x": null})));
972    }
973
974    #[test]
975    fn strings_order_lexicographically() {
976        assert!(eval("x < \"b\"", &json!({"x": "a"})));
977        assert!(!eval("x < \"a\"", &json!({"x": "b"})));
978        assert!(eval("x == \"hi\"", &json!({"x": "hi"})));
979    }
980
981    // --- Number edge cases. ---
982
983    #[test]
984    fn integer_and_float_equality() {
985        assert!(eval("x == 1", &json!({"x": 1.0})));
986        assert!(eval("x == 1.0", &json!({"x": 1})));
987        assert!(eval("x >= 1", &json!({"x": 1.0})));
988    }
989
990    #[test]
991    fn large_integers_compare_exactly() {
992        // Two distinct integers beyond f64's exact range must not collide.
993        let big = json!({"a": 9_007_199_254_740_993_i64});
994        assert!(eval("a == 9007199254740993", &big));
995        assert!(!eval("a == 9007199254740992", &big));
996    }
997
998    #[test]
999    fn negative_and_signed_comparison() {
1000        assert!(eval("x < 0", &json!({"x": -3})));
1001        assert!(eval("x == -0.5", &json!({"x": -0.5})));
1002        assert!(eval("x > y", &json!({"x": 1, "y": -1})));
1003    }
1004
1005    // --- Cap and parse errors. ---
1006
1007    #[test]
1008    fn the_length_cap_rejects_longer_input_and_names_it() {
1009        let ok = "a".repeat(MAX_EXPRESSION_LEN);
1010        assert!(parse(&ok).is_ok());
1011        let too_long = "a".repeat(MAX_EXPRESSION_LEN + 1);
1012        let err = parse(&too_long).expect_err("over the cap");
1013        assert!(
1014            err.message().contains(&MAX_EXPRESSION_LEN.to_string()),
1015            "names the cap: {err}"
1016        );
1017    }
1018
1019    #[test]
1020    fn assorted_syntax_errors() {
1021        for bad in [
1022            "",
1023            "&&",
1024            "a &&",
1025            "a && (b",
1026            "== 5",
1027            "a = 5",
1028            "a & b",
1029            "a | b",
1030            "1.2.3 == 1",
1031            "\"unterminated",
1032            "a.",
1033            "a.-1 == 1",
1034        ] {
1035            assert!(parse(bad).is_err(), "`{bad}` should be a parse error");
1036        }
1037    }
1038
1039    // --- References (the `map` `over` resolver). ---
1040
1041    #[test]
1042    fn a_reference_resolves_a_top_level_and_nested_array() {
1043        let top = parse_reference("items").expect("`items` parses");
1044        assert_eq!(
1045            top.resolve(&json!({"items": [1, 2, 3]})),
1046            Some(&json!([1, 2, 3]))
1047        );
1048        let nested = parse_reference("output.items").expect("`output.items` parses");
1049        assert_eq!(
1050            nested.resolve(&json!({"output": {"items": ["a"]}})),
1051            Some(&json!(["a"]))
1052        );
1053        let indexed = parse_reference("results.0.items").expect("indexed path parses");
1054        assert_eq!(
1055            indexed.resolve(&json!({"results": [{"items": [true]}]})),
1056            Some(&json!([true]))
1057        );
1058    }
1059
1060    #[test]
1061    fn a_missing_reference_resolves_to_none() {
1062        let reference = parse_reference("items").expect("parses");
1063        assert_eq!(reference.resolve(&json!({})), None);
1064        assert_eq!(reference.resolve(&json!({"other": [1]})), None);
1065        // Descending into a non-container is missing, exactly as in eval.
1066        let deep = parse_reference("x.items").expect("parses");
1067        assert_eq!(deep.resolve(&json!({"x": 5})), None);
1068    }
1069
1070    #[test]
1071    fn a_reference_may_name_any_json_value_not_only_arrays() {
1072        // The resolver is type-agnostic; the engine decides an array is required.
1073        let reference = parse_reference("value").expect("parses");
1074        assert_eq!(reference.resolve(&json!({"value": 5})), Some(&json!(5)));
1075        assert_eq!(
1076            reference.resolve(&json!({"value": {"k": 1}})),
1077            Some(&json!({"k": 1}))
1078        );
1079    }
1080
1081    #[test]
1082    fn a_literal_or_malformed_reference_is_rejected() {
1083        assert!(
1084            parse_reference("5").is_err(),
1085            "a bare literal is not a path"
1086        );
1087        assert!(
1088            parse_reference("\"x\"").is_err(),
1089            "a string literal is not a path"
1090        );
1091        assert!(parse_reference("true").is_err(), "a keyword is not a path");
1092        assert!(
1093            parse_reference("items ==").is_err(),
1094            "trailing tokens rejected"
1095        );
1096        assert!(
1097            parse_reference("items.").is_err(),
1098            "a dangling dot is rejected"
1099        );
1100        assert!(
1101            parse_reference("").is_err(),
1102            "an empty reference is rejected"
1103        );
1104    }
1105
1106    // --- Property tests. ---
1107
1108    proptest! {
1109        /// No input string, however arbitrary, panics the parser.
1110        #[test]
1111        fn parsing_never_panics(input in ".*") {
1112            let _ = parse(&input);
1113        }
1114
1115        /// A generator biased toward near-miss syntax also never panics.
1116        #[test]
1117        fn near_miss_parsing_never_panics(
1118            input in "[a-z0-9_. ()!&|<>=\"'.-]{0,80}"
1119        ) {
1120            let _ = parse(&input);
1121        }
1122
1123        /// Any input at or over the cap length is handled without panic, and a
1124        /// too-long one is always rejected.
1125        #[test]
1126        fn cap_always_holds(input in "a{500,700}") {
1127            let result = parse(&input);
1128            if input.chars().count() > MAX_EXPRESSION_LEN {
1129                prop_assert!(result.is_err());
1130            }
1131        }
1132
1133        /// Evaluation never panics for any parsed expression against any JSON
1134        /// value. The expression is drawn from a grammar-shaped generator so
1135        /// real ASTs (not just trivial ones) are exercised.
1136        #[test]
1137        fn eval_never_panics(expr in expr_strategy(), value in json_strategy()) {
1138            if let Ok(parsed) = parse(&expr) {
1139                let _ = parsed.eval(&value);
1140            }
1141        }
1142    }
1143
1144    /// A strategy that builds plausible expression strings from the real
1145    /// vocabulary, so parses often succeed and eval is genuinely exercised.
1146    fn expr_strategy() -> impl Strategy<Value = String> {
1147        let leaf = prop_oneof![
1148            Just("score".to_string()),
1149            Just("output.score".to_string()),
1150            Just("items.0.score".to_string()),
1151            Just("flag".to_string()),
1152            Just("0.8".to_string()),
1153            Just("5".to_string()),
1154            Just("-1".to_string()),
1155            Just("true".to_string()),
1156            Just("null".to_string()),
1157            Just("\"hi\"".to_string()),
1158        ];
1159        let comparison = (leaf.clone(), "==|!=|<|<=|>|>=", leaf.clone())
1160            .prop_map(|(l, op, r)| format!("{l} {op} {r}"));
1161        let atom = prop_oneof![leaf, comparison];
1162        atom.prop_recursive(4, 32, 4, |inner| {
1163            prop_oneof![
1164                inner.clone().prop_map(|e| format!("!{e}")),
1165                inner.clone().prop_map(|e| format!("({e})")),
1166                (inner.clone(), inner.clone()).prop_map(|(a, b)| format!("{a} && {b}")),
1167                (inner.clone(), inner).prop_map(|(a, b)| format!("{a} || {b}")),
1168            ]
1169        })
1170    }
1171
1172    /// A strategy for arbitrary JSON values of bounded depth.
1173    fn json_strategy() -> impl Strategy<Value = Value> {
1174        let leaf = prop_oneof![
1175            Just(Value::Null),
1176            any::<bool>().prop_map(Value::Bool),
1177            any::<i64>().prop_map(|n| json!(n)),
1178            any::<f64>()
1179                .prop_filter("finite", |f| f.is_finite())
1180                .prop_map(|f| json!(f)),
1181            ".*".prop_map(Value::String),
1182        ];
1183        leaf.prop_recursive(3, 16, 4, |inner| {
1184            prop_oneof![
1185                prop::collection::vec(inner.clone(), 0..4).prop_map(Value::Array),
1186                prop::collection::hash_map("[a-z]{1,5}", inner, 0..4)
1187                    .prop_map(|m| Value::Object(m.into_iter().collect())),
1188            ]
1189        })
1190    }
1191}