diff --git a/crates/oxide_compiler/src/compiler.rs b/crates/oxide_compiler/src/compiler.rs index b741ffb6..0a094958 100644 --- a/crates/oxide_compiler/src/compiler.rs +++ b/crates/oxide_compiler/src/compiler.rs @@ -70,6 +70,8 @@ pub(crate) struct CompileCtx { /// Used by arrow functions to capture lexical `this` (D-01). /// Initialized to 254 (conventional this register) at the top level. pub(crate) enclosing_this_reg: u8, + pub(crate) in_derived_constructor: bool, + pub(crate) in_instance_method: bool, } impl CompileCtx { @@ -89,6 +91,8 @@ impl CompileCtx { builtin_reg_map: Vec::new(), sub_modules: Vec::new(), enclosing_this_reg: 254, // conventional this register at top level + in_derived_constructor: false, + in_instance_method: false, } } @@ -297,6 +301,8 @@ impl Compiler { // Propagate enclosing_this_reg so nested arrow functions capture the correct `this`. ctx.enclosing_this_reg = parent_ctx.enclosing_this_reg; + ctx.in_derived_constructor = parent_ctx.in_derived_constructor; + ctx.in_instance_method = parent_ctx.in_instance_method; // Inherit parent's global scope entries so previously-declared function names // are visible from within the body. @@ -402,6 +408,8 @@ impl Compiler { captured_this_const_idx: 0, function_name: None, is_class_constructor: false, + is_derived_constructor: false, + needs_home_object: false, }) } @@ -460,6 +468,8 @@ impl Compiler { captured_this_const_idx: 0, function_name: None, is_class_constructor: false, + is_derived_constructor: false, + needs_home_object: false, }) } } diff --git a/crates/oxide_compiler/src/counter.rs b/crates/oxide_compiler/src/counter.rs index 7c21c0d4..30aecc36 100644 --- a/crates/oxide_compiler/src/counter.rs +++ b/crates/oxide_compiler/src/counter.rs @@ -9,8 +9,18 @@ impl Compiler { fn count_class(&self, class: &oxide_parser::Class, ctx: &mut CompileCtx) { ctx.alloc_reg(); // ctor_reg ctx.alloc_reg(); // proto_reg + if let Some(super_class) = &class.super_class { + self.count_expression(super_class, ctx); + } ctx.projected_pc += 2; // LOAD_CONST ctor + NEW_OBJECT proto + if class.super_class.is_some() { + ctx.alloc_reg(); // parent prototype key + ctx.alloc_reg(); // parent prototype value + ctx.alloc_reg(); // __proto__ key + ctx.projected_pc += 5; // LOAD_CONST + GET_PROP + LOAD_CONST + two SET_PROP + } + for element in &class.body.body { let ClassElement::MethodDefinition(method) = element else { continue; @@ -21,7 +31,7 @@ impl Compiler { MethodDefinitionKind::Method => { ctx.alloc_reg(); // method_reg ctx.alloc_reg(); // key_reg - ctx.projected_pc += 3; // LOAD_CONST method + LOAD_CONST key + SET_PROP + ctx.projected_pc += 4; // LOAD_CONST method + SET_HOME_OBJECT + LOAD_CONST key + SET_PROP } MethodDefinitionKind::Get | MethodDefinitionKind::Set => {} } @@ -335,13 +345,32 @@ impl Compiler { } } Expression::CallExpression(call) => { + if matches!(&call.callee, Expression::Super(_)) { + for arg in &call.arguments { + if let Some(expr) = arg.as_expression() { + self.count_expression(expr, ctx); + } + } + ctx.alloc_reg(); + ctx.projected_pc += 2; // SUPER_CALL + arg_count ext word + return; + } match &call.callee { Expression::StaticMemberExpression(member) => { - self.count_expression(&member.object, ctx); - ctx.alloc_reg(); - ctx.projected_pc += 1; - ctx.alloc_reg(); - ctx.projected_pc += 5; // LOAD_VAR + IC_GET_PROP + 3 ext words + if matches!(&member.object, Expression::Super(_)) { + ctx.alloc_reg(); // this register + ctx.projected_pc += 1; // LOAD_VAR this + ctx.alloc_reg(); // key + ctx.projected_pc += 1; // LOAD_CONST key + ctx.alloc_reg(); // callee + ctx.projected_pc += 1; // SUPER_GET_PROP + } else { + self.count_expression(&member.object, ctx); + ctx.alloc_reg(); + ctx.projected_pc += 1; + ctx.alloc_reg(); + ctx.projected_pc += 5; // LOAD_VAR + IC_GET_PROP + 3 ext words + } } _ => { self.count_expression(&call.callee, ctx); @@ -547,6 +576,15 @@ impl Compiler { } } Expression::StaticMemberExpression(member) => { + if matches!(&member.object, Expression::Super(_)) { + ctx.alloc_reg(); // key + ctx.projected_pc += 1; // LOAD_CONST key + ctx.alloc_reg(); // this + ctx.projected_pc += 1; // LOAD_VAR this + ctx.alloc_reg(); // result + ctx.projected_pc += 1; // SUPER_GET_PROP + return; + } self.count_expression(&member.object, ctx); ctx.alloc_reg(); ctx.projected_pc += 1; // LOAD_CONST key diff --git a/crates/oxide_compiler/src/emitter.rs b/crates/oxide_compiler/src/emitter.rs index 2950aeae..75c4a1b0 100644 --- a/crates/oxide_compiler/src/emitter.rs +++ b/crates/oxide_compiler/src/emitter.rs @@ -19,12 +19,9 @@ impl Compiler { } fn emit_class(&self, class: &Class, ctx: &mut CompileCtx) -> Result { - if class.super_class.is_some() { - return Err("class extends not yet supported".into()); - } - let mut constructor_method = None; let mut instance_methods = Vec::new(); + let is_derived = class.super_class.is_some(); for element in &class.body.body { let ClassElement::MethodDefinition(method) = element else { @@ -59,14 +56,33 @@ impl Compiler { let ctor_reg = ctx.alloc_reg(); let proto_reg = ctx.alloc_reg(); let self_binding = ctor_name.as_deref().map(|name| vec![(name, ctor_reg)]).unwrap_or_default(); + let super_reg = if let Some(super_expr) = &class.super_class { + Some(self.emit_expression(super_expr, ctx)?) + } else { + None + }; + + let saved_derived = ctx.in_derived_constructor; + ctx.in_derived_constructor = is_derived; let mut ctor_module = if let Some(method) = constructor_method { let (param_names, body_stmts) = self.extract_function_parts(method.value.as_ref())?; self.compile_function_body_with_bindings(¶m_names, body_stmts, ctx, false, &self_binding)? } else { - self.compile_function_body_with_bindings(&[], &[], ctx, false, &self_binding)? + let mut module = self.compile_function_body_with_bindings(&[], &[], ctx, false, &self_binding)?; + if is_derived { + module.bytecode.clear(); + module.constants.clear(); + module.n_registers = 1; + module.bytecode.push(opcode::encode(OpCode::SUPER_CALL, 0, 0, 0)); + module.bytecode.push(0); + module.bytecode.push(opcode::encode(OpCode::RETURN, 0, 0, 0)); + } + module }; + ctx.in_derived_constructor = saved_derived; ctor_module.is_class_constructor = true; + ctor_module.is_derived_constructor = is_derived; ctor_module.function_name = ctor_name.clone(); ctx.sub_modules.push(ctor_module); @@ -74,16 +90,35 @@ impl Compiler { ctx.emit_load_const(ctor_reg, ctor_idx); ctx.emit(opcode::encode(OpCode::NEW_OBJECT, proto_reg, 0, 0)); + if let Some(super_reg) = super_reg { + let proto_key_idx = ctx.add_constant(Constant::String("prototype".to_string())); + let parent_proto_key_reg = ctx.alloc_reg(); + ctx.emit_load_const(parent_proto_key_reg, proto_key_idx); + let parent_proto_reg = ctx.alloc_reg(); + ctx.emit(opcode::encode(OpCode::GET_PROP, super_reg, parent_proto_reg, parent_proto_key_reg)); + + let proto_link_idx = ctx.add_constant(Constant::String("__proto__".to_string())); + let proto_link_key_reg = ctx.alloc_reg(); + ctx.emit_load_const(proto_link_key_reg, proto_link_idx); + ctx.emit(opcode::encode(OpCode::SET_PROP, proto_reg, parent_proto_reg, proto_link_key_reg)); + ctx.emit(opcode::encode(OpCode::SET_PROP, ctor_reg, super_reg, proto_link_key_reg)); + } + for (method, method_name) in instance_methods { let (param_names, body_stmts) = self.extract_function_parts(method.value.as_ref())?; + let saved_method = ctx.in_instance_method; + ctx.in_instance_method = true; let mut method_module = self.compile_function_body_with_bindings(¶m_names, body_stmts, ctx, false, &self_binding)?; + ctx.in_instance_method = saved_method; method_module.function_name = Some(method_name.clone()); + method_module.needs_home_object = true; ctx.sub_modules.push(method_module); let method_idx = ctx.add_constant(Constant::BytecodeFunc(ctx.sub_modules.len() as u32)); let method_reg = ctx.alloc_reg(); ctx.emit_load_const(method_reg, method_idx); + ctx.emit(opcode::encode(OpCode::SET_HOME_OBJECT, method_reg, proto_reg, 0)); let key_idx = ctx.add_constant(Constant::String(method_name)); let key_reg = ctx.alloc_reg(); @@ -753,6 +788,20 @@ impl Compiler { } } Expression::StaticMemberExpression(member) => { + if matches!(&member.object, Expression::Super(_)) { + if !ctx.in_instance_method && !ctx.in_derived_constructor { + return Err("super property only supported in class methods".into()); + } + let prop_name = member.property.name.as_str(); + let idx = ctx.add_constant(Constant::String(prop_name.to_string())); + let key_reg = ctx.alloc_reg(); + ctx.emit_load_const(key_reg, idx); + let this_reg = ctx.alloc_reg(); + ctx.emit(opcode::encode(OpCode::LOAD_VAR, this_reg, 254, 0)); + let result_reg = ctx.alloc_reg(); + ctx.emit(opcode::encode(OpCode::SUPER_GET_PROP, result_reg, this_reg, key_reg)); + return Ok(result_reg); + } let obj_reg = self.emit_expression(&member.object, ctx)?; let prop_name = member.property.name.as_str(); let idx = ctx.add_constant(Constant::String(prop_name.to_string())); @@ -1139,19 +1188,49 @@ impl Compiler { } Expression::ParenthesizedExpression(p) => self.emit_expression(&p.expression, ctx), Expression::CallExpression(call) => { + if matches!(&call.callee, Expression::Super(_)) { + if !ctx.in_derived_constructor { + return Err("super() only supported in derived constructors".into()); + } + let mut arg_regs = Vec::new(); + for arg in &call.arguments { + if let Some(expr) = arg.as_expression() { + arg_regs.push(self.emit_expression(expr, ctx)?); + } + } + let first_arg_reg = if arg_regs.is_empty() { 0u8 } else { arg_regs[0] }; + let result_reg = ctx.alloc_reg(); + ctx.emit(opcode::encode(OpCode::SUPER_CALL, result_reg, first_arg_reg, 0)); + ctx.emit(arg_regs.len() as u32); + return Ok(result_reg); + } let (callee_reg, this_reg) = match &call.callee { Expression::StaticMemberExpression(member) => { - let obj_reg = self.emit_expression(&member.object, ctx)?; + let is_super_member = matches!(&member.object, Expression::Super(_)); + let obj_reg = if is_super_member { + let this_reg = ctx.alloc_reg(); + ctx.emit(opcode::encode(OpCode::LOAD_VAR, this_reg, 254, 0)); + this_reg + } else { + self.emit_expression(&member.object, ctx)? + }; let prop_name = member.property.name.as_str(); let idx = ctx.add_constant(Constant::String(prop_name.to_string())); let key_reg = ctx.alloc_reg(); ctx.emit_load_const(key_reg, idx); let callee_reg = ctx.alloc_reg(); - ctx.emit(opcode::encode(OpCode::LOAD_VAR, callee_reg, obj_reg, 0)); - ctx.emit(opcode::encode(OpCode::IC_GET_PROP, 0, callee_reg, key_reg)); - ctx.emit(0); - ctx.emit(0); - ctx.emit(0); + if is_super_member { + if !ctx.in_instance_method && !ctx.in_derived_constructor { + return Err("super property only supported in class methods".into()); + } + ctx.emit(opcode::encode(OpCode::SUPER_GET_PROP, callee_reg, obj_reg, key_reg)); + } else { + ctx.emit(opcode::encode(OpCode::LOAD_VAR, callee_reg, obj_reg, 0)); + ctx.emit(opcode::encode(OpCode::IC_GET_PROP, 0, callee_reg, key_reg)); + ctx.emit(0); + ctx.emit(0); + ctx.emit(0); + } (callee_reg, obj_reg) } _ => { diff --git a/crates/oxide_compiler/src/hash.rs b/crates/oxide_compiler/src/hash.rs index ace4ab6d..dbf9ea0b 100644 --- a/crates/oxide_compiler/src/hash.rs +++ b/crates/oxide_compiler/src/hash.rs @@ -123,6 +123,9 @@ fn hash_statement(stmt: &Statement, h: &mut rustc_hash::FxHasher) { id.name.as_str().hash(h); } class.super_class.is_some().hash(h); + if let Some(super_class) = &class.super_class { + hash_expression(super_class, h); + } for element in &class.body.body { hash_class_element(element, h); } @@ -319,6 +322,9 @@ fn hash_expression(expr: &Expression, h: &mut rustc_hash::FxHasher) { id.name.as_str().hash(h); } class.super_class.is_some().hash(h); + if let Some(super_class) = &class.super_class { + hash_expression(super_class, h); + } for element in &class.body.body { hash_class_element(element, h); } @@ -333,6 +339,9 @@ fn hash_expression(expr: &Expression, h: &mut rustc_hash::FxHasher) { } } } + Expression::Super(_) => { + 19u8.hash(h); + } Expression::RegExpLiteral(lit) => { if let Some(raw) = &lit.raw { raw.to_string().hash(h); diff --git a/crates/oxide_compiler/src/module.rs b/crates/oxide_compiler/src/module.rs index 225a5234..ac4cd0ea 100644 --- a/crates/oxide_compiler/src/module.rs +++ b/crates/oxide_compiler/src/module.rs @@ -34,6 +34,11 @@ pub struct CompiledModule { /// True when this bytecode function is a class constructor. /// Ordinary CALL must reject it, while NEW_EXPRESSION may construct through it. pub is_class_constructor: bool, + /// True when this class constructor has an `extends` clause. + /// `this` stays uninitialized until SUPER_CALL completes. + pub is_derived_constructor: bool, + /// True for prototype methods whose function object needs a runtime home_object. + pub needs_home_object: bool, } impl CompiledModule { @@ -50,6 +55,8 @@ impl CompiledModule { captured_this_const_idx: 0, function_name: None, is_class_constructor: false, + is_derived_constructor: false, + needs_home_object: false, } } } @@ -74,6 +81,8 @@ impl Clone for CompiledModule { captured_this_const_idx: self.captured_this_const_idx, function_name: self.function_name.clone(), is_class_constructor: self.is_class_constructor, + is_derived_constructor: self.is_derived_constructor, + needs_home_object: self.needs_home_object, } } } diff --git a/crates/oxide_compiler/src/opcode.rs b/crates/oxide_compiler/src/opcode.rs index e3a41e96..dd4d1774 100644 --- a/crates/oxide_compiler/src/opcode.rs +++ b/crates/oxide_compiler/src/opcode.rs @@ -84,6 +84,10 @@ pub enum OpCode { RETURN = 0x41, CALL_NATIVE = 0x42, NEW_EXPRESSION = 0x43, + SUPER_CALL = 0x44, + SUPER_GET_PROP = 0x45, + SUPER_STATIC_GET_PROP = 0x46, + SET_HOME_OBJECT = 0x47, // ── Object Property (0x50-0x5F) ── IC_GET_PROP = 0x50, @@ -217,6 +221,10 @@ impl TryFrom for OpCode { 0x41 => Ok(OpCode::RETURN), 0x42 => Ok(OpCode::CALL_NATIVE), 0x43 => Ok(OpCode::NEW_EXPRESSION), + 0x44 => Ok(OpCode::SUPER_CALL), + 0x45 => Ok(OpCode::SUPER_GET_PROP), + 0x46 => Ok(OpCode::SUPER_STATIC_GET_PROP), + 0x47 => Ok(OpCode::SET_HOME_OBJECT), 0x50 => Ok(OpCode::IC_GET_PROP), 0x51 => Ok(OpCode::IC_SET_PROP), 0x52 => Ok(OpCode::GET_PROP), @@ -311,6 +319,10 @@ impl fmt::Display for OpCode { OpCode::RETURN => "RETURN", OpCode::CALL_NATIVE => "CALL_NATIVE", OpCode::NEW_EXPRESSION => "NEW_EXPRESSION", + OpCode::SUPER_CALL => "SUPER_CALL", + OpCode::SUPER_GET_PROP => "SUPER_GET_PROP", + OpCode::SUPER_STATIC_GET_PROP => "SUPER_STATIC_GET_PROP", + OpCode::SET_HOME_OBJECT => "SET_HOME_OBJECT", OpCode::IC_GET_PROP => "IC_GET_PROP", OpCode::IC_SET_PROP => "IC_SET_PROP", OpCode::GET_PROP => "GET_PROP", diff --git a/crates/oxide_types/src/object.rs b/crates/oxide_types/src/object.rs index 709dd2eb..173101e0 100644 --- a/crates/oxide_types/src/object.rs +++ b/crates/oxide_types/src/object.rs @@ -37,9 +37,10 @@ impl PropIndex for i32 { } } -/// Layout (56B): +/// Layout: /// header: u32 bits /// [0:23] shape_id +/// [26] is_derived_constructor /// [27] is_class_constructor /// [28] is_arrow /// [29] is_array @@ -52,6 +53,7 @@ impl PropIndex for i32 { /// native_fn: u64 (8 bytes, 0 = None sentinel) /// sub_module_index: u32 (4 bytes + 4 pad, index into CompiledModule.sub_modules) /// captured_this: JsValue (8 bytes, lexical this for arrow functions) +/// home_object: JsValue (8 bytes, [[HomeObject]] for super lookup) #[repr(C)] pub struct JsObject { header: u32, @@ -65,6 +67,7 @@ pub struct JsObject { sub_module_index: u32, _pad3: [u8; 4], captured_this: JsValue, + home_object: JsValue, } impl JsObject { @@ -81,6 +84,7 @@ impl JsObject { sub_module_index: 0, _pad3: [0; 4], captured_this: JsValue::undefined(), + home_object: JsValue::undefined(), } } @@ -97,6 +101,7 @@ impl JsObject { sub_module_index: 0, _pad3: [0; 4], captured_this: JsValue::undefined(), + home_object: JsValue::undefined(), }; let vec = Box::new(vec![Box::new(JsValue::undefined()); n_elements]); obj.hash_props = Box::into_raw(vec) as *mut u8; @@ -333,6 +338,26 @@ impl JsObject { self.header &= !(1 << 27); } } + + pub fn is_derived_constructor(&self) -> bool { + (self.header >> 26) & 1 != 0 + } + + pub fn set_derived_constructor(&mut self, v: bool) { + if v { + self.header |= 1 << 26; + } else { + self.header &= !(1 << 26); + } + } + + pub fn home_object(&self) -> JsValue { + self.home_object + } + + pub fn set_home_object(&mut self, v: JsValue) { + self.home_object = v; + } } #[cfg(test)] @@ -343,7 +368,7 @@ mod tests { #[test] fn object_size_bounds() { let sz = std::mem::size_of::(); - assert!(sz >= 40 && sz <= 56, "JsObject layout mismatch - expected 40-56B, got {sz}B"); + assert!(sz >= 40 && sz <= 72, "JsObject layout mismatch - expected 40-72B, got {sz}B"); } #[test] diff --git a/crates/oxide_vm/src/builtins/error.rs b/crates/oxide_vm/src/builtins/error.rs index 5cff659b..a0f0e12d 100644 --- a/crates/oxide_vm/src/builtins/error.rs +++ b/crates/oxide_vm/src/builtins/error.rs @@ -170,8 +170,10 @@ mod tests { saved_regs: vec![JsValue::undefined()].into_boxed_slice(), saved_this: JsValue::undefined(), saved_new_target: JsValue::undefined(), + callee: JsValue::undefined(), construct_result_reg: None, constructed_this: None, + is_derived_constructor: false, }); let stack = error_stack_getter(&mut vm, &[0]).unwrap(); diff --git a/crates/oxide_vm/src/dispatch/property.rs b/crates/oxide_vm/src/dispatch/property.rs index 15021e05..5d9c890f 100644 --- a/crates/oxide_vm/src/dispatch/property.rs +++ b/crates/oxide_vm/src/dispatch/property.rs @@ -90,8 +90,15 @@ impl Vm { return Ok(()); } - let obj = unsafe { &mut *obj_ptr }; let prop_name_si = self.property_key_si(self.regs[b]); + if self.kernel.string_forge().lookup(prop_name_si).as_deref() == Some("__proto__") { + let obj = unsafe { &mut *obj_ptr }; + obj.set_proto(self.regs[a]).map_err(|e| e.to_string())?; + self.pc += 3; + return Ok(()); + } + + let obj = unsafe { &mut *obj_ptr }; let ext0 = self.bytecode[self.pc]; let ext1 = self.bytecode[self.pc + 1]; let ext2 = self.bytecode[self.pc + 2]; @@ -150,6 +157,10 @@ impl Vm { } let obj = unsafe { &mut *obj_ptr }; let prop_name_si = self.property_key_si(self.regs[b]); + if self.kernel.string_forge().lookup(prop_name_si).as_deref() == Some("__proto__") { + obj.set_proto(self.regs[a]).map_err(|e| e.to_string())?; + return Ok(()); + } if let Some(pos) = self.kernel.shape_forge().lookup_position(obj.shape_id(), prop_name_si) { obj.set_prop_at(pos, self.regs[a]); } else { @@ -181,6 +192,10 @@ impl Vm { } let obj = unsafe { &mut *obj_ptr }; let prop_name_si = self.property_key_si(self.regs[a]); + if self.kernel.string_forge().lookup(prop_name_si).as_deref() == Some("__proto__") { + obj.set_proto(self.regs[b]).map_err(|e| e.to_string())?; + return Ok(()); + } if let Some(pos) = self.kernel.shape_forge().lookup_position(obj.shape_id(), prop_name_si) { obj.set_prop_at(pos, self.regs[b]); } else { diff --git a/crates/oxide_vm/src/vm.rs b/crates/oxide_vm/src/vm.rs index 3163bda7..6ba5ad35 100644 --- a/crates/oxide_vm/src/vm.rs +++ b/crates/oxide_vm/src/vm.rs @@ -50,8 +50,10 @@ pub struct CallFrame { pub saved_regs: Box<[JsValue]>, pub saved_this: JsValue, pub saved_new_target: JsValue, + pub callee: JsValue, pub construct_result_reg: Option, pub constructed_this: Option, + pub is_derived_constructor: bool, } pub struct ForInIter<'bump> { @@ -246,13 +248,18 @@ impl Vm { /// Create a function JsObject for a BytecodeFunc constant. /// When `is_arrow` is true, captures the current `this` (regs[254]) /// for lexical this binding at call time (D-01). - fn create_function_object(&mut self, sub_idx: u32, is_arrow: bool, is_class_constructor: bool) -> JsValue { + fn create_function_object( + &mut self, sub_idx: u32, is_arrow: bool, is_class_constructor: bool, is_derived_constructor: bool, + needs_home_object: bool, + ) -> JsValue { let func_proto_ptr = self.kernel.builtin_world().function_proto.as_ptr() as *mut JsObject; let proto_val = JsValue::from_js_object(func_proto_ptr); let mut obj = JsObject::new_empty(EMPTY_SHAPE_ID, proto_val); obj.set_function(true); obj.set_sub_module_index(sub_idx); obj.set_class_constructor(is_class_constructor); + obj.set_derived_constructor(is_derived_constructor); + let _ = needs_home_object; if is_arrow { obj.set_arrow(true); // Capture lexical `this` from the enclosing scope (regs[254]). @@ -293,13 +300,25 @@ impl Vm { Constant::Undefined => Ok(JsValue::undefined()), Constant::BytecodeFunc(idx) => { let sub_idx = *idx as usize; - let (is_arrow, is_class_constructor) = if sub_idx > 0 && sub_idx <= self.sub_modules.len() { - let sub_module = &self.sub_modules[sub_idx - 1]; - (sub_module.is_arrow, sub_module.is_class_constructor) - } else { - (false, false) - }; - Ok(self.create_function_object(*idx, is_arrow, is_class_constructor)) + let (is_arrow, is_class_constructor, is_derived_constructor, needs_home_object) = + if sub_idx > 0 && sub_idx <= self.sub_modules.len() { + let sub_module = &self.sub_modules[sub_idx - 1]; + ( + sub_module.is_arrow, + sub_module.is_class_constructor, + sub_module.is_derived_constructor, + sub_module.needs_home_object, + ) + } else { + (false, false, false, false) + }; + Ok(self.create_function_object( + *idx, + is_arrow, + is_class_constructor, + is_derived_constructor, + needs_home_object, + )) } Constant::RegExp(pattern, flags) => { let pat_si = self.kernel.string_forge().intern(pattern).0; @@ -697,6 +716,12 @@ impl Vm { } OpCode::LOAD_VAR => { + if a == 254 + && self.frames.last().map(|frame| frame.is_derived_constructor).unwrap_or(false) + && self.regs[a].is_undefined() + { + throw_err!(self, ReferenceError, "must call super constructor before using 'this'"); + } self.regs[rd] = self.regs[a]; } @@ -790,8 +815,10 @@ impl Vm { saved_regs, saved_this, saved_new_target, + callee, construct_result_reg: None, constructed_this: None, + is_derived_constructor: false, }); self.bytecode = sub_bytecode; @@ -948,7 +975,11 @@ impl Vm { let src_reg = first_arg_reg.wrapping_add(i as u8) as usize; self.regs[sub_param_base + i] = self.regs[src_reg]; } - self.regs[254] = new_obj_val; + self.regs[254] = if ctor_obj.is_derived_constructor() { + JsValue::undefined() + } else { + new_obj_val + }; self.regs[255] = constructor; let converted_sub_constants = self.convert_constants(&sub_constants)?; @@ -967,8 +998,10 @@ impl Vm { saved_regs, saved_this, saved_new_target, + callee: constructor, construct_result_reg: Some(rd as u8), constructed_this: Some(new_obj_val), + is_derived_constructor: ctor_obj.is_derived_constructor(), }); self.bytecode = sub_bytecode; @@ -999,15 +1032,173 @@ impl Vm { } } + OpCode::SUPER_CALL => { + let first_arg_reg = a as u8; + let ext = self.bytecode[self.pc]; + self.pc += 1; + let arg_count = (ext & 0xFF) as usize; + + let Some(frame) = self.frames.last() else { + throw_err!(self, ReferenceError, "super() used outside class constructor"); + }; + if !frame.is_derived_constructor { + throw_err!(self, ReferenceError, "super() used outside derived constructor"); + } + if !self.regs[254].is_undefined() { + throw_err!(self, ReferenceError, "super() called more than once"); + } + let Some(derived_this) = frame.constructed_this else { + throw_err!(self, ReferenceError, "super() without derived this"); + }; + + let new_target = self.regs[255]; + if !new_target.is_object() { + throw_err!(self, TypeError, "super() new.target is not an object"); + } + let new_target_obj = unsafe { &*new_target.as_js_object_ptr() }; + let super_ctor = new_target_obj.proto(); + if !super_ctor.is_object() { + throw_err!(self, TypeError, "super constructor is not an object"); + } + let super_obj = unsafe { &*super_ctor.as_js_object_ptr() }; + if !super_obj.is_function() { + throw_err!(self, TypeError, "super constructor is not a function"); + } + + if super_obj.native_fn().is_some() { + self.regs[253] = derived_this; + let (args_buf, len) = Self::build_native_args(first_arg_reg, arg_count, 253); + let func: NativeFn = unsafe { std::mem::transmute(super_obj.native_fn().unwrap()) }; + match func(self, &args_buf[..len]) { + Ok(val) => { + self.regs[254] = if val.is_object() { val } else { derived_this }; + self.regs[rd] = self.regs[254]; + } + Err(err_val) => { + self.exception_value = Some(err_val); + self.pending_error_kind = Some("Error"); + match self.unwind() { + Ok(()) => continue, + Err(e) => return Err(e), + } + } + } + } else if super_obj.sub_module_index() > 0 { + let sub_idx = super_obj.sub_module_index() as usize - 1; + if sub_idx >= self.sub_modules.len() { + return Err(format!( + "SUPER_CALL: sub_module_index {} out of bounds (max {})", + sub_idx, + self.sub_modules.len() + )); + } + if self.frames.len() >= self.kernel.config.max_call_depth { + return Err("RangeError: Maximum call stack size exceeded".into()); + } + + let sub_bytecode = self.sub_modules[sub_idx].bytecode.clone(); + let sub_n_args = self.sub_modules[sub_idx].n_args as usize; + let sub_n_registers = self.sub_modules[sub_idx].n_registers; + let sub_constants = self.sub_modules[sub_idx].constants.clone(); + let sub_param_base = self.sub_modules[sub_idx].param_base as usize; + let caller_reg_limit = self.active_reg_limit.max(1); + let saved_regs = self.regs[..caller_reg_limit as usize].to_vec().into_boxed_slice(); + let saved_this = self.regs[254]; + let saved_new_target = self.regs[255]; + + for i in 0..sub_n_args { + let src_reg = first_arg_reg.wrapping_add(i as u8) as usize; + self.regs[sub_param_base + i] = self.regs[src_reg]; + } + self.regs[254] = derived_this; + self.regs[255] = new_target; + + let converted_sub_constants = self.convert_constants(&sub_constants)?; + self.saved_bytecode_stack.push(std::mem::take(&mut self.bytecode)); + self.saved_constants_stack.push(std::mem::take(&mut self.constants)); + self.frames.push(CallFrame { + return_addr: self.pc, + function_name: self.sub_modules[sub_idx] + .function_name + .as_deref() + .map(|name| self.kernel.string_forge().intern(name).0) + .unwrap_or(0), + caller_reg_limit, + saved_regs, + saved_this, + saved_new_target, + callee: super_ctor, + construct_result_reg: Some(254), + constructed_this: Some(derived_this), + is_derived_constructor: super_obj.is_derived_constructor(), + }); + + self.bytecode = sub_bytecode; + self.constants = converted_sub_constants; + for (name, reg) in &self.sub_modules[sub_idx].builtin_reg_map { + let si = self.kernel.string_forge().intern(name.as_str()).0; + let global = self.kernel.global_object(); + if let Some(pos) = self.kernel.shape_forge().lookup_position(global.shape_id(), si) { + self.regs[*reg as usize] = global.get_prop_at(pos); + } + } + self.active_reg_limit = sub_n_registers.max(1); + self.pc = 0; + continue; + } else { + throw_err!(self, TypeError, "super constructor is not callable"); + } + } + + OpCode::SUPER_GET_PROP | OpCode::SUPER_STATIC_GET_PROP => { + let key_val = self.regs[b]; + let prop_name_si = self.property_key_si(key_val); + let Some(frame) = self.frames.last() else { + throw_err!(self, ReferenceError, "super property used outside function"); + }; + if !frame.callee.is_object() { + throw_err!(self, ReferenceError, "super property has no home object"); + } + let callee_obj = unsafe { &*frame.callee.as_js_object_ptr() }; + let home_object = callee_obj.home_object(); + if !home_object.is_object() { + throw_err!(self, ReferenceError, "super property has no home object"); + } + let home_obj = unsafe { &*home_object.as_js_object_ptr() }; + let super_base = home_obj.proto(); + if !super_base.is_object() { + self.regs[rd] = JsValue::undefined(); + } else { + let super_obj = unsafe { &*super_base.as_js_object_ptr() }; + self.regs[rd] = self.resolve_property(super_obj, prop_name_si).unwrap_or(JsValue::undefined()); + } + } + + OpCode::SET_HOME_OBJECT => { + let func_val = self.regs[rd]; + let home_val = self.regs[a]; + if !func_val.is_object() || !home_val.is_object() { + throw_err!(self, TypeError, "SET_HOME_OBJECT expects function and object"); + } + let func_obj = unsafe { &mut *func_val.as_js_object_ptr() }; + if !func_obj.is_function() { + throw_err!(self, TypeError, "SET_HOME_OBJECT target is not a function"); + } + func_obj.set_home_object(home_val); + } + OpCode::RETURN => { let result = self.regs[rd]; if let Some(frame) = self.frames.pop() { let construct_result_reg = frame.construct_result_reg; let constructed_this = frame.constructed_this; + let is_derived_constructor = frame.is_derived_constructor; + let callee_this = self.regs[254]; self.restore_frame(frame); - if let (Some(target_reg), Some(constructed_this)) = - (construct_result_reg, constructed_this) - { + if let (Some(target_reg), Some(constructed_this)) = (construct_result_reg, constructed_this) { + if is_derived_constructor && result.is_undefined() && callee_this.is_undefined() { + throw_err!(self, ReferenceError, "derived constructor must call super()"); + } self.regs[target_reg as usize] = if result.is_object() { result } else { constructed_this }; self.regs[0] = self.regs[target_reg as usize]; } else { @@ -1419,8 +1610,10 @@ mod tests { saved_regs: vec![JsValue::undefined()].into_boxed_slice(), saved_this: JsValue::undefined(), saved_new_target: JsValue::undefined(), + callee: JsValue::undefined(), construct_result_reg: None, constructed_this: None, + is_derived_constructor: false, }); vm.for_in_iters.push(std::ptr::dangling_mut::()); vm.for_of_iters.push(std::ptr::dangling_mut::()); diff --git a/crates/oxide_vm/tests/class_tests.rs b/crates/oxide_vm/tests/class_tests.rs index 7d311125..b8ae4b1b 100644 --- a/crates/oxide_vm/tests/class_tests.rs +++ b/crates/oxide_vm/tests/class_tests.rs @@ -9,6 +9,11 @@ fn eval(vm: &mut Vm, source: &str) -> Result { vm.run(&module) } +fn assert_num(value: JsValue, expected: f64) { + let actual = if value.is_int() { value.as_int() as f64 } else { value.as_double() }; + assert!((actual - expected).abs() < 0.0001, "expected {expected}, got {actual}"); +} + #[test] fn class_declaration_is_function_value() { let mut vm = Vm::new(); @@ -68,3 +73,43 @@ fn class_constructor_primitive_return_preserves_instance() { let result = eval(&mut vm, "class A { constructor() { this.x = 1; return 5; } } new A().x").unwrap(); assert_eq!(result.as_int(), 1); } + +#[test] +fn derived_constructor_super_initializes_parent_state() { + let mut vm = Vm::new(); + let result = eval( + &mut vm, + "class A { constructor(){ this.x = 1 } } class B extends A { constructor(){ super(); this.y = 2 } } let b = new B(); b.x + b.y", + ) + .unwrap(); + assert_num(result, 3.0); +} + +#[test] +fn derived_default_constructor_delegates_to_parent() { + let mut vm = Vm::new(); + let result = eval(&mut vm, "class A { constructor(){ this.x = 1 } } class B extends A {} new B().x").unwrap(); + assert_eq!(result.as_int(), 1); +} + +#[test] +fn derived_constructor_this_before_super_throws_reference_error() { + let mut vm = Vm::new(); + let err = eval( + &mut vm, + "class A { constructor(){ this.x = 1 } } class B extends A { constructor(){ this.x = 2; super(); } } new B()", + ) + .unwrap_err(); + assert!(err.contains("ReferenceError"), "expected ReferenceError, got: {err}"); +} + +#[test] +fn derived_method_super_call_uses_current_receiver() { + let mut vm = Vm::new(); + let result = eval( + &mut vm, + "class A { m(){ return this.x } } class B extends A { constructor(){ super(); this.x = 2 } m(){ return super.m() + 1 } } new B().m()", + ) + .unwrap(); + assert_num(result, 3.0); +} diff --git a/crates/oxide_vm/tests/proto_chain_tests.rs b/crates/oxide_vm/tests/proto_chain_tests.rs index 023e7298..54de906a 100644 --- a/crates/oxide_vm/tests/proto_chain_tests.rs +++ b/crates/oxide_vm/tests/proto_chain_tests.rs @@ -161,6 +161,15 @@ fn object_instanceof_object() { assert_bool!(eval("({}) instanceof Object"), true, "({}) instanceof Object"); } +#[test] +fn derived_class_instanceof_parent() { + assert_bool!( + eval("class A {} class B extends A {} let b = new B(); b instanceof A"), + true, + "derived class instanceof parent" + ); +} + // -- instanceof for native types should NOT respond to their own false positives -- #[test] fn number_not_instanceof_array() {