Skip to main content

jpx_core/
parser.rs

1//! Module for parsing JMESPath expressions into an AST.
2//!
3//! This JMESPath parser is implemented using a Pratt parser,
4//! or top down operator precedence parser.
5
6use crate::ast::{Ast, Comparator, KeyValuePair};
7use crate::lexer::{Token, TokenTuple, tokenize};
8use crate::{ErrorReason, JmespathError};
9
10/// Result of parsing an expression.
11pub type ParseResult = Result<Ast, JmespathError>;
12
13/// Parses a JMESPath expression into an AST.
14pub fn parse(expr: &str) -> ParseResult {
15    let tokens = tokenize(expr)?;
16    Parser::new(tokens, expr).parse()
17}
18
19/// The maximum binding power for a token that can stop a projection.
20const PROJECTION_STOP: usize = 10;
21
22struct Parser<'a> {
23    tokens: Vec<TokenTuple<'a>>,
24    cursor: usize,
25    expr: &'a str,
26    offset: usize,
27}
28
29impl<'a> Parser<'a> {
30    fn new(tokens: Vec<TokenTuple<'a>>, expr: &'a str) -> Parser<'a> {
31        Parser {
32            tokens,
33            cursor: 0,
34            expr,
35            offset: 0,
36        }
37    }
38
39    #[inline]
40    fn parse(&mut self) -> ParseResult {
41        self.expr(0).and_then(|result| match self.peek(0) {
42            &Token::Eof => Ok(result),
43            t => Err(self.err(t, "Did not parse the complete expression", true)),
44        })
45    }
46
47    #[inline]
48    fn advance(&mut self) -> Token<'a> {
49        self.advance_with_pos().1
50    }
51
52    #[inline]
53    fn advance_with_pos(&mut self) -> (usize, Token<'a>) {
54        if self.cursor < self.tokens.len() {
55            let (pos, tok) = self.tokens[self.cursor].clone();
56            self.cursor += 1;
57            self.offset = pos;
58            (pos, tok)
59        } else {
60            (self.offset, Token::Eof)
61        }
62    }
63
64    #[inline]
65    fn peek(&self, lookahead: usize) -> &Token<'a> {
66        let idx = self.cursor + lookahead;
67        if idx < self.tokens.len() {
68            &self.tokens[idx].1
69        } else {
70            &Token::Eof
71        }
72    }
73
74    fn err(&self, current_token: &Token<'_>, error_msg: &str, is_peek: bool) -> JmespathError {
75        let mut actual_pos = self.offset;
76        let mut buff = error_msg.to_string();
77        buff.push_str(&format!(" -- found {current_token:?}"));
78        if is_peek && self.cursor < self.tokens.len() {
79            actual_pos = self.tokens[self.cursor].0;
80        }
81        JmespathError::new(self.expr, actual_pos, ErrorReason::Parse(buff))
82    }
83
84    fn expr(&mut self, rbp: usize) -> ParseResult {
85        let mut left = self.nud();
86        while rbp < self.peek(0).lbp() {
87            left = self.led(Box::new(left?));
88        }
89        left
90    }
91
92    fn nud(&mut self) -> ParseResult {
93        let (offset, token) = self.advance_with_pos();
94        match token {
95            Token::At => Ok(Ast::Identity { offset }),
96            #[cfg(feature = "let-expr")]
97            Token::Identifier(value) if *value == *"let" => self.parse_let(offset),
98            Token::Identifier(value) => Ok(Ast::Field {
99                name: value.to_owned(),
100                offset,
101            }),
102            Token::QuotedIdentifier(value) => match self.peek(0) {
103                Token::Lparen => {
104                    let message = "Quoted strings can't be a function name";
105                    Err(self.err(&Token::Lparen, message, true))
106                }
107                _ => Ok(Ast::Field {
108                    name: value,
109                    offset,
110                }),
111            },
112            Token::Star => self.parse_wildcard_values(Box::new(Ast::Identity { offset })),
113            Token::Literal(value) => Ok(Ast::Literal { value, offset }),
114            Token::Lbracket => match self.peek(0) {
115                &Token::Number(_) | &Token::Colon => self.parse_index(),
116                &Token::Star if self.peek(1) == &Token::Rbracket => {
117                    self.advance();
118                    self.parse_wildcard_index(Box::new(Ast::Identity { offset }))
119                }
120                _ => self.parse_multi_list(),
121            },
122            Token::Flatten => self.parse_flatten(Box::new(Ast::Identity { offset })),
123            Token::Lbrace => {
124                let mut pairs = vec![];
125                loop {
126                    pairs.push(self.parse_kvp()?);
127                    match self.advance() {
128                        Token::Rbrace => break,
129                        Token::Comma => continue,
130                        ref t => return Err(self.err(t, "Expected '}' or ','", false)),
131                    }
132                }
133                Ok(Ast::MultiHash {
134                    elements: pairs,
135                    offset,
136                })
137            }
138            t @ Token::Ampersand => {
139                let rhs = self.expr(t.lbp())?;
140                Ok(Ast::Expref {
141                    ast: Box::new(rhs),
142                    offset,
143                })
144            }
145            t @ Token::Not => Ok(Ast::Not {
146                node: Box::new(self.expr(t.lbp())?),
147                offset,
148            }),
149            Token::Filter => self.parse_filter(Box::new(Ast::Identity { offset })),
150            Token::Lparen => {
151                let result = self.expr(0)?;
152                match self.advance() {
153                    Token::Rparen => Ok(result),
154                    ref t => Err(self.err(t, "Expected ')' to close '('", false)),
155                }
156            }
157            #[cfg(feature = "let-expr")]
158            Token::Variable(name) => Ok(Ast::VariableRef {
159                name: name.to_owned(),
160                offset,
161            }),
162            ref t => Err(self.err(t, "Unexpected nud token", false)),
163        }
164    }
165
166    fn led(&mut self, left: Box<Ast>) -> ParseResult {
167        let (offset, token) = self.advance_with_pos();
168        match token {
169            t @ Token::Dot => {
170                if self.peek(0) == &Token::Star {
171                    self.advance();
172                    self.parse_wildcard_values(left)
173                } else {
174                    let rhs = self.parse_dot(t.lbp())?;
175                    Ok(Ast::Subexpr {
176                        offset,
177                        lhs: left,
178                        rhs: Box::new(rhs),
179                    })
180                }
181            }
182            Token::Lbracket => {
183                if match self.peek(0) {
184                    &Token::Number(_) | &Token::Colon => true,
185                    &Token::Star => false,
186                    t => return Err(self.err(t, "Expected number, ':', or '*'", true)),
187                } {
188                    Ok(Ast::Subexpr {
189                        offset,
190                        lhs: left,
191                        rhs: Box::new(self.parse_index()?),
192                    })
193                } else {
194                    self.advance();
195                    self.parse_wildcard_index(left)
196                }
197            }
198            t @ Token::Or => {
199                let rhs = self.expr(t.lbp())?;
200                Ok(Ast::Or {
201                    offset,
202                    lhs: left,
203                    rhs: Box::new(rhs),
204                })
205            }
206            t @ Token::And => {
207                let rhs = self.expr(t.lbp())?;
208                Ok(Ast::And {
209                    offset,
210                    lhs: left,
211                    rhs: Box::new(rhs),
212                })
213            }
214            t @ Token::Pipe => {
215                let rhs = self.expr(t.lbp())?;
216                Ok(Ast::Subexpr {
217                    offset,
218                    lhs: left,
219                    rhs: Box::new(rhs),
220                })
221            }
222            Token::Lparen => match *left {
223                Ast::Field { name: v, .. } => Ok(Ast::Function {
224                    offset,
225                    name: v,
226                    args: self.parse_list(Token::Rparen)?,
227                }),
228                _ => Err(self.err(self.peek(0), "Invalid function name", true)),
229            },
230            Token::Flatten => self.parse_flatten(left),
231            Token::Filter => self.parse_filter(left),
232            Token::Eq => self.parse_comparator(Comparator::Equal, left),
233            Token::Ne => self.parse_comparator(Comparator::NotEqual, left),
234            Token::Gt => self.parse_comparator(Comparator::GreaterThan, left),
235            Token::Gte => self.parse_comparator(Comparator::GreaterThanEqual, left),
236            Token::Lt => self.parse_comparator(Comparator::LessThan, left),
237            Token::Lte => self.parse_comparator(Comparator::LessThanEqual, left),
238            ref t => Err(self.err(t, "Unexpected led token", false)),
239        }
240    }
241
242    #[cfg(feature = "let-expr")]
243    fn parse_let(&mut self, offset: usize) -> ParseResult {
244        let mut bindings = vec![];
245        loop {
246            match self.peek(0) {
247                Token::Variable(_) => {
248                    let var_name = match self.advance() {
249                        Token::Variable(name) => name.to_owned(),
250                        _ => unreachable!(),
251                    };
252                    match self.advance() {
253                        Token::Assign => {}
254                        ref t => {
255                            return Err(self.err(
256                                t,
257                                "Expected '=' after variable in let binding",
258                                false,
259                            ));
260                        }
261                    }
262                    let value = self.parse_let_binding_expr()?;
263                    bindings.push((var_name, value));
264                    match self.peek(0) {
265                        Token::Comma => {
266                            self.advance();
267                        }
268                        Token::Identifier(s) if *s == "in" => {
269                            break;
270                        }
271                        t => {
272                            return Err(self.err(
273                                t,
274                                "Expected ',' or 'in' after let binding",
275                                true,
276                            ));
277                        }
278                    }
279                }
280                t => {
281                    return Err(self.err(t, "Expected variable binding ($name) after 'let'", true));
282                }
283            }
284        }
285        match self.advance() {
286            Token::Identifier(s) if *s == *"in" => {}
287            ref t => {
288                return Err(self.err(t, "Expected 'in' keyword after let bindings", false));
289            }
290        }
291        let body = self.expr(0)?;
292        Ok(Ast::Let {
293            offset,
294            bindings,
295            expr: Box::new(body),
296        })
297    }
298
299    #[cfg(feature = "let-expr")]
300    fn parse_let_binding_expr(&mut self) -> ParseResult {
301        self.parse_let_binding_expr_bp(0)
302    }
303
304    #[cfg(feature = "let-expr")]
305    fn parse_let_binding_expr_bp(&mut self, rbp: usize) -> ParseResult {
306        let mut left = self.nud();
307        loop {
308            match self.peek(0) {
309                Token::Comma => break,
310                Token::Identifier(s) if *s == "in" => break,
311                _ => {}
312            }
313            if rbp >= self.peek(0).lbp() {
314                break;
315            }
316            left = self.led(Box::new(left?));
317        }
318        left
319    }
320
321    fn parse_kvp(&mut self) -> Result<KeyValuePair, JmespathError> {
322        match self.advance() {
323            Token::Identifier(value) => {
324                if self.peek(0) == &Token::Colon {
325                    self.advance();
326                    Ok(KeyValuePair {
327                        key: value.to_owned(),
328                        value: self.expr(0)?,
329                    })
330                } else {
331                    Err(self.err(self.peek(0), "Expected ':' to follow key", true))
332                }
333            }
334            Token::QuotedIdentifier(value) => {
335                if self.peek(0) == &Token::Colon {
336                    self.advance();
337                    Ok(KeyValuePair {
338                        key: value,
339                        value: self.expr(0)?,
340                    })
341                } else {
342                    Err(self.err(self.peek(0), "Expected ':' to follow key", true))
343                }
344            }
345            ref t => Err(self.err(t, "Expected Field to start key value pair", false)),
346        }
347    }
348
349    fn parse_filter(&mut self, lhs: Box<Ast>) -> ParseResult {
350        let condition_lhs = Box::new(self.expr(0)?);
351        match self.advance() {
352            Token::Rbracket => {
353                let condition_rhs = Box::new(self.projection_rhs(Token::Filter.lbp())?);
354                Ok(Ast::Projection {
355                    offset: self.offset,
356                    lhs,
357                    rhs: Box::new(Ast::Condition {
358                        offset: self.offset,
359                        predicate: condition_lhs,
360                        then: condition_rhs,
361                    }),
362                })
363            }
364            ref t => Err(self.err(t, "Expected ']'", false)),
365        }
366    }
367
368    fn parse_flatten(&mut self, lhs: Box<Ast>) -> ParseResult {
369        let rhs = Box::new(self.projection_rhs(Token::Flatten.lbp())?);
370        Ok(Ast::Projection {
371            offset: self.offset,
372            lhs: Box::new(Ast::Flatten {
373                offset: self.offset,
374                node: lhs,
375            }),
376            rhs,
377        })
378    }
379
380    fn parse_comparator(&mut self, cmp: Comparator, lhs: Box<Ast>) -> ParseResult {
381        let rhs = Box::new(self.expr(Token::Eq.lbp())?);
382        Ok(Ast::Comparison {
383            offset: self.offset,
384            comparator: cmp,
385            lhs,
386            rhs,
387        })
388    }
389
390    fn parse_dot(&mut self, lbp: usize) -> ParseResult {
391        if match self.peek(0) {
392            &Token::Lbracket => true,
393            &Token::Identifier(_)
394            | &Token::QuotedIdentifier(_)
395            | &Token::Star
396            | &Token::Lbrace
397            | &Token::Ampersand => false,
398            t => return Err(self.err(t, "Expected identifier, '*', '{', '[', '&', or '[?'", true)),
399        } {
400            self.advance();
401            self.parse_multi_list()
402        } else {
403            self.expr(lbp)
404        }
405    }
406
407    fn projection_rhs(&mut self, lbp: usize) -> ParseResult {
408        if match self.peek(0) {
409            &Token::Dot => true,
410            &Token::Lbracket | &Token::Filter => false,
411            t if t.lbp() < PROJECTION_STOP => {
412                return Ok(Ast::Identity {
413                    offset: self.offset,
414                });
415            }
416            t => {
417                return Err(self.err(t, "Expected '.', '[', or '[?'", true));
418            }
419        } {
420            self.advance();
421            self.parse_dot(lbp)
422        } else {
423            self.expr(lbp)
424        }
425    }
426
427    fn parse_wildcard_index(&mut self, lhs: Box<Ast>) -> ParseResult {
428        match self.advance() {
429            Token::Rbracket => {
430                let rhs = Box::new(self.projection_rhs(Token::Star.lbp())?);
431                Ok(Ast::Projection {
432                    offset: self.offset,
433                    lhs,
434                    rhs,
435                })
436            }
437            ref t => Err(self.err(t, "Expected ']' for wildcard index", false)),
438        }
439    }
440
441    fn parse_wildcard_values(&mut self, lhs: Box<Ast>) -> ParseResult {
442        let rhs = Box::new(self.projection_rhs(Token::Star.lbp())?);
443        Ok(Ast::Projection {
444            offset: self.offset,
445            lhs: Box::new(Ast::ObjectValues {
446                offset: self.offset,
447                node: lhs,
448            }),
449            rhs,
450        })
451    }
452
453    fn parse_index(&mut self) -> ParseResult {
454        let mut parts = [None, None, None];
455        let mut pos = 0;
456        loop {
457            match self.advance() {
458                Token::Number(value) => {
459                    parts[pos] = Some(value);
460                    match self.peek(0) {
461                        &Token::Colon | &Token::Rbracket => (),
462                        t => return Err(self.err(t, "Expected ':', or ']'", true)),
463                    };
464                }
465                Token::Rbracket => break,
466                Token::Colon if pos >= 2 => {
467                    return Err(self.err(&Token::Colon, "Too many colons in slice expr", false));
468                }
469                Token::Colon => {
470                    pos += 1;
471                    match self.peek(0) {
472                        &Token::Number(_) | &Token::Colon | &Token::Rbracket => continue,
473                        t => return Err(self.err(t, "Expected number, ':', or ']'", true)),
474                    };
475                }
476                ref t => return Err(self.err(t, "Expected number, ':', or ']'", false)),
477            }
478        }
479
480        if pos == 0 {
481            Ok(Ast::Index {
482                offset: self.offset,
483                idx: parts[0].ok_or_else(|| {
484                    JmespathError::new(
485                        self.expr,
486                        self.offset,
487                        ErrorReason::Parse(
488                            "Expected parts[0] to be Some; but found None".to_owned(),
489                        ),
490                    )
491                })?,
492            })
493        } else {
494            Ok(Ast::Projection {
495                offset: self.offset,
496                lhs: Box::new(Ast::Slice {
497                    offset: self.offset,
498                    start: parts[0],
499                    stop: parts[1],
500                    step: parts[2].unwrap_or(1),
501                }),
502                rhs: Box::new(self.projection_rhs(Token::Star.lbp())?),
503            })
504        }
505    }
506
507    fn parse_multi_list(&mut self) -> ParseResult {
508        Ok(Ast::MultiList {
509            offset: self.offset,
510            elements: self.parse_list(Token::Rbracket)?,
511        })
512    }
513
514    fn parse_list(&mut self, closing: Token<'_>) -> Result<Vec<Ast>, JmespathError> {
515        let mut nodes = vec![];
516        while self.peek(0) != &closing {
517            nodes.push(self.expr(0)?);
518            if self.peek(0) == &Token::Comma {
519                self.advance();
520                if self.peek(0) == &closing {
521                    return Err(self.err(self.peek(0), "invalid token after ','", true));
522                }
523            }
524        }
525        self.advance();
526        Ok(nodes)
527    }
528}
529
530#[cfg(test)]
531mod tests {
532    use super::*;
533    use crate::ast::Comparator;
534
535    #[test]
536    fn parse_field() {
537        let ast = parse("foo").unwrap();
538        assert!(matches!(ast, Ast::Field { name, .. } if name == "foo"));
539    }
540
541    #[test]
542    fn parse_identity() {
543        let ast = parse("@").unwrap();
544        assert!(matches!(ast, Ast::Identity { .. }));
545    }
546
547    #[test]
548    fn parse_subexpr() {
549        let ast = parse("foo.bar").unwrap();
550        assert!(matches!(ast, Ast::Subexpr { .. }));
551    }
552
553    #[test]
554    fn parse_deeply_nested_subexpr() {
555        let ast = parse("a.b.c.d.e").unwrap();
556        assert!(matches!(ast, Ast::Subexpr { .. }));
557    }
558
559    #[test]
560    fn parse_index_positive() {
561        let ast = parse("[0]").unwrap();
562        assert!(matches!(ast, Ast::Index { idx: 0, .. }));
563    }
564
565    #[test]
566    fn parse_index_negative() {
567        let ast = parse("[-1]").unwrap();
568        assert!(matches!(ast, Ast::Index { idx: -1, .. }));
569    }
570
571    #[test]
572    fn parse_slice_basic() {
573        let ast = parse("[0:5]").unwrap();
574        match ast {
575            Ast::Projection { lhs, .. } => {
576                assert!(matches!(
577                    *lhs,
578                    Ast::Slice {
579                        start: Some(0),
580                        stop: Some(5),
581                        step: 1,
582                        ..
583                    }
584                ));
585            }
586            _ => panic!("expected Projection with Slice lhs"),
587        }
588    }
589
590    #[test]
591    fn parse_slice_with_step() {
592        let ast = parse("[::2]").unwrap();
593        match ast {
594            Ast::Projection { lhs, .. } => {
595                assert!(matches!(
596                    *lhs,
597                    Ast::Slice {
598                        start: None,
599                        stop: None,
600                        step: 2,
601                        ..
602                    }
603                ));
604            }
605            _ => panic!("expected Projection with Slice lhs"),
606        }
607    }
608
609    #[test]
610    fn parse_slice_negative_step() {
611        let ast = parse("[::-1]").unwrap();
612        match ast {
613            Ast::Projection { lhs, .. } => {
614                assert!(matches!(*lhs, Ast::Slice { step: -1, .. }));
615            }
616            _ => panic!("expected Projection with Slice lhs"),
617        }
618    }
619
620    #[test]
621    fn parse_wildcard_values() {
622        let ast = parse("*").unwrap();
623        assert!(matches!(ast, Ast::Projection { .. }));
624    }
625
626    #[test]
627    fn parse_wildcard_index() {
628        let ast = parse("[*]").unwrap();
629        assert!(matches!(ast, Ast::Projection { .. }));
630    }
631
632    #[test]
633    fn parse_flatten() {
634        let ast = parse("[]").unwrap();
635        match ast {
636            Ast::Projection { lhs, .. } => {
637                assert!(matches!(*lhs, Ast::Flatten { .. }));
638            }
639            _ => panic!("expected Projection with Flatten"),
640        }
641    }
642
643    #[test]
644    fn parse_filter() {
645        let ast = parse("[?a > `1`]").unwrap();
646        assert!(matches!(ast, Ast::Projection { .. }));
647    }
648
649    #[test]
650    fn parse_or() {
651        let ast = parse("a || b").unwrap();
652        assert!(matches!(ast, Ast::Or { .. }));
653    }
654
655    #[test]
656    fn parse_and() {
657        let ast = parse("a && b").unwrap();
658        assert!(matches!(ast, Ast::And { .. }));
659    }
660
661    #[test]
662    fn parse_not() {
663        let ast = parse("!a").unwrap();
664        assert!(matches!(ast, Ast::Not { .. }));
665    }
666
667    #[test]
668    fn parse_pipe() {
669        let ast = parse("a | b").unwrap();
670        assert!(matches!(ast, Ast::Subexpr { .. }));
671    }
672
673    #[test]
674    fn parse_function_call() {
675        let ast = parse("length(@)").unwrap();
676        match ast {
677            Ast::Function { name, args, .. } => {
678                assert_eq!(name, "length");
679                assert_eq!(args.len(), 1);
680            }
681            _ => panic!("expected Function"),
682        }
683    }
684
685    #[test]
686    fn parse_multi_list() {
687        let ast = parse("[a, b, c]").unwrap();
688        match ast {
689            Ast::MultiList { elements, .. } => {
690                assert_eq!(elements.len(), 3);
691            }
692            _ => panic!("expected MultiList"),
693        }
694    }
695
696    #[test]
697    fn parse_multi_hash() {
698        let ast = parse("{a: b, c: d}").unwrap();
699        match ast {
700            Ast::MultiHash { elements, .. } => {
701                assert_eq!(elements.len(), 2);
702            }
703            _ => panic!("expected MultiHash"),
704        }
705    }
706
707    #[test]
708    fn parse_literal_string() {
709        let ast = parse("`\"hello\"`").unwrap();
710        assert!(matches!(ast, Ast::Literal { .. }));
711    }
712
713    #[test]
714    fn parse_literal_number() {
715        let ast = parse("`42`").unwrap();
716        assert!(matches!(ast, Ast::Literal { .. }));
717    }
718
719    #[test]
720    fn parse_literal_null() {
721        let ast = parse("`null`").unwrap();
722        assert!(matches!(ast, Ast::Literal { .. }));
723    }
724
725    #[test]
726    fn parse_raw_string() {
727        let ast = parse("'hello'").unwrap();
728        match ast {
729            Ast::Literal { value, .. } => {
730                assert_eq!(value, serde_json::json!("hello"));
731            }
732            _ => panic!("expected Literal from raw string"),
733        }
734    }
735
736    #[test]
737    fn parse_expref() {
738        let ast = parse("&foo").unwrap();
739        assert!(matches!(ast, Ast::Expref { .. }));
740    }
741
742    #[test]
743    fn parse_all_comparators() {
744        for (expr, cmp) in [
745            ("a == b", Comparator::Equal),
746            ("a != b", Comparator::NotEqual),
747            ("a > b", Comparator::GreaterThan),
748            ("a >= b", Comparator::GreaterThanEqual),
749            ("a < b", Comparator::LessThan),
750            ("a <= b", Comparator::LessThanEqual),
751        ] {
752            let ast = parse(expr).unwrap();
753            match ast {
754                Ast::Comparison { comparator, .. } => assert_eq!(comparator, cmp, "for {expr}"),
755                _ => panic!("expected Comparison for {expr}"),
756            }
757        }
758    }
759
760    #[test]
761    fn parse_quoted_identifier() {
762        let ast = parse("\"foo bar\"").unwrap();
763        match ast {
764            Ast::Field { name, .. } => assert_eq!(name, "foo bar"),
765            _ => panic!("expected Field from quoted identifier"),
766        }
767    }
768
769    #[test]
770    fn parse_parenthesized_expression() {
771        let ast = parse("(a)").unwrap();
772        assert!(matches!(ast, Ast::Field { .. }));
773    }
774
775    #[test]
776    fn error_empty_expression() {
777        let result = parse("");
778        assert!(result.is_err());
779    }
780
781    #[test]
782    fn error_unclosed_bracket() {
783        let result = parse("[0");
784        assert!(result.is_err());
785    }
786
787    #[test]
788    fn error_trailing_garbage() {
789        let result = parse("foo bar");
790        assert!(result.is_err());
791    }
792
793    #[test]
794    fn error_quoted_string_as_function() {
795        let result = parse("\"foo\"()");
796        assert!(result.is_err());
797    }
798
799    #[test]
800    fn error_trailing_comma_in_list() {
801        let result = parse("[a, b,]");
802        assert!(result.is_err());
803    }
804
805    #[cfg(feature = "let-expr")]
806    #[test]
807    fn parse_let_expression() {
808        let ast = parse("let $x = `1` in $x").unwrap();
809        assert!(matches!(ast, Ast::Let { .. }));
810    }
811
812    #[cfg(feature = "let-expr")]
813    #[test]
814    fn parse_variable_ref() {
815        // This will parse fine, but searching will fail (no scope)
816        let ast = parse("$x").unwrap();
817        assert!(matches!(ast, Ast::VariableRef { .. }));
818    }
819
820    #[cfg(feature = "let-expr")]
821    #[test]
822    fn error_let_missing_in() {
823        let result = parse("let $x = `1` $x");
824        assert!(result.is_err());
825    }
826}