Skip to main content

haz_query_lang/
expr.rs

1//! The boolean filter-expression syntax tree.
2
3use crate::span::Span;
4
5/// A parsed boolean filter expression, generic over its atom
6/// representation.
7///
8/// The parser produces an `Expr<RawAtom>`. Consumer crates lift
9/// atoms to a typed representation (e.g. `Expr<TagName>`,
10/// `Expr<PathPattern>`, `Expr<RelationalAtom>`) before evaluation.
11///
12/// Trees built by the parser are left-associative for binary
13/// operators and follow the precedence order of `QRY-002`:
14/// `!` tightest, then `&`, then `|`.
15#[derive(Debug, Clone, PartialEq, Eq)]
16pub enum Expr<A> {
17    /// A leaf atom.
18    Atom(A),
19    /// Negation of an inner expression.
20    Not(Box<Expr<A>>),
21    /// Logical conjunction of two sub-expressions.
22    And(Box<Expr<A>>, Box<Expr<A>>),
23    /// Logical disjunction of two sub-expressions.
24    Or(Box<Expr<A>>, Box<Expr<A>>),
25}
26
27/// The raw atom representation emitted by the parser: the
28/// verbatim text of the atom token together with the byte-offset
29/// span it occupied in the original input.
30///
31/// The parser does not interpret the text. Consumer crates apply
32/// per-attribute validation (identifier rules, path-pattern
33/// grammar, relational `kind:value` discrimination) before
34/// evaluation.
35#[derive(Debug, Clone, PartialEq, Eq, Hash)]
36pub struct RawAtom {
37    /// The atom token's verbatim UTF-8 byte content.
38    pub text: String,
39    /// The atom token's byte-offset span in the original input.
40    pub span: Span,
41}
42
43impl<A> Expr<A> {
44    /// Apply `f` to every atom in the tree in canonical
45    /// left-to-right order, returning a new expression whose
46    /// atoms are of type `B`. The traversal is fail-fast: the
47    /// first error returned by `f` short-circuits the rest.
48    ///
49    /// # Errors
50    ///
51    /// Returns the first `E` produced by `f`. Subsequent atoms
52    /// are not visited; consumers that want to accumulate every
53    /// atom-level error MUST layer their own collection on top.
54    pub fn try_map<B, E, F>(self, mut f: F) -> Result<Expr<B>, E>
55    where
56        F: FnMut(A) -> Result<B, E>,
57    {
58        self.try_map_inner(&mut f)
59    }
60
61    fn try_map_inner<B, E, F>(self, f: &mut F) -> Result<Expr<B>, E>
62    where
63        F: FnMut(A) -> Result<B, E>,
64    {
65        match self {
66            Expr::Atom(a) => Ok(Expr::Atom(f(a)?)),
67            Expr::Not(inner) => {
68                let inner = (*inner).try_map_inner(f)?;
69                Ok(Expr::Not(Box::new(inner)))
70            }
71            Expr::And(left, right) => {
72                let left = (*left).try_map_inner(f)?;
73                let right = (*right).try_map_inner(f)?;
74                Ok(Expr::And(Box::new(left), Box::new(right)))
75            }
76            Expr::Or(left, right) => {
77                let left = (*left).try_map_inner(f)?;
78                let right = (*right).try_map_inner(f)?;
79                Ok(Expr::Or(Box::new(left), Box::new(right)))
80            }
81        }
82    }
83
84    /// Evaluate the expression by applying `f` to each atom and
85    /// folding the results under the boolean operators of
86    /// `QRY-002`.
87    ///
88    /// Short-circuits in the standard way: `And` stops at the
89    /// first `false`, `Or` stops at the first `true`.
90    pub fn eval<F>(&self, mut f: F) -> bool
91    where
92        F: FnMut(&A) -> bool,
93    {
94        self.eval_inner(&mut f)
95    }
96
97    fn eval_inner<F>(&self, f: &mut F) -> bool
98    where
99        F: FnMut(&A) -> bool,
100    {
101        match self {
102            Expr::Atom(a) => f(a),
103            Expr::Not(inner) => !inner.eval_inner(f),
104            Expr::And(left, right) => left.eval_inner(f) && right.eval_inner(f),
105            Expr::Or(left, right) => left.eval_inner(f) || right.eval_inner(f),
106        }
107    }
108
109    /// Evaluate the expression by applying a fallible predicate
110    /// `f` to each atom.
111    ///
112    /// Short-circuits in two ways: an `Err` propagates
113    /// immediately, and `And` / `Or` stop as soon as the result
114    /// is determined. Atoms past the short-circuit point are not
115    /// visited and their predicates are not invoked.
116    ///
117    /// # Errors
118    ///
119    /// Returns the first `E` produced by `f`.
120    pub fn try_eval<F, E>(&self, mut f: F) -> Result<bool, E>
121    where
122        F: FnMut(&A) -> Result<bool, E>,
123    {
124        self.try_eval_inner(&mut f)
125    }
126
127    fn try_eval_inner<F, E>(&self, f: &mut F) -> Result<bool, E>
128    where
129        F: FnMut(&A) -> Result<bool, E>,
130    {
131        match self {
132            Expr::Atom(a) => f(a),
133            Expr::Not(inner) => Ok(!inner.try_eval_inner(f)?),
134            Expr::And(left, right) => Ok(left.try_eval_inner(f)? && right.try_eval_inner(f)?),
135            Expr::Or(left, right) => Ok(left.try_eval_inner(f)? || right.try_eval_inner(f)?),
136        }
137    }
138}
139
140#[cfg(test)]
141mod tests {
142    use super::*;
143
144    fn atom(text: &str, start: usize, end: usize) -> Expr<RawAtom> {
145        Expr::Atom(RawAtom {
146            text: text.to_owned(),
147            span: Span { start, end },
148        })
149    }
150
151    #[test]
152    fn expr_is_structurally_equal_to_a_clone() {
153        let original = Expr::And(
154            Box::new(atom("a", 0, 1)),
155            Box::new(Expr::Or(
156                Box::new(atom("b", 4, 5)),
157                Box::new(Expr::Not(Box::new(atom("c", 9, 10)))),
158            )),
159        );
160        let cloned = original.clone();
161        assert_eq!(original, cloned);
162    }
163
164    #[test]
165    fn raw_atom_preserves_byte_text_and_span() {
166        let atom = RawAtom {
167            text: "name:foo".to_owned(),
168            span: Span { start: 2, end: 10 },
169        };
170        assert_eq!(atom.text, "name:foo");
171        assert_eq!(atom.span, Span { start: 2, end: 10 });
172    }
173
174    #[test]
175    fn try_map_transforms_a_single_atom() {
176        let expr: Expr<&str> = Expr::Atom("42");
177        let mapped: Expr<i32> = expr.try_map(str::parse::<i32>).unwrap();
178        assert_eq!(mapped, Expr::Atom(42));
179    }
180
181    #[test]
182    fn try_map_preserves_tree_structure() {
183        let expr: Expr<&str> = Expr::And(
184            Box::new(Expr::Or(
185                Box::new(Expr::Atom("1")),
186                Box::new(Expr::Not(Box::new(Expr::Atom("2")))),
187            )),
188            Box::new(Expr::Atom("3")),
189        );
190        let mapped: Expr<i32> = expr.try_map(str::parse::<i32>).unwrap();
191        let expected: Expr<i32> = Expr::And(
192            Box::new(Expr::Or(
193                Box::new(Expr::Atom(1)),
194                Box::new(Expr::Not(Box::new(Expr::Atom(2)))),
195            )),
196            Box::new(Expr::Atom(3)),
197        );
198        assert_eq!(mapped, expected);
199    }
200
201    #[test]
202    fn try_map_propagates_first_error_and_stops() {
203        let mut visited = Vec::new();
204        let expr: Expr<&str> = Expr::And(
205            Box::new(Expr::Atom("1")),
206            Box::new(Expr::Or(
207                Box::new(Expr::Atom("oops")),
208                Box::new(Expr::Atom("3")),
209            )),
210        );
211        let result: Result<Expr<i32>, _> = expr.try_map(|s| {
212            visited.push(s.to_owned());
213            s.parse::<i32>()
214        });
215        assert!(result.is_err());
216        // The third atom ("3") MUST NOT be visited because "oops"
217        // short-circuits the traversal.
218        assert_eq!(visited, vec!["1", "oops"]);
219    }
220
221    #[test]
222    fn try_map_visits_atoms_in_left_to_right_order() {
223        let mut visited = Vec::new();
224        let expr: Expr<&str> = Expr::Or(
225            Box::new(Expr::And(
226                Box::new(Expr::Atom("a")),
227                Box::new(Expr::Atom("b")),
228            )),
229            Box::new(Expr::Not(Box::new(Expr::Atom("c")))),
230        );
231        let _: Expr<usize> = expr
232            .try_map(|s: &str| {
233                visited.push(s.to_owned());
234                Ok::<usize, ()>(s.len())
235            })
236            .unwrap();
237        assert_eq!(visited, vec!["a", "b", "c"]);
238    }
239
240    // --- Expr::eval ------------------------------------------
241
242    #[test]
243    fn eval_returns_predicate_result_for_single_atom() {
244        let expr = Expr::Atom("yes");
245        assert!(expr.eval(|a| *a == "yes"));
246        assert!(!expr.eval(|a| *a == "no"));
247    }
248
249    #[test]
250    fn eval_applies_negation() {
251        let expr = Expr::Not(Box::new(Expr::Atom(true)));
252        assert!(!expr.eval(|a| *a));
253    }
254
255    #[test]
256    fn eval_folds_and_or_with_precedence() {
257        // (a & !b) | c
258        let expr = Expr::Or(
259            Box::new(Expr::And(
260                Box::new(Expr::Atom("a")),
261                Box::new(Expr::Not(Box::new(Expr::Atom("b")))),
262            )),
263            Box::new(Expr::Atom("c")),
264        );
265        let truthy = |s: &&str| *s == "a" || *s == "c";
266        assert!(expr.eval(truthy));
267        let only_b = |s: &&str| *s == "b";
268        assert!(!expr.eval(only_b));
269    }
270
271    #[test]
272    fn eval_short_circuits_and_on_first_false() {
273        let mut visited: Vec<&str> = Vec::new();
274        let expr = Expr::And(
275            Box::new(Expr::Atom("first")),
276            Box::new(Expr::Atom("second")),
277        );
278        let _ = expr.eval(|a| {
279            visited.push(*a);
280            *a != "first"
281        });
282        assert_eq!(visited, vec!["first"]);
283    }
284
285    #[test]
286    fn eval_short_circuits_or_on_first_true() {
287        let mut visited: Vec<&str> = Vec::new();
288        let expr = Expr::Or(
289            Box::new(Expr::Atom("first")),
290            Box::new(Expr::Atom("second")),
291        );
292        let _ = expr.eval(|a| {
293            visited.push(*a);
294            true
295        });
296        assert_eq!(visited, vec!["first"]);
297    }
298
299    // --- Expr::try_eval --------------------------------------
300
301    #[test]
302    fn try_eval_returns_ok_for_infallible_predicate() {
303        let expr: Expr<i32> = Expr::And(Box::new(Expr::Atom(1)), Box::new(Expr::Atom(2)));
304        let result: Result<bool, ()> = expr.try_eval(|n| Ok(*n > 0));
305        assert_eq!(result, Ok(true));
306    }
307
308    #[test]
309    fn try_eval_propagates_error_from_first_failing_atom() {
310        let mut visited: Vec<i32> = Vec::new();
311        let expr: Expr<i32> = Expr::Or(Box::new(Expr::Atom(1)), Box::new(Expr::Atom(2)));
312        let result: Result<bool, &'static str> = expr.try_eval(|n| {
313            visited.push(*n);
314            if *n == 1 { Err("boom") } else { Ok(true) }
315        });
316        assert_eq!(result, Err("boom"));
317        // Right side MUST NOT be visited.
318        assert_eq!(visited, vec![1]);
319    }
320
321    #[test]
322    fn try_eval_short_circuits_and_on_false_without_visiting_right() {
323        let mut visited: Vec<i32> = Vec::new();
324        let expr: Expr<i32> = Expr::And(Box::new(Expr::Atom(1)), Box::new(Expr::Atom(2)));
325        let result: Result<bool, ()> = expr.try_eval(|n| {
326            visited.push(*n);
327            Ok(*n != 1)
328        });
329        assert_eq!(result, Ok(false));
330        assert_eq!(visited, vec![1]);
331    }
332}