From 74a20055c376f3ebe6bdba74d156de6ecb65854a Mon Sep 17 00:00:00 2001 From: Brummel Date: Thu, 7 May 2026 11:14:41 +0200 Subject: [PATCH] Iter 4c: IR-Snapshot-Tests als Codegen-Regressions-Schutz MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Vier Snapshots in crates/ail/tests/snapshots/ (sum, max3, hello, list) sichern den erzeugten LLVM-IR. Test-Helper normalisiert target triple und trailing whitespace, sonst byte-für-byte-Vergleich. Update via UPDATE_SNAPSHOTS=1 cargo test ir_snapshot_. Mismatch erzeugt eine .actual-Datei mit dem aktuellen Output für Diff-Inspektion. --- .gitignore | 2 + crates/ail/tests/ir_snapshot.rs | 153 ++++++++++++++++++++++++++++ crates/ail/tests/snapshots/hello.ll | 21 ++++ crates/ail/tests/snapshots/list.ll | 66 ++++++++++++ crates/ail/tests/snapshots/max3.ll | 64 ++++++++++++ crates/ail/tests/snapshots/sum.ll | 38 +++++++ 6 files changed, 344 insertions(+) create mode 100644 crates/ail/tests/ir_snapshot.rs create mode 100644 crates/ail/tests/snapshots/hello.ll create mode 100644 crates/ail/tests/snapshots/list.ll create mode 100644 crates/ail/tests/snapshots/max3.ll create mode 100644 crates/ail/tests/snapshots/sum.ll diff --git a/.gitignore b/.gitignore index 7c4cc47..81f760a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ /target *.ll.o /build +crates/ail/tests/snapshots/*.actual +crates/ail/tests/snapshots/*.ll.actual diff --git a/crates/ail/tests/ir_snapshot.rs b/crates/ail/tests/ir_snapshot.rs new file mode 100644 index 0000000..2f9ab42 --- /dev/null +++ b/crates/ail/tests/ir_snapshot.rs @@ -0,0 +1,153 @@ +//! IR-Snapshot-Tests: schützen die Codegen-Pipeline vor unbeabsichtigten +//! Veränderungen am LLVM-IR. Snapshots in tests/snapshots/. Update mit +//! UPDATE_SNAPSHOTS=1 cargo test ir_snapshot_. + +use std::path::{Path, PathBuf}; +use std::process::Command; + +fn ail_bin() -> &'static str { + env!("CARGO_BIN_EXE_ail") +} + +/// Normalisiert IR-Text für Plattform-Stabilität: +/// - `target triple = "..."` -> `target triple = ""` +/// - trailing whitespace pro Zeile entfernen +/// - LF-Zeilenenden, Datei endet mit genau einem `\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 ist `"..."` (mit Anführungszeichen) — komplett normalisieren. + let _ = rest; + out.push_str("target triple = \"\""); + } else { + out.push_str(trimmed); + } + out.push('\n'); + } + // split('\n') erzeugt nach einem trailing `\n` ein leeres letztes Stück, + // das oben mit `\n` abgeschlossen wurde. Dadurch enthält `out` typischerweise + // genau ein abschließendes `\n`. Doppelte `\n\n` am Ende reduzieren. + 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 examples_dir() -> PathBuf { + let manifest_dir = env!("CARGO_MANIFEST_DIR"); + Path::new(manifest_dir) + .parent() + .unwrap() + .parent() + .unwrap() + .join("examples") +} + +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.json", "sum.ll"); +} + +#[test] +fn ir_snapshot_max3() { + check_ir_snapshot("max3.ail.json", "max3.ll"); +} + +#[test] +fn ir_snapshot_hello() { + check_ir_snapshot("hello.ail.json", "hello.ll"); +} + +#[test] +fn ir_snapshot_list() { + check_ir_snapshot("list.ail.json", "list.ll"); +} diff --git a/crates/ail/tests/snapshots/hello.ll b/crates/ail/tests/snapshots/hello.ll new file mode 100644 index 0000000..306a509 --- /dev/null +++ b/crates/ail/tests/snapshots/hello.ll @@ -0,0 +1,21 @@ +; AILang generated module: hello +source_filename = "hello.ail" +target triple = "" + +@.str_str_0 = private unnamed_addr constant [15 x i8] c"Hallo, AILang.\00", align 1 + +declare i32 @printf(ptr, ...) +declare i32 @puts(ptr) +declare ptr @malloc(i64) + +define i8 @ail_main() { +entry: + call i32 @puts(ptr @.str_str_0) + ret i8 0 +} + + +define i32 @main() { + call i8 @ail_main() + ret i32 0 +} diff --git a/crates/ail/tests/snapshots/list.ll b/crates/ail/tests/snapshots/list.ll new file mode 100644 index 0000000..510f47b --- /dev/null +++ b/crates/ail/tests/snapshots/list.ll @@ -0,0 +1,66 @@ +; AILang generated module: list +source_filename = "list.ail" +target triple = "" + +@.str_fmt_int_0 = private unnamed_addr constant [6 x i8] c"%lld\0A\00", align 1 + +declare i32 @printf(ptr, ...) +declare i32 @puts(ptr) +declare ptr @malloc(i64) + +define i64 @ail_sum_list(ptr %arg_xs) { +entry: + %v1 = load i64, ptr %arg_xs, align 8 + switch i64 %v1, label %mdefault.2 [ + i64 0, label %marm.2.0 + i64 1, label %marm.2.1 + ] +marm.2.0: + br label %mjoin.2 +marm.2.1: + %v3 = getelementptr inbounds i8, ptr %arg_xs, i64 8 + %v4 = load i64, ptr %v3, align 8 + %v5 = getelementptr inbounds i8, ptr %arg_xs, i64 16 + %v6 = load ptr, ptr %v5, align 8 + %v7 = call i64 @ail_sum_list(ptr %v6) + %v8 = add i64 %v4, %v7 + br label %mjoin.2 +mdefault.2: + unreachable +mjoin.2: + %v9 = phi i64 [ 0, %marm.2.0 ], [ %v8, %marm.2.1 ] + ret i64 %v9 +} + +define i8 @ail_main() { +entry: + %v1 = call ptr @malloc(i64 8) + store i64 0, ptr %v1, align 8 + %v2 = call ptr @malloc(i64 24) + store i64 1, ptr %v2, align 8 + %v3 = getelementptr inbounds i8, ptr %v2, i64 8 + store i64 12, ptr %v3, align 8 + %v4 = getelementptr inbounds i8, ptr %v2, i64 16 + store ptr %v1, ptr %v4, align 8 + %v5 = call ptr @malloc(i64 24) + store i64 1, ptr %v5, align 8 + %v6 = getelementptr inbounds i8, ptr %v5, i64 8 + store i64 20, ptr %v6, align 8 + %v7 = getelementptr inbounds i8, ptr %v5, i64 16 + store ptr %v2, ptr %v7, align 8 + %v8 = call ptr @malloc(i64 24) + store i64 1, ptr %v8, align 8 + %v9 = getelementptr inbounds i8, ptr %v8, i64 8 + store i64 10, ptr %v9, align 8 + %v10 = getelementptr inbounds i8, ptr %v8, i64 16 + store ptr %v5, ptr %v10, align 8 + %v11 = call i64 @ail_sum_list(ptr %v8) + call i32 (ptr, ...) @printf(ptr @.str_fmt_int_0, i64 %v11) + ret i8 0 +} + + +define i32 @main() { + call i8 @ail_main() + ret i32 0 +} diff --git a/crates/ail/tests/snapshots/max3.ll b/crates/ail/tests/snapshots/max3.ll new file mode 100644 index 0000000..41e204b --- /dev/null +++ b/crates/ail/tests/snapshots/max3.ll @@ -0,0 +1,64 @@ +; AILang generated module: max3 +source_filename = "max3.ail" +target triple = "" + +@.str_fmt_int_0 = private unnamed_addr constant [6 x i8] c"%lld\0A\00", align 1 + +declare i32 @printf(ptr, ...) +declare i32 @puts(ptr) +declare ptr @malloc(i64) + +define i64 @ail_max(i64 %arg_a, i64 %arg_b) { +entry: + %v1 = icmp sgt i64 %arg_a, %arg_b + br i1 %v1, label %then.2, label %else.2 +then.2: + br label %join.2 +else.2: + br label %join.2 +join.2: + %v3 = phi i64 [ %arg_a, %then.2 ], [ %arg_b, %else.2 ] + ret i64 %v3 +} + +define i64 @ail_max3(i64 %arg_a, i64 %arg_b, i64 %arg_c) { +entry: + %v1 = icmp sgt i64 %arg_a, %arg_b + br i1 %v1, label %then.2, label %else.2 +then.2: + %v3 = icmp sgt i64 %arg_a, %arg_c + br i1 %v3, label %then.4, label %else.4 +then.4: + br label %join.4 +else.4: + br label %join.4 +join.4: + %v5 = phi i64 [ %arg_a, %then.4 ], [ %arg_c, %else.4 ] + br label %join.2 +else.2: + %v6 = icmp sgt i64 %arg_b, %arg_c + br i1 %v6, label %then.7, label %else.7 +then.7: + br label %join.7 +else.7: + br label %join.7 +join.7: + %v8 = phi i64 [ %arg_b, %then.7 ], [ %arg_c, %else.7 ] + br label %join.2 +join.2: + %v9 = phi i64 [ %v5, %join.4 ], [ %v8, %join.7 ] + ret i64 %v9 +} + +define i8 @ail_main() { +entry: + %v1 = call i64 @ail_max3(i64 3, i64 17, i64 9) + call i32 (ptr, ...) @printf(ptr @.str_fmt_int_0, i64 %v1) + ret i8 0 +} + + +define i32 @main() { + call i8 @ail_main() + ret i32 0 +} diff --git a/crates/ail/tests/snapshots/sum.ll b/crates/ail/tests/snapshots/sum.ll new file mode 100644 index 0000000..4b72c6e --- /dev/null +++ b/crates/ail/tests/snapshots/sum.ll @@ -0,0 +1,38 @@ +; AILang generated module: sum +source_filename = "sum.ail" +target triple = "" + +@.str_fmt_int_0 = private unnamed_addr constant [6 x i8] c"%lld\0A\00", align 1 + +declare i32 @printf(ptr, ...) +declare i32 @puts(ptr) +declare ptr @malloc(i64) + +define i64 @ail_sum(i64 %arg_n) { +entry: + %v1 = icmp eq i64 %arg_n, 0 + br i1 %v1, label %then.2, label %else.2 +then.2: + br label %join.2 +else.2: + %v3 = sub i64 %arg_n, 1 + %v4 = call i64 @ail_sum(i64 %v3) + %v5 = add i64 %arg_n, %v4 + br label %join.2 +join.2: + %v6 = phi i64 [ 0, %then.2 ], [ %v5, %else.2 ] + ret i64 %v6 +} + +define i8 @ail_main() { +entry: + %v1 = call i64 @ail_sum(i64 10) + call i32 (ptr, ...) @printf(ptr @.str_fmt_int_0, i64 %v1) + ret i8 0 +} + + +define i32 @main() { + call i8 @ail_main() + ret i32 0 +}