From 26b94935e9a0651cdff0d758588c3ce91345593a Mon Sep 17 00:00:00 2001 From: Brandon Lucas Date: Fri, 20 Feb 2026 11:04:33 -0500 Subject: [PATCH] feat: add File.tryRead, File.tryWrite, File.tryDelete returning Result Add safe variants of File operations that return Result instead of crashing with RuntimeError. This prevents server crashes when a file is missing or unwritable. Co-Authored-By: Claude Opus 4.6 --- Cargo.lock | 2 +- src/codegen/c_backend.rs | 117 +++++++++++++++++++++++++++++++++++++++ src/interpreter.rs | 66 ++++++++++++++++++++++ src/types.rs | 27 +++++++++ 4 files changed, 211 insertions(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index 0f949d1..9327e01 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -776,7 +776,7 @@ dependencies = [ [[package]] name = "lux" -version = "0.1.6" +version = "0.1.7" dependencies = [ "glob", "lsp-server", diff --git a/src/codegen/c_backend.rs b/src/codegen/c_backend.rs index 0693e1c..ddf1b07 100644 --- a/src/codegen/c_backend.rs +++ b/src/codegen/c_backend.rs @@ -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" { diff --git a/src/interpreter.rs b/src/interpreter.rs index 8761fd5..b62621b 100644 --- a/src/interpreter.rs +++ b/src/interpreter.rs @@ -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; diff --git a/src/types.rs b/src/types.rs index 102d7fc..0c03222 100644 --- a/src/types.rs +++ b/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], + }, + }, ], }, );