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:
@@ -130,16 +130,27 @@ impl JsBackend {
|
||||
}
|
||||
}
|
||||
|
||||
// Emit top-level let bindings and expressions
|
||||
self.writeln("// Top-level bindings");
|
||||
for decl in &program.declarations {
|
||||
// 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.emit_top_level_let(l)?;
|
||||
self.expr_calls_main(&l.value)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
});
|
||||
|
||||
// Emit top-level let bindings and expressions
|
||||
if program.declarations.iter().any(|d| matches!(d, Declaration::Let(_))) {
|
||||
self.writeln("// Top-level bindings");
|
||||
for decl in &program.declarations {
|
||||
if let Declaration::Let(l) = decl {
|
||||
self.emit_top_level_let(l)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Emit main function call if it exists
|
||||
if self.functions.contains("main") {
|
||||
// Emit main function call if it exists and not already called
|
||||
if self.functions.contains("main") && !has_main_call {
|
||||
self.writeln("");
|
||||
self.writeln("// Entry point");
|
||||
if self.effectful_functions.contains("main") {
|
||||
@@ -176,18 +187,107 @@ impl JsBackend {
|
||||
// Default handlers for effects
|
||||
self.writeln("defaultHandlers: {");
|
||||
self.indent += 1;
|
||||
|
||||
// Console effect
|
||||
self.writeln("Console: {");
|
||||
self.indent += 1;
|
||||
self.writeln("print: (msg) => console.log(msg),");
|
||||
self.writeln("readLine: () => { throw new Error('readLine not supported in browser'); },");
|
||||
self.writeln("readInt: () => { throw new Error('readInt not supported in browser'); }");
|
||||
self.indent -= 1;
|
||||
self.writeln("},");
|
||||
self.writeln("Random: {");
|
||||
self.writeln("readLine: () => {");
|
||||
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.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.writeln("}");
|
||||
|
||||
@@ -410,10 +510,36 @@ impl JsBackend {
|
||||
else_branch,
|
||||
..
|
||||
} => {
|
||||
let cond = self.emit_expr(condition)?;
|
||||
let then_val = self.emit_expr(then_branch)?;
|
||||
let else_val = self.emit_expr(else_branch)?;
|
||||
Ok(format!("({} ? {} : {})", cond, then_val, else_val))
|
||||
// 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 then_val = self.emit_expr(then_branch)?;
|
||||
let else_val = self.emit_expr(else_branch)?;
|
||||
Ok(format!("({} ? {} : {})", cond, then_val, else_val))
|
||||
}
|
||||
}
|
||||
|
||||
Expr::Let {
|
||||
@@ -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
|
||||
fn mangle_name(&self, name: &str) -> String {
|
||||
format!("{}_lux", name)
|
||||
@@ -1265,4 +1433,60 @@ mod tests {
|
||||
let output = compile_and_run(source).expect("Should compile and run");
|
||||
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");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user