diff --git a/compiler/ds-codegen/src/js_emitter.rs b/compiler/ds-codegen/src/js_emitter.rs index 8fa5685..3f12807 100644 --- a/compiler/ds-codegen/src/js_emitter.rs +++ b/compiler/ds-codegen/src/js_emitter.rs @@ -8,8 +8,8 @@ pub struct JsEmitter { output: String, indent: usize, node_id_counter: usize, - /// Non-signal local variables (e.g., for-in loop vars) - local_vars: HashSet, + /// Scoped local variables (e.g., for-in loop vars). Stack of scopes for nesting. + local_var_scopes: Vec>, } impl JsEmitter { @@ -18,10 +18,26 @@ impl JsEmitter { output: String::new(), indent: 0, node_id_counter: 0, - local_vars: HashSet::new(), + local_var_scopes: Vec::new(), } } + /// Push a new variable scope (e.g., entering a for-in loop) + fn push_scope(&mut self, vars: &[&str]) { + let set: HashSet = vars.iter().map(|s| s.to_string()).collect(); + self.local_var_scopes.push(set); + } + + /// Pop the top variable scope (e.g., leaving a for-in loop) + fn pop_scope(&mut self) { + self.local_var_scopes.pop(); + } + + /// Check if a name is a local variable in any active scope + fn is_local_var(&self, name: &str) -> bool { + self.local_var_scopes.iter().any(|scope| scope.contains(name)) + } + /// Generate a complete HTML page with embedded runtime and compiled app. pub fn emit_html(program: &Program, graph: &SignalGraph, views: &[AnalyzedView]) -> String { let mut emitter = Self::new(); @@ -384,7 +400,7 @@ impl JsEmitter { } } Expr::Ident(name) => { - if self.local_vars.contains(name) { + if self.is_local_var(name) { // Non-reactive local variable (e.g., for-in loop var) self.emit_line(&format!( "{}.textContent = {};", @@ -533,20 +549,18 @@ impl JsEmitter { self.emit_line(&format!("__list.forEach(({item}, {idx_var}) => {{")); self.indent += 1; - // Mark loop vars as non-reactive so text bindings use plain access - self.local_vars.insert(item.clone()); + // Push scope for loop variables + let mut scope_vars: Vec<&str> = vec![item.as_str()]; if let Some(idx) = index { - self.local_vars.insert(idx.clone()); + scope_vars.push(idx.as_str()); } + self.push_scope(&scope_vars); let child_var = self.emit_view_expr(body, graph); self.emit_line(&format!("{}.appendChild({});", container_var, child_var)); - // Clean up - self.local_vars.remove(item); - if let Some(idx) = index { - self.local_vars.remove(idx); - } + // Pop scope + self.pop_scope(); self.indent -= 1; self.emit_line("});"); @@ -647,12 +661,11 @@ impl JsEmitter { } format!("`{}`", parts.join("")) } + Expr::Ident(name) if self.is_local_var(name) => { + format!("{name}") + } Expr::Ident(name) => { - if self.local_vars.contains(name) { - format!("{name}") - } else { - format!("{name}.value") - } + format!("{name}.value") } Expr::DotAccess(base, field) => { let base_js = self.emit_expr(base);