diff --git a/crates/oxide_compiler/src/compiler.rs b/crates/oxide_compiler/src/compiler.rs index e6fbcf3..0e08b19 100644 --- a/crates/oxide_compiler/src/compiler.rs +++ b/crates/oxide_compiler/src/compiler.rs @@ -721,6 +721,16 @@ impl Compiler { ctx.declare_initialized(name, reg, VariableDeclarationKind::Var, false)?; } + // Count the parameter destructuring prologue so the count pass matches the emit pass + // below (which emits it via emit_binding_pattern). Without this the projected program + // counter undercounts, drifting every jump target inside a function that has a + // destructured or default parameter. + for spec in param_specs { + if let ParamSpec::Pattern { pattern, .. } = spec { + self.count_binding_pattern(pattern, &mut ctx); + } + } + // Count pass if !fields_after_super { if let Some(count) = count_fields.as_mut() { diff --git a/crates/oxide_compiler/src/counter/helper/binding.rs b/crates/oxide_compiler/src/counter/helper/binding.rs index 975d783..a7c081a 100644 --- a/crates/oxide_compiler/src/counter/helper/binding.rs +++ b/crates/oxide_compiler/src/counter/helper/binding.rs @@ -1,9 +1,7 @@ use super::super::*; impl Compiler { - pub(in crate::counter) fn count_binding_pattern( - &self, pattern: &oxide_parser::BindingPattern, ctx: &mut CompileCtx, - ) { + pub(crate) fn count_binding_pattern(&self, pattern: &oxide_parser::BindingPattern, ctx: &mut CompileCtx) { match pattern { oxide_parser::BindingPattern::BindingIdentifier(_) => { ctx.alloc_reg(); @@ -72,9 +70,11 @@ impl Compiler { ctx.count_instr(); // FOR_OF_DONE ctx.alloc_reg(); ctx.count_instr(); // FOR_OF_NEXT - if elem.is_some() { - ctx.alloc_reg(); - ctx.count_instr(); // STORE_VAR + if let Some(elem) = elem { + // Mirror emit_array_assignment: an element may be a default (`[a = 1]`) or a + // nested pattern (`[[a], b]`), not just an identifier — recurse through the + // shared dispatch instead of hard-coding one STORE_VAR. + self.count_assignment_maybe_default(elem, ctx); } } if let Some(rest) = &ap.rest { diff --git a/crates/oxide_compiler/src/counter/stmt/exception.rs b/crates/oxide_compiler/src/counter/stmt/exception.rs index 408093e..313282b 100644 --- a/crates/oxide_compiler/src/counter/stmt/exception.rs +++ b/crates/oxide_compiler/src/counter/stmt/exception.rs @@ -40,9 +40,14 @@ impl Compiler { ctx.labels.label_map.insert(catch_label, ctx.projected_pc); if let Some(catch) = &stmt.handler { ctx.push_scope(); - if let Some(_param) = &catch.param { + if let Some(param) = &catch.param { + // Mirror the emitter: the catch register is always allocated, but STORE_VAR is + // emitted only for an identifier binding — a destructuring catch param emits no + // store (that binding form is not implemented yet). ctx.alloc_reg(); - ctx.projected_pc += 1; // STORE_VAR + if let oxide_parser::BindingPattern::BindingIdentifier(_) = ¶m.pattern { + ctx.projected_pc += 1; // STORE_VAR + } } for cs in &catch.body.body { self.count_statement(cs, ctx); diff --git a/crates/oxide_compiler/src/counter/stmt/iteration.rs b/crates/oxide_compiler/src/counter/stmt/iteration.rs index 1a80e5a..0eb6a0b 100644 --- a/crates/oxide_compiler/src/counter/stmt/iteration.rs +++ b/crates/oxide_compiler/src/counter/stmt/iteration.rs @@ -41,7 +41,8 @@ impl Compiler { self.count_expression(init_expr, ctx); self.count_binding_pattern(&d.id, ctx); } else { - ctx.alloc_reg(); + ctx.alloc_reg(); // tmp holding undefined + ctx.alloc_reg(); // var register ctx.count_words(2); // LOAD_CONST(undefined) + STORE_VAR } } diff --git a/crates/oxide_compiler/src/emitter/stmt/iteration.rs b/crates/oxide_compiler/src/emitter/stmt/iteration.rs index 484cbb4..3cbe2cb 100644 --- a/crates/oxide_compiler/src/emitter/stmt/iteration.rs +++ b/crates/oxide_compiler/src/emitter/stmt/iteration.rs @@ -75,8 +75,17 @@ impl Compiler { let val_reg = self.emit_expression(init_expr, ctx)?; self.emit_binding_pattern(&d.id, val_reg, decl.kind, is_const, ctx)?; } else if let BindingPattern::BindingIdentifier(bi) = &d.id { + // `for (var i; ...)` initializes `i` to undefined, mirroring a + // statement-level `var i;` — emit LOAD_CONST undefined + STORE_VAR. + // The count pass already counts these two instructions; emitting + // nothing here drifts every jump target after the for-init. + let idx = ctx.add_constant(Constant::Undefined); + 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)); + ctx.init_var(bi.name.as_str()); } } } diff --git a/crates/oxide_compiler/tests/compile_stmt_tests.rs b/crates/oxide_compiler/tests/compile_stmt_tests.rs index 50138ba..1e2632a 100644 --- a/crates/oxide_compiler/tests/compile_stmt_tests.rs +++ b/crates/oxide_compiler/tests/compile_stmt_tests.rs @@ -120,12 +120,14 @@ fn regression_for_var_init() { #[test] fn regression_var_decl_counter_no_init() { - let result = std::panic::catch_unwind(|| { - compile_source("for (var i; i < 3; i = i + 1) {}"); - }); + // `for (var i; ...)` initializes `i` to undefined (var semantics, not TDZ), and the + // counter/emitter agree on the no-init declarator's instruction count, so it compiles + // to a well-formed loop header instead of panicking. + let module = compile_source("for (var i; i < 3; i = i + 1) {}"); + assert!(!module.bytecode.is_empty(), "for(var) no-init should compile"); assert!( - result.is_err(), - "for(var) without initializer should fail - accessing TDZ variable in test expression" + module.bytecode.iter().any(|&i| opcode::opcode(i) == OpCode::JMP_IF_FALSE), + "for(var) no-init should contain JMP_IF_FALSE" ); } diff --git a/crates/oxide_vm/tests/compiler_alignment_binding_tests.rs b/crates/oxide_vm/tests/compiler_alignment_binding_tests.rs new file mode 100644 index 0000000..1e4ba3c --- /dev/null +++ b/crates/oxide_vm/tests/compiler_alignment_binding_tests.rs @@ -0,0 +1,63 @@ +use oxide_compiler::compiler::Compiler; +use oxide_parser::Allocator; +use oxide_vm::vm::Vm; + +fn eval(source: &str) -> String { + let allocator = Allocator::default(); + let program = match oxide_parser::parse(&allocator, source) { + Ok(p) => p, + Err(e) => return format!("parse error: {}", e[0].message), + }; + let module = match Compiler::new().compile(&program) { + Ok(m) => m, + Err(e) => return format!("compile error: {e}"), + }; + let mut vm = Vm::new(); + match vm.run(&module) { + Ok(result) => format!("{result}"), + Err(e) => format!("vm error: {e}"), + } +} + +// Destructured parameter followed by a loop body: the counter must count the parameter +// destructuring prologue or the loop's back-jump lands at the wrong PC. +#[test] +fn param_array_pattern_with_loop_body() { + assert_eq!(eval("function f([a]){var s=0; while(a>0){s=s+1;a=a-1;} return s;} f([3])"), "3"); +} + +#[test] +fn param_array_pattern_default_element() { + assert_eq!(eval("function f([a=5]){return a;} f([])"), "5"); +} + +// Array assignment target with a default element followed by a loop. +#[test] +fn array_assignment_default_element_then_loop() { + assert_eq!(eval("var a,c=0; [a=5]=[]; while(c