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

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);
}