diff --git a/crates/oxide_vm/src/lib.rs b/crates/oxide_vm/src/lib.rs index 6e6f7cfb..c45912e9 100644 --- a/crates/oxide_vm/src/lib.rs +++ b/crates/oxide_vm/src/lib.rs @@ -7,6 +7,7 @@ mod dispatch; pub mod native; pub mod vm; pub mod vm_pool; +mod vm_props; mod vm_runtime; mod vm_support; pub use oxide_types::value::JsValue; diff --git a/crates/oxide_vm/src/vm.rs b/crates/oxide_vm/src/vm.rs index 19835c12..fa702505 100644 --- a/crates/oxide_vm/src/vm.rs +++ b/crates/oxide_vm/src/vm.rs @@ -459,327 +459,6 @@ impl Vm { Ok(()) } - pub(crate) fn ordinary_get( - &mut self, obj: &JsObject, prop_name_si: u32, receiver: JsValue, - ) -> Result { - self.ordinary_get_inner(obj, prop_name_si, receiver, None) - } - - /// `ordinary_get` with an optional dispatch-loop target register. - /// - /// When `target_reg` is `Some(r)` and the property is a bytecode accessor getter, - /// this method pushes a bytecode frame with `FrameContinuation::AccessorGet { target_reg: r }` - /// and sets `self.accessor_frame_target_reg = Some(r)`. The caller must then return - /// `Ok(JsValue::undefined())` — the real value will be stored by the `RETURN` handler. - /// - /// When `target_reg` is `None`, bytecode getters run inline via `call_function_sync` - /// (same epoch, no sub-VM). Safe for callers outside the dispatch loop. - pub(crate) fn ordinary_get_with_target( - &mut self, obj: &JsObject, prop_name_si: u32, receiver: JsValue, target_reg: u8, - ) -> Result { - self.ordinary_get_inner(obj, prop_name_si, receiver, Some(target_reg)) - } - - fn ordinary_get_inner( - &mut self, obj: &JsObject, prop_name_si: u32, receiver: JsValue, target_reg: Option, - ) -> Result { - let length_si = self.kernel.string_forge().intern("length").0; - if obj.is_array() && prop_name_si == length_si { - return Ok(JsValue::int(obj.prop_count() as i32)); - } - if let Some(pos) = self.get_own_property_slot(obj, prop_name_si) { - if let Some(meta) = obj.prop_meta_at(pos) { - if meta.is_accessor { - return if meta.get.is_undefined() { - Ok(JsValue::undefined()) - } else if let Some(tr) = target_reg { - // Dispatch loop path: push bytecode frame, return sentinel. - let getter = meta.get; - let pushed = self.push_bytecode_getter_frame(getter, receiver, tr)?; - if pushed { - return Ok(JsValue::undefined()); // RETURN handler delivers value - } - // Native getter — value already written to regs[tr] by push_bytecode_getter_frame - Ok(self.regs[tr as usize]) - } else { - self.call_function_sync(meta.get, receiver, &[]) - }; - } - } - return Ok(obj.get_prop_at(pos)); - } - let proto = obj.proto(); - if proto.is_object() { - let proto_obj = unsafe { &*proto.as_js_object_ptr() }; - return self.ordinary_get_inner(proto_obj, prop_name_si, receiver, target_reg); - } - Ok(JsValue::undefined()) - } - - /// Push a bytecode frame for a getter accessor, or call native getters inline. - /// - /// Returns `true` if a bytecode frame was pushed (dispatch loop must `continue`). - /// Returns `false` if the getter was native (result already in `regs[target_reg]`). - fn push_bytecode_getter_frame( - &mut self, getter: JsValue, receiver: JsValue, target_reg: u8, - ) -> Result { - if !getter.is_object() { - return Err("TypeError: getter is not callable".into()); - } - let getter_obj = unsafe { &*getter.as_js_object_ptr() }; - if !getter_obj.is_function() { - return Err("TypeError: getter is not callable".into()); - } - - if getter_obj.native_fn().is_some() { - // Native getter — call inline, store result - let result = self.call_function_sync(getter, receiver, &[])?; - self.regs[target_reg as usize] = result; - return Ok(false); - } - - // Bytecode getter — push frame - self.push_bytecode_frame( - getter, - receiver, - &[], - None, - None, - JsValue::undefined(), - FrameContinuation::AccessorGet { target_reg }, - )?; - self.accessor_frame_target_reg = Some(target_reg); - Ok(true) - } - - fn inherited_property_meta(&self, obj: &JsObject, prop_name_si: u32) -> Option { - let mut proto = obj.proto(); - while proto.is_object() { - let proto_obj = unsafe { &*proto.as_js_object_ptr() }; - if let Some(pos) = self.get_own_property_slot(proto_obj, prop_name_si) { - return proto_obj.prop_meta_at(pos); - } - proto = proto_obj.proto(); - } - None - } - - pub(crate) fn ordinary_set( - &mut self, obj: &mut JsObject, prop_name_si: u32, val: JsValue, receiver: JsValue, - ) -> Result<(), String> { - self.ordinary_set_inner(obj, prop_name_si, val, receiver, false) - } - - /// `ordinary_set` variant for the dispatch loop: bytecode setters push a frame instead - /// of spawning a sub-VM. - pub(crate) fn ordinary_set_dispatch( - &mut self, obj: &mut JsObject, prop_name_si: u32, val: JsValue, receiver: JsValue, - ) -> Result<(), String> { - self.ordinary_set_inner(obj, prop_name_si, val, receiver, true) - } - - fn ordinary_set_inner( - &mut self, obj: &mut JsObject, prop_name_si: u32, val: JsValue, receiver: JsValue, use_frame_push: bool, - ) -> Result<(), String> { - if let Some(pos) = self.get_own_property_slot(obj, prop_name_si) { - if let Some(meta) = obj.prop_meta_at(pos) { - if meta.is_accessor { - if meta.set.is_undefined() { - return self.raise_type_error("property has no setter"); - } - return self.call_or_push_setter(meta.set, receiver, val, use_frame_push); - } - if !meta.attributes.writable() { - return self.raise_type_error("cannot assign to read-only property"); - } - } - obj.set_prop_at(pos, val); - return Ok(()); - } - - if let Some(meta) = self.inherited_property_meta(obj, prop_name_si) { - if meta.is_accessor { - if meta.set.is_undefined() { - return self.raise_type_error("property has no setter"); - } - return self.call_or_push_setter(meta.set, receiver, val, use_frame_push); - } - if !meta.attributes.writable() { - return self.raise_type_error("cannot assign to read-only property"); - } - } - - self.set_or_create_prop_value(obj, prop_name_si, val); - Ok(()) - } - - /// Call a setter — either inline (for native setters or sub-VM path) or via frame push - /// (for bytecode setters inside the dispatch loop). - fn call_or_push_setter( - &mut self, setter: JsValue, receiver: JsValue, val: JsValue, use_frame_push: bool, - ) -> Result<(), String> { - if !use_frame_push { - self.call_function_sync(setter, receiver, &[val])?; - return Ok(()); - } - if !setter.is_object() { - return self.raise_type_error("setter is not callable"); - } - let setter_obj = unsafe { &*setter.as_js_object_ptr() }; - if !setter_obj.is_function() { - return self.raise_type_error("setter is not callable"); - } - if setter_obj.native_fn().is_some() { - // Native setter — call inline - self.call_function_sync(setter, receiver, &[val])?; - return Ok(()); - } - // Bytecode setter — push frame; result is discarded via AccessorSet - self.push_bytecode_frame( - setter, - receiver, - &[val], - None, - None, - JsValue::undefined(), - FrameContinuation::AccessorSet, - )?; - Ok(()) - } - - pub(crate) fn read_member_prop( - &mut self, obj: &JsObject, prop_name_si: u32, receiver: JsValue, - ) -> Result { - let ext0 = self.bytecode[self.pc]; - let ext1 = self.bytecode[self.pc + 1]; - let _ext2 = self.bytecode[self.pc + 2]; - self.pc += 3; - if obj.has_prop_meta() { - return self.ordinary_get(obj, prop_name_si, receiver); - } - let cached_shape_id = ext0 & 0x00FF_FFFF; - let cached_slot = ext1; - - let val = - if cached_shape_id != 0 && cached_shape_id == obj.shape_id() && cached_slot < obj.prop_vec_len() as u32 { - obj.get_prop_at(cached_slot) - } else if let Some(template) = self.kernel.prop_forge().get_template(obj.shape_id()) { - if template.prop_name != prop_name_si { - self.ordinary_get(obj, prop_name_si, receiver)? - } else if template.position < obj.prop_vec_len() as u32 { - self.write_ic_back(obj.shape_id(), template.position); - obj.get_prop_at(template.position) - } else { - self.ordinary_get(obj, prop_name_si, receiver)? - } - } else { - let resolved = self.ordinary_get(obj, prop_name_si, receiver)?; - if let Some(pos) = self.kernel.shape_forge().lookup_position(obj.shape_id(), prop_name_si) { - if !obj.is_accessor_meta(pos) { - self.write_ic_back(obj.shape_id(), pos); - } - } - resolved - }; - Ok(val) - } - - pub(crate) fn set_member_prop( - &mut self, obj: &mut JsObject, prop_name_si: u32, val: JsValue, receiver: JsValue, - ) -> Result<(), String> { - if let Some(pos) = self.kernel.shape_forge().lookup_position(obj.shape_id(), prop_name_si) { - if obj.has_prop_meta() { - self.ordinary_set(obj, prop_name_si, val, receiver)?; - return Ok(()); - } - obj.set_prop_at(pos, val); - // IC write-back: 3 extension words (shape_id + slot_index + reserved) - self.write_ic_back(obj.shape_id(), pos); - } else { - self.ordinary_set(obj, prop_name_si, val, receiver)?; - } - Ok(()) - } - - pub(crate) fn set_or_create_prop_value(&mut self, obj: &mut JsObject, prop_name_si: u32, val: JsValue) { - if let Some(pos) = self.kernel.shape_forge().lookup_position(obj.shape_id(), prop_name_si) { - obj.set_prop_at(pos, val); - } else { - let new_shape_id = self.kernel.shape_forge().make_shape(obj.shape_id(), prop_name_si); - obj.set_shape_id(new_shape_id); - obj.push_prop(val); - obj.bump_generation(); - } - } - - pub(crate) fn define_data_property( - &mut self, obj: &mut JsObject, prop_name_si: u32, val: JsValue, attributes: PropAttributes, - ) -> Result<(), String> { - let pos = if let Some(pos) = self.kernel.shape_forge().lookup_position(obj.shape_id(), prop_name_si) { - pos - } else { - let new_shape_id = self.kernel.shape_forge().make_shape(obj.shape_id(), prop_name_si); - obj.set_shape_id(new_shape_id); - obj.push_prop(JsValue::undefined()) - }; - if let Some(current) = obj.prop_meta_at(pos) { - if !current.attributes.configurable() { - if current.is_accessor { - return self.raise_type_error("cannot redefine non-configurable property"); - } - if current.attributes.enumerable() != attributes.enumerable() { - return self.raise_type_error("cannot redefine non-configurable property"); - } - if !current.attributes.writable() - && (attributes.writable() || !coercion::same_value(obj.get_prop_at(pos), val)) - { - return self.raise_type_error("cannot redefine non-configurable property"); - } - if current.attributes.configurable() != attributes.configurable() { - return self.raise_type_error("cannot redefine non-configurable property"); - } - } - } - obj.set_prop_at(pos, val); - obj.set_data_meta(pos, attributes); - obj.bump_generation(); - Ok(()) - } - - pub(crate) fn define_accessor_property( - &mut self, obj: &mut JsObject, prop_name_si: u32, get: JsValue, set: JsValue, attributes: PropAttributes, - ) -> Result<(), String> { - let pos = if let Some(pos) = self.kernel.shape_forge().lookup_position(obj.shape_id(), prop_name_si) { - pos - } else { - let new_shape_id = self.kernel.shape_forge().make_shape(obj.shape_id(), prop_name_si); - obj.set_shape_id(new_shape_id); - obj.push_prop(JsValue::undefined()) - }; - if let Some(current) = obj.prop_meta_at(pos) { - if !current.attributes.configurable() - && (!current.is_accessor - || current.attributes.enumerable() != attributes.enumerable() - || current.attributes.configurable() != attributes.configurable() - || current.get != get - || current.set != set) - { - return self.raise_type_error("cannot redefine non-configurable property"); - } - } - obj.set_prop_at(pos, JsValue::undefined()); - obj.set_accessor_meta(pos, get, set, attributes); - obj.bump_generation(); - Ok(()) - } - - pub(crate) fn write_ic_back(&mut self, shape_id: u32, slot_index: u32) { - debug_assert!(self.pc >= 3, "IC write-back requires 3 extension words before pc"); - self.bytecode[self.pc - 3] = shape_id & 0x00FF_FFFF; - self.bytecode[self.pc - 2] = slot_index; - self.bytecode[self.pc - 1] = 0; - } - pub(crate) fn dispatch(&mut self) -> Result { let mut steps: u64 = 0; loop { diff --git a/crates/oxide_vm/src/vm_props.rs b/crates/oxide_vm/src/vm_props.rs new file mode 100644 index 00000000..bcefca26 --- /dev/null +++ b/crates/oxide_vm/src/vm_props.rs @@ -0,0 +1,303 @@ +use crate::coercion; +use crate::vm::{FrameContinuation, Vm}; +use oxide_types::object::{JsObject, PropAttributes, PropMetaEntry}; +use oxide_types::value::JsValue; + +impl Vm { + pub(crate) fn ordinary_get( + &mut self, obj: &JsObject, prop_name_si: u32, receiver: JsValue, + ) -> Result { + self.ordinary_get_inner(obj, prop_name_si, receiver, None) + } + + pub(crate) fn ordinary_get_with_target( + &mut self, obj: &JsObject, prop_name_si: u32, receiver: JsValue, target_reg: u8, + ) -> Result { + self.ordinary_get_inner(obj, prop_name_si, receiver, Some(target_reg)) + } + + fn ordinary_get_inner( + &mut self, obj: &JsObject, prop_name_si: u32, receiver: JsValue, target_reg: Option, + ) -> Result { + let length_si = self.kernel.string_forge().intern("length").0; + if obj.is_array() && prop_name_si == length_si { + return Ok(JsValue::int(obj.prop_count() as i32)); + } + if let Some(pos) = self.get_own_property_slot(obj, prop_name_si) { + if let Some(meta) = obj.prop_meta_at(pos) { + if meta.is_accessor { + return if meta.get.is_undefined() { + Ok(JsValue::undefined()) + } else if let Some(tr) = target_reg { + let getter = meta.get; + let pushed = self.push_bytecode_getter_frame(getter, receiver, tr)?; + if pushed { + return Ok(JsValue::undefined()); + } + Ok(self.regs[tr as usize]) + } else { + self.call_function_sync(meta.get, receiver, &[]) + }; + } + } + return Ok(obj.get_prop_at(pos)); + } + let proto = obj.proto(); + if proto.is_object() { + let proto_obj = unsafe { &*proto.as_js_object_ptr() }; + return self.ordinary_get_inner(proto_obj, prop_name_si, receiver, target_reg); + } + Ok(JsValue::undefined()) + } + + fn push_bytecode_getter_frame( + &mut self, getter: JsValue, receiver: JsValue, target_reg: u8, + ) -> Result { + if !getter.is_object() { + return Err("TypeError: getter is not callable".into()); + } + let getter_obj = unsafe { &*getter.as_js_object_ptr() }; + if !getter_obj.is_function() { + return Err("TypeError: getter is not callable".into()); + } + + if getter_obj.native_fn().is_some() { + let result = self.call_function_sync(getter, receiver, &[])?; + self.regs[target_reg as usize] = result; + return Ok(false); + } + + self.push_bytecode_frame( + getter, + receiver, + &[], + None, + None, + JsValue::undefined(), + FrameContinuation::AccessorGet { target_reg }, + )?; + self.accessor_frame_target_reg = Some(target_reg); + Ok(true) + } + + fn inherited_property_meta(&self, obj: &JsObject, prop_name_si: u32) -> Option { + let mut proto = obj.proto(); + while proto.is_object() { + let proto_obj = unsafe { &*proto.as_js_object_ptr() }; + if let Some(pos) = self.get_own_property_slot(proto_obj, prop_name_si) { + return proto_obj.prop_meta_at(pos); + } + proto = proto_obj.proto(); + } + None + } + + pub(crate) fn ordinary_set( + &mut self, obj: &mut JsObject, prop_name_si: u32, val: JsValue, receiver: JsValue, + ) -> Result<(), String> { + self.ordinary_set_inner(obj, prop_name_si, val, receiver, false) + } + + pub(crate) fn ordinary_set_dispatch( + &mut self, obj: &mut JsObject, prop_name_si: u32, val: JsValue, receiver: JsValue, + ) -> Result<(), String> { + self.ordinary_set_inner(obj, prop_name_si, val, receiver, true) + } + + fn ordinary_set_inner( + &mut self, obj: &mut JsObject, prop_name_si: u32, val: JsValue, receiver: JsValue, use_frame_push: bool, + ) -> Result<(), String> { + if let Some(pos) = self.get_own_property_slot(obj, prop_name_si) { + if let Some(meta) = obj.prop_meta_at(pos) { + if meta.is_accessor { + if meta.set.is_undefined() { + return self.raise_type_error("property has no setter"); + } + return self.call_or_push_setter(meta.set, receiver, val, use_frame_push); + } + if !meta.attributes.writable() { + return self.raise_type_error("cannot assign to read-only property"); + } + } + obj.set_prop_at(pos, val); + return Ok(()); + } + + if let Some(meta) = self.inherited_property_meta(obj, prop_name_si) { + if meta.is_accessor { + if meta.set.is_undefined() { + return self.raise_type_error("property has no setter"); + } + return self.call_or_push_setter(meta.set, receiver, val, use_frame_push); + } + if !meta.attributes.writable() { + return self.raise_type_error("cannot assign to read-only property"); + } + } + + self.set_or_create_prop_value(obj, prop_name_si, val); + Ok(()) + } + + fn call_or_push_setter( + &mut self, setter: JsValue, receiver: JsValue, val: JsValue, use_frame_push: bool, + ) -> Result<(), String> { + if !use_frame_push { + self.call_function_sync(setter, receiver, &[val])?; + return Ok(()); + } + if !setter.is_object() { + return self.raise_type_error("setter is not callable"); + } + let setter_obj = unsafe { &*setter.as_js_object_ptr() }; + if !setter_obj.is_function() { + return self.raise_type_error("setter is not callable"); + } + if setter_obj.native_fn().is_some() { + self.call_function_sync(setter, receiver, &[val])?; + return Ok(()); + } + self.push_bytecode_frame( + setter, + receiver, + &[val], + None, + None, + JsValue::undefined(), + FrameContinuation::AccessorSet, + )?; + Ok(()) + } + + pub(crate) fn read_member_prop( + &mut self, obj: &JsObject, prop_name_si: u32, receiver: JsValue, + ) -> Result { + let ext0 = self.bytecode[self.pc]; + let ext1 = self.bytecode[self.pc + 1]; + let _ext2 = self.bytecode[self.pc + 2]; + self.pc += 3; + if obj.has_prop_meta() { + return self.ordinary_get(obj, prop_name_si, receiver); + } + let cached_shape_id = ext0 & 0x00FF_FFFF; + let cached_slot = ext1; + + let val = + if cached_shape_id != 0 && cached_shape_id == obj.shape_id() && cached_slot < obj.prop_vec_len() as u32 { + obj.get_prop_at(cached_slot) + } else if let Some(template) = self.kernel.prop_forge().get_template(obj.shape_id()) { + if template.prop_name != prop_name_si { + self.ordinary_get(obj, prop_name_si, receiver)? + } else if template.position < obj.prop_vec_len() as u32 { + self.write_ic_back(obj.shape_id(), template.position); + obj.get_prop_at(template.position) + } else { + self.ordinary_get(obj, prop_name_si, receiver)? + } + } else { + let resolved = self.ordinary_get(obj, prop_name_si, receiver)?; + if let Some(pos) = self.kernel.shape_forge().lookup_position(obj.shape_id(), prop_name_si) { + if !obj.is_accessor_meta(pos) { + self.write_ic_back(obj.shape_id(), pos); + } + } + resolved + }; + Ok(val) + } + + pub(crate) fn set_member_prop( + &mut self, obj: &mut JsObject, prop_name_si: u32, val: JsValue, receiver: JsValue, + ) -> Result<(), String> { + if let Some(pos) = self.kernel.shape_forge().lookup_position(obj.shape_id(), prop_name_si) { + if obj.has_prop_meta() { + self.ordinary_set(obj, prop_name_si, val, receiver)?; + return Ok(()); + } + obj.set_prop_at(pos, val); + self.write_ic_back(obj.shape_id(), pos); + } else { + self.ordinary_set(obj, prop_name_si, val, receiver)?; + } + Ok(()) + } + + pub(crate) fn set_or_create_prop_value(&mut self, obj: &mut JsObject, prop_name_si: u32, val: JsValue) { + if let Some(pos) = self.kernel.shape_forge().lookup_position(obj.shape_id(), prop_name_si) { + obj.set_prop_at(pos, val); + } else { + let new_shape_id = self.kernel.shape_forge().make_shape(obj.shape_id(), prop_name_si); + obj.set_shape_id(new_shape_id); + obj.push_prop(val); + obj.bump_generation(); + } + } + + pub(crate) fn define_data_property( + &mut self, obj: &mut JsObject, prop_name_si: u32, val: JsValue, attributes: PropAttributes, + ) -> Result<(), String> { + let pos = if let Some(pos) = self.kernel.shape_forge().lookup_position(obj.shape_id(), prop_name_si) { + pos + } else { + let new_shape_id = self.kernel.shape_forge().make_shape(obj.shape_id(), prop_name_si); + obj.set_shape_id(new_shape_id); + obj.push_prop(JsValue::undefined()) + }; + if let Some(current) = obj.prop_meta_at(pos) { + if !current.attributes.configurable() { + if current.is_accessor { + return self.raise_type_error("cannot redefine non-configurable property"); + } + if current.attributes.enumerable() != attributes.enumerable() { + return self.raise_type_error("cannot redefine non-configurable property"); + } + if !current.attributes.writable() + && (attributes.writable() || !coercion::same_value(obj.get_prop_at(pos), val)) + { + return self.raise_type_error("cannot redefine non-configurable property"); + } + if current.attributes.configurable() != attributes.configurable() { + return self.raise_type_error("cannot redefine non-configurable property"); + } + } + } + obj.set_prop_at(pos, val); + obj.set_data_meta(pos, attributes); + obj.bump_generation(); + Ok(()) + } + + pub(crate) fn define_accessor_property( + &mut self, obj: &mut JsObject, prop_name_si: u32, get: JsValue, set: JsValue, attributes: PropAttributes, + ) -> Result<(), String> { + let pos = if let Some(pos) = self.kernel.shape_forge().lookup_position(obj.shape_id(), prop_name_si) { + pos + } else { + let new_shape_id = self.kernel.shape_forge().make_shape(obj.shape_id(), prop_name_si); + obj.set_shape_id(new_shape_id); + obj.push_prop(JsValue::undefined()) + }; + if let Some(current) = obj.prop_meta_at(pos) { + if !current.attributes.configurable() + && (!current.is_accessor + || current.attributes.enumerable() != attributes.enumerable() + || current.attributes.configurable() != attributes.configurable() + || current.get != get + || current.set != set) + { + return self.raise_type_error("cannot redefine non-configurable property"); + } + } + obj.set_prop_at(pos, JsValue::undefined()); + obj.set_accessor_meta(pos, get, set, attributes); + obj.bump_generation(); + Ok(()) + } + + pub(crate) fn write_ic_back(&mut self, shape_id: u32, slot_index: u32) { + debug_assert!(self.pc >= 3, "IC write-back requires 3 extension words before pc"); + self.bytecode[self.pc - 3] = shape_id & 0x00FF_FFFF; + self.bytecode[self.pc - 2] = slot_index; + self.bytecode[self.pc - 1] = 0; + } +}