use std::{ env, ffi::OsStr, fs, path::{Path, PathBuf}, process::{Command, Output}, sync::atomic::{AtomicUsize, Ordering}, }; static NEXT_FIXTURE_ID: AtomicUsize = AtomicUsize::new(0); #[test] fn print_string_literal_emits_static_global_and_call() { let fixture = write_fixture( "print-string-literal", r#" (module main) (fn main () -> i32 (print_string "hello") 0) "#, ); 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 rejected print_string literal fixture\nstdout:\n{}\nstderr:\n{}", stdout, stderr ); assert!( stdout.contains("declare void @print_string(ptr)") && stdout.contains( "@.str.0 = private unnamed_addr constant [6 x i8] c\"hello\\00\", align 1" ) && stdout.contains("call void @print_string(ptr @.str.0)"), "LLVM output did not contain expected string runtime shape\nstdout:\n{}", stdout ); assert!(stderr.is_empty(), "compiler wrote stderr:\n{}", stderr); } #[test] fn print_string_literal_escapes_llvm_global_bytes() { let fixture = write_fixture( "print-string-escapes", r#" (module main) (fn main () -> i32 (print_string "line\nquote\"slash\\tab\t") 0) "#, ); 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 rejected escaped print_string literal fixture\nstdout:\n{}\nstderr:\n{}", stdout, stderr ); assert!( stdout.contains( "@.str.0 = private unnamed_addr constant [22 x i8] c\"line\\0Aquote\\22slash\\5Ctab\\09\\00\", align 1" ), "LLVM string global did not preserve expected escapes\nstdout:\n{}", stdout ); assert!(stderr.is_empty(), "compiler wrote stderr:\n{}", stderr); } #[test] fn promoted_string_print_fixture_emits_static_globals_and_calls() { let fixture = Path::new(env!("CARGO_MANIFEST_DIR")).join("../examples/string-print.slo"); 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 rejected promoted string-print fixture\nstdout:\n{}\nstderr:\n{}", stdout, stderr ); assert!( stdout.contains("@.str.0 = private unnamed_addr constant [6 x i8] c\"hello\\00\", align 1") && stdout.contains( "@.str.1 = private unnamed_addr constant [22 x i8] c\"line\\0Aquote\\22slash\\5Ctab\\09\\00\", align 1" ) && stdout.contains("call void @print_string(ptr @.str.0)") && stdout.contains("call void @print_string(ptr @.str.1)"), "promoted fixture LLVM output did not contain expected string globals and calls\nstdout:\n{}", stdout ); assert!(stderr.is_empty(), "compiler wrote stderr:\n{}", stderr); } #[test] fn print_string_requires_exactly_one_string_argument() { let cases = [ ( "print-string-wrong-type", r#" (module main) (fn main () -> i32 (print_string 1) 0) "#, "TypeMismatch", ), ( "print-string-wrong-arity", r#" (module main) (fn main () -> i32 (print_string "a" "b") 0) "#, "ArityMismatch", ), ]; for (name, source, diagnostic) 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 invalid print_string call `{}`\nstdout:\n{}\nstderr:\n{}", name, stdout, stderr ); assert!( stderr.contains(diagnostic), "diagnostic `{}` was not reported for `{}`\nstderr:\n{}", diagnostic, name, stderr ); assert!( stdout.is_empty(), "rejected compile wrote stdout:\n{}", stdout ); } } #[test] fn print_string_literal_rejects_non_ascii_payloads() { let source = format!( r#" (module main) (fn main () -> i32 (print_string "zdravo {}") 0) "#, '\u{017E}' ); let fixture = write_fixture("print-string-non-ascii", &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 non-ASCII print_string literal\nstdout:\n{}\nstderr:\n{}", stdout, stderr ); assert!( stderr.contains("UnsupportedStringLiteral") && stderr.contains("printable ASCII string literal"), "non-ASCII string literal did not report the expected diagnostic\nstderr:\n{}", stderr ); assert!( stdout.is_empty(), "rejected compile wrote stdout:\n{}", stdout ); } #[test] fn print_string_rejects_unsupported_escape_spellings() { for escape in ["\\0", "\\r", "\\x"] { let source = format!( r#" (module main) (fn main () -> i32 (print_string "{}") 0) "#, escape ); let fixture = write_fixture("print-string-unsupported-escape", &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 unsupported escape `{}`\nstdout:\n{}\nstderr:\n{}", escape, stdout, stderr ); assert!( stderr.contains("UnsupportedStringEscape"), "unsupported escape `{}` did not report the expected diagnostic\nstderr:\n{}", escape, stderr ); assert!( stdout.is_empty(), "rejected compile wrote stdout:\n{}", stdout ); } } #[test] fn formatter_rejects_strings_outside_promoted_domain() { let non_ascii_source = format!( r#" (module main) (fn main () -> i32 (print_string "zdravo {}") 0) "#, '\u{017E}' ); let cases: [(&str, String); 2] = [ ("format-non-ascii", non_ascii_source), ( "format-unsupported-escape", r#" (module main) (fn main () -> i32 (print_string "\0") 0) "# .to_string(), ), ]; for (name, source) in cases { let fixture = write_fixture(name, &source); let output = run_glagol([OsStr::new("--format"), fixture.as_os_str()]); let stdout = String::from_utf8_lossy(&output.stdout); let stderr = String::from_utf8_lossy(&output.stderr); assert!( !output.status.success(), "formatter unexpectedly accepted `{}`\nstdout:\n{}\nstderr:\n{}", name, stdout, stderr ); assert!( stderr.contains("UnsupportedString"), "formatter rejection for `{}` did not report a string-domain diagnostic\nstderr:\n{}", name, stderr ); assert!( stdout.is_empty(), "rejected format wrote stdout:\n{}", stdout ); } } #[test] fn string_values_flow_through_returns_locals_calls_and_print_string() { let fixture = write_fixture( "string-value-flow", r#" (module main) (fn label () -> string "hello") (fn echo ((value string)) -> string value) (fn main () -> i32 (let value string (echo (label))) (print_string value) (string_len value)) "#, ); 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 rejected string value-flow fixture\nstdout:\n{}\nstderr:\n{}", stdout, stderr ); assert!( stdout.contains("define ptr @label()") && stdout.contains("define ptr @echo(ptr %value)") && stdout.contains("%value.addr = alloca ptr") && stdout.contains("call void @print_string(ptr %") && stdout.contains("call i32 @string_len(ptr %"), "LLVM output did not contain expected string value-flow shape\nstdout:\n{}", stdout ); assert!(stderr.is_empty(), "compiler wrote stderr:\n{}", stderr); } #[test] fn standard_string_concat_emits_runtime_call_and_flows_as_string() { let fixture = write_fixture( "std-string-concat", r#" (module main) (fn hello () -> string (std.string.concat "hel" "lo")) (fn shout ((value string)) -> string (std.string.concat value "!")) (fn main () -> i32 (std.io.print_string (shout (hello))) (std.string.len (shout (hello)))) (test "std concat equality" (= (std.string.concat "slo" "vo") "slovo")) (test "std concat length" (= (std.string.len (std.string.concat "a" "bc")) 3)) "#, ); 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 rejected std string concat fixture\nstdout:\n{}\nstderr:\n{}", stdout, stderr ); assert!( stdout.contains("declare ptr @__glagol_string_concat(ptr, ptr)") && stdout.contains("call ptr @__glagol_string_concat(ptr @.str.") && stdout.contains("call void @print_string(ptr %") && stdout.contains("call i32 @string_len(ptr %") && !stdout.contains("@std."), "LLVM output did not contain expected concat runtime shape\nstdout:\n{}", stdout ); assert!(stderr.is_empty(), "compiler wrote stderr:\n{}", stderr); let tests = run_glagol([OsStr::new("test"), fixture.as_os_str()]); assert_success("run std string concat tests", &tests); assert_eq!( String::from_utf8_lossy(&tests.stdout), concat!( "test \"std concat equality\" ... ok\n", "test \"std concat length\" ... ok\n", "2 test(s) passed\n", ), "concat test runner stdout drifted" ); } #[test] fn standard_string_concat_is_formatter_and_lowering_inspector_visible() { let fixture = write_fixture( "std-string-concat-tooling", r#" (module main) (fn joined () -> string (std.string.concat (std.string.concat "slo" "v") "o")) (fn main () -> i32 (std.string.len (joined))) "#, ); let formatted = run_glagol([OsStr::new("--format"), fixture.as_os_str()]); assert_success("format std string concat", &formatted); assert_eq!( String::from_utf8_lossy(&formatted.stdout), concat!( "(module main)\n", "\n", "(fn joined () -> string\n", " (std.string.concat (std.string.concat \"slo\" \"v\") \"o\"))\n", "\n", "(fn main () -> i32\n", " (std.string.len (joined)))\n", ), "formatter output drifted for std string concat" ); let surface = run_glagol([ OsStr::new("--inspect-lowering=surface"), fixture.as_os_str(), ]); assert_success("inspect std string concat surface lowering", &surface); let surface_stdout = String::from_utf8_lossy(&surface.stdout); assert!( surface_stdout.matches("call std.string.concat").count() == 2, "surface lowering did not show concat calls\nstdout:\n{}", surface_stdout ); let checked = run_glagol([ OsStr::new("--inspect-lowering=checked"), fixture.as_os_str(), ]); assert_success("inspect std string concat checked lowering", &checked); let checked_stdout = String::from_utf8_lossy(&checked.stdout); assert!( checked_stdout .matches("call std.string.concat : string") .count() == 2, "checked lowering did not show typed concat calls\nstdout:\n{}", checked_stdout ); } #[test] fn standard_string_concat_keeps_plus_deferred_rejects_alias_and_checks_calls() { let cases = [ ( "std-string-concat-left-type", r#" (module main) (fn main () -> string (std.string.concat 1 "a")) "#, "TypeMismatch", ), ( "std-string-concat-arity", r#" (module main) (fn main () -> string (std.string.concat "a")) "#, "ArityMismatch", ), ( "legacy-string-concat-rejected", r#" (module main) (fn main () -> string (string_concat "a" "b")) "#, "UnknownFunction", ), ( "private-runtime-helper-shadow-rejected", r#" (module main) (fn __glagol_string_concat ((left string) (right string)) -> string "intercepted") (fn main () -> string (std.string.concat "a" "b")) "#, "DuplicateFunction", ), ( "plus-string-concat-still-deferred", r#" (module main) (fn main () -> string (+ "a" "b")) "#, "UnsupportedStringConcatenation", ), ]; for (name, source, diagnostic) 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 invalid concat case `{}`\nstdout:\n{}\nstderr:\n{}", name, stdout, stderr ); assert!( stderr.contains(diagnostic), "diagnostic `{}` was not reported for `{}`\nstderr:\n{}", diagnostic, name, stderr ); assert!( stdout.is_empty(), "rejected compile wrote stdout:\n{}", stdout ); } } #[test] fn standard_string_parse_i32_result_emits_i64_runtime_decode_shape() { let fixture = write_fixture( "std-string-parse-i32-result-shape", r#" (module main) (fn parse_or_code ((text string)) -> i32 (match (std.string.parse_i32_result text) ((ok value) value) ((err code) code))) (fn main () -> i32 (parse_or_code "42")) "#, ); 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 rejected std string parse_i32_result fixture\nstdout:\n{}\nstderr:\n{}", stdout, stderr ); assert!( stdout.contains("declare i64 @__glagol_string_parse_i32_result(ptr)") && stdout.contains("call i64 @__glagol_string_parse_i32_result(ptr %text)") && stdout.contains("lshr i64 %") && stdout.contains("trunc i64 %") && stdout.contains("insertvalue { i1, i32 }") && stdout.contains("extractvalue { i1, i32 }") && !stdout.contains("@std.string.parse_i32_result"), "LLVM output did not contain expected parse_i32_result decode shape\nstdout:\n{}", stdout ); assert!(stderr.is_empty(), "compiler wrote stderr:\n{}", stderr); } #[test] fn test_runner_executes_standard_string_parse_i32_result_boundaries() { let non_ascii_digit = { let path = env::temp_dir().join(format!( "glagol-string-parse-non-ascii-digit-{}-{}.txt", std::process::id(), NEXT_FIXTURE_ID.fetch_add(1, Ordering::Relaxed) )); fs::write(&path, "\u{0661}") .unwrap_or_else(|err| panic!("write `{}`: {}", path.display(), err)); path }; let source = format!( r#" (module main) (test "parse ok positive" (= (unwrap_ok (std.string.parse_i32_result "42")) 42)) (test "parse ok negative" (= (unwrap_ok (std.string.parse_i32_result "-7")) -7)) (test "parse ok min i32" (= (unwrap_ok (std.string.parse_i32_result "-2147483648")) -2147483648)) (test "parse ok max i32" (= (unwrap_ok (std.string.parse_i32_result "2147483647")) 2147483647)) (test "parse empty err one" (= (unwrap_err (std.string.parse_i32_result "")) 1)) (test "parse lone minus err one" (= (unwrap_err (std.string.parse_i32_result "-")) 1)) (test "parse plus sign err one" (= (unwrap_err (std.string.parse_i32_result "+1")) 1)) (test "parse leading whitespace err one" (= (unwrap_err (std.string.parse_i32_result " 1")) 1)) (test "parse trailing byte err one" (= (unwrap_err (std.string.parse_i32_result "42\n")) 1)) (test "parse embedded nondigit err one" (= (unwrap_err (std.string.parse_i32_result "4x")) 1)) (test "parse underscore err one" (= (unwrap_err (std.string.parse_i32_result "1_0")) 1)) (test "parse prefix err one" (= (unwrap_err (std.string.parse_i32_result "0x10")) 1)) (test "parse overflow err one" (= (unwrap_err (std.string.parse_i32_result "2147483648")) 1)) (test "parse underflow err one" (= (unwrap_err (std.string.parse_i32_result "-2147483649")) 1)) (test "parse non ascii digit err one" (= (unwrap_err (std.string.parse_i32_result (unwrap_ok (std.fs.read_text_result "{}")))) 1)) "#, slovo_path(&non_ascii_digit) ); let fixture = write_fixture("std-string-parse-i32-result-test-runner", &source); let output = run_glagol([OsStr::new("test"), fixture.as_os_str()]); assert_success("run std string parse_i32_result tests", &output); assert_eq!( String::from_utf8_lossy(&output.stdout), concat!( "test \"parse ok positive\" ... ok\n", "test \"parse ok negative\" ... ok\n", "test \"parse ok min i32\" ... ok\n", "test \"parse ok max i32\" ... ok\n", "test \"parse empty err one\" ... ok\n", "test \"parse lone minus err one\" ... ok\n", "test \"parse plus sign err one\" ... ok\n", "test \"parse leading whitespace err one\" ... ok\n", "test \"parse trailing byte err one\" ... ok\n", "test \"parse embedded nondigit err one\" ... ok\n", "test \"parse underscore err one\" ... ok\n", "test \"parse prefix err one\" ... ok\n", "test \"parse overflow err one\" ... ok\n", "test \"parse underflow err one\" ... ok\n", "test \"parse non ascii digit err one\" ... ok\n", "15 test(s) passed\n", ), "parse_i32_result test runner stdout drifted" ); } #[test] fn standard_string_parse_i32_result_runtime_smoke_when_clang_is_available() { let Some(clang) = find_clang() else { eprintln!( "skipping std string parse_i32_result runtime smoke: set GLAGOL_CLANG or install clang" ); return; }; let fixture = write_fixture( "std-string-parse-i32-result-runtime-smoke", r#" (module main) (fn main () -> i32 (std.io.print_i32 (unwrap_ok (std.string.parse_i32_result "42"))) (std.io.print_i32 (unwrap_ok (std.string.parse_i32_result "-7"))) (unwrap_err (std.string.parse_i32_result "42x"))) "#, ); let compile = run_glagol([fixture.as_os_str()]); assert_success( "compile std string parse_i32_result runtime smoke", &compile, ); let run = compile_and_run_with_runtime(&clang, "std-string-parse-i32-result", &compile.stdout); assert_eq!( run.status.code(), Some(1), "std string parse_i32_result runtime 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), "42\n-7\n", "std string parse_i32_result runtime smoke stdout drifted" ); assert!( run.stderr.is_empty(), "std string parse_i32_result runtime smoke wrote stderr:\n{}", String::from_utf8_lossy(&run.stderr) ); } #[test] fn print_string_runtime_smoke_when_clang_is_available() { let Some(clang) = find_clang() else { eprintln!("skipping string runtime smoke: set GLAGOL_CLANG or install clang"); return; }; let fixture = write_fixture( "print-string-runtime-smoke", r#" (module main) (fn main () -> i32 (print_string "hello from glagol") 0) "#, ); let compile = run_glagol([fixture.as_os_str()]); assert_success("compile string runtime smoke", &compile); let manifest = Path::new(env!("CARGO_MANIFEST_DIR")); let temp_dir = env::temp_dir().join(format!( "glagol-string-runtime-{}-{}", std::process::id(), NEXT_FIXTURE_ID.fetch_add(1, Ordering::Relaxed) )); fs::create_dir_all(&temp_dir) .unwrap_or_else(|err| panic!("create `{}`: {}", temp_dir.display(), err)); let ir_path = temp_dir.join("string-runtime.ll"); let exe_path = temp_dir.join("string-runtime"); fs::write(&ir_path, &compile.stdout) .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_success("clang string runtime smoke", &clang_output); let run = Command::new(&exe_path) .output() .unwrap_or_else(|err| panic!("run `{}`: {}", exe_path.display(), err)); assert_success("run string runtime smoke", &run); assert_eq!( String::from_utf8_lossy(&run.stdout), "hello from glagol\n", "runtime stdout drifted" ); } #[test] fn promoted_string_print_fixture_runtime_smoke_when_clang_is_available() { let Some(clang) = find_clang() else { eprintln!( "skipping promoted string fixture runtime smoke: set GLAGOL_CLANG or install clang" ); return; }; let fixture = Path::new(env!("CARGO_MANIFEST_DIR")).join("../examples/string-print.slo"); let compile = run_glagol([fixture.as_os_str()]); assert_success("compile promoted string-print fixture", &compile); let run = compile_and_run_with_runtime(&clang, "promoted-string-print", &compile.stdout); assert_success("run promoted string-print fixture", &run); assert_eq!( String::from_utf8_lossy(&run.stdout), "hello\nline\nquote\"slash\\tab\t\n", "promoted fixture runtime stdout drifted" ); } #[test] fn v1_2_string_and_bool_runtime_smoke_when_clang_is_available() { let Some(clang) = find_clang() else { eprintln!("skipping v1.2 string/bool runtime smoke: set GLAGOL_CLANG or install clang"); return; }; let fixture = write_fixture( "v1-2-string-bool-runtime-smoke", r#" (module main) (fn label () -> string "slovo") (fn main () -> i32 (print_bool (= (label) "slovo")) (print_bool (= (label) "other")) (print_i32 (string_len (label))) 0) "#, ); let compile = run_glagol([fixture.as_os_str()]); assert_success("compile v1.2 string/bool runtime smoke", &compile); let run = compile_and_run_with_runtime(&clang, "v1-2-string-bool", &compile.stdout); assert_success("run v1.2 string/bool runtime smoke", &run); assert_eq!( String::from_utf8_lossy(&run.stdout), "true\nfalse\n5\n", "v1.2 string/bool runtime stdout drifted" ); } #[test] fn standard_runtime_fixture_uses_legacy_llvm_symbols_and_tests_pass() { let fixture = Path::new(env!("CARGO_MANIFEST_DIR")).join("../examples/standard-runtime.slo"); let compile = run_glagol([fixture.as_os_str()]); let stdout = String::from_utf8_lossy(&compile.stdout); let stderr = String::from_utf8_lossy(&compile.stderr); assert!( compile.status.success(), "compile standard-runtime fixture failed\nstdout:\n{}\nstderr:\n{}", stdout, stderr ); assert!( stdout.contains("call void @print_string(ptr %") && stdout.contains("call void @print_bool(i1 %") && stdout.contains("call void @print_i32(i32 %") && stdout.contains("call i32 @string_len(ptr ") && !stdout.contains("@std."), "standard-runtime LLVM did not lower through legacy symbols\nstdout:\n{}", stdout ); assert!( stderr.is_empty(), "standard-runtime compile wrote stderr:\n{}", stderr ); let run = run_glagol([OsStr::new("--run-tests"), fixture.as_os_str()]); assert_success("run standard-runtime tests", &run); assert_eq!( String::from_utf8_lossy(&run.stdout), concat!( "test \"std string equality\" ... ok\n", "test \"std string byte length\" ... ok\n", "2 test(s) passed\n", ), "standard-runtime test runner stdout drifted" ); } #[test] fn standard_runtime_print_calls_are_rejected_by_test_runner() { let cases = [ ("std-print-i32-rejected", "(std.io.print_i32 1)"), ( "std-print-string-rejected", "(std.io.print_string \"test\")", ), ("std-print-bool-rejected", "(std.io.print_bool true)"), ]; for (name, print_call) in cases { let source = format!( r#" (module main) (fn noisy () -> i32 {} 1) (test "std print rejected" (= (noisy) 1)) "#, print_call ); let fixture = write_fixture(name, &source); let output = run_glagol([OsStr::new("--run-tests"), fixture.as_os_str()]); let stdout = String::from_utf8_lossy(&output.stdout); let stderr = String::from_utf8_lossy(&output.stderr); assert_eq!( output.status.code(), Some(1), "std print test runner rejection `{}` exit code drifted\nstdout:\n{}\nstderr:\n{}", name, stdout, stderr ); assert!( stdout.is_empty(), "std print test runner rejection `{}` wrote stdout:\n{}", name, stdout ); assert!( stderr.contains("UnsupportedTestExpression"), "std print test runner rejection `{}` diagnostic drifted\nstderr:\n{}", name, stderr ); } } #[test] fn standard_runtime_fixture_runtime_smoke_when_clang_is_available() { let Some(clang) = find_clang() else { eprintln!("skipping standard-runtime smoke: set GLAGOL_CLANG or install clang"); return; }; let fixture = Path::new(env!("CARGO_MANIFEST_DIR")).join("../examples/standard-runtime.slo"); let compile = run_glagol([fixture.as_os_str()]); assert_success("compile standard-runtime smoke", &compile); let run = compile_and_run_with_runtime(&clang, "standard-runtime", &compile.stdout); assert_success("run standard-runtime smoke", &run); assert_eq!( String::from_utf8_lossy(&run.stdout), "standard\ntrue\n8\n", "standard-runtime stdout drifted" ); } #[test] fn standard_string_concat_runtime_smoke_when_clang_is_available() { let Some(clang) = find_clang() else { eprintln!("skipping std string concat runtime smoke: set GLAGOL_CLANG or install clang"); return; }; let fixture = write_fixture( "std-string-concat-runtime-smoke", r#" (module main) (fn main () -> i32 (std.io.print_string (std.string.concat "hello" "!")) (std.io.print_i32 (std.string.len (std.string.concat "ab" "c"))) 0) "#, ); let compile = run_glagol([fixture.as_os_str()]); assert_success("compile std string concat runtime smoke", &compile); let run = compile_and_run_with_runtime(&clang, "std-string-concat", &compile.stdout); assert_success("run std string concat runtime smoke", &run); assert_eq!( String::from_utf8_lossy(&run.stdout), "hello!\n3\n", "std string concat runtime stdout drifted" ); } #[test] fn standard_string_concat_allocation_failure_traps_when_clang_is_available() { let Some(clang) = find_clang() else { eprintln!( "skipping std string concat allocation trap smoke: set GLAGOL_CLANG or install clang" ); return; }; let manifest = Path::new(env!("CARGO_MANIFEST_DIR")); let temp_dir = env::temp_dir().join(format!( "glagol-string-runtime-allocation-trap-{}-{}", std::process::id(), NEXT_FIXTURE_ID.fetch_add(1, Ordering::Relaxed) )); fs::create_dir_all(&temp_dir) .unwrap_or_else(|err| panic!("create `{}`: {}", temp_dir.display(), err)); let probe = temp_dir.join("concat-allocation-trap.c"); let exe = temp_dir.join("concat-allocation-trap"); fs::write( &probe, r#"#include char *__glagol_string_concat(const char *left, const char *right); void *__wrap_malloc(size_t size) { (void)size; return NULL; } int main(void) { (void)__glagol_string_concat("a", "b"); return 0; } "#, ) .unwrap_or_else(|err| panic!("write `{}`: {}", probe.display(), err)); let runtime = manifest.join("../runtime/runtime.c"); let mut clang_command = Command::new(&clang); clang_command .arg(&runtime) .arg(&probe) .arg("-Wl,--wrap=malloc") .arg("-o") .arg(&exe) .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_success("clang allocation trap probe", &clang_output); let run = Command::new(&exe) .output() .unwrap_or_else(|err| panic!("run `{}`: {}", exe.display(), err)); assert_eq!( run.status.code(), Some(1), "allocation 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: string allocation failed\n", "allocation trap stderr drifted" ); assert!( run.stdout.is_empty(), "allocation trap wrote stdout:\n{}", String::from_utf8_lossy(&run.stdout) ); } #[test] fn v1_2_runtime_traps_exit_one_with_contract_messages_when_clang_is_available() { let Some(clang) = find_clang() else { eprintln!("skipping v1.2 runtime trap smoke: set GLAGOL_CLANG or install clang"); return; }; let cases = [ ( "array-bounds", r#" (module main) (fn at ((i i32)) -> i32 (index (array i32 7) i)) (fn main () -> i32 (at 1)) "#, "slovo runtime error: array index out of bounds\n", ), ( "unwrap-some", r#" (module main) (fn main () -> i32 (unwrap_some (none i32))) "#, "slovo runtime error: unwrap_some on none\n", ), ( "unwrap-ok", r#" (module main) (fn main () -> i32 (unwrap_ok (err i32 i32 1))) "#, "slovo runtime error: unwrap_ok on err\n", ), ( "unwrap-err", r#" (module main) (fn main () -> i32 (unwrap_err (ok i32 i32 1))) "#, "slovo runtime error: unwrap_err on ok\n", ), ]; for (name, source, expected_stderr) in cases { let fixture = write_fixture(name, source); let compile = run_glagol([fixture.as_os_str()]); assert_success("compile v1.2 runtime trap smoke", &compile); let run = compile_and_run_with_runtime(&clang, name, &compile.stdout); assert_eq!( run.status.code(), Some(1), "trap `{}` exit code drifted\nstdout:\n{}\nstderr:\n{}", name, String::from_utf8_lossy(&run.stdout), String::from_utf8_lossy(&run.stderr) ); assert_eq!( String::from_utf8_lossy(&run.stderr), expected_stderr, "trap `{}` stderr drifted", name ); assert!( run.stdout.is_empty(), "trap `{}` wrote stdout:\n{}", name, String::from_utf8_lossy(&run.stdout) ); } } #[test] fn v1_2_test_runner_reports_runtime_traps_with_contract_messages() { let cases = [ ( "test-array-bounds", r#" (module main) (fn at ((i i32)) -> i32 (index (array i32 7) i)) (test "array trap" (= (at 1) 0)) "#, "slovo runtime error: array index out of bounds", ), ( "test-unwrap-some", r#" (module main) (test "unwrap some trap" (= (unwrap_some (none i32)) 0)) "#, "slovo runtime error: unwrap_some on none", ), ( "test-unwrap-ok", r#" (module main) (test "unwrap ok trap" (= (unwrap_ok (err i32 i32 1)) 0)) "#, "slovo runtime error: unwrap_ok on err", ), ( "test-unwrap-err", r#" (module main) (test "unwrap err trap" (= (unwrap_err (ok i32 i32 1)) 0)) "#, "slovo runtime error: unwrap_err on ok", ), ]; for (name, source, expected_message) in cases { let fixture = write_fixture(name, source); let output = run_glagol([OsStr::new("test"), fixture.as_os_str()]); let stdout = String::from_utf8_lossy(&output.stdout); let stderr = String::from_utf8_lossy(&output.stderr); assert_eq!( output.status.code(), Some(1), "test runner trap `{}` exit code drifted\nstdout:\n{}\nstderr:\n{}", name, stdout, stderr ); assert!( stdout.is_empty(), "test runner trap `{}` wrote stdout:\n{}", name, stdout ); assert!( stderr.contains("TestRuntimeTrap") && stderr.contains("test trapped") && stderr.contains(expected_message), "test runner trap `{}` diagnostic drifted\nstderr:\n{}", name, stderr ); } } fn run_glagol(args: I) -> Output where I: IntoIterator, S: AsRef, { Command::new(env!("CARGO_BIN_EXE_glagol")) .args(args) .current_dir(Path::new(env!("CARGO_MANIFEST_DIR"))) .output() .expect("run glagol") } fn write_fixture(name: &str, source: &str) -> PathBuf { let mut path = env::temp_dir(); path.push(format!( "glagol-string-runtime-{}-{}-{}.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 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 ); } fn compile_and_run_with_runtime(clang: &Path, name: &str, ir: &[u8]) -> Output { let manifest = Path::new(env!("CARGO_MANIFEST_DIR")); let temp_dir = env::temp_dir().join(format!( "glagol-string-runtime-{}-{}", std::process::id(), NEXT_FIXTURE_ID.fetch_add(1, Ordering::Relaxed) )); 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_success("clang string runtime smoke", &clang_output); Command::new(&exe_path) .output() .unwrap_or_else(|err| panic!("run `{}`: {}", exe_path.display(), err)) } fn find_clang() -> Option { 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 find_on_path(program: &str) -> Option { 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 mut paths = vec![lib64, lib]; if let Some(existing) = env::var_os("LD_LIBRARY_PATH") { paths.extend(env::split_paths(&existing)); } let joined = env::join_paths(paths).expect("join LD_LIBRARY_PATH"); command.env("LD_LIBRARY_PATH", joined); }