Skip to main content

edikt_core/
parser.rs

1//! Recursive-descent / precedence parser for the expression language.
2//!
3//! Precedence, lowest to highest: `|` (pipe), `,` (comma), comparison,
4//! additive, multiplicative, unary `-`, primary. Dotted/indexed paths desugar
5//! into a `Path` of steps.
6
7use crate::ast::{BinOp, Expr, Step};
8use crate::comment::CommentKind;
9use crate::lexer::Lx;
10use crate::value::Value;
11use logos::Logos;
12
13/// A parse failure with a byte offset into the source expression.
14#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
15#[error("{msg} (at offset {pos})")]
16pub struct ParseError {
17    pub msg: String,
18    pub pos: usize,
19}
20
21struct Tok {
22    kind: Lx,
23    text: String,
24    start: usize,
25}
26
27/// Parse a complete expression, consuming all of `src`.
28pub fn parse(src: &str) -> Result<Expr, ParseError> {
29    let toks = lex(src)?;
30    let mut p = Parser { toks, pos: 0 };
31    let e = p.parse_pipe()?;
32    if p.pos != p.toks.len() {
33        let t = &p.toks[p.pos];
34        return Err(ParseError {
35            msg: format!("unexpected trailing token `{}`", t.text),
36            pos: t.start,
37        });
38    }
39    Ok(e)
40}
41
42fn lex(src: &str) -> Result<Vec<Tok>, ParseError> {
43    let mut lx = Lx::lexer(src);
44    let mut out = Vec::new();
45    while let Some(res) = lx.next() {
46        let span = lx.span();
47        match res {
48            Ok(kind) => out.push(Tok {
49                kind,
50                text: lx.slice().to_string(),
51                start: span.start,
52            }),
53            Err(_) => {
54                return Err(ParseError {
55                    msg: format!("unexpected character `{}`", lx.slice()),
56                    pos: span.start,
57                });
58            }
59        }
60    }
61    Ok(out)
62}
63
64struct Parser {
65    toks: Vec<Tok>,
66    pos: usize,
67}
68
69impl Parser {
70    fn peek(&self) -> Option<Lx> {
71        self.toks.get(self.pos).map(|t| t.kind)
72    }
73    fn text(&self) -> &str {
74        self.toks
75            .get(self.pos)
76            .map(|t| t.text.as_str())
77            .unwrap_or("")
78    }
79    fn at_end(&self) -> usize {
80        self.toks
81            .last()
82            .map(|t| t.start + t.text.len())
83            .unwrap_or(0)
84    }
85    fn err_here(&self, msg: impl Into<String>) -> ParseError {
86        let pos = self
87            .toks
88            .get(self.pos)
89            .map(|t| t.start)
90            .unwrap_or_else(|| self.at_end());
91        ParseError {
92            msg: msg.into(),
93            pos,
94        }
95    }
96    fn expect(&mut self, kind: Lx, what: &str) -> Result<(), ParseError> {
97        if self.peek() == Some(kind) {
98            self.pos += 1;
99            Ok(())
100        } else {
101            Err(self.err_here(format!("expected {what}")))
102        }
103    }
104
105    fn parse_pipe(&mut self) -> Result<Expr, ParseError> {
106        let mut left = self.parse_comma()?;
107        while self.peek() == Some(Lx::Pipe) {
108            self.pos += 1;
109            let right = self.parse_comma()?;
110            left = Expr::Pipe(Box::new(left), Box::new(right));
111        }
112        Ok(left)
113    }
114
115    fn parse_comma(&mut self) -> Result<Expr, ParseError> {
116        let first = self.parse_assign()?;
117        if self.peek() != Some(Lx::Comma) {
118            return Ok(first);
119        }
120        let mut items = vec![first];
121        while self.peek() == Some(Lx::Comma) {
122            self.pos += 1;
123            items.push(self.parse_assign()?);
124        }
125        Ok(Expr::Comma(items))
126    }
127
128    fn parse_assign(&mut self) -> Result<Expr, ParseError> {
129        let lhs = self.parse_alt()?;
130        match self.peek() {
131            Some(Lx::Assign) => {
132                self.pos += 1;
133                let rhs = self.parse_assign()?; // right-associative
134                Ok(Expr::Assign(Box::new(lhs), Box::new(rhs)))
135            }
136            Some(Lx::PipeAssign) => {
137                self.pos += 1;
138                let rhs = self.parse_assign()?;
139                Ok(Expr::UpdateAssign(Box::new(lhs), Box::new(rhs)))
140            }
141            Some(Lx::PlusAssign) => {
142                self.pos += 1;
143                let rhs = self.parse_assign()?;
144                Ok(Expr::AddAssign(Box::new(lhs), Box::new(rhs)))
145            }
146            _ => Ok(lhs),
147        }
148    }
149
150    /// `a // b` - right-associative, binding tighter than `=` (so
151    /// `.k = .a // "d"` defaults the RHS) and looser than comparison.
152    fn parse_alt(&mut self) -> Result<Expr, ParseError> {
153        let left = self.parse_cmp()?;
154        if self.peek() == Some(Lx::Alt) {
155            self.pos += 1;
156            let right = self.parse_alt()?;
157            return Ok(Expr::Alternative(Box::new(left), Box::new(right)));
158        }
159        Ok(left)
160    }
161
162    fn parse_cmp(&mut self) -> Result<Expr, ParseError> {
163        let left = self.parse_add()?;
164        let op = match self.peek() {
165            Some(Lx::EqEq) => BinOp::Eq,
166            Some(Lx::Ne) => BinOp::Ne,
167            Some(Lx::Lt) => BinOp::Lt,
168            Some(Lx::Gt) => BinOp::Gt,
169            Some(Lx::Le) => BinOp::Le,
170            Some(Lx::Ge) => BinOp::Ge,
171            _ => return Ok(left),
172        };
173        self.pos += 1;
174        let right = self.parse_add()?;
175        Ok(Expr::Binary(op, Box::new(left), Box::new(right)))
176    }
177
178    fn parse_add(&mut self) -> Result<Expr, ParseError> {
179        let mut left = self.parse_mul()?;
180        loop {
181            let op = match self.peek() {
182                Some(Lx::Plus) => BinOp::Add,
183                Some(Lx::Minus) => BinOp::Sub,
184                _ => break,
185            };
186            self.pos += 1;
187            let right = self.parse_mul()?;
188            left = Expr::Binary(op, Box::new(left), Box::new(right));
189        }
190        Ok(left)
191    }
192
193    fn parse_mul(&mut self) -> Result<Expr, ParseError> {
194        let mut left = self.parse_unary()?;
195        loop {
196            let op = match self.peek() {
197                Some(Lx::Star) => BinOp::Mul,
198                Some(Lx::Slash) => BinOp::Div,
199                Some(Lx::Percent) => BinOp::Mod,
200                _ => break,
201            };
202            self.pos += 1;
203            let right = self.parse_unary()?;
204            left = Expr::Binary(op, Box::new(left), Box::new(right));
205        }
206        Ok(left)
207    }
208
209    fn parse_unary(&mut self) -> Result<Expr, ParseError> {
210        if self.peek() == Some(Lx::Minus) {
211            self.pos += 1;
212            return Ok(Expr::Neg(Box::new(self.parse_unary()?)));
213        }
214        self.parse_primary()
215    }
216
217    fn parse_primary(&mut self) -> Result<Expr, ParseError> {
218        match self.peek() {
219            Some(Lx::Dot) => self.parse_path(),
220            Some(Lx::LParen) => {
221                self.pos += 1;
222                let e = self.parse_pipe()?;
223                self.expect(Lx::RParen, "`)`")?;
224                Ok(e)
225            }
226            Some(Lx::LBrack) => {
227                self.pos += 1;
228                if self.peek() == Some(Lx::RBrack) {
229                    self.pos += 1;
230                    return Ok(Expr::Collect(None));
231                }
232                let inner = self.parse_pipe()?;
233                self.expect(Lx::RBrack, "`]`")?;
234                Ok(Expr::Collect(Some(Box::new(inner))))
235            }
236            Some(Lx::LBrace) => self.parse_object_construct(),
237            Some(Lx::Num) => {
238                let v = number_value(self.text());
239                self.pos += 1;
240                Ok(Expr::Literal(v))
241            }
242            Some(Lx::Str) => {
243                let v = Value::Str(unescape(self.text()));
244                self.pos += 1;
245                Ok(Expr::Literal(v))
246            }
247            Some(Lx::Ident) => self.parse_ident(),
248            _ => Err(self.err_here("expected an expression")),
249        }
250    }
251
252    fn parse_object_construct(&mut self) -> Result<Expr, ParseError> {
253        self.pos += 1; // `{`
254        let mut pairs = Vec::new();
255        if self.peek() == Some(Lx::RBrace) {
256            self.pos += 1;
257            return Ok(Expr::ObjectConstruct(pairs));
258        }
259        loop {
260            let key = match self.peek() {
261                Some(Lx::Ident) => self.text().to_string(),
262                Some(Lx::Str) => unescape(self.text()),
263                _ => return Err(self.err_here("expected an object key")),
264            };
265            self.pos += 1;
266            self.expect(Lx::Colon, "`:`")?;
267            // A comparison-level value keeps `,` free to separate entries.
268            let value = self.parse_cmp()?;
269            pairs.push((key, value));
270            match self.peek() {
271                Some(Lx::Comma) => self.pos += 1,
272                Some(Lx::RBrace) => {
273                    self.pos += 1;
274                    break;
275                }
276                _ => return Err(self.err_here("expected `,` or `}`")),
277            }
278        }
279        Ok(Expr::ObjectConstruct(pairs))
280    }
281
282    fn parse_path(&mut self) -> Result<Expr, ParseError> {
283        self.pos += 1; // leading `.`
284        let mut steps = Vec::new();
285        loop {
286            match self.peek() {
287                Some(Lx::Ident) => {
288                    steps.push(Step::Field(self.text().to_string()));
289                    self.pos += 1;
290                }
291                Some(Lx::Str) => {
292                    steps.push(Step::Field(unescape(self.text())));
293                    self.pos += 1;
294                }
295                Some(Lx::LBrack) => {
296                    self.pos += 1;
297                    if self.peek() == Some(Lx::RBrack) {
298                        self.pos += 1;
299                        steps.push(Step::Iterate);
300                    } else if self.peek() == Some(Lx::Str) {
301                        // `.["key"]` - a field by name (dotted/special keys).
302                        let key = unescape(self.text());
303                        self.pos += 1;
304                        self.expect(Lx::RBrack, "`]`")?;
305                        steps.push(Step::Field(key));
306                    } else {
307                        let neg = self.peek() == Some(Lx::Minus);
308                        if neg {
309                            self.pos += 1;
310                        }
311                        if self.peek() != Some(Lx::Num) {
312                            return Err(self.err_here("expected an array index or a string key"));
313                        }
314                        let n = parse_i64(self.text())
315                            .map_err(|_| self.err_here("array index out of range"))?;
316                        self.pos += 1;
317                        self.expect(Lx::RBrack, "`]`")?;
318                        steps.push(Step::Index(if neg { -n } else { n }));
319                    }
320                }
321                Some(Lx::Hash) => {
322                    // `#` addresses the current node's comment. `#` alone is the
323                    // head comment; `#.head`/`#.inline`/`#.foot` pick a kind.
324                    // Terminal - no navigation follows a comment.
325                    self.pos += 1;
326                    let mut kind = CommentKind::Head;
327                    if self.peek() == Some(Lx::Dot)
328                        && let Some(word) =
329                            self.toks.get(self.pos + 1).filter(|t| t.kind == Lx::Ident)
330                        && let Some(k) = comment_kind(&word.text)
331                    {
332                        self.pos += 2; // consume `.` and the kind word
333                        kind = k;
334                    }
335                    steps.push(Step::Comment(kind));
336                    break;
337                }
338                _ => break,
339            }
340            match self.peek() {
341                Some(Lx::Dot) => {
342                    self.pos += 1;
343                    continue;
344                }
345                Some(Lx::LBrack) => continue,
346                _ => break,
347            }
348        }
349        Ok(Expr::Path(steps))
350    }
351
352    fn parse_ident(&mut self) -> Result<Expr, ParseError> {
353        let name = self.text().to_string();
354        self.pos += 1;
355        match name.as_str() {
356            "true" => return Ok(Expr::Literal(Value::Bool(true))),
357            "false" => return Ok(Expr::Literal(Value::Bool(false))),
358            "null" => return Ok(Expr::Literal(Value::Null)),
359            _ => {}
360        }
361        if self.peek() == Some(Lx::LParen) {
362            self.pos += 1;
363            let mut args = vec![self.parse_pipe()?];
364            while self.peek() == Some(Lx::Semi) {
365                self.pos += 1;
366                args.push(self.parse_pipe()?);
367            }
368            self.expect(Lx::RParen, "`)`")?;
369            Ok(Expr::Call(name, args))
370        } else {
371            Ok(Expr::Call(name, Vec::new()))
372        }
373    }
374}
375
376/// Map a `#.<word>` refinement to its comment kind.
377fn comment_kind(word: &str) -> Option<CommentKind> {
378    match word {
379        "head" => Some(CommentKind::Head),
380        "inline" => Some(CommentKind::Inline),
381        "foot" => Some(CommentKind::Foot),
382        _ => None,
383    }
384}
385
386fn number_value(t: &str) -> Value {
387    if t.contains(['.', 'e', 'E']) {
388        Value::Float(t.parse().unwrap_or(0.0))
389    } else {
390        match t.parse::<i64>() {
391            Ok(i) => Value::Int(i),
392            Err(_) => Value::Float(t.parse().unwrap_or(0.0)),
393        }
394    }
395}
396
397fn parse_i64(t: &str) -> Result<i64, std::num::ParseIntError> {
398    t.parse::<i64>()
399}
400
401/// Unescape a JSON-style double-quoted string token (including its quotes).
402fn unescape(tok: &str) -> String {
403    let inner = tok
404        .strip_prefix('"')
405        .and_then(|s| s.strip_suffix('"'))
406        .unwrap_or(tok);
407    let mut out = String::with_capacity(inner.len());
408    let mut chars = inner.chars();
409    while let Some(c) = chars.next() {
410        if c != '\\' {
411            out.push(c);
412            continue;
413        }
414        match chars.next() {
415            Some('"') => out.push('"'),
416            Some('\\') => out.push('\\'),
417            Some('/') => out.push('/'),
418            Some('n') => out.push('\n'),
419            Some('r') => out.push('\r'),
420            Some('t') => out.push('\t'),
421            Some('b') => out.push('\u{0008}'),
422            Some('f') => out.push('\u{000c}'),
423            Some('u') => {
424                let hex: String = chars.by_ref().take(4).collect();
425                if let Some(ch) = u32::from_str_radix(&hex, 16).ok().and_then(char::from_u32) {
426                    out.push(ch);
427                }
428            }
429            Some(other) => {
430                out.push('\\');
431                out.push(other);
432            }
433            None => out.push('\\'),
434        }
435    }
436    out
437}
438
439#[cfg(test)]
440mod tests {
441    use super::*;
442
443    fn p(s: &str) -> Expr {
444        parse(s).unwrap_or_else(|e| panic!("parse `{s}`: {e}"))
445    }
446
447    #[test]
448    fn identity() {
449        assert_eq!(p("."), Expr::Path(vec![]));
450    }
451
452    #[test]
453    fn dotted_path() {
454        assert_eq!(
455            p(".a.b"),
456            Expr::Path(vec![Step::Field("a".into()), Step::Field("b".into())])
457        );
458    }
459
460    #[test]
461    fn index_and_iterate() {
462        assert_eq!(
463            p(".arr[0][]"),
464            Expr::Path(vec![
465                Step::Field("arr".into()),
466                Step::Index(0),
467                Step::Iterate
468            ])
469        );
470        assert_eq!(
471            p(".x[-1]"),
472            Expr::Path(vec![Step::Field("x".into()), Step::Index(-1)])
473        );
474    }
475
476    #[test]
477    fn quoted_field() {
478        assert_eq!(
479            p(r#"."weird key""#),
480            Expr::Path(vec![Step::Field("weird key".into())])
481        );
482    }
483
484    #[test]
485    fn literals() {
486        assert_eq!(p("true"), Expr::Literal(Value::Bool(true)));
487        assert_eq!(p("null"), Expr::Literal(Value::Null));
488        assert_eq!(p("42"), Expr::Literal(Value::Int(42)));
489        assert_eq!(p("1.5"), Expr::Literal(Value::Float(1.5)));
490        assert_eq!(p(r#""hi""#), Expr::Literal(Value::Str("hi".into())));
491    }
492
493    #[test]
494    fn arithmetic_precedence() {
495        // 1 + 2 * 3  ==  1 + (2 * 3)
496        assert_eq!(
497            p("1 + 2 * 3"),
498            Expr::Binary(
499                BinOp::Add,
500                Box::new(Expr::Literal(Value::Int(1))),
501                Box::new(Expr::Binary(
502                    BinOp::Mul,
503                    Box::new(Expr::Literal(Value::Int(2))),
504                    Box::new(Expr::Literal(Value::Int(3))),
505                )),
506            )
507        );
508    }
509
510    #[test]
511    fn pipe_and_select() {
512        let e = p(r#".items[] | select(.name == "x")"#);
513        match e {
514            Expr::Pipe(l, r) => {
515                assert_eq!(
516                    *l,
517                    Expr::Path(vec![Step::Field("items".into()), Step::Iterate])
518                );
519                match *r {
520                    Expr::Call(ref name, ref args) => {
521                        assert_eq!(name, "select");
522                        assert_eq!(args.len(), 1);
523                    }
524                    _ => panic!("expected select call"),
525                }
526            }
527            _ => panic!("expected pipe"),
528        }
529    }
530
531    #[test]
532    fn errors() {
533        assert!(parse(".a.").is_ok()); // lenient trailing dot
534        assert!(parse("(").is_err());
535        assert!(parse(".a b").is_err()); // trailing token
536        assert!(parse("@").is_err()); // bad char
537    }
538
539    #[test]
540    fn comment_accessor() {
541        use crate::comment::CommentKind;
542        // `#` alone is the head comment.
543        assert_eq!(
544            p(".foo.#"),
545            Expr::Path(vec![
546                Step::Field("foo".into()),
547                Step::Comment(CommentKind::Head)
548            ])
549        );
550        // `#.head` / `#.inline` / `#.foot` pick a kind.
551        assert_eq!(
552            p(".foo.#.inline"),
553            Expr::Path(vec![
554                Step::Field("foo".into()),
555                Step::Comment(CommentKind::Inline)
556            ])
557        );
558        assert_eq!(
559            p(".a.#.foot"),
560            Expr::Path(vec![
561                Step::Field("a".into()),
562                Step::Comment(CommentKind::Foot)
563            ])
564        );
565        // `.#` at the top is the document comment.
566        assert_eq!(p(".#"), Expr::Path(vec![Step::Comment(CommentKind::Head)]));
567        // After iteration: `.items[].#`.
568        assert_eq!(
569            p(".items[].#"),
570            Expr::Path(vec![
571                Step::Field("items".into()),
572                Step::Iterate,
573                Step::Comment(CommentKind::Head)
574            ])
575        );
576        // Comment is terminal - nothing may navigate past it.
577        assert!(parse(".foo.#.bar").is_err());
578    }
579}