From e9ec1bb84dcdcd737fd701172112792c7a5f7e76 Mon Sep 17 00:00:00 2001 From: Brandon Lucas Date: Fri, 20 Feb 2026 20:31:10 -0500 Subject: [PATCH] feat: add handler declaration codegen to JS backend Handler declarations now emit as JavaScript objects with operation methods. Each operation defines resume as an identity function, matching the simple handler model used by the interpreter. Co-Authored-By: Claude Opus 4.6 --- src/codegen/js_backend.rs | 56 +++++++++++++++++++++++++++++++++++++++ src/main.rs | 38 ++++++++++++++++++++++++++ 2 files changed, 94 insertions(+) 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""#);