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, 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, base: StandardModuleLoader, } impl TestLoader { fn new(base_path: PathBuf, source: Rc) -> Box { 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: Option<&str>, name: String) -> String { if name == "__test__" { name } else { let base = match base { Some("__test__") => None, _ => base, }; self.base.normalize_module_name(base, name) } } fn load_module(&self, name: &String) -> Result { if name == "__test__" { Ok(ModuleSource::SourceText(self.source.to_string())) } else { self.base.load_module(name) } } } fn test_runtime(_source_path: &str, source: Rc) -> Program { Program::new(TestLoader::new(_source_path.into(), source)) } fn assert_type_at(module: Rc, 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, errors: &[Rc], 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, 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, errors: &[Rc]) { let semantics = module.semantics(); let expected_errors: &[Rc] = &[]; semantic_assert_eq!( &semantics, None, expected_errors, errors, "expected no errors" ); } fn dump_runtime_error(module: &Rc, 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, 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, errors: &[Rc], expected_errors: Vec<&str>) { let semantics = module.semantics(); let errors: Vec = 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, errors: &[Rc], 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"));