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 <noreply@anthropic.com>
This commit is contained in:
2026-02-19 09:24:28 -05:00
parent 52dcc88051
commit ec365ebb3f
5 changed files with 149 additions and 1 deletions

View File

@@ -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 <dirent.h>");
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,

View File

@@ -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;

View File

@@ -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<String>): 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<String>): List<String> =
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);
}
}

View File

@@ -1951,6 +1951,17 @@ impl TypeChecker {
let func_type = self.infer_expr(func);
let arg_types: Vec<Type> = 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<Type> = 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<Type> = 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!(

View File

@@ -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,
},
],
},
);