feat: add Random, Time, and File effects to C backend

New effects with evidence passing support:

Random effect:
- int(min, max) - random integer in range
- float() - random float 0-1
- bool() - random boolean

Time effect:
- now() - milliseconds since epoch
- sleep(ms) - pause execution

File effect:
- read(path) - read file contents
- write(path, content) - write file
- append(path, content) - append to file
- exists(path) - check if file exists
- delete(path) - delete file
- isDir(path) - check if directory
- mkdir(path) - create directory

Also fixed:
- Function calls as statements now properly emit in generated C
- Return type inference for all effect operations

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-02-14 12:08:25 -05:00
parent 52cb38805a
commit 3c7d72f663
2 changed files with 392 additions and 13 deletions

View File

@@ -275,7 +275,7 @@ See [docs/EVIDENCE_PASSING.md](EVIDENCE_PASSING.md) for details.
## Future Roadmap ## Future Roadmap
### Phase 2: Perceus Reference Counting ### Phase 4: Perceus Reference Counting
**Goal:** Deterministic memory management without GC pauses. **Goal:** Deterministic memory management without GC pauses.
@@ -292,15 +292,22 @@ fn increment(xs: List<Int>): List<Int> =
If `xs` has refcount=1, the list can be mutated in-place instead of copied. If `xs` has refcount=1, the list can be mutated in-place instead of copied.
### Phase 3: More Effects ### Phase 2: More Effects ✅ COMPLETE
Implement C versions of: Implemented C versions of:
- `File` (read, write, exists) - `Random` (int, float, bool) - LCG random number generator
- `Http` (get, post) - `Time` (now, sleep) - using clock_gettime/nanosleep
- `Random` (int, bool) - `File` (read, write, append, exists, delete, isDir, mkdir)
- `Time` (now, sleep)
### Phase 4: JavaScript Backend All effects use evidence passing for handler customization.
### Phase 3: Http Effect
Implement HTTP client:
- `Http.get(url)` - GET request
- `Http.post(url, body)` - POST request
### Phase 5: JavaScript Backend
Compile Lux to JavaScript for browser/Node.js: Compile Lux to JavaScript for browser/Node.js:
- Effects → Direct DOM/API calls - Effects → Direct DOM/API calls

View File

@@ -440,12 +440,42 @@ impl CBackend {
self.writeln(" void* env;"); self.writeln(" void* env;");
self.writeln("} LuxReaderHandler;"); self.writeln("} LuxReaderHandler;");
self.writeln(""); self.writeln("");
self.writeln("// Handler struct for Random effect");
self.writeln("typedef struct {");
self.writeln(" LuxInt (*randInt)(void* env, LuxInt min, LuxInt max);");
self.writeln(" LuxFloat (*randFloat)(void* env);");
self.writeln(" LuxBool (*randBool)(void* env);");
self.writeln(" void* env;");
self.writeln("} LuxRandomHandler;");
self.writeln("");
self.writeln("// Handler struct for Time effect");
self.writeln("typedef struct {");
self.writeln(" LuxInt (*now)(void* env);");
self.writeln(" void (*sleep)(void* env, LuxInt ms);");
self.writeln(" void* env;");
self.writeln("} LuxTimeHandler;");
self.writeln("");
self.writeln("// Handler struct for File effect");
self.writeln("typedef struct {");
self.writeln(" LuxString (*read)(void* env, LuxString path);");
self.writeln(" void (*write)(void* env, LuxString path, LuxString content);");
self.writeln(" void (*append)(void* env, LuxString path, LuxString content);");
self.writeln(" LuxBool (*exists)(void* env, LuxString path);");
self.writeln(" void (*delete_file)(void* env, LuxString path);");
self.writeln(" LuxBool (*isDir)(void* env, LuxString path);");
self.writeln(" void (*mkdir)(void* env, LuxString path);");
self.writeln(" void* env;");
self.writeln("} LuxFileHandler;");
self.writeln("");
self.writeln("// Evidence struct - passed to effectful functions"); self.writeln("// Evidence struct - passed to effectful functions");
self.writeln("// Contains pointers to current handlers for each effect type"); self.writeln("// Contains pointers to current handlers for each effect type");
self.writeln("typedef struct {"); self.writeln("typedef struct {");
self.writeln(" LuxConsoleHandler* console;"); self.writeln(" LuxConsoleHandler* console;");
self.writeln(" LuxStateHandler* state;"); self.writeln(" LuxStateHandler* state;");
self.writeln(" LuxReaderHandler* reader;"); self.writeln(" LuxReaderHandler* reader;");
self.writeln(" LuxRandomHandler* random;");
self.writeln(" LuxTimeHandler* time;");
self.writeln(" LuxFileHandler* file;");
self.writeln("} LuxEvidence;"); self.writeln("} LuxEvidence;");
self.writeln(""); self.writeln("");
self.writeln("// Default Console handler using built-in implementations"); self.writeln("// Default Console handler using built-in implementations");
@@ -465,11 +495,191 @@ impl CBackend {
self.writeln(" .env = NULL"); self.writeln(" .env = NULL");
self.writeln("};"); self.writeln("};");
self.writeln(""); self.writeln("");
self.writeln("// === Random Effect Built-in Implementations ===");
self.writeln("");
self.writeln("static unsigned int lux_rand_state = 12345;");
self.writeln("");
self.writeln("static LuxInt lux_random_int(LuxInt min, LuxInt max) {");
self.writeln(" // Simple LCG random number generator");
self.writeln(" lux_rand_state = lux_rand_state * 1103515245 + 12345;");
self.writeln(" LuxInt range = max - min + 1;");
self.writeln(" if (range <= 0) return min;");
self.writeln(" return min + (LuxInt)((lux_rand_state >> 16) % range);");
self.writeln("}");
self.writeln("");
self.writeln("static LuxFloat lux_random_float(void) {");
self.writeln(" lux_rand_state = lux_rand_state * 1103515245 + 12345;");
self.writeln(" return (double)(lux_rand_state >> 16) / 32767.0;");
self.writeln("}");
self.writeln("");
self.writeln("static LuxBool lux_random_bool(void) {");
self.writeln(" return lux_random_int(0, 1) == 1;");
self.writeln("}");
self.writeln("");
self.writeln("static LuxInt default_random_int(void* env, LuxInt min, LuxInt max) {");
self.writeln(" (void)env;");
self.writeln(" return lux_random_int(min, max);");
self.writeln("}");
self.writeln("");
self.writeln("static LuxFloat default_random_float(void* env) {");
self.writeln(" (void)env;");
self.writeln(" return lux_random_float();");
self.writeln("}");
self.writeln("");
self.writeln("static LuxBool default_random_bool(void* env) {");
self.writeln(" (void)env;");
self.writeln(" return lux_random_bool();");
self.writeln("}");
self.writeln("");
self.writeln("static LuxRandomHandler default_random_handler = {");
self.writeln(" .randInt = default_random_int,");
self.writeln(" .randFloat = default_random_float,");
self.writeln(" .randBool = default_random_bool,");
self.writeln(" .env = NULL");
self.writeln("};");
self.writeln("");
self.writeln("// === Time Effect Built-in Implementations ===");
self.writeln("");
self.writeln("#include <time.h>");
self.writeln("");
self.writeln("static LuxInt lux_time_now(void) {");
self.writeln(" struct timespec ts;");
self.writeln(" clock_gettime(CLOCK_REALTIME, &ts);");
self.writeln(" return (LuxInt)(ts.tv_sec * 1000 + ts.tv_nsec / 1000000);");
self.writeln("}");
self.writeln("");
self.writeln("static void lux_time_sleep(LuxInt ms) {");
self.writeln(" struct timespec ts;");
self.writeln(" ts.tv_sec = ms / 1000;");
self.writeln(" ts.tv_nsec = (ms % 1000) * 1000000;");
self.writeln(" nanosleep(&ts, NULL);");
self.writeln("}");
self.writeln("");
self.writeln("static LuxInt default_time_now(void* env) {");
self.writeln(" (void)env;");
self.writeln(" return lux_time_now();");
self.writeln("}");
self.writeln("");
self.writeln("static void default_time_sleep(void* env, LuxInt ms) {");
self.writeln(" (void)env;");
self.writeln(" lux_time_sleep(ms);");
self.writeln("}");
self.writeln("");
self.writeln("static LuxTimeHandler default_time_handler = {");
self.writeln(" .now = default_time_now,");
self.writeln(" .sleep = default_time_sleep,");
self.writeln(" .env = NULL");
self.writeln("};");
self.writeln("");
self.writeln("// === File Effect Built-in Implementations ===");
self.writeln("");
self.writeln("#include <sys/stat.h>");
self.writeln("#include <unistd.h>");
self.writeln("#include <errno.h>");
self.writeln("");
self.writeln("static LuxString lux_file_read(LuxString path) {");
self.writeln(" FILE* f = fopen(path, \"r\");");
self.writeln(" if (!f) return strdup(\"\");");
self.writeln(" fseek(f, 0, SEEK_END);");
self.writeln(" long size = ftell(f);");
self.writeln(" fseek(f, 0, SEEK_SET);");
self.writeln(" char* content = malloc(size + 1);");
self.writeln(" size_t read_size = fread(content, 1, size, f);");
self.writeln(" content[read_size] = '\\0';");
self.writeln(" fclose(f);");
self.writeln(" return content;");
self.writeln("}");
self.writeln("");
self.writeln("static void lux_file_write(LuxString path, LuxString content) {");
self.writeln(" FILE* f = fopen(path, \"w\");");
self.writeln(" if (f) {");
self.writeln(" fputs(content, f);");
self.writeln(" fclose(f);");
self.writeln(" }");
self.writeln("}");
self.writeln("");
self.writeln("static void lux_file_append(LuxString path, LuxString content) {");
self.writeln(" FILE* f = fopen(path, \"a\");");
self.writeln(" if (f) {");
self.writeln(" fputs(content, f);");
self.writeln(" fclose(f);");
self.writeln(" }");
self.writeln("}");
self.writeln("");
self.writeln("static LuxBool lux_file_exists(LuxString path) {");
self.writeln(" return access(path, F_OK) == 0;");
self.writeln("}");
self.writeln("");
self.writeln("static void lux_file_delete(LuxString path) {");
self.writeln(" remove(path);");
self.writeln("}");
self.writeln("");
self.writeln("static LuxBool lux_file_isDir(LuxString path) {");
self.writeln(" struct stat st;");
self.writeln(" if (stat(path, &st) == 0) {");
self.writeln(" return S_ISDIR(st.st_mode);");
self.writeln(" }");
self.writeln(" return false;");
self.writeln("}");
self.writeln("");
self.writeln("static void lux_file_mkdir(LuxString path) {");
self.writeln(" mkdir(path, 0755);");
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);");
self.writeln("}");
self.writeln("");
self.writeln("static void default_file_write(void* env, LuxString path, LuxString content) {");
self.writeln(" (void)env;");
self.writeln(" lux_file_write(path, content);");
self.writeln("}");
self.writeln("");
self.writeln("static void default_file_append(void* env, LuxString path, LuxString content) {");
self.writeln(" (void)env;");
self.writeln(" lux_file_append(path, content);");
self.writeln("}");
self.writeln("");
self.writeln("static LuxBool default_file_exists(void* env, LuxString path) {");
self.writeln(" (void)env;");
self.writeln(" return lux_file_exists(path);");
self.writeln("}");
self.writeln("");
self.writeln("static void default_file_delete(void* env, LuxString path) {");
self.writeln(" (void)env;");
self.writeln(" lux_file_delete(path);");
self.writeln("}");
self.writeln("");
self.writeln("static LuxBool default_file_isDir(void* env, LuxString path) {");
self.writeln(" (void)env;");
self.writeln(" return lux_file_isDir(path);");
self.writeln("}");
self.writeln("");
self.writeln("static void default_file_mkdir(void* env, LuxString path) {");
self.writeln(" (void)env;");
self.writeln(" lux_file_mkdir(path);");
self.writeln("}");
self.writeln("");
self.writeln("static LuxFileHandler default_file_handler = {");
self.writeln(" .read = default_file_read,");
self.writeln(" .write = default_file_write,");
self.writeln(" .append = default_file_append,");
self.writeln(" .exists = default_file_exists,");
self.writeln(" .delete_file = default_file_delete,");
self.writeln(" .isDir = default_file_isDir,");
self.writeln(" .mkdir = default_file_mkdir,");
self.writeln(" .env = NULL");
self.writeln("};");
self.writeln("");
self.writeln("// Default evidence with built-in handlers"); self.writeln("// Default evidence with built-in handlers");
self.writeln("static LuxEvidence default_evidence = {"); self.writeln("static LuxEvidence default_evidence = {");
self.writeln(" .console = &default_console_handler,"); self.writeln(" .console = &default_console_handler,");
self.writeln(" .state = NULL,"); self.writeln(" .state = NULL,");
self.writeln(" .reader = NULL"); self.writeln(" .reader = NULL,");
self.writeln(" .random = &default_random_handler,");
self.writeln(" .time = &default_time_handler,");
self.writeln(" .file = &default_file_handler");
self.writeln("};"); self.writeln("};");
self.writeln(""); self.writeln("");
self.writeln("// === List Types ==="); self.writeln("// === List Types ===");
@@ -1028,7 +1238,22 @@ impl CBackend {
self.writeln(&format!("{} {} = {};", typ, name.name, val)); self.writeln(&format!("{} {} = {};", typ, name.name, val));
} }
Statement::Expr(e) => { Statement::Expr(e) => {
let _ = self.emit_expr(e)?; // Emit expression - if it's a function call that returns void/unit,
// we need to emit it as a statement
let expr_str = self.emit_expr(e)?;
// If emit_expr didn't write the call itself (for non-void returns),
// and it's a function call, emit it as a statement
if !expr_str.is_empty() && expr_str != "NULL" {
// Check if this is a function call expression
if let Expr::Call { func, .. } = e {
if let Expr::Var(ident) = func.as_ref() {
if self.functions.contains(&ident.name) {
// It's a function call - emit as statement
self.writeln(&format!("{};", expr_str));
}
}
}
}
} }
} }
} }
@@ -1048,10 +1273,8 @@ impl CBackend {
if operation.name == "print" { if operation.name == "print" {
let arg = self.emit_expr(&args[0])?; let arg = self.emit_expr(&args[0])?;
if self.has_evidence { if self.has_evidence {
// Use evidence passing
self.writeln(&format!("ev->console->print(ev->console->env, {});", arg)); self.writeln(&format!("ev->console->print(ev->console->env, {});", arg));
} else { } else {
// Fallback to direct call
self.writeln(&format!("lux_console_print({});", arg)); self.writeln(&format!("lux_console_print({});", arg));
} }
return Ok("NULL".to_string()); return Ok("NULL".to_string());
@@ -1064,7 +1287,129 @@ impl CBackend {
} }
} }
// For other effects, emit evidence-passing call // Built-in Random effect
if effect.name == "Random" {
match operation.name.as_str() {
"int" => {
let min = self.emit_expr(&args[0])?;
let max = self.emit_expr(&args[1])?;
if self.has_evidence {
return Ok(format!("ev->random->randInt(ev->random->env, {}, {})", min, max));
} else {
return Ok(format!("lux_random_int({}, {})", min, max));
}
}
"float" => {
if self.has_evidence {
return Ok("ev->random->randFloat(ev->random->env)".to_string());
} else {
return Ok("lux_random_float()".to_string());
}
}
"bool" => {
if self.has_evidence {
return Ok("ev->random->randBool(ev->random->env)".to_string());
} else {
return Ok("lux_random_bool()".to_string());
}
}
_ => {}
}
}
// Built-in Time effect
if effect.name == "Time" {
match operation.name.as_str() {
"now" => {
if self.has_evidence {
return Ok("ev->time->now(ev->time->env)".to_string());
} else {
return Ok("lux_time_now()".to_string());
}
}
"sleep" => {
let ms = self.emit_expr(&args[0])?;
if self.has_evidence {
self.writeln(&format!("ev->time->sleep(ev->time->env, {});", ms));
} else {
self.writeln(&format!("lux_time_sleep({});", ms));
}
return Ok("NULL".to_string());
}
_ => {}
}
}
// Built-in File effect
if effect.name == "File" {
match operation.name.as_str() {
"read" => {
let path = self.emit_expr(&args[0])?;
if self.has_evidence {
return Ok(format!("ev->file->read(ev->file->env, {})", path));
} else {
return Ok(format!("lux_file_read({})", path));
}
}
"write" => {
let path = self.emit_expr(&args[0])?;
let content = self.emit_expr(&args[1])?;
if self.has_evidence {
self.writeln(&format!("ev->file->write(ev->file->env, {}, {});", path, content));
} else {
self.writeln(&format!("lux_file_write({}, {});", path, content));
}
return Ok("NULL".to_string());
}
"append" => {
let path = self.emit_expr(&args[0])?;
let content = self.emit_expr(&args[1])?;
if self.has_evidence {
self.writeln(&format!("ev->file->append(ev->file->env, {}, {});", path, content));
} else {
self.writeln(&format!("lux_file_append({}, {});", path, content));
}
return Ok("NULL".to_string());
}
"exists" => {
let path = self.emit_expr(&args[0])?;
if self.has_evidence {
return Ok(format!("ev->file->exists(ev->file->env, {})", path));
} else {
return Ok(format!("lux_file_exists({})", path));
}
}
"delete" => {
let path = self.emit_expr(&args[0])?;
if self.has_evidence {
self.writeln(&format!("ev->file->delete_file(ev->file->env, {});", path));
} else {
self.writeln(&format!("lux_file_delete({});", path));
}
return Ok("NULL".to_string());
}
"isDir" => {
let path = self.emit_expr(&args[0])?;
if self.has_evidence {
return Ok(format!("ev->file->isDir(ev->file->env, {})", path));
} else {
return Ok(format!("lux_file_isDir({})", path));
}
}
"mkdir" => {
let path = self.emit_expr(&args[0])?;
if self.has_evidence {
self.writeln(&format!("ev->file->mkdir(ev->file->env, {});", path));
} else {
self.writeln(&format!("lux_file_mkdir({});", path));
}
return Ok("NULL".to_string());
}
_ => {}
}
}
// For other effects, emit generic evidence-passing call
let arg_strs: Result<Vec<_>, _> = args.iter().map(|a| self.emit_expr(a)).collect(); let arg_strs: Result<Vec<_>, _> = args.iter().map(|a| self.emit_expr(a)).collect();
if self.has_evidence { if self.has_evidence {
Ok(format!("ev->{}->{}(ev->{}->env{}{})", Ok(format!("ev->{}->{}(ev->{}->env{}{})",
@@ -1550,6 +1895,33 @@ impl CBackend {
"isEmpty" | "any" | "all" => Some("LuxBool".to_string()), "isEmpty" | "any" | "all" => Some("LuxBool".to_string()),
_ => None, _ => None,
} }
} else if effect.name == "Random" {
match operation.name.as_str() {
"int" => Some("LuxInt".to_string()),
"float" => Some("LuxFloat".to_string()),
"bool" => Some("LuxBool".to_string()),
_ => None,
}
} else if effect.name == "Time" {
match operation.name.as_str() {
"now" => Some("LuxInt".to_string()),
"sleep" => Some("void".to_string()),
_ => None,
}
} else if effect.name == "File" {
match operation.name.as_str() {
"read" => Some("LuxString".to_string()),
"write" | "append" | "delete" | "mkdir" => Some("void".to_string()),
"exists" | "isDir" => Some("LuxBool".to_string()),
_ => None,
}
} else if effect.name == "Console" {
match operation.name.as_str() {
"print" => Some("void".to_string()),
"readLine" => Some("LuxString".to_string()),
"readInt" => Some("LuxInt".to_string()),
_ => None,
}
} else { } else {
None None
} }