slovo/compiler/tests/f64_numeric_primitive_alpha.rs
2026-05-22 08:38:43 +02:00

255 lines
8.1 KiB
Rust

use std::{
env,
ffi::OsStr,
fs,
path::{Path, PathBuf},
process::{Command, Output},
sync::atomic::{AtomicUsize, Ordering},
};
static NEXT_FIXTURE_ID: AtomicUsize = AtomicUsize::new(0);
#[test]
fn f64_fixture_lowers_and_runs_tests() {
let fixture = Path::new(env!("CARGO_MANIFEST_DIR")).join("../tests/f64-numeric-primitive.slo");
let llvm = run_glagol([fixture.as_os_str()]);
assert_success("compile f64 fixture", &llvm);
let stdout = String::from_utf8_lossy(&llvm.stdout);
assert!(
stdout.contains("declare void @print_f64(double)")
&& stdout.contains("define double @half(double %value)")
&& stdout.contains("fadd double")
&& stdout.contains("fmul double")
&& stdout.contains("fdiv double")
&& stdout.contains("fcmp ogt double")
&& stdout.contains("fcmp olt double")
&& stdout.contains("fcmp oeq double")
&& stdout.contains("call void @print_f64(double"),
"f64 LLVM shape drifted\nstdout:\n{}",
stdout
);
let tests = run_glagol([OsStr::new("test"), fixture.as_os_str()]);
assert_success("run f64 fixture tests", &tests);
assert_eq!(
String::from_utf8_lossy(&tests.stdout),
concat!(
"test \"f64 arithmetic returns exact fixture value\" ... ok\n",
"test \"f64 comparison works in predicates\" ... ok\n",
"test \"f64 division and equality\" ... ok\n",
"3 test(s) passed\n",
),
"f64 test runner stdout drifted"
);
}
#[test]
fn f64_formatter_and_lowering_are_visible() {
let fixture = Path::new(env!("CARGO_MANIFEST_DIR")).join("../tests/f64-numeric-primitive.slo");
let formatted = run_glagol([OsStr::new("--format"), fixture.as_os_str()]);
assert_success("format f64 fixture", &formatted);
let formatted_stdout = String::from_utf8_lossy(&formatted.stdout);
assert!(
formatted_stdout.contains("(fn half ((value f64)) -> f64")
&& formatted_stdout.contains("(/ value 2.0)")
&& formatted_stdout.contains("(std.io.print_f64 (local_total))"),
"f64 formatter output omitted expected forms\nstdout:\n{}",
formatted_stdout
);
let surface = run_glagol([
OsStr::new("--inspect-lowering=surface"),
fixture.as_os_str(),
]);
assert_success("inspect f64 surface lowering", &surface);
let surface_stdout = String::from_utf8_lossy(&surface.stdout);
assert_eq!(
surface_stdout,
fs::read_to_string(
Path::new(env!("CARGO_MANIFEST_DIR"))
.join("../tests/f64-numeric-primitive.surface.lower")
)
.expect("read f64 surface snapshot"),
"f64 surface lowering snapshot drifted"
);
let checked = run_glagol([
OsStr::new("--inspect-lowering=checked"),
fixture.as_os_str(),
]);
assert_success("inspect f64 checked lowering", &checked);
let checked_stdout = String::from_utf8_lossy(&checked.stdout);
assert_eq!(
checked_stdout,
fs::read_to_string(
Path::new(env!("CARGO_MANIFEST_DIR"))
.join("../tests/f64-numeric-primitive.checked.lower")
)
.expect("read f64 checked snapshot"),
"f64 checked lowering snapshot drifted"
);
}
#[test]
fn mixed_i32_f64_operands_are_rejected_clearly() {
let fixture = write_fixture(
"mixed-numeric",
r#"
(module main)
(fn main () -> i32
(if (= 1 1.0) 0 1))
"#,
);
let output = run_glagol([fixture.as_os_str()]);
let stdout = String::from_utf8_lossy(&output.stdout);
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
!output.status.success(),
"compiler unexpectedly accepted mixed numeric operands\nstdout:\n{}\nstderr:\n{}",
stdout,
stderr
);
assert!(
stdout.is_empty(),
"rejected compile wrote stdout:\n{}",
stdout
);
assert!(
stderr.contains("numeric operands must have the same primitive type")
&& stderr.contains("mixed i32/i64/u32/u64/f64"),
"mixed numeric diagnostic drifted\nstderr:\n{}",
stderr
);
}
#[test]
fn f64_runtime_print_smoke_when_clang_is_available() {
let Some(clang) = find_clang() else {
eprintln!("skipping f64 runtime smoke: set GLAGOL_CLANG or install clang");
return;
};
let fixture =
Path::new(env!("CARGO_MANIFEST_DIR")).join("../examples/f64-numeric-primitive.slo");
let compile = run_glagol([fixture.as_os_str()]);
assert_success("compile f64 runtime smoke", &compile);
let run = compile_and_run_with_runtime(&clang, "f64-runtime-smoke", &compile.stdout);
assert_success("run f64 runtime smoke", &run);
assert_eq!(
String::from_utf8_lossy(&run.stdout),
"10\n",
"f64 runtime print stdout drifted"
);
}
fn run_glagol<I, S>(args: I) -> Output
where
I: IntoIterator<Item = S>,
S: AsRef<OsStr>,
{
Command::new(env!("CARGO_BIN_EXE_glagol"))
.args(args)
.current_dir(Path::new(env!("CARGO_MANIFEST_DIR")))
.output()
.expect("run glagol")
}
fn write_fixture(name: &str, source: &str) -> PathBuf {
let mut path = env::temp_dir();
path.push(format!(
"glagol-f64-alpha-{}-{}-{}.slo",
name,
std::process::id(),
NEXT_FIXTURE_ID.fetch_add(1, Ordering::Relaxed)
));
fs::write(&path, source).unwrap_or_else(|err| panic!("write `{}`: {}", path.display(), err));
path
}
fn assert_success(context: &str, output: &Output) {
let stdout = String::from_utf8_lossy(&output.stdout);
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
output.status.success(),
"{} failed\nstdout:\n{}\nstderr:\n{}",
context,
stdout,
stderr
);
}
fn compile_and_run_with_runtime(clang: &Path, name: &str, ir: &[u8]) -> Output {
let manifest = Path::new(env!("CARGO_MANIFEST_DIR"));
let temp_dir = env::temp_dir().join(format!(
"glagol-f64-alpha-{}-{}",
std::process::id(),
NEXT_FIXTURE_ID.fetch_add(1, Ordering::Relaxed)
));
fs::create_dir_all(&temp_dir)
.unwrap_or_else(|err| panic!("create `{}`: {}", temp_dir.display(), err));
let ir_path = temp_dir.join(format!("{}.ll", name));
let exe_path = temp_dir.join(name);
fs::write(&ir_path, ir).unwrap_or_else(|err| panic!("write `{}`: {}", ir_path.display(), err));
let runtime = manifest.join("../runtime/runtime.c");
let mut clang_command = Command::new(clang);
clang_command
.arg(&runtime)
.arg(&ir_path)
.arg("-o")
.arg(&exe_path)
.current_dir(manifest);
configure_clang_runtime_env(&mut clang_command, clang);
let clang_output = clang_command
.output()
.unwrap_or_else(|err| panic!("run `{}`: {}", clang.display(), err));
assert_success("clang f64 runtime smoke", &clang_output);
Command::new(&exe_path)
.output()
.unwrap_or_else(|err| panic!("run `{}`: {}", exe_path.display(), err))
}
fn find_clang() -> Option<PathBuf> {
if let Some(path) = env::var_os("GLAGOL_CLANG").filter(|value| !value.is_empty()) {
return Some(PathBuf::from(path));
}
let hermetic_clang = PathBuf::from("/tmp/glagol-clang-root/usr/bin/clang");
if hermetic_clang.is_file() {
return Some(hermetic_clang);
}
find_on_path("clang")
}
fn find_on_path(program: &str) -> Option<PathBuf> {
let path = env::var_os("PATH")?;
env::split_paths(&path)
.map(|dir| dir.join(program))
.find(|candidate| candidate.is_file())
}
fn configure_clang_runtime_env(command: &mut Command, clang: &Path) {
if !clang.starts_with("/tmp/glagol-clang-root") {
return;
}
let root = Path::new("/tmp/glagol-clang-root");
let lib64 = root.join("usr/lib64");
let lib = root.join("usr/lib");
let mut paths = vec![lib64, lib];
if let Some(existing) = env::var_os("LD_LIBRARY_PATH") {
paths.extend(env::split_paths(&existing));
}
let joined = env::join_paths(paths).expect("join LD_LIBRARY_PATH");
command.env("LD_LIBRARY_PATH", joined);
}