use lispexp::{parse, Datum, DatumKind, Delim, Dialect, Notation, Options, Prefix};
fn scheme(src: &str) -> Vec<Datum<'_>> {
let parsed = parse(src, &Options::scheme());
assert!(
parsed.errors.is_empty(),
"unexpected errors: {:?}",
parsed.errors
);
parsed.data
}
fn kinds<'a>(data: &'a [Datum<'a>]) -> Vec<&'a DatumKind<'a>> {
data.iter().map(|d| &d.kind).collect()
}
#[test]
fn atoms_symbol_and_number() {
let data = scheme("foo 42 -3.14 .5 list->vector +");
assert_eq!(
kinds(&data),
vec![
&DatumKind::Symbol("foo"),
&DatumKind::Number("42"),
&DatumKind::Number("-3.14"),
&DatumKind::Number(".5"),
&DatumKind::Symbol("list->vector"),
&DatumKind::Symbol("+"),
]
);
}
#[test]
fn strings_and_chars_and_bools() {
let data = scheme(r#""hi\n" #\a #\space #t #false"#);
assert_eq!(
kinds(&data),
vec![
&DatumKind::Str("\"hi\\n\""),
&DatumKind::Char("#\\a"),
&DatumKind::Char("#\\space"),
&DatumKind::Bool(true),
&DatumKind::Bool(false),
]
);
}
#[test]
fn nested_list() {
let data = scheme("(define (f x) (* x x))");
assert_eq!(data.len(), 1);
let DatumKind::List {
delim, items, tail, ..
} = &data[0].kind
else {
panic!("expected list");
};
assert_eq!(*delim, Delim::Round);
assert!(tail.is_none());
assert_eq!(items.len(), 3);
assert_eq!(items[0].kind, DatumKind::Symbol("define"));
}
#[test]
fn square_brackets_are_lists() {
let data = scheme("(let ([x 1]) x)");
let DatumKind::List { items, .. } = &data[0].kind else {
panic!()
};
let DatumKind::List { items: binds, .. } = &items[1].kind else {
panic!("expected bindings list")
};
let DatumKind::List { delim, .. } = &binds[0].kind else {
panic!("expected binding")
};
assert_eq!(*delim, Delim::Square);
}
#[test]
fn dotted_pair() {
let data = scheme("(a . b)");
let DatumKind::List { items, tail, .. } = &data[0].kind else {
panic!()
};
assert_eq!(items.len(), 1);
assert_eq!(items[0].kind, DatumKind::Symbol("a"));
assert_eq!(tail.as_ref().unwrap().kind, DatumKind::Symbol("b"));
}
#[test]
fn dotted_pair_records_dot_span() {
let src = "(a . b)";
let data = scheme(src);
let dot = data[0].dot_span().expect("dotted list has a dot span");
assert_eq!(dot.text(src), ".");
assert_eq!(&src[dot.start as usize..dot.end as usize], ".");
let DatumKind::List { dot: field, .. } = &data[0].kind else {
panic!()
};
assert_eq!(*field, Some(dot));
}
#[test]
fn proper_list_has_no_dot_span() {
let data = scheme("(a b c)");
assert_eq!(data[0].dot_span(), None);
let DatumKind::List { dot, tail, .. } = &data[0].kind else {
panic!()
};
assert!(dot.is_none());
assert!(tail.is_none());
}
#[test]
fn non_list_has_no_dot_span() {
let data = scheme("sym");
assert_eq!(data[0].dot_span(), None);
}
#[test]
fn dot_span_points_at_the_dot_column() {
let src = "(eval . form)";
let data = scheme(src);
let dot = data[0].dot_span().unwrap();
assert_eq!(dot.text(src), ".");
assert_eq!(dot.start as usize, src.find('.').unwrap());
}
#[test]
fn quote_shorthand() {
let data = scheme("'(1 2)");
let DatumKind::Prefixed {
prefix,
notation,
inner,
..
} = &data[0].kind
else {
panic!("expected prefixed")
};
assert_eq!(*prefix, Prefix::Quote);
assert_eq!(*notation, Notation::Shorthand);
assert!(matches!(inner.kind, DatumKind::List { .. }));
}
#[test]
fn quote_longhand_folds() {
let data = scheme("(quote x)");
let DatumKind::Prefixed {
prefix,
notation,
inner,
..
} = &data[0].kind
else {
panic!("expected prefixed from longhand fold")
};
assert_eq!(*prefix, Prefix::Quote);
assert_eq!(*notation, Notation::Longhand);
assert_eq!(inner.kind, DatumKind::Symbol("x"));
}
#[test]
fn quasiquote_unquote_splicing() {
let data = scheme("`(a ,b ,@c)");
let DatumKind::Prefixed { prefix, inner, .. } = &data[0].kind else {
panic!()
};
assert_eq!(*prefix, Prefix::Quasiquote);
let DatumKind::List { items, .. } = &inner.kind else {
panic!()
};
assert!(matches!(
items[1].kind,
DatumKind::Prefixed {
prefix: Prefix::Unquote,
..
}
));
assert!(matches!(
items[2].kind,
DatumKind::Prefixed {
prefix: Prefix::UnquoteSplicing,
..
}
));
}
#[test]
fn vector_hash_literal() {
let data = scheme("#(1 2 3)");
let DatumKind::HashLiteral { tag, inner } = &data[0].kind else {
panic!("expected hash literal")
};
assert_eq!(*tag, "");
let DatumKind::List { items, .. } = &inner.as_ref().unwrap().kind else {
panic!()
};
assert_eq!(items.len(), 3);
}
#[test]
fn bytevector_tag() {
let data = scheme("#u8(1 2)");
let DatumKind::HashLiteral { tag, .. } = &data[0].kind else {
panic!()
};
assert_eq!(*tag, "u8");
}
#[test]
fn line_and_block_and_datum_comments() {
let src = "; a line comment\n(a #| block #| nested |# still |# b) #;(discarded) c";
let data = scheme(src);
assert_eq!(data.len(), 2);
let DatumKind::List { items, .. } = &data[0].kind else {
panic!()
};
assert_eq!(
kinds(items),
vec![&DatumKind::Symbol("a"), &DatumKind::Symbol("b")]
);
assert_eq!(data[1].kind, DatumKind::Symbol("c"));
}
#[test]
fn radix_numbers() {
let data = scheme("#xFF #b1010 #e1.5");
assert_eq!(
kinds(&data),
vec![
&DatumKind::Number("#xFF"),
&DatumKind::Number("#b1010"),
&DatumKind::Number("#e1.5"),
]
);
}
#[test]
fn piped_symbol() {
let data = scheme("|hello world|");
assert_eq!(data[0].kind, DatumKind::Symbol("|hello world|"));
}
#[test]
fn datum_labels() {
let data = scheme("#0=(a . #0#)");
let DatumKind::Label { id, inner } = &data[0].kind else {
panic!("expected label")
};
assert_eq!(*id, "0");
let DatumKind::List { tail, .. } = &inner.kind else {
panic!()
};
assert_eq!(tail.as_ref().unwrap().kind, DatumKind::LabelRef { id: "0" });
}
#[test]
fn unicode_symbol() {
let data = scheme("(λ (x) x)");
let DatumKind::List { items, .. } = &data[0].kind else {
panic!()
};
assert_eq!(items[0].kind, DatumKind::Symbol("λ"));
}
#[test]
fn line_numbers() {
let data = scheme("a\n(b\n c)\nd");
assert_eq!(data[0].line, 1); assert_eq!(data[1].line, 2); assert_eq!(data[2].line, 4); }
#[test]
fn hash_vector_inner_quote_not_folded() {
let data = scheme("#(quote x)");
let DatumKind::HashLiteral { tag, inner } = &data[0].kind else {
panic!("expected hash literal, got {:?}", data[0].kind)
};
assert_eq!(*tag, "");
let inner = inner.as_ref().expect("vector has an inner list");
let DatumKind::List { items, .. } = &inner.kind else {
panic!("expected inner list, got {:?}", inner.kind)
};
assert_eq!(items.len(), 2);
assert_eq!(items[0].kind, DatumKind::Symbol("quote"));
assert_eq!(items[1].kind, DatumKind::Symbol("x"));
}
#[test]
fn bool_requires_terminator() {
let data = scheme("#thing");
assert_eq!(data.len(), 1, "expected one atom, got {:?}", data);
assert!(
!matches!(data[0].kind, DatumKind::Bool(_)),
"`#thing` must not lex as a boolean: {:?}",
data[0].kind
);
assert_eq!(scheme("#t #f #true #false").len(), 4);
assert_eq!(scheme("#t")[0].kind, DatumKind::Bool(true));
assert_eq!(scheme("#false")[0].kind, DatumKind::Bool(false));
}
#[test]
fn srfi4_bytevector_tag_is_one_hash_literal() {
let data = scheme("#f64(1.0 2.0)");
assert_eq!(data.len(), 1, "expected one datum, got {:?}", data);
let DatumKind::HashLiteral { tag, inner } = &data[0].kind else {
panic!("expected hash literal, got {:?}", data[0].kind)
};
assert_eq!(*tag, "f64");
let DatumKind::List { items, .. } = &inner.as_ref().unwrap().kind else {
panic!("expected inner list")
};
assert_eq!(items.len(), 2);
}
#[test]
fn radix_r_number_classifies() {
let data = scheme("#36rHELLO #2r1010");
assert_eq!(data[0].kind, DatumKind::Number("#36rHELLO"));
assert_eq!(data[1].kind, DatumKind::Number("#2r1010"));
}
#[test]
fn digit_led_symbols_are_symbols() {
let data = scheme("1+ 1- 1x 1 1.5 -2/3 1e-5 .5 #xFF");
let kinds: Vec<_> = data.iter().map(|d| &d.kind).collect();
assert_eq!(*kinds[0], DatumKind::Symbol("1+"));
assert_eq!(*kinds[1], DatumKind::Symbol("1-"));
assert_eq!(*kinds[2], DatumKind::Symbol("1x"));
assert_eq!(*kinds[3], DatumKind::Number("1"));
assert_eq!(*kinds[4], DatumKind::Number("1.5"));
assert_eq!(*kinds[5], DatumKind::Number("-2/3"));
assert_eq!(*kinds[6], DatumKind::Number("1e-5"));
assert_eq!(*kinds[7], DatumKind::Number(".5"));
assert_eq!(*kinds[8], DatumKind::Number("#xFF"));
}
#[test]
fn datum_accessors() {
let data = scheme(r#"foo :kw 42 "s" #\a (define (f x) x)"#);
assert_eq!(data[0].as_symbol(), Some("foo"));
assert_eq!(data[0].as_keyword(), None);
assert_eq!(data[1].as_symbol(), Some(":kw"));
assert_eq!(data[2].as_number(), Some("42"));
assert_eq!(data[2].as_symbol(), None);
assert_eq!(data[3].as_str(), Some("\"s\""));
assert_eq!(data[4].as_char(), Some("#\\a"));
let define = &data[5];
assert_eq!(define.head_symbol(), Some("define"));
let items = define.items().expect("list has items");
assert_eq!(items.len(), 3);
assert_eq!(items[0].as_symbol(), Some("define"));
assert_eq!(data[0].items(), None);
assert_eq!(data[0].head_symbol(), None);
}
#[test]
fn as_keyword_on_a_dialect_with_keywords() {
let parsed = parse(":foo", &Options::for_dialect(Dialect::Clojure));
assert!(parsed.errors.is_empty());
assert_eq!(parsed.data[0].as_keyword(), Some(":foo"));
assert_eq!(parsed.data[0].as_symbol(), None);
}
#[test]
fn datum_text_sugar() {
let source = "(+ 1 2)";
let data = scheme(source);
assert_eq!(data[0].text(source), "(+ 1 2)");
let items = data[0].items().unwrap();
assert_eq!(items[0].text(source), "+");
}
#[test]
fn hash_tag_with_reader_macro_glyph_is_one_hash_literal() {
let data = scheme("#`(lambda (x) x)");
let DatumKind::HashLiteral { tag, inner } = &data[0].kind else {
panic!("expected HashLiteral, got {:?}", data[0].kind);
};
assert_eq!(*tag, "`");
assert!(matches!(
inner.as_deref().map(|d| &d.kind),
Some(DatumKind::List {
delim: Delim::Round,
..
})
));
let data = scheme("#,(x)");
assert!(matches!(
&data[0].kind,
DatumKind::HashLiteral { tag: ",", .. }
));
}
#[test]
fn hash_tag_without_a_delimiter_is_a_symbol() {
let data = scheme("#`x");
assert_eq!(data[0].kind, DatumKind::Symbol("#`x"));
}