oden/fine/tests/example_tests.rs
John Doty f20f5a5e03 [fine] Assignments!
And new error capabilities!
2024-01-19 19:08:17 -08:00

318 lines
8.7 KiB
Rust

use fine::compiler::{compile, Function, Module};
use fine::parser::SyntaxTree;
use fine::semantics::{check, Error, Semantics, Type};
use fine::tokens::Lines;
use fine::vm::{eval_export_fn, Context};
use pretty_assertions::assert_eq;
use std::fmt::Write as _;
fn rebase_section(source_path: &str, section: &str, value: &str) {
let contents = std::fs::read_to_string(source_path)
.expect(&format!("unable to read input file {}", source_path));
let mut result = String::new();
let mut lines = contents.lines();
// Search for the section.
let mut found_section = false;
let marker = format!("// @{section}:");
while let Some(line) = lines.next() {
result.push_str(line);
result.push_str("\n");
if line == marker {
found_section = true;
break;
}
}
if !found_section {
panic!(
"unable to locate the {section} section in {source_path}. Is there a line that starts with '// @{section}:'?"
);
}
// We've found the section we care about, replace all the lines we care
// about with the actual lines.
let mut replaced_output = false;
while let Some(line) = lines.next() {
if line.starts_with("// | ") {
// Skip copying lines here, because we're skipping
// the existing concrete syntax tree.
} else {
// OK we're out of concrete syntax tree; copy in the
// new CST. (We do this inline so we don't lose
// `line`.)
for expected_line in value.lines() {
result.push_str("// | ");
result.push_str(expected_line);
result.push_str("\n");
}
// (Make sure not to drop this line.)
result.push_str(line);
result.push_str("\n");
replaced_output = true;
break;
}
}
if !replaced_output {
panic!(
"didn't actually replace the output section in {}",
source_path
);
}
// Now just copy the rest of the lines.
while let Some(line) = lines.next() {
result.push_str(line);
result.push_str("\n");
}
// ... and re-write the file.
std::fs::write(source_path, result).expect("unable to write the new file!");
}
fn should_rebase() -> bool {
let rebase = std::env::var("FINE_TEST_REBASE")
.unwrap_or(String::new())
.to_lowercase();
match rebase.as_str() {
"1" | "true" | "yes" | "y" => true,
_ => false,
}
}
fn assert_concrete(tree: &SyntaxTree, expected: &str, source_path: &str) {
let dump = tree.dump(false);
if dump != expected {
if should_rebase() {
rebase_section(source_path, "concrete", &dump)
} else {
assert_eq!(expected, dump, "concrete syntax trees did not match (set FINE_TEST_REBASE=1 to auto-rebase if the diff is expected)")
}
}
}
macro_rules! semantic_panic {
($semantics:expr, $tr:expr, $($t:tt)*) => {{
let message = format!($($t)*);
eprintln!("{message}!");
$semantics.dump_compiler_state($tr);
panic!("{message}");
}};
}
macro_rules! semantic_assert {
($semantics:expr, $tr:expr, $pred:expr, $($t:tt)*) => {{
if !$pred {
let message = format!($($t)*);
eprintln!("{message}!");
$semantics.dump_compiler_state($tr);
panic!("{message}");
}
}};
}
macro_rules! semantic_assert_eq {
($semantics:expr, $tr:expr, $left:expr, $right:expr, $($t:tt)*) => {{
let ll = $left;
let rr = $right;
if ll != rr {
let message = format!($($t)*);
eprintln!("{message}!");
$semantics.dump_compiler_state($tr);
assert_eq!(ll, rr, "{}", message);
}
}};
}
fn assert_type_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,
None,
"Unable to find the subtee at position {pos}"
),
};
let tree_type = semantics.type_of(tree_ref);
let actual = format!("{}", tree_type);
semantic_assert_eq!(
&semantics,
Some(tree_ref),
expected,
actual,
"The type of the tree at position {pos} was incorrect"
);
}
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,
None,
"Unable to find the subtee at position {pos}"
),
};
let tree_type = semantics.type_of(tree_ref);
semantic_assert!(
&semantics,
Some(tree_ref),
matches!(tree_type, 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,
Some(tree_ref),
errors.iter().any(|e| e.message == expected),
"Unable to find the expected error message '{expected}'"
);
}
fn dump_function(out: &mut String, function: &Function) -> std::fmt::Result {
writeln!(
out,
"function {} ({} args, {} locals):",
function.name(),
function.args(),
function.locals()
)?;
let strings = function.strings();
writeln!(out, " strings ({}):", strings.len())?;
for (i, s) in strings.iter().enumerate() {
writeln!(out, " {}: {}", i, s)?; // TODO: ESCAPE
}
let code = function.instructions();
writeln!(out, " code ({}):", code.len())?;
for (i, inst) in code.iter().enumerate() {
writeln!(out, " {}: {:?}", i, inst)?;
}
Ok(())
}
fn dump_module(out: &mut String, module: &Module) -> std::fmt::Result {
for function in module.functions() {
dump_function(out, function)?;
}
Ok(())
}
fn assert_compiles_to(tree: &SyntaxTree, lines: &Lines, expected: &str, source_path: &str) {
let semantics = Semantics::new(tree, lines);
let module = compile(&semantics);
let mut actual = String::new();
dump_module(&mut actual, &module).expect("no dumping?");
if expected != actual {
if should_rebase() {
rebase_section(source_path, "compiles-to", &actual)
} else {
semantic_assert_eq!(
&semantics,
None,
expected,
actual,
"did not compile as expected (set FINE_TEST_REBASE=1 to auto-rebase if the diff is expected)"
)
}
}
}
fn assert_no_errors(tree: &SyntaxTree, lines: &Lines) {
let semantics = Semantics::new(tree, lines);
check(&semantics);
let expected_errors: Vec<Error> = Vec::new();
let errors = semantics.snapshot_errors();
semantic_assert_eq!(
&semantics,
None,
expected_errors,
errors,
"expected no errors"
);
}
fn assert_eval_ok(tree: &SyntaxTree, lines: &Lines, expected: &str) {
let semantics = Semantics::new(tree, lines);
let module = compile(&semantics);
let mut context = Context::new(module);
context.init().expect("Unable to initialize module");
match eval_export_fn(&mut context, "test", &[]) {
Ok(v) => {
let actual = format!("{:?}", v);
semantic_assert_eq!(
&semantics,
None,
expected,
&actual,
"wrong return from test function"
);
}
Err(e) => {
semantic_panic!(&semantics, None, "error occurred while running: {:?}", e);
}
}
}
fn assert_errors(tree: &SyntaxTree, lines: &Lines, expected_errors: Vec<&str>) {
let semantics = Semantics::new(tree, lines);
check(&semantics);
let errors: Vec<String> = semantics
.snapshot_errors()
.iter()
.map(|e| format!("{}", e))
.collect();
semantic_assert_eq!(
&semantics,
None,
expected_errors,
errors,
"expected no errors"
);
}
fn assert_check_error(tree: &SyntaxTree, lines: &Lines, expected: &str) {
let semantics = Semantics::new(tree, lines);
check(&semantics);
let errors = semantics.snapshot_errors();
semantic_assert!(
&semantics,
None,
errors.iter().any(|e| e.message == expected),
"Unable to find the expected error message '{expected}'"
);
}
include!(concat!(env!("OUT_DIR"), "/generated_tests.rs"));