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:
2026-02-13 16:09:20 -05:00
parent a037f5bd2f
commit b0f6756411
6 changed files with 508 additions and 14 deletions

View File

@@ -167,19 +167,51 @@ impl Formatter {
self.write(" =");
// Body
let body_str = self.format_expr(&func.body);
if self.is_block_expr(&func.body) || body_str.contains('\n') {
// Body - handle blocks specially to keep `= {` on same line
if let Expr::Block { statements, result, .. } = &func.body {
self.write(" {");
self.newline();
self.indent_level += 1;
for stmt in statements {
let indent = self.indent();
match stmt {
Statement::Let { name, typ, value, .. } => {
let type_str = typ.as_ref()
.map(|t| format!(": {}", self.format_type_expr(t)))
.unwrap_or_default();
self.write(&indent);
self.writeln(&format!(
"let {}{} = {}",
name.name,
type_str,
self.format_expr(value)
));
}
Statement::Expr(e) => {
self.write(&indent);
self.writeln(&self.format_expr(e));
}
}
}
self.write(&self.indent());
self.write(&body_str);
self.writeln(&self.format_expr(result));
self.indent_level -= 1;
self.write(&self.indent());
self.writeln("}");
} else {
self.write(" ");
self.write(&body_str);
let body_str = self.format_expr(&func.body);
if body_str.contains('\n') {
self.newline();
self.indent_level += 1;
self.write(&self.indent());
self.write(&body_str);
self.indent_level -= 1;
self.newline();
} else {
self.write(" ");
self.writeln(&body_str);
}
}
self.newline();
}
fn format_let(&mut self, let_decl: &LetDecl) {
@@ -674,12 +706,16 @@ impl Formatter {
)
}
Expr::Run { expr, handlers, .. } => {
let mut s = format!("run {} with {{\n", self.format_expr(expr));
for (effect, handler) in handlers {
s.push_str(&format!(" {} = {},\n", effect.name, self.format_expr(handler)));
if handlers.is_empty() {
format!("run {} with {{}}", self.format_expr(expr))
} else {
let mut s = format!("run {} with {{\n", self.format_expr(expr));
for (effect, handler) in handlers {
s.push_str(&format!(" {} = {},\n", effect.name, self.format_expr(handler)));
}
s.push('}');
s
}
s.push('}');
s
}
Expr::Resume { value, .. } => {
format!("resume({})", self.format_expr(value))