slovo/compiler/tests/result_based_host_errors.rs

1132 lines
34 KiB
Rust

use std::{
env,
ffi::OsStr,
fs,
io::Write,
path::{Path, PathBuf},
process::{Command, Output, Stdio},
sync::atomic::{AtomicUsize, Ordering},
time::{SystemTime, UNIX_EPOCH},
};
static NEXT_FIXTURE_ID: AtomicUsize = AtomicUsize::new(0);
#[test]
fn exp10_fixture_formats_and_lowers_when_implementation_lands() {
let fixture = Path::new(env!("CARGO_MANIFEST_DIR")).join("../tests/host-io-result.slo");
let formatted = run_glagol([OsStr::new("--format"), fixture.as_os_str()]);
assert_success_stdout(
formatted,
&fs::read_to_string(&fixture).expect("read exp-10 formatter fixture"),
"exp-10 formatter fixture",
);
let surface = run_glagol([
OsStr::new("--inspect-lowering=surface"),
fixture.as_os_str(),
]);
assert_success("exp-10 surface lowering", &surface);
let surface_stdout = String::from_utf8_lossy(&surface.stdout);
assert!(
surface_stdout.contains("fn ok_text(value: string) -> (result string i32)")
&& surface_stdout.contains("ok string i32")
&& surface_stdout.contains("call std.process.arg_result")
&& surface_stdout.contains("call std.env.get_result")
&& surface_stdout.contains("call std.fs.read_text_result")
&& surface_stdout.contains("call std.fs.write_text_result")
&& surface_stdout.contains("match")
&& surface_stdout.contains("unwrap_ok")
&& surface_stdout.contains("unwrap_err"),
"surface lowering lost exp-10 result host shape\nstdout:\n{}",
surface_stdout
);
let checked = run_glagol([
OsStr::new("--inspect-lowering=checked"),
fixture.as_os_str(),
]);
assert_success("exp-10 checked lowering", &checked);
let checked_stdout = String::from_utf8_lossy(&checked.stdout);
assert!(
checked_stdout.contains("ok : (result string i32)")
&& checked_stdout.contains("err : (result string i32)")
&& checked_stdout.contains("call std.process.arg_result : (result string i32)")
&& checked_stdout.contains("call std.env.get_result : (result string i32)")
&& checked_stdout.contains("call std.fs.read_text_result : (result string i32)")
&& checked_stdout.contains("call std.fs.write_text_result : (result i32 i32)"),
"checked lowering lost exp-10 typed result host shape\nstdout:\n{}",
checked_stdout
);
}
#[test]
fn exp10_fixture_emits_private_result_host_runtime_shape() {
let fixture = Path::new(env!("CARGO_MANIFEST_DIR")).join("../examples/host-io-result.slo");
let compile = run_glagol([fixture.as_os_str()]);
assert_success("compile exp-10 host result fixture", &compile);
let stdout = String::from_utf8_lossy(&compile.stdout);
assert!(
stdout.contains("__glagol_process_arg_result")
&& stdout.contains("__glagol_env_get_result")
&& stdout.contains("__glagol_fs_read_text_result")
&& stdout.contains("__glagol_fs_write_text_result")
&& stdout.contains("ptr")
&& stdout.contains("i32")
&& !stdout.contains("@std.process.arg_result")
&& !stdout.contains("@std.env.get_result")
&& !stdout.contains("@std.fs.read_text_result")
&& !stdout.contains("@std.fs.write_text_result"),
"LLVM output did not contain expected exp-10 private runtime shape\nstdout:\n{}",
stdout
);
}
#[test]
fn test_runner_executes_deterministic_exp10_ok_and_err_paths() {
let root = temp_root("test-runner");
fs::create_dir_all(&root).unwrap_or_else(|err| panic!("create `{}`: {}", root.display(), err));
let existing = root.join("existing.txt");
let roundtrip = root.join("roundtrip.txt");
let missing = root.join("missing.txt");
let unwritable = root.join("missing-dir").join("out.txt");
fs::write(&existing, "fixture text")
.unwrap_or_else(|err| panic!("write `{}`: {}", existing.display(), err));
let missing_env = format!(
"GLAGOL_EXP10_MISSING_{}_{}",
std::process::id(),
NEXT_FIXTURE_ID.fetch_add(1, Ordering::Relaxed)
);
let source = format!(
r#"
(module main)
(test "env present result ok"
(= (unwrap_ok (std.env.get_result "GLAGOL_EXP10_PRESENT")) "env-value"))
(test "env missing result err one"
(= (unwrap_err (std.env.get_result "{}")) 1))
(test "arg zero result ok"
(is_ok (std.process.arg_result 0)))
(test "arg negative result err one"
(= (unwrap_err (std.process.arg_result -1)) 1))
(test "arg out of range result err one"
(= (unwrap_err (std.process.arg_result 99999)) 1))
(test "read text result ok"
(= (unwrap_ok (std.fs.read_text_result "{}")) "fixture text"))
(test "read text result err one"
(= (unwrap_err (std.fs.read_text_result "{}")) 1))
(test "write text result ok zero"
(= (unwrap_ok (std.fs.write_text_result "{}" "roundtrip")) 0))
(test "write text result err one"
(= (unwrap_err (std.fs.write_text_result "{}" "nope")) 1))
(test "read written text result ok"
(= (unwrap_ok (std.fs.read_text_result "{}")) "roundtrip"))
(test "string payload can be matched"
(= (match (std.fs.read_text_result "{}")
((ok payload)
(std.string.len payload))
((err code)
code))
12))
(test "stdin result deterministic ok"
(is_ok (std.io.read_stdin_result)))
(test "stdin result deterministic empty payload"
(= (std.string.len (unwrap_ok (std.io.read_stdin_result))) 0))
"#,
missing_env,
slovo_path(&existing),
slovo_path(&missing),
slovo_path(&roundtrip),
slovo_path(&unwritable),
slovo_path(&roundtrip),
slovo_path(&existing)
);
let fixture = write_fixture("test-runner", &source);
let run = run_glagol_configured([OsStr::new("test"), fixture.as_os_str()], |command| {
command
.env("GLAGOL_EXP10_PRESENT", "env-value")
.env_remove(&missing_env);
});
assert_success_stdout(
run,
concat!(
"test \"env present result ok\" ... ok\n",
"test \"env missing result err one\" ... ok\n",
"test \"arg zero result ok\" ... ok\n",
"test \"arg negative result err one\" ... ok\n",
"test \"arg out of range result err one\" ... ok\n",
"test \"read text result ok\" ... ok\n",
"test \"read text result err one\" ... ok\n",
"test \"write text result ok zero\" ... ok\n",
"test \"write text result err one\" ... ok\n",
"test \"read written text result ok\" ... ok\n",
"test \"string payload can be matched\" ... ok\n",
"test \"stdin result deterministic ok\" ... ok\n",
"test \"stdin result deterministic empty payload\" ... ok\n",
"13 test(s) passed\n",
),
"exp-10 test-runner host result output",
);
}
#[test]
fn exp3_host_calls_keep_trap_and_status_behavior() {
let root = temp_root("exp3-regression");
fs::create_dir_all(&root).unwrap_or_else(|err| panic!("create `{}`: {}", root.display(), err));
let unwritable = root.join("missing-dir").join("out.txt");
let missing_env = format!(
"GLAGOL_EXP10_EXP3_MISSING_{}_{}",
std::process::id(),
NEXT_FIXTURE_ID.fetch_add(1, Ordering::Relaxed)
);
let source = format!(
r#"
(module main)
(test "exp3 missing env is still empty"
(= (std.env.get "{}") ""))
(test "exp3 write failure is still status one"
(= (std.fs.write_text "{}" "nope") 1))
"#,
missing_env,
slovo_path(&unwritable)
);
let fixture = write_fixture("exp3-regression", &source);
let run = run_glagol_configured([OsStr::new("test"), fixture.as_os_str()], |command| {
command.env_remove(&missing_env);
});
assert_success_stdout(
run,
concat!(
"test \"exp3 missing env is still empty\" ... ok\n",
"test \"exp3 write failure is still status one\" ... ok\n",
"2 test(s) passed\n",
),
"exp-3 host regression output",
);
}
#[test]
fn exp10_diagnostics_cover_promoted_and_deferred_boundaries() {
let cases = [
(
"arg-result-arity",
r#"
(module main)
(fn main () -> i32
(std.process.arg_result)
0)
"#,
"ArityMismatch",
),
(
"env-result-type",
r#"
(module main)
(fn main () -> i32
(std.env.get_result 1)
0)
"#,
"TypeMismatch",
),
(
"write-result-type",
r#"
(module main)
(fn main () -> i32
(std.fs.write_text_result "path" 1)
0)
"#,
"TypeMismatch",
),
(
"fs-exists-type",
r#"
(module main)
(fn main () -> i32
(std.fs.exists 1)
0)
"#,
"TypeMismatch",
),
(
"fs-remove-file-type",
r#"
(module main)
(fn main () -> i32
(std.fs.remove_file_result 1)
0)
"#,
"TypeMismatch",
),
(
"fs-create-dir-type",
r#"
(module main)
(fn main () -> i32
(std.fs.create_dir_result 1)
0)
"#,
"TypeMismatch",
),
(
"fs-open-handle-type",
r#"
(module main)
(fn main () -> i32
(std.fs.open_text_read_result 1)
0)
"#,
"TypeMismatch",
),
(
"fs-close-handle-type",
r#"
(module main)
(fn main () -> i32
(std.fs.close_result "handle")
0)
"#,
"TypeMismatch",
),
(
"unsupported-result-payload-family",
r#"
(module main)
(fn main () -> i32
(ok string bool "value"))
"#,
"UnsupportedResultPayloadType",
),
(
"result-equality",
r#"
(module main)
(fn main () -> i32
(if (= (ok string i32 "a") (err string i32 1)) 1 0))
"#,
"UnsupportedOptionResultEquality",
),
(
"result-printing",
r#"
(module main)
(fn main () -> i32
(std.io.print_i32 (ok string i32 "a"))
0)
"#,
"UnsupportedOptionResultPrint",
),
(
"result-mapping",
r#"
(module main)
(fn main () -> i32
(std.result.map (ok string i32 "a"))
0)
"#,
"UnsupportedStandardLibraryCall",
),
(
"promoted-name-shadow",
r#"
(module main)
(fn std.process.arg_result ((index i32)) -> (result string i32)
(err string i32 1))
(fn main () -> i32
0)
"#,
"DuplicateFunction",
),
(
"helper-shadow",
r#"
(module main)
(fn __glagol_process_arg_result ((index i32)) -> (result string i32)
(err string i32 1))
(fn main () -> i32
0)
"#,
"DuplicateFunction",
),
];
for (name, source, expected_code) in cases {
let fixture = write_fixture(name, source);
let output = run_glagol([fixture.as_os_str()]);
let stdout = String::from_utf8_lossy(&output.stdout);
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
!output.status.success(),
"compiler unexpectedly accepted `{}`\nstdout:\n{}\nstderr:\n{}",
name,
stdout,
stderr
);
assert!(
stdout.is_empty(),
"rejected exp-10 diagnostic case `{}` wrote stdout:\n{}",
name,
stdout
);
assert!(
stderr.contains(expected_code),
"diagnostic `{}` was not reported for `{}`\nstderr:\n{}",
expected_code,
name,
stderr
);
}
}
#[test]
fn beta2_file_resource_handles_execute_in_test_runner() {
let root = temp_root("resource-test-runner");
fs::create_dir_all(&root).unwrap_or_else(|err| panic!("create `{}`: {}", root.display(), err));
let existing = root.join("resource.txt");
let missing = root.join("missing-resource.txt");
fs::write(&existing, "resource text")
.unwrap_or_else(|err| panic!("write `{}`: {}", existing.display(), err));
let source = format!(
r#"
(module main)
(fn read_len_and_close ((path string)) -> i32
(match (std.fs.open_text_read_result path)
((ok handle)
(let text string (unwrap_ok (std.fs.read_open_text_result handle)))
(let close_status i32 (unwrap_ok (std.fs.close_result handle)))
(+ (std.string.len text) close_status))
((err code)
code)))
(fn read_after_close_err ((path string)) -> bool
(match (std.fs.open_text_read_result path)
((ok handle)
(std.fs.close_result handle)
(= (unwrap_err (std.fs.read_open_text_result handle)) 1))
((err code)
false)))
(test "resource handle open read close"
(= (read_len_and_close "{}") 13))
(test "resource handle missing open is err one"
(= (unwrap_err (std.fs.open_text_read_result "{}")) 1))
(test "resource handle closed read is err one"
(read_after_close_err "{}"))
(test "resource handle invalid close is err one"
(= (unwrap_err (std.fs.close_result -1)) 1))
"#,
slovo_path(&existing),
slovo_path(&missing),
slovo_path(&existing)
);
let fixture = write_fixture("resource-test-runner", &source);
let run = run_glagol([OsStr::new("test"), fixture.as_os_str()]);
assert_success_stdout(
run,
concat!(
"test \"resource handle open read close\" ... ok\n",
"test \"resource handle missing open is err one\" ... ok\n",
"test \"resource handle closed read is err one\" ... ok\n",
"test \"resource handle invalid close is err one\" ... ok\n",
"4 test(s) passed\n",
),
"beta.2 resource handle test-runner output",
);
}
#[test]
fn beta2_file_resource_handles_emit_expected_private_runtime_shape() {
let source = r#"
(module main)
(fn main () -> i32
(match (std.fs.open_text_read_result "missing.txt")
((ok handle)
(std.string.len (unwrap_ok (std.fs.read_open_text_result handle))))
((err code)
(unwrap_err (std.fs.close_result -1)))))
"#;
let fixture = write_fixture("resource-lowering", source);
let compile = run_glagol([fixture.as_os_str()]);
assert_success("compile beta.2 resource handle lowering", &compile);
let stdout = String::from_utf8_lossy(&compile.stdout);
assert!(
stdout.contains("__glagol_fs_open_text_read_result")
&& stdout.contains("__glagol_fs_read_open_text_result")
&& stdout.contains("__glagol_fs_close_result")
&& stdout.contains("declare i64 @__glagol_fs_open_text_read_result(ptr)")
&& stdout.contains("declare ptr @__glagol_fs_read_open_text_result(i32)")
&& stdout.contains("declare i32 @__glagol_fs_close_result(i32)")
&& !stdout.contains("@std.fs.open_text_read_result")
&& !stdout.contains("@std.fs.read_open_text_result")
&& !stdout.contains("@std.fs.close_result"),
"LLVM output did not contain expected beta.2 resource runtime shape\nstdout:\n{}",
stdout
);
}
#[test]
fn beta2_file_status_and_mutation_execute_in_test_runner() {
let root = temp_root("resource-status-test-runner");
fs::create_dir_all(&root).unwrap_or_else(|err| panic!("create `{}`: {}", root.display(), err));
let existing = root.join("existing.txt");
let removable = root.join("removable.txt");
let missing = root.join("missing.txt");
let created_dir = root.join("created-dir");
fs::write(&existing, "existing")
.unwrap_or_else(|err| panic!("write `{}`: {}", existing.display(), err));
fs::write(&removable, "remove me")
.unwrap_or_else(|err| panic!("write `{}`: {}", removable.display(), err));
let source = format!(
r#"
(module main)
(fn existing_file_status_ok ((path string)) -> bool
(if (std.fs.exists path)
(if (std.fs.is_file path)
(if (std.fs.is_dir path)
false
true)
false)
false))
(test "fs existing file status"
(existing_file_status_ok "{}"))
(test "fs missing status false"
(if (std.fs.exists "{}")
false
true))
(test "fs create dir result ok"
(= (unwrap_ok (std.fs.create_dir_result "{}")) 0))
(test "fs created path is dir"
(std.fs.is_dir "{}"))
(test "fs create existing dir err one"
(= (unwrap_err (std.fs.create_dir_result "{}")) 1))
(test "fs remove file result ok"
(= (unwrap_ok (std.fs.remove_file_result "{}")) 0))
(test "fs removed file no longer exists"
(if (std.fs.exists "{}")
false
true))
(test "fs remove missing file err one"
(= (unwrap_err (std.fs.remove_file_result "{}")) 1))
"#,
slovo_path(&existing),
slovo_path(&missing),
slovo_path(&created_dir),
slovo_path(&created_dir),
slovo_path(&created_dir),
slovo_path(&removable),
slovo_path(&removable),
slovo_path(&missing)
);
let fixture = write_fixture("resource-status-test-runner", &source);
let run = run_glagol([OsStr::new("test"), fixture.as_os_str()]);
assert_success_stdout(
run,
concat!(
"test \"fs existing file status\" ... ok\n",
"test \"fs missing status false\" ... ok\n",
"test \"fs create dir result ok\" ... ok\n",
"test \"fs created path is dir\" ... ok\n",
"test \"fs create existing dir err one\" ... ok\n",
"test \"fs remove file result ok\" ... ok\n",
"test \"fs removed file no longer exists\" ... ok\n",
"test \"fs remove missing file err one\" ... ok\n",
"8 test(s) passed\n",
),
"beta.2 fs status/mutation test-runner output",
);
}
#[test]
fn beta2_file_status_and_mutation_emit_expected_private_runtime_shape() {
let source = r#"
(module main)
(fn main () -> i32
(if (std.fs.exists "existing.txt")
(if (std.fs.is_file "existing.txt")
(if (std.fs.is_dir "existing.txt")
1
(unwrap_ok (std.fs.remove_file_result "existing.txt")))
2)
(unwrap_err (std.fs.create_dir_result "created-dir"))))
"#;
let fixture = write_fixture("resource-status-lowering", source);
let compile = run_glagol([fixture.as_os_str()]);
assert_success("compile beta.2 fs status/mutation lowering", &compile);
let stdout = String::from_utf8_lossy(&compile.stdout);
assert!(
stdout.contains("call i1 @__glagol_fs_exists")
&& stdout.contains("call i1 @__glagol_fs_is_file")
&& stdout.contains("call i1 @__glagol_fs_is_dir")
&& stdout.contains("call i32 @__glagol_fs_remove_file_result")
&& stdout.contains("call i32 @__glagol_fs_create_dir_result")
&& !stdout.contains("@std.fs.exists")
&& !stdout.contains("@std.fs.is_file")
&& !stdout.contains("@std.fs.is_dir")
&& !stdout.contains("@std.fs.remove_file_result")
&& !stdout.contains("@std.fs.create_dir_result"),
"LLVM output did not contain expected beta.2 fs status/mutation runtime shape\nstdout:\n{}",
stdout
);
}
#[test]
fn hosted_runtime_executes_beta2_file_resource_handles_when_clang_is_available() {
let Some(clang) = find_clang() else {
eprintln!("skipping beta.2 resource runtime smoke: set GLAGOL_CLANG or install clang");
return;
};
let root = temp_root("resource-native");
fs::create_dir_all(&root).unwrap_or_else(|err| panic!("create `{}`: {}", root.display(), err));
let existing = root.join("native-resource.txt");
fs::write(&existing, "native handle")
.unwrap_or_else(|err| panic!("write `{}`: {}", existing.display(), err));
let source = format!(
r#"
(module main)
(fn main () -> i32
(match (std.fs.open_text_read_result "{}")
((ok handle)
(let text string (unwrap_ok (std.fs.read_open_text_result handle)))
(let close_status i32 (unwrap_ok (std.fs.close_result handle)))
(+ (std.string.len text) close_status))
((err code)
code)))
"#,
slovo_path(&existing)
);
let fixture = write_fixture("resource-native", &source);
let compile = run_glagol([fixture.as_os_str()]);
assert_success("compile beta.2 resource native smoke", &compile);
let run =
compile_and_run_with_runtime(&clang, "beta2-resource-native", &compile.stdout, |_| {});
assert_eq!(
run.status.code(),
Some(13),
"beta.2 resource native smoke exit code drifted\nstdout:\n{}\nstderr:\n{}",
String::from_utf8_lossy(&run.stdout),
String::from_utf8_lossy(&run.stderr)
);
assert!(
run.stdout.is_empty(),
"beta.2 resource native smoke wrote stdout:\n{}",
String::from_utf8_lossy(&run.stdout)
);
assert!(
run.stderr.is_empty(),
"beta.2 resource native smoke wrote stderr:\n{}",
String::from_utf8_lossy(&run.stderr)
);
}
#[test]
fn hosted_runtime_executes_beta2_file_status_and_mutation_when_clang_is_available() {
let Some(clang) = find_clang() else {
eprintln!(
"skipping beta.2 fs status/mutation runtime smoke: set GLAGOL_CLANG or install clang"
);
return;
};
let root = temp_root("resource-status-native");
fs::create_dir_all(&root).unwrap_or_else(|err| panic!("create `{}`: {}", root.display(), err));
let existing = root.join("native-existing.txt");
let removable = root.join("native-removable.txt");
let created_dir = root.join("native-created-dir");
fs::write(&existing, "native existing")
.unwrap_or_else(|err| panic!("write `{}`: {}", existing.display(), err));
fs::write(&removable, "native removable")
.unwrap_or_else(|err| panic!("write `{}`: {}", removable.display(), err));
let source = format!(
r#"
(module main)
(fn main () -> i32
(if (std.fs.exists "{}")
(if (std.fs.is_file "{}")
(if (std.fs.is_dir "{}")
1
(if (= (unwrap_ok (std.fs.create_dir_result "{}")) 0)
(if (std.fs.is_dir "{}")
(if (= (unwrap_ok (std.fs.remove_file_result "{}")) 0)
(if (std.fs.exists "{}")
2
0)
3)
4)
5))
6)
7))
"#,
slovo_path(&existing),
slovo_path(&existing),
slovo_path(&existing),
slovo_path(&created_dir),
slovo_path(&created_dir),
slovo_path(&removable),
slovo_path(&removable)
);
let fixture = write_fixture("resource-status-native", &source);
let compile = run_glagol([fixture.as_os_str()]);
assert_success("compile beta.2 fs status/mutation native smoke", &compile);
let run = compile_and_run_with_runtime(
&clang,
"beta2-resource-status-native",
&compile.stdout,
|_| {},
);
assert_eq!(
run.status.code(),
Some(0),
"beta.2 fs status/mutation native smoke exit code drifted\nstdout:\n{}\nstderr:\n{}",
String::from_utf8_lossy(&run.stdout),
String::from_utf8_lossy(&run.stderr)
);
assert!(
run.stdout.is_empty(),
"beta.2 fs status/mutation native smoke wrote stdout:\n{}",
String::from_utf8_lossy(&run.stdout)
);
assert!(
run.stderr.is_empty(),
"beta.2 fs status/mutation native smoke wrote stderr:\n{}",
String::from_utf8_lossy(&run.stderr)
);
}
#[test]
fn hosted_runtime_executes_exp10_results_when_clang_is_available() {
let Some(clang) = find_clang() else {
eprintln!("skipping exp-10 runtime smoke: set GLAGOL_CLANG or install clang");
return;
};
let root = temp_root("native");
fs::create_dir_all(&root).unwrap_or_else(|err| panic!("create `{}`: {}", root.display(), err));
let output = root.join("native.txt");
let missing = root.join("missing.txt");
let source = format!(
r#"
(module main)
(fn write_status () -> i32
(match (std.fs.write_text_result "{}" "native text")
((ok code)
code)
((err code)
code)))
(fn main () -> i32
(std.io.print_string (unwrap_ok (std.env.get_result "GLAGOL_EXP10_NATIVE_PRESENT")))
(std.io.print_string (unwrap_ok (std.process.arg_result 1)))
(std.io.print_i32 (write_status))
(std.io.print_string (unwrap_ok (std.fs.read_text_result "{}")))
(unwrap_err (std.fs.read_text_result "{}")))
"#,
slovo_path(&output),
slovo_path(&output),
slovo_path(&missing)
);
let fixture = write_fixture("native", &source);
let compile = run_glagol([fixture.as_os_str()]);
assert_success("compile exp-10 native smoke", &compile);
let run = compile_and_run_with_runtime(&clang, "exp10-native", &compile.stdout, |command| {
command
.arg("argv-native")
.env("GLAGOL_EXP10_NATIVE_PRESENT", "env-native");
});
assert_eq!(
run.status.code(),
Some(1),
"exp-10 native smoke exit code drifted\nstdout:\n{}\nstderr:\n{}",
String::from_utf8_lossy(&run.stdout),
String::from_utf8_lossy(&run.stderr)
);
assert_eq!(
String::from_utf8_lossy(&run.stdout),
"env-native\nargv-native\n0\nnative text\n",
"exp-10 native smoke stdout drifted"
);
assert!(
run.stderr.is_empty(),
"exp-10 native smoke wrote stderr:\n{}",
String::from_utf8_lossy(&run.stderr)
);
}
#[test]
fn hosted_runtime_executes_exp12_stdin_result_when_clang_is_available() {
let Some(clang) = find_clang() else {
eprintln!("skipping exp-12 stdin runtime smoke: set GLAGOL_CLANG or install clang");
return;
};
let source = r#"
(module main)
(fn stdin_len_or_code () -> i32
(match (std.io.read_stdin_result)
((ok text)
(std.string.len text))
((err code)
code)))
(fn main () -> i32
(stdin_len_or_code))
"#;
let fixture = write_fixture("stdin-native", source);
let compile = run_glagol([fixture.as_os_str()]);
assert_success("compile exp-12 stdin native smoke", &compile);
let ir = String::from_utf8_lossy(&compile.stdout);
assert!(
ir.contains("declare ptr @__glagol_io_read_stdin_result()")
&& ir.contains("call ptr @__glagol_io_read_stdin_result()")
&& !ir.contains("@std.io.read_stdin_result"),
"LLVM output did not contain expected exp-12 stdin runtime shape\nstdout:\n{}",
ir
);
let run = compile_and_run_with_runtime_input(
&clang,
"exp12-stdin-native",
&compile.stdout,
b"native stdin",
|_| {},
);
assert_eq!(
run.status.code(),
Some(12),
"exp-12 stdin native smoke exit code drifted\nstdout:\n{}\nstderr:\n{}",
String::from_utf8_lossy(&run.stdout),
String::from_utf8_lossy(&run.stderr)
);
assert!(
run.stdout.is_empty(),
"exp-12 stdin native smoke wrote stdout:\n{}",
String::from_utf8_lossy(&run.stdout)
);
assert!(
run.stderr.is_empty(),
"exp-12 stdin native smoke wrote stderr:\n{}",
String::from_utf8_lossy(&run.stderr)
);
let eof =
compile_and_run_with_runtime_input(&clang, "exp12-stdin-eof", &compile.stdout, b"", |_| {});
assert_eq!(
eof.status.code(),
Some(0),
"exp-12 stdin EOF should be ok empty string\nstdout:\n{}\nstderr:\n{}",
String::from_utf8_lossy(&eof.stdout),
String::from_utf8_lossy(&eof.stderr)
);
}
#[test]
fn hosted_runtime_keeps_exp3_traps_when_clang_is_available() {
let Some(clang) = find_clang() else {
eprintln!("skipping exp-3 runtime regression: set GLAGOL_CLANG or install clang");
return;
};
let source = r#"
(module main)
(fn main () -> i32
(std.string.len (std.process.arg 99)))
"#;
let fixture = write_fixture("exp3-native-trap", source);
let compile = run_glagol([fixture.as_os_str()]);
assert_success("compile exp-3 native trap regression", &compile);
let run = compile_and_run_with_runtime(&clang, "exp3-native-trap", &compile.stdout, |_| {});
assert_eq!(
run.status.code(),
Some(1),
"exp-3 native trap exit code drifted\nstdout:\n{}\nstderr:\n{}",
String::from_utf8_lossy(&run.stdout),
String::from_utf8_lossy(&run.stderr)
);
assert_eq!(
String::from_utf8_lossy(&run.stderr),
"slovo runtime error: process argument index out of bounds\n",
"exp-3 native trap stderr drifted"
);
}
fn run_glagol<I, S>(args: I) -> Output
where
I: IntoIterator<Item = S>,
S: AsRef<OsStr>,
{
run_glagol_configured(args, |_| {})
}
fn run_glagol_configured<I, S, F>(args: I, configure: F) -> Output
where
I: IntoIterator<Item = S>,
S: AsRef<OsStr>,
F: FnOnce(&mut Command),
{
let mut command = Command::new(env!("CARGO_BIN_EXE_glagol"));
command
.args(args)
.current_dir(Path::new(env!("CARGO_MANIFEST_DIR")));
configure(&mut command);
command.output().expect("run glagol")
}
fn write_fixture(name: &str, source: &str) -> PathBuf {
let mut path = env::temp_dir();
path.push(format!(
"glagol-exp10-host-result-{}-{}-{}.slo",
name,
std::process::id(),
NEXT_FIXTURE_ID.fetch_add(1, Ordering::Relaxed)
));
fs::write(&path, source).unwrap_or_else(|err| panic!("write `{}`: {}", path.display(), err));
path
}
fn temp_root(name: &str) -> PathBuf {
let nanos = SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("system clock before Unix epoch")
.as_nanos();
env::temp_dir().join(format!(
"glagol-exp10-host-result-{}-{}-{}-{}",
name,
std::process::id(),
NEXT_FIXTURE_ID.fetch_add(1, Ordering::Relaxed),
nanos
))
}
fn slovo_path(path: &Path) -> String {
path.to_string_lossy()
.replace('\\', "\\\\")
.replace('"', "\\\"")
}
fn assert_success(context: &str, output: &Output) {
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!(stderr.is_empty(), "{} wrote stderr:\n{}", context, stderr);
}
fn assert_success_stdout(output: 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, "{} stdout drifted", context);
assert!(stderr.is_empty(), "{} wrote stderr:\n{}", context, stderr);
}
fn compile_and_run_with_runtime<F>(clang: &Path, name: &str, ir: &[u8], configure: F) -> Output
where
F: FnOnce(&mut Command),
{
let manifest = Path::new(env!("CARGO_MANIFEST_DIR"));
let temp_dir = temp_root("clang");
fs::create_dir_all(&temp_dir)
.unwrap_or_else(|err| panic!("create `{}`: {}", temp_dir.display(), err));
let ir_path = temp_dir.join(format!("{}.ll", name));
let exe_path = temp_dir.join(name);
fs::write(&ir_path, ir).unwrap_or_else(|err| panic!("write `{}`: {}", ir_path.display(), err));
let runtime = manifest.join("../runtime/runtime.c");
let mut clang_command = Command::new(clang);
clang_command
.arg(&runtime)
.arg(&ir_path)
.arg("-o")
.arg(&exe_path)
.current_dir(manifest);
configure_clang_runtime_env(&mut clang_command, clang);
let clang_output = clang_command
.output()
.unwrap_or_else(|err| panic!("run `{}`: {}", clang.display(), err));
assert_status_success("clang exp-10 runtime smoke", &clang_output);
let mut run = Command::new(&exe_path);
configure(&mut run);
run.output()
.unwrap_or_else(|err| panic!("run `{}`: {}", exe_path.display(), err))
}
fn compile_and_run_with_runtime_input<F>(
clang: &Path,
name: &str,
ir: &[u8],
input: &[u8],
configure: F,
) -> Output
where
F: FnOnce(&mut Command),
{
let manifest = Path::new(env!("CARGO_MANIFEST_DIR"));
let temp_dir = temp_root("clang");
fs::create_dir_all(&temp_dir)
.unwrap_or_else(|err| panic!("create `{}`: {}", temp_dir.display(), err));
let ir_path = temp_dir.join(format!("{}.ll", name));
let exe_path = temp_dir.join(name);
fs::write(&ir_path, ir).unwrap_or_else(|err| panic!("write `{}`: {}", ir_path.display(), err));
let runtime = manifest.join("../runtime/runtime.c");
let mut clang_command = Command::new(clang);
clang_command
.arg(&runtime)
.arg(&ir_path)
.arg("-o")
.arg(&exe_path)
.current_dir(manifest);
configure_clang_runtime_env(&mut clang_command, clang);
let clang_output = clang_command
.output()
.unwrap_or_else(|err| panic!("run `{}`: {}", clang.display(), err));
assert_status_success("clang exp-12 runtime smoke", &clang_output);
let mut run = Command::new(&exe_path);
run.stdin(Stdio::piped());
configure(&mut run);
let mut child = run
.spawn()
.unwrap_or_else(|err| panic!("run `{}`: {}", exe_path.display(), err));
let mut stdin = child
.stdin
.take()
.unwrap_or_else(|| panic!("open stdin for `{}`", exe_path.display()));
stdin
.write_all(input)
.unwrap_or_else(|err| panic!("write stdin to `{}`: {}", exe_path.display(), err));
drop(stdin);
child
.wait_with_output()
.unwrap_or_else(|err| panic!("wait for `{}`: {}", exe_path.display(), err))
}
fn find_clang() -> Option<PathBuf> {
if let Some(path) = env::var_os("GLAGOL_CLANG").filter(|value| !value.is_empty()) {
return Some(PathBuf::from(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 assert_status_success(context: &str, output: &Output) {
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
);
}
fn find_on_path(program: &str) -> Option<PathBuf> {
let path = env::var_os("PATH")?;
env::split_paths(&path)
.map(|dir| dir.join(program))
.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);
}