Files
AILang/crates/ail/tests/ir_snapshot.rs
T
Brummel 7577ab8a90 Translate project content to English
Make English the project-wide language: all comments, string literals,
CLI help text, design docs, journal, agent prompts, and README. Only
CLAUDE.md (the user's own instruction file) stays German, and the live
conversation between user and Claude continues in German.

Adds a new "Project language: English" section to docs/DESIGN.md as
the durable convention. No logic changes — translation only. Four
internal error strings in the typechecker were retranslated; no
test asserts on their wording.

Verified: cargo test --workspace passes (44/44).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 12:17:48 +02:00

163 lines
5.1 KiB
Rust

//! 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 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 = "<NORMALIZED>"`
/// - 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 = \"<NORMALIZED>\"");
} 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 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("<EOF>"))),
(None, Some(y)) => return Some((idx, String::from("<EOF>"), 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");
}
/// 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.json", "ws_main.ll");
}