From a0fff1814e522dcf8f0700c7a29177fd0b6ea158 Mon Sep 17 00:00:00 2001 From: Brandon Lucas Date: Thu, 19 Feb 2026 01:10:55 -0500 Subject: [PATCH] fix: JS backend scoping for let/match/if inside closures Three related bugs fixed: - BUG-009: let bindings inside lambdas hoisted to top-level - BUG-011: match expressions inside lambdas hoisted to top-level - BUG-012: variable name deduplication leaked across function scopes Root cause: emit_expr() uses writeln() for statements, but lambdas captured only the return value, not the emitted statements. Also, var_substitutions from emit_function() leaked to subsequent code. Fix: Lambda handler now captures all output emitted during body evaluation and places it inside the function body. Both emit_function and Lambda save/restore var_substitutions to prevent cross-scope leaks. Lambda params are registered as identity substitutions to override any outer bindings with the same name. Co-Authored-By: Claude Opus 4.6 --- src/codegen/js_backend.rs | 31 +++++++++++++++++++++++++++---- 1 file changed, 27 insertions(+), 4 deletions(-) diff --git a/src/codegen/js_backend.rs b/src/codegen/js_backend.rs index 84523ba..25204f2 100644 --- a/src/codegen/js_backend.rs +++ b/src/codegen/js_backend.rs @@ -888,7 +888,8 @@ impl JsBackend { let prev_has_handlers = self.has_handlers; self.has_handlers = is_effectful; - // Clear var substitutions for this function + // Save and clear var substitutions for this function scope + let saved_substitutions = self.var_substitutions.clone(); self.var_substitutions.clear(); // Emit function body @@ -896,6 +897,7 @@ impl JsBackend { self.writeln(&format!("return {};", body_code)); self.has_handlers = prev_has_handlers; + self.var_substitutions = saved_substitutions; self.indent -= 1; self.writeln("}"); @@ -1218,18 +1220,39 @@ impl JsBackend { param_names }; - // Save handler state + // Save state let prev_has_handlers = self.has_handlers; + let saved_substitutions = self.var_substitutions.clone(); self.has_handlers = !effects.is_empty(); + // Register lambda params as themselves (override any outer substitutions) + for p in &all_params { + self.var_substitutions.insert(p.clone(), p.clone()); + } + + // Capture any statements emitted during body evaluation + let output_start = self.output.len(); + let prev_indent = self.indent; + self.indent += 1; + let body_code = self.emit_expr(body)?; + self.writeln(&format!("return {};", body_code)); + // Extract body statements and restore output + let body_statements = self.output[output_start..].to_string(); + self.output.truncate(output_start); + self.indent = prev_indent; + + // Restore state self.has_handlers = prev_has_handlers; + self.var_substitutions = saved_substitutions; + let indent_str = " ".repeat(self.indent); Ok(format!( - "(function({}) {{ return {}; }})", + "(function({}) {{\n{}{}}})", all_params.join(", "), - body_code + body_statements, + indent_str, )) }