From f2688072ac2fc700ab2734a988d1a1a3a3cb169d Mon Sep 17 00:00:00 2001 From: Brandon Lucas Date: Fri, 20 Feb 2026 10:33:59 -0500 Subject: [PATCH] feat: add File.glob for file pattern matching (issue 15) Add File.glob(pattern) effect operation that returns a list of file paths matching a glob pattern (e.g., "src/**/*.lux"). Implemented across interpreter (using glob crate), JS backend (handler-based), and C backend (using POSIX glob.h). Co-Authored-By: Claude Opus 4.6 --- Cargo.lock | 13 ++++++++++--- Cargo.toml | 1 + src/codegen/c_backend.rs | 37 ++++++++++++++++++++++++++++++++++++- src/interpreter.rs | 23 +++++++++++++++++++++++ src/types.rs | 5 +++++ 5 files changed, 75 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index aea470d..0f949d1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -225,7 +225,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -392,6 +392,12 @@ dependencies = [ "wasip3", ] +[[package]] +name = "glob" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" + [[package]] name = "h2" version = "0.3.27" @@ -772,6 +778,7 @@ dependencies = [ name = "lux" version = "0.1.6" dependencies = [ + "glob", "lsp-server", "lsp-types", "postgres", @@ -1182,7 +1189,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -1475,7 +1482,7 @@ dependencies = [ "getrandom 0.4.1", "once_cell", "rustix", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 84b3623..2aa96a4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,6 +17,7 @@ reqwest = { version = "0.11", default-features = false, features = ["blocking", tiny_http = "0.12" rusqlite = { version = "0.31", features = ["bundled"] } postgres = "0.19" +glob = "0.3" [dev-dependencies] diff --git a/src/codegen/c_backend.rs b/src/codegen/c_backend.rs index afe63b8..0693e1c 100644 --- a/src/codegen/c_backend.rs +++ b/src/codegen/c_backend.rs @@ -1166,6 +1166,7 @@ impl CBackend { self.writeln(" void (*mkdir)(void* env, LuxString path);"); self.writeln(" void (*copy)(void* env, LuxString src, LuxString dst);"); self.writeln(" LuxList* (*readDir)(void* env, LuxString path);"); + self.writeln(" LuxList* (*glob)(void* env, LuxString pattern);"); self.writeln(" void* env;"); self.writeln("} LuxFileHandler;"); self.writeln(""); @@ -1386,6 +1387,23 @@ impl CBackend { self.writeln(" return result;"); self.writeln("}"); self.writeln(""); + self.writeln("#include "); + self.writeln("static LuxList* lux_file_glob(LuxString pattern) {"); + self.writeln(" LuxList* result = lux_list_new(16);"); + self.writeln(" glob_t globbuf;"); + self.writeln(" int ret = glob(pattern, 0, NULL, &globbuf);"); + self.writeln(" if (ret == 0) {"); + self.writeln(" for (size_t i = 0; i < globbuf.gl_pathc; i++) {"); + self.writeln(" size_t len = strlen(globbuf.gl_pathv[i]);"); + self.writeln(" LuxString name = (LuxString)lux_rc_alloc(len + 1, LUX_TAG_STRING);"); + self.writeln(" memcpy(name, globbuf.gl_pathv[i], len + 1);"); + self.writeln(" lux_list_push(result, (void*)name);"); + self.writeln(" }"); + self.writeln(" globfree(&globbuf);"); + self.writeln(" }"); + self.writeln(" return result;"); + self.writeln("}"); + self.writeln(""); self.writeln("static LuxString default_file_read(void* env, LuxString path) {"); self.writeln(" (void)env;"); self.writeln(" return lux_file_read(path);"); @@ -1431,6 +1449,11 @@ impl CBackend { self.writeln(" return lux_file_readDir(path);"); self.writeln("}"); self.writeln(""); + self.writeln("static LuxList* default_file_glob(void* env, LuxString pattern) {"); + self.writeln(" (void)env;"); + self.writeln(" return lux_file_glob(pattern);"); + self.writeln("}"); + self.writeln(""); self.writeln("static LuxFileHandler default_file_handler = {"); self.writeln(" .read = default_file_read,"); self.writeln(" .write = default_file_write,"); @@ -1441,6 +1464,7 @@ impl CBackend { self.writeln(" .mkdir = default_file_mkdir,"); self.writeln(" .copy = default_file_copy,"); self.writeln(" .readDir = default_file_readDir,"); + self.writeln(" .glob = default_file_glob,"); self.writeln(" .env = NULL"); self.writeln("};"); self.writeln(""); @@ -3790,6 +3814,17 @@ impl CBackend { self.register_rc_var(&temp, "LuxList*"); return Ok(temp); } + "glob" => { + let pattern = self.emit_expr(&args[0])?; + let temp = format!("_glob_{}", self.fresh_name()); + if self.has_evidence { + self.writeln(&format!("LuxList* {} = ev->file->glob(ev->file->env, {});", temp, pattern)); + } else { + self.writeln(&format!("LuxList* {} = lux_file_glob({});", temp, pattern)); + } + self.register_rc_var(&temp, "LuxList*"); + return Ok(temp); + } _ => {} } } @@ -5353,7 +5388,7 @@ impl CBackend { "read" => Some("LuxString".to_string()), "write" | "append" | "delete" | "mkdir" | "copy" => Some("void".to_string()), "exists" | "isDir" => Some("LuxBool".to_string()), - "readDir" | "listDir" => Some("LuxList*".to_string()), + "readDir" | "listDir" | "glob" => Some("LuxList*".to_string()), _ => None, } } else if effect.name == "Http" { diff --git a/src/interpreter.rs b/src/interpreter.rs index 55f9d8b..8761fd5 100644 --- a/src/interpreter.rs +++ b/src/interpreter.rs @@ -3968,6 +3968,29 @@ impl Interpreter { } } + ("File", "glob") => { + let pattern = match request.args.first() { + Some(Value::String(s)) => s.clone(), + _ => return Err(RuntimeError { + message: "File.glob requires a string pattern".to_string(), + span: None, + }), + }; + match glob::glob(&pattern) { + Ok(paths) => { + let entries: Vec = paths + .filter_map(|entry| entry.ok()) + .map(|path| Value::String(path.to_string_lossy().to_string())) + .collect(); + Ok(Value::List(entries)) + } + Err(e) => Err(RuntimeError { + message: format!("Invalid glob pattern '{}': {}", pattern, e), + span: None, + }), + } + } + // ===== Process Effect ===== ("Process", "exec") => { use std::process::Command; diff --git a/src/types.rs b/src/types.rs index ba1f85e..102d7fc 100644 --- a/src/types.rs +++ b/src/types.rs @@ -964,6 +964,11 @@ impl TypeEnv { ], return_type: Type::Unit, }, + EffectOpDef { + name: "glob".to_string(), + params: vec![("pattern".to_string(), Type::String)], + return_type: Type::List(Box::new(Type::String)), + }, ], }, );