From 2c6fad0b851aa1963802336e75e6a94fc8253147 Mon Sep 17 00:00:00 2001 From: hyperpolymath <6759885+hyperpolymath@users.noreply.github.com> Date: Mon, 18 May 2026 11:05:16 +0100 Subject: [PATCH] feat(solo/stdlib): fs_list_dir(path) -> Array Last remaining Solo stdlib gap from the #45 set. Audit/scan tooling needs to enumerate a directory; only fs_exists/read/write/create_dir_all existed. fs_list_dir returns entry names (not full paths), sorted for deterministic tooling output, erroring if path is not a readable dir. Registered alongside the other fs_* builtins. Adds examples/fs_list_dir.my and a self-contained integration test (builds a dir via fs_* then lists it). Refs #55 Co-Authored-By: Claude Opus 4.7 (1M context) --- crates/my-lang/src/stdlib.rs | 32 ++++++++++++++++++++++++++++++++ examples/fs_list_dir.my | 12 ++++++++++++ tests/integration_test.rs | 21 +++++++++++++++++++++ 3 files changed, 65 insertions(+) create mode 100644 examples/fs_list_dir.my diff --git a/crates/my-lang/src/stdlib.rs b/crates/my-lang/src/stdlib.rs index 07ffd17..8b8d3bb 100644 --- a/crates/my-lang/src/stdlib.rs +++ b/crates/my-lang/src/stdlib.rs @@ -1471,6 +1471,37 @@ fn register_fs_functions(define: &mut impl FnMut(String, Value)) { }, }), ); + + // fs_list_dir(path) -> Array - entry names (not full paths) in + // `path`, sorted for deterministic tooling output. Errors if `path` is not + // a readable directory. See hyperpolymath/my-lang#55. + define( + "fs_list_dir".to_string(), + Value::NativeFunction(NativeFunction { + name: "fs_list_dir".to_string(), + arity: 1, + func: |args| match &args[0] { + Value::String(path) => { + let rd = std::fs::read_dir(path).map_err(|e| { + RuntimeError::Custom(format!("fs_list_dir({}) failed: {}", path, e)) + })?; + let mut names: Vec = Vec::new(); + for entry in rd { + let entry = entry.map_err(|e| { + RuntimeError::Custom(format!("fs_list_dir({}) failed: {}", path, e)) + })?; + names.push(entry.file_name().to_string_lossy().into_owned()); + } + names.sort(); + Ok(Value::Array(names.into_iter().map(Value::String).collect())) + } + _ => Err(RuntimeError::TypeError { + expected: "string".to_string(), + got: format!("{:?}", args[0]), + }), + }, + }), + ); } // ============================================================================ @@ -1880,6 +1911,7 @@ pub fn stdlib_functions() -> Vec<&'static str> { "fs_read_file", "fs_create_dir_all", "fs_exists", + "fs_list_dir", // Env extras "env_args", // Map / dict diff --git a/examples/fs_list_dir.my b/examples/fs_list_dir.my new file mode 100644 index 0000000..ae9a23a --- /dev/null +++ b/examples/fs_list_dir.my @@ -0,0 +1,12 @@ +// Solo fs_list_dir() builtin (hyperpolymath/my-lang#55). +// +// my run examples/fs_list_dir.my +// +// Lists this repo's examples/ directory, sorted. Output includes the other +// example files (date.my, json.my, map.my, fs_list_dir.my, ...). + +fn main() -> Int { + let entries = fs_list_dir("examples"); + println(str_join(entries, "\n")); + return 0; +} diff --git a/tests/integration_test.rs b/tests/integration_test.rs index 11f3012..79a3c39 100644 --- a/tests/integration_test.rs +++ b/tests/integration_test.rs @@ -293,6 +293,27 @@ fn test_eval_date_today() { } } +// Solo fs_list_dir() stdlib builtin (hyperpolymath/my-lang#55): enumerate a +// directory, sorted entry names. Self-contained: builds a dir with fs_* builtins +// then lists it (exercises fs_create_dir_all/fs_write_file/fs_list_dir/str_join). +#[test] +fn test_eval_fs_list_dir() { + let source = r#" + fn main() -> String { + let dir = "target/.it_fs_list_dir"; + fs_create_dir_all(dir); + fs_write_file("target/.it_fs_list_dir/b.txt", "x"); + fs_write_file("target/.it_fs_list_dir/a.txt", "y"); + let names = fs_list_dir(dir); + return str_join(names, ","); + } + "#; + match eval(source) { + Ok(Value::String(s)) => assert_eq!(s, "a.txt,b.txt"), + other => panic!("expected sorted dir listing, got {:?}", other), + } +} + // AI Runtime tests (require API keys, so just test initialization) #[test] fn test_ai_runtime_creation() {