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:
@@ -1149,6 +1149,7 @@ impl CBackend {
|
|||||||
self.writeln(" void (*delete_file)(void* env, LuxString path);");
|
self.writeln(" void (*delete_file)(void* env, LuxString path);");
|
||||||
self.writeln(" LuxBool (*isDir)(void* env, LuxString path);");
|
self.writeln(" LuxBool (*isDir)(void* env, LuxString path);");
|
||||||
self.writeln(" void (*mkdir)(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(" LuxList* (*readDir)(void* env, LuxString path);");
|
||||||
self.writeln(" void* env;");
|
self.writeln(" void* env;");
|
||||||
self.writeln("} LuxFileHandler;");
|
self.writeln("} LuxFileHandler;");
|
||||||
@@ -1336,6 +1337,20 @@ impl CBackend {
|
|||||||
self.writeln(" mkdir(path, 0755);");
|
self.writeln(" mkdir(path, 0755);");
|
||||||
self.writeln("}");
|
self.writeln("}");
|
||||||
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("#include <dirent.h>");
|
||||||
self.writeln("// Forward declarations needed by lux_file_readDir");
|
self.writeln("// Forward declarations needed by lux_file_readDir");
|
||||||
self.writeln("static LuxList* lux_list_new(int64_t capacity);");
|
self.writeln("static LuxList* lux_list_new(int64_t capacity);");
|
||||||
@@ -1391,6 +1406,11 @@ impl CBackend {
|
|||||||
self.writeln(" lux_file_mkdir(path);");
|
self.writeln(" lux_file_mkdir(path);");
|
||||||
self.writeln("}");
|
self.writeln("}");
|
||||||
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("static LuxList* default_file_readDir(void* env, LuxString path) {");
|
||||||
self.writeln(" (void)env;");
|
self.writeln(" (void)env;");
|
||||||
self.writeln(" return lux_file_readDir(path);");
|
self.writeln(" return lux_file_readDir(path);");
|
||||||
@@ -1404,6 +1424,7 @@ impl CBackend {
|
|||||||
self.writeln(" .delete_file = default_file_delete,");
|
self.writeln(" .delete_file = default_file_delete,");
|
||||||
self.writeln(" .isDir = default_file_isDir,");
|
self.writeln(" .isDir = default_file_isDir,");
|
||||||
self.writeln(" .mkdir = default_file_mkdir,");
|
self.writeln(" .mkdir = default_file_mkdir,");
|
||||||
|
self.writeln(" .copy = default_file_copy,");
|
||||||
self.writeln(" .readDir = default_file_readDir,");
|
self.writeln(" .readDir = default_file_readDir,");
|
||||||
self.writeln(" .env = NULL");
|
self.writeln(" .env = NULL");
|
||||||
self.writeln("};");
|
self.writeln("};");
|
||||||
@@ -3679,6 +3700,16 @@ impl CBackend {
|
|||||||
}
|
}
|
||||||
return Ok("NULL".to_string());
|
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" => {
|
"readDir" | "listDir" => {
|
||||||
let path = self.emit_expr(&args[0])?;
|
let path = self.emit_expr(&args[0])?;
|
||||||
let temp = format!("_readdir_{}", self.fresh_name());
|
let temp = format!("_readdir_{}", self.fresh_name());
|
||||||
@@ -5175,7 +5206,7 @@ impl CBackend {
|
|||||||
} else if effect.name == "File" {
|
} else if effect.name == "File" {
|
||||||
match operation.name.as_str() {
|
match operation.name.as_str() {
|
||||||
"read" => Some("LuxString".to_string()),
|
"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()),
|
"exists" | "isDir" => Some("LuxBool".to_string()),
|
||||||
"readDir" | "listDir" => Some("LuxList*".to_string()),
|
"readDir" | "listDir" => Some("LuxList*".to_string()),
|
||||||
_ => None,
|
_ => None,
|
||||||
|
|||||||
@@ -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 Effect =====
|
||||||
("Process", "exec") => {
|
("Process", "exec") => {
|
||||||
use std::process::Command;
|
use std::process::Command;
|
||||||
|
|||||||
51
src/main.rs
51
src/main.rs
@@ -5629,4 +5629,55 @@ c")"#;
|
|||||||
"#;
|
"#;
|
||||||
assert_eq!(eval(source).unwrap(), "Some(30)");
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1951,6 +1951,17 @@ impl TypeChecker {
|
|||||||
let func_type = self.infer_expr(func);
|
let func_type = self.infer_expr(func);
|
||||||
let arg_types: Vec<Type> = args.iter().map(|a| self.infer_expr(a)).collect();
|
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
|
// Check property constraints from where clauses
|
||||||
if let Expr::Var(func_id) = func {
|
if let Expr::Var(func_id) = func {
|
||||||
if let Some(constraints) = self.property_constraints.get(&func_id.name).cloned() {
|
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) {
|
if let Some((_, field_type)) = fields.iter().find(|(n, _)| n == &operation.name) {
|
||||||
// It's a function call on a module field
|
// It's a function call on a module field
|
||||||
let arg_types: Vec<Type> = args.iter().map(|a| self.infer_expr(a)).collect();
|
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 result_type = Type::var();
|
||||||
let expected_fn = Type::function(arg_types, result_type.clone());
|
let expected_fn = Type::function(arg_types, result_type.clone());
|
||||||
|
|
||||||
@@ -2120,6 +2143,17 @@ impl TypeChecker {
|
|||||||
// Check argument types
|
// Check argument types
|
||||||
let arg_types: Vec<Type> = args.iter().map(|a| self.infer_expr(a)).collect();
|
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() {
|
if arg_types.len() != op.params.len() {
|
||||||
self.errors.push(TypeError {
|
self.errors.push(TypeError {
|
||||||
message: format!(
|
message: format!(
|
||||||
|
|||||||
@@ -956,6 +956,14 @@ impl TypeEnv {
|
|||||||
params: vec![("path".to_string(), Type::String)],
|
params: vec![("path".to_string(), Type::String)],
|
||||||
return_type: Type::Unit,
|
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,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user