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

267 lines
8.6 KiB
Rust

use std::{fs, path::Path, process::Command};
const UNSAFE_HEADS: &[&str] = &[
"alloc",
"dealloc",
"load",
"store",
"ptr_add",
"unchecked_index",
"reinterpret",
"ffi_call",
];
#[test]
fn unsafe_fixture_is_formatter_stable() {
let fixture = Path::new("../tests/unsafe.slo");
let expected = fs::read_to_string(fixture).expect("read unsafe formatter fixture");
let output = run_glagol(["--format", fixture.to_str().expect("fixture path is UTF-8")]);
assert_success_stdout(output, &expected, "unsafe formatter output");
}
#[test]
fn unsafe_fixture_prints_expected_surface_ast() {
let expected =
fs::read_to_string("../tests/unsafe.surface.lower").expect("read surface fixture");
let output = run_glagol(["--inspect-lowering=surface", "../tests/unsafe.slo"]);
assert_success_stdout(output, &expected, "unsafe surface lowering output");
}
#[test]
fn unsafe_fixture_prints_expected_checked_ast() {
let expected =
fs::read_to_string("../tests/unsafe.checked.lower").expect("read checked fixture");
let output = run_glagol(["--inspect-lowering=checked", "../tests/unsafe.slo"]);
assert_success_stdout(output, &expected, "unsafe checked lowering output");
}
#[test]
fn unsafe_fixture_emits_safe_body_llvm_shape() {
let output = run_glagol(["../examples/unsafe.slo"]);
let stdout = String::from_utf8_lossy(&output.stdout);
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
output.status.success(),
"compiler rejected unsafe fixture\nstdout:\n{}\nstderr:\n{}",
stdout,
stderr
);
assert!(
stdout.contains("define i32 @add_one_in_unsafe(i32 %value)")
&& stdout.contains("%0 = add i32 %value, 1")
&& stdout.contains("ret i32 %0")
&& !stdout.contains("%one.addr = alloca i32")
&& !stdout.contains("load i32, ptr %one.addr")
&& stdout.contains("define i32 @main()"),
"compiler output for unsafe fixture lost safe block shape\nstdout:\n{}",
stdout
);
assert!(
!stdout.contains("unsafe block returns final value")
&& !stdout.contains("unsafe block can return bool"),
"compiler output should not emit top-level test names as LLVM metadata\nstdout:\n{}",
stdout
);
assert!(
stderr.is_empty(),
"compiler wrote stderr for unsafe fixture:\n{}",
stderr
);
}
#[test]
fn unsafe_fixture_runs_top_level_tests() {
let output = run_glagol(["--run-tests", "../examples/unsafe.slo"]);
assert_success_stdout(
output,
concat!(
"test \"unsafe block returns final value\" ... ok\n",
"test \"unsafe block can return bool\" ... ok\n",
"2 test(s) passed\n",
),
"unsafe test runner output",
);
}
#[test]
fn unsafe_operation_heads_require_marker_before_function_lookup() {
for head in UNSAFE_HEADS {
let source = format!("(module main)\n\n(fn main () -> i32\n ({} 1))\n", head);
let output = run_glagol_source(&format!("unsafe-required-{}", head), &source);
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
!output.status.success(),
"compiler accepted unsafe operation `{}` outside unsafe",
head
);
assert!(
stderr.contains("(code UnsafeRequired)") && !stderr.contains("UnknownFunction"),
"unsafe operation `{}` did not produce UnsafeRequired before call lookup\nstderr:\n{}",
head,
stderr
);
}
let output = run_glagol_source(
"reserved-unsafe-head",
"(module main)\n\n(fn alloc () -> i32\n 1)\n\n(fn main () -> i32\n (alloc))\n",
);
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
!output.status.success(),
"compiler allowed user-defined unsafe head call outside unsafe"
);
assert!(
stderr.contains("(code UnsafeRequired)") && !stderr.contains("UnknownFunction"),
"reserved unsafe head did not produce UnsafeRequired before call lookup\nstderr:\n{}",
stderr
);
}
#[test]
fn unsafe_operation_heads_are_reserved_from_user_bindings() {
for head in UNSAFE_HEADS {
let local_function = format!("(module main)\n\n(fn {} () -> i32\n 1)\n", head);
let output = run_glagol_source(&format!("reserved-function-{}", head), &local_function);
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
!output.status.success() && stderr.contains("(code DuplicateFunction)"),
"compiler allowed unsafe head `{}` as a function\nstderr:\n{}",
head,
stderr
);
let parameter = format!(
"(module main)\n\n(fn main (({} i32)) -> i32\n {})\n",
head, head
);
let output = run_glagol_source(&format!("reserved-param-{}", head), &parameter);
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
!output.status.success() && stderr.contains("(code ParameterShadowsCallable)"),
"compiler allowed unsafe head `{}` as a parameter\nstderr:\n{}",
head,
stderr
);
let local = format!(
"(module main)\n\n(fn main () -> i32\n (let {} i32 1)\n {})\n",
head, head
);
let output = run_glagol_source(&format!("reserved-local-{}", head), &local);
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
!output.status.success() && stderr.contains("(code LocalShadowsCallable)"),
"compiler allowed unsafe head `{}` as a local\nstderr:\n{}",
head,
stderr
);
}
}
#[test]
fn unsafe_operation_heads_remain_unsupported_inside_marker() {
for head in UNSAFE_HEADS {
let source = format!(
"(module main)\n\n(fn main () -> i32\n (unsafe\n ({} 1)))\n",
head
);
let output = run_glagol_source(&format!("unsupported-unsafe-{}", head), &source);
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
!output.status.success(),
"compiler accepted raw unsafe operation `{}` inside unsafe",
head
);
assert!(
stderr.contains("(code UnsupportedUnsafeOperation)")
&& !stderr.contains("UnknownFunction"),
"unsafe operation `{}` did not produce UnsupportedUnsafeOperation inside unsafe\nstderr:\n{}",
head,
stderr
);
}
}
#[test]
fn scoped_unsafe_locals_get_hygienic_llvm_storage_names() {
let source = r#"
(module main)
(fn scoped () -> i32
(unsafe
(var one i32 1)
(print_i32 one))
(unsafe
(var one i32 2)
(print_i32 one))
(var one i32 3)
one)
(fn main () -> i32
(scoped))
"#;
let output = run_glagol_source("unsafe-scoped-locals", source);
let stdout = String::from_utf8_lossy(&output.stdout);
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
output.status.success(),
"compiler rejected scoped unsafe locals\nstdout:\n{}\nstderr:\n{}",
stdout,
stderr
);
assert!(
stdout.matches("%one.addr = alloca i32").count() == 1
&& stdout.matches("%one.addr.1 = alloca i32").count() == 1
&& stdout.matches("%one.addr.2 = alloca i32").count() == 1,
"LLVM output did not assign hygienic local storage names\nstdout:\n{}",
stdout
);
}
fn run_glagol<const N: usize>(args: [&str; N]) -> std::process::Output {
Command::new(env!("CARGO_BIN_EXE_glagol"))
.args(args)
.output()
.expect("run glagol")
}
fn run_glagol_source(name: &str, source: &str) -> std::process::Output {
let path =
std::env::temp_dir().join(format!("glagol-unsafe-{}-{}.slo", name, std::process::id()));
fs::write(&path, source).unwrap_or_else(|err| panic!("write `{}`: {}", path.display(), err));
let output = Command::new(env!("CARGO_BIN_EXE_glagol"))
.arg(&path)
.output()
.unwrap_or_else(|err| panic!("run glagol on `{}`: {}", path.display(), err));
let _ = fs::remove_file(path);
output
}
fn assert_success_stdout(output: std::process::Output, expected: &str, context: &str) {
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
);
assert_eq!(stdout, expected, "{} drifted", context);
assert!(stderr.is_empty(), "{} wrote stderr:\n{}", context, stderr);
}