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 _; 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, tree: Rc, expected: &str, source_path: &str) { 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); } }}; } fn assert_type_at( source: Rc, tree: Rc, lines: Rc, pos: usize, expected: &str, _source_path: &str, ) { let semantics = Semantics::new(source, tree.clone(), 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( source: Rc, tree: Rc, lines: Rc, pos: usize, expected: &str, _source_path: &str, ) { let semantics = Semantics::new(source, tree.clone(), 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( source: Rc, tree: Rc, lines: Rc, expected: &str, source_path: &str, ) { let semantics = Rc::new(Semantics::new(source, tree, lines)); let module = compile(semantics.clone()); 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(source: Rc, tree: Rc, lines: Rc) { let semantics = Semantics::new(source, tree, lines); check(&semantics); let expected_errors: Vec = Vec::new(); let errors = semantics.snapshot_errors(); semantic_assert_eq!( &semantics, None, expected_errors, errors, "expected no errors" ); } fn assert_eval_ok(source: Rc, tree: Rc, lines: Rc, expected: &str) { let semantics = Rc::new(Semantics::new(source, tree, lines)); let module = compile(semantics.clone()); 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( source: Rc, tree: Rc, lines: Rc, expected_errors: Vec<&str>, ) { let semantics = Semantics::new(source, tree, lines); check(&semantics); let errors: Vec = semantics .snapshot_errors() .iter() .map(|e| format!("{}", e)) .collect(); semantic_assert_eq!( &semantics, None, expected_errors, errors, "expected error messages to match" ); } fn assert_check_error(source: Rc, tree: Rc, lines: Rc, expected: &str) { let semantics = Semantics::new(source, 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"));