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`] — numbers (integer / decimal / rational), `#u8(...)` byte
9//!   vectors, `#"..."` base64 bytes, and the `#t` / `#f` rejection.
10//! - [`strings`] — `"..."` and `"""..."""` string literals.
11//! - [`forms`] — `'`/`,`/`,@`/`\`` reader macros and parenthesised
12//!   lists (including dotted-pair tail).
13//! - [`atoms`] — `:keyword` and bare symbols, including `nil` and `t`.
14
15mod atoms;
16mod forms;
17mod literals;
18mod strings;
19mod whitespace;
20
21#[cfg(test)]
22mod boolean_tests;
23#[cfg(test)]
24mod tests;
25
26use tracing::debug;
27use winnow::combinator::alt;
28use winnow::error::{AddContext, ContextError, ErrMode, ModalResult, StrContext, StrContextValue};
29use winnow::prelude::*;
30
31use crate::ast::{Annotation, Expr, Program};
32use crate::error::{Error, Result};
33
34pub struct Reader;
35
36impl Reader {
37    pub fn parse(input: &str) -> Result<Program> {
38        debug!(input_len = input.len(), "parse start");
39        let mut remaining = input;
40        let (exprs, annotations) = parse_program(&mut remaining).map_err(|e| {
41            let offset = input.len() - remaining.len();
42            Error::parse(format_parse_error(e), input, offset..offset + 1)
43        })?;
44        debug!(
45            expr_count = exprs.len(),
46            annotation_count = annotations.len(),
47            "parse complete"
48        );
49        Ok(Program::with_annotations(exprs, annotations))
50    }
51
52    #[must_use]
53    pub fn is_incomplete(input: &str) -> bool {
54        let mut remaining = input;
55        parse_program(&mut remaining).is_err() && remaining.is_empty()
56    }
57
58    pub fn parse_expr(input: &str) -> Result<Expr> {
59        let mut remaining = input;
60        parse_expr(&mut remaining).map_err(|e| {
61            let offset = input.len() - remaining.len();
62            Error::parse(format_parse_error(e), input, offset..offset + 1)
63        })
64    }
65}
66
67fn format_parse_error(err: ErrMode<ContextError>) -> String {
68    match err {
69        ErrMode::Backtrack(ctx) | ErrMode::Cut(ctx) => format_context_error(&ctx),
70        ErrMode::Incomplete(_) => "unexpected end of input".to_string(),
71    }
72}
73
74fn format_context_error(ctx: &ContextError) -> String {
75    if let Some(cause) = ctx.cause() {
76        return cause.to_string();
77    }
78    let mut parts = Vec::new();
79    for c in ctx.context() {
80        match c {
81            StrContext::Label(label) => parts.push(format!("in {label}")),
82            StrContext::Expected(StrContextValue::Description(desc)) => {
83                parts.push(format!("expected {desc}"));
84            }
85            StrContext::Expected(StrContextValue::CharLiteral(c)) => {
86                parts.push(format!("expected '{c}'"));
87            }
88            StrContext::Expected(StrContextValue::StringLiteral(s)) => {
89                parts.push(format!("expected \"{s}\""));
90            }
91            _ => {}
92        }
93    }
94    if parts.is_empty() {
95        "parse error".to_string()
96    } else {
97        parts.join(", ")
98    }
99}
100
101fn parse_program(input: &mut &str) -> ModalResult<(Vec<Expr>, Vec<Annotation>)> {
102    let mut annotations = Vec::new();
103    whitespace::skip_ws_and_comments(input, &mut annotations)?;
104
105    let mut exprs = Vec::new();
106    loop {
107        if input.is_empty() {
108            break;
109        }
110        let expr = parse_expr(input)?;
111        exprs.push(expr);
112        whitespace::skip_ws_and_comments(input, &mut annotations)?;
113    }
114
115    Ok((exprs, annotations))
116}
117
118/// Nesting the reader accepts before refusing the input.
119///
120/// `parse_expr` and `parse_list` are mutually recursive with a stack frame per
121/// level, so without a ceiling a few kilobytes of `(((…` aborts the whole
122/// process — and the reader is reachable from a report query parameter and from
123/// the SSH eval channel. Same purpose as `MAX_MACRO_EXPANSION_DEPTH`.
124///
125/// 64 is measured, not guessed: on a 2 MiB stack (the default for both tokio
126/// workers and libtest threads) a debug build survives 128 levels and aborts at
127/// 192, so this keeps a 2x margin — and is still far more nesting than any real
128/// filter or program.
129const MAX_READ_DEPTH: usize = 64;
130
131thread_local! {
132    static READ_DEPTH: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
133}
134
135/// Restores the reader's depth however the parser returns.
136struct DepthGuard;
137
138impl Drop for DepthGuard {
139    fn drop(&mut self) {
140        READ_DEPTH.with(|d| d.set(d.get().saturating_sub(1)));
141    }
142}
143
144pub(super) fn parse_expr(input: &mut &str) -> ModalResult<Expr> {
145    let depth = READ_DEPTH.with(|d| {
146        let next = d.get() + 1;
147        d.set(next);
148        next
149    });
150    let _guard = DepthGuard;
151    if depth > MAX_READ_DEPTH {
152        return Err(ErrMode::Cut(
153            ContextError::new()
154                .add_context(input, &input.checkpoint(), StrContext::Label("expression"))
155                .add_context(
156                    input,
157                    &input.checkpoint(),
158                    StrContext::Expected(StrContextValue::Description(
159                        "nesting within the reader depth limit",
160                    )),
161                ),
162        ));
163    }
164
165    let expr = alt((
166        alt((
167            literals::parse_hash_literal,
168            literals::parse_number,
169            strings::parse_string,
170            forms::parse_quote,
171        )),
172        alt((
173            forms::parse_quasiquote,
174            forms::parse_unquote,
175            forms::parse_list,
176            atoms::parse_keyword,
177            atoms::parse_symbol,
178        )),
179    ))
180    .parse_next(input)?;
181    debug!(expr = ?expr, "parsed expression");
182    Ok(expr)
183}