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(args: [&str; N]) -> std::process::Output { Command::new(env!("CARGO_BIN_EXE_glagol")) .args(args) .output() .expect("run glagol") } fn run_glagol_os(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 { 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 { 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); }