267 lines
8.6 KiB
Rust
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), ¶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<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);
|
|
}
|