diff --git a/boa_engine/src/context/mod.rs b/boa_engine/src/context/mod.rs index d2649eb3363..84bc462d367 100644 --- a/boa_engine/src/context/mod.rs +++ b/boa_engine/src/context/mod.rs @@ -103,6 +103,7 @@ pub struct Context<'host> { job_queue: &'host dyn JobQueue, pub(crate) root_shape: Shape, + pub(crate) empty_object_shape: Shape, } impl std::fmt::Debug for Context<'_> { @@ -631,6 +632,9 @@ impl<'icu, 'hooks, 'queue> ContextBuilder<'icu, 'hooks, 'queue> { { let host_hooks = self.host_hooks.unwrap_or(&DefaultHooks); + let root_shape = Shape::root(); + // let empty_object_shape = root_shape.change_prototype_transition(prototype); + let mut context = Context { realm: Realm::create(host_hooks), interner: self.interner.unwrap_or_default(), @@ -646,10 +650,16 @@ impl<'icu, 'hooks, 'queue> ContextBuilder<'icu, 'hooks, 'queue> { kept_alive: Vec::new(), host_hooks, job_queue: self.job_queue.unwrap_or(&IdleJobQueue), - root_shape: Shape::root(), + // TODO: probably should be moved to realm, maybe into intrinsics. + root_shape: root_shape.clone(), + empty_object_shape: root_shape, }; builtins::set_default_global_bindings(&mut context)?; + let object_prototype = context.intrinsics().constructors().object().prototype(); + context.empty_object_shape = context + .root_shape + .change_prototype_transition(Some(object_prototype)); Ok(context) } diff --git a/boa_engine/src/object/internal_methods/mod.rs b/boa_engine/src/object/internal_methods/mod.rs index fc5680bdf96..37976b2abd8 100644 --- a/boa_engine/src/object/internal_methods/mod.rs +++ b/boa_engine/src/object/internal_methods/mod.rs @@ -348,14 +348,12 @@ pub(crate) fn ordinary_set_prototype_of( _: &mut Context<'_>, ) -> JsResult { // 1. Assert: Either Type(V) is Object or Type(V) is Null. - { - // 2. Let current be O.[[Prototype]]. - let current = obj.prototype(); + // 2. Let current be O.[[Prototype]]. + let current = obj.prototype(); - // 3. If SameValue(V, current) is true, return true. - if val == *current { - return Ok(true); - } + // 3. If SameValue(V, current) is true, return true. + if val == current { + return Ok(true); } // 4. Let extensible be O.[[Extensible]]. @@ -384,7 +382,7 @@ pub(crate) fn ordinary_set_prototype_of( break; } // ii. Else, set p to p.[[Prototype]]. - p = proto.prototype().clone(); + p = proto.prototype(); } // 9. Set O.[[Prototype]] to V. diff --git a/boa_engine/src/object/jsobject.rs b/boa_engine/src/object/jsobject.rs index 6b6a47a71cd..d61e843c8e9 100644 --- a/boa_engine/src/object/jsobject.rs +++ b/boa_engine/src/object/jsobject.rs @@ -17,6 +17,7 @@ use std::{ collections::HashMap, error::Error, fmt::{self, Debug, Display}, + hash::Hash, result::Result as StdResult, }; @@ -77,9 +78,8 @@ impl JsObject { Self { inner: Gc::new(GcRefCell::new(Object { data, - prototype: prototype.into(), extensible: true, - properties: PropertyMap::default(), + properties: PropertyMap::from_prototype(prototype.into()), private_elements: Vec::new(), })), } @@ -278,8 +278,8 @@ impl JsObject { /// Panics if the object is currently mutably borrowed. #[inline] #[track_caller] - pub fn prototype(&self) -> Ref<'_, JsPrototype> { - Ref::map(self.borrow(), Object::prototype) + pub fn prototype(&self) -> JsPrototype { + self.borrow().prototype() } /// Get the extensibility of the object. @@ -786,6 +786,14 @@ impl PartialEq for JsObject { } } +impl Eq for JsObject {} + +impl Hash for JsObject { + fn hash(&self, state: &mut H) { + std::ptr::hash(self.as_ref(), state); + } +} + /// An error returned by [`JsObject::try_borrow`](struct.JsObject.html#method.try_borrow). #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct BorrowError; diff --git a/boa_engine/src/object/mod.rs b/boa_engine/src/object/mod.rs index 20f0eb510cc..079b94eb7ad 100644 --- a/boa_engine/src/object/mod.rs +++ b/boa_engine/src/object/mod.rs @@ -124,8 +124,6 @@ pub struct Object { pub data: ObjectData, /// The collection of properties contained in the object properties: PropertyMap, - /// Instance prototype `__proto__`. - prototype: JsPrototype, /// Whether it can have new properties added to it. extensible: bool, /// The `[[PrivateElements]]` internal slot. @@ -136,7 +134,6 @@ unsafe impl Trace for Object { boa_gc::custom_trace!(this, { mark(&this.data); mark(&this.properties); - mark(&this.prototype); for (_, element) in &this.private_elements { mark(element); } @@ -781,7 +778,6 @@ impl Default for Object { Self { data: ObjectData::ordinary(), properties: PropertyMap::default(), - prototype: None, extensible: true, private_elements: Vec::default(), } @@ -1621,8 +1617,8 @@ impl Object { /// Gets the prototype instance of this object. #[inline] - pub const fn prototype(&self) -> &JsPrototype { - &self.prototype + pub fn prototype(&self) -> JsPrototype { + self.properties.shape.prototype() } /// Sets the prototype instance of the object. @@ -1634,12 +1630,12 @@ impl Object { pub fn set_prototype>(&mut self, prototype: O) -> bool { let prototype = prototype.into(); if self.extensible { - self.prototype = prototype; + self.properties.shape = self.properties.shape.change_prototype_transition(prototype); true } else { // If target is non-extensible, [[SetPrototypeOf]] must return false // unless V is the SameValue as the target's observed [[GetPrototypeOf]] value. - self.prototype == prototype + self.prototype() == prototype } } diff --git a/boa_engine/src/object/property_map.rs b/boa_engine/src/object/property_map.rs index 426cb4a161f..6ef368f7701 100644 --- a/boa_engine/src/object/property_map.rs +++ b/boa_engine/src/object/property_map.rs @@ -1,9 +1,9 @@ use super::{ - shape::{PropertyDescriptorAttribute, Shape, Slot, TransitionKey}, - PropertyDescriptor, PropertyKey, + shape::{PropertyDescriptorAttribute, Shape, Slot, TransitionKey, UniqueShape}, + JsPrototype, PropertyDescriptor, PropertyKey, }; use crate::{property::PropertyDescriptorBuilder, JsString, JsSymbol, JsValue}; -use boa_gc::{custom_trace, Finalize, Trace}; +use boa_gc::{custom_trace, Finalize, GcRefCell, Trace}; use indexmap::IndexMap; use rustc_hash::{FxHashMap, FxHasher}; use std::{collections::hash_map, hash::BuildHasherDefault, iter::FusedIterator}; @@ -235,6 +235,20 @@ impl PropertyMap { } } + /// TOOD: doc + #[must_use] + #[inline] + pub fn from_prototype(prototype: JsPrototype) -> Self { + Self { + indexed_properties: IndexedProperties::default(), + shape: Shape::unique(UniqueShape::new( + GcRefCell::new(prototype), + IndexMap::default(), + )), + storage: Vec::default(), + } + } + /// Get the property with the given key from the [`PropertyMap`]. #[must_use] pub fn get(&self, key: &PropertyKey) -> Option { diff --git a/boa_engine/src/object/shape/mod.rs b/boa_engine/src/object/shape/mod.rs index 90255539629..51650ac978a 100644 --- a/boa_engine/src/object/shape/mod.rs +++ b/boa_engine/src/object/shape/mod.rs @@ -11,6 +11,8 @@ mod shared_shape; mod unique_shape; +pub(crate) use unique_shape::UniqueShape; + use std::{ any::Any, cell::{Cell, RefCell}, @@ -28,7 +30,7 @@ use crate::{ JsString, }; -use self::{shared_shape::SharedShape, unique_shape::UniqueShape}; +use self::shared_shape::SharedShape; use super::JsPrototype; @@ -75,10 +77,12 @@ unsafe impl Trace for TransitionKey { enum TransitionType { /// Inserts a new property. Insert, - // Change existing property attributes. + + /// Change existing property attributes. Configure, - // TODO: - // Prototype, + + /// Change prototype. + Prototype, } unsafe impl Trace for TransitionType { @@ -87,7 +91,7 @@ unsafe impl Trace for TransitionType { #[derive(Debug, Trace, Finalize, Clone)] enum Inner { - Unique(#[unsafe_ignore_trace] UniqueShape), + Unique(UniqueShape), Shared(SharedShape), } @@ -148,6 +152,19 @@ impl Shape { } } + pub(crate) fn change_prototype_transition(&self, prototype: JsPrototype) -> Self { + match &self.inner { + Inner::Shared(shape) => Self::shared(shape.change_prototype_transition(prototype)), + Inner::Unique(shape) => Self::unique(shape.change_prototype_transition(prototype)), + } + } + pub(crate) fn prototype(&self) -> JsPrototype { + match &self.inner { + Inner::Shared(shape) => shape.prototype(), + Inner::Unique(shape) => shape.prototype(), + } + } + #[inline] pub fn lookup(&self, key: &PropertyKey) -> Option { match &self.inner { diff --git a/boa_engine/src/object/shape/shared_shape.rs b/boa_engine/src/object/shape/shared_shape.rs index 552b5f1ce6f..6a47931903b 100644 --- a/boa_engine/src/object/shape/shared_shape.rs +++ b/boa_engine/src/object/shape/shared_shape.rs @@ -41,17 +41,22 @@ impl PropertyTable { slot_index: u32, property_count: u32, ) -> (u32, Self) { + // TODO: possible optimization if we are the only ones holding a reference to self we can add the property directly. { let mut properties = self.inner.properties.borrow_mut(); if (property_count as usize) == properties.len() && !properties.contains_key(&key) { - // println!("Extending PropertyTable with {key} - Slot {slot_index} - PC {property_count}"); + println!( + "Extending PropertyTable with {key} - Slot {slot_index} - PC {property_count}" + ); let (index, value) = properties.insert_full(key, (slot_index, attributes)); debug_assert!(value.is_none()); return (index as u32, self.clone()); } } - // println!("Creating fork PropertyTable with {key} - Slot {slot_index} - PC {property_count}"); + println!( + "Creating fork PropertyTable with {key} - Slot {slot_index} - PC {property_count}" + ); // property is already present need to make deep clone of property table. let this = self.deep_clone(property_count); @@ -108,9 +113,13 @@ impl PropertyTable { #[derive(Debug, Trace, Finalize)] struct Inner { forward_property_transitions: GcRefCell>>, + forward_prototype_transitions: GcRefCell>>, property_index: u32, + /// Instance prototype `__proto__`. + prototype: JsPrototype, + #[unsafe_ignore_trace] property_table: PropertyTable, @@ -126,22 +135,23 @@ pub(crate) struct SharedShape { } impl SharedShape { - const DEBUG: bool = false; + const DEBUG: bool = true; fn property_table_count(&self) -> u32 { - if self.previous().is_some() { - return self.property_index() + 1; + if self.previous().is_none() { + // root has no elements + return 0; } - // root has no elements - 0 + self.property_index() + 1 } - #[inline] pub(crate) fn previous(&self) -> Option<&SharedShape> { self.inner.previous.as_ref() } - #[inline] + pub(crate) fn prototype(&self) -> JsPrototype { + self.inner.prototype.clone() + } pub(crate) fn property(&self) -> (PropertyKey, SlotIndex, PropertyDescriptorAttribute) { let properties = self.inner.property_table.properties().borrow(); let (key, (index, attributes)) = properties @@ -152,18 +162,20 @@ impl SharedShape { fn transition_type(&self) -> TransitionType { self.inner.transition_type } - #[inline] pub(crate) fn is_root(&self) -> bool { self.inner.previous.is_none() } fn property_index(&self) -> u32 { self.inner.property_index } - #[inline] - fn get_forward_transition(&self, key: &TransitionKey) -> Option> { + fn get_forward_property_transition(&self, key: &TransitionKey) -> Option> { let map = self.inner.forward_property_transitions.borrow(); map.get(key).cloned() } + fn get_forward_prototype_transition(&self, prototype: &JsPrototype) -> Option> { + let map = self.inner.forward_prototype_transitions.borrow(); + map.get(prototype).cloned() + } fn new(inner: Inner) -> Self { Self { @@ -171,24 +183,23 @@ impl SharedShape { } } - #[inline] pub(crate) fn root() -> Self { Self::new(Inner { + prototype: None, property_index: 0, property_table: PropertyTable::default(), - forward_property_transitions: GcRefCell::new(FxHashMap::default()), + forward_property_transitions: GcRefCell::default(), + forward_prototype_transitions: GcRefCell::default(), previous: None, transition_type: TransitionType::Insert, }) } fn create_property_transition(&self, key: TransitionKey) -> Self { - let slot_index = if self.previous().is_none() { - 0 - } else { - // TODO: implement different sized storage - (self.property_index() + 1) * 2 - }; + // TODO: implement different sized storage + let slot_index = (self.property_index() + + u32::from(self.transition_type() != TransitionType::Prototype)) + * 2; let (property_index, property_table) = self.inner.property_table.add_property_deep_clone_if_needed( @@ -198,7 +209,9 @@ impl SharedShape { self.property_table_count(), ); let new_inner_shape = Inner { - forward_property_transitions: GcRefCell::new(FxHashMap::default()), + prototype: self.prototype(), + forward_property_transitions: GcRefCell::default(), + forward_prototype_transitions: GcRefCell::default(), property_table, property_index, previous: Some(self.clone()), @@ -212,9 +225,35 @@ impl SharedShape { new_shape } + pub(crate) fn change_prototype_transition(&self, prototype: JsPrototype) -> Self { + if let Some(shape) = self.get_forward_prototype_transition(&prototype) { + if let Some(inner) = shape.upgrade() { + if Self::DEBUG { + println!(" Shape: Resusing previous prototype change shape"); + } + return Self { inner }; + } + } + let new_inner_shape = Inner { + prototype: prototype.clone(), + forward_property_transitions: GcRefCell::default(), + forward_prototype_transitions: GcRefCell::default(), + property_table: self.inner.property_table.clone(), + property_index: self.property_index(), + previous: Some(self.clone()), + transition_type: TransitionType::Prototype, + }; + let new_shape = Self::new(new_inner_shape); + + let mut map = self.inner.forward_prototype_transitions.borrow_mut(); + map.insert(prototype, WeakGc::new(&new_shape.inner)); + + new_shape + } + pub(crate) fn insert_property_transition(&self, key: TransitionKey) -> Self { // Check if we have already creaded such a transition, if so use it! - if let Some(shape) = self.get_forward_transition(&key) { + if let Some(shape) = self.get_forward_property_transition(&key) { if Self::DEBUG { println!("Shape: Trying to resuse previous shape"); } @@ -239,9 +278,11 @@ impl SharedShape { let property_table = self.inner.property_table.deep_clone_all(); property_table.set_attributes(&key.property_key, key.attributes); let new_inner_shape = Inner { + prototype: self.prototype(), property_table, property_index: self.inner.property_index, - forward_property_transitions: GcRefCell::new(FxHashMap::default()), + forward_property_transitions: GcRefCell::default(), + forward_prototype_transitions: GcRefCell::default(), previous: Some(self.clone()), transition_type: TransitionType::Configure, }; @@ -281,15 +322,27 @@ impl SharedShape { println!("Shape: deleting {key}"); } + let mut prototype = None; let mut transitions: IndexMap = IndexMap::default(); - let mut current = Some(self); + let mut current = Some(self); let mut base = loop { let Some(current_shape) = current else { unreachable!("The chain should have insert transition type!") }; + // We only take the latest prototype change it, if it exists. + if current_shape.transition_type() == TransitionType::Prototype { + if prototype.is_none() { + prototype = Some(current_shape.prototype().clone()); + } + + // Skip when it is a prototype transition. + current = current_shape.previous(); + continue; + } + let (current_property_key, _, current_property_attributes) = current_shape.property(); if current_shape.transition_type() == TransitionType::Insert @@ -316,6 +369,11 @@ impl SharedShape { current = current_shape.previous(); }; + // Apply prototype transition, if it was found. + if let Some(prototype) = prototype { + base = base.change_prototype_transition(prototype); + } + for (property_key, attributes) in transitions.into_iter().rev() { let transition = TransitionKey { property_key, @@ -333,8 +391,9 @@ impl SharedShape { println!("Shape: lookup {key}"); } - // Root has not element, return None. - self.previous()?; + if self.property_table_count() == 0 { + return None; + } let property_table = self.inner.property_table.properties().borrow(); if let Some((property_table_index, property_key, (slot_index, attributes))) = diff --git a/boa_engine/src/object/shape/unique_shape.rs b/boa_engine/src/object/shape/unique_shape.rs index adce2ebaa73..899f6536b7c 100644 --- a/boa_engine/src/object/shape/unique_shape.rs +++ b/boa_engine/src/object/shape/unique_shape.rs @@ -20,23 +20,34 @@ use crate::{ use super::{JsPrototype, PropertyDescriptorAttribute, Slot, TransitionKey}; /// TODO: doc -#[derive(Default, Debug)] +#[derive(Default, Debug, Trace, Finalize)] struct Inner { + #[unsafe_ignore_trace] property_table: RefCell>, + + prototype: GcRefCell, } /// TODO: doc -#[derive(Default, Debug, Clone)] +#[derive(Default, Debug, Clone, Trace, Finalize)] pub(crate) struct UniqueShape { - inner: Rc, + inner: Gc, } impl UniqueShape { - /// Create shape from a pre initialized property table. - fn new(property_table: IndexMap) -> Self { + pub(crate) fn prototype(&self) -> JsPrototype { + self.inner.prototype.borrow().clone() + } + + /// Create a new unique shape. + pub(crate) fn new( + prototype: GcRefCell, + property_table: IndexMap, + ) -> Self { Self { - inner: Rc::new(Inner { + inner: Gc::new(Inner { property_table: property_table.into(), + prototype, }), } } @@ -71,7 +82,8 @@ impl UniqueShape { // Therefore we need to create a new unique shape, // to invalidate any pointers to this shape i.e inline caches. let property_table = std::mem::take(&mut *property_table); - Self::new(property_table) + let prototype = self.inner.prototype.clone(); + Self::new(prototype, property_table) } /// TODO: doc @@ -99,6 +111,14 @@ impl UniqueShape { } self.clone() } + pub(crate) fn change_prototype_transition(&self, prototype: JsPrototype) -> Self { + let mut property_table = self.inner.property_table.borrow_mut(); + + // We need to create a new unique shape, + // to invalidate any pointers to this shape i.e inline caches. + let property_table = std::mem::take(&mut *property_table); + Self::new(GcRefCell::new(prototype), property_table) + } /// Gets all keys first strings then symbols. pub(crate) fn keys(&self) -> Vec { diff --git a/boa_engine/src/value/hash.rs b/boa_engine/src/value/hash.rs index 6536b1a77ae..c657037b1cd 100644 --- a/boa_engine/src/value/hash.rs +++ b/boa_engine/src/value/hash.rs @@ -45,7 +45,7 @@ impl Hash for JsValue { Self::BigInt(ref bigint) => bigint.hash(state), Self::Rational(rational) => RationalHashable(*rational).hash(state), Self::Symbol(ref symbol) => Hash::hash(symbol, state), - Self::Object(ref object) => std::ptr::hash(object.as_ref(), state), + Self::Object(ref object) => object.hash(state), } } } diff --git a/boa_engine/src/vm/opcode/push/object.rs b/boa_engine/src/vm/opcode/push/object.rs index 7a81c96e986..25a5a38cc06 100644 --- a/boa_engine/src/vm/opcode/push/object.rs +++ b/boa_engine/src/vm/opcode/push/object.rs @@ -17,7 +17,7 @@ impl Operation for PushEmptyObject { fn execute(context: &mut Context<'_>) -> JsResult { let o = JsObject::with_object_proto(context); - o.set_shape(context.root_shape.clone()); + o.set_shape(context.empty_object_shape.clone()); context.vm.push(o); Ok(CompletionType::Normal) }