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
|
// Check if any top-level let calls main (to avoid double invocation)
|
||||||
self.writeln("// Top-level bindings");
|
let has_main_call = program.declarations.iter().any(|decl| {
|
||||||
for decl in &program.declarations {
|
|
||||||
if let Declaration::Let(l) = 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
|
// 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,10 +510,36 @@ impl JsBackend {
|
|||||||
else_branch,
|
else_branch,
|
||||||
..
|
..
|
||||||
} => {
|
} => {
|
||||||
let cond = self.emit_expr(condition)?;
|
// Check if branches contain statements that need if-else instead of ternary
|
||||||
let then_val = self.emit_expr(then_branch)?;
|
let needs_block = self.expr_has_statements(then_branch)
|
||||||
let else_val = self.emit_expr(else_branch)?;
|
|| self.expr_has_statements(else_branch);
|
||||||
Ok(format!("({} ? {} : {})", cond, then_val, else_val))
|
|
||||||
|
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 {
|
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
|
/// 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");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user