use fine::compiler::{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::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(source: Rc) -> Box { Box::new(TestLoader { source, base: StandardModuleLoader {}, }) } } impl ModuleLoader for TestLoader { fn normalize_module_name(&self, name: String) -> String { if name == "__test__" { name } else { self.base.normalize_module_name(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: Rc) -> Runtime { Runtime::new(TestLoader::new(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: &[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, expected: &str, source_path: &str) { let semantics = module.semantics(); let module = module.compiled(); 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: &[Error]) { let semantics = module.semantics(); let expected_errors: &[Error] = &[]; semantic_assert_eq!( &semantics, None, expected_errors, errors, "expected no errors" ); } fn assert_eval_ok(module: Rc, expected: &str) { let semantics = module.semantics(); let module = module.compiled(); 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, errors: &[Error], 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: &[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"));