Skip to main content

polydat_grammar/comprehension/
predicate.rs

1// Copyright 2024-2026 Jonathan Shook
2// SPDX-License-Identifier: Apache-2.0
3
4//! A comprehension's `where` predicate, parsed to a tree.
5//!
6//! A predicate is a Polydat boolean expression over the tuple's
7//! elements, each written `{name}` (comprehension_forms.md §10.9.1). It
8//! parses with the language's one precedence table,
9//! [`crate::parser::binding_power`]: unary `!` and `-` bind tighter
10//! than every binary operator, arithmetic binds tighter than
11//! comparison, comparison tighter than `&&`, and `&&` tighter than
12//! `||`, so `!{done} || {retry}` is `(!{done}) || {retry}`. Membership,
13//! `{name} in [v1, v2, …]`, binds as a relational comparison. A string
14//! is quoted, as everywhere in the language (`{region} == "us-east"`);
15//! a bare word is a name, which a predicate has none of beside its
16//! `{name}` elements.
17//!
18//! The tree holds the structure the runtime evaluates directly and the
19//! predicate analyzer factorizes: `||`, `&&`, `!`, the six comparisons,
20//! and membership, over element references and literals. Arithmetic
21//! (`+ - * / % **`) is an [`PredicateKind::Arith`] node that keeps its
22//! operands, and any other expression (a function call, a cast, a
23//! bitwise operator) is an [`PredicateKind::Expr`] leaf; the runtime
24//! evaluates both as a Polydat expression from their text, and the
25//! optimizer reads an `Arith` node's operands to decide whether it can
26//! fail. Every node carries the byte range of its text in the
27//! predicate, enclosing parentheses included.
28
29use std::collections::BTreeSet;
30use std::ops::Range;
31
32use crate::ast::BinOpKind;
33use crate::parser::binding_power;
34
35/// One node of a parsed predicate and the byte range of its text.
36#[derive(Debug, Clone, PartialEq)]
37pub struct Predicate {
38    /// What the node is.
39    pub kind: PredicateKind,
40    /// Its text's byte range in the predicate, enclosing parentheses
41    /// included.
42    pub span: Range<usize>,
43}
44
45/// The kinds of predicate node.
46#[derive(Debug, Clone, PartialEq)]
47pub enum PredicateKind {
48    /// `p || q || …`: true when any operand is.
49    Or(Vec<Predicate>),
50    /// `p && q && …`: true when every operand is.
51    And(Vec<Predicate>),
52    /// `!p`: true when the operand is not.
53    Not(Box<Predicate>),
54    /// `a OP b` for one of the six comparisons.
55    Compare(Comparison, Box<Predicate>, Box<Predicate>),
56    /// `a in [v1, v2, …]`: true when `a` equals any item.
57    In(Box<Predicate>, Vec<Predicate>),
58    /// `{name}`: the value the tuple, or the scope it was drawn in,
59    /// binds to `name`.
60    Element(String),
61    /// A literal value.
62    Literal(PredicateLiteral),
63    /// `a OP b` for one of the arithmetic operators `+ - * / % **`,
64    /// evaluated as a Polydat expression from its text.
65    Arith(BinOpKind, Box<Predicate>, Box<Predicate>),
66    /// Any other Polydat expression, evaluated from its text.
67    Expr,
68}
69
70/// A comparison operator.
71#[derive(Debug, Clone, Copy, PartialEq, Eq)]
72pub enum Comparison {
73    /// `==`
74    Eq,
75    /// `!=`
76    Ne,
77    /// `<`
78    Lt,
79    /// `<=`
80    Le,
81    /// `>`
82    Gt,
83    /// `>=`
84    Ge,
85}
86
87impl Comparison {
88    /// The operator's spelling.
89    pub fn as_str(self) -> &'static str {
90        match self {
91            Comparison::Eq => "==",
92            Comparison::Ne => "!=",
93            Comparison::Lt => "<",
94            Comparison::Le => "<=",
95            Comparison::Gt => ">",
96            Comparison::Ge => ">=",
97        }
98    }
99
100    /// The comparison with its operands swapped: `a < b` is `b > a`.
101    pub fn swapped(self) -> Self {
102        match self {
103            Comparison::Lt => Comparison::Gt,
104            Comparison::Le => Comparison::Ge,
105            Comparison::Gt => Comparison::Lt,
106            Comparison::Ge => Comparison::Le,
107            other => other,
108        }
109    }
110}
111
112/// A literal in a predicate.
113#[derive(Debug, Clone, PartialEq)]
114pub enum PredicateLiteral {
115    /// An integer, negative when written with a leading `-`.
116    Int(i128),
117    /// A float.
118    Float(f64),
119    /// A quoted string, `"load"` or `'load'`.
120    Str(String),
121    /// `true` or `false`.
122    Bool(bool),
123}
124
125impl Predicate {
126    /// This node's text in `source`, the predicate it was parsed from.
127    pub fn text<'a>(&self, source: &'a str) -> &'a str {
128        &source[self.span.clone()]
129    }
130}
131
132/// Parse a predicate. An error names what the predicate grammar does
133/// not accept; the caller may still evaluate the whole text as a
134/// Polydat expression.
135pub fn parse_predicate(text: &str) -> Result<Predicate, String> {
136    let tokens = tokenize(text)?;
137    let mut parser = Parser { tokens, pos: 0 };
138    let predicate = parser.expression(0)?;
139    match parser.tokens.get(parser.pos) {
140        None => Ok(predicate),
141        Some(token) => Err(format!(
142            "unexpected `{}` in predicate `{text}`",
143            &text[token.span.clone()]
144        )),
145    }
146}
147
148/// The names a predicate reads (comprehension_forms.md §5 V3).
149#[derive(Debug, Clone, Default, PartialEq, Eq)]
150pub struct PredicateReads {
151    /// Every `{name}` element: a name the tuple binds, or one the scope
152    /// the predicate is evaluated in supplies.
153    pub elements: BTreeSet<String>,
154    /// Every bare word the predicate reads as a name. A predicate reads
155    /// a scope only through `{name}`, so no scope supplies one.
156    pub bare: BTreeSet<String>,
157}
158
159/// The names `text` reads, as the runtime evaluates it: the elements and
160/// literals of its tree directly, and each opaque expression as the
161/// language's expression it is once its elements are interpolated, so a
162/// callee, a cast's type, and `true` and `false` are not names. A
163/// predicate that does not parse is read whole as one expression.
164pub fn predicate_reads(text: &str) -> PredicateReads {
165    let mut reads = PredicateReads::default();
166    match parse_predicate(text) {
167        Ok(tree) => collect_reads(&tree, text, &mut reads),
168        Err(_) => expression_reads(text, &mut reads),
169    }
170    reads
171}
172
173fn collect_reads(node: &Predicate, text: &str, reads: &mut PredicateReads) {
174    match &node.kind {
175        PredicateKind::Or(parts) | PredicateKind::And(parts) => {
176            for part in parts {
177                collect_reads(part, text, reads);
178            }
179        }
180        PredicateKind::Not(inner) => collect_reads(inner, text, reads),
181        PredicateKind::Compare(_, a, b) | PredicateKind::Arith(_, a, b) => {
182            collect_reads(a, text, reads);
183            collect_reads(b, text, reads);
184        }
185        PredicateKind::In(needle, items) => {
186            collect_reads(needle, text, reads);
187            for item in items {
188                collect_reads(item, text, reads);
189            }
190        }
191        PredicateKind::Element(name) => {
192            reads.elements.insert(name.clone());
193        }
194        PredicateKind::Literal(_) => {}
195        PredicateKind::Expr => expression_reads(node.text(text), reads),
196    }
197}
198
199/// An opaque expression's reads. The runtime interpolates every
200/// `{name}` in its text, inside a quoted string too, before it
201/// evaluates the rest as an expression; each of those is an element,
202/// and each name the rest references is a bare word.
203fn expression_reads(text: &str, reads: &mut PredicateReads) {
204    let mut interpolated = BTreeSet::new();
205    crate::refs::collect_string_interpolation_refs(text, &mut interpolated);
206    let mut plain = String::with_capacity(text.len());
207    let mut at = 0;
208    if let Ok(tokens) = tokenize(text) {
209        for token in tokens {
210            if let Tok::Element(_) = token.tok {
211                plain.push_str(&text[at..token.span.start]);
212                plain.push('0');
213                at = token.span.end;
214            }
215        }
216    }
217    plain.push_str(&text[at..]);
218    reads.bare.extend(
219        crate::refs::referenced_names(&plain)
220            .into_iter()
221            .filter(|name| !interpolated.contains(name)),
222    );
223    reads.elements.extend(interpolated);
224}
225
226#[derive(Debug, Clone)]
227enum Tok {
228    Element(String),
229    Ident(String),
230    Int(i128),
231    Float(f64),
232    Str(String),
233    Op(BinOpKind),
234    Bang,
235    LParen,
236    RParen,
237    LBracket,
238    RBracket,
239    Comma,
240    Other,
241}
242
243#[derive(Debug, Clone)]
244struct Token {
245    tok: Tok,
246    span: Range<usize>,
247}
248
249fn is_ident_start(c: char) -> bool {
250    c.is_ascii_alphabetic() || c == '_'
251}
252
253fn is_ident_char(c: char) -> bool {
254    c.is_ascii_alphanumeric() || c == '_'
255}
256
257fn tokenize(text: &str) -> Result<Vec<Token>, String> {
258    const OPERATORS: [(&str, Option<BinOpKind>); 22] = [
259        ("||", Some(BinOpKind::Or)),
260        ("&&", Some(BinOpKind::And)),
261        ("==", Some(BinOpKind::Eq)),
262        ("!=", Some(BinOpKind::Ne)),
263        ("<=", Some(BinOpKind::Le)),
264        (">=", Some(BinOpKind::Ge)),
265        ("<<", Some(BinOpKind::Shl)),
266        (">>", Some(BinOpKind::Shr)),
267        ("**", Some(BinOpKind::Pow)),
268        ("|", Some(BinOpKind::BitOr)),
269        ("&", Some(BinOpKind::BitAnd)),
270        ("^", Some(BinOpKind::BitXor)),
271        ("<", Some(BinOpKind::Lt)),
272        (">", Some(BinOpKind::Gt)),
273        ("+", Some(BinOpKind::Add)),
274        ("-", Some(BinOpKind::Sub)),
275        ("*", Some(BinOpKind::Mul)),
276        ("/", Some(BinOpKind::Div)),
277        ("%", Some(BinOpKind::Mod)),
278        ("!", None),
279        ("(", None),
280        (")", None),
281    ];
282    let mut tokens = Vec::new();
283    let bytes = text.as_bytes();
284    let mut i = 0;
285    'scan: while i < text.len() {
286        let c = text[i..].chars().next().expect("in bounds");
287        let start = i;
288        if c.is_whitespace() {
289            i += c.len_utf8();
290            continue;
291        }
292        if c == '{' {
293            let rest = &text[i + 1..];
294            let name_len = rest
295                .find(|ch: char| !is_ident_char(ch))
296                .unwrap_or(rest.len());
297            if name_len > 0 && rest[name_len..].starts_with('}') {
298                tokens.push(Token {
299                    tok: Tok::Element(rest[..name_len].to_string()),
300                    span: start..i + name_len + 2,
301                });
302                i += name_len + 2;
303                continue;
304            }
305        }
306        if c == '"' || c == '\'' {
307            let mut value = String::new();
308            let mut j = i + 1;
309            loop {
310                let Some(ch) = text[j..].chars().next() else {
311                    return Err(format!("unterminated string in predicate `{text}`"));
312                };
313                j += ch.len_utf8();
314                if ch == c {
315                    break;
316                }
317                if ch == '\\'
318                    && let Some(escaped) = text[j..].chars().next()
319                {
320                    value.push(escaped);
321                    j += escaped.len_utf8();
322                    continue;
323                }
324                value.push(ch);
325            }
326            tokens.push(Token {
327                tok: Tok::Str(value),
328                span: start..j,
329            });
330            i = j;
331            continue;
332        }
333        if c.is_ascii_digit() {
334            let mut j = i;
335            while j < text.len() && bytes[j].is_ascii_digit() {
336                j += 1;
337            }
338            let mut float = false;
339            if j + 1 < text.len() && bytes[j] == b'.' && bytes[j + 1].is_ascii_digit() {
340                float = true;
341                j += 1;
342                while j < text.len() && bytes[j].is_ascii_digit() {
343                    j += 1;
344                }
345            }
346            if j < text.len() && (bytes[j] == b'e' || bytes[j] == b'E') {
347                let mut k = j + 1;
348                if k < text.len() && (bytes[k] == b'+' || bytes[k] == b'-') {
349                    k += 1;
350                }
351                if k < text.len() && bytes[k].is_ascii_digit() {
352                    float = true;
353                    j = k;
354                    while j < text.len() && bytes[j].is_ascii_digit() {
355                        j += 1;
356                    }
357                }
358            }
359            let digits = &text[i..j];
360            let tok = if float {
361                Tok::Float(
362                    digits
363                        .parse()
364                        .map_err(|e| format!("bad number `{digits}`: {e}"))?,
365                )
366            } else {
367                Tok::Int(
368                    digits
369                        .parse()
370                        .map_err(|e| format!("bad number `{digits}`: {e}"))?,
371                )
372            };
373            tokens.push(Token { tok, span: i..j });
374            i = j;
375            continue;
376        }
377        if is_ident_start(c) {
378            let rest = &text[i..];
379            let len = rest
380                .find(|ch: char| !is_ident_char(ch))
381                .unwrap_or(rest.len());
382            tokens.push(Token {
383                tok: Tok::Ident(rest[..len].to_string()),
384                span: i..i + len,
385            });
386            i += len;
387            continue;
388        }
389        for (spelling, op) in OPERATORS {
390            if text[i..].starts_with(spelling) {
391                let tok = match (spelling, op) {
392                    (_, Some(op)) => Tok::Op(op),
393                    ("!", None) => Tok::Bang,
394                    ("(", None) => Tok::LParen,
395                    _ => Tok::RParen,
396                };
397                tokens.push(Token {
398                    tok,
399                    span: i..i + spelling.len(),
400                });
401                i += spelling.len();
402                continue 'scan;
403            }
404        }
405        let tok = match c {
406            '[' => Tok::LBracket,
407            ']' => Tok::RBracket,
408            ',' => Tok::Comma,
409            _ => Tok::Other,
410        };
411        tokens.push(Token {
412            tok,
413            span: i..i + c.len_utf8(),
414        });
415        i += c.len_utf8();
416    }
417    Ok(tokens)
418}
419
420struct Parser {
421    tokens: Vec<Token>,
422    pos: usize,
423}
424
425/// An infix operator of the predicate grammar.
426#[derive(Clone, Copy)]
427enum Infix {
428    Binary(BinOpKind),
429    In,
430}
431
432impl Parser {
433    fn peek(&self) -> Option<&Tok> {
434        self.tokens.get(self.pos).map(|t| &t.tok)
435    }
436
437    fn peek_at(&self, offset: usize) -> Option<&Tok> {
438        self.tokens.get(self.pos + offset).map(|t| &t.tok)
439    }
440
441    fn next(&mut self) -> Result<Token, String> {
442        let token = self
443            .tokens
444            .get(self.pos)
445            .cloned()
446            .ok_or_else(|| "the predicate ends early".to_string())?;
447        self.pos += 1;
448        Ok(token)
449    }
450
451    /// A Pratt parse at `min_bp`, with the binding powers of
452    /// [`binding_power`].
453    fn expression(&mut self, min_bp: u8) -> Result<Predicate, String> {
454        let mut lhs = self.prefixed()?;
455        lhs = self.postfix_as(lhs)?;
456        loop {
457            let infix = match self.peek() {
458                Some(Tok::Op(op)) => Infix::Binary(*op),
459                Some(Tok::Ident(word)) if word == "in" => Infix::In,
460                _ => break,
461            };
462            let (l_bp, r_bp) = match infix {
463                Infix::Binary(op) => binding_power(op),
464                Infix::In => binding_power(BinOpKind::Lt),
465            };
466            if l_bp < min_bp {
467                break;
468            }
469            self.pos += 1;
470            lhs = match infix {
471                Infix::In => {
472                    let (items, end) = self.list()?;
473                    Predicate {
474                        span: lhs.span.start..end,
475                        kind: PredicateKind::In(Box::new(lhs), items),
476                    }
477                }
478                Infix::Binary(op) => {
479                    let rhs = self.expression(r_bp)?;
480                    combine(op, lhs, rhs)
481                }
482            };
483        }
484        Ok(lhs)
485    }
486
487    /// An atom behind any number of unary operators.
488    fn prefixed(&mut self) -> Result<Predicate, String> {
489        match self.peek() {
490            Some(Tok::Bang) => {
491                let start = self.next()?.span.start;
492                let operand = self.prefixed()?;
493                Ok(Predicate {
494                    span: start..operand.span.end,
495                    kind: PredicateKind::Not(Box::new(operand)),
496                })
497            }
498            Some(Tok::Op(BinOpKind::Sub)) => {
499                let start = self.next()?.span.start;
500                let operand = self.prefixed()?;
501                let span = start..operand.span.end;
502                let kind = match operand.kind {
503                    PredicateKind::Literal(PredicateLiteral::Int(n)) => {
504                        PredicateKind::Literal(PredicateLiteral::Int(-n))
505                    }
506                    PredicateKind::Literal(PredicateLiteral::Float(f)) => {
507                        PredicateKind::Literal(PredicateLiteral::Float(-f))
508                    }
509                    _ => PredicateKind::Expr,
510                };
511                Ok(Predicate { kind, span })
512            }
513            _ => self.atom(),
514        }
515    }
516
517    /// `<expr> as <type>` casts, each an expression leaf.
518    fn postfix_as(&mut self, mut operand: Predicate) -> Result<Predicate, String> {
519        while matches!(self.peek(), Some(Tok::Ident(w)) if w == "as")
520            && matches!(self.peek_at(1), Some(Tok::Ident(_)))
521        {
522            self.pos += 1;
523            let end = self.next()?.span.end;
524            operand = Predicate {
525                span: operand.span.start..end,
526                kind: PredicateKind::Expr,
527            };
528        }
529        Ok(operand)
530    }
531
532    fn atom(&mut self) -> Result<Predicate, String> {
533        let token = self.next()?;
534        let span = token.span.clone();
535        let kind = match token.tok {
536            Tok::LParen => {
537                let inner = self.expression(0)?;
538                let close = self.next()?;
539                if !matches!(close.tok, Tok::RParen) {
540                    return Err("expected `)` in predicate".to_string());
541                }
542                return Ok(Predicate {
543                    kind: inner.kind,
544                    span: span.start..close.span.end,
545                });
546            }
547            Tok::Element(name) => PredicateKind::Element(name),
548            Tok::Int(n) => PredicateKind::Literal(PredicateLiteral::Int(n)),
549            Tok::Float(f) => PredicateKind::Literal(PredicateLiteral::Float(f)),
550            Tok::Str(s) => PredicateKind::Literal(PredicateLiteral::Str(s)),
551            Tok::Ident(word) if matches!(self.peek(), Some(Tok::LParen)) => {
552                // A call: its arguments are the callee's, evaluated
553                // with the call.
554                let _ = word;
555                let end = self.skip_group()?;
556                return Ok(Predicate {
557                    kind: PredicateKind::Expr,
558                    span: span.start..end,
559                });
560            }
561            Tok::Ident(word) => match word.as_str() {
562                "true" => PredicateKind::Literal(PredicateLiteral::Bool(true)),
563                "false" => PredicateKind::Literal(PredicateLiteral::Bool(false)),
564                // A bare word is a name, evaluated as the language
565                // evaluates one; it is never a string.
566                _ => PredicateKind::Expr,
567            },
568            _ => return Err("expected a value in predicate".to_string()),
569        };
570        Ok(Predicate { kind, span })
571    }
572
573    /// Skip a parenthesized group, returning the end of its `)`.
574    fn skip_group(&mut self) -> Result<usize, String> {
575        let mut depth = 0usize;
576        loop {
577            let token = self.next()?;
578            match token.tok {
579                Tok::LParen => depth += 1,
580                Tok::RParen => {
581                    depth -= 1;
582                    if depth == 0 {
583                        return Ok(token.span.end);
584                    }
585                }
586                _ => {}
587            }
588        }
589    }
590
591    /// `[item, item, …]` after `in`, and the end of its `]`.
592    fn list(&mut self) -> Result<(Vec<Predicate>, usize), String> {
593        if !matches!(self.next()?.tok, Tok::LBracket) {
594            return Err("expected `[` after `in`".to_string());
595        }
596        let mut items = Vec::new();
597        if matches!(self.peek(), Some(Tok::RBracket)) {
598            return Ok((items, self.next()?.span.end));
599        }
600        loop {
601            items.push(self.expression(0)?);
602            let token = self.next()?;
603            match token.tok {
604                Tok::Comma => {}
605                Tok::RBracket => return Ok((items, token.span.end)),
606                _ => return Err("expected `,` or `]` in an `in` list".to_string()),
607            }
608        }
609    }
610}
611
612/// `lhs op rhs` as a node: `||` and `&&` gather their chains,
613/// comparisons compare, arithmetic keeps its operands, and any other
614/// operator is an expression leaf.
615fn combine(op: BinOpKind, lhs: Predicate, rhs: Predicate) -> Predicate {
616    let span = lhs.span.start..rhs.span.end;
617    let comparison = match op {
618        BinOpKind::Eq => Comparison::Eq,
619        BinOpKind::Ne => Comparison::Ne,
620        BinOpKind::Lt => Comparison::Lt,
621        BinOpKind::Le => Comparison::Le,
622        BinOpKind::Gt => Comparison::Gt,
623        BinOpKind::Ge => Comparison::Ge,
624        BinOpKind::Or => {
625            let parts = gather(lhs, rhs, |k| match k {
626                PredicateKind::Or(parts) => Some(parts),
627                _ => None,
628            });
629            return Predicate {
630                kind: PredicateKind::Or(parts),
631                span,
632            };
633        }
634        BinOpKind::And => {
635            let parts = gather(lhs, rhs, |k| match k {
636                PredicateKind::And(parts) => Some(parts),
637                _ => None,
638            });
639            return Predicate {
640                kind: PredicateKind::And(parts),
641                span,
642            };
643        }
644        BinOpKind::Add
645        | BinOpKind::Sub
646        | BinOpKind::Mul
647        | BinOpKind::Div
648        | BinOpKind::Mod
649        | BinOpKind::Pow => {
650            return Predicate {
651                kind: PredicateKind::Arith(op, Box::new(lhs), Box::new(rhs)),
652                span,
653            };
654        }
655        _ => {
656            return Predicate {
657                kind: PredicateKind::Expr,
658                span,
659            };
660        }
661    };
662    Predicate {
663        kind: PredicateKind::Compare(comparison, Box::new(lhs), Box::new(rhs)),
664        span,
665    }
666}
667
668/// The operands of a chain: `lhs`'s own operands when it is the same
669/// chain written without parentheses, then `rhs`.
670fn gather(
671    lhs: Predicate,
672    rhs: Predicate,
673    parts_of: impl Fn(PredicateKind) -> Option<Vec<Predicate>>,
674) -> Vec<Predicate> {
675    let span = lhs.span.clone();
676    let mut parts = match parts_of(lhs.kind.clone()) {
677        Some(parts) if parts.first().map(|p| p.span.start) == Some(span.start) => parts,
678        _ => vec![lhs],
679    };
680    parts.push(rhs);
681    parts
682}
683
684#[cfg(test)]
685mod tests {
686    use super::*;
687
688    /// The predicate with its structure written out: every node that is
689    /// not a leaf parenthesized.
690    fn shape(text: &str) -> String {
691        fn walk(p: &Predicate, text: &str) -> String {
692            match &p.kind {
693                PredicateKind::Or(parts) => format!(
694                    "({})",
695                    parts
696                        .iter()
697                        .map(|q| walk(q, text))
698                        .collect::<Vec<_>>()
699                        .join(" || ")
700                ),
701                PredicateKind::And(parts) => format!(
702                    "({})",
703                    parts
704                        .iter()
705                        .map(|q| walk(q, text))
706                        .collect::<Vec<_>>()
707                        .join(" && ")
708                ),
709                PredicateKind::Not(inner) => format!("(!{})", walk(inner, text)),
710                PredicateKind::Compare(c, a, b) => {
711                    format!("({} {} {})", walk(a, text), c.as_str(), walk(b, text))
712                }
713                PredicateKind::In(a, items) => format!(
714                    "({} in [{}])",
715                    walk(a, text),
716                    items
717                        .iter()
718                        .map(|q| walk(q, text))
719                        .collect::<Vec<_>>()
720                        .join(", ")
721                ),
722                PredicateKind::Element(name) => format!("{{{name}}}"),
723                PredicateKind::Literal(PredicateLiteral::Int(n)) => n.to_string(),
724                PredicateKind::Literal(PredicateLiteral::Float(f)) => f.to_string(),
725                PredicateKind::Literal(PredicateLiteral::Str(s)) => format!("'{s}'"),
726                PredicateKind::Literal(PredicateLiteral::Bool(b)) => b.to_string(),
727                PredicateKind::Arith(..) | PredicateKind::Expr => format!("<{}>", p.text(text)),
728            }
729        }
730        walk(&parse_predicate(text).unwrap(), text)
731    }
732
733    /// Unary `!` binds tighter than `&&`, which binds tighter than
734    /// `||`.
735    #[test]
736    fn not_binds_tighter_than_and_and_or() {
737        assert_eq!(shape("!{done} || {retry}"), "((!{done}) || {retry})");
738        assert_eq!(shape("!{a} && {b}"), "((!{a}) && {b})");
739        assert_eq!(shape("{a} || !{b}"), "({a} || (!{b}))");
740        assert_eq!(shape("!({a} || {b})"), "(!({a} || {b}))");
741        assert_eq!(shape("!!{a}"), "(!(!{a}))");
742    }
743
744    /// Every pair of adjacent precedence levels, in both orders.
745    #[test]
746    fn every_operator_pair_binds_by_the_one_table() {
747        let cases = [
748            ("{a} || {b} && {c}", "({a} || ({b} && {c}))"),
749            ("{a} && {b} || {c}", "(({a} && {b}) || {c})"),
750            ("{a} && {b} && {c}", "({a} && {b} && {c})"),
751            ("{a} || {b} || {c}", "({a} || {b} || {c})"),
752            ("({a} || {b}) && {c}", "(({a} || {b}) && {c})"),
753            ("{a} == 1 && {b} != 2", "(({a} == 1) && ({b} != 2))"),
754            ("{a} < 1 || {b} >= 2", "(({a} < 1) || ({b} >= 2))"),
755            ("{a} < {b} == {c}", "(({a} < {b}) == {c})"),
756            ("{a} == {b} < {c}", "({a} == ({b} < {c}))"),
757            ("{a} <= 1 && {a} > -1", "(({a} <= 1) && ({a} > -1))"),
758            ("!{a} == {b}", "((!{a}) == {b})"),
759            ("!{a} < 3", "((!{a}) < 3)"),
760            ("{a} + 1 > {b} * 2", "(<{a} + 1> > <{b} * 2>)"),
761            ("{a} + {b} * 2 == 7", "(<{a} + {b} * 2> == 7)"),
762            (
763                "{a} % 2 == 0 || {b} ** 2 > 3",
764                "((<{a} % 2> == 0) || (<{b} ** 2> > 3))",
765            ),
766            ("{a} & 1 == 1", "(<{a} & 1> == 1)"),
767            ("{a} | 1 < 3", "(<{a} | 1> < 3)"),
768            ("{a} << 1 >= 4 && true", "((<{a} << 1> >= 4) && true)"),
769            (
770                "{a} in [1, 2] && {b} == x",
771                "(({a} in [1, 2]) && ({b} == <x>))",
772            ),
773            ("{a} in [1] || !{b}", "(({a} in [1]) || (!{b}))"),
774            ("u64_add({a}, {b}) > 7", "(<u64_add({a}, {b})> > 7)"),
775            ("!is_even({a}) && {b}", "((!<is_even({a})>) && {b})"),
776            ("{a} as f64 > 1.5", "(<{a} as f64> > 1.5)"),
777            ("-{a} < 0", "(<-{a}> < 0)"),
778            ("\"s0\" != 2", "('s0' != 2)"),
779            ("{b} == 'us-east'", "({b} == 'us-east')"),
780            // A bare word is a name, never a string.
781            ("{b} == us-east", "({b} == <us-east>)"),
782            ("{b} in [s0, \"s1\"]", "({b} in [<s0>, 's1'])"),
783        ];
784        for (text, expected) in cases {
785            assert_eq!(shape(text), expected, "{text}");
786        }
787    }
788
789    /// A node's span is its own text, parentheses included, so the
790    /// analyzer can hand an operand on as written.
791    #[test]
792    fn spans_cover_each_operand_as_written() {
793        let text = "({a} > 1 || {b} < 2) && !{c}";
794        let p = parse_predicate(text).unwrap();
795        let PredicateKind::And(parts) = &p.kind else {
796            panic!("{p:?}")
797        };
798        assert_eq!(parts[0].text(text), "({a} > 1 || {b} < 2)");
799        assert_eq!(parts[1].text(text), "!{c}");
800        assert_eq!(p.text(text), text);
801    }
802
803    /// Arithmetic keeps its operator and operands, grouped by the one
804    /// table; a bitwise operator, a call, and a cast stay opaque.
805    #[test]
806    fn arithmetic_keeps_its_operands() {
807        let text = "{a} + {b} * 2 > 3";
808        let p = parse_predicate(text).unwrap();
809        let PredicateKind::Compare(Comparison::Gt, lhs, _) = &p.kind else {
810            panic!("{p:?}")
811        };
812        let PredicateKind::Arith(BinOpKind::Add, a, product) = &lhs.kind else {
813            panic!("{lhs:?}")
814        };
815        assert_eq!(a.kind, PredicateKind::Element("a".into()));
816        assert!(matches!(
817            &product.kind,
818            PredicateKind::Arith(BinOpKind::Mul, b, two)
819                if b.kind == PredicateKind::Element("b".into())
820                    && two.kind == PredicateKind::Literal(PredicateLiteral::Int(2))
821        ));
822        for opaque in ["{a} & 1", "u64_add({a}, 1)", "{a} as f64"] {
823            assert_eq!(parse_predicate(opaque).unwrap().kind, PredicateKind::Expr);
824        }
825    }
826
827    #[test]
828    fn what_the_grammar_does_not_accept_is_an_error() {
829        for text in ["", "{a} >", "({a} > 1", "{a} in 3", "{a} . b", "\"open"] {
830            assert!(parse_predicate(text).is_err(), "{text}");
831        }
832    }
833}