From ec365ebb3fd051372112630a329d9e110e8ab18d Mon Sep 17 00:00:00 2001 From: Brandon Lucas Date: Thu, 19 Feb 2026 09:24:28 -0500 Subject: [PATCH] feat: add File.copy and propagate effectful callback effects (WISH-7, WISH-14) File.copy(source, dest) copies files via interpreter (std::fs::copy) and C backend (fread/fwrite). Effectful callbacks passed to higher-order functions like List.map/forEach now propagate their effects to the enclosing function's inferred effect set. Co-Authored-By: Claude Opus 4.6 --- src/codegen/c_backend.rs | 33 +++++++++++++++++++++++++- src/interpreter.rs | 24 +++++++++++++++++++ src/main.rs | 51 ++++++++++++++++++++++++++++++++++++++++ src/typechecker.rs | 34 +++++++++++++++++++++++++++ src/types.rs | 8 +++++++ 5 files changed, 149 insertions(+), 1 deletion(-) diff --git a/src/codegen/c_backend.rs b/src/codegen/c_backend.rs index 0eb34d4..f176665 100644 --- a/src/codegen/c_backend.rs +++ b/src/codegen/c_backend.rs @@ -1149,6 +1149,7 @@ impl CBackend { self.writeln(" void (*delete_file)(void* env, LuxString path);"); self.writeln(" LuxBool (*isDir)(void* env, LuxString path);"); 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(" void* env;"); self.writeln("} LuxFileHandler;"); @@ -1336,6 +1337,20 @@ impl CBackend { self.writeln(" mkdir(path, 0755);"); self.writeln("}"); self.writeln(""); + self.writeln("static void lux_file_copy(LuxString src, LuxString dst) {"); + self.writeln(" FILE* fin = fopen(src, \"rb\");"); + self.writeln(" if (!fin) return;"); + self.writeln(" FILE* fout = fopen(dst, \"wb\");"); + self.writeln(" if (!fout) { fclose(fin); return; }"); + self.writeln(" char buf[4096];"); + self.writeln(" size_t n;"); + self.writeln(" while ((n = fread(buf, 1, sizeof(buf), fin)) > 0) {"); + self.writeln(" fwrite(buf, 1, n, fout);"); + self.writeln(" }"); + self.writeln(" fclose(fin);"); + self.writeln(" fclose(fout);"); + self.writeln("}"); + self.writeln(""); self.writeln("#include "); self.writeln("// Forward declarations needed by lux_file_readDir"); self.writeln("static LuxList* lux_list_new(int64_t capacity);"); @@ -1391,6 +1406,11 @@ impl CBackend { self.writeln(" lux_file_mkdir(path);"); self.writeln("}"); self.writeln(""); + self.writeln("static void default_file_copy(void* env, LuxString src, LuxString dst) {"); + self.writeln(" (void)env;"); + self.writeln(" lux_file_copy(src, dst);"); + self.writeln("}"); + self.writeln(""); self.writeln("static LuxList* default_file_readDir(void* env, LuxString path) {"); self.writeln(" (void)env;"); self.writeln(" return lux_file_readDir(path);"); @@ -1404,6 +1424,7 @@ impl CBackend { self.writeln(" .delete_file = default_file_delete,"); self.writeln(" .isDir = default_file_isDir,"); self.writeln(" .mkdir = default_file_mkdir,"); + self.writeln(" .copy = default_file_copy,"); self.writeln(" .readDir = default_file_readDir,"); self.writeln(" .env = NULL"); self.writeln("};"); @@ -3679,6 +3700,16 @@ impl CBackend { } return Ok("NULL".to_string()); } + "copy" => { + let src = self.emit_expr(&args[0])?; + let dst = self.emit_expr(&args[1])?; + if self.has_evidence { + self.writeln(&format!("ev->file->copy(ev->file->env, {}, {});", src, dst)); + } else { + self.writeln(&format!("lux_file_copy({}, {});", src, dst)); + } + return Ok("NULL".to_string()); + } "readDir" | "listDir" => { let path = self.emit_expr(&args[0])?; let temp = format!("_readdir_{}", self.fresh_name()); @@ -5175,7 +5206,7 @@ impl CBackend { } else if effect.name == "File" { match operation.name.as_str() { "read" => Some("LuxString".to_string()), - "write" | "append" | "delete" | "mkdir" => Some("void".to_string()), + "write" | "append" | "delete" | "mkdir" | "copy" => Some("void".to_string()), "exists" | "isDir" => Some("LuxBool".to_string()), "readDir" | "listDir" => Some("LuxList*".to_string()), _ => None, diff --git a/src/interpreter.rs b/src/interpreter.rs index bc66646..0d9bf30 100644 --- a/src/interpreter.rs +++ b/src/interpreter.rs @@ -3864,6 +3864,30 @@ impl Interpreter { } } + ("File", "copy") => { + let source = match request.args.first() { + Some(Value::String(s)) => s.clone(), + _ => return Err(RuntimeError { + message: "File.copy requires a string source path".to_string(), + span: None, + }), + }; + let dest = match request.args.get(1) { + Some(Value::String(s)) => s.clone(), + _ => return Err(RuntimeError { + message: "File.copy requires a string destination path".to_string(), + span: None, + }), + }; + match std::fs::copy(&source, &dest) { + Ok(_) => Ok(Value::Unit), + Err(e) => Err(RuntimeError { + message: format!("Failed to copy '{}' to '{}': {}", source, dest, e), + span: None, + }), + } + } + // ===== Process Effect ===== ("Process", "exec") => { use std::process::Command; diff --git a/src/main.rs b/src/main.rs index 9879de8..b2fb592 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5629,4 +5629,55 @@ c")"#; "#; assert_eq!(eval(source).unwrap(), "Some(30)"); } + + #[test] + fn test_file_copy() { + use std::io::Write; + // Create a temp file, copy it, verify contents + let dir = std::env::temp_dir().join("lux_test_file_copy"); + let _ = std::fs::create_dir_all(&dir); + let src = dir.join("src.txt"); + let dst = dir.join("dst.txt"); + std::fs::File::create(&src).unwrap().write_all(b"hello copy").unwrap(); + let _ = std::fs::remove_file(&dst); + + let source = format!(r#" + fn main(): Unit with {{File}} = + File.copy("{}", "{}") + let _ = run main() with {{}} + let result = "done" + "#, src.display(), dst.display()); + let result = eval(&source); + assert!(result.is_ok(), "File.copy failed: {:?}", result); + let contents = std::fs::read_to_string(&dst).unwrap(); + assert_eq!(contents, "hello copy"); + + // Cleanup + let _ = std::fs::remove_dir_all(&dir); + } + + #[test] + fn test_effectful_callback_propagation() { + // WISH-7: effectful callbacks in List.forEach should propagate effects + // This should type-check successfully because Console effect is inferred + let source = r#" + fn printAll(items: List): Unit = + List.forEach(items, fn(x: String): Unit => Console.print(x)) + let result = "ok" + "#; + let result = eval(source); + assert!(result.is_ok(), "Effectful callback should type-check: {:?}", result); + } + + #[test] + fn test_effectful_callback_in_map() { + // Effectful callback in List.map should propagate effects + let source = r#" + fn readAll(paths: List): List = + List.map(paths, fn(p: String): String => File.read(p)) + let result = "ok" + "#; + let result = eval(source); + assert!(result.is_ok(), "Effectful callback in map should type-check: {:?}", result); + } } diff --git a/src/typechecker.rs b/src/typechecker.rs index fedda1b..049bef9 100644 --- a/src/typechecker.rs +++ b/src/typechecker.rs @@ -1951,6 +1951,17 @@ impl TypeChecker { let func_type = self.infer_expr(func); let arg_types: Vec = args.iter().map(|a| self.infer_expr(a)).collect(); + // Propagate effects from callback arguments to enclosing scope + for arg_type in &arg_types { + if let Type::Function { effects, .. } = arg_type { + for effect in &effects.effects { + if self.inferring_effects { + self.inferred_effects.insert(effect.clone()); + } + } + } + } + // Check property constraints from where clauses if let Expr::Var(func_id) = func { if let Some(constraints) = self.property_constraints.get(&func_id.name).cloned() { @@ -2061,6 +2072,18 @@ impl TypeChecker { if let Some((_, field_type)) = fields.iter().find(|(n, _)| n == &operation.name) { // It's a function call on a module field let arg_types: Vec = args.iter().map(|a| self.infer_expr(a)).collect(); + + // Propagate effects from callback arguments to enclosing scope + for arg_type in &arg_types { + if let Type::Function { effects, .. } = arg_type { + for effect in &effects.effects { + if self.inferring_effects { + self.inferred_effects.insert(effect.clone()); + } + } + } + } + let result_type = Type::var(); let expected_fn = Type::function(arg_types, result_type.clone()); @@ -2120,6 +2143,17 @@ impl TypeChecker { // Check argument types let arg_types: Vec = args.iter().map(|a| self.infer_expr(a)).collect(); + // Propagate effects from callback arguments to enclosing scope + for arg_type in &arg_types { + if let Type::Function { effects, .. } = arg_type { + for effect in &effects.effects { + if self.inferring_effects { + self.inferred_effects.insert(effect.clone()); + } + } + } + } + if arg_types.len() != op.params.len() { self.errors.push(TypeError { message: format!( diff --git a/src/types.rs b/src/types.rs index e9e1f09..4059a52 100644 --- a/src/types.rs +++ b/src/types.rs @@ -956,6 +956,14 @@ impl TypeEnv { params: vec![("path".to_string(), Type::String)], return_type: Type::Unit, }, + EffectOpDef { + name: "copy".to_string(), + params: vec![ + ("source".to_string(), Type::String), + ("dest".to_string(), Type::String), + ], + return_type: Type::Unit, + }, ], }, );