fix: 对齐 counter/emitter 指令条数,修解构参数、数组赋值元素、解构catch、for-init 无初始化的跳转漂移

This commit is contained in:
2026-06-27 20:32:33 +08:00
parent 365b0e58f6
commit 3794ae3fd5
7 changed files with 104 additions and 14 deletions
+10
View File
@@ -721,6 +721,16 @@ impl Compiler {
ctx.declare_initialized(name, reg, VariableDeclarationKind::Var, false)?; 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 // Count pass
if !fields_after_super { if !fields_after_super {
if let Some(count) = count_fields.as_mut() { if let Some(count) = count_fields.as_mut() {
@@ -1,9 +1,7 @@
use super::super::*; use super::super::*;
impl Compiler { impl Compiler {
pub(in crate::counter) fn count_binding_pattern( pub(crate) fn count_binding_pattern(&self, pattern: &oxide_parser::BindingPattern, ctx: &mut CompileCtx) {
&self, pattern: &oxide_parser::BindingPattern, ctx: &mut CompileCtx,
) {
match pattern { match pattern {
oxide_parser::BindingPattern::BindingIdentifier(_) => { oxide_parser::BindingPattern::BindingIdentifier(_) => {
ctx.alloc_reg(); ctx.alloc_reg();
@@ -72,9 +70,11 @@ impl Compiler {
ctx.count_instr(); // FOR_OF_DONE ctx.count_instr(); // FOR_OF_DONE
ctx.alloc_reg(); ctx.alloc_reg();
ctx.count_instr(); // FOR_OF_NEXT ctx.count_instr(); // FOR_OF_NEXT
if elem.is_some() { if let Some(elem) = elem {
ctx.alloc_reg(); // Mirror emit_array_assignment: an element may be a default (`[a = 1]`) or a
ctx.count_instr(); // STORE_VAR // 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 { if let Some(rest) = &ap.rest {
@@ -40,9 +40,14 @@ impl Compiler {
ctx.labels.label_map.insert(catch_label, ctx.projected_pc); ctx.labels.label_map.insert(catch_label, ctx.projected_pc);
if let Some(catch) = &stmt.handler { if let Some(catch) = &stmt.handler {
ctx.push_scope(); 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.alloc_reg();
ctx.projected_pc += 1; // STORE_VAR if let oxide_parser::BindingPattern::BindingIdentifier(_) = &param.pattern {
ctx.projected_pc += 1; // STORE_VAR
}
} }
for cs in &catch.body.body { for cs in &catch.body.body {
self.count_statement(cs, ctx); self.count_statement(cs, ctx);
@@ -41,7 +41,8 @@ impl Compiler {
self.count_expression(init_expr, ctx); self.count_expression(init_expr, ctx);
self.count_binding_pattern(&d.id, ctx); self.count_binding_pattern(&d.id, ctx);
} else { } else {
ctx.alloc_reg(); ctx.alloc_reg(); // tmp holding undefined
ctx.alloc_reg(); // var register
ctx.count_words(2); // LOAD_CONST(undefined) + STORE_VAR ctx.count_words(2); // LOAD_CONST(undefined) + STORE_VAR
} }
} }
@@ -75,8 +75,17 @@ impl Compiler {
let val_reg = self.emit_expression(init_expr, ctx)?; let val_reg = self.emit_expression(init_expr, ctx)?;
self.emit_binding_pattern(&d.id, val_reg, decl.kind, is_const, ctx)?; self.emit_binding_pattern(&d.id, val_reg, decl.kind, is_const, ctx)?;
} else if let BindingPattern::BindingIdentifier(bi) = &d.id { } 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(); let var_reg = ctx.alloc_reg();
ctx.declare(bi.name.as_str(), var_reg, decl.kind, is_const)?; 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());
} }
} }
} }
@@ -120,12 +120,14 @@ fn regression_for_var_init() {
#[test] #[test]
fn regression_var_decl_counter_no_init() { fn regression_var_decl_counter_no_init() {
let result = std::panic::catch_unwind(|| { // `for (var i; ...)` initializes `i` to undefined (var semantics, not TDZ), and the
compile_source("for (var i; i < 3; i = i + 1) {}"); // 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!( assert!(
result.is_err(), module.bytecode.iter().any(|&i| opcode::opcode(i) == OpCode::JMP_IF_FALSE),
"for(var) without initializer should fail - accessing TDZ variable in test expression" "for(var) no-init should contain JMP_IF_FALSE"
); );
} }
@@ -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<a){c=c+1;} c"), "5");
}
// Nested array assignment target element.
#[test]
fn array_assignment_nested_element() {
assert_eq!(eval("var a,b; [[a],b]=[[1],2]; a*10+b"), "12");
}
// Destructuring catch param followed by a loop: the counter must not count a STORE_VAR the
// emitter does not emit for a pattern catch binding.
#[test]
fn destructuring_catch_param_then_loop() {
assert_eq!(eval("var c=0; try{throw 1;}catch({e}){} while(c<3){c=c+1;} c"), "3");
}
// Regression: for-init declaration without an initializer followed by loop iterations.
#[test]
fn for_init_no_initializer_declarator() {
assert_eq!(eval("var c=0; for(var i; c<3; c=c+1){} c"), "3");
}
// Identifier element array assignment stays correct (simple path unchanged).
#[test]
fn array_assignment_identifier_elements() {
assert_eq!(eval("var a,b; [a,b]=[7,8]; a*10+b"), "78");
}