use super::*; fn read_one(source: &str) -> Sexpr { let doc = read(source).expect("reads"); assert_eq!(doc.values.len(), 1, "expected exactly one value"); doc.values.into_iter().next().unwrap() } #[test] fn atoms() { assert_eq!( read_one("hello"), Sexpr::Symbol("hello".into(), Span::new(0, 5)) ); assert_eq!( read_one(":class"), Sexpr::Keyword("class".into(), Span::new(0, 6)) ); assert_eq!(read_one("42"), Sexpr::Int(42, Span::new(0, 2))); assert_eq!(read_one("-7"), Sexpr::Int(-7, Span::new(0, 2))); assert_eq!(read_one("2.5"), Sexpr::Float(2.5, Span::new(0, 3))); assert_eq!(read_one("\"hi\""), Sexpr::Str("hi".into(), Span::new(0, 4))); // Operator and kebab symbols. assert_eq!(read_one("+"), Sexpr::Symbol("+".into(), Span::new(0, 1))); assert_eq!( read_one("flex-start"), Sexpr::Symbol("flex-start".into(), Span::new(0, 10)) ); } #[test] fn string_escapes() { assert_eq!( read_one(r#""a\nb""#), Sexpr::Str("a\nb".into(), Span::new(0, 6)) ); assert_eq!(read_one(r#""say \"hi\"""#).as_symbol(), None); let Sexpr::Str(s, _) = read_one(r#""say \"hi\"""#) else { panic!() }; assert_eq!(s, "say \"hi\""); let Sexpr::Str(s, _) = read_one(r#""\{not-interp""#) else { panic!() }; assert_eq!(s, "{not-interp"); // Unicode passes through. let Sexpr::Str(s, _) = read_one("\"Ω中\"") else { panic!() }; assert_eq!(s, "Ω中"); } #[test] fn nested_lists_and_spans() { let value = read_one("(a (b 1) :k \"s\")"); let Sexpr::List(items, span) = &value else { panic!() }; assert_eq!(*span, Span::new(0, 16)); assert_eq!(items.len(), 4); let Sexpr::List(inner, inner_span) = &items[1] else { panic!() }; assert_eq!(*inner_span, Span::new(3, 8)); assert_eq!(inner[1], Sexpr::Int(1, Span::new(6, 7))); } #[test] fn comments_are_recorded_not_parsed() { let doc = read("; header\n(a) ; trailing\n(b)").expect("reads"); assert_eq!(doc.values.len(), 2); assert_eq!(doc.comments.len(), 2); assert_eq!(doc.comments[0].text, "; header"); assert_eq!(doc.comments[0].span, Span::new(0, 8)); assert_eq!(doc.comments[1].text, "; trailing"); } #[test] fn keyword_adjacent_to_value() { // `:gap 8` inside a list; also keywords terminate symbols. let value = read_one("(box :gap 8)"); let items = value.as_list().unwrap(); assert_eq!(items[1], Sexpr::Keyword("gap".into(), Span::new(5, 9))); assert_eq!(items[2], Sexpr::Int(8, Span::new(10, 11))); } #[test] fn errors_have_positions() { let err = read("(a (b)").unwrap_err(); assert!(err.message.contains("unclosed")); assert_eq!(err.span, Span::new(0, 1), "points at the opening paren"); let err = read("\"oops").unwrap_err(); assert!(err.message.contains("unterminated string")); let err = read("(a))").unwrap_err(); assert!(err.message.contains("unmatched")); assert_eq!(err.span, Span::new(3, 4)); let err = read(r#""bad \q escape""#).unwrap_err(); assert!(err.message.contains("unknown escape")); } #[test] fn negative_symbol_vs_number() { // A lone `-` or `-x` is a symbol; `-3` is a number. assert_eq!(read_one("-"), Sexpr::Symbol("-".into(), Span::new(0, 1))); assert_eq!(read_one("-3"), Sexpr::Int(-3, Span::new(0, 2))); }