use crate::s_expr::*;
fn assert_reader_error(input: &str, message_contains: &str, offset: usize)
{
let err = read_s_expr(input).expect_err(&format!(
"expected reader error for {:?} (matching {:?}), got success",
input, message_contains
));
let rendered = err.to_string();
assert!(
rendered.contains(message_contains),
"input {:?}: expected message to contain {:?}, got {:?}",
input,
message_contains,
rendered
);
assert_eq!(
err.location().offset,
offset,
"input {:?}: expected offset {} but got {} (rendered was {:?})",
input,
offset,
err.location().offset,
rendered
);
}
#[test]
fn test_reader_rejects_empty_input()
{
assert_reader_error("", "expected '('", 0);
}
#[test]
fn test_reader_rejects_non_function_top_level_keyword()
{
assert_reader_error(
" (body 42)",
"expected 'function', found 'body'",
6
);
}
#[test]
fn test_reader_rejects_nested_function_keyword()
{
assert_reader_error(
"(function [] (function [] 42))",
"unexpected 'function' inside expression",
14
);
}
#[test]
fn test_reader_rejects_unknown_keyword()
{
assert_reader_error(
"(function [] (bogus 1 2))",
"unknown keyword 'bogus'",
14
);
}
#[test]
fn test_reader_rejects_missing_opening_paren()
{
assert_reader_error("42", "expected '('", 0);
}
#[test]
fn test_reader_rejects_missing_top_level_close_paren()
{
assert_reader_error(
"(function [] 42",
"expected ')', found end of input",
15
);
}
#[test]
fn test_reader_rejects_wrong_char_where_close_paren_expected()
{
assert_reader_error("(function [] 42]", "expected ')', found ']'", 15);
}
#[test]
fn test_reader_rejects_trailing_input()
{
assert_reader_error(
"(function [] 42) extra",
"trailing input: 'extra'",
17
);
}
#[test]
fn test_reader_rejects_empty_compound_form()
{
assert_reader_error("(function [] ( ))", "expected a word", 15);
}
#[test]
fn test_reader_rejects_missing_params_open_bracket()
{
assert_reader_error("(function x 42)", "expected '[', found 'x'", 10);
}
#[test]
fn test_reader_rejects_unterminated_parameter_list()
{
assert_reader_error("(function [x", "expected a word", 12);
}
#[test]
fn test_reader_rejects_unterminated_quoted_identifier()
{
assert_reader_error(
r#"(function ["unterminated"#,
"unterminated quoted identifier",
11
);
}
#[test]
fn test_reader_rejects_invalid_integer_literal()
{
assert_reader_error("(function [] -xyz)", "invalid integer '-xyz'", 13);
}
#[test]
fn test_reader_rejects_integer_overflow()
{
assert_reader_error(
"(function [] 99999999999999)",
"invalid integer '99999999999999'",
13
);
}
#[test]
fn test_reader_rejects_integer_underflow()
{
assert_reader_error(
"(function [] -99999999999999)",
"invalid integer '-99999999999999'",
13
);
}
#[test]
fn test_reader_rejects_empty_custom_dice_faces_list()
{
assert_reader_error(
"(function [] (custom-dice 1 []))",
"custom dice faces list must not be empty",
30
);
}
#[test]
fn test_reader_rejects_invalid_face_value()
{
assert_reader_error(
"(function [] (custom-dice 1 [1 xyz 3]))",
"invalid integer 'xyz'",
31
);
}
#[test]
fn test_reader_rejects_non_dice_child_of_drop_lowest()
{
assert_reader_error(
"(function [] (drop-lowest 5))",
"expected dice expression",
25
);
}
#[test]
fn test_reader_rejects_non_dice_child_of_drop_highest()
{
assert_reader_error(
"(function [] (drop-highest 5))",
"expected dice expression",
26
);
}
#[test]
fn test_reader_rejects_inverted_span_prefix()
{
assert_reader_error(
"^[5 2] (function [] 0)",
"span start 5 exceeds end 2",
0
);
}
#[test]
fn test_reader_rejects_non_bracket_metadata_shape()
{
assert_reader_error("^{0 5} (function [] 0)", "expected '[' after '^'", 1);
}
#[test]
fn test_reader_rejects_dangling_caret()
{
assert_reader_error("^", "expected '[' after '^', found end of input", 1);
}
#[test]
fn test_reader_rejects_non_numeric_span_start()
{
assert_reader_error("^[x 5] (function [] 0)", "invalid byte offset 'x'", 2);
}
#[test]
fn test_reader_rejects_missing_span_close_bracket()
{
assert_reader_error("^[0 5 (function [] 0)", "expected ']', found '('", 6);
}
#[test]
fn test_reader_accepts_zero_length_span()
{
let ast = read_s_expr("^[3 3] (function [] 0)").unwrap();
assert_eq!(ast.span, crate::span::SourceSpan { start: 3, end: 3 });
}
#[test]
fn test_reader_allows_synthetic_parent_with_annotated_child()
{
let ast = read_s_expr("(function [] ^[0 5] (add 1 2))").unwrap();
assert_eq!(ast.span, crate::span::SourceSpan::SYNTHETIC);
assert_eq!(
crate::span::Spanned::span(&ast.body),
crate::span::SourceSpan { start: 0, end: 5 }
);
}
#[test]
fn test_reader_allows_mixed_sibling_annotation_order()
{
let ast = read_s_expr("(function [^[5 10] alpha beta] 42)").unwrap();
let params = ast.parameters.as_ref().expect("expected parameters");
assert_eq!(params.len(), 2);
assert_eq!(params[0].name, "alpha");
assert_eq!(
params[0].span,
crate::span::SourceSpan { start: 5, end: 10 }
);
assert_eq!(params[1].name, "beta");
assert_eq!(params[1].span, crate::span::SourceSpan::SYNTHETIC);
}
#[test]
fn test_reader_allows_annotated_parent_with_synthetic_child()
{
let ast = read_s_expr("^[0 9] (function [] (add 1 2))").unwrap();
assert_eq!(ast.span, crate::span::SourceSpan { start: 0, end: 9 });
assert_eq!(
crate::span::Spanned::span(&ast.body),
crate::span::SourceSpan::SYNTHETIC
);
}
#[test]
fn test_reader_rejects_parameter_span_escaping_parent()
{
let err = read_s_expr("^[0 5] (function [^[10 11] x] 0)")
.expect_err("expected containment failure");
let rendered = err.to_string();
assert!(
rendered.contains("escapes parent span"),
"message was {:?}",
rendered
);
assert!(
matches!(err, SExprError::ChildSpanEscapesParent { .. }),
"expected ChildSpanEscapesParent, got {:?}",
err
);
}
#[test]
fn test_reader_rejects_out_of_order_parameters()
{
let err = read_s_expr("^[0 9] (function [^[5 6] a ^[2 3] b] 0)")
.expect_err("expected sibling-order failure");
let rendered = err.to_string();
assert!(
rendered.contains("overlaps or precedes prior sibling"),
"message was {:?}",
rendered
);
assert!(
matches!(err, SExprError::SiblingSpanOutOfOrder { .. }),
"expected SiblingSpanOutOfOrder, got {:?}",
err
);
}
#[test]
fn test_reader_rejects_neg_missing_operand()
{
assert_reader_error("(function [] (neg))", "expected a word", 17);
}
#[test]
fn test_reader_rejects_add_missing_right_operand()
{
assert_reader_error("(function [] (add 1))", "expected a word", 19);
}
#[test]
fn test_sexpr_error_display_format()
{
let err = read_s_expr("(function [] )").unwrap_err();
let rendered = err.to_string();
let location = err.location();
assert!(
rendered.starts_with("S-expression error at line "),
"unexpected Display prefix: {:?}",
rendered
);
assert!(
rendered.contains(&format!(
"line {}, column {} (byte {})",
location.line, location.column, location.offset
)),
"Display output must include location suffix: {:?}",
rendered
);
assert!(
rendered.contains(&location.offset.to_string()),
"Display output must contain the offset: {:?}",
rendered
);
}
#[test]
fn test_location_single_line_byte_and_column_agree()
{
let err = read_s_expr(" (body 42)").expect_err("expected error");
let loc = err.location();
assert_eq!(loc.line, 1, "expected line 1 on single-line input");
assert_eq!(loc.offset, 6, "expected byte offset 6");
assert_eq!(loc.column, 7, "expected 1-based column 7 (= offset + 1)");
}
#[test]
fn test_location_multiline_reports_line_and_column()
{
let input = "\n\n (body 42)";
let err = read_s_expr(input).expect_err("expected error");
let loc = err.location();
assert_eq!(loc.line, 3, "expected line 3");
assert_eq!(loc.column, 7, "expected column 7 (after 6 spaces)");
assert_eq!(
loc.offset, 8,
"byte offset counts the newlines plus the leading spaces"
);
}
#[test]
fn test_location_crlf_line_endings()
{
let input = "\r\n (body 42)";
let err = read_s_expr(input).expect_err("expected error");
let loc = err.location();
assert_eq!(loc.line, 2, "CRLF counts as a single line terminator");
assert_eq!(loc.column, 7);
}
#[test]
fn test_child_span_escapes_parent_variant_shape()
{
let err = read_s_expr("^[0 5] (function [^[10 11] x] 0)")
.expect_err("expected containment failure");
let SExprError::ChildSpanEscapesParent { child, parent, .. } = err
else
{
panic!("expected ChildSpanEscapesParent")
};
assert_eq!(child, crate::span::SourceSpan { start: 10, end: 11 });
assert_eq!(parent, crate::span::SourceSpan { start: 0, end: 5 });
}
#[test]
fn test_inverted_span_variant_shape()
{
let err = read_s_expr("^[5 2] (function [] 0)")
.expect_err("expected inverted-span error");
let SExprError::InvertedSpan { start, end, .. } = err
else
{
panic!("expected InvertedSpan")
};
assert_eq!(start, 5);
assert_eq!(end, 2);
}
#[test]
fn test_unknown_keyword_variant_shape()
{
let err = read_s_expr("(function [] (bogus 1 2))")
.expect_err("expected unknown-keyword error");
let SExprError::UnknownKeyword { keyword, .. } = err
else
{
panic!("expected UnknownKeyword")
};
assert_eq!(keyword, "bogus");
}
#[test]
fn test_expected_char_distinguishes_eoi_from_mismatch()
{
let eoi = read_s_expr("(function [] 42")
.expect_err("expected missing-close-paren error");
let SExprError::ExpectedChar {
expected, found, ..
} = eoi
else
{
panic!("expected ExpectedChar")
};
assert_eq!(expected, ')');
assert_eq!(found, None);
let mismatch = read_s_expr("(function [] 42]")
.expect_err("expected wrong-close-paren error");
let SExprError::ExpectedChar {
expected, found, ..
} = mismatch
else
{
panic!("expected ExpectedChar")
};
assert_eq!(expected, ')');
assert_eq!(found, Some(']'));
}