Lines
89.42 %
Functions
55.56 %
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`] — numbers (integer / decimal / rational), `#u8(...)` byte
//! vectors, `#"..."` base64 bytes, and the `#t` / `#f` rejection.
//! - [`strings`] — `"..."` and `"""..."""` string literals.
//! - [`forms`] — `'`/`,`/`,@`/`\`` reader macros and parenthesised
//! lists (including dotted-pair tail).
//! - [`atoms`] — `:keyword` and bare symbols, including `nil` and `t`.
mod atoms;
mod forms;
mod literals;
mod strings;
mod whitespace;
#[cfg(test)]
mod boolean_tests;
mod tests;
use tracing::debug;
use winnow::combinator::alt;
use winnow::error::{AddContext, 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 {
if let Some(cause) = ctx.cause() {
return cause.to_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))
/// Nesting the reader accepts before refusing the input.
///
/// `parse_expr` and `parse_list` are mutually recursive with a stack frame per
/// level, so without a ceiling a few kilobytes of `(((…` aborts the whole
/// process — and the reader is reachable from a report query parameter and from
/// the SSH eval channel. Same purpose as `MAX_MACRO_EXPANSION_DEPTH`.
/// 64 is measured, not guessed: on a 2 MiB stack (the default for both tokio
/// workers and libtest threads) a debug build survives 128 levels and aborts at
/// 192, so this keeps a 2x margin — and is still far more nesting than any real
/// filter or program.
const MAX_READ_DEPTH: usize = 64;
thread_local! {
static READ_DEPTH: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
/// Restores the reader's depth however the parser returns.
struct DepthGuard;
impl Drop for DepthGuard {
fn drop(&mut self) {
READ_DEPTH.with(|d| d.set(d.get().saturating_sub(1)));
pub(super) fn parse_expr(input: &mut &str) -> ModalResult<Expr> {
let depth = READ_DEPTH.with(|d| {
let next = d.get() + 1;
d.set(next);
next
});
let _guard = DepthGuard;
if depth > MAX_READ_DEPTH {
return Err(ErrMode::Cut(
ContextError::new()
.add_context(input, &input.checkpoint(), StrContext::Label("expression"))
.add_context(
input,
&input.checkpoint(),
StrContext::Expected(StrContextValue::Description(
"nesting within the reader depth limit",
)),
),
));
let expr = alt((
alt((
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)