scheme-edit 0.1.0

Lossless Scheme S-expression parser and editor (toml_edit-style CST)
Documentation
use crate::cst::{Item, ListKind, Node};
use crate::lexer::{Lexer, ParseError, Token, TokenKind};

pub struct Parser {
    lexer: Lexer,
    peeked: Option<Token>,
}

impl Parser {
    pub fn new(src: &str) -> Self {
        Parser {
            lexer: Lexer::new(src),
            peeked: None,
        }
    }

    fn next(&mut self) -> Result<Option<Token>, ParseError> {
        if let Some(t) = self.peeked.take() {
            return Ok(Some(t));
        }
        self.lexer.next_token()
    }

    fn peek(&mut self) -> Result<Option<&Token>, ParseError> {
        if self.peeked.is_none() {
            self.peeked = self.lexer.next_token()?;
        }
        Ok(self.peeked.as_ref())
    }

    pub fn parse_items(
        &mut self,
        terminator: Option<char>,
        open_pos: (u32, u32),
    ) -> Result<Vec<Item>, ParseError> {
        let mut items = Vec::new();
        loop {
            let Some(tok) = self.next()? else {
                return if terminator.is_none() {
                    Ok(items)
                } else {
                    Err(ParseError {
                        message: "unclosed list".into(),
                        line: open_pos.0,
                        col: open_pos.1,
                    })
                };
            };
            match tok.kind {
                TokenKind::Ws => items.push(Item::Ws(tok.text)),
                TokenKind::LineComment => items.push(Item::LineComment(tok.text)),
                TokenKind::BlockComment => items.push(Item::BlockComment(tok.text)),
                TokenKind::DatumCommentStart => {
                    let (line, col) = (tok.line, tok.col);
                    let mut text = tok.text;
                    self.collect_trivia(&mut text)?;
                    let datum = self.parse_datum(line, col)?;
                    text.push_str(&datum.to_source());
                    items.push(Item::DatumComment(text));
                }
                TokenKind::Close(c) => {
                    return if terminator == Some(c) {
                        Ok(items)
                    } else {
                        Err(ParseError {
                            message: format!("unexpected `{c}`"),
                            line: tok.line,
                            col: tok.col,
                        })
                    };
                }
                _ => {
                    let node = self.parse_node_from(tok)?;
                    items.push(Item::Node(node));
                }
            }
        }
    }

    fn parse_node_from(&mut self, tok: Token) -> Result<Node, ParseError> {
        match tok.kind {
            TokenKind::Open(kind) => {
                let close = match kind {
                    ListKind::Bracket => ']',
                    _ => ')',
                };
                let items = self.parse_items(Some(close), (tok.line, tok.col))?;
                Ok(Node::List { kind, items })
            }
            TokenKind::Atom => Ok(Node::Atom(tok.text)),
            TokenKind::Str => Ok(Node::Str(tok.text)),
            TokenKind::Prefix => {
                let (line, col) = (tok.line, tok.col);
                // Trivia between prefix and datum is folded into the prefix
                // string verbatim so round-trip stays byte-identical.
                let mut prefix = tok.text;
                self.collect_trivia(&mut prefix)?;
                let inner = self.parse_datum(line, col)?;
                Ok(Node::Prefixed {
                    prefix,
                    inner: Box::new(inner),
                })
            }
            _ => Err(ParseError {
                message: "expected datum".into(),
                line: tok.line,
                col: tok.col,
            }),
        }
    }

    fn parse_datum(&mut self, line: u32, col: u32) -> Result<Node, ParseError> {
        let Some(tok) = self.next()? else {
            return Err(ParseError {
                message: "expected datum".into(),
                line,
                col,
            });
        };
        self.parse_node_from(tok)
    }

    fn collect_trivia(&mut self, out: &mut String) -> Result<(), ParseError> {
        loop {
            let Some(t) = self.peek()? else {
                return Ok(());
            };
            match t.kind {
                TokenKind::Ws | TokenKind::LineComment | TokenKind::BlockComment => {
                    if let Some(t) = self.next()? {
                        out.push_str(&t.text);
                    }
                }
                TokenKind::DatumCommentStart => {
                    let Some(t) = self.next()? else {
                        return Ok(());
                    };
                    let (line, col) = (t.line, t.col);
                    out.push_str(&t.text);
                    self.collect_trivia(out)?;
                    let datum = self.parse_datum(line, col)?;
                    out.push_str(&datum.to_source());
                }
                _ => return Ok(()),
            }
        }
    }
}