feat: v2 codegen hardening — scoped local variables
Replace flat HashSet<String> local_vars with Vec<HashSet<String>> scope stack. For-in loops push/pop scopes for loop variables. Nested loops no longer clobber outer loop variable visibility. Methods: push_scope(), pop_scope(), is_local_var() 110 tests, 0 failures.
This commit is contained in:
parent
6368b798cf
commit
55ec9353ae
1 changed files with 30 additions and 17 deletions
|
|
@ -8,8 +8,8 @@ pub struct JsEmitter {
|
||||||
output: String,
|
output: String,
|
||||||
indent: usize,
|
indent: usize,
|
||||||
node_id_counter: usize,
|
node_id_counter: usize,
|
||||||
/// Non-signal local variables (e.g., for-in loop vars)
|
/// Scoped local variables (e.g., for-in loop vars). Stack of scopes for nesting.
|
||||||
local_vars: HashSet<String>,
|
local_var_scopes: Vec<HashSet<String>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl JsEmitter {
|
impl JsEmitter {
|
||||||
|
|
@ -18,10 +18,26 @@ impl JsEmitter {
|
||||||
output: String::new(),
|
output: String::new(),
|
||||||
indent: 0,
|
indent: 0,
|
||||||
node_id_counter: 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<String> = 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.
|
/// Generate a complete HTML page with embedded runtime and compiled app.
|
||||||
pub fn emit_html(program: &Program, graph: &SignalGraph, views: &[AnalyzedView]) -> String {
|
pub fn emit_html(program: &Program, graph: &SignalGraph, views: &[AnalyzedView]) -> String {
|
||||||
let mut emitter = Self::new();
|
let mut emitter = Self::new();
|
||||||
|
|
@ -384,7 +400,7 @@ impl JsEmitter {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Expr::Ident(name) => {
|
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)
|
// Non-reactive local variable (e.g., for-in loop var)
|
||||||
self.emit_line(&format!(
|
self.emit_line(&format!(
|
||||||
"{}.textContent = {};",
|
"{}.textContent = {};",
|
||||||
|
|
@ -533,20 +549,18 @@ impl JsEmitter {
|
||||||
self.emit_line(&format!("__list.forEach(({item}, {idx_var}) => {{"));
|
self.emit_line(&format!("__list.forEach(({item}, {idx_var}) => {{"));
|
||||||
self.indent += 1;
|
self.indent += 1;
|
||||||
|
|
||||||
// Mark loop vars as non-reactive so text bindings use plain access
|
// Push scope for loop variables
|
||||||
self.local_vars.insert(item.clone());
|
let mut scope_vars: Vec<&str> = vec![item.as_str()];
|
||||||
if let Some(idx) = index {
|
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);
|
let child_var = self.emit_view_expr(body, graph);
|
||||||
self.emit_line(&format!("{}.appendChild({});", container_var, child_var));
|
self.emit_line(&format!("{}.appendChild({});", container_var, child_var));
|
||||||
|
|
||||||
// Clean up
|
// Pop scope
|
||||||
self.local_vars.remove(item);
|
self.pop_scope();
|
||||||
if let Some(idx) = index {
|
|
||||||
self.local_vars.remove(idx);
|
|
||||||
}
|
|
||||||
|
|
||||||
self.indent -= 1;
|
self.indent -= 1;
|
||||||
self.emit_line("});");
|
self.emit_line("});");
|
||||||
|
|
@ -647,12 +661,11 @@ impl JsEmitter {
|
||||||
}
|
}
|
||||||
format!("`{}`", parts.join(""))
|
format!("`{}`", parts.join(""))
|
||||||
}
|
}
|
||||||
Expr::Ident(name) => {
|
Expr::Ident(name) if self.is_local_var(name) => {
|
||||||
if self.local_vars.contains(name) {
|
|
||||||
format!("{name}")
|
format!("{name}")
|
||||||
} else {
|
|
||||||
format!("{name}.value")
|
|
||||||
}
|
}
|
||||||
|
Expr::Ident(name) => {
|
||||||
|
format!("{name}.value")
|
||||||
}
|
}
|
||||||
Expr::DotAccess(base, field) => {
|
Expr::DotAccess(base, field) => {
|
||||||
let base_js = self.emit_expr(base);
|
let base_js = self.emit_expr(base);
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue