diff --git a/src/codegen/js_backend.rs b/src/codegen/js_backend.rs index d7718c1..db5f6f7 100644 --- a/src/codegen/js_backend.rs +++ b/src/codegen/js_backend.rs @@ -157,6 +157,13 @@ impl JsBackend { } } + // Emit handlers + for decl in &program.declarations { + if let Declaration::Handler(h) = decl { + self.emit_handler(h)?; + } + } + // 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 { @@ -1080,6 +1087,55 @@ impl JsBackend { Ok(()) } + /// Emit a handler declaration as a JS object + fn emit_handler(&mut self, handler: &HandlerDecl) -> Result<(), JsGenError> { + let handler_name = self.mangle_name(&handler.name.name); + + self.writeln(&format!("const {} = {{", handler_name)); + self.indent += 1; + + for (i, imp) in handler.implementations.iter().enumerate() { + // Build parameter list for this operation (just the effect op params, not resume) + let params: Vec = imp.params.iter().map(|p| p.name.clone()).collect(); + + self.writeln(&format!( + "{}: function({}) {{", + imp.op_name.name, + params.join(", ") + )); + self.indent += 1; + + // Set up handler context — handlers can use effects + let prev_has_handlers = self.has_handlers; + self.has_handlers = true; + + let saved_substitutions = self.var_substitutions.clone(); + + // In the simple handler model, resume is the identity function. + // Expr::Resume nodes emit `resume(val)`, so define it in every operation. + self.writeln("const resume = (x) => x;"); + + let body_code = self.emit_expr(&imp.body)?; + self.writeln(&format!("return {};", body_code)); + + self.var_substitutions = saved_substitutions; + self.has_handlers = prev_has_handlers; + + self.indent -= 1; + if i < handler.implementations.len() - 1 { + self.writeln("},"); + } else { + self.writeln("}"); + } + } + + self.indent -= 1; + self.writeln("};"); + self.writeln(""); + + Ok(()) + } + /// Emit a top-level let binding fn emit_top_level_let(&mut self, let_decl: &LetDecl) -> Result<(), JsGenError> { let val = self.emit_expr(&let_decl.value)?; diff --git a/src/main.rs b/src/main.rs index ab03f0a..eca6308 100644 --- a/src/main.rs +++ b/src/main.rs @@ -4311,6 +4311,44 @@ c")"#; assert!(!js.contains("main_lux"), "let main should not be mangled: {}", js); } + #[test] + fn test_handler_js_codegen() { + use crate::codegen::js_backend::JsBackend; + use crate::parser::Parser; + use crate::lexer::Lexer; + + let source = r#" + effect Log { + fn info(msg: String): Unit + fn debug(msg: String): Unit + } + + handler consoleLogger: Log { + fn info(msg) = { + Console.print("[INFO] " + msg) + resume(()) + } + fn debug(msg) = { + Console.print("[DEBUG] " + msg) + resume(()) + } + } + "#; + + let tokens = Lexer::new(source).tokenize().unwrap(); + let program = Parser::new(tokens).parse_program().unwrap(); + let mut backend = JsBackend::new(); + let js = backend.generate(&program).unwrap(); + + // Handler should be emitted as a const object + assert!(js.contains("const consoleLogger_lux"), "JS should contain handler const: {}", js); + // Should have operation methods + assert!(js.contains("info: function(msg)"), "JS should contain info operation: {}", js); + assert!(js.contains("debug: function(msg)"), "JS should contain debug operation: {}", js); + // Should define resume locally + assert!(js.contains("const resume = (x) => x"), "JS should define resume: {}", js); + } + #[test] fn test_invalid_escape_sequence() { let result = eval(r#"let x = "\z""#);