use std::{ env, ffi::OsStr, fs, path::{Path, PathBuf}, process::{Command, Output}, sync::atomic::{AtomicUsize, Ordering}, }; static NEXT_TEMP_ID: AtomicUsize = AtomicUsize::new(0); const EXPECTED_TEST_OUTPUT: &str = concat!( "test \"explicit std string contains\" ... ok\n", "test \"explicit std string index_of_option\" ... ok\n", "test \"explicit std string last_index_of_option\" ... ok\n", "test \"explicit std string ascii trim\" ... ok\n", "test \"explicit std string search trim composition\" ... ok\n", "test \"explicit std string search trim all\" ... ok\n", "6 test(s) passed\n", ); const STANDARD_STRING_SEARCH_TRIM_BETA20: &[&str] = &[ "contains", "index_of_option", "last_index_of_option", "trim_ascii_start", "trim_ascii_end", "trim_ascii", ]; const ALLOWED_STD_REFERENCES: &[&str] = &[ "std.result", "std.string.parse_bool_result", "std.string.parse_f64_result", "std.string.parse_i64_result", "std.string.parse_u64_result", "std.string.parse_i32_result", "std.string.parse_u32_result", "std.string.byte_at_result", "std.string.slice_result", "std.string.starts_with", "std.string.ends_with", "std.string.concat", "std.string.len", ]; #[test] fn explicit_std_string_search_and_ascii_trim_helpers_check_and_test() { let project = write_project( "std-string-search-trim-beta20", r#" (module main) (import std.string (contains index_of_option last_index_of_option trim_ascii_start trim_ascii_end trim_ascii)) (fn option_i32_eq ((maybe (option i32)) (expected i32)) -> bool (match maybe ((some value) (= value expected)) ((none) false))) (fn option_i32_none ((maybe (option i32))) -> bool (match maybe ((some value) false) ((none) true))) (fn imported_string_contains_ok () -> bool (if (contains "slovo compiler" "slo") (if (contains "slovo compiler" "compiler") (= (contains "slovo compiler" "missing") false) false) false)) (fn imported_string_index_of_ok () -> bool (if (option_i32_eq (index_of_option "bananana" "ana") 1) (if (option_i32_eq (index_of_option "slovo" "s") 0) (if (option_i32_eq (index_of_option "slovo" "vo") 3) (option_i32_none (index_of_option "slovo" "compiler")) false) false) false)) (fn imported_string_last_index_of_ok () -> bool (if (option_i32_eq (last_index_of_option "bananana" "ana") 5) (if (option_i32_eq (last_index_of_option "slovo" "o") 4) (if (option_i32_eq (last_index_of_option "slovo" "s") 0) (option_i32_none (last_index_of_option "slovo" "compiler")) false) false) false)) (fn imported_string_ascii_trim_ok () -> bool (if (= (trim_ascii_start "\n\t slovo \t") "slovo \t") (if (= (trim_ascii_end "\n\t slovo \t") "\n\t slovo") (if (= (trim_ascii "\n\t slovo \t") "slovo") (if (= (trim_ascii "slovo") "slovo") (= (trim_ascii " ") "") false) false) false) false)) (fn imported_string_search_trim_composes_ok () -> bool (if (= (trim_ascii " slovo compiler ") "slovo compiler") (if (contains (trim_ascii " slovo compiler ") "compiler") (if (option_i32_eq (index_of_option (trim_ascii_start "\t\tprefix-core") "core") 7) (option_i32_eq (last_index_of_option (trim_ascii_end "core-core\n") "core") 5) false) false) false)) (fn imported_string_search_trim_all_ok () -> bool (if (imported_string_contains_ok) (if (imported_string_index_of_ok) (if (imported_string_last_index_of_ok) (if (imported_string_ascii_trim_ok) (imported_string_search_trim_composes_ok) false) false) false) false)) (fn main () -> i32 (if (imported_string_search_trim_all_ok) 42 1)) (test "explicit std string contains" (imported_string_contains_ok)) (test "explicit std string index_of_option" (imported_string_index_of_ok)) (test "explicit std string last_index_of_option" (imported_string_last_index_of_ok)) (test "explicit std string ascii trim" (imported_string_ascii_trim_ok)) (test "explicit std string search trim composition" (imported_string_search_trim_composes_ok)) (test "explicit std string search trim all" (= (main) 42)) "#, ); let source = read(&project.join("src/main.slo")); let std_string = read(&std_string_path()); assert!( !project.join("src/string.slo").exists(), "beta20 fixture must exercise repo-root std.string, not a local module copy" ); assert!( source.starts_with("(module main)\n\n(import std.string ("), "beta20 fixture must use an explicit std.string import" ); assert_std_string_search_trim_facades(&std_string); let fmt = run_glagol([ OsStr::new("fmt"), OsStr::new("--check"), project.as_os_str(), ]); assert_success("std string search trim fmt --check", &fmt); let check = run_glagol([OsStr::new("check"), project.as_os_str()]); assert_success_stdout(check, "", "std string search trim check"); let test = run_glagol([OsStr::new("test"), project.as_os_str()]); assert_success_stdout(test, EXPECTED_TEST_OUTPUT, "std string search trim test"); } #[test] fn string_search_and_ascii_trim_helpers_are_not_compiler_known_runtime_calls() { let std_string = read(&std_string_path()); assert_std_string_search_trim_facades(&std_string); for helper in STANDARD_STRING_SEARCH_TRIM_BETA20 { assert!( !std_string.contains(&format!("std.string.{}", helper)), "std.string.{} must remain source-authored, not a compiler-known runtime call", helper ); assert!( !std_string.contains(&format!("__glagol_string_{}", helper)), "std.string.{} must not introduce a private runtime symbol", helper ); } let cases = [ UnsupportedRuntimeCase { name: "contains", symbol: "std.string.contains", source: r#" (module main) (fn main () -> i32 (if (std.string.contains "slovo" "ovo") 0 1)) "#, }, UnsupportedRuntimeCase { name: "index-of-option", symbol: "std.string.index_of_option", source: r#" (module main) (fn main () -> i32 (match (std.string.index_of_option "slovo" "o") ((some value) value) ((none) 0))) "#, }, UnsupportedRuntimeCase { name: "last-index-of-option", symbol: "std.string.last_index_of_option", source: r#" (module main) (fn main () -> i32 (match (std.string.last_index_of_option "slovo" "o") ((some value) value) ((none) 0))) "#, }, UnsupportedRuntimeCase { name: "trim-ascii-start", symbol: "std.string.trim_ascii_start", source: r#" (module main) (fn main () -> i32 (std.string.len (std.string.trim_ascii_start " slovo"))) "#, }, UnsupportedRuntimeCase { name: "trim-ascii-end", symbol: "std.string.trim_ascii_end", source: r#" (module main) (fn main () -> i32 (std.string.len (std.string.trim_ascii_end "slovo "))) "#, }, UnsupportedRuntimeCase { name: "trim-ascii", symbol: "std.string.trim_ascii", source: r#" (module main) (fn main () -> i32 (std.string.len (std.string.trim_ascii " slovo "))) "#, }, ]; for case in cases { let fixture = write_fixture(case.name, case.source); let output = run_glagol([fixture.as_os_str()]); assert_failure_stderr_contains( &format!("direct {} runtime call", case.symbol), &output, &format!("standard library call `{}` is not supported", case.symbol), ); } } fn assert_std_string_search_trim_facades(std_string: &str) { assert!( std_string.starts_with("(module string (export "), "lib/std/string.slo must stay a source-authored module export" ); let mut non_allowed_std = std_string.to_owned(); for allowed in ALLOWED_STD_REFERENCES { non_allowed_std = non_allowed_std.replace(allowed, ""); } assert!( !non_allowed_std.contains("std."), "std.string beta20 helpers must use only existing std.result bridges and promoted beta16-or-earlier std.string primitives" ); for helper in STANDARD_STRING_SEARCH_TRIM_BETA20 { assert!( std_string.contains(&format!("(fn {} ", helper)), "lib/std/string.slo is missing source facade `{}`", helper ); } let search_trim_source = search_trim_source_region(std_string); for primitive in [ ("len", ["std.string.len", "(len "]), ( "byte_at_result", ["std.string.byte_at_result", "(byte_at_result "], ), ( "slice_result", ["std.string.slice_result", "(slice_result "], ), ("starts_with", ["std.string.starts_with", "(starts_with "]), ] { assert!( primitive .1 .iter() .any(|needle| search_trim_source.contains(needle)), "beta20 search/trim facades must compose over existing beta16 string primitive `{}`", primitive.0 ); } assert!( !std_string.contains("unicode") && !std_string.contains("grapheme") && !std_string.contains("locale") && !std_string.contains("case_insensitive") && !std_string.contains("regex"), "beta20 string helpers must not claim deferred Unicode, locale, case-folding, or regex APIs" ); } fn search_trim_source_region(source: &str) -> &str { let ends_with_end = function_range(source, "ends_with").1; let parse_start = function_range(source, "parse_i32_result").0; &source[ends_with_end..parse_start] } fn function_range(source: &str, name: &str) -> (usize, usize) { let needle = format!("(fn {} ", name); let start = source .find(&needle) .unwrap_or_else(|| panic!("missing function `{}`", name)); let mut depth = 0usize; for (offset, byte) in source.as_bytes()[start..].iter().enumerate() { match byte { b'(' => depth += 1, b')' => { depth = depth .checked_sub(1) .unwrap_or_else(|| panic!("unbalanced function `{}`", name)); if depth == 0 { return (start, start + offset + 1); } } _ => {} } } panic!("unterminated function `{}`", name); } 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_project(name: &str, source: &str) -> PathBuf { let root = temp_root(name); let src = root.join("src"); fs::create_dir_all(&src).unwrap_or_else(|err| panic!("create `{}`: {}", src.display(), err)); fs::write( root.join("slovo.toml"), format!( "[project]\nname = \"{}\"\nsource_root = \"src\"\nentry = \"main\"\n", name ), ) .unwrap_or_else(|err| panic!("write project manifest: {}", err)); fs::write(src.join("main.slo"), source.trim_start()) .unwrap_or_else(|err| panic!("write project main.slo: {}", err)); root } fn write_fixture(name: &str, source: &str) -> PathBuf { let mut path = env::temp_dir(); path.push(format!( "glagol-standard-string-search-trim-beta20-{}-{}-{}.slo", name, std::process::id(), NEXT_TEMP_ID.fetch_add(1, Ordering::Relaxed) )); fs::write(&path, source.trim_start()) .unwrap_or_else(|err| panic!("write `{}`: {}", path.display(), err)); path } fn temp_root(name: &str) -> PathBuf { let root = env::temp_dir().join(format!( "glagol-standard-string-search-trim-beta20-{}-{}-{}", name, std::process::id(), NEXT_TEMP_ID.fetch_add(1, Ordering::Relaxed) )); let _ = fs::remove_dir_all(&root); fs::create_dir_all(&root).unwrap_or_else(|err| panic!("create `{}`: {}", root.display(), err)); root } fn std_string_path() -> PathBuf { Path::new(env!("CARGO_MANIFEST_DIR")).join("../lib/std/string.slo") } fn read(path: &Path) -> String { fs::read_to_string(path).unwrap_or_else(|err| panic!("read `{}`: {}", path.display(), err)) } 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\nstatus: {:?}\nstdout:\n{}\nstderr:\n{}", context, output.status.code(), stdout, stderr ); assert!(stderr.is_empty(), "{} wrote stderr:\n{}", context, stderr); } fn assert_success_stdout(output: Output, expected: &str, context: &str) { assert_success(context, &output); let stdout = String::from_utf8_lossy(&output.stdout); assert_eq!(stdout, expected, "{}", context); } fn assert_failure_stderr_contains(context: &str, output: &Output, needle: &str) { let stdout = String::from_utf8_lossy(&output.stdout); let stderr = String::from_utf8_lossy(&output.stderr); assert!( !output.status.success(), "{} unexpectedly passed\nstdout:\n{}\nstderr:\n{}", context, stdout, stderr ); assert!( stdout.is_empty(), "{} rejected compile wrote stdout:\n{}", context, stdout ); assert!( stderr.contains(needle), "{} stderr did not contain `{}`:\n{}", context, needle, stderr ); } struct UnsupportedRuntimeCase { name: &'static str, symbol: &'static str, source: &'static str, }