diff --git a/crates/oxide_compiler/src/counter/stmt/declaration.rs b/crates/oxide_compiler/src/counter/stmt/declaration.rs index 0317160..e2acec3 100644 --- a/crates/oxide_compiler/src/counter/stmt/declaration.rs +++ b/crates/oxide_compiler/src/counter/stmt/declaration.rs @@ -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(); diff --git a/crates/oxide_compiler/src/emitter/mod.rs b/crates/oxide_compiler/src/emitter/mod.rs index a8a45de..ef1e807 100644 --- a/crates/oxide_compiler/src/emitter/mod.rs +++ b/crates/oxide_compiler/src/emitter/mod.rs @@ -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(()) } diff --git a/crates/oxide_compiler/tests/symbol_table_tests.rs b/crates/oxide_compiler/tests/symbol_table_tests.rs index f75c816..a2c186b 100644 --- a/crates/oxide_compiler/tests/symbol_table_tests.rs +++ b/crates/oxide_compiler/tests/symbol_table_tests.rs @@ -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] diff --git a/crates/oxide_vm/tests/compiler_alignment_expr_tests.rs b/crates/oxide_vm/tests/compiler_alignment_expr_tests.rs index 050eeb6..b871d13 100644 --- a/crates/oxide_vm/tests/compiler_alignment_expr_tests.rs +++ b/crates/oxide_vm/tests/compiler_alignment_expr_tests.rs @@ -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!(