diff --git a/boa_engine/src/builtins/array/mod.rs b/boa_engine/src/builtins/array/mod.rs index bfea05fe982..de306a0d80f 100644 --- a/boa_engine/src/builtins/array/mod.rs +++ b/boa_engine/src/builtins/array/mod.rs @@ -245,7 +245,13 @@ impl Array { // 5. Set A.[[DefineOwnProperty]] as specified in 10.4.2.1. let prototype = prototype.unwrap_or_else(|| context.intrinsics().constructors().array().prototype()); - let array = JsObject::from_proto_and_data(prototype, ObjectData::array()); + + // TODO: possible optimization: directly initialize with pre-initialized array instance shape. + let array = JsObject::from_proto_and_data_with_shared_shape( + context.root_shape.clone(), + prototype, + ObjectData::array(), + ); // 6. Perform ! OrdinaryDefineOwnProperty(A, "length", PropertyDescriptor { [[Value]]: 𝔽(length), [[Writable]]: true, [[Enumerable]]: false, [[Configurable]]: false }). crate::object::internal_methods::ordinary_define_own_property( diff --git a/boa_engine/src/context/mod.rs b/boa_engine/src/context/mod.rs index bdaff918fee..84bc462d367 100644 --- a/boa_engine/src/context/mod.rs +++ b/boa_engine/src/context/mod.rs @@ -22,7 +22,7 @@ use crate::{ class::{Class, ClassBuilder}, job::{IdleJobQueue, JobQueue, NativeJob}, native_function::NativeFunction, - object::{FunctionObjectBuilder, GlobalPropertyMap, JsObject}, + object::{shape::Shape, FunctionObjectBuilder, JsObject, PropertyMap}, property::{Attribute, PropertyDescriptor, PropertyKey}, realm::Realm, vm::{CallFrame, CodeBlock, Vm}, @@ -101,6 +101,9 @@ pub struct Context<'host> { host_hooks: &'host dyn HostHooks, job_queue: &'host dyn JobQueue, + + pub(crate) root_shape: Shape, + pub(crate) empty_object_shape: Shape, } impl std::fmt::Debug for Context<'_> { @@ -323,7 +326,7 @@ impl Context<'_> { .build(); self.global_bindings_mut().insert( - name.into(), + &PropertyKey::String(name.into()), PropertyDescriptor::builder() .value(function) .writable(true) @@ -355,7 +358,7 @@ impl Context<'_> { .build(); self.global_bindings_mut().insert( - name.into(), + &PropertyKey::String(name.into()), PropertyDescriptor::builder() .value(function) .writable(true) @@ -393,7 +396,8 @@ impl Context<'_> { .configurable(T::ATTRIBUTES.configurable()) .build(); - self.global_bindings_mut().insert(T::NAME.into(), property); + self.global_bindings_mut() + .insert(&PropertyKey::String(T::NAME.into()), property); Ok(()) } @@ -460,7 +464,7 @@ impl Context<'_> { impl Context<'_> { /// Return a mutable reference to the global object string bindings. - pub(crate) fn global_bindings_mut(&mut self) -> &mut GlobalPropertyMap { + pub(crate) fn global_bindings_mut(&mut self) -> &mut PropertyMap { self.realm.global_bindings_mut() } @@ -628,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(), @@ -643,9 +650,16 @@ impl<'icu, 'hooks, 'queue> ContextBuilder<'icu, 'hooks, 'queue> { kept_alive: Vec::new(), host_hooks, job_queue: self.job_queue.unwrap_or(&IdleJobQueue), + // 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/environments/compile.rs b/boa_engine/src/environments/compile.rs index 5b7725295b8..cdb81d00162 100644 --- a/boa_engine/src/environments/compile.rs +++ b/boa_engine/src/environments/compile.rs @@ -1,5 +1,7 @@ use crate::{ - environments::runtime::BindingLocator, property::PropertyDescriptor, Context, JsString, JsValue, + environments::runtime::BindingLocator, + property::{PropertyDescriptor, PropertyKey}, + Context, JsString, JsValue, }; use boa_ast::expression::Identifier; use boa_gc::{Finalize, Gc, GcRefCell, Trace}; @@ -289,14 +291,12 @@ impl Context<'_> { .interner() .resolve_expect(name.sym()) .into_common::(false); - let desc = self - .realm - .global_property_map - .string_property_map() - .get(&name_str); + + let key = PropertyKey::String(name_str); + let desc = self.realm.global_property_map.get(&key); if desc.is_none() { self.global_bindings_mut().insert( - name_str, + &key, PropertyDescriptor::builder() .value(JsValue::Undefined) .writable(true) diff --git a/boa_engine/src/object/internal_methods/global.rs b/boa_engine/src/object/internal_methods/global.rs index 41be3c3d052..f9c699fef35 100644 --- a/boa_engine/src/object/internal_methods/global.rs +++ b/boa_engine/src/object/internal_methods/global.rs @@ -254,30 +254,15 @@ pub(crate) fn global_own_property_keys( }; // 2. For each own property key P of O such that P is an array index, in ascending numeric index order, do - // a. Add P as the last element of keys. + // a. Add P as the last element of keys. keys.extend(ordered_indexes.into_iter().map(Into::into)); // 3. For each own property key P of O such that Type(P) is String and P is not an array index, in ascending chronological order of property creation, do - // a. Add P as the last element of keys. - keys.extend( - context - .realm - .global_property_map - .string_property_keys() - .cloned() - .map(Into::into), - ); - + // a. Add P as the last element of keys. + // // 4. For each own property key P of O such that Type(P) is Symbol, in ascending chronological order of property creation, do - // a. Add P as the last element of keys. - keys.extend( - context - .realm - .global_property_map - .symbol_property_keys() - .cloned() - .map(Into::into), - ); + // a. Add P as the last element of keys. + keys.extend(context.realm.global_property_map.shape.keys()); // 5. Return keys. Ok(keys) diff --git a/boa_engine/src/object/internal_methods/integer_indexed.rs b/boa_engine/src/object/internal_methods/integer_indexed.rs index e5cf99063d5..2e9f5d13117 100644 --- a/boa_engine/src/object/internal_methods/integer_indexed.rs +++ b/boa_engine/src/object/internal_methods/integer_indexed.rs @@ -222,30 +222,19 @@ pub(crate) fn integer_indexed_exotic_own_property_keys( vec![] } else { // 2. If IsDetachedBuffer(O.[[ViewedArrayBuffer]]) is false, then - // a. For each integer i starting with 0 such that i < O.[[ArrayLength]], in ascending order, do - // i. Add ! ToString(𝔽(i)) as the last element of keys. + // a. For each integer i starting with 0 such that i < O.[[ArrayLength]], in ascending order, do + // i. Add ! ToString(𝔽(i)) as the last element of keys. (0..inner.array_length()) .map(|index| PropertyKey::Index(index as u32)) .collect() }; // 3. For each own property key P of O such that Type(P) is String and P is not an array index, in ascending chronological order of property creation, do - // a. Add P as the last element of keys. - keys.extend( - obj.properties - .string_property_keys() - .cloned() - .map(Into::into), - ); - + // a. Add P as the last element of keys. + // // 4. For each own property key P of O such that Type(P) is Symbol, in ascending chronological order of property creation, do - // a. Add P as the last element of keys. - keys.extend( - obj.properties - .symbol_property_keys() - .cloned() - .map(Into::into), - ); + // a. Add P as the last element of keys. + keys.extend(obj.properties.shape.keys()); // 5. Return keys. Ok(keys) diff --git a/boa_engine/src/object/internal_methods/mod.rs b/boa_engine/src/object/internal_methods/mod.rs index 00279675f66..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. @@ -714,24 +712,11 @@ pub(crate) fn ordinary_own_property_keys( keys.extend(ordered_indexes.into_iter().map(Into::into)); // 3. For each own property key P of O such that Type(P) is String and P is not an array index, in ascending chronological order of property creation, do - // a. Add P as the last element of keys. - keys.extend( - obj.borrow() - .properties - .string_property_keys() - .cloned() - .map(Into::into), - ); - + // a. Add P as the last element of keys. + // // 4. For each own property key P of O such that Type(P) is Symbol, in ascending chronological order of property creation, do - // a. Add P as the last element of keys. - keys.extend( - obj.borrow() - .properties - .symbol_property_keys() - .cloned() - .map(Into::into), - ); + // a. Add P as the last element of keys. + keys.extend(obj.borrow().properties.shape.keys()); // 5. Return keys. Ok(keys) diff --git a/boa_engine/src/object/internal_methods/string.rs b/boa_engine/src/object/internal_methods/string.rs index c4151741694..0c919b5fab5 100644 --- a/boa_engine/src/object/internal_methods/string.rs +++ b/boa_engine/src/object/internal_methods/string.rs @@ -101,12 +101,12 @@ pub(crate) fn string_exotic_own_property_keys( let mut keys = Vec::with_capacity(len); // 5. For each integer i starting with 0 such that i < len, in ascending order, do - // a. Add ! ToString(𝔽(i)) as the last element of keys. + // a. Add ! ToString(𝔽(i)) as the last element of keys. keys.extend((0..len).map(Into::into)); // 6. For each own property key P of O such that P is an array index // and ! ToIntegerOrInfinity(P) ≥ len, in ascending numeric index order, do - // a. Add P as the last element of keys. + // a. Add P as the last element of keys. let mut remaining_indices: Vec<_> = obj .properties .index_property_keys() @@ -117,23 +117,12 @@ pub(crate) fn string_exotic_own_property_keys( // 7. For each own property key P of O such that Type(P) is String and P is not // an array index, in ascending chronological order of property creation, do - // a. Add P as the last element of keys. - keys.extend( - obj.properties - .string_property_keys() - .cloned() - .map(Into::into), - ); + // a. Add P as the last element of keys. // 8. For each own property key P of O such that Type(P) is Symbol, in ascending // chronological order of property creation, do - // a. Add P as the last element of keys. - keys.extend( - obj.properties - .symbol_property_keys() - .cloned() - .map(Into::into), - ); + // a. Add P as the last element of keys. + keys.extend(obj.properties.shape.keys()); // 9. Return keys. Ok(keys) diff --git a/boa_engine/src/object/jsobject.rs b/boa_engine/src/object/jsobject.rs index f1dd101062a..e5e85c6ecda 100644 --- a/boa_engine/src/object/jsobject.rs +++ b/boa_engine/src/object/jsobject.rs @@ -2,7 +2,7 @@ //! //! The `JsObject` is a garbage collected Object. -use super::{JsPrototype, NativeObject, Object, PropertyMap}; +use super::{shape::Shape, JsPrototype, NativeObject, Object, PropertyMap}; use crate::{ error::JsNativeError, object::{ObjectData, ObjectKind}, @@ -17,6 +17,7 @@ use std::{ collections::HashMap, error::Error, fmt::{self, Debug, Display}, + hash::Hash, result::Result as StdResult, }; @@ -33,6 +34,12 @@ pub struct JsObject { } impl JsObject { + /// TODO: doc + pub(crate) fn set_shape(&self, shape: Shape) { + let mut object = self.inner.borrow_mut(); + object.properties_mut().shape = shape; + } + /// Creates a new ordinary object with its prototype set to the `Object` prototype. /// /// This is equivalent to calling the specification's abstract operation @@ -71,9 +78,33 @@ impl JsObject { Self { inner: Gc::new(GcRefCell::new(Object { data, - prototype: prototype.into(), extensible: true, - properties: PropertyMap::default(), + properties: PropertyMap::from_prototype_unique_shape(prototype.into()), + private_elements: Vec::new(), + })), + } + } + + /// Creates a new object with the provided prototype and object data. + /// + /// This is equivalent to calling the specification's abstract operation [`OrdinaryObjectCreate`], + /// with the difference that the `additionalInternalSlotsList` parameter is automatically set by + /// the [`ObjectData`] provided. + /// + /// [`OrdinaryObjectCreate`]: https://tc39.es/ecma262/#sec-ordinaryobjectcreate + pub fn from_proto_and_data_with_shared_shape>>( + root_shape: Shape, + prototype: O, + data: ObjectData, + ) -> Self { + Self { + inner: Gc::new(GcRefCell::new(Object { + data, + extensible: true, + properties: PropertyMap::from_prototype_with_shared_shape( + root_shape, + prototype.into(), + ), private_elements: Vec::new(), })), } @@ -272,8 +303,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. @@ -700,7 +731,7 @@ Cannot both specify accessors and a value or writable attribute", /// Helper function for property insertion. #[track_caller] - pub(crate) fn insert(&self, key: K, property: P) -> Option + pub(crate) fn insert(&self, key: K, property: P) -> bool where K: Into, P: Into, @@ -710,9 +741,9 @@ Cannot both specify accessors and a value or writable attribute", /// Inserts a field in the object `properties` without checking if it's writable. /// - /// If a field was already in the object with the same name that a `Some` is returned - /// with that field, otherwise None is returned. - pub fn insert_property(&self, key: K, property: P) -> Option + /// If a field was already in the object with the same name, than `true` is returned + /// with that field, otherwise `false` is returned. + pub fn insert_property(&self, key: K, property: P) -> bool where K: Into, P: Into, @@ -780,6 +811,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 86bfef4b7c3..079b94eb7ad 100644 --- a/boa_engine/src/object/mod.rs +++ b/boa_engine/src/object/mod.rs @@ -71,6 +71,7 @@ pub mod builtins; mod jsobject; mod operations; mod property_map; +pub(crate) mod shape; pub(crate) use builtins::*; @@ -123,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. @@ -135,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); } @@ -780,7 +778,6 @@ impl Default for Object { Self { data: ObjectData::ordinary(), properties: PropertyMap::default(), - prototype: None, extensible: true, private_elements: Vec::default(), } @@ -1620,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. @@ -1633,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 } } @@ -1836,9 +1833,9 @@ impl Object { /// Inserts a field in the object `properties` without checking if it's writable. /// - /// If a field was already in the object with the same name, then a `Some` is returned - /// with that field's value, otherwise, `None` is returned. - pub(crate) fn insert(&mut self, key: K, property: P) -> Option + /// If a field was already in the object with the same name, then `true` is returned + /// otherwise, `false` is returned. + pub(crate) fn insert(&mut self, key: K, property: P) -> bool where K: Into, P: Into, @@ -1848,7 +1845,7 @@ impl Object { /// Helper function for property removal. #[inline] - pub(crate) fn remove(&mut self, key: &PropertyKey) -> Option { + pub(crate) fn remove(&mut self, key: &PropertyKey) -> bool { self.properties.remove(key) } diff --git a/boa_engine/src/object/property_map.rs b/boa_engine/src/object/property_map.rs index 67b2ec08de1..9e256f365c1 100644 --- a/boa_engine/src/object/property_map.rs +++ b/boa_engine/src/object/property_map.rs @@ -1,14 +1,17 @@ -use super::{PropertyDescriptor, PropertyKey}; +use super::{ + shape::{ + shared_shape::TransitionKey, + slot::{Slot, SlotAttribute}, + Shape, 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}; -/// Type alias to make it easier to work with the string properties on the global object. -pub(crate) type GlobalPropertyMap = - IndexMap>; - /// Wrapper around `indexmap::IndexMap` for usage in `PropertyMap`. #[derive(Debug, Finalize)] struct OrderedHashMap(IndexMap>); @@ -98,9 +101,9 @@ impl IndexedProperties { } /// Inserts a property descriptor with the specified key. - fn insert(&mut self, key: u32, property: PropertyDescriptor) -> Option { + fn insert(&mut self, key: u32, property: PropertyDescriptor) -> bool { let vec = match self { - Self::Sparse(map) => return map.insert(key, property), + Self::Sparse(map) => return map.insert(key, property).is_some(), Self::Dense(vec) => { let len = vec.len() as u32; if key <= len @@ -121,19 +124,12 @@ impl IndexedProperties { // Since the previous key is the current key - 1. Meaning that the elements are continuos. if key == len { vec.push(value); - return None; + return false; } // If it the key points in at a already taken index, swap and return it. std::mem::swap(&mut vec[key as usize], &mut value); - return Some( - PropertyDescriptorBuilder::new() - .writable(true) - .enumerable(true) - .configurable(true) - .value(value) - .build(), - ); + return true; } vec @@ -142,37 +138,32 @@ impl IndexedProperties { // Slow path: converting to sparse storage. let mut map = Self::convert_dense_to_sparse(vec); - let old_property = map.insert(key, property); + let replaced = map.insert(key, property).is_some(); *self = Self::Sparse(map); - old_property + replaced } /// Inserts a property descriptor with the specified key. - fn remove(&mut self, key: u32) -> Option { + fn remove(&mut self, key: u32) -> bool { let vec = match self { - Self::Sparse(map) => return map.remove(&key), + Self::Sparse(map) => { + return map.remove(&key).is_some(); + } Self::Dense(vec) => { // Fast Path: contiguous storage. // Has no elements or out of range, nothing to delete! if vec.is_empty() || key as usize >= vec.len() { - return None; + return false; } - // If the key is pointing at the last element, then we pop it and return it. + // If the key is pointing at the last element, then we pop it. // - // It does not make the storage sparse + // It does not make the storage sparse. if key as usize == vec.len().wrapping_sub(1) { - let value = vec.pop().expect("Already checked if it is out of bounds"); - return Some( - PropertyDescriptorBuilder::new() - .writable(true) - .enumerable(true) - .configurable(true) - .value(value) - .build(), - ); + vec.pop().expect("Already checked if it is out of bounds"); + return true; } vec @@ -181,10 +172,10 @@ impl IndexedProperties { // Slow Path: conversion to sparse storage. let mut map = Self::convert_dense_to_sparse(vec); - let old_property = map.remove(&key); + let removed = map.remove(&key).is_some(); *self = Self::Sparse(map); - old_property + removed } /// Check if we contain the key to a property descriptor. @@ -225,55 +216,183 @@ pub struct PropertyMap { /// Properties stored with integers as keys. indexed_properties: IndexedProperties, - /// Properties stored with `String`s a keys. - string_properties: OrderedHashMap, - - /// Properties stored with `Symbol`s a keys. - symbol_properties: OrderedHashMap, + pub(crate) shape: Shape, + // FIXME: currently every property takes 2 spaces in the array + // to account for the accessor (get, set), add different sized + // storage based on attribute flags. + storage: Vec, } impl PropertyMap { /// Create a new [`PropertyMap`]. #[must_use] #[inline] - pub fn new() -> Self { - Self::default() + pub fn new(shape: Shape) -> Self { + Self { + indexed_properties: IndexedProperties::default(), + shape, + storage: Vec::default(), + } + } + + /// TOOD: doc + #[must_use] + #[inline] + pub fn from_prototype_unique_shape(prototype: JsPrototype) -> Self { + Self { + indexed_properties: IndexedProperties::default(), + shape: Shape::unique(UniqueShape::new( + GcRefCell::new(prototype), + IndexMap::default(), + )), + storage: Vec::default(), + } + } + + /// TOOD: doc + #[must_use] + #[inline] + pub fn from_prototype_with_shared_shape(mut shape: Shape, prototype: JsPrototype) -> Self { + shape = shape.change_prototype_transition(prototype); + Self { + indexed_properties: IndexedProperties::default(), + shape, + storage: Vec::default(), + } } /// Get the property with the given key from the [`PropertyMap`]. #[must_use] pub fn get(&self, key: &PropertyKey) -> Option { - match key { - PropertyKey::Index(index) => self.indexed_properties.get(*index), - PropertyKey::String(string) => self.string_properties.0.get(string).cloned(), - PropertyKey::Symbol(symbol) => self.symbol_properties.0.get(symbol).cloned(), + if let PropertyKey::Index(index) = key { + return self.indexed_properties.get(*index); + } + if let Some(slot) = self.shape.lookup(key) { + return Some(self.get_storage(slot)); } + + None + } + + /// Get the property with the given key from the [`PropertyMap`]. + #[must_use] + pub fn get_storage(&self, Slot { index, attributes }: Slot) -> PropertyDescriptor { + let index = index as usize; + let mut builder = PropertyDescriptor::builder() + .configurable(attributes.contains(SlotAttribute::CONFIGURABLE)) + .enumerable(attributes.contains(SlotAttribute::ENUMERABLE)); + if attributes.is_accessor_descriptor() { + if attributes.has_get() { + builder = builder.get(self.storage[index].clone()); + } + if attributes.has_set() { + builder = builder.set(self.storage[index + 1].clone()); + } + } else { + builder = builder.writable(attributes.contains(SlotAttribute::WRITABLE)); + builder = builder.value(self.storage[index].clone()); + } + builder.build() } /// Insert the given property descriptor with the given key [`PropertyMap`]. - pub fn insert( - &mut self, - key: &PropertyKey, - property: PropertyDescriptor, - ) -> Option { - match &key { - PropertyKey::Index(index) => self.indexed_properties.insert(*index, property), - PropertyKey::String(string) => { - self.string_properties.0.insert(string.clone(), property) + // FIXME: Temporary lint allow, remove after prototyping + #[allow(clippy::missing_panics_doc)] + pub fn insert(&mut self, key: &PropertyKey, property: PropertyDescriptor) -> bool { + if let PropertyKey::Index(index) = key { + return self.indexed_properties.insert(*index, property); + } + + // TODO: maybe extract into a PropertyDescriptor method? + let mut attributes = SlotAttribute::empty(); + attributes.set(SlotAttribute::CONFIGURABLE, property.expect_configurable()); + attributes.set(SlotAttribute::ENUMERABLE, property.expect_enumerable()); + if property.is_data_descriptor() { + attributes.set(SlotAttribute::WRITABLE, property.expect_writable()); + } else if property.is_accessor_descriptor() { + attributes.set(SlotAttribute::HAS_GET, property.get().is_some()); + attributes.set(SlotAttribute::HAS_SET, property.set().is_some()); + } else { + unreachable!() + } + + if let Some(slot) = self.shape.lookup(key) { + let index = slot.index as usize; + + if slot.attributes != attributes { + let key = TransitionKey { + property_key: key.clone(), + attributes, + }; + self.shape = self.shape.change_attributes_transition(key); } - PropertyKey::Symbol(symbol) => { - self.symbol_properties.0.insert(symbol.clone(), property) + + if attributes.is_accessor_descriptor() { + if attributes.has_get() { + self.storage[index] = property + .get() + .cloned() + .map(JsValue::new) + .unwrap_or_default(); + } + if attributes.has_set() { + self.storage[index + 1] = property + .set() + .cloned() + .map(JsValue::new) + .unwrap_or_default(); + } + } else { + // TODO: maybe take the value instead of cloning, we own it. + self.storage[index] = property.expect_value().clone(); } + return true; + } + + let transition_key = TransitionKey { + property_key: key.clone(), + attributes, + }; + self.shape = self.shape.insert_property_transition(transition_key); + if attributes.is_accessor_descriptor() { + self.storage.push( + property + .get() + .cloned() + .map(JsValue::new) + .unwrap_or_default(), + ); + self.storage.push( + property + .set() + .cloned() + .map(JsValue::new) + .unwrap_or_default(), + ); + } else { + self.storage + .push(property.value().cloned().unwrap_or_default()); + self.storage.push(JsValue::undefined()); } + + false } /// Remove the property with the given key from the [`PropertyMap`]. - pub fn remove(&mut self, key: &PropertyKey) -> Option { - match key { - PropertyKey::Index(index) => self.indexed_properties.remove(*index), - PropertyKey::String(string) => self.string_properties.0.shift_remove(string), - PropertyKey::Symbol(symbol) => self.symbol_properties.0.shift_remove(symbol), + pub fn remove(&mut self, key: &PropertyKey) -> bool { + if let PropertyKey::Index(index) = key { + return self.indexed_properties.remove(*index); + } + if let Some(slot) = self.shape.lookup(key) { + // shift all elements + self.storage.remove(slot.index as usize + 1); + self.storage.remove(slot.index as usize); + + self.shape = self.shape.remove_property_transition(key); + return true; } + + false } /// Overrides all the indexed properties, setting it to dense storage. @@ -290,65 +409,6 @@ impl PropertyMap { } } - /// An iterator visiting all key-value pairs in arbitrary order. The iterator element type is `(PropertyKey, &'a Property)`. - /// - /// This iterator does not recurse down the prototype chain. - #[inline] - #[must_use] - pub fn iter(&self) -> Iter<'_> { - Iter { - indexed_properties: self.indexed_properties.iter(), - string_properties: self.string_properties.0.iter(), - symbol_properties: self.symbol_properties.0.iter(), - } - } - - /// An iterator visiting all keys in arbitrary order. The iterator element type is `PropertyKey`. - /// - /// This iterator does not recurse down the prototype chain. - #[inline] - #[must_use] - pub fn keys(&self) -> Keys<'_> { - Keys(self.iter()) - } - - /// An iterator visiting all values in arbitrary order. The iterator element type is `&'a Property`. - /// - /// This iterator does not recurse down the prototype chain. - #[inline] - #[must_use] - pub fn values(&self) -> Values<'_> { - Values(self.iter()) - } - - /// An iterator visiting all symbol key-value pairs in arbitrary order. The iterator element type is `(&'a RcSymbol, &'a Property)`. - /// - /// - /// This iterator does not recurse down the prototype chain. - #[inline] - #[must_use] - pub fn symbol_properties(&self) -> SymbolProperties<'_> { - SymbolProperties(self.symbol_properties.0.iter()) - } - - /// An iterator visiting all symbol keys in arbitrary order. The iterator element type is `&'a RcSymbol`. - /// - /// This iterator does not recurse down the prototype chain. - #[inline] - #[must_use] - pub fn symbol_property_keys(&self) -> SymbolPropertyKeys<'_> { - SymbolPropertyKeys(self.symbol_properties.0.keys()) - } - - /// An iterator visiting all symbol values in arbitrary order. The iterator element type is `&'a Property`. - /// - /// This iterator does not recurse down the prototype chain. - #[inline] - #[must_use] - pub fn symbol_property_values(&self) -> SymbolPropertyValues<'_> { - SymbolPropertyValues(self.symbol_properties.0.values()) - } - /// An iterator visiting all indexed key-value pairs in arbitrary order. The iterator element type is `(&'a u32, &'a Property)`. /// /// This iterator does not recurse down the prototype chain. @@ -376,50 +436,18 @@ impl PropertyMap { self.indexed_properties.values() } - /// An iterator visiting all string key-value pairs in arbitrary order. The iterator element type is `(&'a RcString, &'a Property)`. - /// - /// This iterator does not recurse down the prototype chain. - #[inline] - #[must_use] - pub fn string_properties(&self) -> StringProperties<'_> { - StringProperties(self.string_properties.0.iter()) - } - - /// An iterator visiting all string keys in arbitrary order. The iterator element type is `&'a RcString`. - /// - /// This iterator does not recurse down the prototype chain. - #[inline] - #[must_use] - pub fn string_property_keys(&self) -> StringPropertyKeys<'_> { - StringPropertyKeys(self.string_properties.0.keys()) - } - - /// An iterator visiting all string values in arbitrary order. The iterator element type is `&'a Property`. - /// - /// This iterator does not recurse down the prototype chain. - #[inline] - #[must_use] - pub fn string_property_values(&self) -> StringPropertyValues<'_> { - StringPropertyValues(self.string_properties.0.values()) - } - /// Returns `true` if the given key is contained in the [`PropertyMap`]. #[inline] #[must_use] pub fn contains_key(&self, key: &PropertyKey) -> bool { - match key { - PropertyKey::Index(index) => self.indexed_properties.contains_key(*index), - PropertyKey::String(string) => self.string_properties.0.contains_key(string), - PropertyKey::Symbol(symbol) => self.symbol_properties.0.contains_key(symbol), + if let PropertyKey::Index(index) = key { + return self.indexed_properties.contains_key(*index); + } + if self.shape.lookup(key).is_some() { + return true; } - } - - pub(crate) const fn string_property_map(&self) -> &GlobalPropertyMap { - &self.string_properties.0 - } - pub(crate) fn string_property_map_mut(&mut self) -> &mut GlobalPropertyMap { - &mut self.string_properties.0 + false } } diff --git a/boa_engine/src/object/shape/mod.rs b/boa_engine/src/object/shape/mod.rs new file mode 100644 index 00000000000..781ec099eed --- /dev/null +++ b/boa_engine/src/object/shape/mod.rs @@ -0,0 +1,125 @@ +// TODO: remove, after prototyping +#![allow(dead_code)] +#![allow(unreachable_code)] +#![allow(unused_imports)] +#![allow(clippy::let_and_return)] +#![allow(clippy::needless_pass_by_value)] +#![allow(unused)] + +//! TODO: doc + +pub(crate) mod shared_shape; +pub(crate) mod slot; +mod unique_shape; + +pub(crate) use unique_shape::UniqueShape; + +use std::{ + any::Any, + cell::{Cell, RefCell}, + collections::VecDeque, + fmt::Debug, + rc::Rc, +}; + +use bitflags::bitflags; +use boa_gc::{empty_trace, Finalize, Gc, GcRefCell, Trace, WeakGc}; +use rustc_hash::FxHashMap; + +use crate::{ + property::{Attribute, PropertyKey}, + JsString, +}; + +use self::{ + shared_shape::{SharedShape, TransitionKey}, + slot::Slot, +}; + +use super::JsPrototype; + +#[derive(Debug, Trace, Finalize, Clone)] +enum Inner { + Unique(UniqueShape), + Shared(SharedShape), +} + +#[derive(Debug, Trace, Finalize, Clone)] +pub struct Shape { + inner: Inner, +} + +impl Default for Shape { + fn default() -> Self { + Shape::unique(UniqueShape::default()) + } +} + +impl Shape { + pub(crate) fn root() -> Self { + Self::shared(SharedShape::root()) + } + + #[inline] + fn shared(inner: SharedShape) -> Self { + Self { + inner: Inner::Shared(inner), + } + } + + #[inline] + pub(crate) const fn unique(shape: UniqueShape) -> Self { + Self { + inner: Inner::Unique(shape), + } + } + + pub(crate) fn insert_property_transition(&self, key: TransitionKey) -> Self { + match &self.inner { + Inner::Shared(shape) => Self::shared(shape.insert_property_transition(key)), + Inner::Unique(shape) => Self::unique(shape.insert_property_transition(key)), + } + } + + pub(crate) fn change_attributes_transition(&self, key: TransitionKey) -> Self { + match &self.inner { + Inner::Shared(shape) => Self::shared(shape.change_attributes_transition(key)), + Inner::Unique(shape) => Self::unique(shape.change_attributes_transition(key)), + } + } + + pub(crate) fn remove_property_transition(&self, key: &PropertyKey) -> Self { + match &self.inner { + Inner::Shared(shape) => Self::shared(shape.remove_property_transition(key)), + Inner::Unique(shape) => Self::unique(shape.remove_property_transition(key)), + } + } + + 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 { + Inner::Shared(shape) => shape.lookup(key), + Inner::Unique(shape) => shape.lookup(key), + } + } + + pub(crate) fn keys(&self) -> Vec { + match &self.inner { + Inner::Shared(shape) => shape.keys(), + Inner::Unique(shape) => shape.keys(), + } + } +} diff --git a/boa_engine/src/object/shape/shared_shape/forward_transition.rs b/boa_engine/src/object/shape/shared_shape/forward_transition.rs new file mode 100644 index 00000000000..b3063010da5 --- /dev/null +++ b/boa_engine/src/object/shape/shared_shape/forward_transition.rs @@ -0,0 +1,50 @@ +use std::hash::Hash; + +use boa_gc::{Finalize, Gc, GcRefCell, Trace, WeakGc}; +use rustc_hash::FxHashMap; + +use crate::object::JsPrototype; + +use super::{Inner as SharedShapeInner, TransitionKey}; + +type TransitionMap = FxHashMap>; + +#[derive(Default, Debug, Trace, Finalize)] +struct Inner { + properties: Option>>, + prototypes: Option>>, +} + +/// Holds a forward reference to a previously created transition. +/// The reference is weak, therefore it can be garbage collected if it is not in use. +#[derive(Default, Debug, Trace, Finalize)] +pub(super) struct ForwardTransition { + inner: GcRefCell, +} + +impl ForwardTransition { + pub(super) fn insert_property(&self, key: TransitionKey, value: &Gc) { + let mut this = self.inner.borrow_mut(); + let properties = this.properties.get_or_insert_with(Box::default); + properties.insert(key, WeakGc::new(value)); + } + pub(super) fn insert_prototype(&self, key: JsPrototype, value: &Gc) { + let mut this = self.inner.borrow_mut(); + let prototypes = this.prototypes.get_or_insert_with(Box::default); + prototypes.insert(key, WeakGc::new(value)); + } + pub(super) fn get_property(&self, key: &TransitionKey) -> Option> { + let this = self.inner.borrow(); + let Some(transitions) = this.properties.as_ref() else { + return None; + }; + transitions.get(key).cloned() + } + pub(super) fn get_prototype(&self, key: &JsPrototype) -> Option> { + let this = self.inner.borrow(); + let Some(transitions) = this.prototypes.as_ref() else { + return None; + }; + transitions.get(key).cloned() + } +} diff --git a/boa_engine/src/object/shape/shared_shape/mod.rs b/boa_engine/src/object/shape/shared_shape/mod.rs new file mode 100644 index 00000000000..b8e970760ff --- /dev/null +++ b/boa_engine/src/object/shape/shared_shape/mod.rs @@ -0,0 +1,364 @@ +mod forward_transition; +mod property_table; + +use std::{ + cell::{Cell, RefCell}, + collections::hash_map::RandomState, + hash::Hash, + rc::Rc, +}; + +use boa_gc::{empty_trace, Finalize, Gc, GcRefCell, Trace, WeakGc}; +use indexmap::{IndexMap, IndexSet}; +use rustc_hash::FxHashMap; + +use crate::{ + object::JsPrototype, + property::{Attribute, PropertyDescriptor, PropertyKey}, +}; + +use self::{forward_transition::ForwardTransition, property_table::PropertyTable}; + +use super::{ + slot::{SlotAttribute, SlotIndex}, + Shape, Slot, +}; + +#[derive(Debug, Finalize, Clone, PartialEq, Eq, Hash)] +pub(crate) struct TransitionKey { + pub(crate) property_key: PropertyKey, + pub(crate) attributes: SlotAttribute, +} + +// SAFETY: non of the member of this struct are garbage collected, +// so this should be fine. +unsafe impl Trace for TransitionKey { + empty_trace!(); +} + +#[derive(Debug, Finalize, Clone, Copy, PartialEq, Eq)] +pub(crate) enum TransitionType { + /// Inserts a new property. + Insert, + + /// Change existing property attributes. + Configure, + + /// Change prototype. + Prototype, +} + +unsafe impl Trace for TransitionType { + empty_trace!(); +} + +/// TODO: doc +#[derive(Debug, Trace, Finalize)] +struct Inner { + forward_transitions: ForwardTransition, + + property_count: u32, + + /// Instance prototype `__proto__`. + prototype: JsPrototype, + + #[unsafe_ignore_trace] + property_table: PropertyTable, + + previous: Option, + + // TODO: add prototype to shape + transition_type: TransitionType, +} + +#[derive(Debug, Trace, Finalize, Clone)] +pub(crate) struct SharedShape { + inner: Gc, +} + +impl SharedShape { + const DEBUG: bool = false; + + /// Return the property count that this shape owns in the [`PropertyTable`]. + fn property_count(&self) -> u32 { + self.inner.property_count + } + /// Return the index to the property in the the [`PropertyTable`]. + fn property_index(&self) -> u32 { + self.inner.property_count.saturating_sub(1) + } + + pub(crate) fn previous(&self) -> Option<&SharedShape> { + self.inner.previous.as_ref() + } + pub(crate) fn prototype(&self) -> JsPrototype { + self.inner.prototype.clone() + } + pub(crate) fn property(&self) -> (PropertyKey, Slot) { + let properties = self.inner.property_table.properties().borrow(); + let (key, slot) = properties + .get_index(self.property_index() as usize) + .expect("There should be a property"); + (key.clone(), *slot) + } + fn transition_type(&self) -> TransitionType { + self.inner.transition_type + } + pub(crate) fn is_root(&self) -> bool { + self.inner.previous.is_none() + } + fn forward_transitions(&self) -> &ForwardTransition { + &self.inner.forward_transitions + } + fn new(inner: Inner) -> Self { + Self { + inner: Gc::new(inner), + } + } + + pub(crate) fn root() -> Self { + Self::new(Inner { + forward_transitions: ForwardTransition::default(), + prototype: None, + property_count: 0, + property_table: PropertyTable::default(), + previous: None, + transition_type: TransitionType::Insert, + }) + } + + fn create_property_transition(&self, key: TransitionKey) -> Self { + let slot_index = self.property_count() + // TODO: implement different sized storage + * 2; + + let (property_index, property_table) = + self.inner.property_table.add_property_deep_clone_if_needed( + key.property_key.clone(), + key.attributes, + slot_index, + self.property_count(), + ); + let new_inner_shape = Inner { + prototype: self.prototype(), + forward_transitions: ForwardTransition::default(), + property_table, + property_count: self.property_count() + 1, + previous: Some(self.clone()), + transition_type: TransitionType::Insert, + }; + let new_shape = Self::new(new_inner_shape); + + self.forward_transitions() + .insert_property(key, &new_shape.inner); + + new_shape + } + + pub(crate) fn change_prototype_transition(&self, prototype: JsPrototype) -> Self { + if let Some(shape) = self.forward_transitions().get_prototype(&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 { + forward_transitions: ForwardTransition::default(), + prototype: prototype.clone(), + property_table: self.inner.property_table.clone(), + property_count: self.property_count(), + previous: Some(self.clone()), + transition_type: TransitionType::Prototype, + }; + let new_shape = Self::new(new_inner_shape); + + self.forward_transitions() + .insert_prototype(prototype, &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.forward_transitions().get_property(&key) { + if Self::DEBUG { + println!("Shape: Trying to resuse previous shape"); + } + if let Some(inner) = shape.upgrade() { + // println!(" Shape: Resusing previous shape"); + return Self { inner }; + } + + if Self::DEBUG { + println!(" Shape: Recreating shape because it got GC collected"); + } + return self.create_property_transition(key); + } + + if Self::DEBUG { + println!("Shape: Creating new shape"); + } + self.create_property_transition(key) + } + + pub(crate) fn change_attributes_transition(&self, key: TransitionKey) -> Self { + let property_table = self.inner.property_table.deep_clone_all(); + property_table.set_attributes(&key.property_key, key.attributes); + let new_inner_shape = Inner { + forward_transitions: ForwardTransition::default(), + prototype: self.prototype(), + property_table, + property_count: self.property_count(), + previous: Some(self.clone()), + transition_type: TransitionType::Configure, + }; + let new_shape = Self::new(new_inner_shape); + + self.forward_transitions() + .insert_property(key, &new_shape.inner); + + new_shape + } + + // Rollback to the transition before the propery was inserted. + // + // For example with the following chain: + // + // INSERT(x) INSERT(y) INSERT(z) + // { } ------------> { x } ------------> { x, y } ------------> { x, y, z } + // + // Then we call delete on `y`. We rollback to before the property was added and we put + // the transitions in a array for reconstruction of the new branch: + // + // INSERT(x) INSERT(y) INSERT(z) + // { } ------------> { x } ------------> { x, y } ------------> { x, y, z } + // ^ + // \--- base ( with array of transitions to be performed: INSERT(z) ) + // + // Then we apply transitions (z): + // + // INSERT(x) INSERT(y) INSERT(z) + // { } ------------> { x } ------------> { x, y } ------------> { x, y, z } + // | + // | INSERT(z) + // \----------------> { x, z } <----- The shape we return :) + // + pub(crate) fn remove_property_transition(&self, key: &PropertyKey) -> Self { + if Self::DEBUG { + println!("Shape: deleting {key}"); + } + + let mut prototype = None; + let mut transitions: IndexMap = + IndexMap::default(); + + 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, slot) = current_shape.property(); + + if current_shape.transition_type() == TransitionType::Insert + && ¤t_property_key == key + { + let mut base = if let Some(base) = current_shape.previous() { + base.clone() + } else { + // It's the root, because it doesn't have previous. + current_shape.clone() + }; + break base; + } + + // Do not add property that we are trying to delete. + // this can happen if a configure was called after inserting it into the shape + if ¤t_property_key != key { + // Only take the latest changes to a property. To try to build a smaller tree. + transitions + .entry(current_property_key) + .or_insert(slot.attributes); + } + + 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, + attributes, + }; + base = base.insert_property_transition(transition); + } + + base + } + + #[inline] + pub(crate) fn lookup(&self, key: &PropertyKey) -> Option { + if Self::DEBUG { + println!("Shape: lookup {key}"); + } + + let property_count = self.property_count(); + if property_count == 0 { + return None; + } + + let property_table = self.inner.property_table.properties().borrow(); + if let Some((property_table_index, property_key, slot)) = property_table.get_full(key) { + debug_assert_eq!(key, property_key); + + if Self::DEBUG { + println!("Key {key}, PTI: {property_table_index}, SI: {}", slot.index); + } + // Check if we are trying to access properties that belong to another shape. + if (property_table_index as u32) < self.property_count() { + return Some(*slot); + } + } + None + } + + pub(crate) fn keys(&self) -> Vec { + let property_table = self.inner.property_table.properties().borrow(); + + let property_count = self.property_count() as usize; + + let mut keys: Vec = property_table + .keys() + .take(property_count) + .filter(|key| matches!(key, PropertyKey::String(_))) + .cloned() + .collect(); + + keys.extend( + property_table + .keys() + .take(property_count) + .filter(|key| matches!(key, PropertyKey::Symbol(_))) + .cloned(), + ); + + keys + } +} diff --git a/boa_engine/src/object/shape/shared_shape/property_table.rs b/boa_engine/src/object/shape/shared_shape/property_table.rs new file mode 100644 index 00000000000..fd5daa26fa0 --- /dev/null +++ b/boa_engine/src/object/shape/shared_shape/property_table.rs @@ -0,0 +1,115 @@ +use std::{cell::RefCell, rc::Rc}; + +use indexmap::IndexMap; + +use crate::{ + object::shape::slot::{Slot, SlotAttribute}, + property::PropertyKey, +}; + +#[derive(Default, Debug, Clone)] +struct Inner { + properties: RefCell>, +} + +#[derive(Default, Debug, Clone)] +pub(crate) struct PropertyTable { + inner: Rc, +} + +impl PropertyTable { + pub(crate) fn properties(&self) -> &RefCell> { + &self.inner.properties + } + + pub(crate) fn add_property_deep_clone_if_needed( + &self, + key: PropertyKey, + attributes: SlotAttribute, + 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. + // TOOD: possible optimization if we already have the property in the exact position we want it to be with the same properties, + // then just return self, this might happen, if a branch is created than after not being used gets gc collected we still have the + // properties here. + // TODO: possible optimization, figure out if there **always** are as many properties as there are strong references. If so truncate + // on drop Rc ref drop, maybe? + { + 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}" + // ); + let (index, value) = properties.insert_full( + key, + Slot { + index: 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}" + // ); + + // property is already present need to make deep clone of property table. + let this = self.deep_clone(property_count); + let index = { + let mut properties = this.inner.properties.borrow_mut(); + let (index, value) = properties.insert_full( + key, + Slot { + index: slot_index, + attributes, + }, + ); + debug_assert!(value.is_none()); + index + }; + (index as u32, this) + } + + pub(crate) fn deep_clone(&self, count: u32) -> Self { + let count = count as usize; + + let properties = { + let mut properties = self.inner.properties.borrow(); + let mut result = IndexMap::default(); + result.extend( + properties + .iter() + .take(count) + .map(|(key, slot)| (key.clone(), *slot)), + ); + result + }; + Self { + inner: Rc::new(Inner { + properties: RefCell::new(properties), + }), + } + } + + pub(crate) fn deep_clone_all(&self) -> Self { + Self { + inner: Rc::new((*self.inner).clone()), + } + } + + pub(crate) fn set_attributes( + &self, + property_key: &PropertyKey, + property_attributes: SlotAttribute, + ) { + let mut properties = self.inner.properties.borrow_mut(); + let Some(Slot { attributes, .. }) = properties.get_mut(property_key) else { + unreachable!("There should already be a property!") + }; + *attributes = property_attributes; + } +} diff --git a/boa_engine/src/object/shape/slot.rs b/boa_engine/src/object/shape/slot.rs new file mode 100644 index 00000000000..1580d03e47a --- /dev/null +++ b/boa_engine/src/object/shape/slot.rs @@ -0,0 +1,35 @@ +use bitflags::bitflags; +pub(crate) type SlotIndex = u32; + +bitflags! { + #[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Hash)] + pub struct SlotAttribute: u8 { + const WRITABLE = 0b0000_0001; + const ENUMERABLE = 0b0000_0010; + const CONFIGURABLE = 0b0000_0100; + const HAS_GET = 0b0000_1000; + const HAS_SET = 0b0001_0000; + } +} + +impl SlotAttribute { + pub const fn is_accessor_descriptor(self) -> bool { + self.contains(Self::HAS_GET) || self.contains(Self::HAS_SET) + } + pub const fn is_data_descriptor(self) -> bool { + !self.is_accessor_descriptor() + } + + pub const fn has_get(self) -> bool { + self.contains(Self::HAS_GET) + } + pub const fn has_set(self) -> bool { + self.contains(Self::HAS_SET) + } +} + +#[derive(Debug, Clone, Copy)] +pub struct Slot { + pub index: SlotIndex, + pub attributes: SlotAttribute, +} diff --git a/boa_engine/src/object/shape/unique_shape.rs b/boa_engine/src/object/shape/unique_shape.rs new file mode 100644 index 00000000000..b83435568ff --- /dev/null +++ b/boa_engine/src/object/shape/unique_shape.rs @@ -0,0 +1,143 @@ +use std::{ + any::Any, + cell::{Cell, RefCell}, + collections::VecDeque, + fmt::Debug, + rc::Rc, +}; + +use bitflags::bitflags; +use boa_ast::property; +use boa_gc::{empty_trace, Finalize, Gc, GcRefCell, Trace, WeakGc}; +use indexmap::{IndexMap, IndexSet}; +use rustc_hash::FxHashMap; + +use crate::{ + property::{Attribute, PropertyKey}, + JsString, +}; + +use super::{shared_shape::TransitionKey, slot::SlotAttribute, JsPrototype, Slot}; + +/// TODO: doc +#[derive(Default, Debug, Trace, Finalize)] +struct Inner { + #[unsafe_ignore_trace] + property_table: RefCell>, + + prototype: GcRefCell, +} + +/// TODO: doc +#[derive(Default, Debug, Clone, Trace, Finalize)] +pub(crate) struct UniqueShape { + inner: Gc, +} + +impl UniqueShape { + 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: Gc::new(Inner { + property_table: property_table.into(), + prototype, + }), + } + } + + /// TODO: doc + pub(crate) fn insert_property_transition(&self, key: TransitionKey) -> Self { + { + let mut property_table = self.inner.property_table.borrow_mut(); + let (index, inserted) = property_table.insert_full(key.property_key, key.attributes); + + debug_assert!(inserted.is_none()); + debug_assert!(index + 1 == property_table.len()); + } + + self.clone() + } + + /// TODO: doc + pub(crate) fn remove_property_transition(&self, key: &PropertyKey) -> Self { + let mut property_table = self.inner.property_table.borrow_mut(); + let Some((index, _property_key, _attributes)) = property_table.shift_remove_full(key) else { + return self.clone(); + }; + + /// Check if it's the last property, that was deleted, + /// if so delete it and return self + if index == property_table.len() { + return self.clone(); + } + + // The property that was deleted was not the last property added. + // 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); + let prototype = self.inner.prototype.clone(); + Self::new(prototype, property_table) + } + + /// TODO: doc + pub(crate) fn lookup(&self, key: &PropertyKey) -> Option { + // println!("Unique: lookup {key}"); + let mut property_table = self.inner.property_table.borrow(); + if let Some((index, _property_key, attributes)) = property_table.get_full(key) { + return Some(Slot { + index: index as u32 * 2, + attributes: *attributes, + }); + } + + None + } + + pub(crate) fn change_attributes_transition(&self, key: TransitionKey) -> Self { + { + let mut property_table = self.inner.property_table.borrow_mut(); + let Some(attributes) = property_table.get_mut(&key.property_key) else { + unreachable!("Attribute change can only happen on existing property") + }; + + *attributes = key.attributes; + } + 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 { + let mut property_table = self.inner.property_table.borrow(); + + let mut keys = Vec::with_capacity(property_table.len()); + for key in property_table + .keys() + .filter(|x| matches!(x, PropertyKey::String(_))) + { + keys.push(key.clone()); + } + for key in property_table + .keys() + .filter(|x| matches!(x, PropertyKey::Symbol(_))) + { + keys.push(key.clone()); + } + + keys + } +} diff --git a/boa_engine/src/property/attribute/mod.rs b/boa_engine/src/property/attribute/mod.rs index ac816d172a6..fe67effba1f 100644 --- a/boa_engine/src/property/attribute/mod.rs +++ b/boa_engine/src/property/attribute/mod.rs @@ -15,7 +15,7 @@ bitflags! { /// - `[[Configurable]]` (`CONFIGURABLE`) - If `false`, attempts to delete the property, /// change the property to be an `accessor property`, or change its attributes (other than `[[Value]]`, /// or changing `[[Writable]]` to `false`) will fail. - #[derive(Debug, Clone, Copy)] + #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub struct Attribute: u8 { /// The `Writable` attribute decides whether the value associated with the property can be changed or not, from its initial value. const WRITABLE = 0b0000_0001; diff --git a/boa_engine/src/property/mod.rs b/boa_engine/src/property/mod.rs index fd639b6857b..cb748e0b490 100644 --- a/boa_engine/src/property/mod.rs +++ b/boa_engine/src/property/mod.rs @@ -557,7 +557,7 @@ impl From for PropertyDescriptor { /// - [ECMAScript reference][spec] /// /// [spec]: https://tc39.es/ecma262/#sec-ispropertykey -#[derive(PartialEq, Debug, Clone, Eq, Hash)] +#[derive(Finalize, PartialEq, Debug, Clone, Eq, Hash)] pub enum PropertyKey { /// A string property key. String(JsString), diff --git a/boa_engine/src/realm.rs b/boa_engine/src/realm.rs index e76c323cbf9..960cfbe8dd9 100644 --- a/boa_engine/src/realm.rs +++ b/boa_engine/src/realm.rs @@ -9,7 +9,7 @@ use crate::{ context::{intrinsics::Intrinsics, HostHooks}, environments::{CompileTimeEnvironment, DeclarativeEnvironmentStack}, - object::{GlobalPropertyMap, JsObject, PropertyMap}, + object::{JsObject, PropertyMap}, }; use boa_gc::{Gc, GcRefCell}; use boa_profiler::Profiler; @@ -62,8 +62,8 @@ impl Realm { &self.global_this } - pub(crate) fn global_bindings_mut(&mut self) -> &mut GlobalPropertyMap { - self.global_property_map.string_property_map_mut() + pub(crate) fn global_bindings_mut(&mut self) -> &mut PropertyMap { + &mut self.global_property_map } /// Set the number of bindings on the global environment. diff --git a/boa_engine/src/value/display.rs b/boa_engine/src/value/display.rs index ea54b0ffc1c..313f31b2b4f 100644 --- a/boa_engine/src/value/display.rs +++ b/boa_engine/src/value/display.rs @@ -67,15 +67,19 @@ macro_rules! print_obj_value { } }; (props of $obj:expr, $display_fn:ident, $indent:expr, $encounters:expr, $print_internals:expr) => { - print_obj_value!(impl $obj, |(key, val)| { + {let mut keys: Vec<_> = $obj.borrow().properties().index_property_keys().map(crate::property::PropertyKey::Index).collect(); + keys.extend($obj.borrow().properties().shape.keys()); + let mut result = Vec::default(); + for key in keys { + let val = $obj.borrow().properties().get(&key).expect("There should be a value"); if val.is_data_descriptor() { let v = &val.expect_value(); - format!( + result.push(format!( "{:>width$}: {}", key, $display_fn(v, $encounters, $indent.wrapping_add(4), $print_internals), width = $indent, - ) + )); } else { let display = match (val.set().is_some(), val.get().is_some()) { (true, true) => "Getter & Setter", @@ -83,20 +87,10 @@ macro_rules! print_obj_value { (false, true) => "Getter", _ => "No Getter/Setter" }; - format!("{:>width$}: {}", key, display, width = $indent) + result.push(format!("{:>width$}: {}", key, display, width = $indent)); } - }) - }; - - // A private overload of the macro - // DO NOT use directly - (impl $v:expr, $f:expr) => { - $v - .borrow() - .properties() - .iter() - .map($f) - .collect::>() + } + result} }; } 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/value/serde_json.rs b/boa_engine/src/value/serde_json.rs index 153c05c2680..7f2187a55b1 100644 --- a/boa_engine/src/value/serde_json.rs +++ b/boa_engine/src/value/serde_json.rs @@ -137,8 +137,8 @@ impl JsValue { Ok(Value::Array(arr)) } else { let mut map = Map::new(); - for (key, property) in obj.borrow().properties().iter() { - let key = match &key { + for property_key in obj.borrow().properties().shape.keys() { + let key = match &property_key { PropertyKey::String(string) => string.to_std_string_escaped(), PropertyKey::Index(i) => i.to_string(), PropertyKey::Symbol(_sym) => { @@ -148,7 +148,12 @@ impl JsValue { } }; - let value = match property.value() { + let value = match obj + .borrow() + .properties() + .get(&property_key) + .and_then(|x| x.value().cloned()) + { Some(val) => val.to_json(context)?, None => Value::Null, }; diff --git a/boa_engine/src/vm/opcode/define/mod.rs b/boa_engine/src/vm/opcode/define/mod.rs index 30adcab0665..dc6561df4ab 100644 --- a/boa_engine/src/vm/opcode/define/mod.rs +++ b/boa_engine/src/vm/opcode/define/mod.rs @@ -1,5 +1,5 @@ use crate::{ - property::PropertyDescriptor, + property::{PropertyDescriptor, PropertyKey}, vm::{opcode::Operation, CompletionType}, Context, JsResult, JsString, JsValue, }; @@ -30,14 +30,19 @@ impl Operation for DefVar { .interner() .resolve_expect(binding_locator.name().sym()) .into_common(false); - context.global_bindings_mut().entry(key).or_insert( - PropertyDescriptor::builder() - .value(JsValue::Undefined) - .writable(true) - .enumerable(true) - .configurable(true) - .build(), - ); + + let key = PropertyKey::String(key); + if context.global_bindings_mut().get(&key).is_none() { + context.global_bindings_mut().insert( + &key, + PropertyDescriptor::builder() + .value(JsValue::Undefined) + .writable(true) + .enumerable(true) + .configurable(true) + .build(), + ); + }; } else { context.realm.environments.put_value_if_uninitialized( binding_locator.environment_index(), diff --git a/boa_engine/src/vm/opcode/get/name.rs b/boa_engine/src/vm/opcode/get/name.rs index 7a128716bb2..6e5c078da7b 100644 --- a/boa_engine/src/vm/opcode/get/name.rs +++ b/boa_engine/src/vm/opcode/get/name.rs @@ -1,6 +1,6 @@ use crate::{ error::JsNativeError, - property::DescriptorKind, + property::{DescriptorKind, PropertyKey}, vm::{opcode::Operation, CompletionType}, Context, JsResult, JsString, JsValue, }; @@ -29,7 +29,10 @@ impl Operation for GetName { .interner() .resolve_expect(binding_locator.name().sym()) .into_common(false); - match context.global_bindings_mut().get(&key) { + match context + .global_bindings_mut() + .get(&PropertyKey::String(key.clone())) + { Some(desc) => match desc.kind() { DescriptorKind::Data { value: Some(value), .. @@ -98,7 +101,7 @@ impl Operation for GetNameOrUndefined { .interner() .resolve_expect(binding_locator.name().sym()) .into_common(false); - match context.global_bindings_mut().get(&key) { + match context.global_bindings_mut().get(&PropertyKey::String(key)) { Some(desc) => match desc.kind() { DescriptorKind::Data { value: Some(value), .. diff --git a/boa_engine/src/vm/opcode/push/object.rs b/boa_engine/src/vm/opcode/push/object.rs index 9ec4b2bda03..25a5a38cc06 100644 --- a/boa_engine/src/vm/opcode/push/object.rs +++ b/boa_engine/src/vm/opcode/push/object.rs @@ -17,6 +17,7 @@ impl Operation for PushEmptyObject { fn execute(context: &mut Context<'_>) -> JsResult { let o = JsObject::with_object_proto(context); + o.set_shape(context.empty_object_shape.clone()); context.vm.push(o); Ok(CompletionType::Normal) } diff --git a/boa_engine/src/vm/opcode/set/name.rs b/boa_engine/src/vm/opcode/set/name.rs index e3f99f76a29..7ed206e22b7 100644 --- a/boa_engine/src/vm/opcode/set/name.rs +++ b/boa_engine/src/vm/opcode/set/name.rs @@ -1,5 +1,6 @@ use crate::{ error::JsNativeError, + property::PropertyKey, vm::{opcode::Operation, CompletionType}, Context, JsResult, JsString, }; @@ -30,7 +31,10 @@ impl Operation for SetName { .interner() .resolve_expect(binding_locator.name().sym()) .into_common(false); - let exists = context.global_bindings_mut().contains_key(&key); + let exists = context + .global_bindings_mut() + .get(&PropertyKey::String(key.clone())) + .is_some(); if !exists && context.vm.frame().code_block.strict { return Err(JsNativeError::reference()