From 22f1275f6be46e381dda0014996eeac6eea3d408 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=88=B1=E7=89=B9=E6=AF=8F=E7=BE=8A=E9=98=9F?= Date: Wed, 5 Aug 2026 15:21:00 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E7=BC=96=E8=AF=91?= =?UTF-8?q?=E9=98=B6=E6=AE=B5=E7=BC=BA=E5=A4=B1=E7=9A=84=E9=A2=84=E6=B3=A8?= =?UTF-8?q?=E5=86=8C=E9=A1=B6=E5=B1=82=20var=20=E5=90=8D=E6=AD=A5=E9=AA=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/oxide_compiler/src/compiler.rs | 75 +++++++++++++++++++ crates/oxide_compiler/src/shared/binding.rs | 13 +++- .../src/stmt/declaration/var.rs | 12 ++- .../oxide_compiler/src/stmt/iteration/for_.rs | 12 ++- 4 files changed, 104 insertions(+), 8 deletions(-) diff --git a/crates/oxide_compiler/src/compiler.rs b/crates/oxide_compiler/src/compiler.rs index e69e0eb..db38beb 100644 --- a/crates/oxide_compiler/src/compiler.rs +++ b/crates/oxide_compiler/src/compiler.rs @@ -1283,6 +1283,73 @@ impl Compiler { } } + /// Pre-declare all `var` bindings in this statement list so hoisted function + /// declarations emitted earlier can resolve them. Mirrors the removed count + /// pass behavior (see fe8bd86): top-level `var` names must be visible while + /// compiling function bodies that close over them. + fn predeclare_var_declarations(&self, statements: &[Statement], ctx: &mut CompileCtx) { + for statement in statements { + match statement { + Statement::VariableDeclaration(decl) => { + if !matches!(decl.kind, VariableDeclarationKind::Var) { + continue; + } + for d in &decl.declarations { + if let oxide_parser::BindingPattern::BindingIdentifier(bi) = &d.id { + let reg = ctx.alloc_reg(); + let _ = ctx.declare_initialized(bi.name.as_str(), reg, VariableDeclarationKind::Var, false); + } + } + } + Statement::BlockStatement(bs) => self.predeclare_var_declarations(&bs.body, ctx), + Statement::IfStatement(is) => { + self.predeclare_var_declarations(std::slice::from_ref(&is.consequent), ctx); + if let Some(alt) = &is.alternate { + self.predeclare_var_declarations(std::slice::from_ref(alt), ctx); + } + } + Statement::WhileStatement(wh) => { + self.predeclare_var_declarations(std::slice::from_ref(&wh.body), ctx); + } + Statement::DoWhileStatement(dw) => { + self.predeclare_var_declarations(std::slice::from_ref(&dw.body), ctx); + } + Statement::ForStatement(fs) => { + if let Some(oxide_parser::ForStatementInit::VariableDeclaration(decl)) = &fs.init { + if matches!(decl.kind, VariableDeclarationKind::Var) { + for d in &decl.declarations { + if let oxide_parser::BindingPattern::BindingIdentifier(bi) = &d.id { + let reg = ctx.alloc_reg(); + let _ = + ctx.declare_initialized(bi.name.as_str(), reg, VariableDeclarationKind::Var, false); + } + } + } + } + self.predeclare_var_declarations(std::slice::from_ref(&fs.body), ctx); + } + Statement::SwitchStatement(sw) => { + for case in &sw.cases { + self.predeclare_var_declarations(&case.consequent, ctx); + } + } + Statement::TryStatement(ts) => { + self.predeclare_var_declarations(&ts.block.body, ctx); + if let Some(handler) = &ts.handler { + self.predeclare_var_declarations(&handler.body.body, ctx); + } + if let Some(finalizer) = &ts.finalizer { + self.predeclare_var_declarations(&finalizer.body, ctx); + } + } + Statement::LabeledStatement(ls) => { + self.predeclare_var_declarations(std::slice::from_ref(&ls.body), ctx); + } + _ => {} + } + } + } + #[allow(clippy::too_many_arguments)] pub(crate) fn compile_function_body_with_field_hooks<'a, E>( &self, param_specs: &[ParamSpec<'a>], body_stmts: &[Statement<'a>], parent_ctx: &CompileCtx, @@ -1391,6 +1458,10 @@ impl Compiler { // register, so builtin slots never collide with reused temporaries. self.pre_register_builtin_references(body_stmts, &mut ctx); + // Pre-declare `var` names so hoisted function declarations (emitted in the + // first sub-pass below) can resolve the outer vars they close over. + self.predeclare_var_declarations(body_stmts, &mut ctx); + if let Some(emit) = emit_fields.as_mut() { if fields_after_super { let mut parent_bytecode = Vec::new(); @@ -1569,6 +1640,10 @@ impl Compiler { // is emitted, keeping builtin slots clear of the temporary register pool. self.pre_register_builtin_references(&program.body, &mut ctx); + // Pre-declare top-level `var` names so hoisted function declarations + // (emitted in the first sub-pass below) can resolve the outer vars. + self.predeclare_var_declarations(&program.body, &mut ctx); + // First sub-pass: emit FunctionDeclarations (hoisting) // This ensures function objects are available before any code runs. for stmt in &program.body { diff --git a/crates/oxide_compiler/src/shared/binding.rs b/crates/oxide_compiler/src/shared/binding.rs index da02a95..74602f8 100644 --- a/crates/oxide_compiler/src/shared/binding.rs +++ b/crates/oxide_compiler/src/shared/binding.rs @@ -27,13 +27,18 @@ impl Compiler { fn emit_bind_target( &self, name: &str, src_reg: u8, kind: VariableDeclarationKind, is_const: bool, ctx: &mut CompileCtx, ) -> Result<(), String> { - let var_reg = ctx.alloc_reg(); let target_reg = if matches!(kind, VariableDeclarationKind::Var) { - match ctx.declare(name, var_reg, kind, is_const) { - Ok(()) => var_reg, - Err(_) => ctx.lookup(name).unwrap_or(var_reg), + // `var` names are pre-declared (hoisting); reuse the pre-registered slot + // instead of allocating a new one so n_registers matches let/const. + if let Some(reg) = ctx.scopes.symbols.lookup_any(name) { + reg + } else { + let var_reg = ctx.alloc_reg(); + ctx.declare(name, var_reg, kind, is_const)?; + var_reg } } else { + let var_reg = ctx.alloc_reg(); ctx.declare(name, var_reg, kind, is_const)?; var_reg }; diff --git a/crates/oxide_compiler/src/stmt/declaration/var.rs b/crates/oxide_compiler/src/stmt/declaration/var.rs index e0a3cf0..90fb154 100644 --- a/crates/oxide_compiler/src/stmt/declaration/var.rs +++ b/crates/oxide_compiler/src/stmt/declaration/var.rs @@ -37,14 +37,22 @@ impl Compiler { let tmp = ctx.alloc_reg(); ctx.emit_load_const(tmp, idx); let var_reg = ctx.alloc_reg(); - ctx.declare(bi.name.as_str(), var_reg, decl.kind, is_const)?; + let target_reg = if matches!(decl.kind, VariableDeclarationKind::Var) { + match ctx.declare(bi.name.as_str(), var_reg, decl.kind, is_const) { + Ok(()) => var_reg, + Err(_) => ctx.lookup(bi.name.as_str()).unwrap_or(var_reg), + } + } else { + ctx.declare(bi.name.as_str(), var_reg, decl.kind, is_const)?; + var_reg + }; let is_captured = ctx.scopes.symbols.lookup_is_captured(bi.name.as_str()); if is_captured { let cell_idx = ctx.scopes.cell_registry.len() as u8; ctx.scopes.cell_registry.push((bi.name.to_string(), cell_idx)); ctx.emit(opcode::encode(OpCode::MAKE_CELL, tmp, cell_idx, 0)); } else { - ctx.emit(opcode::encode(OpCode::STORE_VAR, var_reg, tmp, 0)); + ctx.emit(opcode::encode(OpCode::STORE_VAR, target_reg, tmp, 0)); } ctx.init_var(bi.name.as_str()); r = Some(var_reg); diff --git a/crates/oxide_compiler/src/stmt/iteration/for_.rs b/crates/oxide_compiler/src/stmt/iteration/for_.rs index 7dc1ba7..0301a5d 100644 --- a/crates/oxide_compiler/src/stmt/iteration/for_.rs +++ b/crates/oxide_compiler/src/stmt/iteration/for_.rs @@ -30,8 +30,16 @@ impl Compiler { let tmp = ctx.alloc_reg(); ctx.emit_load_const(tmp, idx); let var_reg = ctx.alloc_reg(); - ctx.declare(bi.name.as_str(), var_reg, decl.kind, is_const)?; - ctx.emit(opcode::encode(OpCode::STORE_VAR, var_reg, tmp, 0)); + let target_reg = if matches!(decl.kind, VariableDeclarationKind::Var) { + match ctx.declare(bi.name.as_str(), var_reg, decl.kind, is_const) { + Ok(()) => var_reg, + Err(_) => ctx.lookup(bi.name.as_str()).unwrap_or(var_reg), + } + } else { + ctx.declare(bi.name.as_str(), var_reg, decl.kind, is_const)?; + var_reg + }; + ctx.emit(opcode::encode(OpCode::STORE_VAR, target_reg, tmp, 0)); ctx.init_var(bi.name.as_str()); } }