Skip to main content

nomiscript/reader/
mod.rs

1//! S-expression reader (winnow-based).
2//!
3//! Public surface: [`Reader::parse`] for full programs and
4//! [`Reader::parse_expr`] for a single expression. Internally split
5//! into topic-focused submodules to stay under the ~500-line
6//! CLAUDE.md guideline:
7//! - [`whitespace`] — whitespace + `;` / `; @annotation` comments.
8//! - [`literals`] — `nil`, `#t` / `#f`, numbers (integer / decimal /
9//!   rational), `#u8(...)` byte vectors, `#"..."` base64 bytes.
10//! - [`strings`] — `"..."` and `"""..."""` string literals.
11//! - [`forms`] — `'`/`,`/`,@`/`\`` reader macros and parenthesised
12//!   lists (including dotted-pair tail).
13//! - [`atoms`] — `:keyword` and bare symbols.
14
15mod atoms;
16mod forms;
17mod literals;
18mod strings;
19mod whitespace;
20
21#[cfg(test)]
22mod tests;
23
24use tracing::debug;
25use winnow::combinator::alt;
26use winnow::error::{ContextError, ErrMode, ModalResult, StrContext, StrContextValue};
27use winnow::prelude::*;
28
29use crate::ast::{Annotation, Expr, Program};
30use crate::error::{Error, Result};
31
32pub struct Reader;
33
34impl Reader {
35    pub fn parse(input: &str) -> Result<Program> {
36        debug!(input_len = input.len(), "parse start");
37        let mut remaining = input;
38        let (exprs, annotations) = parse_program(&mut remaining).map_err(|e| {
39            let offset = input.len() - remaining.len();
40            Error::parse(format_parse_error(e), input, offset..offset + 1)
41        })?;
42        debug!(
43            expr_count = exprs.len(),
44            annotation_count = annotations.len(),
45            "parse complete"
46        );
47        Ok(Program::with_annotations(exprs, annotations))
48    }
49
50    #[must_use]
51    pub fn is_incomplete(input: &str) -> bool {
52        let mut remaining = input;
53        parse_program(&mut remaining).is_err() && remaining.is_empty()
54    }
55
56    pub fn parse_expr(input: &str) -> Result<Expr> {
57        let mut remaining = input;
58        parse_expr(&mut remaining).map_err(|e| {
59            let offset = input.len() - remaining.len();
60            Error::parse(format_parse_error(e), input, offset..offset + 1)
61        })
62    }
63}
64
65fn format_parse_error(err: ErrMode<ContextError>) -> String {
66    match err {
67        ErrMode::Backtrack(ctx) | ErrMode::Cut(ctx) => format_context_error(&ctx),
68        ErrMode::Incomplete(_) => "unexpected end of input".to_string(),
69    }
70}
71
72fn format_context_error(ctx: &ContextError) -> String {
73    let mut parts = Vec::new();
74    for c in ctx.context() {
75        match c {
76            StrContext::Label(label) => parts.push(format!("in {label}")),
77            StrContext::Expected(StrContextValue::Description(desc)) => {
78                parts.push(format!("expected {desc}"));
79            }
80            StrContext::Expected(StrContextValue::CharLiteral(c)) => {
81                parts.push(format!("expected '{c}'"));
82            }
83            StrContext::Expected(StrContextValue::StringLiteral(s)) => {
84                parts.push(format!("expected \"{s}\""));
85            }
86            _ => {}
87        }
88    }
89    if parts.is_empty() {
90        "parse error".to_string()
91    } else {
92        parts.join(", ")
93    }
94}
95
96fn parse_program(input: &mut &str) -> ModalResult<(Vec<Expr>, Vec<Annotation>)> {
97    let mut annotations = Vec::new();
98    whitespace::skip_ws_and_comments(input, &mut annotations)?;
99
100    let mut exprs = Vec::new();
101    loop {
102        if input.is_empty() {
103            break;
104        }
105        let expr = parse_expr(input)?;
106        exprs.push(expr);
107        whitespace::skip_ws_and_comments(input, &mut annotations)?;
108    }
109
110    Ok((exprs, annotations))
111}
112
113pub(super) fn parse_expr(input: &mut &str) -> ModalResult<Expr> {
114    let expr = alt((
115        alt((
116            literals::parse_nil,
117            literals::parse_hash_literal,
118            literals::parse_number,
119            strings::parse_string,
120            forms::parse_quote,
121        )),
122        alt((
123            forms::parse_quasiquote,
124            forms::parse_unquote,
125            forms::parse_list,
126            atoms::parse_keyword,
127            atoms::parse_symbol,
128        )),
129    ))
130    .parse_next(input)?;
131    debug!(expr = ?expr, "parsed expression");
132    Ok(expr)
133}