use chappi_syntax::{Atom, FormKind, canonicalize, parse_document};
use proptest::prelude::*;
#[test]
fn reports_each_malformed_top_level_line_with_its_source_span() {
let errors = parse_document("(broken \"\\q\")\n)\n(claim good)\n")
.expect_err("malformed lines must be rejected");
assert_eq!(errors.len(), 2);
assert_eq!(errors[0].code, "invalid-escape");
assert_eq!(errors[0].span.start_line, 1);
assert_eq!(errors[1].code, "unexpected-close-paren");
assert_eq!(errors[1].span.start_line, 2);
}
#[test]
fn raw_string_line_break_does_not_discard_the_next_top_level_line() {
let errors = parse_document("(broken \"line one\n)\n")
.expect_err("raw line breaks in strings must be rejected");
assert_eq!(errors.len(), 2);
assert_eq!(errors[0].code, "unclosed-string");
assert_eq!(errors[0].span.start_line, 1);
assert_eq!(errors[1].code, "unexpected-close-paren");
assert_eq!(errors[1].span.start_line, 2);
}
#[test]
fn unterminated_unicode_escape_does_not_discard_the_next_top_level_line() {
let errors = parse_document("(broken \"\\u{1\n)\n")
.expect_err("unterminated Unicode escapes must be rejected");
assert_eq!(errors.len(), 2);
assert_eq!(errors[0].code, "invalid-unicode-escape");
assert_eq!(errors[0].span.start_line, 1);
assert_eq!(errors[1].code, "unexpected-close-paren");
assert_eq!(errors[1].span.start_line, 2);
}
#[test]
fn excessive_nesting_is_rejected_with_a_stable_diagnostic_and_line_recovery() {
let source = format!("{}x{}\n)\n", "(".repeat(50_000), ")".repeat(50_000));
let errors = parse_document(&source).expect_err("excessive nesting must be rejected");
assert_eq!(errors.len(), 2);
assert_eq!(errors[0].code, "nesting-depth-exceeded");
assert_eq!(errors[0].severity, chappi_syntax::Severity::Error);
assert_eq!(errors[0].span.start, 256);
assert_eq!(errors[0].span.end, 257);
assert_eq!(errors[0].span.start_line, 1);
assert_eq!(errors[0].span.start_column, 257);
assert!(errors[0].semantic_path.is_empty());
assert_eq!(
errors[0].message,
"S-expression nesting exceeds the Chappi v1 limit of 256 lists"
);
assert_eq!(
errors[0].hint.as_deref(),
Some("Reduce list nesting to at most 256 levels.")
);
assert_eq!(errors[1].code, "unexpected-close-paren");
assert_eq!(errors[1].span.start_line, 2);
}
#[test]
fn nesting_at_the_v1_limit_remains_valid() {
let source = format!("{}x{}\n", "(".repeat(256), ")".repeat(256));
let document = parse_document(&source).expect("256 nested lists are valid in Chappi v1");
assert_eq!(document.forms.len(), 1);
}
#[test]
fn parses_comments_and_all_atom_kinds_with_spans() {
let document = parse_document("; before\n(item symbol -002 03.40 \"a\\n\\u{301}\") ; after\n")
.expect("comments and atoms should parse");
let FormKind::List(items) = &document.forms[0].kind else {
panic!("top-level form must be a list");
};
assert!(matches!(items[1].kind, FormKind::Atom(Atom::Symbol(ref value)) if value == "symbol"));
assert!(matches!(items[2].kind, FormKind::Atom(Atom::Integer(ref value)) if value == "-002"));
assert!(matches!(items[3].kind, FormKind::Atom(Atom::Decimal(ref value)) if value == "03.40"));
assert!(
matches!(items[4].kind, FormKind::Atom(Atom::String(ref value)) if value == "a\n\u{301}")
);
assert_eq!(document.forms[0].span.start_line, 2);
assert_eq!(document.forms[0].span.start_column, 1);
}
fn escaped_source_string(value: &str) -> String {
value
.chars()
.flat_map(|character| match character {
'"' => "\\\"".chars().collect::<Vec<_>>(),
'\\' => "\\\\".chars().collect::<Vec<_>>(),
'\n' => "\\n".chars().collect::<Vec<_>>(),
'\r' => "\\r".chars().collect::<Vec<_>>(),
'\t' => "\\t".chars().collect::<Vec<_>>(),
_ => vec![character],
})
.collect()
}
proptest! {
#[test]
fn canonical_output_round_trips_for_valid_generic_forms(
symbol in "[a-z][a-z0-9_-]{0,8}",
value in "[A-Za-z0-9 .,!?\\\"\\\\\\n\\t]{0,24}",
) {
let source = format!("(entity {symbol} \"{}\")\n", escaped_source_string(&value));
let document = parse_document(&source).expect("generated source is valid");
let canonical = canonicalize(&document);
let reparsed = parse_document(std::str::from_utf8(&canonical).expect("canonical source is UTF-8"))
.expect("canonical output is valid source");
prop_assert_eq!(canonical, canonicalize(&reparsed));
}
}