Lines
99.74 %
Functions
100 %
Branches
//! Reader tests covering nil/bool/numbers/strings/symbols/lists/quasi/byte-vectors.
use base64::Engine;
use base64::engine::general_purpose::STANDARD as BASE64_STANDARD;
use crate::ast::{Expr, Fraction};
use super::Reader;
#[test]
fn test_parse_nil() {
let program = Reader::parse("nil").unwrap();
assert_eq!(program.exprs, vec![Expr::Nil]);
}
fn test_parse_bool() {
let program = Reader::parse("#t #T t T").unwrap();
assert_eq!(
program.exprs,
vec![
Expr::Bool(true),
]
);
fn test_parse_false_is_nil() {
let program = Reader::parse("#f #F").unwrap();
assert_eq!(program.exprs, vec![Expr::Nil, Expr::Nil]);
fn test_parse_nil_case_insensitive() {
assert_eq!(Reader::parse("nil").unwrap().exprs, vec![Expr::Nil]);
assert_eq!(Reader::parse("NIL").unwrap().exprs, vec![Expr::Nil]);
assert_eq!(Reader::parse("Nil").unwrap().exprs, vec![Expr::Nil]);
fn test_parse_integer() {
let program = Reader::parse("42").unwrap();
vec![Expr::Number(Fraction::from_integer(42))]
fn test_parse_negative_number() {
let program = Reader::parse("-17").unwrap();
vec![Expr::Number(Fraction::from_integer(-17))]
fn test_parse_decimal() {
let program = Reader::parse("0.1").unwrap();
assert_eq!(program.exprs, vec![Expr::Number(Fraction::new(1, 10))]);
fn test_parse_rational_simple() {
let program = Reader::parse("1/2").unwrap();
assert_eq!(program.exprs, vec![Expr::Number(Fraction::new(1, 2))]);
fn test_parse_rational_negative() {
let program = Reader::parse("-3/4").unwrap();
assert_eq!(program.exprs, vec![Expr::Number(Fraction::new(-3, 4))]);
fn test_parse_rational_reduces() {
let program = Reader::parse("4/8").unwrap();
fn test_parse_rational_in_list() {
let program = Reader::parse("(+ 1/2 1/3)").unwrap();
vec![Expr::List(vec![
Expr::Symbol("+".into()),
Expr::Number(Fraction::new(1, 2)),
Expr::Number(Fraction::new(1, 3)),
])]
fn test_parse_rational_zero_denominator_rejected() {
let result = Reader::parse("1/0");
assert!(result.is_err());
fn test_parse_integer_then_division_call() {
let program = Reader::parse("(/ 1 2)").unwrap();
Expr::Symbol("/".into()),
Expr::Number(Fraction::from_integer(1)),
Expr::Number(Fraction::from_integer(2)),
fn test_parse_symbol() {
let program = Reader::parse("foo").unwrap();
assert_eq!(program.exprs, vec![Expr::Symbol("FOO".into())]);
fn test_parse_symbol_case_insensitive() {
Reader::parse("foo").unwrap().exprs,
Reader::parse("FOO").unwrap().exprs
Reader::parse("Sum").unwrap().exprs,
Reader::parse("SUM").unwrap().exprs
fn test_parse_keyword() {
let program = Reader::parse(":foo").unwrap();
assert_eq!(program.exprs, vec![Expr::Keyword("FOO".into())]);
fn test_parse_keyword_case_insensitive() {
Reader::parse(":foo").unwrap().exprs,
Reader::parse(":FOO").unwrap().exprs
Reader::parse(":Name").unwrap().exprs,
Reader::parse(":NAME").unwrap().exprs
// ADR-0029 colon-namespace grammar.
fn qualified_symbol_single_colon() {
let program = Reader::parse("finance:add-money").unwrap();
vec![Expr::Symbol("FINANCE:ADD-MONEY".into())]
fn qualified_symbol_double_colon_folds_to_single() {
// `::` is accepted as input syntax but folds to the canonical single-colon
// key — same binding (the `:`/`::` distinction is reserved for the future).
Reader::parse("finance::add-money").unwrap().exprs,
Reader::parse("finance:add-money").unwrap().exprs
fn qualified_symbol_case_insensitive() {
Reader::parse("Finance:Add-Money").unwrap().exprs,
fn qualified_symbol_as_list_head() {
let program = Reader::parse("(split:list-for-transaction tx)").unwrap();
Expr::Symbol("SPLIT:LIST-FOR-TRANSACTION".into()),
Expr::Symbol("TX".into()),
fn leading_colon_still_keyword() {
// Regression: a leading colon is a keyword, never a qualified symbol.
vec![Expr::Keyword("FOO".into())]
fn qualified_symbol_under_quote_and_quasiquote() {
Reader::parse("'finance:x").unwrap().exprs,
vec![Expr::Quote(Box::new(Expr::Symbol("FINANCE:X".into())))]
Reader::parse("`finance:x").unwrap().exprs,
vec![Expr::Quasiquote(Box::new(Expr::Symbol("FINANCE:X".into())))]
fn mixed_qualified_and_keyword_in_list() {
let program = Reader::parse("(a finance:b :c)").unwrap();
Expr::Symbol("A".into()),
Expr::Symbol("FINANCE:B".into()),
Expr::Keyword("C".into()),
fn trailing_colon_is_error() {
assert!(Reader::parse("foo:").is_err());
assert!(Reader::parse("foo::").is_err());
fn multiple_separators_are_error() {
assert!(Reader::parse("a:b:c").is_err());
assert!(Reader::parse("a::b::c").is_err());
assert!(Reader::parse("a:b::c").is_err());
fn leading_double_colon_and_bare_colon_are_error() {
assert!(Reader::parse("::foo").is_err());
assert!(Reader::parse(":").is_err());
fn test_parse_string() {
let program = Reader::parse(r#""hello""#).unwrap();
assert_eq!(program.exprs, vec![Expr::String("hello".into())]);
fn test_parse_string_with_escapes() {
let program = Reader::parse(r#""hello\nworld""#).unwrap();
assert_eq!(program.exprs, vec![Expr::String("hello\nworld".into())]);
fn test_parse_list() {
let program = Reader::parse("(+ 1 2)").unwrap();
fn test_parse_nested_list() {
let program = Reader::parse("(define (square x) (* x x))").unwrap();
Expr::Symbol("DEFINE".into()),
Expr::List(vec![
Expr::Symbol("SQUARE".into()),
Expr::Symbol("X".into()),
]),
Expr::Symbol("*".into()),
fn test_parse_quote() {
let program = Reader::parse("'foo").unwrap();
vec![Expr::Quote(Box::new(Expr::Symbol("FOO".into())))]
fn test_parse_quoted_list() {
let program = Reader::parse("'(1 2 3)").unwrap();
vec![Expr::Quote(Box::new(Expr::List(vec![
Expr::Number(Fraction::from_integer(3)),
])))]
fn test_parse_multiple_exprs() {
let program = Reader::parse("(DEFINE x 10) (+ x 5)").unwrap();
assert_eq!(program.exprs.len(), 2);
fn test_parse_line_comment() {
let program = Reader::parse("; this is a comment\n42").unwrap();
fn test_parse_inline_comment() {
let program = Reader::parse("42 ; inline comment").unwrap();
fn test_parse_comment_in_list() {
let program = Reader::parse("(+ 1 ; add one\n 2)").unwrap();
fn test_parse_multiple_comments() {
let code = "; comment 1
; comment 2
42
; comment 3";
let program = Reader::parse(code).unwrap();
fn test_parse_only_comments() {
let program = Reader::parse("; just a comment").unwrap();
assert!(program.exprs.is_empty());
fn test_parse_annotation() {
let code = "; @test (= (count 'defun) 5)\n42";
assert_eq!(program.exprs.len(), 1);
assert_eq!(program.annotations.len(), 1);
assert_eq!(program.annotations[0].name, "test");
assert!(matches!(&program.annotations[0].value, Expr::List(_)));
fn test_parse_multiple_annotations() {
let code = "; @test (= (count 'defun) 5)\n; @test (= (count 'defvar) 2)\n42";
assert_eq!(program.annotations.len(), 2);
fn test_annotation_with_simple_expr() {
let code = "; @version 1\n42";
assert_eq!(program.annotations[0].name, "version");
program.annotations[0].value,
Expr::Number(Fraction::from_integer(1))
fn test_regular_comment_no_annotation() {
let code = "; just a comment\n42";
assert!(program.annotations.is_empty());
fn test_parse_cons() {
let program = Reader::parse("(a . b)").unwrap();
vec![Expr::cons(
Expr::Symbol("B".into())
)]
fn test_parse_cons_proper_list() {
let program = Reader::parse("(a . (b . nil))").unwrap();
Expr::cons(Expr::Symbol("B".into()), Expr::Nil)
fn test_parse_improper_list() {
let program = Reader::parse("(1 2 . 3)").unwrap();
Expr::cons(
Expr::Number(Fraction::from_integer(3))
)
fn test_invalid_dotted_pair_multiple_exprs_after_dot() {
let result = Reader::parse("(1 . (2 . 3) (3 . nil))");
let err = result.unwrap_err();
let msg = err.to_string();
assert!(
msg.contains("closing paren") || msg.contains("one expression after dot"),
"Error should mention closing paren or one expression after dot, got: {msg}"
fn test_parse_quasiquote() {
let program = Reader::parse("`foo").unwrap();
vec![Expr::Quasiquote(Box::new(Expr::Symbol("FOO".into())))]
fn test_parse_unquote() {
let program = Reader::parse(",foo").unwrap();
vec![Expr::Unquote(Box::new(Expr::Symbol("FOO".into())))]
fn test_parse_unquote_splicing() {
let program = Reader::parse(",@foo").unwrap();
vec![Expr::UnquoteSplicing(Box::new(Expr::Symbol("FOO".into())))]
fn test_parse_quasiquoted_list() {
let program = Reader::parse("`(if ,test ,body)").unwrap();
vec![Expr::Quasiquote(Box::new(Expr::List(vec![
Expr::Symbol("IF".into()),
Expr::Unquote(Box::new(Expr::Symbol("TEST".into()))),
Expr::Unquote(Box::new(Expr::Symbol("BODY".into()))),
fn test_parse_byte_vector_empty() {
let program = Reader::parse("#u8()").unwrap();
assert_eq!(program.exprs, vec![Expr::Bytes(Vec::new())]);
fn test_parse_byte_vector_simple() {
let program = Reader::parse("#u8(0 1 255)").unwrap();
assert_eq!(program.exprs, vec![Expr::Bytes(vec![0, 1, 255])]);
fn test_parse_byte_vector_with_whitespace() {
let program = Reader::parse("#u8( 10 20 30 )").unwrap();
assert_eq!(program.exprs, vec![Expr::Bytes(vec![10, 20, 30])]);
fn test_parse_byte_vector_full_byte_range() {
let program = Reader::parse("#u8(0 127 128 255)").unwrap();
assert_eq!(program.exprs, vec![Expr::Bytes(vec![0, 127, 128, 255])]);
fn test_parse_byte_vector_overflow_rejected() {
let result = Reader::parse("#u8(256)");
fn test_parse_byte_vector_unclosed_rejected() {
let result = Reader::parse("#u8(1 2 3");
fn test_parse_base64_empty() {
let program = Reader::parse("#\"\"").unwrap();
fn test_parse_base64_simple() {
let program = Reader::parse("#\"aGVsbG8=\"").unwrap();
assert_eq!(program.exprs, vec![Expr::Bytes(b"hello".to_vec())]);
fn test_parse_base64_full_byte_range() {
let mut bytes: Vec<u8> = (0u8..=255).collect();
let encoded = BASE64_STANDARD.encode(&bytes);
let source = format!("#\"{encoded}\"");
let program = Reader::parse(&source).unwrap();
let parsed = match program.exprs.as_slice() {
[Expr::Bytes(b)] => b.clone(),
other => panic!("expected single bytes expression, got {other:?}"),
};
bytes.sort();
let mut parsed_sorted = parsed.clone();
parsed_sorted.sort();
assert_eq!(parsed_sorted, bytes);
fn test_parse_base64_invalid_rejected() {
let result = Reader::parse("#\"not!base64!\"");
fn test_parse_base64_unclosed_rejected() {
let result = Reader::parse("#\"aGVsbG8=");
fn test_byte_vector_in_list() {
let program = Reader::parse("(blob #u8(1 2 3) :tag)").unwrap();
Expr::Symbol("BLOB".into()),
Expr::Bytes(vec![1, 2, 3]),
Expr::Keyword("TAG".into()),
fn test_bool_still_parses_after_hash_dispatch() {
let program = Reader::parse("#t #f #T #F").unwrap();
vec![Expr::Bool(true), Expr::Nil, Expr::Bool(true), Expr::Nil,]