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 <noreply@anthropic.com>
This commit is contained in:
2026-02-20 20:31:10 -05:00
parent e46afd98eb
commit e9ec1bb84d
2 changed files with 94 additions and 0 deletions

View File

@@ -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<String> = 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)?;

View File

@@ -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""#);