diff --git a/crates/oxide_compiler/src/compiler.rs b/crates/oxide_compiler/src/compiler.rs index 0e08b19..08ff3ac 100644 --- a/crates/oxide_compiler/src/compiler.rs +++ b/crates/oxide_compiler/src/compiler.rs @@ -160,6 +160,10 @@ pub(crate) struct CompileCtx { pub(crate) static_block_this_reg: Option, pub(crate) after_super_insert: Option>, pub(crate) after_super_inserted: bool, + /// Count-pass mirror of `after_super_insert`: the instruction count of a derived + /// constructor's instance-field code, added at the super() call site during body + /// counting so projected_pc matches where the emit pass splices the field bytecode. + pub(crate) after_super_count_words: Option, /// Set when alloc_reg() overflows into the reserved this/new.target range (≥254). /// Checked after each emit phase to produce a compile error rather than silent corruption. pub(crate) reg_overflow: bool, @@ -233,6 +237,7 @@ impl CompileCtx { static_block_this_reg: None, after_super_insert: None, after_super_inserted: false, + after_super_count_words: None, reg_overflow: false, const_overflow: false, jump_overflow: false, @@ -731,20 +736,25 @@ impl Compiler { } } - // Count pass - if !fields_after_super { - if let Some(count) = count_fields.as_mut() { + // Count pass. For derived constructors the emit pass allocates/counts the instance + // fields up front but splices the field bytecode into the body right after SUPER_CALL. + // Mirror that: count the fields (allocating the same registers/constants), capture + // their instruction count, undo the position here, and re-add it at the super() call + // site during body counting so labels after super() line up with the emit pass. + if let Some(count) = count_fields.as_mut() { + if fields_after_super { + let before = ctx.projected_pc; + count(self, &mut ctx); + let field_words = ctx.projected_pc - before; + ctx.projected_pc = before; + ctx.after_super_count_words = Some(field_words); + } else { count(self, &mut ctx); } } for stmt in body_stmts { self.count_statement(stmt, &mut ctx); } - if fields_after_super { - if let Some(count) = count_fields.as_mut() { - count(self, &mut ctx); - } - } ctx.max_regs = ctx.max_regs.max(1); ctx.reg_overflow = false; ctx.reset_regs(); diff --git a/crates/oxide_compiler/src/counter/expr/call.rs b/crates/oxide_compiler/src/counter/expr/call.rs index 9a1015a..e7df509 100644 --- a/crates/oxide_compiler/src/counter/expr/call.rs +++ b/crates/oxide_compiler/src/counter/expr/call.rs @@ -13,6 +13,10 @@ impl Compiler { } ctx.alloc_reg(); ctx.count_call_instr_with_arg_ext(); // SUPER_CALL + arg_count ext word + // Mirror emit: derived-ctor instance fields are injected right after super(). + if let Some(words) = ctx.after_super_count_words.take() { + ctx.projected_pc += words; + } return; } diff --git a/crates/oxide_compiler/src/counter/helper/class.rs b/crates/oxide_compiler/src/counter/helper/class.rs index bb12136..51b125b 100644 --- a/crates/oxide_compiler/src/counter/helper/class.rs +++ b/crates/oxide_compiler/src/counter/helper/class.rs @@ -35,15 +35,19 @@ impl Compiler { continue; } if matches!(method.key, oxide_parser::PropertyKey::PrivateIdentifier(_)) { - if method.r#static { - ctx.count_load_const(); // private id - ctx.count_load_const(); // method - ctx.count_instr(); // INIT_PRIVATE - } + // Mirror emit_private_method_init for static AND non-static private + // methods: private-id LOAD_CONST + CREATE_CLOSURE + SET_HOME_OBJECT + + // INIT_PRIVATE. + ctx.count_load_const(); // private id reg + ctx.count_load_const(); // method closure (CREATE_CLOSURE) + ctx.count_instr(); // SET_HOME_OBJECT + ctx.count_instr(); // INIT_PRIVATE continue; } - ctx.count_load_const(); // key_reg - ctx.count_load_const(); // method_reg + // Computed method key emits the full key expression; a plain key is one + // LOAD_CONST. Mirror emit_class_key_reg via the shared count_class_key. + self.count_class_key(&method.key, method.computed, ctx); + ctx.count_load_const(); // method closure (CREATE_CLOSURE) ctx.count_instr(); // SET_HOME_OBJECT match method.kind { MethodDefinitionKind::Method => { @@ -59,7 +63,9 @@ impl Compiler { ClassElement::PropertyDefinition(prop) => { let prop = prop.as_ref(); if prop.r#static { - ctx.count_load_const(); // key_reg + // Computed static field key emits the full key expression; mirror + // emit_public_field_init via the shared count_class_key. + self.count_class_key(&prop.key, prop.computed, ctx); if let Some(value) = &prop.value { self.count_expression(value, ctx); } else { diff --git a/crates/oxide_compiler/src/emitter/mod.rs b/crates/oxide_compiler/src/emitter/mod.rs index 83805fb..a8a45de 100644 --- a/crates/oxide_compiler/src/emitter/mod.rs +++ b/crates/oxide_compiler/src/emitter/mod.rs @@ -247,7 +247,7 @@ impl Compiler { self.emit_expression(key.to_expression(), ctx) } - fn count_class_key(&self, key: &PropertyKey, computed: bool, ctx: &mut CompileCtx) { + pub(crate) fn count_class_key(&self, key: &PropertyKey, computed: bool, ctx: &mut CompileCtx) { if computed { self.count_expression(key.to_expression(), ctx); } else { diff --git a/crates/oxide_vm/tests/compiler_alignment_class_tests.rs b/crates/oxide_vm/tests/compiler_alignment_class_tests.rs new file mode 100644 index 0000000..ab22f34 --- /dev/null +++ b/crates/oxide_vm/tests/compiler_alignment_class_tests.rs @@ -0,0 +1,93 @@ +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}"), + } +} + +// A class body must not drift the instruction count of code that follows it. Each repro +// places a bounded loop after the class so a wrong count corrupts the loop's jump target. + +#[test] +fn non_static_private_method_then_loop() { + assert_eq!(eval("class C{#m(){return 1;}} var c=0; while(c<3){c=c+1;} c"), "3"); +} + +#[test] +fn static_private_method_then_loop() { + assert_eq!(eval("class C{static #m(){return 1;}} var c=0; while(c<3){c=c+1;} c"), "3"); +} + +// Multi-word computed key — a single-identifier key happens to be one word and would not +// expose the drift. +#[test] +fn computed_method_key_then_loop() { + assert_eq!(eval("class C{[\"x\"+\"y\"](){return 1;}} var c=0; while(c<3){c=c+1;} c"), "3"); +} + +#[test] +fn computed_static_field_key_then_loop() { + assert_eq!(eval("class C{static [\"a\"+\"b\"]=1;} var c=0; while(c<3){c=c+1;} c"), "3"); +} + +// Derived constructor with an instance field and a loop after super(): the field code is +// injected after SUPER_CALL, so the loop's jump target must account for it. +#[test] +fn derived_ctor_field_then_post_super_loop() { + assert_eq!( + eval("class A{} class B extends A{ x=2; constructor(){super(); this.r=0; while(this.r<3){this.r=this.r+1;}}} new B().r"), + "3" + ); +} + +#[test] +fn derived_ctor_field_value_initialized() { + assert_eq!(eval("class A{} class B extends A{ x=5; constructor(){super();} } new B().x"), "5"); +} + +#[test] +fn derived_ctor_statement_before_super() { + assert_eq!( + eval("class A{constructor(){this.a=1;}} class B extends A{ y=2; constructor(){var z=0; super(); while(z<3){z=z+1;} this.z=z;} } new B().z"), + "3" + ); +} + +// Non-derived class with an instance field and a ctor loop stays correct (unchanged path). +#[test] +fn non_derived_ctor_field_then_loop() { + assert_eq!( + eval("class C{ x=1; constructor(){this.r=0; while(this.r<3){this.r=this.r+1;}} } new C().r"), + "3" + ); +} + +// Behavioral regressions: methods/getters/private calls still work. +#[test] +fn plain_method_call_regression() { + assert_eq!(eval("class C{m(){return 9;}} new C().m()"), "9"); +} + +#[test] +fn private_method_call_regression() { + assert_eq!(eval("class C{#m(){return 7;} run(){return this.#m();}} new C().run()"), "7"); +} + +#[test] +fn getter_regression() { + assert_eq!(eval("class C{get x(){return 5;}} new C().x"), "5"); +}