451 lines
16 KiB
Rust
451 lines
16 KiB
Rust
use std::{
|
|
env, fs,
|
|
path::{Path, PathBuf},
|
|
process::Command,
|
|
};
|
|
|
|
#[test]
|
|
fn c_import_emits_declare_and_direct_call_inside_unsafe() {
|
|
let output = run_glagol(["../examples/ffi/exp-6-c-add/main.slo"]);
|
|
let stdout = String::from_utf8_lossy(&output.stdout);
|
|
let stderr = String::from_utf8_lossy(&output.stderr);
|
|
|
|
assert!(
|
|
output.status.success(),
|
|
"compiler rejected C FFI fixture\nstdout:\n{}\nstderr:\n{}",
|
|
stdout,
|
|
stderr
|
|
);
|
|
assert!(
|
|
stdout.contains("declare i32 @c_add(i32, i32)")
|
|
&& stdout.contains("call i32 @c_add(i32 40, i32 2)")
|
|
&& stdout.contains("define i32 @main()"),
|
|
"LLVM output lost C import declaration/call shape\nstdout:\n{}",
|
|
stdout
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn c_import_call_requires_unsafe_before_backend_behavior() {
|
|
let output = run_glagol_source(
|
|
"c-ffi-unsafe-required",
|
|
"(module main)\n\n(import_c c_add ((lhs i32) (rhs i32)) -> i32)\n\n(fn main () -> i32\n (c_add 1 2))\n",
|
|
&[],
|
|
);
|
|
let stderr = String::from_utf8_lossy(&output.stderr);
|
|
|
|
assert!(
|
|
!output.status.success(),
|
|
"compiler accepted safe C import call"
|
|
);
|
|
assert!(
|
|
stderr.contains("(code UnsafeRequired)") && !stderr.contains("UnsupportedBackendFeature"),
|
|
"C import call outside unsafe did not produce UnsafeRequired first\nstderr:\n{}",
|
|
stderr
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn c_import_rejects_unsupported_signature_types() {
|
|
for (name, source) in [
|
|
(
|
|
"bool-param",
|
|
"(module main)\n\n(import_c c_bool ((flag bool)) -> i32)\n\n(fn main () -> i32\n 0)\n",
|
|
),
|
|
(
|
|
"string-return",
|
|
"(module main)\n\n(import_c c_string () -> string)\n\n(fn main () -> i32\n 0)\n",
|
|
),
|
|
(
|
|
"array-param",
|
|
"(module main)\n\n(import_c c_array ((xs (array i32 2))) -> i32)\n\n(fn main () -> i32\n 0)\n",
|
|
),
|
|
] {
|
|
let output = run_glagol_source(&format!("c-ffi-{}", name), source, &[]);
|
|
let stderr = String::from_utf8_lossy(&output.stderr);
|
|
assert!(
|
|
!output.status.success() && stderr.contains("(code UnsupportedCImportType)"),
|
|
"unsupported C import type `{}` did not produce UnsupportedCImportType\nstderr:\n{}",
|
|
name,
|
|
stderr
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn c_import_rejects_malformed_duplicate_and_reserved_names() {
|
|
let malformed = run_glagol_source(
|
|
"c-ffi-malformed",
|
|
"(module main)\n\n(import_c c_add ((lhs i32) (rhs i32)) i32)\n\n(fn main () -> i32\n 0)\n",
|
|
&[],
|
|
);
|
|
assert_diagnostic(&malformed, "MalformedCImport");
|
|
|
|
let invalid_symbol = run_glagol_source(
|
|
"c-ffi-invalid-symbol",
|
|
"(module main)\n\n(import_c c-add () -> i32)\n\n(fn main () -> i32\n 0)\n",
|
|
&[],
|
|
);
|
|
assert_diagnostic(&invalid_symbol, "MalformedCImport");
|
|
|
|
let duplicate_param = run_glagol_source(
|
|
"c-ffi-duplicate-param",
|
|
"(module main)\n\n(import_c c_add ((value i32) (value i32)) -> i32)\n\n(fn main () -> i32\n 0)\n",
|
|
&[],
|
|
);
|
|
assert_diagnostic(&duplicate_param, "DuplicateName");
|
|
|
|
let duplicate = run_glagol_source(
|
|
"c-ffi-duplicate",
|
|
"(module main)\n\n(fn c_add () -> i32\n 1)\n\n(import_c c_add () -> i32)\n\n(fn main () -> i32\n (c_add))\n",
|
|
&[],
|
|
);
|
|
assert_diagnostic(&duplicate, "DuplicateTopLevelName");
|
|
|
|
let reserved = run_glagol_source(
|
|
"c-ffi-reserved",
|
|
"(module main)\n\n(import_c ffi_call () -> i32)\n\n(fn main () -> i32\n 0)\n",
|
|
&[],
|
|
);
|
|
assert_diagnostic(&reserved, "ReservedName");
|
|
}
|
|
|
|
#[test]
|
|
fn raw_unsafe_ffi_call_remains_unsupported_inside_unsafe() {
|
|
let output = run_glagol_source(
|
|
"c-ffi-raw-ffi-call",
|
|
"(module main)\n\n(fn main () -> i32\n (unsafe\n (ffi_call 1)))\n",
|
|
&[],
|
|
);
|
|
assert_diagnostic(&output, "UnsupportedUnsafeOperation");
|
|
}
|
|
|
|
#[test]
|
|
fn formatter_and_lowering_show_c_imports() {
|
|
let fixture = "../examples/ffi/exp-6-c-add/main.slo";
|
|
let formatted = run_glagol(["--format", fixture]);
|
|
let stdout = String::from_utf8_lossy(&formatted.stdout);
|
|
assert!(
|
|
formatted.status.success()
|
|
&& stdout.contains("(import_c c_add ((lhs i32) (rhs i32)) -> i32)"),
|
|
"formatter did not preserve import_c\nstdout:\n{}\nstderr:\n{}",
|
|
stdout,
|
|
String::from_utf8_lossy(&formatted.stderr)
|
|
);
|
|
|
|
let surface = run_glagol(["--inspect-lowering=surface", fixture]);
|
|
let surface_stdout = String::from_utf8_lossy(&surface.stdout);
|
|
assert!(
|
|
surface.status.success()
|
|
&& surface_stdout.contains(" import_c c_add(lhs: i32, rhs: i32) -> i32"),
|
|
"surface lowering did not show C import\nstdout:\n{}",
|
|
surface_stdout
|
|
);
|
|
|
|
let checked = run_glagol(["--inspect-lowering=checked", fixture]);
|
|
let checked_stdout = String::from_utf8_lossy(&checked.stdout);
|
|
assert!(
|
|
checked.status.success()
|
|
&& checked_stdout.contains(" import_c c_add(lhs: i32, rhs: i32) -> i32"),
|
|
"checked lowering did not show C import\nstdout:\n{}",
|
|
checked_stdout
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_runner_reports_c_imports_as_unsupported_execution() {
|
|
let output = run_glagol(["--run-tests", "../examples/ffi/exp-6-c-add/main.slo"]);
|
|
let stderr = String::from_utf8_lossy(&output.stderr);
|
|
assert!(
|
|
!output.status.success() && stderr.contains("(code UnsupportedTestExpression)"),
|
|
"test runner did not report explicit C import execution diagnostic\nstderr:\n{}",
|
|
stderr
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_runner_filter_skips_unselected_c_import_execution() {
|
|
let source = "(module main)\n\n(import_c c_add ((lhs i32) (rhs i32)) -> i32)\n\n(test \"pure selected\"\n true)\n\n(test \"ffi skipped\"\n (= (unsafe\n (c_add 1 2)) 3))\n";
|
|
let path = temp_path("c-ffi-filter-skips-test-runner", "input.slo");
|
|
fs::write(&path, source).unwrap_or_else(|err| panic!("write `{}`: {}", path.display(), err));
|
|
|
|
let skipped = run_glagol_os([
|
|
"--run-tests".as_ref(),
|
|
"--filter".as_ref(),
|
|
"pure".as_ref(),
|
|
path.as_os_str(),
|
|
]);
|
|
let stdout = String::from_utf8_lossy(&skipped.stdout);
|
|
let stderr = String::from_utf8_lossy(&skipped.stderr);
|
|
assert!(
|
|
skipped.status.success()
|
|
&& stdout.contains("test \"ffi skipped\" ... skipped")
|
|
&& stdout.contains("selected 1")
|
|
&& stdout.contains("skipped 1")
|
|
&& !stderr.contains("UnsupportedTestExpression"),
|
|
"filtered run evaluated skipped C import test\nstdout:\n{}\nstderr:\n{}",
|
|
stdout,
|
|
stderr
|
|
);
|
|
|
|
let selected = run_glagol_os([
|
|
"--run-tests".as_ref(),
|
|
"--filter".as_ref(),
|
|
"ffi".as_ref(),
|
|
path.as_os_str(),
|
|
]);
|
|
let selected_stderr = String::from_utf8_lossy(&selected.stderr);
|
|
assert!(
|
|
!selected.status.success() && selected_stderr.contains("(code UnsupportedTestExpression)"),
|
|
"selected C import test did not keep unsupported execution diagnostic\nstderr:\n{}",
|
|
selected_stderr
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn unit_return_c_import_is_supported_and_release_gated() {
|
|
let source = "(module main)\n\n(import_c c_log ((value i32)) -> unit)\n\n(fn main () -> i32\n (unsafe\n (c_log 7))\n 0)\n";
|
|
|
|
let llvm = run_glagol_source("c-ffi-unit-return-llvm", source, &[]);
|
|
let stdout = String::from_utf8_lossy(&llvm.stdout);
|
|
assert!(
|
|
llvm.status.success()
|
|
&& stdout.contains("declare void @c_log(i32)")
|
|
&& stdout.contains("call void @c_log(i32 7)")
|
|
&& stdout.contains("define i32 @main()"),
|
|
"unit C import did not lower to void declaration/call\nstdout:\n{}\nstderr:\n{}",
|
|
stdout,
|
|
String::from_utf8_lossy(&llvm.stderr)
|
|
);
|
|
|
|
let formatted = run_glagol_source("c-ffi-unit-return-fmt", source, &["--format"]);
|
|
let formatted_stdout = String::from_utf8_lossy(&formatted.stdout);
|
|
assert!(
|
|
formatted.status.success()
|
|
&& formatted_stdout.contains("(import_c c_log ((value i32)) -> unit)")
|
|
&& formatted_stdout.contains("(c_log 7)"),
|
|
"formatter did not preserve unit C import\nstdout:\n{}\nstderr:\n{}",
|
|
formatted_stdout,
|
|
String::from_utf8_lossy(&formatted.stderr)
|
|
);
|
|
|
|
let source_path = temp_path("c-ffi-unit-return-manifest", "input.slo");
|
|
let manifest = temp_path("c-ffi-unit-return-manifest", "manifest.slo");
|
|
fs::write(&source_path, source)
|
|
.unwrap_or_else(|err| panic!("write `{}`: {}", source_path.display(), err));
|
|
let manifest_output = run_glagol_os([
|
|
"check".as_ref(),
|
|
source_path.as_os_str(),
|
|
"--manifest".as_ref(),
|
|
manifest.as_os_str(),
|
|
]);
|
|
assert_success("unit C FFI check manifest", &manifest_output);
|
|
let manifest_text = fs::read_to_string(&manifest).expect("read artifact manifest");
|
|
assert!(
|
|
manifest_text.contains("(foreign_import")
|
|
&& manifest_text.contains("(source_module \"main\")")
|
|
&& manifest_text.contains("(name \"c_log\")")
|
|
&& manifest_text.contains("(return \"unit\")"),
|
|
"unit C import manifest did not record source module and return metadata\n{}",
|
|
manifest_text
|
|
);
|
|
|
|
let test_source = "(module main)\n\n(import_c c_log ((value i32)) -> unit)\n\n(fn main () -> i32\n 0)\n\n(test \"unit C import test runner diagnostic\"\n (var i i32 0)\n (while (< i 1)\n (unsafe\n (c_log i))\n (set i (+ i 1)))\n (= i 1))\n";
|
|
let test_path = temp_path("c-ffi-unit-return-test-runner", "input.slo");
|
|
fs::write(&test_path, test_source)
|
|
.unwrap_or_else(|err| panic!("write `{}`: {}", test_path.display(), err));
|
|
let test_output = run_glagol_os(["--run-tests".as_ref(), test_path.as_os_str()]);
|
|
let test_stderr = String::from_utf8_lossy(&test_output.stderr);
|
|
assert!(
|
|
!test_output.status.success() && test_stderr.contains("(code UnsupportedTestExpression)"),
|
|
"test runner did not reject reached unit C import execution\nstderr:\n{}",
|
|
test_stderr
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn single_file_manifest_records_foreign_import_metadata() {
|
|
let manifest = temp_path("c-ffi-single-file-manifest", "manifest.slo");
|
|
let output = run_glagol_os([
|
|
"check".as_ref(),
|
|
"../examples/ffi/exp-6-c-add/main.slo".as_ref(),
|
|
"--manifest".as_ref(),
|
|
manifest.as_os_str(),
|
|
]);
|
|
assert_success("single-file C FFI check manifest", &output);
|
|
|
|
let manifest_text = fs::read_to_string(&manifest).expect("read artifact manifest");
|
|
assert!(
|
|
manifest_text.contains("(foreign_import")
|
|
&& manifest_text.contains("(name \"c_add\")")
|
|
&& manifest_text.contains("(source_module \"main\")")
|
|
&& manifest_text.contains("(symbol \"c_add\")")
|
|
&& manifest_text.contains("(abi \"experimental-fixture-c\")"),
|
|
"single-file manifest did not record C import metadata\n{}",
|
|
manifest_text
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn project_mode_emits_foreign_import_and_manifest_metadata() {
|
|
let fixture = "../examples/ffi/exp-6-c-add";
|
|
let manifest = temp_path("c-ffi-project-manifest", "manifest.slo");
|
|
let binary = temp_path("c-ffi-project-manifest", "bin");
|
|
let output = run_glagol_os([
|
|
"build".as_ref(),
|
|
fixture.as_ref(),
|
|
"-o".as_ref(),
|
|
binary.as_os_str(),
|
|
"--link-c".as_ref(),
|
|
"../examples/ffi/exp-6-c-add/c_add.c".as_ref(),
|
|
"--manifest".as_ref(),
|
|
manifest.as_os_str(),
|
|
]);
|
|
if !output.status.success() {
|
|
let stderr = String::from_utf8_lossy(&output.stderr);
|
|
if stderr.contains("ToolchainUnavailable") {
|
|
eprintln!("skipping project manifest assertion: clang is unavailable");
|
|
return;
|
|
}
|
|
panic!(
|
|
"project build failed\nstdout:\n{}\nstderr:\n{}",
|
|
String::from_utf8_lossy(&output.stdout),
|
|
stderr
|
|
);
|
|
}
|
|
let manifest_text = fs::read_to_string(&manifest).expect("read artifact manifest");
|
|
assert!(
|
|
manifest_text.contains("(foreign_import")
|
|
&& manifest_text.contains("(name \"c_add\")")
|
|
&& manifest_text.contains("(source_module \"main\")")
|
|
&& manifest_text.contains("(symbol \"c_add\")")
|
|
&& manifest_text.contains("(input \"../examples/ffi/exp-6-c-add/c_add.c\")"),
|
|
"manifest did not record C import/link input\n{}",
|
|
manifest_text
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn hosted_build_with_link_c_runs_when_clang_is_available() {
|
|
let Some(clang) = find_clang() else {
|
|
eprintln!("skipping C FFI hosted build: set GLAGOL_CLANG or install clang");
|
|
return;
|
|
};
|
|
|
|
let binary = temp_path("c-ffi-hosted", "bin");
|
|
let mut build_command = Command::new(env!("CARGO_BIN_EXE_glagol"));
|
|
build_command
|
|
.args([
|
|
"build".as_ref(),
|
|
"../examples/ffi/exp-6-c-add".as_ref(),
|
|
"-o".as_ref(),
|
|
binary.as_os_str(),
|
|
"--link-c".as_ref(),
|
|
"../examples/ffi/exp-6-c-add/c_add.c".as_ref(),
|
|
])
|
|
.env("GLAGOL_CLANG", &clang);
|
|
configure_clang_runtime_env(&mut build_command, &clang);
|
|
let build = build_command
|
|
.output()
|
|
.expect("run glagol hosted C FFI build");
|
|
assert_success("hosted C FFI build", &build);
|
|
|
|
let run = Command::new(&binary)
|
|
.output()
|
|
.expect("run hosted C FFI binary");
|
|
assert!(
|
|
run.status.code() == Some(42),
|
|
"hosted C FFI binary returned unexpected status\nstdout:\n{}\nstderr:\n{:?}",
|
|
String::from_utf8_lossy(&run.stdout),
|
|
String::from_utf8_lossy(&run.stderr)
|
|
);
|
|
}
|
|
|
|
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_os<const N: usize>(args: [&std::ffi::OsStr; 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, extra_args: &[&str]) -> std::process::Output {
|
|
let path = temp_path(name, "input.slo");
|
|
fs::write(&path, source).unwrap_or_else(|err| panic!("write `{}`: {}", path.display(), err));
|
|
let mut command = Command::new(env!("CARGO_BIN_EXE_glagol"));
|
|
command.arg(&path);
|
|
command.args(extra_args);
|
|
command
|
|
.output()
|
|
.unwrap_or_else(|err| panic!("run glagol on `{}`: {}", path.display(), err))
|
|
}
|
|
|
|
fn assert_diagnostic(output: &std::process::Output, code: &str) {
|
|
let stderr = String::from_utf8_lossy(&output.stderr);
|
|
assert!(
|
|
!output.status.success() && stderr.contains(&format!("(code {})", code)),
|
|
"expected diagnostic `{}`\nstdout:\n{}\nstderr:\n{}",
|
|
code,
|
|
String::from_utf8_lossy(&output.stdout),
|
|
stderr
|
|
);
|
|
}
|
|
|
|
fn assert_success(context: &str, output: &std::process::Output) {
|
|
assert!(
|
|
output.status.success(),
|
|
"{} failed\nstdout:\n{}\nstderr:\n{}",
|
|
context,
|
|
String::from_utf8_lossy(&output.stdout),
|
|
String::from_utf8_lossy(&output.stderr)
|
|
);
|
|
}
|
|
|
|
fn temp_path(name: &str, file: &str) -> PathBuf {
|
|
let dir = std::env::temp_dir().join(format!("glagol-{}-{}", name, std::process::id()));
|
|
fs::create_dir_all(&dir).unwrap_or_else(|err| panic!("create `{}`: {}", dir.display(), err));
|
|
dir.join(file)
|
|
}
|
|
|
|
fn find_clang() -> Option<PathBuf> {
|
|
if let Ok(path) = std::env::var("GLAGOL_CLANG") {
|
|
let path = PathBuf::from(path);
|
|
if path.is_file() {
|
|
return Some(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(name: &str) -> Option<PathBuf> {
|
|
let path = env::var_os("PATH")?;
|
|
env::split_paths(&path)
|
|
.map(|dir| dir.join(name))
|
|
.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 existing = env::var_os("LD_LIBRARY_PATH").unwrap_or_default();
|
|
let mut paths = vec![lib64, lib];
|
|
paths.extend(env::split_paths(&existing));
|
|
let joined = env::join_paths(paths).expect("join LD_LIBRARY_PATH");
|
|
command.env("LD_LIBRARY_PATH", joined);
|
|
}
|