Lines
83.75 %
Functions
40 %
Branches
100 %
//! S-expression reader (winnow-based).
//!
//! Public surface: [`Reader::parse`] for full programs and
//! [`Reader::parse_expr`] for a single expression. Internally split
//! into topic-focused submodules to stay under the ~500-line
//! CLAUDE.md guideline:
//! - [`whitespace`] — whitespace + `;` / `; @annotation` comments.
//! - [`literals`] — `nil`, `#t` / `#f`, numbers (integer / decimal /
//! rational), `#u8(...)` byte vectors, `#"..."` base64 bytes.
//! - [`strings`] — `"..."` and `"""..."""` string literals.
//! - [`forms`] — `'`/`,`/`,@`/`\`` reader macros and parenthesised
//! lists (including dotted-pair tail).
//! - [`atoms`] — `:keyword` and bare symbols.
mod atoms;
mod forms;
mod literals;
mod strings;
mod whitespace;
#[cfg(test)]
mod tests;
use tracing::debug;
use winnow::combinator::alt;
use winnow::error::{ContextError, ErrMode, ModalResult, StrContext, StrContextValue};
use winnow::prelude::*;
use crate::ast::{Annotation, Expr, Program};
use crate::error::{Error, Result};
pub struct Reader;
impl Reader {
pub fn parse(input: &str) -> Result<Program> {
debug!(input_len = input.len(), "parse start");
let mut remaining = input;
let (exprs, annotations) = parse_program(&mut remaining).map_err(|e| {
let offset = input.len() - remaining.len();
Error::parse(format_parse_error(e), input, offset..offset + 1)
})?;
debug!(
expr_count = exprs.len(),
annotation_count = annotations.len(),
"parse complete"
);
Ok(Program::with_annotations(exprs, annotations))
}
#[must_use]
pub fn is_incomplete(input: &str) -> bool {
parse_program(&mut remaining).is_err() && remaining.is_empty()
pub fn parse_expr(input: &str) -> Result<Expr> {
parse_expr(&mut remaining).map_err(|e| {
})
fn format_parse_error(err: ErrMode<ContextError>) -> String {
match err {
ErrMode::Backtrack(ctx) | ErrMode::Cut(ctx) => format_context_error(&ctx),
ErrMode::Incomplete(_) => "unexpected end of input".to_string(),
fn format_context_error(ctx: &ContextError) -> String {
let mut parts = Vec::new();
for c in ctx.context() {
match c {
StrContext::Label(label) => parts.push(format!("in {label}")),
StrContext::Expected(StrContextValue::Description(desc)) => {
parts.push(format!("expected {desc}"));
StrContext::Expected(StrContextValue::CharLiteral(c)) => {
parts.push(format!("expected '{c}'"));
StrContext::Expected(StrContextValue::StringLiteral(s)) => {
parts.push(format!("expected \"{s}\""));
_ => {}
if parts.is_empty() {
"parse error".to_string()
} else {
parts.join(", ")
fn parse_program(input: &mut &str) -> ModalResult<(Vec<Expr>, Vec<Annotation>)> {
let mut annotations = Vec::new();
whitespace::skip_ws_and_comments(input, &mut annotations)?;
let mut exprs = Vec::new();
loop {
if input.is_empty() {
break;
let expr = parse_expr(input)?;
exprs.push(expr);
Ok((exprs, annotations))
pub(super) fn parse_expr(input: &mut &str) -> ModalResult<Expr> {
let expr = alt((
alt((
literals::parse_nil,
literals::parse_hash_literal,
literals::parse_number,
strings::parse_string,
forms::parse_quote,
)),
forms::parse_quasiquote,
forms::parse_unquote,
forms::parse_list,
atoms::parse_keyword,
atoms::parse_symbol,
))
.parse_next(input)?;
debug!(expr = ?expr, "parsed expression");
Ok(expr)