feat: add File.tryRead, File.tryWrite, File.tryDelete returning Result
Add safe variants of File operations that return Result<T, String> instead of crashing with RuntimeError. This prevents server crashes when a file is missing or unwritable. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1167,6 +1167,9 @@ impl CBackend {
|
||||
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(" Result (*tryRead)(void* env, LuxString path);");
|
||||
self.writeln(" Result (*tryWrite)(void* env, LuxString path, LuxString content);");
|
||||
self.writeln(" Result (*tryDelete)(void* env, LuxString path);");
|
||||
self.writeln(" void* env;");
|
||||
self.writeln("} LuxFileHandler;");
|
||||
self.writeln("");
|
||||
@@ -1404,6 +1407,68 @@ impl CBackend {
|
||||
self.writeln(" return result;");
|
||||
self.writeln("}");
|
||||
self.writeln("");
|
||||
self.writeln("static Result lux_file_tryRead(LuxString path) {");
|
||||
self.writeln(" Result r;");
|
||||
self.writeln(" FILE* f = fopen(path, \"r\");");
|
||||
self.writeln(" if (!f) {");
|
||||
self.writeln(" char buf[512];");
|
||||
self.writeln(" snprintf(buf, sizeof(buf), \"Failed to read file '%s': %s\", path, strerror(errno));");
|
||||
self.writeln(" size_t len = strlen(buf);");
|
||||
self.writeln(" LuxString msg = (LuxString)lux_rc_alloc(len + 1, LUX_TAG_STRING);");
|
||||
self.writeln(" memcpy(msg, buf, len + 1);");
|
||||
self.writeln(" r.tag = Result_TAG_ERR;");
|
||||
self.writeln(" r.data.err.field0 = (void*)msg;");
|
||||
self.writeln(" return r;");
|
||||
self.writeln(" }");
|
||||
self.writeln(" fseek(f, 0, SEEK_END);");
|
||||
self.writeln(" long size = ftell(f);");
|
||||
self.writeln(" fseek(f, 0, SEEK_SET);");
|
||||
self.writeln(" LuxString content = (LuxString)lux_rc_alloc(size + 1, LUX_TAG_STRING);");
|
||||
self.writeln(" size_t read_size = fread(content, 1, size, f);");
|
||||
self.writeln(" content[read_size] = '\\0';");
|
||||
self.writeln(" fclose(f);");
|
||||
self.writeln(" r.tag = Result_TAG_OK;");
|
||||
self.writeln(" r.data.ok.field0 = (void*)content;");
|
||||
self.writeln(" return r;");
|
||||
self.writeln("}");
|
||||
self.writeln("");
|
||||
self.writeln("static Result lux_file_tryWrite(LuxString path, LuxString content) {");
|
||||
self.writeln(" Result r;");
|
||||
self.writeln(" FILE* f = fopen(path, \"w\");");
|
||||
self.writeln(" if (!f) {");
|
||||
self.writeln(" char buf[512];");
|
||||
self.writeln(" snprintf(buf, sizeof(buf), \"Failed to write file '%s': %s\", path, strerror(errno));");
|
||||
self.writeln(" size_t len = strlen(buf);");
|
||||
self.writeln(" LuxString msg = (LuxString)lux_rc_alloc(len + 1, LUX_TAG_STRING);");
|
||||
self.writeln(" memcpy(msg, buf, len + 1);");
|
||||
self.writeln(" r.tag = Result_TAG_ERR;");
|
||||
self.writeln(" r.data.err.field0 = (void*)msg;");
|
||||
self.writeln(" return r;");
|
||||
self.writeln(" }");
|
||||
self.writeln(" fputs(content, f);");
|
||||
self.writeln(" fclose(f);");
|
||||
self.writeln(" r.tag = Result_TAG_OK;");
|
||||
self.writeln(" r.data.ok.field0 = NULL;");
|
||||
self.writeln(" return r;");
|
||||
self.writeln("}");
|
||||
self.writeln("");
|
||||
self.writeln("static Result lux_file_tryDelete(LuxString path) {");
|
||||
self.writeln(" Result r;");
|
||||
self.writeln(" if (remove(path) != 0) {");
|
||||
self.writeln(" char buf[512];");
|
||||
self.writeln(" snprintf(buf, sizeof(buf), \"Failed to delete file '%s': %s\", path, strerror(errno));");
|
||||
self.writeln(" size_t len = strlen(buf);");
|
||||
self.writeln(" LuxString msg = (LuxString)lux_rc_alloc(len + 1, LUX_TAG_STRING);");
|
||||
self.writeln(" memcpy(msg, buf, len + 1);");
|
||||
self.writeln(" r.tag = Result_TAG_ERR;");
|
||||
self.writeln(" r.data.err.field0 = (void*)msg;");
|
||||
self.writeln(" return r;");
|
||||
self.writeln(" }");
|
||||
self.writeln(" r.tag = Result_TAG_OK;");
|
||||
self.writeln(" r.data.ok.field0 = NULL;");
|
||||
self.writeln(" return r;");
|
||||
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);");
|
||||
@@ -1454,6 +1519,21 @@ impl CBackend {
|
||||
self.writeln(" return lux_file_glob(pattern);");
|
||||
self.writeln("}");
|
||||
self.writeln("");
|
||||
self.writeln("static Result default_file_tryRead(void* env, LuxString path) {");
|
||||
self.writeln(" (void)env;");
|
||||
self.writeln(" return lux_file_tryRead(path);");
|
||||
self.writeln("}");
|
||||
self.writeln("");
|
||||
self.writeln("static Result default_file_tryWrite(void* env, LuxString path, LuxString content) {");
|
||||
self.writeln(" (void)env;");
|
||||
self.writeln(" return lux_file_tryWrite(path, content);");
|
||||
self.writeln("}");
|
||||
self.writeln("");
|
||||
self.writeln("static Result default_file_tryDelete(void* env, LuxString path) {");
|
||||
self.writeln(" (void)env;");
|
||||
self.writeln(" return lux_file_tryDelete(path);");
|
||||
self.writeln("}");
|
||||
self.writeln("");
|
||||
self.writeln("static LuxFileHandler default_file_handler = {");
|
||||
self.writeln(" .read = default_file_read,");
|
||||
self.writeln(" .write = default_file_write,");
|
||||
@@ -1465,6 +1545,9 @@ impl CBackend {
|
||||
self.writeln(" .copy = default_file_copy,");
|
||||
self.writeln(" .readDir = default_file_readDir,");
|
||||
self.writeln(" .glob = default_file_glob,");
|
||||
self.writeln(" .tryRead = default_file_tryRead,");
|
||||
self.writeln(" .tryWrite = default_file_tryWrite,");
|
||||
self.writeln(" .tryDelete = default_file_tryDelete,");
|
||||
self.writeln(" .env = NULL");
|
||||
self.writeln("};");
|
||||
self.writeln("");
|
||||
@@ -3825,6 +3908,39 @@ impl CBackend {
|
||||
self.register_rc_var(&temp, "LuxList*");
|
||||
return Ok(temp);
|
||||
}
|
||||
"tryRead" => {
|
||||
let path = self.emit_expr(&args[0])?;
|
||||
let temp = format!("_file_tryread_{}", self.fresh_name());
|
||||
if self.has_evidence {
|
||||
self.writeln(&format!("Result {} = ev->file->tryRead(ev->file->env, {});", temp, path));
|
||||
} else {
|
||||
self.writeln(&format!("Result {} = lux_file_tryRead({});", temp, path));
|
||||
}
|
||||
return Ok(temp);
|
||||
}
|
||||
"tryWrite" => {
|
||||
let path = self.emit_expr(&args[0])?;
|
||||
let content = self.emit_expr(&args[1])?;
|
||||
if self.has_evidence {
|
||||
let temp = format!("_file_trywrite_{}", self.fresh_name());
|
||||
self.writeln(&format!("Result {} = ev->file->tryWrite(ev->file->env, {}, {});", temp, path, content));
|
||||
return Ok(temp);
|
||||
} else {
|
||||
let temp = format!("_file_trywrite_{}", self.fresh_name());
|
||||
self.writeln(&format!("Result {} = lux_file_tryWrite({}, {});", temp, path, content));
|
||||
return Ok(temp);
|
||||
}
|
||||
}
|
||||
"tryDelete" => {
|
||||
let path = self.emit_expr(&args[0])?;
|
||||
let temp = format!("_file_trydelete_{}", self.fresh_name());
|
||||
if self.has_evidence {
|
||||
self.writeln(&format!("Result {} = ev->file->tryDelete(ev->file->env, {});", temp, path));
|
||||
} else {
|
||||
self.writeln(&format!("Result {} = lux_file_tryDelete({});", temp, path));
|
||||
}
|
||||
return Ok(temp);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
@@ -5389,6 +5505,7 @@ impl CBackend {
|
||||
"write" | "append" | "delete" | "mkdir" | "copy" => Some("void".to_string()),
|
||||
"exists" | "isDir" => Some("LuxBool".to_string()),
|
||||
"readDir" | "listDir" | "glob" => Some("LuxList*".to_string()),
|
||||
"tryRead" | "tryWrite" | "tryDelete" => Some("Result".to_string()),
|
||||
_ => None,
|
||||
}
|
||||
} else if effect.name == "Http" {
|
||||
|
||||
@@ -3991,6 +3991,72 @@ impl Interpreter {
|
||||
}
|
||||
}
|
||||
|
||||
// ===== File Effect (safe Result-returning variants) =====
|
||||
("File", "tryRead") => {
|
||||
let path = match request.args.first() {
|
||||
Some(Value::String(s)) => s.clone(),
|
||||
_ => return Err(RuntimeError {
|
||||
message: "File.tryRead requires a string path".to_string(),
|
||||
span: None,
|
||||
}),
|
||||
};
|
||||
match std::fs::read_to_string(&path) {
|
||||
Ok(content) => Ok(Value::Constructor {
|
||||
name: "Ok".to_string(),
|
||||
fields: vec![Value::String(content)],
|
||||
}),
|
||||
Err(e) => Ok(Value::Constructor {
|
||||
name: "Err".to_string(),
|
||||
fields: vec![Value::String(format!("Failed to read file '{}': {}", path, e))],
|
||||
}),
|
||||
}
|
||||
}
|
||||
("File", "tryWrite") => {
|
||||
let path = match request.args.first() {
|
||||
Some(Value::String(s)) => s.clone(),
|
||||
_ => return Err(RuntimeError {
|
||||
message: "File.tryWrite requires a string path".to_string(),
|
||||
span: None,
|
||||
}),
|
||||
};
|
||||
let content = match request.args.get(1) {
|
||||
Some(Value::String(s)) => s.clone(),
|
||||
_ => return Err(RuntimeError {
|
||||
message: "File.tryWrite requires string content".to_string(),
|
||||
span: None,
|
||||
}),
|
||||
};
|
||||
match std::fs::write(&path, &content) {
|
||||
Ok(()) => Ok(Value::Constructor {
|
||||
name: "Ok".to_string(),
|
||||
fields: vec![Value::Unit],
|
||||
}),
|
||||
Err(e) => Ok(Value::Constructor {
|
||||
name: "Err".to_string(),
|
||||
fields: vec![Value::String(format!("Failed to write file '{}': {}", path, e))],
|
||||
}),
|
||||
}
|
||||
}
|
||||
("File", "tryDelete") => {
|
||||
let path = match request.args.first() {
|
||||
Some(Value::String(s)) => s.clone(),
|
||||
_ => return Err(RuntimeError {
|
||||
message: "File.tryDelete requires a string path".to_string(),
|
||||
span: None,
|
||||
}),
|
||||
};
|
||||
match std::fs::remove_file(&path) {
|
||||
Ok(()) => Ok(Value::Constructor {
|
||||
name: "Ok".to_string(),
|
||||
fields: vec![Value::Unit],
|
||||
}),
|
||||
Err(e) => Ok(Value::Constructor {
|
||||
name: "Err".to_string(),
|
||||
fields: vec![Value::String(format!("Failed to delete file '{}': {}", path, e))],
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
// ===== Process Effect =====
|
||||
("Process", "exec") => {
|
||||
use std::process::Command;
|
||||
|
||||
27
src/types.rs
27
src/types.rs
@@ -969,6 +969,33 @@ impl TypeEnv {
|
||||
params: vec![("pattern".to_string(), Type::String)],
|
||||
return_type: Type::List(Box::new(Type::String)),
|
||||
},
|
||||
EffectOpDef {
|
||||
name: "tryRead".to_string(),
|
||||
params: vec![("path".to_string(), Type::String)],
|
||||
return_type: Type::App {
|
||||
constructor: Box::new(Type::Named("Result".to_string())),
|
||||
args: vec![Type::String, Type::String],
|
||||
},
|
||||
},
|
||||
EffectOpDef {
|
||||
name: "tryWrite".to_string(),
|
||||
params: vec![
|
||||
("path".to_string(), Type::String),
|
||||
("content".to_string(), Type::String),
|
||||
],
|
||||
return_type: Type::App {
|
||||
constructor: Box::new(Type::Named("Result".to_string())),
|
||||
args: vec![Type::Unit, Type::String],
|
||||
},
|
||||
},
|
||||
EffectOpDef {
|
||||
name: "tryDelete".to_string(),
|
||||
params: vec![("path".to_string(), Type::String)],
|
||||
return_type: Type::App {
|
||||
constructor: Box::new(Type::Named("Result".to_string())),
|
||||
args: vec![Type::Unit, Type::String],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user