diff --git a/fine/build.rs b/fine/build.rs index d6b2c298..24565124 100644 --- a/fine/build.rs +++ b/fine/build.rs @@ -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 = 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)* } }; diff --git a/fine/src/semantics.rs b/fine/src/semantics.rs index 3f0ced21..b7988a68 100644 --- a/fine/src/semantics.rs +++ b/fine/src/semantics.rs @@ -148,6 +148,10 @@ impl<'a> Semantics<'a> { semantics } + pub fn tree(&self) -> &SyntaxTree<'a> { + &self.syntax_tree + } + pub fn snapshot_errors(&self) -> Vec { (*self.errors.borrow()).clone() } diff --git a/fine/tests/example_tests.rs b/fine/tests/example_tests.rs index a80b057b..ba991106 100644 --- a/fine/tests/example_tests.rs +++ b/fine/tests/example_tests.rs @@ -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")); diff --git a/fine/tests/expression/errors/binary_mismatch.fine b/fine/tests/expression/errors/binary_mismatch.fine new file mode 100644 index 00000000..9de7ab12 --- /dev/null +++ b/fine/tests/expression/errors/binary_mismatch.fine @@ -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) \ No newline at end of file diff --git a/fine/tests/expression/errors/if_mismatched_arms.fine b/fine/tests/expression/errors/if_mismatched_arms.fine new file mode 100644 index 00000000..65dbf06b --- /dev/null +++ b/fine/tests/expression/errors/if_mismatched_arms.fine @@ -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) diff --git a/fine/tests/expression/errors/if_not_bool.fine b/fine/tests/expression/errors/if_not_bool.fine new file mode 100644 index 00000000..01d436af --- /dev/null +++ b/fine/tests/expression/errors/if_not_bool.fine @@ -0,0 +1,2 @@ +if 23 { "what" } else { "the" } +// type-error: 0 conditions must yield a boolean diff --git a/fine/tests/expression/errors/if_requires_else.fine b/fine/tests/expression/errors/if_requires_else.fine new file mode 100644 index 00000000..f258ee5a --- /dev/null +++ b/fine/tests/expression/errors/if_requires_else.fine @@ -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 diff --git a/fine/tests/expression/errors/unary_mismatch.fine b/fine/tests/expression/errors/unary_mismatch.fine new file mode 100644 index 00000000..1f8b79e0 --- /dev/null +++ b/fine/tests/expression/errors/unary_mismatch.fine @@ -0,0 +1,2 @@ +- "twenty five"; +// type-error: 0 cannot apply unary operator '-' to value of type string