use thiserror::Error;
use crate::lexer::{LexError, Token, TokenKind, tokenize};
use crate::node::{Node, NodeKind};
use crate::span::Span;
use crate::trivia::{Trivia, TriviaKind};
#[derive(Debug, Error)]
pub enum ParseError {
#[error("lexer: {0}")]
Lex(#[from] LexError),
#[error("unexpected token {kind:?} at {span}")]
Unexpected { kind: TokenKind, span: Span },
#[error("unexpected end of input")]
Eof,
#[error("unmatched ')' at {0}")]
UnmatchedClose(Span),
#[error("reader macro ({0}) without a following form at {1}")]
DanglingReader(&'static str, Span),
}
pub fn parse(src: &str) -> Result<Vec<Node>, ParseError> {
let tokens = tokenize(src)?;
let mut p = Parser {
tokens: &tokens,
pos: 0,
};
let mut out: Vec<Node> = Vec::new();
loop {
let mut leading = p.consume_trivia();
if let Some(prev_end) = out.last().map(|n: &Node| n.span.end as usize) {
let own_line = leading
.iter()
.position(|t| {
let start = t.span.start as usize;
start >= prev_end && src[prev_end..start].contains('\n')
})
.unwrap_or(leading.len());
if own_line > 0 {
let same_line: Vec<_> = leading.drain(..own_line).collect();
if let Some(last) = out.last_mut() {
last.after.extend(same_line);
}
}
}
if p.peek().is_none() {
if let Some(last) = out.last_mut() {
last.after.extend(leading);
}
break;
}
let mut node = p.node()?;
if node.leading.is_empty() {
node.leading = leading;
} else {
let mut combined = leading;
combined.extend(node.leading.drain(..));
node.leading = combined;
}
out.push(node);
}
Ok(out)
}
struct Parser<'a> {
tokens: &'a [Token],
pos: usize,
}
impl<'a> Parser<'a> {
fn peek(&self) -> Option<&'a Token> {
self.tokens.get(self.pos)
}
fn bump(&mut self) -> Option<&'a Token> {
let t = self.tokens.get(self.pos)?;
self.pos += 1;
Some(t)
}
fn consume_trivia(&mut self) -> Vec<Trivia> {
let mut out = Vec::new();
while let Some(tok) = self.peek() {
match &tok.kind {
TokenKind::Shebang(s) => {
out.push(Trivia {
kind: TriviaKind::Shebang(s.clone()),
span: tok.span,
});
self.pos += 1;
}
TokenKind::LineComment(s) => {
out.push(Trivia {
kind: TriviaKind::LineComment(s.clone()),
span: tok.span,
});
self.pos += 1;
}
TokenKind::Newlines(n) if *n >= 2 => {
out.push(Trivia {
kind: TriviaKind::BlankLine,
span: tok.span,
});
self.pos += 1;
}
TokenKind::Newlines(_) | TokenKind::Whitespace => {
self.pos += 1;
}
_ => break,
}
}
out
}
fn node(&mut self) -> Result<Node, ParseError> {
let tok = self.peek().ok_or(ParseError::Eof)?;
let span = tok.span;
match &tok.kind {
TokenKind::LParen => self.sequence(&TokenKind::RParen, NodeKind::List),
TokenKind::LBrace => self.sequence(&TokenKind::RBrace, NodeKind::Map),
TokenKind::LBracket => self.sequence(&TokenKind::RBracket, NodeKind::Vector),
TokenKind::RParen | TokenKind::RBrace | TokenKind::RBracket => {
Err(ParseError::UnmatchedClose(span))
}
TokenKind::Quote => self.reader_macro("quote", |n| NodeKind::Quote(Box::new(n))),
TokenKind::Quasiquote => {
self.reader_macro("quasiquote", |n| NodeKind::Quasiquote(Box::new(n)))
}
TokenKind::Unquote => self.reader_macro("unquote", |n| NodeKind::Unquote(Box::new(n))),
TokenKind::UnquoteSplice => {
self.reader_macro("unquote-splicing", |n| NodeKind::UnquoteSplice(Box::new(n)))
}
TokenKind::Str(s) => {
let s = s.clone();
self.pos += 1;
Ok(Node::new(NodeKind::Str(s), span))
}
TokenKind::Int(i) => {
let i = *i;
self.pos += 1;
Ok(Node::new(NodeKind::Int(i), span))
}
TokenKind::Float(f) => {
let f = *f;
self.pos += 1;
Ok(Node::new(NodeKind::Float(f), span))
}
TokenKind::Bool(b) => {
let b = *b;
self.pos += 1;
Ok(Node::new(NodeKind::Bool(b), span))
}
TokenKind::Nil => {
self.pos += 1;
Ok(Node::new(NodeKind::Nil, span))
}
TokenKind::Symbol(s) => {
let s = s.clone();
self.pos += 1;
Ok(Node::new(NodeKind::Symbol(s), span))
}
TokenKind::Keyword(s) => {
let s = s.clone();
self.pos += 1;
Ok(Node::new(NodeKind::Keyword(s), span))
}
kind => Err(ParseError::Unexpected {
kind: kind.clone(),
span,
}),
}
}
fn sequence(
&mut self,
close_kind: &TokenKind,
wrap: fn(Vec<Node>) -> NodeKind,
) -> Result<Node, ParseError> {
let open = self.bump().expect("opening delimiter").span;
let mut items = Vec::new();
loop {
let leading = self.consume_trivia();
let next = self.peek();
match next {
None => return Err(ParseError::Eof),
Some(tok) if tok.kind == *close_kind => {
let close = self.bump().expect("closing delimiter").span;
let span = open.union(close);
let mut node = Node::new(wrap(items), span);
node.leading = Vec::new();
node.trailing = leading;
return Ok(node);
}
Some(_) => {
let mut child = self.node()?;
if child.leading.is_empty() {
child.leading = leading;
}
items.push(child);
}
}
}
}
fn reader_macro(
&mut self,
name: &'static str,
wrap: impl FnOnce(Node) -> NodeKind,
) -> Result<Node, ParseError> {
let head = self.bump().expect("reader macro token").span;
self.consume_trivia();
let inner = self.peek().ok_or(ParseError::DanglingReader(name, head))?;
let _ = inner;
let target = self.node()?;
let span = head.union(target.span);
Ok(Node::new(wrap(target), span))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_atom() {
let nodes = parse("42").unwrap();
assert_eq!(nodes.len(), 1);
assert!(matches!(nodes[0].kind, NodeKind::Int(42)));
assert_eq!(nodes[0].span, Span::new(0, 2));
}
#[test]
fn parse_list() {
let nodes = parse("(a b c)").unwrap();
assert_eq!(nodes.len(), 1);
let Some(items) = nodes[0].kind.as_list() else {
panic!("expected list");
};
assert_eq!(items.len(), 3);
assert!(matches!(items[0].kind, NodeKind::Symbol(ref s) if s == "a"));
}
#[test]
fn parse_map_and_vector() {
let nodes =
parse(r#"(defcaixa demo :package { :name "d" } :workflows [ :a :b ])"#).unwrap();
let Some(items) = nodes[0].kind.as_list() else {
panic!("expected list")
};
assert_eq!(items.len(), 6, "got {items:#?}");
let NodeKind::Map(m) = &items[3].kind else {
panic!("expected map, got {:?}", items[3].kind)
};
assert_eq!(m.len(), 2);
assert!(matches!(&m[0].kind, NodeKind::Keyword(k) if k == "name"));
let NodeKind::Vector(v) = &items[5].kind else {
panic!("expected vector, got {:?}", items[5].kind)
};
assert_eq!(v.len(), 2);
assert!(matches!(&v[0].kind, NodeKind::Keyword(k) if k == "a"));
}
#[test]
fn delimiters_terminate_atoms_without_whitespace() {
let nodes = parse(r"{:a 1}").unwrap();
let NodeKind::Map(m) = &nodes[0].kind else {
panic!("expected map, got {:?}", nodes[0].kind)
};
assert_eq!(m.len(), 2);
assert!(matches!(&m[0].kind, NodeKind::Keyword(k) if k == "a"));
assert!(matches!(m[1].kind, NodeKind::Int(1)));
let nodes = parse(r"[a b]").unwrap();
let NodeKind::Vector(v) = &nodes[0].kind else {
panic!("expected vector")
};
assert_eq!(v.len(), 2);
assert!(matches!(&v[1].kind, NodeKind::Symbol(s) if s == "b"));
}
#[test]
fn unmatched_closing_delimiters_are_rejected() {
for src in ["}", "]", ")"] {
assert!(
matches!(parse(src), Err(ParseError::UnmatchedClose(_))),
"{src:?} must be an unmatched-close error"
);
}
for src in ["{", "[", "("] {
assert!(
matches!(parse(src), Err(ParseError::Eof)),
"{src:?} must be an EOF error"
);
}
}
#[test]
fn parse_kwargs() {
let nodes = parse(r#"(defcaixa :nome "demo" :versao "0.1.0")"#).unwrap();
assert_eq!(nodes[0].head_symbol(), Some("defcaixa"));
assert!(matches!(
nodes[0].kwarg("nome").map(|n| &n.kind),
Some(NodeKind::Str(s)) if s == "demo"
));
}
#[test]
fn parse_nested_with_comments() {
let src = r#"
;; leading doc
(defcaixa
:nome "demo"
;; inline note
:versao "0.1.0")
"#;
let nodes = parse(src).unwrap();
assert_eq!(nodes.len(), 1);
assert!(!nodes[0].leading.is_empty());
}
#[test]
fn parse_reader_macros() {
let nodes = parse("`(a ,b ,@cs)").unwrap();
let NodeKind::Quasiquote(inner) = &nodes[0].kind else {
panic!("expected quasiquote");
};
let Some(items) = inner.kind.as_list() else {
panic!("expected list inside quasiquote");
};
assert_eq!(items.len(), 3);
assert!(matches!(items[1].kind, NodeKind::Unquote(_)));
assert!(matches!(items[2].kind, NodeKind::UnquoteSplice(_)));
}
#[test]
fn to_tatara_sexp_equivalence() {
use tatara_lisp::{Atom, Sexp};
let src = r#"(defcaixa :nome "demo" :kind Biblioteca)"#;
let nodes = parse(src).unwrap();
let lowered = nodes[0].to_tatara_sexp();
match lowered {
Sexp::List(items) => {
assert_eq!(items.len(), 5);
assert!(matches!(items[0], Sexp::Atom(Atom::Symbol(ref s)) if s == "defcaixa"));
assert!(matches!(items[1], Sexp::Atom(Atom::Keyword(ref s)) if s == "nome"));
assert!(matches!(items[2], Sexp::Atom(Atom::Str(ref s)) if s == "demo"));
assert!(matches!(items[3], Sexp::Atom(Atom::Keyword(ref s)) if s == "kind"));
assert!(matches!(items[4], Sexp::Atom(Atom::Symbol(ref s)) if s == "Biblioteca"));
}
other => panic!("expected List, got {other:?}"),
}
}
}