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    /// Every path this expression reads, in the order the source names them, as
150    /// borrowed segment slices.
151    ///
152    /// Literal operands are not paths and do not appear. A path repeated in the
153    /// source appears once per mention, because this reports what the
154    /// expression READS rather than a de-duplicated set of locations.
155    ///
156    /// Exported for the same reason [`compare`] is: a second consumer needs the
157    /// expression's own answer rather than one re-derived beside it. The graph
158    /// validator checks a fold's `stop_when` against the declared shape of the
159    /// value it will read, and the only honest source for "which locations does
160    /// this predicate read" is the parse that already found them. Eval is
161    /// untouched: this walks the same AST and changes nothing about it.
162    #[must_use]
163    pub fn paths(&self) -> Vec<&[Segment]> {
164        let mut found = Vec::new();
165        collect_paths(&self.root, &mut found);
166        found
167    }
168}
169
170/// Pushes every path operand under `node` onto `found`, left to right.
171fn collect_paths<'a>(node: &'a Bool, found: &mut Vec<&'a [Segment]>) {
172    match node {
173        Bool::Or(left, right) | Bool::And(left, right) => {
174            collect_paths(left, found);
175            collect_paths(right, found);
176        }
177        Bool::Not(inner) => collect_paths(inner, found),
178        Bool::Compare { left, right, .. } => {
179            if let Operand::Path(segments) = left {
180                found.push(segments);
181            }
182            if let Operand::Path(segments) = right {
183                found.push(segments);
184            }
185        }
186        Bool::Truthy(operand) => {
187            if let Operand::Path(segments) = operand {
188                found.push(segments);
189            }
190        }
191    }
192}
193
194/// Orders two JSON values by the expression language's OWN ordering rule, the
195/// one `<`, `<=`, `>`, and `>=` are defined by: two numbers compare by
196/// mathematical value (exactly through `i128` when both are integral, as `f64`
197/// when either is fractional), two strings compare lexicographically, and every
198/// other pairing is unordered (`None`).
199///
200/// Exported because a second consumer needs the identical order: a `fold`
201/// node's `FoldJoin::BestBy` is an argmax over the passes, and an argmax that
202/// ordered values even slightly differently from the `stop_when` predicate
203/// beside it would make one node speak two languages. Calling this is what
204/// keeps the two from ever drifting; re-deriving the rule would not.
205///
206/// `None` also answers "is this value orderable at all", because a value orders
207/// against itself exactly when its type is one this rule orders. That is the
208/// check a `best_by` argmax needs to decide which passes may win, and it needs
209/// no type list of its own.
210#[must_use]
211pub fn compare(left: &Value, right: &Value) -> Option<Ordering> {
212    order(left, right)
213}
214
215/// A parsed reference into a routed value: a dot-separated path with array
216/// indexing, the same `path` grammar the expression language uses for an
217/// operand, standing alone as a value reference rather than inside a boolean.
218///
219/// This is how a `map` node's `over` field names the list it fans out over: the
220/// reference is resolved against the node's routed value at fan-out time, and the
221/// engine reuses the identical missing-path semantics the branch expressions use,
222/// so references resolve one consistent way across the whole document. It owns
223/// its whole path, borrows nothing, and holds no IO, clock, or randomness, so it
224/// is safe to keep and re-resolve for the life of a run.
225#[derive(Clone, Debug, PartialEq)]
226pub struct Reference {
227    segments: Vec<Segment>,
228}
229
230impl Reference {
231    /// Resolves the reference against a routed value, returning the value it
232    /// names or `None` when the path is missing (an absent key, an index past the
233    /// end, or a descent into a non-container).
234    ///
235    /// This is TOTAL: it never panics for any `value`, and its missing-path
236    /// semantics are exactly those [`Expr::eval`] uses for a path operand.
237    #[must_use]
238    pub fn resolve<'a>(&self, value: &'a Value) -> Option<&'a Value> {
239        resolve_path(&self.segments, value)
240    }
241
242    /// The path's steps, in order, as the parse read them.
243    ///
244    /// The counterpart of [`Expr::paths`] for a reference standing alone, and
245    /// exported for the same one reason: a caller that must judge a path
246    /// against a declared schema has to walk it a step at a time, and
247    /// [`Reference::resolve`] answers a different question (what does this name
248    /// in a value that already exists).
249    #[must_use]
250    pub fn segments(&self) -> &[Segment] {
251        &self.segments
252    }
253}
254
255/// Parses a bare reference path (`items`, `output.items`, `results.0.items`), the
256/// standalone counterpart of a `path` operand in the expression grammar.
257///
258/// The [`MAX_EXPRESSION_LEN`]-character cap is enforced first, exactly as [`parse`]
259/// does. Only a path is accepted: a literal (`5`, `"x"`, `true`) is rejected, so a
260/// reference always names a location in the routed value rather than a constant.
261///
262/// # Errors
263///
264/// Returns an [`ExprError`] when the input exceeds the length cap, is not a
265/// well-formed path, or is a bare literal rather than a path.
266pub fn parse_reference(input: &str) -> Result<Reference, ExprError> {
267    let chars: Vec<char> = input.chars().collect();
268    if chars.len() > MAX_EXPRESSION_LEN {
269        return Err(ExprError::new(format!(
270            "reference is {} characters long, which exceeds the {MAX_EXPRESSION_LEN}-character limit",
271            chars.len()
272        )));
273    }
274
275    let tokens = lex(&chars)?;
276    let mut parser = Parser {
277        tokens: &tokens,
278        pos: 0,
279        end: chars.len(),
280    };
281    let operand = parser.parse_operand()?;
282    parser.expect_end()?;
283    match operand {
284        Operand::Path(segments) => Ok(Reference { segments }),
285        Operand::Literal(_) => Err(ExprError::new(
286            "a reference must be a path into the routed value, not a literal",
287        )),
288    }
289}
290
291/// A boolean-valued node of the AST.
292#[derive(Clone, Debug, PartialEq)]
293enum Bool {
294    Or(Box<Bool>, Box<Bool>),
295    And(Box<Bool>, Box<Bool>),
296    Not(Box<Bool>),
297    Compare {
298        left: Operand,
299        op: CmpOp,
300        right: Operand,
301    },
302    /// A bare operand used in boolean position: true only if it resolves to the
303    /// JSON boolean `true`.
304    Truthy(Operand),
305}
306
307/// One side of a comparison, or a bare boolean operand: a literal value or a
308/// path into the routed value.
309#[derive(Clone, Debug, PartialEq)]
310enum Operand {
311    Literal(Value),
312    Path(Vec<Segment>),
313}
314
315/// One step of a path: an object key or an array index.
316///
317/// Public because a path's steps are what a caller judging a path against a
318/// declared JSON Schema has to walk; the segments are read-only data either
319/// way, and no evaluation rule reads them through this type.
320#[derive(Clone, Debug, PartialEq)]
321pub enum Segment {
322    /// An object key: the `score` in `review.score`.
323    Key(String),
324    /// An array index: the `0` in `results.0.score`.
325    Index(usize),
326}
327
328/// A comparison operator.
329#[derive(Clone, Copy, Debug, PartialEq)]
330enum CmpOp {
331    Eq,
332    Ne,
333    Lt,
334    Le,
335    Gt,
336    Ge,
337}
338
339// ---------------------------------------------------------------------------
340// Parsing
341// ---------------------------------------------------------------------------
342
343/// Parses a condition expression, or reports why it is not well-formed.
344///
345/// The [`MAX_EXPRESSION_LEN`]-character cap is enforced first, so an over-long
346/// input is rejected before any lexing. On success the returned [`Expr`] is
347/// guaranteed to evaluate totally against any JSON value.
348///
349/// # Errors
350///
351/// Returns an [`ExprError`] whose message names the problem (and, where known,
352/// the character position) when the input exceeds the length cap or does not
353/// match the grammar.
354pub fn parse(input: &str) -> Result<Expr, ExprError> {
355    let chars: Vec<char> = input.chars().collect();
356    if chars.len() > MAX_EXPRESSION_LEN {
357        return Err(ExprError::new(format!(
358            "expression is {} characters long, which exceeds the {MAX_EXPRESSION_LEN}-character limit",
359            chars.len()
360        )));
361    }
362
363    let tokens = lex(&chars)?;
364    let mut parser = Parser {
365        tokens: &tokens,
366        pos: 0,
367        end: chars.len(),
368    };
369    let root = parser.parse_or()?;
370    parser.expect_end()?;
371    Ok(Expr { root })
372}
373
374/// A lexed token together with the character position it started at.
375#[derive(Clone, Debug, PartialEq)]
376struct Spanned {
377    token: Token,
378    at: usize,
379}
380
381/// A lexical token.
382#[derive(Clone, Debug, PartialEq)]
383enum Token {
384    Ident(String),
385    Number(Number),
386    Str(String),
387    True,
388    False,
389    Null,
390    Dot,
391    LParen,
392    RParen,
393    Bang,
394    AndAnd,
395    OrOr,
396    EqEq,
397    NotEq,
398    Lt,
399    Le,
400    Gt,
401    Ge,
402}
403
404fn is_ident_start(c: char) -> bool {
405    c.is_ascii_alphabetic() || c == '_'
406}
407
408fn is_ident_continue(c: char) -> bool {
409    c.is_ascii_alphanumeric() || c == '_'
410}
411
412/// Turns the character slice into a token stream, or reports the first bad
413/// character.
414fn lex(chars: &[char]) -> Result<Vec<Spanned>, ExprError> {
415    let mut tokens = Vec::new();
416    let mut i = 0;
417    let len = chars.len();
418
419    while i < len {
420        let c = chars[i];
421        let at = i;
422
423        if c.is_whitespace() {
424            i += 1;
425            continue;
426        }
427
428        match c {
429            '(' => {
430                tokens.push(Spanned {
431                    token: Token::LParen,
432                    at,
433                });
434                i += 1;
435            }
436            ')' => {
437                tokens.push(Spanned {
438                    token: Token::RParen,
439                    at,
440                });
441                i += 1;
442            }
443            '.' => {
444                tokens.push(Spanned {
445                    token: Token::Dot,
446                    at,
447                });
448                i += 1;
449            }
450            '!' => {
451                if i + 1 < len && chars[i + 1] == '=' {
452                    tokens.push(Spanned {
453                        token: Token::NotEq,
454                        at,
455                    });
456                    i += 2;
457                } else {
458                    tokens.push(Spanned {
459                        token: Token::Bang,
460                        at,
461                    });
462                    i += 1;
463                }
464            }
465            '=' => {
466                if i + 1 < len && chars[i + 1] == '=' {
467                    tokens.push(Spanned {
468                        token: Token::EqEq,
469                        at,
470                    });
471                    i += 2;
472                } else {
473                    return Err(at_char(at, "a lone `=`; did you mean `==`?"));
474                }
475            }
476            '<' => {
477                if i + 1 < len && chars[i + 1] == '=' {
478                    tokens.push(Spanned {
479                        token: Token::Le,
480                        at,
481                    });
482                    i += 2;
483                } else {
484                    tokens.push(Spanned {
485                        token: Token::Lt,
486                        at,
487                    });
488                    i += 1;
489                }
490            }
491            '>' => {
492                if i + 1 < len && chars[i + 1] == '=' {
493                    tokens.push(Spanned {
494                        token: Token::Ge,
495                        at,
496                    });
497                    i += 2;
498                } else {
499                    tokens.push(Spanned {
500                        token: Token::Gt,
501                        at,
502                    });
503                    i += 1;
504                }
505            }
506            '&' => {
507                if i + 1 < len && chars[i + 1] == '&' {
508                    tokens.push(Spanned {
509                        token: Token::AndAnd,
510                        at,
511                    });
512                    i += 2;
513                } else {
514                    return Err(at_char(at, "a lone `&`; did you mean `&&`?"));
515                }
516            }
517            '|' => {
518                if i + 1 < len && chars[i + 1] == '|' {
519                    tokens.push(Spanned {
520                        token: Token::OrOr,
521                        at,
522                    });
523                    i += 2;
524                } else {
525                    return Err(at_char(at, "a lone `|`; did you mean `||`?"));
526                }
527            }
528            '"' => {
529                let (token, next) = lex_string(chars, i)?;
530                tokens.push(Spanned { token, at });
531                i = next;
532            }
533            _ if c.is_ascii_digit()
534                || (c == '-' && i + 1 < len && chars[i + 1].is_ascii_digit()) =>
535            {
536                let (token, next) = lex_number(chars, i)?;
537                tokens.push(Spanned { token, at });
538                i = next;
539            }
540            _ if is_ident_start(c) => {
541                let (token, next) = lex_ident(chars, i);
542                tokens.push(Spanned { token, at });
543                i = next;
544            }
545            _ => {
546                return Err(at_char(at, format!("an unexpected character `{c}`")));
547            }
548        }
549    }
550
551    Ok(tokens)
552}
553
554/// Lexes a double-quoted string starting at the opening quote. Supports the
555/// escapes `\"`, `\\`, `\/`, `\n`, `\t`, and `\r`; any other escape, or an
556/// unterminated string, is an error.
557fn lex_string(chars: &[char], start: usize) -> Result<(Token, usize), ExprError> {
558    let len = chars.len();
559    let mut i = start + 1;
560    let mut value = String::new();
561
562    while i < len {
563        let c = chars[i];
564        if c == '"' {
565            return Ok((Token::Str(value), i + 1));
566        }
567        if c == '\\' {
568            i += 1;
569            if i >= len {
570                break;
571            }
572            match chars[i] {
573                '"' => value.push('"'),
574                '\\' => value.push('\\'),
575                '/' => value.push('/'),
576                'n' => value.push('\n'),
577                't' => value.push('\t'),
578                'r' => value.push('\r'),
579                other => {
580                    return Err(at_char(
581                        i,
582                        format!("an unsupported string escape `\\{other}`"),
583                    ));
584                }
585            }
586            i += 1;
587        } else {
588            value.push(c);
589            i += 1;
590        }
591    }
592
593    Err(at_char(start, "an unterminated string literal"))
594}
595
596/// Lexes an integer or decimal number (optionally negative). Exponents are not
597/// supported.
598fn lex_number(chars: &[char], start: usize) -> Result<(Token, usize), ExprError> {
599    let len = chars.len();
600    let mut i = start;
601    if chars[i] == '-' {
602        i += 1;
603    }
604    while i < len && chars[i].is_ascii_digit() {
605        i += 1;
606    }
607    // A fractional part is consumed only when a digit actually follows the dot,
608    // so `items.0` lexes as the number `0` then a `.`, not as `0.` waiting for a
609    // fraction.
610    if i + 1 < len && chars[i] == '.' && chars[i + 1].is_ascii_digit() {
611        i += 1;
612        while i < len && chars[i].is_ascii_digit() {
613            i += 1;
614        }
615    }
616
617    let text: String = chars[start..i].iter().collect();
618    let number: Number = serde_json::from_str(&text)
619        .map_err(|_| at_char(start, format!("a malformed number `{text}`")))?;
620    Ok((Token::Number(number), i))
621}
622
623/// Lexes an identifier, promoting the three reserved words to their keyword
624/// tokens.
625fn lex_ident(chars: &[char], start: usize) -> (Token, usize) {
626    let len = chars.len();
627    let mut i = start;
628    while i < len && is_ident_continue(chars[i]) {
629        i += 1;
630    }
631    let text: String = chars[start..i].iter().collect();
632    let token = match text.as_str() {
633        "true" => Token::True,
634        "false" => Token::False,
635        "null" => Token::Null,
636        _ => Token::Ident(text),
637    };
638    (token, i)
639}
640
641fn at_char(position: usize, what: impl std::fmt::Display) -> ExprError {
642    ExprError::new(format!("found {what} at character {position}"))
643}
644
645/// A recursive-descent parser over the token stream. Each grammar rule is one
646/// method, and they call one another top-down, so the source reads in the same
647/// order as the grammar in the module doc.
648struct Parser<'a> {
649    tokens: &'a [Spanned],
650    pos: usize,
651    /// The character length of the source, used to position an
652    /// unexpected-end-of-input error.
653    end: usize,
654}
655
656impl Parser<'_> {
657    fn peek(&self) -> Option<&Token> {
658        self.tokens.get(self.pos).map(|s| &s.token)
659    }
660
661    fn position(&self) -> usize {
662        self.tokens.get(self.pos).map_or(self.end, |s| s.at)
663    }
664
665    fn advance(&mut self) {
666        self.pos += 1;
667    }
668
669    fn expect_end(&self) -> Result<(), ExprError> {
670        match self.peek() {
671            None => Ok(()),
672            Some(_) => Err(at_char(
673                self.position(),
674                "an unexpected trailing token; the expression already ended",
675            )),
676        }
677    }
678
679    fn parse_or(&mut self) -> Result<Bool, ExprError> {
680        let mut left = self.parse_and()?;
681        while matches!(self.peek(), Some(Token::OrOr)) {
682            self.advance();
683            let right = self.parse_and()?;
684            left = Bool::Or(Box::new(left), Box::new(right));
685        }
686        Ok(left)
687    }
688
689    fn parse_and(&mut self) -> Result<Bool, ExprError> {
690        let mut left = self.parse_unary()?;
691        while matches!(self.peek(), Some(Token::AndAnd)) {
692            self.advance();
693            let right = self.parse_unary()?;
694            left = Bool::And(Box::new(left), Box::new(right));
695        }
696        Ok(left)
697    }
698
699    fn parse_unary(&mut self) -> Result<Bool, ExprError> {
700        if matches!(self.peek(), Some(Token::Bang)) {
701            self.advance();
702            let inner = self.parse_unary()?;
703            Ok(Bool::Not(Box::new(inner)))
704        } else {
705            self.parse_atom()
706        }
707    }
708
709    fn parse_atom(&mut self) -> Result<Bool, ExprError> {
710        if matches!(self.peek(), Some(Token::LParen)) {
711            self.advance();
712            let inner = self.parse_or()?;
713            match self.peek() {
714                Some(Token::RParen) => {
715                    self.advance();
716                    Ok(inner)
717                }
718                _ => Err(at_char(self.position(), "a missing closing `)`")),
719            }
720        } else {
721            self.parse_comparison()
722        }
723    }
724
725    fn parse_comparison(&mut self) -> Result<Bool, ExprError> {
726        let left = self.parse_operand()?;
727        let op = match self.peek() {
728            Some(Token::EqEq) => CmpOp::Eq,
729            Some(Token::NotEq) => CmpOp::Ne,
730            Some(Token::Lt) => CmpOp::Lt,
731            Some(Token::Le) => CmpOp::Le,
732            Some(Token::Gt) => CmpOp::Gt,
733            Some(Token::Ge) => CmpOp::Ge,
734            _ => return Ok(Bool::Truthy(left)),
735        };
736        self.advance();
737        let right = self.parse_operand()?;
738        Ok(Bool::Compare { left, op, right })
739    }
740
741    fn parse_operand(&mut self) -> Result<Operand, ExprError> {
742        match self.peek() {
743            Some(Token::Number(n)) => {
744                let value = Operand::Literal(Value::Number(n.clone()));
745                self.advance();
746                Ok(value)
747            }
748            Some(Token::Str(s)) => {
749                let value = Operand::Literal(Value::String(s.clone()));
750                self.advance();
751                Ok(value)
752            }
753            Some(Token::True) => {
754                self.advance();
755                Ok(Operand::Literal(Value::Bool(true)))
756            }
757            Some(Token::False) => {
758                self.advance();
759                Ok(Operand::Literal(Value::Bool(false)))
760            }
761            Some(Token::Null) => {
762                self.advance();
763                Ok(Operand::Literal(Value::Null))
764            }
765            Some(Token::Ident(name)) => {
766                let name = name.clone();
767                self.advance();
768                self.parse_path(name)
769            }
770            _ => Err(at_char(
771                self.position(),
772                "a value or path where one was required",
773            )),
774        }
775    }
776
777    fn parse_path(&mut self, first: String) -> Result<Operand, ExprError> {
778        let mut segments = vec![Segment::Key(first)];
779        while matches!(self.peek(), Some(Token::Dot)) {
780            self.advance();
781            match self.peek() {
782                Some(Token::Ident(name)) => {
783                    segments.push(Segment::Key(name.clone()));
784                    self.advance();
785                }
786                Some(Token::Number(n)) => {
787                    let index = n
788                        .as_u64()
789                        .and_then(|v| usize::try_from(v).ok())
790                        .ok_or_else(|| {
791                            at_char(
792                                self.position(),
793                                "a path segment that is not a non-negative integer index",
794                            )
795                        })?;
796                    segments.push(Segment::Index(index));
797                    self.advance();
798                }
799                _ => {
800                    return Err(at_char(self.position(), "a missing path segment after `.`"));
801                }
802            }
803        }
804        Ok(Operand::Path(segments))
805    }
806}
807
808// ---------------------------------------------------------------------------
809// Evaluation
810// ---------------------------------------------------------------------------
811
812fn eval_bool(node: &Bool, root: &Value) -> bool {
813    match node {
814        Bool::Or(a, b) => eval_bool(a, root) || eval_bool(b, root),
815        Bool::And(a, b) => eval_bool(a, root) && eval_bool(b, root),
816        Bool::Not(a) => !eval_bool(a, root),
817        Bool::Compare { left, op, right } => eval_compare(left, *op, right, root),
818        Bool::Truthy(operand) => matches!(resolve(operand, root), Some(Value::Bool(true))),
819    }
820}
821
822/// Resolves an operand to the value it names, or `None` when a path is missing.
823/// A literal always resolves (a literal `null` resolves to `Some(Null)`, which
824/// is what keeps `null` distinct from a missing path).
825fn resolve<'a>(operand: &'a Operand, root: &'a Value) -> Option<&'a Value> {
826    match operand {
827        Operand::Literal(value) => Some(value),
828        Operand::Path(segments) => resolve_path(segments, root),
829    }
830}
831
832/// Walks a path of segments from the root of a value, returning the value it
833/// names or `None` when the path is missing (an absent key, an index past the
834/// end, or a descent into a non-container). Shared by the expression evaluator
835/// and by [`Reference::resolve`], so a `map` node's `over` reference and a branch
836/// path resolve a routed value the identical way.
837fn resolve_path<'a>(segments: &[Segment], root: &'a Value) -> Option<&'a Value> {
838    let mut current = root;
839    for segment in segments {
840        current = match (current, segment) {
841            (Value::Object(map), Segment::Key(key)) => map.get(key)?,
842            (Value::Array(items), Segment::Index(index)) => items.get(*index)?,
843            _ => return None,
844        };
845    }
846    Some(current)
847}
848
849fn eval_compare(left: &Operand, op: CmpOp, right: &Operand, root: &Value) -> bool {
850    // A missing operand makes every comparison false, so a branch never fires on
851    // absent data.
852    let (Some(l), Some(r)) = (resolve(left, root), resolve(right, root)) else {
853        return false;
854    };
855
856    match op {
857        CmpOp::Eq => values_equal(l, r),
858        CmpOp::Ne => !values_equal(l, r),
859        CmpOp::Lt => matches!(order(l, r), Some(Ordering::Less)),
860        CmpOp::Le => matches!(order(l, r), Some(Ordering::Less | Ordering::Equal)),
861        CmpOp::Gt => matches!(order(l, r), Some(Ordering::Greater)),
862        CmpOp::Ge => matches!(order(l, r), Some(Ordering::Greater | Ordering::Equal)),
863    }
864}
865
866/// Equality across all types: numbers compare by mathematical value, and any two
867/// values of different types are unequal.
868fn values_equal(l: &Value, r: &Value) -> bool {
869    match (l, r) {
870        (Value::Number(a), Value::Number(b)) => number_cmp(a, b) == Some(Ordering::Equal),
871        _ => l == r,
872    }
873}
874
875/// Ordering, defined only for two numbers or two strings; every other pairing is
876/// unordered.
877fn order(l: &Value, r: &Value) -> Option<Ordering> {
878    match (l, r) {
879        (Value::Number(a), Value::Number(b)) => number_cmp(a, b),
880        (Value::String(a), Value::String(b)) => Some(a.cmp(b)),
881        _ => None,
882    }
883}
884
885/// Compares two JSON numbers by mathematical value. Two integers compare exactly
886/// through `i128`; when either is floating-point both are compared as `f64`.
887fn number_cmp(a: &Number, b: &Number) -> Option<Ordering> {
888    if let (Some(x), Some(y)) = (as_i128(a), as_i128(b)) {
889        return Some(x.cmp(&y));
890    }
891    match (a.as_f64(), b.as_f64()) {
892        (Some(x), Some(y)) => x.partial_cmp(&y),
893        _ => None,
894    }
895}
896
897/// The exact integer value of a number, or `None` when it is floating-point.
898fn as_i128(n: &Number) -> Option<i128> {
899    n.as_u64()
900        .map(i128::from)
901        .or_else(|| n.as_i64().map(i128::from))
902}
903
904#[cfg(test)]
905mod tests {
906    use super::*;
907    use proptest::prelude::*;
908    use serde_json::json;
909
910    fn eval(expr: &str, value: &Value) -> bool {
911        parse(expr)
912            .unwrap_or_else(|e| panic!("`{expr}` should parse: {e}"))
913            .eval(value)
914    }
915
916    // --- The sample from the validator's own test must parse and evaluate. ---
917
918    #[test]
919    fn sample_score_expression() {
920        assert!(eval("score > 0.8", &json!({"score": 0.9})));
921        assert!(!eval("score > 0.8", &json!({"score": 0.5})));
922    }
923
924    // --- Each operator. ---
925
926    #[test]
927    fn every_comparison_operator() {
928        let v = json!({"n": 5});
929        assert!(eval("n == 5", &v));
930        assert!(!eval("n == 4", &v));
931        assert!(eval("n != 4", &v));
932        assert!(!eval("n != 5", &v));
933        assert!(eval("n < 6", &v));
934        assert!(!eval("n < 5", &v));
935        assert!(eval("n <= 5", &v));
936        assert!(eval("n > 4", &v));
937        assert!(!eval("n > 5", &v));
938        assert!(eval("n >= 5", &v));
939    }
940
941    #[test]
942    fn boolean_operators() {
943        let v = json!({"a": true, "b": false});
944        assert!(eval("a && !b", &v));
945        assert!(eval("a || b", &v));
946        assert!(!eval("!a", &v));
947        assert!(eval("!b", &v));
948        assert!(!eval("a && b", &v));
949    }
950
951    // --- Precedence and parenthesization. ---
952
953    #[test]
954    fn and_binds_tighter_than_or() {
955        // false || (true && true) == true; if || bound tighter it would be
956        // (false || true) && true == true too, so use a distinguishing case:
957        // true || (false && false) == true, vs (true || false) && false == false.
958        let v = json!({});
959        assert!(eval("true || false && false", &v));
960        assert!(!eval("(true || false) && false", &v));
961    }
962
963    #[test]
964    fn not_binds_tighter_than_and() {
965        let v = json!({"a": false, "b": true});
966        // !a && b parses as (!a) && b == true && true == true.
967        assert!(eval("!a && b", &v));
968    }
969
970    #[test]
971    fn comparison_binds_tighter_than_not() {
972        // !score > 0.8 parses as !(score > 0.8).
973        assert!(eval("!score > 0.8", &json!({"score": 0.5})));
974        assert!(!eval("!score > 0.8", &json!({"score": 0.9})));
975    }
976
977    #[test]
978    fn parentheses_group_boolean_expressions() {
979        let v = json!({"a": true, "b": false, "c": true});
980        assert!(eval("a && (b || c)", &v));
981        assert!(!eval("(a && b) || (b && c)", &v));
982    }
983
984    #[test]
985    fn comparisons_do_not_chain() {
986        assert!(parse("1 < 2 < 3").is_err());
987    }
988
989    #[test]
990    fn a_boolean_group_is_not_a_comparison_operand() {
991        assert!(parse("(a > b) > c").is_err());
992    }
993
994    // --- Paths, including array indexing. ---
995
996    #[test]
997    fn nested_and_indexed_paths() {
998        let v = json!({"output": {"score": 0.9}, "items": [{"score": 1}, {"score": 2}]});
999        assert!(eval("output.score > 0.8", &v));
1000        assert!(eval("items.0.score == 1", &v));
1001        assert!(eval("items.1.score == 2", &v));
1002    }
1003
1004    #[test]
1005    fn bare_path_is_truthy_only_for_boolean_true() {
1006        assert!(eval("flag", &json!({"flag": true})));
1007        assert!(!eval("flag", &json!({"flag": false})));
1008        assert!(!eval("flag", &json!({"flag": 1})));
1009        assert!(!eval("flag", &json!({"flag": "true"})));
1010        assert!(!eval("flag", &json!({})));
1011    }
1012
1013    // --- Missing-path semantics. ---
1014
1015    #[test]
1016    fn missing_path_makes_every_comparison_false() {
1017        let v = json!({});
1018        assert!(!eval("missing == 1", &v));
1019        assert!(!eval("missing != 1", &v));
1020        assert!(!eval("missing < 1", &v));
1021        assert!(!eval("missing > 1", &v));
1022        // But negation gives a deliberate handle on absence.
1023        assert!(eval("!(missing > 1)", &v));
1024    }
1025
1026    #[test]
1027    fn missing_is_distinct_from_null() {
1028        // A present null equals a null literal; a missing path does not.
1029        assert!(eval("x == null", &json!({"x": null})));
1030        assert!(!eval("missing == null", &json!({})));
1031    }
1032
1033    #[test]
1034    fn descending_into_a_non_container_is_missing() {
1035        let v = json!({"x": 5});
1036        assert!(!eval("x.y == 1", &v));
1037        assert!(!eval("x.0 == 1", &v));
1038    }
1039
1040    // --- Type-mismatch semantics. ---
1041
1042    #[test]
1043    fn cross_type_equality_is_never_equal() {
1044        assert!(!eval("x == 5", &json!({"x": "5"})));
1045        assert!(eval("x != 5", &json!({"x": "5"})));
1046        assert!(!eval("x == 1", &json!({"x": true})));
1047    }
1048
1049    #[test]
1050    fn cross_type_ordering_is_false() {
1051        assert!(!eval("x < 5", &json!({"x": "5"})));
1052        assert!(!eval("x > 5", &json!({"x": "5"})));
1053        assert!(!eval("x < 5", &json!({"x": true})));
1054        assert!(!eval("x < 5", &json!({"x": null})));
1055    }
1056
1057    #[test]
1058    fn strings_order_lexicographically() {
1059        assert!(eval("x < \"b\"", &json!({"x": "a"})));
1060        assert!(!eval("x < \"a\"", &json!({"x": "b"})));
1061        assert!(eval("x == \"hi\"", &json!({"x": "hi"})));
1062    }
1063
1064    // --- Number edge cases. ---
1065
1066    #[test]
1067    fn integer_and_float_equality() {
1068        assert!(eval("x == 1", &json!({"x": 1.0})));
1069        assert!(eval("x == 1.0", &json!({"x": 1})));
1070        assert!(eval("x >= 1", &json!({"x": 1.0})));
1071    }
1072
1073    #[test]
1074    fn large_integers_compare_exactly() {
1075        // Two distinct integers beyond f64's exact range must not collide.
1076        let big = json!({"a": 9_007_199_254_740_993_i64});
1077        assert!(eval("a == 9007199254740993", &big));
1078        assert!(!eval("a == 9007199254740992", &big));
1079    }
1080
1081    #[test]
1082    fn negative_and_signed_comparison() {
1083        assert!(eval("x < 0", &json!({"x": -3})));
1084        assert!(eval("x == -0.5", &json!({"x": -0.5})));
1085        assert!(eval("x > y", &json!({"x": 1, "y": -1})));
1086    }
1087
1088    // --- Cap and parse errors. ---
1089
1090    #[test]
1091    fn the_length_cap_rejects_longer_input_and_names_it() {
1092        let ok = "a".repeat(MAX_EXPRESSION_LEN);
1093        assert!(parse(&ok).is_ok());
1094        let too_long = "a".repeat(MAX_EXPRESSION_LEN + 1);
1095        let err = parse(&too_long).expect_err("over the cap");
1096        assert!(
1097            err.message().contains(&MAX_EXPRESSION_LEN.to_string()),
1098            "names the cap: {err}"
1099        );
1100    }
1101
1102    #[test]
1103    fn assorted_syntax_errors() {
1104        for bad in [
1105            "",
1106            "&&",
1107            "a &&",
1108            "a && (b",
1109            "== 5",
1110            "a = 5",
1111            "a & b",
1112            "a | b",
1113            "1.2.3 == 1",
1114            "\"unterminated",
1115            "a.",
1116            "a.-1 == 1",
1117        ] {
1118            assert!(parse(bad).is_err(), "`{bad}` should be a parse error");
1119        }
1120    }
1121
1122    // --- The exported ordering (the `fold` `best_by` argmax). ---
1123
1124    #[test]
1125    fn the_exported_order_is_the_one_the_operators_use() {
1126        // Numbers by mathematical value, integers exactly, strings
1127        // lexicographically: the same answers `<` and `>` give.
1128        assert_eq!(compare(&json!(1), &json!(2)), Some(Ordering::Less));
1129        assert_eq!(compare(&json!(1), &json!(1.0)), Some(Ordering::Equal));
1130        assert_eq!(
1131            compare(
1132                &json!(9_007_199_254_740_993_i64),
1133                &json!(9_007_199_254_740_992_i64)
1134            ),
1135            Some(Ordering::Greater)
1136        );
1137        assert_eq!(compare(&json!("a"), &json!("b")), Some(Ordering::Less));
1138        // Everything else is unordered, so nothing outside numbers and strings
1139        // can win an argmax.
1140        for value in [json!(true), json!(null), json!({"a": 1}), json!([1])] {
1141            assert_eq!(compare(&value, &value), None, "{value} must not order");
1142        }
1143        assert_eq!(compare(&json!(1), &json!("1")), None);
1144    }
1145
1146    proptest! {
1147        /// The export agrees with the operators for every pair of values: an
1148        /// ordering the argmax reads is one `<` would have agreed with.
1149        #[test]
1150        fn the_export_agrees_with_the_ordering_operators(
1151            left in json_strategy(),
1152            right in json_strategy(),
1153        ) {
1154            let value = json!({"l": left, "r": right});
1155            let less = parse("l < r").unwrap().eval(&value);
1156            let greater = parse("l > r").unwrap().eval(&value);
1157            let (l, r) = (&value["l"], &value["r"]);
1158            prop_assert_eq!(compare(l, r) == Some(Ordering::Less), less);
1159            prop_assert_eq!(compare(l, r) == Some(Ordering::Greater), greater);
1160        }
1161    }
1162
1163    // --- References (the `map` `over` resolver). ---
1164
1165    #[test]
1166    fn a_reference_resolves_a_top_level_and_nested_array() {
1167        let top = parse_reference("items").expect("`items` parses");
1168        assert_eq!(
1169            top.resolve(&json!({"items": [1, 2, 3]})),
1170            Some(&json!([1, 2, 3]))
1171        );
1172        let nested = parse_reference("output.items").expect("`output.items` parses");
1173        assert_eq!(
1174            nested.resolve(&json!({"output": {"items": ["a"]}})),
1175            Some(&json!(["a"]))
1176        );
1177        let indexed = parse_reference("results.0.items").expect("indexed path parses");
1178        assert_eq!(
1179            indexed.resolve(&json!({"results": [{"items": [true]}]})),
1180            Some(&json!([true]))
1181        );
1182    }
1183
1184    #[test]
1185    fn a_missing_reference_resolves_to_none() {
1186        let reference = parse_reference("items").expect("parses");
1187        assert_eq!(reference.resolve(&json!({})), None);
1188        assert_eq!(reference.resolve(&json!({"other": [1]})), None);
1189        // Descending into a non-container is missing, exactly as in eval.
1190        let deep = parse_reference("x.items").expect("parses");
1191        assert_eq!(deep.resolve(&json!({"x": 5})), None);
1192    }
1193
1194    #[test]
1195    fn a_reference_may_name_any_json_value_not_only_arrays() {
1196        // The resolver is type-agnostic; the engine decides an array is required.
1197        let reference = parse_reference("value").expect("parses");
1198        assert_eq!(reference.resolve(&json!({"value": 5})), Some(&json!(5)));
1199        assert_eq!(
1200            reference.resolve(&json!({"value": {"k": 1}})),
1201            Some(&json!({"k": 1}))
1202        );
1203    }
1204
1205    #[test]
1206    fn a_literal_or_malformed_reference_is_rejected() {
1207        assert!(
1208            parse_reference("5").is_err(),
1209            "a bare literal is not a path"
1210        );
1211        assert!(
1212            parse_reference("\"x\"").is_err(),
1213            "a string literal is not a path"
1214        );
1215        assert!(parse_reference("true").is_err(), "a keyword is not a path");
1216        assert!(
1217            parse_reference("items ==").is_err(),
1218            "trailing tokens rejected"
1219        );
1220        assert!(
1221            parse_reference("items.").is_err(),
1222            "a dangling dot is rejected"
1223        );
1224        assert!(
1225            parse_reference("").is_err(),
1226            "an empty reference is rejected"
1227        );
1228    }
1229
1230    /// A reference hands back the steps it parsed, keys and indices alike, so a
1231    /// caller can walk a declared schema by them.
1232    #[test]
1233    fn a_reference_reports_its_segments() {
1234        let reference = parse_reference("results.0.review.score").expect("parses");
1235        assert_eq!(
1236            reference.segments(),
1237            [
1238                Segment::Key("results".to_owned()),
1239                Segment::Index(0),
1240                Segment::Key("review".to_owned()),
1241                Segment::Key("score".to_owned()),
1242            ]
1243        );
1244    }
1245
1246    /// An expression reports every path it reads and no literal, in source
1247    /// order, including a path mentioned twice.
1248    #[test]
1249    fn an_expression_reports_the_paths_it_reads() {
1250        let expr =
1251            parse("score >= 0.85 && !(review.flags.0 == \"stale\") || score < 0").expect("parses");
1252        let paths: Vec<Vec<Segment>> = expr.paths().iter().map(|p| p.to_vec()).collect();
1253        assert_eq!(
1254            paths,
1255            vec![
1256                vec![Segment::Key("score".to_owned())],
1257                vec![
1258                    Segment::Key("review".to_owned()),
1259                    Segment::Key("flags".to_owned()),
1260                    Segment::Index(0),
1261                ],
1262                vec![Segment::Key("score".to_owned())],
1263            ]
1264        );
1265    }
1266
1267    /// An expression made only of literals reads nothing, so it reports no
1268    /// path at all.
1269    #[test]
1270    fn a_literal_only_expression_reads_no_path() {
1271        assert!(parse("1 == 1").expect("parses").paths().is_empty());
1272        assert!(parse("true").expect("parses").paths().is_empty());
1273    }
1274
1275    // --- Property tests. ---
1276
1277    proptest! {
1278        /// No input string, however arbitrary, panics the parser.
1279        #[test]
1280        fn parsing_never_panics(input in ".*") {
1281            let _ = parse(&input);
1282        }
1283
1284        /// A generator biased toward near-miss syntax also never panics.
1285        #[test]
1286        fn near_miss_parsing_never_panics(
1287            input in "[a-z0-9_. ()!&|<>=\"'.-]{0,80}"
1288        ) {
1289            let _ = parse(&input);
1290        }
1291
1292        /// Any input at or over the cap length is handled without panic, and a
1293        /// too-long one is always rejected.
1294        #[test]
1295        fn cap_always_holds(input in "a{500,700}") {
1296            let result = parse(&input);
1297            if input.chars().count() > MAX_EXPRESSION_LEN {
1298                prop_assert!(result.is_err());
1299            }
1300        }
1301
1302        /// Evaluation never panics for any parsed expression against any JSON
1303        /// value. The expression is drawn from a grammar-shaped generator so
1304        /// real ASTs (not just trivial ones) are exercised.
1305        #[test]
1306        fn eval_never_panics(expr in expr_strategy(), value in json_strategy()) {
1307            if let Ok(parsed) = parse(&expr) {
1308                let _ = parsed.eval(&value);
1309            }
1310        }
1311    }
1312
1313    /// A strategy that builds plausible expression strings from the real
1314    /// vocabulary, so parses often succeed and eval is genuinely exercised.
1315    fn expr_strategy() -> impl Strategy<Value = String> {
1316        let leaf = prop_oneof![
1317            Just("score".to_string()),
1318            Just("output.score".to_string()),
1319            Just("items.0.score".to_string()),
1320            Just("flag".to_string()),
1321            Just("0.8".to_string()),
1322            Just("5".to_string()),
1323            Just("-1".to_string()),
1324            Just("true".to_string()),
1325            Just("null".to_string()),
1326            Just("\"hi\"".to_string()),
1327        ];
1328        let comparison = (leaf.clone(), "==|!=|<|<=|>|>=", leaf.clone())
1329            .prop_map(|(l, op, r)| format!("{l} {op} {r}"));
1330        let atom = prop_oneof![leaf, comparison];
1331        atom.prop_recursive(4, 32, 4, |inner| {
1332            prop_oneof![
1333                inner.clone().prop_map(|e| format!("!{e}")),
1334                inner.clone().prop_map(|e| format!("({e})")),
1335                (inner.clone(), inner.clone()).prop_map(|(a, b)| format!("{a} && {b}")),
1336                (inner.clone(), inner).prop_map(|(a, b)| format!("{a} || {b}")),
1337            ]
1338        })
1339    }
1340
1341    /// A strategy for arbitrary JSON values of bounded depth.
1342    fn json_strategy() -> impl Strategy<Value = Value> {
1343        let leaf = prop_oneof![
1344            Just(Value::Null),
1345            any::<bool>().prop_map(Value::Bool),
1346            any::<i64>().prop_map(|n| json!(n)),
1347            any::<f64>()
1348                .prop_filter("finite", |f| f.is_finite())
1349                .prop_map(|f| json!(f)),
1350            ".*".prop_map(Value::String),
1351        ];
1352        leaf.prop_recursive(3, 16, 4, |inner| {
1353            prop_oneof![
1354                prop::collection::vec(inner.clone(), 0..4).prop_map(Value::Array),
1355                prop::collection::hash_map("[a-z]{1,5}", inner, 0..4)
1356                    .prop_map(|m| Value::Object(m.into_iter().collect())),
1357            ]
1358        })
1359    }
1360}