Skip to main content

ravenrs_criteria/
parse.rs

1//! Lexer and recursive-descent parser.
2
3use crate::ast::{AggregateCall, BinaryOp, Criteria, GroupOp, UnaryOp};
4use crate::functions::{Aggregate, Function};
5use crate::value::{DateTime, Value};
6
7/// A syntax error, with the byte offset where it was found.
8#[derive(Clone, Debug, PartialEq, Eq)]
9pub struct ParseError {
10    /// What is wrong.
11    pub message: String,
12    /// Byte offset in the source.
13    pub offset: usize,
14}
15
16impl std::fmt::Display for ParseError {
17    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
18        write!(f, "{} (at {})", self.message, self.offset)
19    }
20}
21
22impl std::error::Error for ParseError {}
23
24/// Deepest nesting accepted, so hostile input cannot overflow the stack.
25const MAX_DEPTH: u32 = 128;
26
27#[derive(Clone, Debug, PartialEq)]
28enum Tok {
29    Num(f64),
30    Str(String),
31    Date(DateTime),
32    Ident(String),
33    /// `[...]`, with its text and the text's offset; an empty one marks a collection.
34    Field(String, usize),
35    Param(String),
36    LParen,
37    RParen,
38    Comma,
39    Dot,
40    Op(&'static str),
41    End,
42}
43
44fn lex(src: &str, base: usize) -> Result<Vec<(Tok, usize)>, ParseError> {
45    let mut out = Vec::new();
46    let mut pos = 0;
47    let err = |message: String, at: usize| ParseError { message, offset: base + at };
48    loop {
49        let rest = &src[pos..];
50        let trimmed = rest.trim_start();
51        pos += rest.len() - trimmed.len();
52        if trimmed.starts_with("/*") {
53            let Some(end) = trimmed.find("*/") else { return Err(err("unterminated comment".into(), pos)) };
54            pos += end + 2;
55            continue;
56        }
57        let start = pos;
58        let Some(c) = trimmed.chars().next() else {
59            out.push((Tok::End, base + start));
60            return Ok(out);
61        };
62        let tok = match c {
63            '0'..='9' => {
64                let digits = trimmed
65                    .char_indices()
66                    .find(|(i, ch)| {
67                        !(ch.is_ascii_digit()
68                            || (*ch == '.' && trimmed[i + 1..].starts_with(|n: char| n.is_ascii_digit())))
69                    })
70                    .map_or(trimmed.len(), |(i, _)| i);
71                let n = trimmed[..digits].parse().map_err(|_| err("invalid number".into(), pos))?;
72                pos += digits;
73                // Type suffixes (12m, 1.5f...) do not change the value here.
74                if let Some(s) = src[pos..].chars().next()
75                    && matches!(s.to_ascii_lowercase(), 'm' | 'f' | 's' | 'b' | 'l' | 'u')
76                    && !src[pos + 1..].starts_with(|n: char| n.is_alphanumeric() || n == '_')
77                {
78                    pos += 1;
79                }
80                Tok::Num(n)
81            }
82            '\'' => {
83                let mut s = String::new();
84                let mut chars = trimmed[1..].char_indices();
85                let mut end = None;
86                while let Some((i, ch)) = chars.next() {
87                    if ch == '\'' {
88                        if trimmed[1 + i + 1..].starts_with('\'') {
89                            s.push('\'');
90                            chars.next();
91                            continue;
92                        }
93                        end = Some(1 + i + 1);
94                        break;
95                    }
96                    s.push(ch);
97                }
98                let Some(end) = end else { return Err(err("unterminated text".into(), pos)) };
99                pos += end;
100                Tok::Str(s)
101            }
102            '#' => {
103                let Some(close) = trimmed[1..].find('#') else {
104                    return Err(err("unterminated date".into(), pos));
105                };
106                let d =
107                    DateTime::parse(&trimmed[1..1 + close]).ok_or_else(|| err("invalid date".into(), pos))?;
108                pos += close + 2;
109                Tok::Date(d)
110            }
111            '{' => {
112                let Some(close) = trimmed.find('}') else { return Err(err("unterminated '{'".into(), pos)) };
113                let guid = trimmed[1..close].to_owned();
114                pos += close + 1;
115                Tok::Str(guid)
116            }
117            '[' => {
118                // Brackets nest in `[Items][[Qty] > 0]`, and quoted text may hold brackets.
119                let (mut depth, mut quoted, mut close) = (0, false, None);
120                for (i, ch) in trimmed.char_indices() {
121                    match ch {
122                        '\'' => quoted = !quoted,
123                        '[' if !quoted => depth += 1,
124                        ']' if !quoted => {
125                            depth -= 1;
126                            if depth == 0 {
127                                close = Some(i);
128                                break;
129                            }
130                        }
131                        _ => {}
132                    }
133                }
134                let Some(close) = close else { return Err(err("missing ']'".into(), pos)) };
135                let inner = trimmed[1..close].to_owned();
136                pos += close + 1;
137                Tok::Field(inner, base + start + 1)
138            }
139            '?' => {
140                let len = 1 + trimmed[1..]
141                    .find(|ch: char| !(ch.is_alphanumeric() || ch == '_'))
142                    .unwrap_or(trimmed.len() - 1);
143                pos += len;
144                // A lone `?` is the null constant.
145                Tok::Param(trimmed[1..len].to_owned())
146            }
147            '(' | ')' | ',' | '.' => {
148                pos += 1;
149                match c {
150                    '(' => Tok::LParen,
151                    ')' => Tok::RParen,
152                    ',' => Tok::Comma,
153                    _ => Tok::Dot,
154                }
155            }
156            c if c.is_alphabetic() || c == '_' => {
157                let len =
158                    trimmed.find(|ch: char| !(ch.is_alphanumeric() || ch == '_')).unwrap_or(trimmed.len());
159                pos += len;
160                Tok::Ident(trimmed[..len].to_owned())
161            }
162            _ => {
163                const OPS: [&str; 19] = [
164                    "<>", "!=", "==", "<=", ">=", "&&", "||", "+", "-", "*", "/", "%", "=", "<", ">", "!",
165                    "&", "|", "^",
166                ];
167                if let Some(op) = OPS.iter().find(|op| trimmed.starts_with(**op)) {
168                    pos += op.len();
169                    Tok::Op(op)
170                } else if c == '~' {
171                    pos += 1;
172                    Tok::Op("~")
173                } else {
174                    return Err(err(format!("unexpected '{c}'"), pos));
175                }
176            }
177        };
178        out.push((tok, base + start));
179    }
180}
181
182/// Splits `field = expression; ...` into its fields and parsed expressions. A field is a name or
183/// `[name]`; `;` and `=` inside quotes or brackets belong to the expression.
184pub(crate) fn parse_assignments(source: &str) -> Result<Vec<(String, Criteria)>, ParseError> {
185    let mut found = Vec::new();
186    let mut start = 0;
187    for end in top_level(source, b';').into_iter().chain([source.len()]) {
188        let piece = &source[start..end];
189        if !piece.trim().is_empty() {
190            let at = |offset: usize| start + offset;
191            let Some(&equals) = top_level(piece, b'=').first() else {
192                let first = piece.len() - piece.trim_start().len();
193                return Err(ParseError {
194                    message: "expected `field = expression`".into(),
195                    offset: at(first),
196                });
197            };
198            let name = piece[..equals].trim();
199            let name = name.strip_prefix('[').and_then(|n| n.strip_suffix(']')).unwrap_or(name).trim();
200            let simple = name.starts_with(|c: char| c.is_alphabetic() || c == '_')
201                && name.chars().all(|c| c.is_alphanumeric() || c == '_' || c == '.');
202            if !simple {
203                let first = piece.len() - piece.trim_start().len();
204                return Err(ParseError {
205                    message: "expected the name of a field before `=`".into(),
206                    offset: at(first),
207                });
208            }
209            let text = &piece[equals + 1..];
210            let expression =
211                parse(text).map_err(|e| ParseError { offset: at(equals + 1 + e.offset), ..e })?;
212            found.push((name.to_owned(), expression));
213        }
214        start = end + 1;
215    }
216    Ok(found)
217}
218
219/// Where `wanted` stands outside quotes and brackets; an `=` next to `<`, `>`, `!` or `=` is part
220/// of an operator.
221fn top_level(source: &str, wanted: u8) -> Vec<usize> {
222    let bytes = source.as_bytes();
223    let (mut quoted, mut brackets, mut found) = (false, 0usize, Vec::new());
224    for (i, &byte) in bytes.iter().enumerate() {
225        match byte {
226            b'\'' => quoted = !quoted,
227            _ if quoted => {}
228            b'[' => brackets += 1,
229            b']' => brackets = brackets.saturating_sub(1),
230            _ if brackets > 0 => {}
231            b'=' if wanted == b'=' => {
232                let before = i.checked_sub(1).map(|j| bytes[j]);
233                let after = bytes.get(i + 1).copied();
234                let operator = matches!(before, Some(b'<' | b'>' | b'!' | b'=')) || after == Some(b'=');
235                if !operator {
236                    found.push(i);
237                }
238            }
239            _ if byte == wanted => found.push(i),
240            _ => {}
241        }
242    }
243    found
244}
245
246struct Parser {
247    toks: Vec<(Tok, usize)>,
248    i: usize,
249    depth: u32,
250}
251
252/// Parses an expression.
253pub fn parse(source: &str) -> Result<Criteria, ParseError> {
254    parse_at(source, 0)
255}
256
257fn parse_at(source: &str, base: usize) -> Result<Criteria, ParseError> {
258    let toks = lex(source, base)?;
259    let mut p = Parser { toks, i: 0, depth: 0 };
260    let node = p.or()?;
261    match p.peek() {
262        Tok::End => Ok(node),
263        other => Err(p.err(format!("unexpected {}", describe(other)))),
264    }
265}
266
267fn describe(t: &Tok) -> String {
268    match t {
269        Tok::End => "end of expression".into(),
270        Tok::Ident(s) => format!("'{s}'"),
271        Tok::Op(o) => format!("'{o}'"),
272        Tok::Field(f, _) => format!("'[{f}]'"),
273        Tok::RParen => "')'".into(),
274        Tok::LParen => "'('".into(),
275        Tok::Comma => "','".into(),
276        Tok::Dot => "'.'".into(),
277        Tok::Num(n) => format!("'{n}'"),
278        Tok::Str(s) => format!("'{s}'"),
279        Tok::Date(d) => format!("'{d}'"),
280        Tok::Param(p) => format!("'?{p}'"),
281    }
282}
283
284fn bin(op: BinaryOp, a: Criteria, b: Criteria) -> Criteria {
285    Criteria::Binary(op, Box::new(a), Box::new(b))
286}
287
288impl Parser {
289    fn peek(&self) -> &Tok {
290        &self.toks[self.i].0
291    }
292
293    fn offset(&self) -> usize {
294        self.toks[self.i].1
295    }
296
297    fn err(&self, message: impl Into<String>) -> ParseError {
298        ParseError { message: message.into(), offset: self.offset() }
299    }
300
301    fn bump(&mut self) -> Tok {
302        let t = self.toks[self.i].0.clone();
303        if self.i + 1 < self.toks.len() {
304            self.i += 1;
305        }
306        t
307    }
308
309    fn is_word(&self, word: &str) -> bool {
310        matches!(self.peek(), Tok::Ident(s) if s.eq_ignore_ascii_case(word))
311    }
312
313    fn is_op(&self, ops: &[&str]) -> Option<&'static str> {
314        match self.peek() {
315            Tok::Op(o) if ops.contains(o) => Some(o),
316            _ => None,
317        }
318    }
319
320    fn expect(&mut self, t: &Tok) -> Result<(), ParseError> {
321        if self.peek() == t {
322            self.bump();
323            Ok(())
324        } else {
325            Err(self.err(format!("expected {}, found {}", describe(t), describe(self.peek()))))
326        }
327    }
328
329    fn enter(&mut self) -> Result<(), ParseError> {
330        self.depth += 1;
331        if self.depth > MAX_DEPTH {
332            return Err(self.err("expression is nested too deeply"));
333        }
334        Ok(())
335    }
336
337    fn or(&mut self) -> Result<Criteria, ParseError> {
338        let first = self.and()?;
339        let mut items = vec![first];
340        while self.is_word("or") || self.is_op(&["||"]).is_some() {
341            self.bump();
342            items.push(self.and()?);
343        }
344        Ok(if items.len() == 1 { items.remove(0) } else { Criteria::Group(GroupOp::Or, items) })
345    }
346
347    fn and(&mut self) -> Result<Criteria, ParseError> {
348        let first = self.not()?;
349        let mut items = vec![first];
350        while self.is_word("and") || self.is_op(&["&&"]).is_some() {
351            self.bump();
352            items.push(self.not()?);
353        }
354        Ok(if items.len() == 1 { items.remove(0) } else { Criteria::Group(GroupOp::And, items) })
355    }
356
357    fn not(&mut self) -> Result<Criteria, ParseError> {
358        if self.is_word("not") || self.is_op(&["!"]).is_some() {
359            self.bump();
360            self.enter()?;
361            let inner = self.not()?;
362            self.depth -= 1;
363            return Ok(Criteria::Unary(UnaryOp::Not, Box::new(inner)));
364        }
365        self.bit_or()
366    }
367
368    fn bit_or(&mut self) -> Result<Criteria, ParseError> {
369        let mut left = self.bit_xor()?;
370        while self.is_op(&["|"]).is_some() {
371            self.bump();
372            left = bin(BinaryOp::BitOr, left, self.bit_xor()?);
373        }
374        Ok(left)
375    }
376
377    fn bit_xor(&mut self) -> Result<Criteria, ParseError> {
378        let mut left = self.bit_and()?;
379        while self.is_op(&["^"]).is_some() {
380            self.bump();
381            left = bin(BinaryOp::BitXor, left, self.bit_and()?);
382        }
383        Ok(left)
384    }
385
386    fn bit_and(&mut self) -> Result<Criteria, ParseError> {
387        let mut left = self.comparison()?;
388        while self.is_op(&["&"]).is_some() {
389            self.bump();
390            left = bin(BinaryOp::BitAnd, left, self.comparison()?);
391        }
392        Ok(left)
393    }
394
395    fn comparison(&mut self) -> Result<Criteria, ParseError> {
396        let mut left = self.additive()?;
397        loop {
398            if let Some(op) = self.is_op(&["=", "==", "<>", "!=", "<", "<=", ">", ">="]) {
399                self.bump();
400                let op = match op {
401                    "=" | "==" => BinaryOp::Eq,
402                    "<>" | "!=" => BinaryOp::Ne,
403                    "<" => BinaryOp::Lt,
404                    "<=" => BinaryOp::Le,
405                    ">" => BinaryOp::Gt,
406                    _ => BinaryOp::Ge,
407                };
408                left = bin(op, left, self.additive()?);
409                continue;
410            }
411            // `[A] Not In (...)`, `[A] Not Between(...)`, `[A] Not Like '...'`.
412            let negated = self.is_word("not")
413                && matches!(self.toks.get(self.i + 1), Some((Tok::Ident(w), _))
414                    if ["in", "between", "like"].iter().any(|k| w.eq_ignore_ascii_case(k)));
415            if negated {
416                self.bump();
417            }
418            let wrap = |c: Criteria| if negated { Criteria::Unary(UnaryOp::Not, Box::new(c)) } else { c };
419            if self.is_word("like") {
420                self.bump();
421                let pattern = self.additive()?;
422                left = wrap(bin(BinaryOp::Like, left, pattern));
423            } else if self.is_word("in") {
424                self.bump();
425                self.expect(&Tok::LParen)?;
426                let items = self.args()?;
427                left = wrap(Criteria::In(Box::new(left), items));
428            } else if self.is_word("between") {
429                self.bump();
430                self.expect(&Tok::LParen)?;
431                let lo = self.or()?;
432                self.expect(&Tok::Comma)?;
433                let hi = self.or()?;
434                self.expect(&Tok::RParen)?;
435                left = wrap(Criteria::Between(Box::new(left), Box::new(lo), Box::new(hi)));
436            } else if self.is_word("is") {
437                self.bump();
438                let is_not = self.is_word("not");
439                if is_not {
440                    self.bump();
441                }
442                if !self.is_word("null") {
443                    return Err(self.err("expected 'Null' after 'Is'"));
444                }
445                self.bump();
446                let c = Criteria::IsNull(Box::new(left));
447                left = if is_not { Criteria::Unary(UnaryOp::Not, Box::new(c)) } else { c };
448            } else {
449                return Ok(left);
450            }
451        }
452    }
453
454    fn additive(&mut self) -> Result<Criteria, ParseError> {
455        let mut left = self.multiplicative()?;
456        while let Some(op) = self.is_op(&["+", "-"]) {
457            self.bump();
458            let op = if op == "+" { BinaryOp::Add } else { BinaryOp::Sub };
459            left = bin(op, left, self.multiplicative()?);
460        }
461        Ok(left)
462    }
463
464    fn multiplicative(&mut self) -> Result<Criteria, ParseError> {
465        let mut left = self.unary()?;
466        while let Some(op) = self.is_op(&["*", "/", "%"]) {
467            self.bump();
468            let op = match op {
469                "*" => BinaryOp::Mul,
470                "/" => BinaryOp::Div,
471                _ => BinaryOp::Rem,
472            };
473            left = bin(op, left, self.unary()?);
474        }
475        Ok(left)
476    }
477
478    fn unary(&mut self) -> Result<Criteria, ParseError> {
479        if let Some(op) = self.is_op(&["-", "+", "~"]) {
480            self.bump();
481            self.enter()?;
482            let inner = self.unary()?;
483            self.depth -= 1;
484            return Ok(match op {
485                "-" => Criteria::Unary(UnaryOp::Minus, Box::new(inner)),
486                "~" => Criteria::Unary(UnaryOp::BitNot, Box::new(inner)),
487                _ => inner,
488            });
489        }
490        self.primary()
491    }
492
493    /// Arguments up to and including the closing parenthesis.
494    fn args(&mut self) -> Result<Vec<Criteria>, ParseError> {
495        let mut args = Vec::new();
496        if *self.peek() == Tok::RParen {
497            self.bump();
498            return Ok(args);
499        }
500        self.enter()?;
501        loop {
502            args.push(self.or()?);
503            let at = self.offset();
504            match self.bump() {
505                Tok::Comma => {}
506                Tok::RParen => {
507                    self.depth -= 1;
508                    return Ok(args);
509                }
510                other => {
511                    return Err(ParseError {
512                        message: format!("expected ',' or ')', found {}", describe(&other)),
513                        offset: at,
514                    });
515                }
516            }
517        }
518    }
519
520    fn primary(&mut self) -> Result<Criteria, ParseError> {
521        let at = self.offset();
522        match self.bump() {
523            Tok::Num(n) => Ok(Criteria::Value(Value::Number(n))),
524            Tok::Str(s) => Ok(Criteria::Value(Value::Text(s))),
525            Tok::Date(d) => Ok(Criteria::Value(Value::Date(d))),
526            Tok::Param(p) if p.is_empty() => Ok(Criteria::Value(Value::Null)),
527            Tok::Param(p) => Ok(Criteria::Parameter(p)),
528            Tok::LParen => {
529                self.enter()?;
530                let inner = self.or()?;
531                self.depth -= 1;
532                self.expect(&Tok::RParen)?;
533                Ok(inner)
534            }
535            Tok::Field(name, _) if name.trim().is_empty() => {
536                Err(ParseError { message: "empty field '[]'".into(), offset: at })
537            }
538            Tok::Field(name, _) => self.field(&name, at),
539            Tok::Ident(word) => self.word(&word, at),
540            other => Err(ParseError { message: format!("unexpected {}", describe(&other)), offset: at }),
541        }
542    }
543
544    fn field(&mut self, name: &str, at: usize) -> Result<Criteria, ParseError> {
545        let path: Vec<String> = name.split('.').map(|s| s.trim().to_owned()).collect();
546        if path.iter().any(String::is_empty) {
547            return Err(ParseError { message: format!("invalid field '[{name}]'"), offset: at });
548        }
549        // `[Parameters.Name]` is the older spelling of `?Name`.
550        if path.len() == 2 && path[0].eq_ignore_ascii_case("parameters") {
551            return Ok(Criteria::Parameter(path[1].clone()));
552        }
553        // `[Items][].Sum(...)` or `[Items][[Qty] > 0].Sum(...)`: an aggregate over a collection.
554        if let Tok::Field(inner, inner_at) = self.peek().clone() {
555            self.bump();
556            let condition = if inner.trim().is_empty() { None } else { Some(parse_at(&inner, inner_at)?) };
557            self.expect(&Tok::Dot)?;
558            let fat = self.offset();
559            let Tok::Ident(f) = self.bump() else {
560                return Err(ParseError { message: "expected an aggregate after '].'".into(), offset: fat });
561            };
562            let function = Aggregate::from_name(&f)
563                .ok_or_else(|| ParseError { message: format!("unknown aggregate '{f}'"), offset: fat })?;
564            self.expect(&Tok::LParen)?;
565            let args = self.args()?;
566            check_aggregate(function, args.len(), fat)?;
567            return Ok(Criteria::Aggregate(Box::new(AggregateCall {
568                collection: Some(path),
569                condition,
570                function,
571                args,
572            })));
573        }
574        Ok(Criteria::Property(path))
575    }
576
577    fn word(&mut self, word: &str, at: usize) -> Result<Criteria, ParseError> {
578        let lower = word.to_lowercase();
579        match lower.as_str() {
580            "true" => return Ok(Criteria::Value(Value::Bool(true))),
581            "false" => return Ok(Criteria::Value(Value::Bool(false))),
582            "null" => return Ok(Criteria::Value(Value::Null)),
583            _ => {}
584        }
585        if *self.peek() != Tok::LParen {
586            return Err(ParseError {
587                message: format!("unknown name '{word}' (fields go in brackets: [{word}])"),
588                offset: at,
589            });
590        }
591        self.bump();
592        let args = self.args()?;
593        // Min and Max with two arguments compare; with one they aggregate.
594        let scalar = Function::from_name(word)
595            .filter(|f| !matches!(f, Function::Min2 | Function::Max2) || args.len() == 2);
596        if let Some(f) = scalar {
597            let (min, max) = f.arity();
598            if args.len() < min || args.len() > max || (f == Function::Iif && args.len() % 2 == 0) {
599                return Err(ParseError {
600                    message: format!("wrong number of arguments ({}) for {}", args.len(), f.info().signature),
601                    offset: at,
602                });
603            }
604            return Ok(Criteria::Function(f, args));
605        }
606        if let Some(function) = Aggregate::from_name(word) {
607            check_aggregate(function, args.len(), at)?;
608            return Ok(Criteria::Aggregate(Box::new(AggregateCall {
609                collection: None,
610                condition: None,
611                function,
612                args,
613            })));
614        }
615        // Resolved when evaluated, against the functions the environment registers.
616        Ok(Criteria::Custom(word.to_owned(), args))
617    }
618}
619
620fn check_aggregate(function: Aggregate, args: usize, at: usize) -> Result<(), ParseError> {
621    let (min, max) = function.arity();
622    if args < min || args > max {
623        return Err(ParseError {
624            message: format!("wrong number of arguments ({args}) for {}", function.info().signature),
625            offset: at,
626        });
627    }
628    Ok(())
629}