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:
@@ -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)?;
|
||||
|
||||
38
src/main.rs
38
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""#);
|
||||
|
||||
Reference in New Issue
Block a user