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), ¶meter); 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(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); }