//! IR snapshot tests: guard the codegen pipeline against unintended //! changes to the LLVM IR. Snapshots in tests/snapshots/. Update with //! UPDATE_SNAPSHOTS=1 cargo test ir_snapshot_. use ailang_test_support::examples_dir; use std::path::{Path, PathBuf}; use std::process::Command; fn ail_bin() -> &'static str { env!("CARGO_BIN_EXE_ail") } /// Normalizes IR text for platform stability: /// - `target triple = "..."` -> `target triple = ""` /// - strip trailing whitespace per line /// - LF line endings, file ends with exactly one `\n` fn normalize(ir: &str) -> String { let mut out = String::with_capacity(ir.len()); for line in ir.split('\n') { let line = line.strip_suffix('\r').unwrap_or(line); let trimmed = line.trim_end(); if let Some(rest) = trimmed.strip_prefix("target triple = ") { // rest is `"..."` (with quotes) — fully normalize. let _ = rest; out.push_str("target triple = \"\""); } else { out.push_str(trimmed); } out.push('\n'); } // split('\n') after a trailing `\n` produces an empty last piece, // which was closed with `\n` above. So `out` typically ends in // exactly one trailing `\n`. Reduce double `\n\n` at the end. while out.ends_with("\n\n") { out.pop(); } if !out.ends_with('\n') { out.push('\n'); } out } fn snapshots_dir() -> PathBuf { let manifest_dir = env!("CARGO_MANIFEST_DIR"); Path::new(manifest_dir).join("tests").join("snapshots") } fn first_diff_line(a: &str, b: &str) -> Option<(usize, String, String)> { let mut la = a.lines(); let mut lb = b.lines(); let mut idx = 1usize; loop { match (la.next(), lb.next()) { (Some(x), Some(y)) => { if x != y { return Some((idx, x.to_string(), y.to_string())); } } (Some(x), None) => return Some((idx, x.to_string(), String::from(""))), (None, Some(y)) => return Some((idx, String::from(""), y.to_string())), (None, None) => return None, } idx += 1; } } fn check_ir_snapshot(example: &str, snapshot_name: &str) { let example_path = examples_dir().join(example); assert!( example_path.exists(), "example missing: {}", example_path.display() ); let tmpdir = std::env::temp_dir().join(format!( "ailang_ir_snapshot_{}_{}", snapshot_name.replace('.', "_"), std::process::id() )); std::fs::create_dir_all(&tmpdir).unwrap(); let out_ll = tmpdir.join(format!("{snapshot_name}.tmp.ll")); let status = Command::new(ail_bin()) .args(["emit-ir", example_path.to_str().unwrap(), "-o"]) .arg(&out_ll) .status() .expect("ail emit-ir failed to run"); assert!(status.success(), "ail emit-ir failed for {example}"); let actual_raw = std::fs::read_to_string(&out_ll).expect("read emitted IR"); let actual = normalize(&actual_raw); let snapshot_path = snapshots_dir().join(snapshot_name); if std::env::var_os("UPDATE_SNAPSHOTS").is_some() { std::fs::create_dir_all(snapshots_dir()).unwrap(); std::fs::write(&snapshot_path, &actual).expect("write snapshot"); eprintln!("updated snapshot: {}", snapshot_path.display()); return; } let expected_raw = std::fs::read_to_string(&snapshot_path).unwrap_or_else(|e| { panic!( "missing snapshot {}: {e}\nrun with UPDATE_SNAPSHOTS=1 to create it", snapshot_path.display() ) }); let expected = normalize(&expected_raw); if actual != expected { let actual_path = snapshot_path.with_extension("ll.actual"); std::fs::write(&actual_path, &actual).expect("write .actual"); let diff_msg = match first_diff_line(&expected, &actual) { Some((line, want, got)) => format!( "first diff at line {line}:\n expected: {want}\n actual: {got}", ), None => "files differ but no line-level diff found (whitespace?)".to_string(), }; panic!( "IR snapshot mismatch for {example}\n{diff_msg}\nactual written to: {}\nrun UPDATE_SNAPSHOTS=1 cargo test ir_snapshot_ to refresh.", actual_path.display() ); } } #[test] fn ir_snapshot_sum() { check_ir_snapshot("sum.ail", "sum.ll"); } #[test] fn ir_snapshot_max3() { check_ir_snapshot("max3.ail", "max3.ll"); } #[test] fn ir_snapshot_hello() { check_ir_snapshot("hello.ail", "hello.ll"); } #[test] fn ir_snapshot_list() { check_ir_snapshot("list.ail", "list.ll"); } /// Guards workspace lowering (Iter 5c): the entry module `ws_main` /// imports `ws_lib`; both are emitted into the same `.ll`, /// `@ail_ws_main_main` calls `@ail_ws_lib_add`, and the trampoline /// `@main` calls `@ail_ws_main_main`. #[test] fn ir_snapshot_ws_main() { check_ir_snapshot("ws_main.ail", "ws_main.ll"); }