Skip to main content

caixa_ast/
parser.rs

1//! Top-down parser — consumes the lexer's token stream, emits [`Node`]s with
2//! leading trivia attached.
3
4use thiserror::Error;
5
6use crate::lexer::{LexError, Token, TokenKind, tokenize};
7use crate::node::{Node, NodeKind};
8use crate::span::Span;
9use crate::trivia::{Trivia, TriviaKind};
10
11#[derive(Debug, Error)]
12pub enum ParseError {
13    #[error("lexer: {0}")]
14    Lex(#[from] LexError),
15    #[error("unexpected token {kind:?} at {span}")]
16    Unexpected { kind: TokenKind, span: Span },
17    #[error("unexpected end of input")]
18    Eof,
19    #[error("unmatched ')' at {0}")]
20    UnmatchedClose(Span),
21    #[error("reader macro ({0}) without a following form at {1}")]
22    DanglingReader(&'static str, Span),
23}
24
25pub fn parse(src: &str) -> Result<Vec<Node>, ParseError> {
26    let tokens = tokenize(src)?;
27    let mut p = Parser {
28        tokens: &tokens,
29        pos: 0,
30    };
31    let mut out: Vec<Node> = Vec::new();
32    loop {
33        let mut leading = p.consume_trivia();
34
35        // A comment on the SAME LINE as the preceding form belongs to that
36        // form, not to whatever comes next. Without this split, `; first`
37        // in
38        //
39        //     (define a 1) ; first
40        //     (define b 2)
41        //
42        // became the LEADING trivia of `(define b 2)` and was re-emitted on
43        // its own line above it — so a note about `a` silently turned into
44        // a note about `b`. The comment survived; its meaning did not.
45        // Deciding by an actual newline in the source (rather than by
46        // guessing) keeps this exact and total.
47        if let Some(prev_end) = out.last().map(|n: &Node| n.span.end as usize) {
48            let own_line = leading
49                .iter()
50                .position(|t| {
51                    let start = t.span.start as usize;
52                    start >= prev_end && src[prev_end..start].contains('\n')
53                })
54                .unwrap_or(leading.len());
55            if own_line > 0 {
56                let same_line: Vec<_> = leading.drain(..own_line).collect();
57                if let Some(last) = out.last_mut() {
58                    last.after.extend(same_line);
59                }
60            }
61        }
62
63        if p.peek().is_none() {
64            // Trivia before EOF has no following node to lead, so it used
65            // to be DROPPED here — silently deleting any comment at the
66            // end of a file, and any comment trailing the final form.
67            // Park it after the last node so it survives the round trip.
68            if let Some(last) = out.last_mut() {
69                last.after.extend(leading);
70            }
71            break;
72        }
73        let mut node = p.node()?;
74        if node.leading.is_empty() {
75            node.leading = leading;
76        } else {
77            // uncommon, but merge
78            let mut combined = leading;
79            combined.extend(node.leading.drain(..));
80            node.leading = combined;
81        }
82        out.push(node);
83    }
84    Ok(out)
85}
86
87struct Parser<'a> {
88    tokens: &'a [Token],
89    pos: usize,
90}
91
92impl<'a> Parser<'a> {
93    fn peek(&self) -> Option<&'a Token> {
94        self.tokens.get(self.pos)
95    }
96
97    fn bump(&mut self) -> Option<&'a Token> {
98        let t = self.tokens.get(self.pos)?;
99        self.pos += 1;
100        Some(t)
101    }
102
103    /// Collect leading comments / blank-line markers. Whitespace is dropped.
104    fn consume_trivia(&mut self) -> Vec<Trivia> {
105        let mut out = Vec::new();
106        while let Some(tok) = self.peek() {
107            match &tok.kind {
108                TokenKind::Shebang(s) => {
109                    out.push(Trivia {
110                        kind: TriviaKind::Shebang(s.clone()),
111                        span: tok.span,
112                    });
113                    self.pos += 1;
114                }
115                TokenKind::LineComment(s) => {
116                    out.push(Trivia {
117                        kind: TriviaKind::LineComment(s.clone()),
118                        span: tok.span,
119                    });
120                    self.pos += 1;
121                }
122                TokenKind::Newlines(n) if *n >= 2 => {
123                    out.push(Trivia {
124                        kind: TriviaKind::BlankLine,
125                        span: tok.span,
126                    });
127                    self.pos += 1;
128                }
129                TokenKind::Newlines(_) | TokenKind::Whitespace => {
130                    self.pos += 1;
131                }
132                _ => break,
133            }
134        }
135        out
136    }
137
138    fn node(&mut self) -> Result<Node, ParseError> {
139        let tok = self.peek().ok_or(ParseError::Eof)?;
140        let span = tok.span;
141        match &tok.kind {
142            TokenKind::LParen => self.sequence(&TokenKind::RParen, NodeKind::List),
143            TokenKind::LBrace => self.sequence(&TokenKind::RBrace, NodeKind::Map),
144            TokenKind::LBracket => self.sequence(&TokenKind::RBracket, NodeKind::Vector),
145            TokenKind::RParen | TokenKind::RBrace | TokenKind::RBracket => {
146                Err(ParseError::UnmatchedClose(span))
147            }
148            TokenKind::Quote => self.reader_macro("quote", |n| NodeKind::Quote(Box::new(n))),
149            TokenKind::Quasiquote => {
150                self.reader_macro("quasiquote", |n| NodeKind::Quasiquote(Box::new(n)))
151            }
152            TokenKind::Unquote => self.reader_macro("unquote", |n| NodeKind::Unquote(Box::new(n))),
153            TokenKind::UnquoteSplice => {
154                self.reader_macro("unquote-splicing", |n| NodeKind::UnquoteSplice(Box::new(n)))
155            }
156            TokenKind::Str(s) => {
157                let s = s.clone();
158                self.pos += 1;
159                Ok(Node::new(NodeKind::Str(s), span))
160            }
161            TokenKind::Int(i) => {
162                let i = *i;
163                self.pos += 1;
164                Ok(Node::new(NodeKind::Int(i), span))
165            }
166            TokenKind::Float(f) => {
167                let f = *f;
168                self.pos += 1;
169                Ok(Node::new(NodeKind::Float(f), span))
170            }
171            TokenKind::Bool(b) => {
172                let b = *b;
173                self.pos += 1;
174                Ok(Node::new(NodeKind::Bool(b), span))
175            }
176            TokenKind::Nil => {
177                self.pos += 1;
178                Ok(Node::new(NodeKind::Nil, span))
179            }
180            TokenKind::Symbol(s) => {
181                let s = s.clone();
182                self.pos += 1;
183                Ok(Node::new(NodeKind::Symbol(s), span))
184            }
185            TokenKind::Keyword(s) => {
186                let s = s.clone();
187                self.pos += 1;
188                Ok(Node::new(NodeKind::Keyword(s), span))
189            }
190            kind => Err(ParseError::Unexpected {
191                kind: kind.clone(),
192                span,
193            }),
194        }
195    }
196
197    /// Parse a delimited sequence: `(…)`, `{…}` or `[…]`.
198    ///
199    /// One routine for all three because they differ only in their
200    /// closing token and the `NodeKind` they build — the trivia rules,
201    /// the dangling-comment handling and the EOF error are identical, and
202    /// duplicating them per delimiter is how the three drift apart.
203    fn sequence(
204        &mut self,
205        close_kind: &TokenKind,
206        wrap: fn(Vec<Node>) -> NodeKind,
207    ) -> Result<Node, ParseError> {
208        let open = self.bump().expect("opening delimiter").span;
209        let mut items = Vec::new();
210        loop {
211            let leading = self.consume_trivia();
212            let next = self.peek();
213            match next {
214                None => return Err(ParseError::Eof),
215                Some(tok) if tok.kind == *close_kind => {
216                    let close = self.bump().expect("closing delimiter").span;
217                    let span = open.union(close);
218                    let mut node = Node::new(wrap(items), span);
219                    // the sequence's own leading trivia is handled at the caller
220                    node.leading = Vec::new();
221                    // Trivia sitting between the last item and the closer has
222                    // no child to attach to. It used to be dropped here, which
223                    // silently deleted the last comment of every form the
224                    // formatter round-tripped. Park it on the list's own
225                    // `trailing` — the one slot the parser never otherwise
226                    // fills — so the printer can re-emit it before the `)`.
227                    node.trailing = leading;
228                    return Ok(node);
229                }
230                Some(_) => {
231                    let mut child = self.node()?;
232                    if child.leading.is_empty() {
233                        child.leading = leading;
234                    }
235                    items.push(child);
236                }
237            }
238        }
239    }
240
241    fn reader_macro(
242        &mut self,
243        name: &'static str,
244        wrap: impl FnOnce(Node) -> NodeKind,
245    ) -> Result<Node, ParseError> {
246        let head = self.bump().expect("reader macro token").span;
247        self.consume_trivia();
248        let inner = self.peek().ok_or(ParseError::DanglingReader(name, head))?;
249        let _ = inner;
250        let target = self.node()?;
251        let span = head.union(target.span);
252        Ok(Node::new(wrap(target), span))
253    }
254}
255
256#[cfg(test)]
257mod tests {
258    use super::*;
259
260    #[test]
261    fn parse_atom() {
262        let nodes = parse("42").unwrap();
263        assert_eq!(nodes.len(), 1);
264        assert!(matches!(nodes[0].kind, NodeKind::Int(42)));
265        assert_eq!(nodes[0].span, Span::new(0, 2));
266    }
267
268    #[test]
269    fn parse_list() {
270        let nodes = parse("(a b c)").unwrap();
271        assert_eq!(nodes.len(), 1);
272        let Some(items) = nodes[0].kind.as_list() else {
273            panic!("expected list");
274        };
275        assert_eq!(items.len(), 3);
276        assert!(matches!(items[0].kind, NodeKind::Symbol(ref s) if s == "a"));
277    }
278
279    /// The brace/vector dialect (D4). Before caixa-ast had these tokens
280    /// they fell through to the Symbol regex, so `{` and `}` parsed as
281    /// ordinary symbols and every nested map became a flat odd-length run.
282    #[test]
283    fn parse_map_and_vector() {
284        let nodes =
285            parse(r#"(defcaixa demo :package { :name "d" } :workflows [ :a :b ])"#).unwrap();
286        let Some(items) = nodes[0].kind.as_list() else {
287            panic!("expected list")
288        };
289        // head, name, :package, {…}, :workflows, [ … ]  — SIX items, not
290        // the eleven you get when the delimiters are their own symbols.
291        assert_eq!(items.len(), 6, "got {items:#?}");
292
293        let NodeKind::Map(m) = &items[3].kind else {
294            panic!("expected map, got {:?}", items[3].kind)
295        };
296        assert_eq!(m.len(), 2);
297        assert!(matches!(&m[0].kind, NodeKind::Keyword(k) if k == "name"));
298
299        let NodeKind::Vector(v) = &items[5].kind else {
300            panic!("expected vector, got {:?}", items[5].kind)
301        };
302        assert_eq!(v.len(), 2);
303        assert!(matches!(&v[0].kind, NodeKind::Keyword(k) if k == "a"));
304    }
305
306    /// Delimiters terminate atoms, so no whitespace is required around
307    /// them. `{:name` must be LBrace + Keyword, never one symbol.
308    #[test]
309    fn delimiters_terminate_atoms_without_whitespace() {
310        let nodes = parse(r"{:a 1}").unwrap();
311        let NodeKind::Map(m) = &nodes[0].kind else {
312            panic!("expected map, got {:?}", nodes[0].kind)
313        };
314        assert_eq!(m.len(), 2);
315        assert!(matches!(&m[0].kind, NodeKind::Keyword(k) if k == "a"));
316        assert!(matches!(m[1].kind, NodeKind::Int(1)));
317
318        let nodes = parse(r"[a b]").unwrap();
319        let NodeKind::Vector(v) = &nodes[0].kind else {
320            panic!("expected vector")
321        };
322        assert_eq!(v.len(), 2);
323        assert!(matches!(&v[1].kind, NodeKind::Symbol(s) if s == "b"));
324    }
325
326    #[test]
327    fn unmatched_closing_delimiters_are_rejected() {
328        for src in ["}", "]", ")"] {
329            assert!(
330                matches!(parse(src), Err(ParseError::UnmatchedClose(_))),
331                "{src:?} must be an unmatched-close error"
332            );
333        }
334        for src in ["{", "[", "("] {
335            assert!(
336                matches!(parse(src), Err(ParseError::Eof)),
337                "{src:?} must be an EOF error"
338            );
339        }
340    }
341
342    #[test]
343    fn parse_kwargs() {
344        let nodes = parse(r#"(defcaixa :nome "demo" :versao "0.1.0")"#).unwrap();
345        assert_eq!(nodes[0].head_symbol(), Some("defcaixa"));
346        assert!(matches!(
347            nodes[0].kwarg("nome").map(|n| &n.kind),
348            Some(NodeKind::Str(s)) if s == "demo"
349        ));
350    }
351
352    #[test]
353    fn parse_nested_with_comments() {
354        let src = r#"
355;; leading doc
356(defcaixa
357  :nome "demo"
358  ;; inline note
359  :versao "0.1.0")
360"#;
361        let nodes = parse(src).unwrap();
362        assert_eq!(nodes.len(), 1);
363        assert!(!nodes[0].leading.is_empty());
364        // inline comment is trivia attached to the next kwarg
365    }
366
367    #[test]
368    fn parse_reader_macros() {
369        let nodes = parse("`(a ,b ,@cs)").unwrap();
370        let NodeKind::Quasiquote(inner) = &nodes[0].kind else {
371            panic!("expected quasiquote");
372        };
373        let Some(items) = inner.kind.as_list() else {
374            panic!("expected list inside quasiquote");
375        };
376        assert_eq!(items.len(), 3);
377        assert!(matches!(items[1].kind, NodeKind::Unquote(_)));
378        assert!(matches!(items[2].kind, NodeKind::UnquoteSplice(_)));
379    }
380
381    #[test]
382    fn to_tatara_sexp_equivalence() {
383        use tatara_lisp::{Atom, Sexp};
384        let src = r#"(defcaixa :nome "demo" :kind Biblioteca)"#;
385        let nodes = parse(src).unwrap();
386        let lowered = nodes[0].to_tatara_sexp();
387        match lowered {
388            Sexp::List(items) => {
389                assert_eq!(items.len(), 5);
390                assert!(matches!(items[0], Sexp::Atom(Atom::Symbol(ref s)) if s == "defcaixa"));
391                assert!(matches!(items[1], Sexp::Atom(Atom::Keyword(ref s)) if s == "nome"));
392                assert!(matches!(items[2], Sexp::Atom(Atom::Str(ref s)) if s == "demo"));
393                assert!(matches!(items[3], Sexp::Atom(Atom::Keyword(ref s)) if s == "kind"));
394                assert!(matches!(items[4], Sexp::Atom(Atom::Symbol(ref s)) if s == "Biblioteca"));
395            }
396            other => panic!("expected List, got {other:?}"),
397        }
398    }
399}