feat: add effects to JS backend and fix code generation bugs

Phase 2 of JS backend: implement effect handlers in runtime

Effects added:
- Console: print, readLine, readInt
- Random: int, bool, float
- Time: now, sleep
- Http: get, post, postJson (async with fetch)

Bug fixes:
- Fix if-else with blocks executing both branches (use if-else
  statement instead of ternary for branches with statements)
- Fix main function being called twice when top-level let binding
  already invokes it
- Fix List module operations incorrectly treated as effect operations

New tests:
- test_js_random_int
- test_js_random_bool
- test_js_random_float
- test_js_time_now

All 19 JS backend tests pass.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-02-14 21:33:57 -05:00
parent ce4344810d
commit 38bf825134

View File

@@ -130,16 +130,27 @@ impl JsBackend {
} }
} }
// Check if any top-level let calls main (to avoid double invocation)
let has_main_call = program.declarations.iter().any(|decl| {
if let Declaration::Let(l) = decl {
self.expr_calls_main(&l.value)
} else {
false
}
});
// Emit top-level let bindings and expressions // Emit top-level let bindings and expressions
if program.declarations.iter().any(|d| matches!(d, Declaration::Let(_))) {
self.writeln("// Top-level bindings"); self.writeln("// Top-level bindings");
for decl in &program.declarations { for decl in &program.declarations {
if let Declaration::Let(l) = decl { if let Declaration::Let(l) = decl {
self.emit_top_level_let(l)?; self.emit_top_level_let(l)?;
} }
} }
}
// Emit main function call if it exists // Emit main function call if it exists and not already called
if self.functions.contains("main") { if self.functions.contains("main") && !has_main_call {
self.writeln(""); self.writeln("");
self.writeln("// Entry point"); self.writeln("// Entry point");
if self.effectful_functions.contains("main") { if self.effectful_functions.contains("main") {
@@ -176,18 +187,107 @@ impl JsBackend {
// Default handlers for effects // Default handlers for effects
self.writeln("defaultHandlers: {"); self.writeln("defaultHandlers: {");
self.indent += 1; self.indent += 1;
// Console effect
self.writeln("Console: {"); self.writeln("Console: {");
self.indent += 1; self.indent += 1;
self.writeln("print: (msg) => console.log(msg),"); self.writeln("print: (msg) => console.log(msg),");
self.writeln("readLine: () => { throw new Error('readLine not supported in browser'); },"); self.writeln("readLine: () => {");
self.writeln("readInt: () => { throw new Error('readInt not supported in browser'); }");
self.indent -= 1;
self.writeln("},");
self.writeln("Random: {");
self.indent += 1; self.indent += 1;
self.writeln("int: (min, max) => Math.floor(Math.random() * (max - min + 1)) + min"); self.writeln("if (typeof require !== 'undefined') {");
self.indent += 1;
self.writeln("const readline = require('readline');");
self.writeln("const rl = readline.createInterface({ input: process.stdin, output: process.stdout });");
self.writeln("return new Promise(resolve => rl.question('', answer => { rl.close(); resolve(answer); }));");
self.indent -= 1; self.indent -= 1;
self.writeln("}"); self.writeln("}");
self.writeln("return prompt('') || '';");
self.indent -= 1;
self.writeln("},");
self.writeln("readInt: () => parseInt(Lux.defaultHandlers.Console.readLine(), 10)");
self.indent -= 1;
self.writeln("},");
// Random effect
self.writeln("Random: {");
self.indent += 1;
self.writeln("int: (min, max) => Math.floor(Math.random() * (max - min + 1)) + min,");
self.writeln("bool: () => Math.random() < 0.5,");
self.writeln("float: () => Math.random()");
self.indent -= 1;
self.writeln("},");
// Time effect
self.writeln("Time: {");
self.indent += 1;
self.writeln("now: () => Date.now(),");
self.writeln("sleep: (ms) => new Promise(resolve => setTimeout(resolve, ms))");
self.indent -= 1;
self.writeln("},");
// Http effect (browser/Node compatible)
self.writeln("Http: {");
self.indent += 1;
self.writeln("get: async (url) => {");
self.indent += 1;
self.writeln("try {");
self.indent += 1;
self.writeln("const response = await fetch(url);");
self.writeln("const body = await response.text();");
self.writeln("const headers = [];");
self.writeln("response.headers.forEach((v, k) => headers.push([k, v]));");
self.writeln("return Lux.Ok({ status: response.status, body, headers });");
self.indent -= 1;
self.writeln("} catch (e) {");
self.indent += 1;
self.writeln("return Lux.Err(e.message);");
self.indent -= 1;
self.writeln("}");
self.indent -= 1;
self.writeln("},");
self.writeln("post: async (url, body) => {");
self.indent += 1;
self.writeln("try {");
self.indent += 1;
self.writeln("const response = await fetch(url, { method: 'POST', body });");
self.writeln("const respBody = await response.text();");
self.writeln("const headers = [];");
self.writeln("response.headers.forEach((v, k) => headers.push([k, v]));");
self.writeln("return Lux.Ok({ status: response.status, body: respBody, headers });");
self.indent -= 1;
self.writeln("} catch (e) {");
self.indent += 1;
self.writeln("return Lux.Err(e.message);");
self.indent -= 1;
self.writeln("}");
self.indent -= 1;
self.writeln("},");
self.writeln("postJson: async (url, json) => {");
self.indent += 1;
self.writeln("try {");
self.indent += 1;
self.writeln("const response = await fetch(url, {");
self.indent += 1;
self.writeln("method: 'POST',");
self.writeln("headers: { 'Content-Type': 'application/json' },");
self.writeln("body: JSON.stringify(json)");
self.indent -= 1;
self.writeln("});");
self.writeln("const body = await response.text();");
self.writeln("const headers = [];");
self.writeln("response.headers.forEach((v, k) => headers.push([k, v]));");
self.writeln("return Lux.Ok({ status: response.status, body, headers });");
self.indent -= 1;
self.writeln("} catch (e) {");
self.indent += 1;
self.writeln("return Lux.Err(e.message);");
self.indent -= 1;
self.writeln("}");
self.indent -= 1;
self.writeln("}");
self.indent -= 1;
self.writeln("}");
self.indent -= 1; self.indent -= 1;
self.writeln("}"); self.writeln("}");
@@ -410,11 +510,37 @@ impl JsBackend {
else_branch, else_branch,
.. ..
} => { } => {
// Check if branches contain statements that need if-else instead of ternary
let needs_block = self.expr_has_statements(then_branch)
|| self.expr_has_statements(else_branch);
if needs_block {
// Use if-else statement for branches with statements
let result_var = format!("_if_result_{}", self.fresh_name());
let cond = self.emit_expr(condition)?;
self.writeln(&format!("let {};", result_var));
self.writeln(&format!("if ({}) {{", cond));
self.indent += 1;
let then_val = self.emit_expr(then_branch)?;
self.writeln(&format!("{} = {};", result_var, then_val));
self.indent -= 1;
self.writeln("} else {");
self.indent += 1;
let else_val = self.emit_expr(else_branch)?;
self.writeln(&format!("{} = {};", result_var, else_val));
self.indent -= 1;
self.writeln("}");
Ok(result_var)
} else {
// Simple ternary for pure expressions
let cond = self.emit_expr(condition)?; let cond = self.emit_expr(condition)?;
let then_val = self.emit_expr(then_branch)?; let then_val = self.emit_expr(then_branch)?;
let else_val = self.emit_expr(else_branch)?; let else_val = self.emit_expr(else_branch)?;
Ok(format!("({} ? {} : {})", cond, then_val, else_val)) Ok(format!("({} ? {} : {})", cond, then_val, else_val))
} }
}
Expr::Let { Expr::Let {
name, value, body, .. name, value, body, ..
@@ -983,6 +1109,48 @@ impl JsBackend {
} }
} }
/// Check if an expression contains statements that emit code before the result
fn expr_has_statements(&self, expr: &Expr) -> bool {
match expr {
Expr::Block { statements, .. } => !statements.is_empty(),
Expr::Let { .. } => true,
Expr::Match { .. } => true,
Expr::If { then_branch, else_branch, .. } => {
self.expr_has_statements(then_branch) || self.expr_has_statements(else_branch)
}
_ => false,
}
}
/// Check if an expression calls the main function
fn expr_calls_main(&self, expr: &Expr) -> bool {
match expr {
Expr::Call { func, .. } => {
if let Expr::Var(ident) = func.as_ref() {
if ident.name == "main" {
return true;
}
}
false
}
Expr::Run { expr, .. } => self.expr_calls_main(expr),
Expr::Let { value, body, .. } => {
self.expr_calls_main(value) || self.expr_calls_main(body)
}
Expr::Block { statements, result, .. } => {
for stmt in statements {
if let Statement::Let { value, .. } = stmt {
if self.expr_calls_main(value) {
return true;
}
}
}
self.expr_calls_main(result)
}
_ => false,
}
}
/// Mangle a Lux name to a valid JavaScript name /// Mangle a Lux name to a valid JavaScript name
fn mangle_name(&self, name: &str) -> String { fn mangle_name(&self, name: &str) -> String {
format!("{}_lux", name) format!("{}_lux", name)
@@ -1265,4 +1433,60 @@ mod tests {
let output = compile_and_run(source).expect("Should compile and run"); let output = compile_and_run(source).expect("Should compile and run");
assert_eq!(output, "Hello, World!"); assert_eq!(output, "Hello, World!");
} }
#[test]
fn test_js_random_int() {
let source = r#"
fn main(): Unit with {Console, Random} = {
let n = Random.int(1, 10)
// Just verify it runs without error and produces a number
Console.print("Random: " + toString(n))
}
"#;
let output = compile_and_run(source).expect("Should compile and run");
assert!(output.starts_with("Random: "));
}
#[test]
fn test_js_random_bool() {
let source = r#"
fn main(): Unit with {Console, Random} = {
let b = Random.bool()
Console.print("Bool: " + toString(b))
}
"#;
let output = compile_and_run(source).expect("Should compile and run");
assert!(output == "Bool: true" || output == "Bool: false");
}
#[test]
fn test_js_random_float() {
let source = r#"
fn main(): Unit with {Console, Random} = {
let f = Random.float()
// Float should be between 0 and 1
Console.print("Float generated")
}
"#;
let output = compile_and_run(source).expect("Should compile and run");
assert_eq!(output, "Float generated");
}
#[test]
fn test_js_time_now() {
let source = r#"
fn main(): Unit with {Console, Time} = {
let t = Time.now()
// Should be a positive timestamp
if t > 0 then Console.print("Time works")
else Console.print("Time failed")
}
"#;
let output = compile_and_run(source).expect("Should compile and run");
assert_eq!(output, "Time works");
}
} }