[fine] Tests for type errors

WHEEEEEEE!
This commit is contained in:
John Doty 2024-01-05 19:29:45 -08:00
parent 618e0028d3
commit a9c1b04920
8 changed files with 123 additions and 47 deletions

View file

@ -7,10 +7,8 @@ fn generate_test_for_file(path: PathBuf) -> String {
let contents = fs::read_to_string(&path).expect("Unable to read input");
let display_path = path.display().to_string();
let mut concrete_stuff: Option<String> = None;
// Start iterating over lines and processing directives....
let mut type_assertions = Vec::new();
let mut assertions = Vec::new();
let mut lines = contents.lines();
while let Some(line) = lines.next() {
let line = match line.strip_prefix("//") {
@ -30,7 +28,10 @@ fn generate_test_for_file(path: PathBuf) -> String {
concrete.push_str(line);
concrete.push_str("\n");
}
concrete_stuff = Some(concrete);
assertions.push(quote! {
crate::assert_concrete(&_tree, #concrete, #display_path);
});
} else if let Some(line) = line.strip_prefix("type:") {
let (pos, expected) = line
.trim()
@ -41,26 +42,30 @@ fn generate_test_for_file(path: PathBuf) -> String {
.parse()
.expect(&format!("Unable to parse position '{pos}'"));
let expected = expected.trim();
type_assertions.push(quote! {
assertions.push(quote! {
crate::assert_type_at(&_tree, &_lines, #pos, #expected, #display_path);
});
} else if let Some(line) = line.strip_prefix("type-error:") {
let (pos, expected) = line
.trim()
.split_once(' ')
.expect("Mal-formed type-error expectation");
let pos: usize = pos
.trim()
.parse()
.expect(&format!("Unable to parse position '{pos}'"));
let expected = expected.trim();
assertions.push(quote! {
crate::assert_type_error_at(&_tree, &_lines, #pos, #expected, #display_path);
});
}
}
let concrete_comparison = if let Some(concrete) = concrete_stuff {
quote! {
crate::assert_concrete(&_tree, #concrete, #display_path)
}
} else {
quote! {}
};
let name = format_ident!("{}", path.file_stem().unwrap().to_string_lossy());
let test_method = quote! {
fn #name() {
let (_tree, _lines) = fine::parser::parse(#contents);
#concrete_comparison;
#(#type_assertions)*
#(#assertions)*
}
};

View file

@ -148,6 +148,10 @@ impl<'a> Semantics<'a> {
semantics
}
pub fn tree(&self) -> &SyntaxTree<'a> {
&self.syntax_tree
}
pub fn snapshot_errors(&self) -> Vec<Error> {
(*self.errors.borrow()).clone()
}

View file

@ -85,6 +85,58 @@ fn assert_concrete(tree: &SyntaxTree, expected: &str, source_path: &str) {
}
}
fn report_semantic_error(semantics: &Semantics, message: &str) {
let tree = semantics.tree();
println!("{message}! Parsed the tree as:");
println!("\n{}", tree.dump(true));
let errors = semantics.snapshot_errors();
if errors.len() == 0 {
println!("There were no errors reported during checking.\n");
} else {
println!(
"{} error{} reported during checking:",
errors.len(),
if errors.len() == 1 { "" } else { "s" }
);
for error in &errors {
println!(" Error: {error}");
}
println!();
}
}
macro_rules! semantic_panic {
($semantics:expr, $($t:tt)*) => {{
let message = format!($($t)*);
report_semantic_error($semantics, &message);
panic!("{message}");
}};
}
macro_rules! semantic_assert {
($semantics:expr, $pred:expr, $($t:tt)*) => {{
if !$pred {
let message = format!($($t)*);
report_semantic_error($semantics, &message);
panic!("{message}");
}
}};
}
macro_rules! semantic_assert_eq {
($semantics:expr, $left:expr, $right:expr, $($t:tt)*) => {{
let ll = $left;
let rr = $right;
if ll != rr {
let message = format!($($t)*);
report_semantic_error($semantics, &message);
assert_eq!(ll, rr, "{}", message);
}
}};
}
fn assert_type_at(
tree: &SyntaxTree,
lines: &Lines,
@ -92,46 +144,49 @@ fn assert_type_at(
expected: &str,
_source_path: &str,
) {
let semantics = Semantics::new(tree, lines);
let tree_ref = match tree.find_tree_at(pos) {
Some(t) => t,
None => {
println!("Unable to find the subtee at position {pos}! Parsed the tree as:");
println!("\n{}", tree.dump(true));
panic!("Cannot find tree at position {pos}");
}
None => semantic_panic!(&semantics, "Unable to find the subtee at position {pos}"),
};
let semantics = Semantics::new(tree, lines);
let tree_type = semantics.type_of(tree_ref, true);
let actual = format!("{}", tree_type.unwrap_or(Type::Error));
if actual != expected {
println!(
"The type of the {:?} tree at position {pos} had the wrong type! Parsed the tree as:",
tree[tree_ref].kind
);
println!("\n{}", tree.dump(true));
semantic_assert_eq!(
&semantics,
expected,
actual,
"The type of the tree at position {pos} was incorrect"
);
}
let errors = semantics.snapshot_errors();
if errors.len() == 0 {
println!("There were no errors reported during type checking.\n");
} else {
println!(
"{} error{} reported during type checking:",
errors.len(),
if errors.len() == 1 { "" } else { "s" }
);
for error in &errors {
println!(" Error: {error}");
}
println!();
}
fn assert_type_error_at(
tree: &SyntaxTree,
lines: &Lines,
pos: usize,
expected: &str,
_source_path: &str,
) {
let semantics = Semantics::new(tree, lines);
let tree_ref = match tree.find_tree_at(pos) {
Some(t) => t,
None => semantic_panic!(&semantics, "Unable to find the subtee at position {pos}"),
};
assert_eq!(
expected, actual,
"The type of the tree at position {pos} was incorrect"
);
}
let tree_type = semantics.type_of(tree_ref, true);
semantic_assert!(
&semantics,
matches!(tree_type, Some(Type::Error)),
"The type of the {:?} tree at position {pos} was '{tree_type:?}', not an error",
tree[tree_ref].kind
);
let errors = semantics.snapshot_errors();
semantic_assert!(
&semantics,
errors.iter().any(|e| e.message == expected),
"Unable to find the expected error message '{expected}'"
);
}
include!(concat!(env!("OUT_DIR"), "/generated_tests.rs"));

View file

@ -0,0 +1,4 @@
112 - "twenty five";
"twenty five" - 112;
// type-error: 4 cannot apply binary operator '-' to expressions of type 'f64' (on the left) and 'string' (on the right)
// type-error: 35 cannot apply binary operator '-' to expressions of type 'string' (on the left) and 'f64' (on the right)

View file

@ -0,0 +1,2 @@
if true { "blarg" } else { 23 }
// type-error: 0 the type of the `then` branch (string) must match the type of the `else` branch (f64)

View file

@ -0,0 +1,2 @@
if 23 { "what" } else { "the" }
// type-error: 0 conditions must yield a boolean

View file

@ -0,0 +1,2 @@
if (if false { true }) { 32 } else { 23 }
// type-error: 4 this conditional expression needs an else arm to produce a value

View file

@ -0,0 +1,2 @@
- "twenty five";
// type-error: 0 cannot apply unary operator '-' to value of type string