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,