fix: count pass 注册顶层 var 名,hoisted 函数声明可解析外层 var
This commit is contained in:
@@ -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();
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
// 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]
|
||||
fn coalesce_nonsimple_then_for() {
|
||||
assert_eq!(
|
||||
|
||||
Reference in New Issue
Block a user