oden/fine/tests/example_tests.rs
John Doty 85ffc0c7dd [fine] All error sentinels carry diagnostics
The system has an invariant that if you ever return an error
sentinel (error environment, error type) then that sentinel is caused
by an error that was reported to the user. We have had too many bugs
over the last little while where that was not the case!

(An example is if we mis-interpret the tree by calling `nth_tree` with
the wrong index or something, and get `None`, and think "oh must be a
syntax error", but it was really just the wrong index. Then there's an
error sentinel with no error diagnostic and we don't discover the
mistake until much farther along.)

Now we enforce this by requiring that whoever constructs the error
sentinel *prove* that they can do so by providing a diagnostic. It's
less efficient but prevents the problem.

This actually uncovered a couple of latent bugs where we were
generating error sentinels instead of a more appropriate type! Whoops!
2024-03-25 08:07:18 -07:00

369 lines
10 KiB
Rust

use fine::compiler::{compile, Function, Module};
use fine::semantics::{Error, Type};
use fine::vm::{eval_export_fn, Context};
use fine::{ModuleLoadError, ModuleLoader, ModuleSource, Runtime, StandardModuleLoader};
use pretty_assertions::assert_eq;
use std::fmt::Write as _;
use std::path::PathBuf;
use std::rc::Rc;
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(source: Rc<str>, expected: &str, source_path: &str) {
let (tree, _) = fine::parser::parse(&source);
let dump = tree.dump(&source, 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);
}
}};
}
struct TestLoader {
source: Rc<str>,
base: StandardModuleLoader,
}
impl TestLoader {
fn new(base_path: PathBuf, source: Rc<str>) -> Box<Self> {
let base_path = base_path
.parent()
.map(|p| p.to_owned())
.unwrap_or(base_path);
Box::new(TestLoader {
source,
base: StandardModuleLoader::new(base_path),
})
}
}
impl ModuleLoader for TestLoader {
fn normalize_module_name(&self, base: &str, name: String) -> String {
if name == "__test__" {
name
} else {
let base = if base == "__test__" { "" } else { base };
self.base.normalize_module_name(base, name)
}
}
fn load_module(&self, name: &String) -> Result<ModuleSource, ModuleLoadError> {
if name == "__test__" {
Ok(ModuleSource::SourceText(self.source.to_string()))
} else {
self.base.load_module(name)
}
}
}
fn test_runtime(_source_path: &str, source: Rc<str>) -> Runtime {
Runtime::new(TestLoader::new(_source_path.into(), source))
}
fn assert_type_at(module: Rc<fine::Module>, pos: usize, expected: &str, _source_path: &str) {
let semantics = module.semantics();
let tree = semantics.tree();
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(
module: Rc<fine::Module>,
errors: &[Rc<Error>],
pos: usize,
expected: &str,
_source_path: &str,
) {
let semantics = module.semantics();
let tree = semantics.tree();
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
);
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(module: Rc<fine::Module>, expected: &str, source_path: &str) {
let semantics = module.semantics();
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(module: Rc<fine::Module>, errors: &[Rc<Error>]) {
let semantics = module.semantics();
let expected_errors: &[Rc<Error>] = &[];
semantic_assert_eq!(
&semantics,
None,
expected_errors,
errors,
"expected no errors"
);
}
fn assert_eval_ok(module: Rc<fine::Module>, expected: &str) {
let semantics = module.semantics();
let module = compile(&semantics);
let mut context = Context::new(module.clone());
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) => {
semantics.dump_compiler_state(None);
let mut actual = String::new();
let _ = dump_module(&mut actual, &module);
eprintln!("{actual}");
eprintln!("Backtrace:");
for frame in e.stack.iter() {
let func = frame.func();
eprint!(" {} (", func.name());
for arg in frame.args().iter() {
eprint!("{:?},", arg);
}
eprintln!(") @ {}", frame.pc());
}
eprintln!();
panic!("error occurred while running: {:?}", e.code);
}
}
}
fn assert_errors(module: Rc<fine::Module>, errors: &[Rc<Error>], expected_errors: Vec<&str>) {
let semantics = module.semantics();
let errors: Vec<String> = errors.iter().map(|e| format!("{}", e)).collect();
semantic_assert_eq!(
&semantics,
None,
expected_errors,
errors,
"expected error messages to match"
);
}
fn assert_check_error(module: Rc<fine::Module>, errors: &[Rc<Error>], expected: &str) {
let semantics = module.semantics();
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"));