use crate::ast::{Atom, QuoteForm, Sexp};
use crate::error::{LispError, Result};
use crate::span::Span;
use crate::spanned::{Spanned, SpannedForm};
#[derive(Clone, Debug, PartialEq)]
enum Token {
LParen,
RParen,
Quoted(QuoteForm),
Atom(String),
Str(String),
}
#[derive(Clone, Debug, PartialEq)]
struct SpannedToken {
kind: Token,
span: Span,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
struct SourceTail {
eof: usize,
last_token_end: usize,
}
impl SourceTail {
fn of(src: &str, tokens: &[SpannedToken]) -> Self {
Self {
eof: src.len(),
last_token_end: tokens.last().map_or(0, |t| t.span.end),
}
}
fn unclosed_from(self, open_start: usize) -> Span {
Span::new(open_start, self.last_token_end)
}
fn eof_span(self) -> Span {
Span::new(self.eof, self.eof)
}
}
pub fn read(src: &str) -> Result<Vec<Sexp>> {
let tokens = tokenize(src)?;
let tail = SourceTail::of(src, &tokens);
let mut it = tokens.into_iter().peekable();
let mut forms = Vec::new();
while it.peek().is_some() {
forms.push(parse(&mut it, tail)?);
}
Ok(forms)
}
pub fn read_spanned(src: &str) -> Result<Vec<Spanned>> {
let tokens = tokenize(src)?;
let tail = SourceTail::of(src, &tokens);
let mut it = tokens.into_iter().peekable();
let mut forms = Vec::new();
while it.peek().is_some() {
forms.push(parse_spanned(&mut it, tail)?);
}
Ok(forms)
}
fn tokenize(src: &str) -> Result<Vec<SpannedToken>> {
let mut out: Vec<SpannedToken> = Vec::new();
let mut chars = src.char_indices().peekable();
while let Some(&(pos, c)) = chars.peek() {
if let Some(qf_head) = QuoteForm::from_lead_char(c) {
chars.next();
let qf = if let Some(promoted) = chars
.peek()
.and_then(|&(_, next)| qf_head.promote_via_next_char(next))
{
chars.next();
promoted
} else {
qf_head
};
out.push(SpannedToken {
kind: Token::Quoted(qf),
span: Span::new(pos, pos + qf.prefix().len()),
});
continue;
}
match c {
ws if ws.is_whitespace() => {
chars.next();
}
Sexp::COMMENT_LEAD => {
while let Some(&(_, ch)) = chars.peek() {
chars.next();
if ch == Sexp::COMMENT_TERM {
break;
}
}
}
Sexp::LIST_OPEN => {
chars.next();
out.push(SpannedToken {
kind: Token::LParen,
span: Span::new(pos, pos + Sexp::LIST_OPEN.len_utf8()),
});
}
Sexp::LIST_CLOSE => {
chars.next();
out.push(SpannedToken {
kind: Token::RParen,
span: Span::new(pos, pos + Sexp::LIST_CLOSE.len_utf8()),
});
}
Atom::STR_DELIMITER => {
chars.next();
let start = pos;
let mut s = String::new();
let end;
loop {
match chars.next() {
Some((_, Atom::STR_ESCAPE_LEAD)) => {
if let Some((_, esc)) = chars.next() {
if esc == 'u' {
let mut hex = String::new();
let mut saw_open = false;
for (_, c) in chars.by_ref() {
if !saw_open {
if c != '{' {
break;
}
saw_open = true;
continue;
}
if c == '}' {
break;
}
hex.push(c);
}
match u32::from_str_radix(&hex, 16)
.ok()
.and_then(char::from_u32)
{
Some(ch) => s.push(ch),
None => {
s.push('u');
s.push('{');
s.push_str(&hex);
s.push('}');
}
}
} else {
s.push(Atom::decode_str_escape(esc));
}
}
}
Some((p, Atom::STR_DELIMITER)) => {
end = p + Atom::STR_DELIMITER.len_utf8();
break;
}
Some((_, ch)) => s.push(ch),
None => {
return Err(LispError::UnterminatedString(Span::new(pos, src.len())))
}
}
}
out.push(SpannedToken {
kind: Token::Str(s),
span: Span::new(start, end),
});
}
_ => {
let start = pos;
let mut s = String::new();
let mut end = pos;
while let Some(&(p, ch)) = chars.peek() {
if Sexp::is_bare_atom_boundary(ch) {
break;
}
s.push(ch);
end = p + ch.len_utf8();
chars.next();
}
out.push(SpannedToken {
kind: Token::Atom(s),
span: Span::new(start, end),
});
}
}
}
Ok(out)
}
fn parse<I: Iterator<Item = SpannedToken>>(
it: &mut std::iter::Peekable<I>,
tail: SourceTail,
) -> Result<Sexp> {
match it.next() {
Some(SpannedToken {
kind: Token::LParen,
span: open_span,
}) => {
let mut xs = Vec::new();
loop {
match it.peek() {
Some(SpannedToken {
kind: Token::RParen,
..
}) => {
it.next();
return Ok(Sexp::List(xs));
}
Some(_) => xs.push(parse(it, tail)?),
None => {
return Err(LispError::UnmatchedOpenParen {
span: tail.unclosed_from(open_span.start),
})
}
}
}
}
Some(SpannedToken {
kind: Token::RParen,
span,
}) => Err(LispError::UnmatchedParen { span }),
Some(SpannedToken {
kind: Token::Quoted(qf),
..
}) => read_quoted(it, tail, qf),
Some(SpannedToken {
kind: Token::Str(s),
..
}) => Ok(Sexp::Atom(Atom::Str(s))),
Some(SpannedToken {
kind: Token::Atom(s),
..
}) => Ok(Sexp::Atom(Atom::from_lexeme(&s))),
None => Err(LispError::Eof {
span: tail.eof_span(),
}),
}
}
fn read_quoted<I: Iterator<Item = SpannedToken>>(
it: &mut std::iter::Peekable<I>,
tail: SourceTail,
qf: QuoteForm,
) -> Result<Sexp> {
let inner = parse(it, tail)?;
Ok(qf.wrap(inner))
}
fn parse_spanned<I: Iterator<Item = SpannedToken>>(
it: &mut std::iter::Peekable<I>,
tail: SourceTail,
) -> Result<Spanned> {
match it.next() {
Some(SpannedToken {
kind: Token::LParen,
span: open_span,
}) => {
let mut xs: Vec<Spanned> = Vec::new();
loop {
match it.peek() {
Some(SpannedToken {
kind: Token::RParen,
span: close_span,
}) => {
let close = *close_span;
it.next();
return Ok(Spanned::new(
Span::new(open_span.start, close.end),
SpannedForm::List(xs),
));
}
Some(_) => xs.push(parse_spanned(it, tail)?),
None => {
return Err(LispError::UnmatchedOpenParen {
span: tail.unclosed_from(open_span.start),
})
}
}
}
}
Some(SpannedToken {
kind: Token::RParen,
span,
}) => Err(LispError::UnmatchedParen { span }),
Some(SpannedToken {
kind: Token::Quoted(qf),
span,
}) => read_quoted_spanned(it, tail, qf, span),
Some(SpannedToken {
kind: Token::Str(s),
span,
}) => Ok(Spanned::new(span, SpannedForm::Atom(Atom::Str(s)))),
Some(SpannedToken {
kind: Token::Atom(s),
span,
}) => Ok(Spanned::new(
span,
SpannedForm::Atom(Atom::from_lexeme(&s)),
)),
None => Err(LispError::Eof {
span: tail.eof_span(),
}),
}
}
fn read_quoted_spanned<I: Iterator<Item = SpannedToken>>(
it: &mut std::iter::Peekable<I>,
tail: SourceTail,
qf: QuoteForm,
prefix_span: Span,
) -> Result<Spanned> {
let inner = parse_spanned(it, tail)?;
let full = Span::new(prefix_span.start, inner.span.end.max(prefix_span.end));
Ok(Spanned::new(full, SpannedForm::wrap(qf, inner)))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn reads_atoms() {
let forms = read("foo 42 2.5 \"hello\" :kw #t #f").unwrap();
assert_eq!(forms.len(), 7);
assert_eq!(forms[0].as_symbol(), Some("foo"));
assert_eq!(forms[1], Sexp::int(42));
assert_eq!(forms[2], Sexp::float(2.5));
assert_eq!(forms[3].as_string(), Some("hello"));
assert_eq!(forms[4].as_keyword(), Some("kw"));
assert_eq!(forms[5], Sexp::boolean(true));
assert_eq!(forms[6], Sexp::boolean(false));
}
#[test]
fn reads_nested_lists() {
let f = read("(defpoint obs :class (Gate Observability))").unwrap();
assert_eq!(f.len(), 1);
let outer = f[0].as_list().unwrap();
assert_eq!(outer[0].as_symbol(), Some("defpoint"));
assert_eq!(outer[1].as_symbol(), Some("obs"));
assert_eq!(outer[2].as_keyword(), Some("class"));
let inner = outer[3].as_list().unwrap();
assert_eq!(inner[0].as_symbol(), Some("Gate"));
assert_eq!(inner[1].as_symbol(), Some("Observability"));
}
#[test]
fn handles_comments() {
let f = read("; top-level comment\n(a b) ; inline\n(c)").unwrap();
assert_eq!(f.len(), 2);
}
#[test]
fn string_escapes() {
let f = read(r#""line\nbreak\ttab""#).unwrap();
assert_eq!(f[0].as_string(), Some("line\nbreak\ttab"));
}
#[test]
fn quote_form() {
let f = read("'(a b)").unwrap();
match &f[0] {
Sexp::Quote(inner) => assert!(inner.is_list()),
_ => panic!("expected quote"),
}
}
#[test]
fn unmatched_paren_errors() {
assert!(read("(a b").is_err());
assert!(read(")").is_err());
}
#[test]
fn unmatched_closing_paren_reports_byte_offset() {
let err = read(" )").unwrap_err();
match err {
LispError::UnmatchedParen { span } => assert_eq!(span, Span::new(3, 4)),
other => panic!("expected UnmatchedParen, got {other:?}"),
}
}
#[test]
fn unmatched_opening_paren_reports_offset_of_open() {
let err = read("(a (b c").unwrap_err();
match err {
LispError::UnmatchedOpenParen { span } => assert_eq!(span, Span::new(3, 7)),
other => panic!("expected UnmatchedOpenParen, got {other:?}"),
}
}
#[test]
fn outer_unmatched_open_reports_outer_offset() {
let err = read("(a b").unwrap_err();
match err {
LispError::UnmatchedOpenParen { span } => assert_eq!(span, Span::new(0, 4)),
other => panic!("expected UnmatchedOpenParen, got {other:?}"),
}
}
#[test]
fn unmatched_open_span_ends_at_last_token_not_at_eof() {
let src = "(a b ; tail\n";
assert_eq!(src.len(), 14);
let err = read(src).unwrap_err();
match err {
LispError::UnmatchedOpenParen { span } => assert_eq!(span, Span::new(0, 4)),
other => panic!("expected UnmatchedOpenParen, got {other:?}"),
}
}
#[test]
fn unterminated_string_span_covers_the_dangling_literal() {
let src = "(a \"bc";
let err = read(src).unwrap_err();
match err {
LispError::UnterminatedString(span) => assert_eq!(span, Span::new(3, src.len())),
other => panic!("expected UnterminatedString, got {other:?}"),
}
}
#[test]
fn eof_span_is_zero_width_at_end_of_input() {
let src = "(a b) '";
let err = read(src).unwrap_err();
match err {
LispError::Eof { span } => {
assert_eq!(span, Span::new(src.len(), src.len()));
assert_eq!(span.end - span.start, 0, "EOF span must be zero-width");
}
other => panic!("expected Eof, got {other:?}"),
}
}
#[test]
fn dangling_quote_reports_eof_at_input_length() {
let src = "(a b) '";
let err = read(src).unwrap_err();
match err {
LispError::Eof { span } => assert_eq!(span.start, src.len()),
other => panic!("expected Eof, got {other:?}"),
}
}
#[test]
fn spanned_parser_reports_the_same_offsets_as_the_plain_one() {
for src in [" )", "(a (b c", "(a b", "(a b) '", "\"unterminated"] {
let plain = read(src).unwrap_err();
let spanned = read_spanned(src).unwrap_err();
assert_eq!(
format!("{plain:?}"),
format!("{spanned:?}"),
"{src:?}: plain and spanned readers disagreed on the error"
);
}
}
#[test]
fn error_display_includes_position() {
let err = read(") ").unwrap_err();
let rendered = format!("{err}");
assert!(
rendered.contains("position 0"),
"expected position in display, got {rendered:?}"
);
}
#[test]
fn quote_prefix_round_trips_through_read_quoted_into_sexp_quote() {
let f = read("'foo").unwrap();
assert_eq!(f.len(), 1);
match &f[0] {
Sexp::Quote(inner) => assert_eq!(inner.as_symbol(), Some("foo")),
other => panic!("expected Sexp::Quote, got {other:?}"),
}
}
#[test]
fn quasiquote_prefix_round_trips_through_read_quoted_into_sexp_quasiquote() {
let f = read("`foo").unwrap();
assert_eq!(f.len(), 1);
match &f[0] {
Sexp::Quasiquote(inner) => assert_eq!(inner.as_symbol(), Some("foo")),
other => panic!("expected Sexp::Quasiquote, got {other:?}"),
}
}
#[test]
fn unquote_prefix_round_trips_through_read_quoted_into_sexp_unquote() {
let f = read(",foo").unwrap();
assert_eq!(f.len(), 1);
match &f[0] {
Sexp::Unquote(inner) => assert_eq!(inner.as_symbol(), Some("foo")),
other => panic!("expected Sexp::Unquote, got {other:?}"),
}
}
#[test]
fn unquote_splice_prefix_round_trips_through_read_quoted_into_sexp_unquote_splice() {
let f = read(",@xs").unwrap();
assert_eq!(f.len(), 1);
match &f[0] {
Sexp::UnquoteSplice(inner) => assert_eq!(inner.as_symbol(), Some("xs")),
other => panic!("expected Sexp::UnquoteSplice, got {other:?}"),
}
}
#[test]
fn quote_prefix_recursively_wraps_via_read_quoted_for_nested_homoiconic_forms() {
let f = read("',foo").unwrap();
assert_eq!(f.len(), 1);
match &f[0] {
Sexp::Quote(outer) => match outer.as_ref() {
Sexp::Unquote(inner) => assert_eq!(inner.as_symbol(), Some("foo")),
other => panic!("expected inner Sexp::Unquote, got {other:?}"),
},
other => panic!("expected outer Sexp::Quote, got {other:?}"),
}
}
#[test]
fn read_quoted_propagates_inner_parse_error_unchanged() {
let src = "'";
let err = read(src).unwrap_err();
match err {
LispError::Eof { span } => assert_eq!(span, Span::new(src.len(), src.len())),
other => panic!("expected Eof, got {other:?}"),
}
}
#[test]
fn reader_threads_each_prefix_through_quote_form_wrap_dual_of_as_quote_form() {
let inner = Sexp::symbol("payload");
for (src, expected_qf) in [
("'payload", QuoteForm::Quote),
("`payload", QuoteForm::Quasiquote),
(",payload", QuoteForm::Unquote),
(",@payload", QuoteForm::UnquoteSplice),
] {
let forms = read(src).expect(src);
assert_eq!(forms.len(), 1, "{src} must produce one form");
assert_eq!(
forms[0],
expected_qf.wrap(inner.clone()),
"{src} drifted from QuoteForm::wrap dual"
);
let (qf, body) = forms[0]
.as_quote_form()
.unwrap_or_else(|| panic!("{src} must project through as_quote_form"));
assert_eq!(qf, expected_qf, "{src} produced wrong typed marker");
assert_eq!(body, &inner, "{src} drifted inner body");
}
}
#[test]
fn token_quoted_arms_carry_typed_quote_form_marker_for_every_prefix() {
for (src, expected_qf) in [
("'", QuoteForm::Quote),
("`", QuoteForm::Quasiquote),
(",", QuoteForm::Unquote),
(",@", QuoteForm::UnquoteSplice),
] {
let tokens = tokenize(src).expect(src);
assert_eq!(tokens.len(), 1, "{src} must produce one token");
assert_eq!(
tokens[0].kind,
Token::Quoted(expected_qf),
"{src} drifted typed marker"
);
assert_eq!(
tokens[0].span,
Span::new(0, expected_qf.prefix().len()),
"{src} drifted span"
);
}
}
#[test]
fn token_quoted_unquote_splice_two_char_marker_collapses_to_single_token() {
let tokens = tokenize(",@xs").expect(",@xs");
assert_eq!(tokens.len(), 2, ",@xs must tokenize as splice + atom");
assert_eq!(tokens[0].kind, Token::Quoted(QuoteForm::UnquoteSplice));
assert_eq!(tokens[0].span, Span::new(0, 2));
assert_eq!(tokens[1].kind, Token::Atom("xs".into()));
assert_eq!(tokens[1].span, Span::new(2, 4));
let bare = tokenize(",xs").expect(",xs");
assert_eq!(bare[0].kind, Token::Quoted(QuoteForm::Unquote));
assert_eq!(bare[0].span, Span::new(0, 1));
}
#[test]
fn read_quoted_propagates_unmatched_open_paren_for_quoted_list() {
let err = read("'(a b").unwrap_err();
match err {
LispError::UnmatchedOpenParen { span } => assert_eq!(span, Span::new(1, 5)),
other => panic!("expected UnmatchedOpenParen, got {other:?}"),
}
}
#[test]
fn reader_atom_token_arm_routes_through_atom_from_lexeme_for_every_kind() {
let cases: &[&str] = &[
"foo", "defpoint", "seph.1", ":parent", ":kw", "42", "-7", "0", "1", "1.0", "1.5",
"-2.5", "1e3", "#t", "#f",
"true", "false", "+", "a-b",
];
for src in cases {
let forms = read(src).unwrap_or_else(|e| panic!("reader rejected {src:?}: {e}"));
assert_eq!(forms.len(), 1, "{src:?} must read as exactly one form");
assert_eq!(
&forms[0],
&Sexp::Atom(Atom::from_lexeme(src)),
"{src:?}: reader's bare-atom arm drifted from Atom::from_lexeme"
);
}
}
#[test]
fn both_projections_classify_every_bare_atom_identically() {
let cases: &[&str] = &[
"foo", ":kw", "42", "-7", "1.5", "1e3", "#t", "#f", "true", "false", "+", "a-b",
];
for src in cases {
let plain = read(src).expect(src);
let spanned = read_spanned(src).expect(src);
let projected: Vec<Sexp> = spanned.iter().map(Spanned::to_sexp).collect();
assert_eq!(projected, plain, "{src:?}: projections disagreed");
}
}
#[test]
fn reader_str_open_close_arms_bind_to_atom_str_delimiter() {
let payload = "hello world";
let source = format!("{}{payload}{}", Atom::STR_DELIMITER, Atom::STR_DELIMITER);
let tokens = tokenize(&source)
.unwrap_or_else(|e| panic!("tokenize rejected `{source}`: {e}"));
assert_eq!(tokens.len(), 1, "must tokenize as exactly one Token::Str");
assert_eq!(tokens[0].kind, Token::Str(payload.to_string()));
assert_eq!(tokens[0].span, Span::new(0, source.len()));
}
#[test]
fn reader_str_escape_self_escape_arm_routes_through_atom_str_delimiter() {
let escape_source = format!(
"{}\\{}{}",
Atom::STR_DELIMITER,
Atom::STR_DELIMITER,
Atom::STR_DELIMITER,
);
let forms = read(&escape_source)
.unwrap_or_else(|e| panic!("reader rejected `{escape_source}`: {e}"));
assert_eq!(forms.len(), 1);
assert_eq!(
forms[0],
Sexp::Atom(Atom::string(Atom::STR_DELIMITER.to_string())),
);
}
#[test]
fn reader_str_escape_lead_arms_route_through_atom_str_escape_lead() {
let escape_source = format!(
"{}{}{}{}",
Atom::STR_DELIMITER,
Atom::STR_ESCAPE_LEAD,
Atom::STR_ESCAPE_LEAD,
Atom::STR_DELIMITER,
);
let forms = read(&escape_source)
.unwrap_or_else(|e| panic!("reader rejected `{escape_source}`: {e}"));
assert_eq!(forms.len(), 1);
assert_eq!(
forms[0],
Sexp::Atom(Atom::string(Atom::STR_ESCAPE_LEAD.to_string())),
);
}
#[test]
fn tokenizer_quote_family_outer_dispatch_routes_through_quote_form_from_lead_char() {
for qf in QuoteForm::ALL {
let source = format!("{}xs", qf.prefix());
let tokens = tokenize(&source)
.unwrap_or_else(|e| panic!("tokenize rejected `{source}`: {e}"));
assert!(!tokens.is_empty());
assert_eq!(tokens[0].kind, Token::Quoted(qf));
assert_eq!(tokens[0].span, Span::new(0, qf.prefix().len()));
}
}
#[test]
fn tokenizer_splice_promotion_routes_through_quote_form_promote_via_next_char() {
for qf in QuoteForm::ALL {
let expected_promoted = qf.promote_via_next_char(QuoteForm::SPLICE_DISCRIMINATOR);
let source = format!("{}{}xs", qf.prefix(), QuoteForm::SPLICE_DISCRIMINATOR);
let tokens = tokenize(&source)
.unwrap_or_else(|e| panic!("tokenize rejected `{source}`: {e}"));
assert!(!tokens.is_empty());
match expected_promoted {
Some(promoted) => assert_eq!(tokens[0].kind, Token::Quoted(promoted)),
None => {
assert_eq!(tokens[0].kind, Token::Quoted(qf));
let expected_atom_pos = qf.prefix().len();
assert_eq!(tokens[1].kind, Token::Atom("@xs".into()));
assert_eq!(tokens[1].span.start, expected_atom_pos);
}
}
}
}
#[test]
fn tokenizer_bare_atom_terminator_disjunct_routes_through_is_bare_atom_boundary() {
for qf in [QuoteForm::Quote, QuoteForm::Quasiquote, QuoteForm::Unquote] {
let source = format!("foo{}xs", qf.prefix());
let tokens = tokenize(&source)
.unwrap_or_else(|e| panic!("tokenize rejected `{source}`: {e}"));
assert!(tokens.len() >= 2);
assert_eq!(tokens[0].kind, Token::Atom("foo".into()));
assert_eq!(tokens[0].span, Span::new(0, 3));
assert_eq!(tokens[1].kind, Token::Quoted(qf));
assert_eq!(tokens[1].span.start, 3);
}
}
#[test]
fn spanned_atoms_carry_byte_ranges() {
let src = "foo 42 \"hi\" :kw";
let forms = read_spanned(src).unwrap();
assert_eq!(forms.len(), 4);
assert_eq!(forms[0].span, Span::new(0, 3));
assert_eq!(forms[1].span, Span::new(4, 6));
assert_eq!(forms[2].span, Span::new(7, 11));
assert_eq!(forms[3].span, Span::new(12, 15));
let plain: Vec<Sexp> = forms.iter().map(Spanned::to_sexp).collect();
assert_eq!(plain, read(src).unwrap());
}
#[test]
fn spanned_list_outer_span_covers_parens() {
let src = "(a b c)";
let forms = read_spanned(src).unwrap();
assert_eq!(forms[0].span, Span::new(0, 7));
let SpannedForm::List(children) = &forms[0].form else {
panic!("expected list")
};
assert_eq!(children[0].span, Span::new(1, 2));
assert_eq!(children[1].span, Span::new(3, 4));
assert_eq!(children[2].span, Span::new(5, 6));
}
#[test]
fn spanned_comments_and_whitespace_skipped() {
let src = "; header\n(a b) ; inline\n";
let forms = read_spanned(src).unwrap();
assert_eq!(forms.len(), 1);
let start = src.find('(').unwrap();
let end = src.find(')').unwrap() + 1;
assert_eq!(forms[0].span, Span::new(start, end));
}
#[test]
fn spanned_quote_span_covers_tick_and_inner() {
let src = "'(a b)";
let forms = read_spanned(src).unwrap();
assert_eq!(forms[0].span, Span::new(0, 6));
let SpannedForm::Quote(inner) = &forms[0].form else {
panic!("expected quote")
};
assert_eq!(inner.span, Span::new(1, 6));
}
#[test]
fn spanned_nested_lists_have_proper_containment() {
let src = "(a (b c) d)";
let forms = read_spanned(src).unwrap();
let outer = forms[0].span;
let SpannedForm::List(children) = &forms[0].form else {
panic!()
};
let inner = children[1].span;
assert!(outer.start <= inner.start);
assert!(inner.end <= outer.end);
assert!(inner.start > outer.start);
assert!(inner.end < outer.end);
}
#[test]
fn spanned_reader_threads_every_prefix_through_spanned_form_wrap() {
for qf in QuoteForm::ALL {
let src = format!("{}payload", qf.prefix());
let forms = read_spanned(&src).expect(&src);
assert_eq!(forms.len(), 1);
assert_eq!(
forms[0].to_sexp(),
qf.wrap(Sexp::symbol("payload")),
"{src}: spanned wrap table drifted from QuoteForm::wrap"
);
assert_eq!(forms[0].span, Span::new(0, src.len()));
}
}
}
#[cfg(test)]
mod str_round_trip {
use crate::read_spanned;
#[test]
fn every_string_survives_a_display_read_round_trip() {
let payloads = [
"",
"plain",
"with space",
"quote\" inside",
"back\\slash",
"newline\nhere",
"tab\there",
"nul\0byte",
"accented é",
"emoji 😀",
"combining e\u{301}",
"zero\u{200B}width",
"rtl \u{202E} mark",
"cr\rlf",
];
for payload in payloads {
let sexp = crate::Sexp::Atom(crate::Atom::Str(payload.to_string()));
let text = sexp.to_string();
let forms = read_spanned(&text)
.unwrap_or_else(|e| panic!("{payload:?} rendered as {text:?} would not re-read: {e:?}"));
let got = match forms.first().map(|f| &f.form) {
Some(crate::SpannedForm::Atom(crate::Atom::Str(s))) => s.clone(),
other => panic!("{payload:?} came back as {other:?}"),
};
assert_eq!(
got, payload,
"round trip changed the payload\n wrote: {text:?}\n read: {got:?}"
);
}
}
#[test]
fn nul_survives_a_round_trip() {
let sexp = crate::Sexp::Atom(crate::Atom::Str("nul\0byte".to_string()));
let forms = read_spanned(&sexp.to_string()).expect("re-read");
let got = match forms.first().map(|f| &f.form) {
Some(crate::SpannedForm::Atom(crate::Atom::Str(s))) => s.clone(),
other => panic!("got {other:?}"),
};
assert_eq!(got, "nul\0byte");
}
#[test]
fn a_literal_backslash_zero_in_source_is_still_the_digit() {
let forms = read_spanned(r#""a\0b""#).expect("read");
let got = match forms.first().map(|f| &f.form) {
Some(crate::SpannedForm::Atom(crate::Atom::Str(s))) => s.clone(),
other => panic!("got {other:?}"),
};
assert_eq!(got, "a0b", "the reader's meaning for \\0 must not have moved");
}
#[test]
fn a_malformed_unicode_escape_is_not_silently_substituted() {
for text in [r#""\u{zzzz}""#, r#""\u{D800}""#, r#""\u{110000}""#] {
let forms = read_spanned(text).expect("must still read");
let got = match forms.first().map(|f| &f.form) {
Some(crate::SpannedForm::Atom(crate::Atom::Str(s))) => s.clone(),
other => panic!("got {other:?}"),
};
assert!(
!got.contains('\u{FFFD}'),
"{text} must not become a replacement character: {got:?}"
);
}
}
}