rusty_promql_parser/parser/
binary.rs

1//! Binary expression parsing for PromQL.
2//!
3//! This module handles parsing of binary operators and their modifiers.
4//!
5//! # Binary Operators
6//!
7//! Operators listed from lowest to highest precedence:
8//!
9//! | Precedence | Operators                  | Description              |
10//! |------------|----------------------------|--------------------------|
11//! | 1          | `or`                       | Set union                |
12//! | 2          | `and`, `unless`            | Set intersection/diff    |
13//! | 3          | `==`, `!=`, `<`, `<=`, `>`, `>=` | Comparison         |
14//! | 4          | `+`, `-`                   | Addition/subtraction     |
15//! | 5          | `*`, `/`, `%`, `atan2`     | Multiplication/division  |
16//! | 6          | `^`                        | Power (right-associative)|
17//!
18//! # Vector Matching Modifiers
19//!
20//! Binary operations between vectors can use matching modifiers:
21//!
22//! - `on(label, ...)` - Match only on specified labels
23//! - `ignoring(label, ...)` - Match ignoring specified labels
24//! - `group_left(label, ...)` - Many-to-one matching
25//! - `group_right(label, ...)` - One-to-many matching
26//! - `bool` - Return 0/1 instead of filtering (for comparisons)
27//!
28//! # Examples
29//!
30//! ```rust
31//! use rusty_promql_parser::parser::binary::binary_op;
32//! use rusty_promql_parser::ast::BinaryOp;
33//!
34//! let (_, op) = binary_op("+").unwrap();
35//! assert_eq!(op, BinaryOp::Add);
36//!
37//! let (_, op) = binary_op("and").unwrap();
38//! assert_eq!(op, BinaryOp::And);
39//! ```
40
41use nom::{
42    IResult, Parser,
43    branch::alt,
44    bytes::complete::{tag, tag_no_case},
45    character::complete::{char, satisfy},
46    combinator::{map, not, opt, peek, value},
47    multi::separated_list0,
48    sequence::delimited,
49};
50
51use crate::ast::{
52    BinaryModifier, BinaryOp, GroupModifier, GroupSide, VectorMatching, VectorMatchingOp,
53};
54use crate::lexer::{identifier::label_name, whitespace::ws_opt};
55
56/// Parser that succeeds only at a word boundary (not followed by alphanumeric or underscore)
57fn word_boundary(input: &str) -> IResult<&str, ()> {
58    not(peek(satisfy(|c| c.is_alphanumeric() || c == '_'))).parse(input)
59}
60
61/// Parse a binary operator
62///
63/// Handles all binary operators including the keyword operators
64/// (and, or, unless, atan2).
65pub fn binary_op(input: &str) -> IResult<&str, BinaryOp> {
66    alt((
67        // Two-character operators must come before single-character
68        value(BinaryOp::Eq, tag("==")),
69        value(BinaryOp::Ne, tag("!=")),
70        value(BinaryOp::Le, tag("<=")),
71        value(BinaryOp::Ge, tag(">=")),
72        // Single-character operators
73        value(BinaryOp::Add, tag("+")),
74        value(BinaryOp::Sub, tag("-")),
75        value(BinaryOp::Mul, tag("*")),
76        value(BinaryOp::Div, tag("/")),
77        value(BinaryOp::Mod, tag("%")),
78        value(BinaryOp::Pow, tag("^")),
79        value(BinaryOp::Lt, tag("<")),
80        value(BinaryOp::Gt, tag(">")),
81        // Keyword operators (case-insensitive)
82        keyword_binary_op,
83    ))
84    .parse(input)
85}
86
87/// Parse keyword binary operators (case-insensitive)
88fn keyword_binary_op(input: &str) -> IResult<&str, BinaryOp> {
89    // We need to ensure these are complete words, not prefixes
90    (
91        alt((
92            value(BinaryOp::And, tag_no_case("and")),
93            value(BinaryOp::Or, tag_no_case("or")),
94            value(BinaryOp::Unless, tag_no_case("unless")),
95            value(BinaryOp::Atan2, tag_no_case("atan2")),
96        )),
97        word_boundary,
98    )
99        .map(|(op, _)| op)
100        .parse(input)
101}
102
103/// Parse the `bool` modifier
104fn bool_modifier(input: &str) -> IResult<&str, bool> {
105    (tag_no_case("bool"), word_boundary)
106        .map(|_| true)
107        .parse(input)
108}
109
110/// Parse the matching operation (on/ignoring)
111fn vector_matching_op(input: &str) -> IResult<&str, VectorMatchingOp> {
112    (
113        alt((
114            value(VectorMatchingOp::On, tag_no_case("on")),
115            value(VectorMatchingOp::Ignoring, tag_no_case("ignoring")),
116        )),
117        word_boundary,
118    )
119        .map(|(op, _)| op)
120        .parse(input)
121}
122
123/// Parse a label list in parentheses: `(label1, label2)`
124fn label_list(input: &str) -> IResult<&str, Vec<String>> {
125    delimited(
126        (char('('), ws_opt),
127        separated_list0(
128            delimited(ws_opt, char(','), ws_opt),
129            map(label_name, |s| s.to_string()),
130        ),
131        (ws_opt, char(')')),
132    )
133    .parse(input)
134}
135
136/// Parse the group modifier (group_left/group_right)
137fn group_modifier(input: &str) -> IResult<&str, GroupModifier> {
138    (
139        alt((
140            value(GroupSide::Left, tag_no_case("group_left")),
141            value(GroupSide::Right, tag_no_case("group_right")),
142        )),
143        word_boundary,
144        ws_opt,
145        opt(label_list),
146    )
147        .map(|(side, _, _, labels)| GroupModifier {
148            side,
149            labels: labels.unwrap_or_default(),
150        })
151        .parse(input)
152}
153
154/// Parse vector matching specification: `on(labels) group_left(labels)`
155fn vector_matching(input: &str) -> IResult<&str, VectorMatching> {
156    (
157        vector_matching_op,
158        ws_opt,
159        label_list,
160        ws_opt,
161        opt(group_modifier),
162    )
163        .map(|(op, _, labels, _, group)| VectorMatching { op, labels, group })
164        .parse(input)
165}
166
167/// Parse binary expression modifier: `bool on(labels) group_left(labels)`
168///
169/// This parses the optional modifiers that can appear between the operator
170/// and the right-hand side operand.
171pub(crate) fn binary_modifier(input: &str) -> IResult<&str, BinaryModifier> {
172    let (rest, (_, return_bool, _, matching)) =
173        (ws_opt, opt(bool_modifier), ws_opt, opt(vector_matching)).parse(input)?;
174
175    // If neither bool nor matching, fail
176    if return_bool.is_none() && matching.is_none() {
177        return Err(nom::Err::Error(nom::error::Error::new(
178            input,
179            nom::error::ErrorKind::Tag,
180        )));
181    }
182
183    Ok((
184        rest,
185        BinaryModifier {
186            return_bool: return_bool.unwrap_or(false),
187            matching,
188        },
189    ))
190}
191
192#[cfg(test)]
193mod tests {
194    use super::*;
195
196    // Binary operator tests
197    #[test]
198    fn test_binary_op_arithmetic() {
199        assert_eq!(binary_op("+").unwrap().1, BinaryOp::Add);
200        assert_eq!(binary_op("-").unwrap().1, BinaryOp::Sub);
201        assert_eq!(binary_op("*").unwrap().1, BinaryOp::Mul);
202        assert_eq!(binary_op("/").unwrap().1, BinaryOp::Div);
203        assert_eq!(binary_op("%").unwrap().1, BinaryOp::Mod);
204        assert_eq!(binary_op("^").unwrap().1, BinaryOp::Pow);
205    }
206
207    #[test]
208    fn test_binary_op_comparison() {
209        assert_eq!(binary_op("==").unwrap().1, BinaryOp::Eq);
210        assert_eq!(binary_op("!=").unwrap().1, BinaryOp::Ne);
211        assert_eq!(binary_op("<").unwrap().1, BinaryOp::Lt);
212        assert_eq!(binary_op("<=").unwrap().1, BinaryOp::Le);
213        assert_eq!(binary_op(">").unwrap().1, BinaryOp::Gt);
214        assert_eq!(binary_op(">=").unwrap().1, BinaryOp::Ge);
215    }
216
217    #[test]
218    fn test_binary_op_keywords() {
219        assert_eq!(binary_op("and").unwrap().1, BinaryOp::And);
220        assert_eq!(binary_op("AND").unwrap().1, BinaryOp::And);
221        assert_eq!(binary_op("or").unwrap().1, BinaryOp::Or);
222        assert_eq!(binary_op("OR").unwrap().1, BinaryOp::Or);
223        assert_eq!(binary_op("unless").unwrap().1, BinaryOp::Unless);
224        assert_eq!(binary_op("UNLESS").unwrap().1, BinaryOp::Unless);
225        assert_eq!(binary_op("atan2").unwrap().1, BinaryOp::Atan2);
226        assert_eq!(binary_op("ATAN2").unwrap().1, BinaryOp::Atan2);
227    }
228
229    #[test]
230    fn test_binary_op_word_boundary() {
231        // "andy" should not match "and"
232        assert!(binary_op("andy").is_err());
233        // "orange" should not match "or"
234        assert!(binary_op("orange").is_err());
235        // "atan2x" should not match "atan2"
236        assert!(binary_op("atan2x").is_err());
237    }
238
239    #[test]
240    fn test_binary_op_with_remaining() {
241        let (rest, op) = binary_op("+ foo").unwrap();
242        assert_eq!(op, BinaryOp::Add);
243        assert_eq!(rest, " foo");
244
245        let (rest, op) = binary_op("and bar").unwrap();
246        assert_eq!(op, BinaryOp::And);
247        assert_eq!(rest, " bar");
248    }
249
250    // Vector matching tests
251    #[test]
252    fn test_vector_matching_on() {
253        let (rest, vm) = vector_matching("on(job, instance)").unwrap();
254        assert!(rest.is_empty());
255        assert_eq!(vm.op, VectorMatchingOp::On);
256        assert_eq!(vm.labels, vec!["job", "instance"]);
257        assert!(vm.group.is_none());
258    }
259
260    #[test]
261    fn test_vector_matching_ignoring() {
262        let (rest, vm) = vector_matching("ignoring(instance)").unwrap();
263        assert!(rest.is_empty());
264        assert_eq!(vm.op, VectorMatchingOp::Ignoring);
265        assert_eq!(vm.labels, vec!["instance"]);
266    }
267
268    #[test]
269    fn test_vector_matching_empty() {
270        let (rest, vm) = vector_matching("on()").unwrap();
271        assert!(rest.is_empty());
272        assert_eq!(vm.op, VectorMatchingOp::On);
273        assert!(vm.labels.is_empty());
274    }
275
276    #[test]
277    fn test_vector_matching_with_group_left() {
278        let (rest, vm) = vector_matching("on(job) group_left").unwrap();
279        assert!(rest.is_empty());
280        assert_eq!(vm.op, VectorMatchingOp::On);
281        let group = vm.group.unwrap();
282        assert_eq!(group.side, GroupSide::Left);
283        assert!(group.labels.is_empty());
284    }
285
286    #[test]
287    fn test_vector_matching_with_group_right_labels() {
288        let (rest, vm) = vector_matching("ignoring(instance) group_right(job)").unwrap();
289        assert!(rest.is_empty());
290        assert_eq!(vm.op, VectorMatchingOp::Ignoring);
291        let group = vm.group.unwrap();
292        assert_eq!(group.side, GroupSide::Right);
293        assert_eq!(group.labels, vec!["job"]);
294    }
295
296    #[test]
297    fn test_vector_matching_case_insensitive() {
298        let (_, vm) = vector_matching("ON(job)").unwrap();
299        assert_eq!(vm.op, VectorMatchingOp::On);
300
301        let (_, vm) = vector_matching("IGNORING(job)").unwrap();
302        assert_eq!(vm.op, VectorMatchingOp::Ignoring);
303
304        let (_, vm) = vector_matching("on(job) GROUP_LEFT").unwrap();
305        assert!(vm.group.is_some());
306    }
307
308    // Binary modifier tests
309    #[test]
310    fn test_binary_modifier_bool_only() {
311        let (rest, m) = binary_modifier(" bool").unwrap();
312        assert!(rest.is_empty() || rest.chars().all(|c| c.is_whitespace()));
313        assert!(m.return_bool);
314        assert!(m.matching.is_none());
315    }
316
317    #[test]
318    fn test_binary_modifier_matching_only() {
319        let (rest, m) = binary_modifier(" on(job)").unwrap();
320        assert!(rest.is_empty());
321        assert!(!m.return_bool);
322        assert!(m.matching.is_some());
323    }
324
325    #[test]
326    fn test_binary_modifier_bool_and_matching() {
327        let (rest, m) = binary_modifier(" bool on(job)").unwrap();
328        assert!(rest.is_empty());
329        assert!(m.return_bool);
330        assert!(m.matching.is_some());
331    }
332
333    #[test]
334    fn test_binary_modifier_fails_on_empty() {
335        assert!(binary_modifier("foo").is_err());
336    }
337
338    // Display tests
339    #[test]
340    fn test_vector_matching_display() {
341        let vm = VectorMatching {
342            op: VectorMatchingOp::On,
343            labels: vec!["job".to_string()],
344            group: None,
345        };
346        assert_eq!(vm.to_string(), "on (job)");
347
348        let vm = VectorMatching {
349            op: VectorMatchingOp::Ignoring,
350            labels: vec!["job".to_string(), "instance".to_string()],
351            group: Some(GroupModifier {
352                side: GroupSide::Left,
353                labels: vec![],
354            }),
355        };
356        // Empty group labels: no parens needed
357        assert_eq!(vm.to_string(), "ignoring (job, instance) group_left");
358    }
359
360    #[test]
361    fn test_binary_modifier_display() {
362        let m = BinaryModifier {
363            return_bool: true,
364            matching: None,
365        };
366        assert_eq!(m.to_string(), "bool");
367
368        let m = BinaryModifier {
369            return_bool: false,
370            matching: Some(VectorMatching {
371                op: VectorMatchingOp::On,
372                labels: vec!["job".to_string()],
373                group: None,
374            }),
375        };
376        assert_eq!(m.to_string(), "on (job)");
377    }
378}