fix: count pass 注册顶层 var 名,hoisted 函数声明可解析外层 var

This commit is contained in:
爱特每羊队
2026-06-30 16:37:50 +08:00
parent b797dcdec0
commit caa50afbe6
4 changed files with 50 additions and 15 deletions
@@ -2,9 +2,22 @@ use super::*;
impl Compiler { impl Compiler {
fn count_variable_declaration(&self, decl: &oxide_parser::VariableDeclaration<'_>, ctx: &mut CompileCtx) { fn count_variable_declaration(&self, decl: &oxide_parser::VariableDeclaration<'_>, ctx: &mut CompileCtx) {
let is_var = matches!(decl.kind, VariableDeclarationKind::Var);
for d in &decl.declarations { for d in &decl.declarations {
if let Some(init) = &d.init { if let Some(init) = &d.init {
self.count_expression(init, ctx); self.count_expression(init, ctx);
if is_var {
if let oxide_parser::BindingPattern::BindingIdentifier(bi) = &d.id {
// `var` hoisting: register the name during the count pass (mirrors
// count_function_declaration) so function bodies emitted earlier in the
// hoisting sub-pass can resolve it. count_binding_pattern would only
// allocate a register without declaring the name.
let reg = ctx.alloc_reg();
let _ = ctx.declare_initialized(bi.name.as_str(), reg, VariableDeclarationKind::Var, false);
ctx.projected_pc += 1; // STORE_VAR
continue;
}
}
self.count_binding_pattern(&d.id, ctx); self.count_binding_pattern(&d.id, ctx);
} else { } else {
ctx.alloc_reg(); ctx.alloc_reg();
+13 -2
View File
@@ -355,8 +355,19 @@ impl Compiler {
&self, name: &str, src_reg: u8, kind: VariableDeclarationKind, is_const: bool, ctx: &mut CompileCtx, &self, name: &str, src_reg: u8, kind: VariableDeclarationKind, is_const: bool, ctx: &mut CompileCtx,
) -> Result<(), String> { ) -> Result<(), String> {
let var_reg = ctx.alloc_reg(); let var_reg = ctx.alloc_reg();
ctx.declare(name, var_reg, kind, is_const)?; let target_reg = if matches!(kind, VariableDeclarationKind::Var) {
ctx.emit(opcode::encode(OpCode::STORE_VAR, var_reg, src_reg, if is_const { 1 } else { 0 })); // `var` is hoisted, so the count pass already registered the name. A redeclare
// here reuses that existing register instead of erroring; the freshly allocated
// reg matches it (count/emit allocate in lock-step) so either resolves the same.
match ctx.declare(name, var_reg, kind, is_const) {
Ok(()) => var_reg,
Err(_) => ctx.lookup(name).unwrap_or(var_reg),
}
} else {
ctx.declare(name, var_reg, kind, is_const)?;
var_reg
};
ctx.emit(opcode::encode(OpCode::STORE_VAR, target_reg, src_reg, if is_const { 1 } else { 0 }));
ctx.init_var(name); ctx.init_var(name);
Ok(()) Ok(())
} }
@@ -30,22 +30,19 @@ fn symbol_table_nested_scopes() {
} }
#[test] #[test]
fn symbol_table_tdz_global_shadow() { fn symbol_table_var_self_init_in_block_is_legal() {
let result = std::panic::catch_unwind(|| { // `var` has no temporal dead zone and hoists to the function scope, so a block-level
compile_source("var x = 1; { var x = x; }"); // `var x = x` reads the same already-initialized binding — legal in JS, not an error.
}); let module = compile_source("var x = 1; { var x = x; }");
assert!(result.is_err(), "TDZ: accessing x in its own initializer inside a block should error"); assert!(!module.bytecode.is_empty(), "var self-init in block should compile");
} }
#[test] #[test]
fn symbol_table_duplicate_var() { fn symbol_table_duplicate_var_is_legal() {
let result = std::panic::catch_unwind(|| { // Duplicate `var` in the same scope is legal in JavaScript (even in strict mode);
compile_source("var x = 1; var x = 2;"); // only duplicate `let`/`const` declarations are a SyntaxError.
}); let module = compile_source("var x = 1; var x = 2;");
assert!( assert!(!module.bytecode.is_empty(), "duplicate var should compile");
result.is_err(),
"duplicate var declaration in same scope should error (strict-mode semantics)"
);
} }
#[test] #[test]
@@ -58,6 +58,20 @@ fn while_then_hoisted_function() {
assert_eq!(eval("var n=0; while(n<3){n=n+1;} function f(){return 100} n + f()"), "103"); assert_eq!(eval("var n=0; while(n<3){n=n+1;} function f(){return 100} n + f()"), "103");
} }
// A hoisted function declaration is emitted before the `var` statements it closes over,
// so the count pass must register top-level `var` names too (not just function names);
// otherwise the hoisted body cannot resolve the outer var ("Identifier not defined").
#[test]
fn hoisted_function_reads_outer_var() {
assert_eq!(eval("var s=3; function f(){return s} f()"), "3");
}
#[test]
fn hoisted_function_reads_outer_var_and_function() {
assert_eq!(eval("var s=3; function g(){return 5} function f(){return g()+s} f()"), "8");
}
#[test] #[test]
fn coalesce_nonsimple_then_for() { fn coalesce_nonsimple_then_for() {
assert_eq!( assert_eq!(