#[cfg(test)]
mod tests {
use crate::ast::{Expression, MethodKind, Node, Param, Statement, TypeAnnotation};
use crate::parse;
use lexer::token::Span;
fn parse_statement(input: &str) -> Statement {
let Node::Program(program) = parse(input).unwrap() else {
panic!("expected a program for {}", input)
};
assert_eq!(program.body.len(), 1, "expected exactly one statement in {}", input);
return program.body.into_iter().next().unwrap();
}
fn let_annotation(input: &str) -> TypeAnnotation {
let Statement::Let(statement) = parse_statement(input) else {
panic!("expected a let statement for {}", input)
};
return statement
.type_annotation
.unwrap_or_else(|| panic!("expected a type annotation in {}", input));
}
fn function(input: &str) -> (Vec<Param>, Option<TypeAnnotation>) {
let Statement::Expr(Expression::FUNCTION(function)) = parse_statement(input) else {
panic!("expected a function expression for {}", input)
};
return (function.params, function.return_type);
}
fn slice<'a>(input: &'a str, span: &Span) -> &'a str {
return &input[span.start..span.end];
}
fn first_error(input: &str) -> String {
let errors = parse(input).expect_err("expected a parse error");
return errors[0].clone();
}
#[test]
fn parse_type_forms() {
let cases = [
("let a: int = 1;", "int"),
("let a: bool = true;", "bool"),
("let a: string = \"x\";", "string"),
("let a: any = 1;", "any"),
("let a: null = 1;", "null"),
("let a: Point = 1;", "Point"),
("let a: int? = 1;", "int?"),
("let a: [int] = 1;", "[int]"),
("let a: [[int]] = 1;", "[[int]]"),
("let a: [int?] = 1;", "[int?]"),
("let a: [int]? = 1;", "[int]?"),
("let a: {string: int} = 1;", "{string: int}"),
("let a: {string: [int]} = 1;", "{string: [int]}"),
("let a: {string: int}? = 1;", "{string: int}?"),
("let a: fn(): int = 1;", "fn(): int"),
("let a: fn(int): int = 1;", "fn(int): int"),
("let a: fn(int, string): bool = 1;", "fn(int, string): bool"),
("let a: fn(fn(int): int, int): [string] = 1;", "fn(fn(int): int, int): [string]"),
("let a: (fn(int): int)? = 1;", "(fn(int): int)?"),
("let a: fn(int): int? = 1;", "fn(int): int?"),
];
for (input, expected) in cases {
assert_eq!(let_annotation(input).to_string(), expected, "for input {}", input);
}
}
#[test]
fn type_display_round_trips() {
let inputs = [
"let a: int? = 1;",
"let a: [int]? = 1;",
"let a: {string: [int]} = 1;",
"let a: fn(fn(int): int, int): [string] = 1;",
"let a: (fn(int): int)? = 1;",
];
for input in inputs {
let printed = let_annotation(input).to_string();
let reparsed = let_annotation(&format!("let a: {} = 1;", printed));
assert_eq!(reparsed.to_string(), printed, "for input {}", input);
}
}
#[test]
fn type_spans_cover_their_source() {
let cases = [
"let a: int? = 1;",
"let a: [int] = 1;",
"let a: {string: [int]} = 1;",
"let a: fn(fn(int): int, int): [string] = 1;",
"let a: (fn(int): int)? = 1;",
];
for input in cases {
let annotation = let_annotation(input);
let expected = input
.trim_start_matches("let a: ")
.trim_end_matches(" = 1;");
assert_eq!(slice(input, annotation.span()), expected, "for input {}", input);
}
}
#[test]
fn nested_type_spans() {
let input = "let a: {string: [int]} = 1;";
let TypeAnnotation::Hash(hash) = let_annotation(input) else {
panic!("expected a hash type")
};
assert_eq!(slice(input, hash.key.span()), "string");
assert_eq!(slice(input, hash.value.span()), "[int]");
let TypeAnnotation::Array(array) = *hash.value else { panic!("expected an array type") };
assert_eq!(slice(input, array.element.span()), "int");
}
#[test]
fn function_type_spans() {
let input = "let a: fn(int, string): bool = 1;";
let TypeAnnotation::Function(function) = let_annotation(input) else {
panic!("expected a function type")
};
assert_eq!(slice(input, &function.span), "fn(int, string): bool");
assert_eq!(slice(input, function.params[0].span()), "int");
assert_eq!(slice(input, function.params[1].span()), "string");
assert_eq!(slice(input, function.return_type.span()), "bool");
}
#[test]
fn grouping_widens_the_span_without_adding_a_node() {
let input = "let a: (int) = 1;";
let annotation = let_annotation(input);
assert!(matches!(annotation, TypeAnnotation::Named(_)));
assert_eq!(slice(input, annotation.span()), "(int)");
}
#[test]
fn double_question_normalizes() {
let input = "let a: int?? = 1;";
let annotation = let_annotation(input);
assert_eq!(annotation.to_string(), "int?");
assert_eq!(slice(input, annotation.span()), "int??");
let TypeAnnotation::Optional(optional) = annotation else {
panic!("expected an optional type")
};
assert!(matches!(*optional.inner, TypeAnnotation::Named(_)));
}
#[test]
fn parse_function_annotations() {
let (params, return_type) = function("fn(a: int, b: [string]): bool { a; }");
assert_eq!(params.len(), 2);
assert_eq!(params[0].to_string(), "a: int");
assert_eq!(params[1].to_string(), "b: [string]");
assert_eq!(return_type.unwrap().to_string(), "bool");
let (params, return_type) = function("fn(a, b: int) { a; }");
assert_eq!(params[0].to_string(), "a");
assert_eq!(params[1].to_string(), "b: int");
assert!(return_type.is_none());
let (params, return_type) = function("fn(a, b) { a; }");
assert!(params.iter().all(|param| param.type_annotation.is_none()));
assert!(return_type.is_none());
}
#[test]
fn param_spans_cover_name_and_annotation() {
let input = "fn(a: int, b) { a; }";
let (params, _) = function(input);
assert_eq!(slice(input, ¶ms[0].span), "a: int");
assert_eq!(slice(input, ¶ms[1].span), "b");
assert_eq!(slice(input, ¶ms[0].identifier.span), "a");
}
#[test]
fn annotated_function_display() {
let cases = [
("fn(a: int): int { a; }", "fn (a: int): int { a }"),
("fn(a: int?, b: [string]) { a; }", "fn (a: int?, b: [string]) { a }"),
(
"let f: fn(int): int = fn(a: int): int { a; };",
"let f: fn(int): int = fn f(a: int): int { a };",
),
];
for (input, expected) in cases {
assert_eq!(parse(input).unwrap().to_string(), expected, "for input {}", input);
}
}
#[test]
fn parse_class_method_annotations() {
let input =
"class Point { constructor(x: int, y: int) { this.x = x; } norm(): int { 1; } }";
let Statement::Class(class) = parse_statement(input) else {
panic!("expected a class declaration")
};
let constructor = &class.methods[0];
assert_eq!(constructor.kind, MethodKind::Constructor);
assert_eq!(constructor.params[0].to_string(), "x: int");
assert!(constructor.return_type.is_none());
let method = &class.methods[1];
assert_eq!(method.kind, MethodKind::Method);
assert_eq!(method.return_type.as_ref().unwrap().to_string(), "int");
}
#[test]
fn type_names_stay_soft_keywords() {
let cases = [
("let int = 5;", "let int = 5;"),
("let string = \"x\";", "let string = \"x\";"),
("let any = fn(any) { any; };", "let any = fn any(any) { any };"),
("let null: int = 5;", "let null: int = 5;"),
("let int: int = int;", "let int: int = int;"),
];
for (input, expected) in cases {
assert_eq!(parse(input).unwrap().to_string(), expected, "for input {}", input);
}
}
#[test]
fn reject_malformed_types() {
let cases = [
("let x: = 5;", "expected a type, got"),
("fn(a:) { a; }", "expected a type, got"),
("let x: [int = 5;", "expected token: ]"),
("let x: {int} = 5;", "expected token: :"),
("let x: {int: } = 5;", "expected a type, got"),
("let x: fn(int) = 5;", "function type requires a return type"),
("let x: fn(int: int): int = 5;", "expected token: )"),
("let x: (int = 5;", "expected token: )"),
("let x: 5 = 5;", "expected a type, got"),
(
"class P { constructor(): int { 1; } }",
"constructor cannot have a return type annotation",
),
];
for (input, expected) in cases {
let error = first_error(input);
assert!(
error.contains(expected),
"expected {} to report {:?}, got {:?}",
input,
expected,
error
);
}
}
#[test]
fn question_mark_outside_type_position_is_rejected() {
assert!(parse("1 ? 2 : 3").is_err());
}
}