It's a little bit complicated, loading a module is a two-step dance but here's how it's done. Probably some surface-area refactoring needs to happen so that we do the right thing.
377 lines
10 KiB
Rust
377 lines
10 KiB
Rust
use fine::compile_program;
|
|
use fine::compiler::{compile_module, CompiledModule, Function};
|
|
use fine::program::{
|
|
Module, ModuleLoadError, ModuleLoader, ModuleSource, Program, StandardModuleLoader,
|
|
};
|
|
use fine::semantics::{Error, Type};
|
|
use fine::vm::{eval_export_fn, Context, VMError};
|
|
|
|
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>) -> Program {
|
|
Program::new(TestLoader::new(_source_path.into(), source))
|
|
}
|
|
|
|
fn assert_type_at(module: Rc<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<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: &CompiledModule) -> std::fmt::Result {
|
|
for function in module.functions() {
|
|
dump_function(out, function)?;
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn assert_compiles_to(module: Rc<Module>, expected: &str, source_path: &str) {
|
|
let semantics = module.semantics();
|
|
let module = compile_module(&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<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 dump_runtime_error(module: &Rc<Module>, context: &Context, e: VMError) -> ! {
|
|
let semantics = module.semantics();
|
|
semantics.dump_compiler_state(None);
|
|
|
|
if let Some(module) = context.get_module(module.id()) {
|
|
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_eval_ok(program: &Program, module: Rc<Module>, expected: &str) {
|
|
let semantics = module.semantics();
|
|
let mut context = Context::new();
|
|
if let Err(e) = compile_program(&program, &mut context) {
|
|
dump_runtime_error(&module, &context, e);
|
|
};
|
|
|
|
match eval_export_fn(&mut context, module.id(), "test", &[]) {
|
|
Ok(v) => {
|
|
let actual = format!("{:?}", v);
|
|
semantic_assert_eq!(
|
|
&semantics,
|
|
None,
|
|
expected,
|
|
&actual,
|
|
"wrong return from test function"
|
|
);
|
|
}
|
|
Err(e) => dump_runtime_error(&module, &context, e),
|
|
}
|
|
}
|
|
|
|
fn assert_errors(module: Rc<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<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"));
|