feat: add File and Process effects for real I/O
Adds two essential effects that enable Lux to interact with the system:
File effect:
- read(path) - Read file contents as string
- write(path, content) - Write string to file
- append(path, content) - Append to file
- exists(path) - Check if file/directory exists
- delete(path) - Delete a file
- readDir(path) - List directory contents
- isDir(path) - Check if path is directory
- mkdir(path) - Create directory (including parents)
Process effect:
- exec(cmd) - Run shell command, return stdout
- execStatus(cmd) - Run command, return exit code
- env(name) - Get environment variable (returns Option)
- args() - Get command line arguments
- exit(code) - Exit program with code
- cwd() - Get current working directory
- setCwd(path) - Change working directory
Also fixes formatter bug with empty handler blocks in `run ... with {}`.
These effects make Lux capable of writing real CLI tools and scripts.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -2638,6 +2638,253 @@ impl Interpreter {
|
||||
thread::sleep(Duration::from_millis(ms));
|
||||
Ok(Value::Unit)
|
||||
}
|
||||
|
||||
// ===== File Effect =====
|
||||
("File", "read") => {
|
||||
let path = match request.args.first() {
|
||||
Some(Value::String(s)) => s.clone(),
|
||||
_ => return Err(RuntimeError {
|
||||
message: "File.read requires a string path".to_string(),
|
||||
span: None,
|
||||
}),
|
||||
};
|
||||
match std::fs::read_to_string(&path) {
|
||||
Ok(content) => Ok(Value::String(content)),
|
||||
Err(e) => Err(RuntimeError {
|
||||
message: format!("Failed to read file '{}': {}", path, e),
|
||||
span: None,
|
||||
}),
|
||||
}
|
||||
}
|
||||
("File", "write") => {
|
||||
let path = match request.args.first() {
|
||||
Some(Value::String(s)) => s.clone(),
|
||||
_ => return Err(RuntimeError {
|
||||
message: "File.write 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.write requires string content".to_string(),
|
||||
span: None,
|
||||
}),
|
||||
};
|
||||
match std::fs::write(&path, &content) {
|
||||
Ok(()) => Ok(Value::Unit),
|
||||
Err(e) => Err(RuntimeError {
|
||||
message: format!("Failed to write file '{}': {}", path, e),
|
||||
span: None,
|
||||
}),
|
||||
}
|
||||
}
|
||||
("File", "append") => {
|
||||
use std::fs::OpenOptions;
|
||||
use std::io::Write;
|
||||
let path = match request.args.first() {
|
||||
Some(Value::String(s)) => s.clone(),
|
||||
_ => return Err(RuntimeError {
|
||||
message: "File.append 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.append requires string content".to_string(),
|
||||
span: None,
|
||||
}),
|
||||
};
|
||||
match OpenOptions::new().create(true).append(true).open(&path) {
|
||||
Ok(mut file) => {
|
||||
file.write_all(content.as_bytes()).map_err(|e| RuntimeError {
|
||||
message: format!("Failed to append to file '{}': {}", path, e),
|
||||
span: None,
|
||||
})?;
|
||||
Ok(Value::Unit)
|
||||
}
|
||||
Err(e) => Err(RuntimeError {
|
||||
message: format!("Failed to open file '{}': {}", path, e),
|
||||
span: None,
|
||||
}),
|
||||
}
|
||||
}
|
||||
("File", "exists") => {
|
||||
let path = match request.args.first() {
|
||||
Some(Value::String(s)) => s.clone(),
|
||||
_ => return Err(RuntimeError {
|
||||
message: "File.exists requires a string path".to_string(),
|
||||
span: None,
|
||||
}),
|
||||
};
|
||||
Ok(Value::Bool(std::path::Path::new(&path).exists()))
|
||||
}
|
||||
("File", "delete") => {
|
||||
let path = match request.args.first() {
|
||||
Some(Value::String(s)) => s.clone(),
|
||||
_ => return Err(RuntimeError {
|
||||
message: "File.delete requires a string path".to_string(),
|
||||
span: None,
|
||||
}),
|
||||
};
|
||||
match std::fs::remove_file(&path) {
|
||||
Ok(()) => Ok(Value::Unit),
|
||||
Err(e) => Err(RuntimeError {
|
||||
message: format!("Failed to delete file '{}': {}", path, e),
|
||||
span: None,
|
||||
}),
|
||||
}
|
||||
}
|
||||
("File", "readDir") => {
|
||||
let path = match request.args.first() {
|
||||
Some(Value::String(s)) => s.clone(),
|
||||
_ => return Err(RuntimeError {
|
||||
message: "File.readDir requires a string path".to_string(),
|
||||
span: None,
|
||||
}),
|
||||
};
|
||||
match std::fs::read_dir(&path) {
|
||||
Ok(entries) => {
|
||||
let files: Vec<Value> = entries
|
||||
.filter_map(|e| e.ok())
|
||||
.map(|e| Value::String(e.file_name().to_string_lossy().to_string()))
|
||||
.collect();
|
||||
Ok(Value::List(files))
|
||||
}
|
||||
Err(e) => Err(RuntimeError {
|
||||
message: format!("Failed to read directory '{}': {}", path, e),
|
||||
span: None,
|
||||
}),
|
||||
}
|
||||
}
|
||||
("File", "isDir") => {
|
||||
let path = match request.args.first() {
|
||||
Some(Value::String(s)) => s.clone(),
|
||||
_ => return Err(RuntimeError {
|
||||
message: "File.isDir requires a string path".to_string(),
|
||||
span: None,
|
||||
}),
|
||||
};
|
||||
Ok(Value::Bool(std::path::Path::new(&path).is_dir()))
|
||||
}
|
||||
("File", "mkdir") => {
|
||||
let path = match request.args.first() {
|
||||
Some(Value::String(s)) => s.clone(),
|
||||
_ => return Err(RuntimeError {
|
||||
message: "File.mkdir requires a string path".to_string(),
|
||||
span: None,
|
||||
}),
|
||||
};
|
||||
match std::fs::create_dir_all(&path) {
|
||||
Ok(()) => Ok(Value::Unit),
|
||||
Err(e) => Err(RuntimeError {
|
||||
message: format!("Failed to create directory '{}': {}", path, e),
|
||||
span: None,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
// ===== Process Effect =====
|
||||
("Process", "exec") => {
|
||||
use std::process::Command;
|
||||
let cmd = match request.args.first() {
|
||||
Some(Value::String(s)) => s.clone(),
|
||||
_ => return Err(RuntimeError {
|
||||
message: "Process.exec requires a string command".to_string(),
|
||||
span: None,
|
||||
}),
|
||||
};
|
||||
match Command::new("sh").arg("-c").arg(&cmd).output() {
|
||||
Ok(output) => {
|
||||
let stdout = String::from_utf8_lossy(&output.stdout).to_string();
|
||||
Ok(Value::String(stdout))
|
||||
}
|
||||
Err(e) => Err(RuntimeError {
|
||||
message: format!("Failed to execute command: {}", e),
|
||||
span: None,
|
||||
}),
|
||||
}
|
||||
}
|
||||
("Process", "execStatus") => {
|
||||
use std::process::Command;
|
||||
let cmd = match request.args.first() {
|
||||
Some(Value::String(s)) => s.clone(),
|
||||
_ => return Err(RuntimeError {
|
||||
message: "Process.execStatus requires a string command".to_string(),
|
||||
span: None,
|
||||
}),
|
||||
};
|
||||
match Command::new("sh").arg("-c").arg(&cmd).status() {
|
||||
Ok(status) => {
|
||||
let code = status.code().unwrap_or(-1) as i64;
|
||||
Ok(Value::Int(code))
|
||||
}
|
||||
Err(e) => Err(RuntimeError {
|
||||
message: format!("Failed to execute command: {}", e),
|
||||
span: None,
|
||||
}),
|
||||
}
|
||||
}
|
||||
("Process", "env") => {
|
||||
let var_name = match request.args.first() {
|
||||
Some(Value::String(s)) => s.clone(),
|
||||
_ => return Err(RuntimeError {
|
||||
message: "Process.env requires a string name".to_string(),
|
||||
span: None,
|
||||
}),
|
||||
};
|
||||
match std::env::var(&var_name) {
|
||||
Ok(value) => Ok(Value::Constructor {
|
||||
name: "Some".to_string(),
|
||||
fields: vec![Value::String(value)],
|
||||
}),
|
||||
Err(_) => Ok(Value::Constructor {
|
||||
name: "None".to_string(),
|
||||
fields: vec![],
|
||||
}),
|
||||
}
|
||||
}
|
||||
("Process", "args") => {
|
||||
let args: Vec<Value> = std::env::args()
|
||||
.skip(1) // Skip the program name
|
||||
.map(Value::String)
|
||||
.collect();
|
||||
Ok(Value::List(args))
|
||||
}
|
||||
("Process", "exit") => {
|
||||
let code = match request.args.first() {
|
||||
Some(Value::Int(n)) => *n as i32,
|
||||
_ => 0,
|
||||
};
|
||||
std::process::exit(code);
|
||||
}
|
||||
("Process", "cwd") => {
|
||||
match std::env::current_dir() {
|
||||
Ok(path) => Ok(Value::String(path.to_string_lossy().to_string())),
|
||||
Err(e) => Err(RuntimeError {
|
||||
message: format!("Failed to get current directory: {}", e),
|
||||
span: None,
|
||||
}),
|
||||
}
|
||||
}
|
||||
("Process", "setCwd") => {
|
||||
let path = match request.args.first() {
|
||||
Some(Value::String(s)) => s.clone(),
|
||||
_ => return Err(RuntimeError {
|
||||
message: "Process.setCwd requires a string path".to_string(),
|
||||
span: None,
|
||||
}),
|
||||
};
|
||||
match std::env::set_current_dir(&path) {
|
||||
Ok(()) => Ok(Value::Unit),
|
||||
Err(e) => Err(RuntimeError {
|
||||
message: format!("Failed to change directory to '{}': {}", path, e),
|
||||
span: None,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
_ => Err(RuntimeError {
|
||||
message: format!(
|
||||
"Unhandled effect operation: {}.{}",
|
||||
|
||||
Reference in New Issue
Block a user