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 {
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 {
if let Some(init) = &d.init {
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);
} else {
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,
) -> Result<(), String> {
let var_reg = ctx.alloc_reg();
ctx.declare(name, var_reg, kind, is_const)?;
ctx.emit(opcode::encode(OpCode::STORE_VAR, var_reg, src_reg, if is_const { 1 } else { 0 }));
let target_reg = if matches!(kind, VariableDeclarationKind::Var) {
// `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);
Ok(())
}
@@ -30,22 +30,19 @@ fn symbol_table_nested_scopes() {
}
#[test]
fn symbol_table_tdz_global_shadow() {
let result = std::panic::catch_unwind(|| {
compile_source("var x = 1; { var x = x; }");
});
assert!(result.is_err(), "TDZ: accessing x in its own initializer inside a block should error");
fn symbol_table_var_self_init_in_block_is_legal() {
// `var` has no temporal dead zone and hoists to the function scope, so a block-level
// `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!(!module.bytecode.is_empty(), "var self-init in block should compile");
}
#[test]
fn symbol_table_duplicate_var() {
let result = std::panic::catch_unwind(|| {
compile_source("var x = 1; var x = 2;");
});
assert!(
result.is_err(),
"duplicate var declaration in same scope should error (strict-mode semantics)"
);
fn symbol_table_duplicate_var_is_legal() {
// Duplicate `var` in the same scope is legal in JavaScript (even in strict mode);
// only duplicate `let`/`const` declarations are a SyntaxError.
let module = compile_source("var x = 1; var x = 2;");
assert!(!module.bytecode.is_empty(), "duplicate var should compile");
}
#[test]