mirror of https://github.com/boa-dev/boa.git
Browse Source
This PR implements `Hidden Classes`, I named them as `Shapes` (like Spidermonkey does), calling them maps like v8 seems confusing because there already is a JS builtin, likewise with `Hidden classes` since there are already classes in JS. There are two types of shapes: `shared` shapes that create the transition tree, and are shared between objects, this is mainly intended for user defined objects this makes more sense because shapes can create transitions trees, doing that for the builtins seems wasteful (unless users wanted to creating an object with the same property names and the same property attributes in the same order... which seems unlikely). That's why I added `unique` shapes, only one object has it. This is similar to previous solution, but this architecture enables us to use inline caching. There will probably be a performance hit until we implement inline caching. There still a lot of work that needs to be done, on this: - [x] Move Property Attributes to shape - [x] Move Prototype to shape - [x] ~~Move extensible flag to shape~~, On further evaluation this doesn't give any benefit (at least right now), since it isn't used by inline caching also adding one more transition. - [x] Implement delete for unique shapes. - [x] If the chain is too long we should probably convert it into a `unique` shape - [x] Figure out threshold ~~(maybe more that 256 properties ?)~~ curently set to an arbitrary number (`1024`) - [x] Implement shared property table between shared shapes - [x] Add code Document - [x] Varying size storage for properties (get+set = 2, data = 1) - [x] Add shapes to more object: - [x] ordinary object - [x] Arrays - [x] Functions - [x] Other builtins - [x] Add `shapes.md` doc explaining shapes in depth with mermaid diagrams :) - [x] Add `$boa.shape` module - [x] `$boa.shape.id(o)` - [x] `$boa.shape.type(o)` - [x] `$boa.shape.same(o1, o2)` - [x] add doc to `boa_object.md`pull/2872/head
Haled Odat
2 years ago
90 changed files with 3335 additions and 1121 deletions
@ -0,0 +1,66 @@
|
||||
use boa_engine::{ |
||||
js_string, object::ObjectInitializer, Context, JsArgs, JsNativeError, JsObject, JsResult, |
||||
JsValue, NativeFunction, |
||||
}; |
||||
|
||||
fn get_object(args: &[JsValue], position: usize) -> JsResult<&JsObject> { |
||||
let value = args.get_or_undefined(position); |
||||
|
||||
let Some(object) = value.as_object() else { |
||||
return Err(JsNativeError::typ() |
||||
.with_message(format!("expected object in argument position {position}, got {}", value.type_of())) |
||||
.into()); |
||||
}; |
||||
|
||||
Ok(object) |
||||
} |
||||
|
||||
/// Returns object's shape pointer in memory.
|
||||
fn id(_: &JsValue, args: &[JsValue], _: &mut Context<'_>) -> JsResult<JsValue> { |
||||
let object = get_object(args, 0)?; |
||||
let object = object.borrow(); |
||||
let shape = object.shape(); |
||||
Ok(format!("0x{:X}", shape.to_addr_usize()).into()) |
||||
} |
||||
|
||||
/// Returns object's shape type.
|
||||
fn r#type(_: &JsValue, args: &[JsValue], _: &mut Context<'_>) -> JsResult<JsValue> { |
||||
let object = get_object(args, 0)?; |
||||
let object = object.borrow(); |
||||
let shape = object.shape(); |
||||
|
||||
Ok(if shape.is_shared() { |
||||
js_string!("shared") |
||||
} else { |
||||
js_string!("unique") |
||||
} |
||||
.into()) |
||||
} |
||||
|
||||
/// Returns object's shape type.
|
||||
fn same(_: &JsValue, args: &[JsValue], _: &mut Context<'_>) -> JsResult<JsValue> { |
||||
let lhs = get_object(args, 0)?; |
||||
let rhs = get_object(args, 1)?; |
||||
|
||||
let lhs_shape_ptr = { |
||||
let object = lhs.borrow(); |
||||
let shape = object.shape(); |
||||
shape.to_addr_usize() |
||||
}; |
||||
|
||||
let rhs_shape_ptr = { |
||||
let object = rhs.borrow(); |
||||
let shape = object.shape(); |
||||
shape.to_addr_usize() |
||||
}; |
||||
|
||||
Ok(JsValue::new(lhs_shape_ptr == rhs_shape_ptr)) |
||||
} |
||||
|
||||
pub(super) fn create_object(context: &mut Context<'_>) -> JsObject { |
||||
ObjectInitializer::new(context) |
||||
.function(NativeFunction::from_fn_ptr(id), "id", 1) |
||||
.function(NativeFunction::from_fn_ptr(r#type), "type", 1) |
||||
.function(NativeFunction::from_fn_ptr(same), "same", 2) |
||||
.build() |
||||
} |
@ -0,0 +1,217 @@
|
||||
//! Implements object shapes.
|
||||
|
||||
pub(crate) mod property_table; |
||||
pub(crate) mod shared_shape; |
||||
pub(crate) mod slot; |
||||
pub(crate) mod unique_shape; |
||||
|
||||
pub use shared_shape::SharedShape; |
||||
pub(crate) use unique_shape::UniqueShape; |
||||
|
||||
use std::fmt::Debug; |
||||
|
||||
use boa_gc::{Finalize, Trace}; |
||||
|
||||
use crate::property::PropertyKey; |
||||
|
||||
use self::{shared_shape::TransitionKey, slot::Slot}; |
||||
|
||||
use super::JsPrototype; |
||||
|
||||
/// Action to be performed after a property attribute change
|
||||
//
|
||||
// Example: of { get/set x() { ... }, y: ... } into { x: ..., y: ... }
|
||||
//
|
||||
// 0 1 2
|
||||
// Storage: | get x | set x | y |
|
||||
//
|
||||
// We delete at position of x which is index 0 (it spans two elements) + 1:
|
||||
//
|
||||
// 0 1
|
||||
// Storage: | x | y |
|
||||
pub(crate) enum ChangeTransitionAction { |
||||
/// Do nothing to storage.
|
||||
Nothing, |
||||
|
||||
/// Remove element at (index + 1) from storage.
|
||||
Remove, |
||||
|
||||
/// Insert element at (index + 1) into storage.
|
||||
Insert, |
||||
} |
||||
|
||||
/// The result of a change property attribute transition.
|
||||
pub(crate) struct ChangeTransition<T> { |
||||
/// The shape after transition.
|
||||
pub(crate) shape: T, |
||||
|
||||
/// The needed action to be performed after transition to the object storage.
|
||||
pub(crate) action: ChangeTransitionAction, |
||||
} |
||||
|
||||
/// The internal representation of [`Shape`].
|
||||
#[derive(Debug, Trace, Finalize, Clone)] |
||||
enum Inner { |
||||
Unique(UniqueShape), |
||||
Shared(SharedShape), |
||||
} |
||||
|
||||
/// Represents the shape of an object.
|
||||
#[derive(Debug, Trace, Finalize, Clone)] |
||||
pub struct Shape { |
||||
inner: Inner, |
||||
} |
||||
|
||||
impl Default for Shape { |
||||
fn default() -> Self { |
||||
Shape::unique(UniqueShape::default()) |
||||
} |
||||
} |
||||
|
||||
impl Shape { |
||||
/// The max transition count of a [`SharedShape`] from the root node,
|
||||
/// before the shape will be converted into a [`UniqueShape`]
|
||||
///
|
||||
/// NOTE: This only applies to [`SharedShape`].
|
||||
const TRANSITION_COUNT_MAX: u16 = 1024; |
||||
|
||||
/// Create a [`Shape`] from a [`SharedShape`].
|
||||
pub(crate) fn shared(inner: SharedShape) -> Self { |
||||
Self { |
||||
inner: Inner::Shared(inner), |
||||
} |
||||
} |
||||
|
||||
/// Create a [`Shape`] from a [`UniqueShape`].
|
||||
pub(crate) const fn unique(shape: UniqueShape) -> Self { |
||||
Self { |
||||
inner: Inner::Unique(shape), |
||||
} |
||||
} |
||||
|
||||
/// Returns `true` if it's a shared shape, `false` otherwise.
|
||||
#[inline] |
||||
pub const fn is_shared(&self) -> bool { |
||||
matches!(self.inner, Inner::Shared(_)) |
||||
} |
||||
|
||||
/// Returns `true` if it's a unique shape, `false` otherwise.
|
||||
#[inline] |
||||
pub const fn is_unique(&self) -> bool { |
||||
matches!(self.inner, Inner::Unique(_)) |
||||
} |
||||
|
||||
pub(crate) const fn as_unique(&self) -> Option<&UniqueShape> { |
||||
if let Inner::Unique(shape) = &self.inner { |
||||
return Some(shape); |
||||
} |
||||
None |
||||
} |
||||
|
||||
/// Create an insert property transitions returning the new transitioned [`Shape`].
|
||||
///
|
||||
/// NOTE: This assumes that there is no property with the given key!
|
||||
pub(crate) fn insert_property_transition(&self, key: TransitionKey) -> Self { |
||||
match &self.inner { |
||||
Inner::Shared(shape) => { |
||||
let shape = shape.insert_property_transition(key); |
||||
if shape.transition_count() >= Self::TRANSITION_COUNT_MAX { |
||||
return Self::unique(shape.to_unique()); |
||||
} |
||||
Self::shared(shape) |
||||
} |
||||
Inner::Unique(shape) => Self::unique(shape.insert_property_transition(key)), |
||||
} |
||||
} |
||||
|
||||
/// Create a change attribute property transitions returning [`ChangeTransition`] containing the new [`Shape`]
|
||||
/// and actions to be performed
|
||||
///
|
||||
/// NOTE: This assumes that there already is a property with the given key!
|
||||
pub(crate) fn change_attributes_transition( |
||||
&self, |
||||
key: TransitionKey, |
||||
) -> ChangeTransition<Shape> { |
||||
match &self.inner { |
||||
Inner::Shared(shape) => { |
||||
let change_transition = shape.change_attributes_transition(key); |
||||
let shape = |
||||
if change_transition.shape.transition_count() >= Self::TRANSITION_COUNT_MAX { |
||||
Self::unique(change_transition.shape.to_unique()) |
||||
} else { |
||||
Self::shared(change_transition.shape) |
||||
}; |
||||
ChangeTransition { |
||||
shape, |
||||
action: change_transition.action, |
||||
} |
||||
} |
||||
Inner::Unique(shape) => shape.change_attributes_transition(&key), |
||||
} |
||||
} |
||||
|
||||
/// Remove a property property from the [`Shape`] returning the new transitioned [`Shape`].
|
||||
///
|
||||
/// NOTE: This assumes that there already is a property with the given key!
|
||||
pub(crate) fn remove_property_transition(&self, key: &PropertyKey) -> Self { |
||||
match &self.inner { |
||||
Inner::Shared(shape) => { |
||||
let shape = shape.remove_property_transition(key); |
||||
if shape.transition_count() >= Self::TRANSITION_COUNT_MAX { |
||||
return Self::unique(shape.to_unique()); |
||||
} |
||||
Self::shared(shape) |
||||
} |
||||
Inner::Unique(shape) => Self::unique(shape.remove_property_transition(key)), |
||||
} |
||||
} |
||||
|
||||
/// Create a prototype transitions returning the new transitioned [`Shape`].
|
||||
pub(crate) fn change_prototype_transition(&self, prototype: JsPrototype) -> Self { |
||||
match &self.inner { |
||||
Inner::Shared(shape) => { |
||||
let shape = shape.change_prototype_transition(prototype); |
||||
if shape.transition_count() >= Self::TRANSITION_COUNT_MAX { |
||||
return Self::unique(shape.to_unique()); |
||||
} |
||||
Self::shared(shape) |
||||
} |
||||
Inner::Unique(shape) => Self::unique(shape.change_prototype_transition(prototype)), |
||||
} |
||||
} |
||||
|
||||
/// Get the [`JsPrototype`] of the [`Shape`].
|
||||
pub fn prototype(&self) -> JsPrototype { |
||||
match &self.inner { |
||||
Inner::Shared(shape) => shape.prototype(), |
||||
Inner::Unique(shape) => shape.prototype(), |
||||
} |
||||
} |
||||
|
||||
/// Lookup a property in the shape
|
||||
#[inline] |
||||
pub(crate) fn lookup(&self, key: &PropertyKey) -> Option<Slot> { |
||||
match &self.inner { |
||||
Inner::Shared(shape) => shape.lookup(key), |
||||
Inner::Unique(shape) => shape.lookup(key), |
||||
} |
||||
} |
||||
|
||||
/// Returns the keys of the [`Shape`], in insertion order.
|
||||
#[inline] |
||||
pub fn keys(&self) -> Vec<PropertyKey> { |
||||
match &self.inner { |
||||
Inner::Shared(shape) => shape.keys(), |
||||
Inner::Unique(shape) => shape.keys(), |
||||
} |
||||
} |
||||
|
||||
/// Return location in memory of the [`Shape`].
|
||||
#[inline] |
||||
pub fn to_addr_usize(&self) -> usize { |
||||
match &self.inner { |
||||
Inner::Shared(shape) => shape.to_addr_usize(), |
||||
Inner::Unique(shape) => shape.to_addr_usize(), |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,149 @@
|
||||
use std::{cell::RefCell, rc::Rc}; |
||||
|
||||
use rustc_hash::FxHashMap; |
||||
|
||||
use crate::{ |
||||
object::shape::slot::{Slot, SlotAttributes}, |
||||
property::PropertyKey, |
||||
}; |
||||
|
||||
/// The internal representation of [`PropertyTable`].
|
||||
#[derive(Default, Debug, Clone)] |
||||
pub(crate) struct PropertyTableInner { |
||||
pub(crate) map: FxHashMap<PropertyKey, (u32, Slot)>, |
||||
pub(crate) keys: Vec<(PropertyKey, Slot)>, |
||||
} |
||||
|
||||
impl PropertyTableInner { |
||||
/// Returns all the keys, in insertion order.
|
||||
pub(crate) fn keys(&self) -> Vec<PropertyKey> { |
||||
self.keys_cloned_n(self.keys.len() as u32) |
||||
} |
||||
|
||||
/// Returns `n` cloned keys, in insertion order.
|
||||
pub(crate) fn keys_cloned_n(&self, n: u32) -> Vec<PropertyKey> { |
||||
let n = n as usize; |
||||
|
||||
self.keys |
||||
.iter() |
||||
.take(n) |
||||
.map(|(key, _)| key) |
||||
.filter(|key| matches!(key, PropertyKey::String(_))) |
||||
.chain( |
||||
self.keys |
||||
.iter() |
||||
.take(n) |
||||
.map(|(key, _)| key) |
||||
.filter(|key| matches!(key, PropertyKey::Symbol(_))), |
||||
) |
||||
.cloned() |
||||
.collect() |
||||
} |
||||
|
||||
/// Returns a new table with `n` cloned properties.
|
||||
pub(crate) fn clone_count(&self, n: u32) -> Self { |
||||
let n = n as usize; |
||||
|
||||
let mut keys = Vec::with_capacity(n); |
||||
let mut map = FxHashMap::default(); |
||||
|
||||
for (index, (key, slot)) in self.keys.iter().take(n).enumerate() { |
||||
keys.push((key.clone(), *slot)); |
||||
map.insert(key.clone(), (index as u32, *slot)); |
||||
} |
||||
|
||||
Self { map, keys } |
||||
} |
||||
|
||||
/// Insert a property entry into the table.
|
||||
pub(crate) fn insert(&mut self, key: PropertyKey, attributes: SlotAttributes) { |
||||
let slot = Slot::from_previous(self.keys.last().map(|x| x.1), attributes); |
||||
let index = self.keys.len() as u32; |
||||
self.keys.push((key.clone(), slot)); |
||||
let value = self.map.insert(key, (index, slot)); |
||||
debug_assert!(value.is_none()); |
||||
} |
||||
} |
||||
|
||||
/// Represents an ordered property table, that maps [`PropertyTable`] to [`Slot`].
|
||||
///
|
||||
/// This is shared between [`crate::object::shape::SharedShape`].
|
||||
#[derive(Default, Debug, Clone)] |
||||
pub(crate) struct PropertyTable { |
||||
pub(super) inner: Rc<RefCell<PropertyTableInner>>, |
||||
} |
||||
|
||||
impl PropertyTable { |
||||
/// Returns the inner representation of a [`PropertyTable`].
|
||||
pub(super) fn inner(&self) -> &RefCell<PropertyTableInner> { |
||||
&self.inner |
||||
} |
||||
|
||||
/// Add a property to the [`PropertyTable`] or deep clone it,
|
||||
/// if there already is a property or the property has attributes that are not the same.
|
||||
pub(crate) fn add_property_deep_clone_if_needed( |
||||
&self, |
||||
key: PropertyKey, |
||||
attributes: SlotAttributes, |
||||
property_count: u32, |
||||
) -> Self { |
||||
{ |
||||
let mut inner = self.inner.borrow_mut(); |
||||
if (property_count as usize) == inner.keys.len() && !inner.map.contains_key(&key) { |
||||
inner.insert(key, attributes); |
||||
return self.clone(); |
||||
} |
||||
} |
||||
|
||||
// property is already present need to make deep clone of property table.
|
||||
let this = self.deep_clone(property_count); |
||||
{ |
||||
let mut inner = this.inner.borrow_mut(); |
||||
inner.insert(key, attributes); |
||||
} |
||||
this |
||||
} |
||||
|
||||
/// Deep clone the [`PropertyTable`] in insertion order with the first n properties.
|
||||
pub(crate) fn deep_clone(&self, n: u32) -> Self { |
||||
Self { |
||||
inner: Rc::new(RefCell::new(self.inner.borrow().clone_count(n))), |
||||
} |
||||
} |
||||
|
||||
/// Deep clone the [`PropertyTable`].
|
||||
pub(crate) fn deep_clone_all(&self) -> Self { |
||||
Self { |
||||
inner: Rc::new(RefCell::new((*self.inner.borrow()).clone())), |
||||
} |
||||
} |
||||
|
||||
/// Change the attributes of a property.
|
||||
pub(crate) fn set_attributes_at_index( |
||||
&self, |
||||
key: &PropertyKey, |
||||
property_attributes: SlotAttributes, |
||||
) { |
||||
let mut inner = self.inner.borrow_mut(); |
||||
let Some((index, slot)) = inner.map.get_mut(key) else { |
||||
unreachable!("There should already be a property!") |
||||
}; |
||||
slot.attributes = property_attributes; |
||||
let index = *index as usize; |
||||
|
||||
inner.keys[index].1.attributes = property_attributes; |
||||
} |
||||
|
||||
/// Get a property from the [`PropertyTable`].
|
||||
///
|
||||
/// Panics:
|
||||
///
|
||||
/// If it is not in the [`PropertyTable`].
|
||||
pub(crate) fn get_expect(&self, key: &PropertyKey) -> Slot { |
||||
let inner = self.inner.borrow(); |
||||
let Some((_, slot)) = inner.map.get(key) else { |
||||
unreachable!("There should already be a property!") |
||||
}; |
||||
*slot |
||||
} |
||||
} |
@ -0,0 +1,58 @@
|
||||
use boa_gc::{Finalize, Gc, GcRefCell, Trace, WeakGc}; |
||||
use rustc_hash::FxHashMap; |
||||
|
||||
use crate::object::JsPrototype; |
||||
|
||||
use super::{Inner as SharedShapeInner, TransitionKey}; |
||||
|
||||
/// Maps transition key type to a [`SharedShapeInner`] transition.
|
||||
type TransitionMap<T> = FxHashMap<T, WeakGc<SharedShapeInner>>; |
||||
|
||||
/// The internal representation of [`ForwardTransition`].
|
||||
#[derive(Default, Debug, Trace, Finalize)] |
||||
struct Inner { |
||||
properties: Option<Box<TransitionMap<TransitionKey>>>, |
||||
prototypes: Option<Box<TransitionMap<JsPrototype>>>, |
||||
} |
||||
|
||||
/// 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<Inner>, |
||||
} |
||||
|
||||
impl ForwardTransition { |
||||
/// Insert a property transition.
|
||||
pub(super) fn insert_property(&self, key: TransitionKey, value: &Gc<SharedShapeInner>) { |
||||
let mut this = self.inner.borrow_mut(); |
||||
let properties = this.properties.get_or_insert_with(Box::default); |
||||
properties.insert(key, WeakGc::new(value)); |
||||
} |
||||
|
||||
/// Insert a prototype transition.
|
||||
pub(super) fn insert_prototype(&self, key: JsPrototype, value: &Gc<SharedShapeInner>) { |
||||
let mut this = self.inner.borrow_mut(); |
||||
let prototypes = this.prototypes.get_or_insert_with(Box::default); |
||||
prototypes.insert(key, WeakGc::new(value)); |
||||
} |
||||
|
||||
/// Get a property transition, return [`None`] otherwise.
|
||||
pub(super) fn get_property(&self, key: &TransitionKey) -> Option<WeakGc<SharedShapeInner>> { |
||||
let this = self.inner.borrow(); |
||||
let Some(transitions) = this.properties.as_ref() else { |
||||
return None; |
||||
}; |
||||
transitions.get(key).cloned() |
||||
} |
||||
|
||||
/// Get a prototype transition, return [`None`] otherwise.
|
||||
pub(super) fn get_prototype(&self, key: &JsPrototype) -> Option<WeakGc<SharedShapeInner>> { |
||||
let this = self.inner.borrow(); |
||||
let Some(transitions) = this.prototypes.as_ref() else { |
||||
return None; |
||||
}; |
||||
transitions.get(key).cloned() |
||||
} |
||||
} |
@ -0,0 +1,469 @@
|
||||
mod forward_transition; |
||||
pub(crate) mod template; |
||||
|
||||
use std::{collections::hash_map::RandomState, hash::Hash}; |
||||
|
||||
use bitflags::bitflags; |
||||
use boa_gc::{empty_trace, Finalize, Gc, Trace}; |
||||
use indexmap::IndexMap; |
||||
|
||||
use crate::{object::JsPrototype, property::PropertyKey, JsObject}; |
||||
|
||||
use self::forward_transition::ForwardTransition; |
||||
|
||||
use super::{ |
||||
property_table::PropertyTable, slot::SlotAttributes, ChangeTransition, ChangeTransitionAction, |
||||
Slot, UniqueShape, |
||||
}; |
||||
|
||||
/// Represent a [`SharedShape`] property transition.
|
||||
#[derive(Debug, Finalize, Clone, PartialEq, Eq, Hash)] |
||||
pub(crate) struct TransitionKey { |
||||
pub(crate) property_key: PropertyKey, |
||||
pub(crate) attributes: SlotAttributes, |
||||
} |
||||
|
||||
// SAFETY: Non of the member of this struct are garbage collected,
|
||||
// so this should be fine.
|
||||
unsafe impl Trace for TransitionKey { |
||||
empty_trace!(); |
||||
} |
||||
|
||||
const INSERT_PROPERTY_TRANSITION_TYPE: u8 = 0b0000_0000; |
||||
const CONFIGURE_PROPERTY_TRANSITION_TYPE: u8 = 0b0000_0001; |
||||
const PROTOTYPE_TRANSITION_TYPE: u8 = 0b0000_0010; |
||||
|
||||
// Reserved for future use!
|
||||
#[allow(unused)] |
||||
const RESEREVED_TRANSITION_TYPE: u8 = 0b0000_0011; |
||||
|
||||
bitflags! { |
||||
/// Flags of a shape.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Finalize)] |
||||
pub struct ShapeFlags: u8 { |
||||
/// Represents the transition type of a [`SharedShape`].
|
||||
const TRANSITION_TYPE = 0b0000_0011; |
||||
} |
||||
} |
||||
|
||||
impl Default for ShapeFlags { |
||||
fn default() -> Self { |
||||
Self::empty() |
||||
} |
||||
} |
||||
|
||||
impl ShapeFlags { |
||||
// NOTE: Remove type bits and set the new ones.
|
||||
fn insert_property_transition_from(previous: ShapeFlags) -> Self { |
||||
previous.difference(Self::TRANSITION_TYPE) |
||||
| Self::from_bits_retain(INSERT_PROPERTY_TRANSITION_TYPE) |
||||
} |
||||
fn configure_property_transition_from(previous: ShapeFlags) -> Self { |
||||
previous.difference(Self::TRANSITION_TYPE) |
||||
| Self::from_bits_retain(CONFIGURE_PROPERTY_TRANSITION_TYPE) |
||||
} |
||||
fn prototype_transition_from(previous: ShapeFlags) -> Self { |
||||
previous.difference(Self::TRANSITION_TYPE) |
||||
| Self::from_bits_retain(PROTOTYPE_TRANSITION_TYPE) |
||||
} |
||||
|
||||
const fn is_insert_transition_type(self) -> bool { |
||||
self.intersection(Self::TRANSITION_TYPE).bits() == INSERT_PROPERTY_TRANSITION_TYPE |
||||
} |
||||
const fn is_prototype_transition_type(self) -> bool { |
||||
self.intersection(Self::TRANSITION_TYPE).bits() == PROTOTYPE_TRANSITION_TYPE |
||||
} |
||||
} |
||||
|
||||
// SAFETY: Non of the member of this struct are garbage collected,
|
||||
// so this should be fine.
|
||||
unsafe impl Trace for ShapeFlags { |
||||
empty_trace!(); |
||||
} |
||||
|
||||
/// The internal representation of a [`SharedShape`].
|
||||
#[derive(Debug, Trace, Finalize)] |
||||
struct Inner { |
||||
/// See [`ForwardTransition`].
|
||||
forward_transitions: ForwardTransition, |
||||
|
||||
/// The count of how many properties this [`SharedShape`] holds.
|
||||
property_count: u32, |
||||
|
||||
/// Instance prototype `__proto__`.
|
||||
prototype: JsPrototype, |
||||
|
||||
// SAFETY: This is safe because nothing in [`PropertyTable`]
|
||||
// needs tracing
|
||||
#[unsafe_ignore_trace] |
||||
property_table: PropertyTable, |
||||
|
||||
/// The previous shape in the transition chain.
|
||||
///
|
||||
/// [`None`] if it is the root shape.
|
||||
previous: Option<SharedShape>, |
||||
|
||||
/// How many transitions have happened from the root node.
|
||||
transition_count: u16, |
||||
|
||||
/// Flags about the shape.
|
||||
flags: ShapeFlags, |
||||
} |
||||
|
||||
/// Represents a shared object shape.
|
||||
#[derive(Debug, Trace, Finalize, Clone)] |
||||
pub struct SharedShape { |
||||
inner: Gc<Inner>, |
||||
} |
||||
|
||||
impl SharedShape { |
||||
fn property_table(&self) -> &PropertyTable { |
||||
&self.inner.property_table |
||||
} |
||||
/// 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) |
||||
} |
||||
/// Getter for the transition count field.
|
||||
pub fn transition_count(&self) -> u16 { |
||||
self.inner.transition_count |
||||
} |
||||
/// Getter for the previous field.
|
||||
pub fn previous(&self) -> Option<&SharedShape> { |
||||
self.inner.previous.as_ref() |
||||
} |
||||
/// Get the prototype of the shape.
|
||||
pub fn prototype(&self) -> JsPrototype { |
||||
self.inner.prototype.clone() |
||||
} |
||||
/// Get the property this [`SharedShape`] refers to.
|
||||
pub(crate) fn property(&self) -> (PropertyKey, Slot) { |
||||
let inner = self.property_table().inner().borrow(); |
||||
let (key, slot) = inner |
||||
.keys |
||||
.get(self.property_index() as usize) |
||||
.expect("There should be a property"); |
||||
(key.clone(), *slot) |
||||
} |
||||
/// Get the flags of the shape.
|
||||
fn flags(&self) -> ShapeFlags { |
||||
self.inner.flags |
||||
} |
||||
/// Getter for the [`ForwardTransition`] field.
|
||||
fn forward_transitions(&self) -> &ForwardTransition { |
||||
&self.inner.forward_transitions |
||||
} |
||||
/// Check if the shape has the given prototype.
|
||||
pub fn has_prototype(&self, prototype: &JsObject) -> bool { |
||||
self.inner |
||||
.prototype |
||||
.as_ref() |
||||
.map_or(false, |this| this == prototype) |
||||
} |
||||
|
||||
/// Create a new [`SharedShape`].
|
||||
fn new(inner: Inner) -> Self { |
||||
Self { |
||||
inner: Gc::new(inner), |
||||
} |
||||
} |
||||
|
||||
/// Create a root [`SharedShape`].
|
||||
#[must_use] |
||||
pub fn root() -> Self { |
||||
Self::new(Inner { |
||||
forward_transitions: ForwardTransition::default(), |
||||
prototype: None, |
||||
property_count: 0, |
||||
property_table: PropertyTable::default(), |
||||
previous: None, |
||||
flags: ShapeFlags::default(), |
||||
transition_count: 0, |
||||
}) |
||||
} |
||||
|
||||
/// Create a [`SharedShape`] change prototype transition.
|
||||
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() { |
||||
return Self { inner }; |
||||
} |
||||
} |
||||
let new_inner_shape = Inner { |
||||
forward_transitions: ForwardTransition::default(), |
||||
prototype: prototype.clone(), |
||||
property_table: self.property_table().clone(), |
||||
property_count: self.property_count(), |
||||
previous: Some(self.clone()), |
||||
transition_count: self.transition_count() + 1, |
||||
flags: ShapeFlags::prototype_transition_from(self.flags()), |
||||
}; |
||||
let new_shape = Self::new(new_inner_shape); |
||||
|
||||
self.forward_transitions() |
||||
.insert_prototype(prototype, &new_shape.inner); |
||||
|
||||
new_shape |
||||
} |
||||
|
||||
/// Create a [`SharedShape`] insert property transition.
|
||||
pub(crate) fn insert_property_transition(&self, key: TransitionKey) -> Self { |
||||
// Check if we have already created such a transition, if so use it!
|
||||
if let Some(shape) = self.forward_transitions().get_property(&key) { |
||||
if let Some(inner) = shape.upgrade() { |
||||
return Self { inner }; |
||||
} |
||||
} |
||||
|
||||
let property_table = self.property_table().add_property_deep_clone_if_needed( |
||||
key.property_key.clone(), |
||||
key.attributes, |
||||
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_count: self.transition_count() + 1, |
||||
flags: ShapeFlags::insert_property_transition_from(self.flags()), |
||||
}; |
||||
let new_shape = Self::new(new_inner_shape); |
||||
|
||||
self.forward_transitions() |
||||
.insert_property(key, &new_shape.inner); |
||||
|
||||
new_shape |
||||
} |
||||
|
||||
/// Create a [`SharedShape`] change prototype transition, returning [`ChangeTransition`].
|
||||
pub(crate) fn change_attributes_transition( |
||||
&self, |
||||
key: TransitionKey, |
||||
) -> ChangeTransition<SharedShape> { |
||||
let slot = self.property_table().get_expect(&key.property_key); |
||||
|
||||
// Check if we have already created such a transition, if so use it!
|
||||
if let Some(shape) = self.forward_transitions().get_property(&key) { |
||||
if let Some(inner) = shape.upgrade() { |
||||
let action = if slot.attributes.width_match(key.attributes) { |
||||
ChangeTransitionAction::Nothing |
||||
} else if slot.attributes.is_accessor_descriptor() { |
||||
// Accessor property --> Data property
|
||||
ChangeTransitionAction::Remove |
||||
} else { |
||||
// Data property --> Accessor property
|
||||
ChangeTransitionAction::Insert |
||||
}; |
||||
|
||||
return ChangeTransition { |
||||
shape: Self { inner }, |
||||
action, |
||||
}; |
||||
} |
||||
} |
||||
|
||||
// The attribute change transitions, didn't change from accessor to data property or vice-versa.
|
||||
if slot.attributes.width_match(key.attributes) { |
||||
let property_table = self.property_table().deep_clone_all(); |
||||
property_table.set_attributes_at_index(&key.property_key, key.attributes); |
||||
let inner_shape = Inner { |
||||
forward_transitions: ForwardTransition::default(), |
||||
prototype: self.prototype(), |
||||
property_table, |
||||
property_count: self.property_count(), |
||||
previous: Some(self.clone()), |
||||
transition_count: self.transition_count() + 1, |
||||
flags: ShapeFlags::configure_property_transition_from(self.flags()), |
||||
}; |
||||
let shape = Self::new(inner_shape); |
||||
|
||||
self.forward_transitions() |
||||
.insert_property(key, &shape.inner); |
||||
|
||||
return ChangeTransition { |
||||
shape, |
||||
action: ChangeTransitionAction::Nothing, |
||||
}; |
||||
} |
||||
|
||||
// Rollback before the property has added.
|
||||
let (mut base, prototype, transitions) = self.rollback_before(&key.property_key); |
||||
|
||||
// Apply prototype transition, if it was found.
|
||||
if let Some(prototype) = prototype { |
||||
base = base.change_prototype_transition(prototype); |
||||
} |
||||
|
||||
// Apply this property.
|
||||
base = base.insert_property_transition(key); |
||||
|
||||
// Apply previous properties.
|
||||
for (property_key, attributes) in transitions.into_iter().rev() { |
||||
let transition = TransitionKey { |
||||
property_key, |
||||
attributes, |
||||
}; |
||||
base = base.insert_property_transition(transition); |
||||
} |
||||
|
||||
// Determine action to be performed on the storage.
|
||||
let action = if slot.attributes.is_accessor_descriptor() { |
||||
// Accessor property --> Data property
|
||||
ChangeTransitionAction::Remove |
||||
} else { |
||||
// Data property --> Accessor property
|
||||
ChangeTransitionAction::Insert |
||||
}; |
||||
|
||||
ChangeTransition { |
||||
shape: base, |
||||
action, |
||||
} |
||||
} |
||||
|
||||
/// Rollback to shape before the insertion of the [`PropertyKey`] that is provided.
|
||||
///
|
||||
/// This returns the shape before the insertion, if it sees a prototype transition it will return the lastest one,
|
||||
/// ignoring any others, [`None`] otherwise. It also will return the property transitions ordered from
|
||||
/// latest to oldest that it sees.
|
||||
///
|
||||
/// NOTE: In the transitions it does not include the property that we are rolling back.
|
||||
///
|
||||
/// NOTE: The prototype transitions if it sees a property insert and then later an attribute change it will condense
|
||||
/// into one property insert transition with the new attribute in the change attribute transition,
|
||||
/// in the same place that the property was inserted initially.
|
||||
//
|
||||
// For example with the following chain:
|
||||
//
|
||||
// INSERT(x) INSERT(y) INSERT(z)
|
||||
// { } ------------> { x } ------------> { x, y } ------------> { x, y, z }
|
||||
//
|
||||
// Then we call rollback on `y`:
|
||||
//
|
||||
// INSERT(x) INSERT(y) INSERT(z)
|
||||
// { } ------------> { x } ------------> { x, y } ------------> { x, y, z }
|
||||
// ^
|
||||
// \--- base (with array of transitions to be performed: INSERT(z),
|
||||
// and protortype: None )
|
||||
fn rollback_before( |
||||
&self, |
||||
key: &PropertyKey, |
||||
) -> ( |
||||
SharedShape, |
||||
Option<JsPrototype>, |
||||
IndexMap<PropertyKey, SlotAttributes>, |
||||
) { |
||||
let mut prototype = None; |
||||
let mut transitions: IndexMap<PropertyKey, SlotAttributes, RandomState> = |
||||
IndexMap::default(); |
||||
|
||||
let mut current = Some(self); |
||||
let 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.flags().is_prototype_transition_type() { |
||||
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.flags().is_insert_transition_type() && ¤t_property_key == key { |
||||
let 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(); |
||||
}; |
||||
|
||||
(base, prototype, transitions) |
||||
} |
||||
|
||||
/// Remove a property from [`SharedShape`], returning the new [`SharedShape`].
|
||||
pub(crate) fn remove_property_transition(&self, key: &PropertyKey) -> Self { |
||||
let (mut base, prototype, transitions) = self.rollback_before(key); |
||||
|
||||
// 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 |
||||
} |
||||
|
||||
/// Do a property lookup, returns [`None`] if property not found.
|
||||
pub(crate) fn lookup(&self, key: &PropertyKey) -> Option<Slot> { |
||||
let property_count = self.property_count(); |
||||
if property_count == 0 { |
||||
return None; |
||||
} |
||||
|
||||
let property_table_inner = self.property_table().inner().borrow(); |
||||
if let Some((property_table_index, slot)) = property_table_inner.map.get(key) { |
||||
// Check if we are trying to access properties that belong to another shape.
|
||||
if *property_table_index < self.property_count() { |
||||
return Some(*slot); |
||||
} |
||||
} |
||||
None |
||||
} |
||||
|
||||
/// Gets all keys first strings then symbols in creation order.
|
||||
pub(crate) fn keys(&self) -> Vec<PropertyKey> { |
||||
let property_table = self.property_table().inner().borrow(); |
||||
property_table.keys_cloned_n(self.property_count()) |
||||
} |
||||
|
||||
/// Returns a new [`UniqueShape`] with the properties of the [`SharedShape`].
|
||||
pub(crate) fn to_unique(&self) -> UniqueShape { |
||||
UniqueShape::new( |
||||
self.prototype(), |
||||
self.property_table() |
||||
.inner() |
||||
.borrow() |
||||
.clone_count(self.property_count()), |
||||
) |
||||
} |
||||
|
||||
/// Return location in memory of the [`UniqueShape`].
|
||||
pub(crate) fn to_addr_usize(&self) -> usize { |
||||
let ptr: *const _ = self.inner.as_ref(); |
||||
ptr as usize |
||||
} |
||||
} |
@ -0,0 +1,141 @@
|
||||
use boa_gc::{Finalize, Trace}; |
||||
use thin_vec::ThinVec; |
||||
|
||||
use crate::{ |
||||
object::{ |
||||
shape::{slot::SlotAttributes, Shape}, |
||||
JsObject, Object, ObjectData, PropertyMap, |
||||
}, |
||||
property::{Attribute, PropertyKey}, |
||||
JsValue, |
||||
}; |
||||
|
||||
use super::{SharedShape, TransitionKey}; |
||||
|
||||
/// Represent a template of an objects properties and prototype.
|
||||
/// This is used to construct as many objects as needed from a predefined [`SharedShape`].
|
||||
#[derive(Debug, Clone, Trace, Finalize)] |
||||
pub(crate) struct ObjectTemplate { |
||||
shape: SharedShape, |
||||
} |
||||
|
||||
impl ObjectTemplate { |
||||
/// Create a new [`ObjectTemplate`]
|
||||
pub(crate) fn new(root_shape: &SharedShape) -> Self { |
||||
Self { |
||||
shape: root_shape.clone(), |
||||
} |
||||
} |
||||
|
||||
/// Create and [`ObjectTemplate`] with a prototype.
|
||||
pub(crate) fn with_prototype(root_shape: &SharedShape, prototype: JsObject) -> Self { |
||||
let shape = root_shape.change_prototype_transition(Some(prototype)); |
||||
Self { shape } |
||||
} |
||||
|
||||
/// Check if the shape has a specific, prototype.
|
||||
pub(crate) fn has_prototype(&self, prototype: &JsObject) -> bool { |
||||
self.shape.has_prototype(prototype) |
||||
} |
||||
|
||||
/// Set the prototype of the [`ObjectTemplate`].
|
||||
///
|
||||
/// This assumes that the prototype has not been set yet.
|
||||
pub(crate) fn set_prototype(&mut self, prototype: JsObject) -> &mut Self { |
||||
self.shape = self.shape.change_prototype_transition(Some(prototype)); |
||||
self |
||||
} |
||||
|
||||
/// Add a data property to the [`ObjectTemplate`].
|
||||
///
|
||||
/// This assumes that the property with the given key was not previously set
|
||||
/// and that it's a string or symbol.
|
||||
pub(crate) fn property(&mut self, key: PropertyKey, attributes: Attribute) -> &mut Self { |
||||
debug_assert!(!matches!(&key, PropertyKey::Index(_))); |
||||
|
||||
let attributes = SlotAttributes::from_bits_truncate(attributes.bits()); |
||||
self.shape = self.shape.insert_property_transition(TransitionKey { |
||||
property_key: key, |
||||
attributes, |
||||
}); |
||||
|
||||
self |
||||
} |
||||
|
||||
/// Add a accessor property to the [`ObjectTemplate`].
|
||||
///
|
||||
/// This assumes that the property with the given key was not previously set
|
||||
/// and that it's a string or symbol.
|
||||
pub(crate) fn accessor( |
||||
&mut self, |
||||
key: PropertyKey, |
||||
get: bool, |
||||
set: bool, |
||||
attributes: Attribute, |
||||
) -> &mut Self { |
||||
// TOOD: We don't support indexed keys.
|
||||
debug_assert!(!matches!(&key, PropertyKey::Index(_))); |
||||
|
||||
let attributes = { |
||||
let mut result = SlotAttributes::empty(); |
||||
result.set( |
||||
SlotAttributes::CONFIGURABLE, |
||||
attributes.contains(Attribute::CONFIGURABLE), |
||||
); |
||||
result.set( |
||||
SlotAttributes::ENUMERABLE, |
||||
attributes.contains(Attribute::ENUMERABLE), |
||||
); |
||||
|
||||
result.set(SlotAttributes::GET, get); |
||||
result.set(SlotAttributes::SET, set); |
||||
|
||||
result |
||||
}; |
||||
|
||||
self.shape = self.shape.insert_property_transition(TransitionKey { |
||||
property_key: key, |
||||
attributes, |
||||
}); |
||||
|
||||
self |
||||
} |
||||
|
||||
/// Create an object from the [`ObjectTemplate`]
|
||||
///
|
||||
/// The storage must match the properties provided.
|
||||
pub(crate) fn create(&self, data: ObjectData, storage: Vec<JsValue>) -> JsObject { |
||||
let mut object = Object { |
||||
kind: data.kind, |
||||
extensible: true, |
||||
properties: PropertyMap::new(Shape::shared(self.shape.clone()), ThinVec::default()), |
||||
private_elements: ThinVec::new(), |
||||
}; |
||||
|
||||
object.properties.storage = storage; |
||||
|
||||
JsObject::from_object_and_vtable(object, data.internal_methods) |
||||
} |
||||
|
||||
/// Create an object from the [`ObjectTemplate`]
|
||||
///
|
||||
/// The storage must match the properties provided. It does not apply to
|
||||
/// the indexed propeties.
|
||||
pub(crate) fn create_with_indexed_properties( |
||||
&self, |
||||
data: ObjectData, |
||||
storage: Vec<JsValue>, |
||||
elements: ThinVec<JsValue>, |
||||
) -> JsObject { |
||||
let mut object = Object { |
||||
kind: data.kind, |
||||
extensible: true, |
||||
properties: PropertyMap::new(Shape::shared(self.shape.clone()), elements), |
||||
private_elements: ThinVec::new(), |
||||
}; |
||||
|
||||
object.properties.storage = storage; |
||||
|
||||
JsObject::from_object_and_vtable(object, data.internal_methods) |
||||
} |
||||
} |
@ -0,0 +1,77 @@
|
||||
use bitflags::bitflags; |
||||
pub(crate) type SlotIndex = u32; |
||||
|
||||
bitflags! { |
||||
/// Attributes of a slot.
|
||||
#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Hash)] |
||||
pub(crate) struct SlotAttributes: u8 { |
||||
const WRITABLE = 0b0000_0001; |
||||
const ENUMERABLE = 0b0000_0010; |
||||
const CONFIGURABLE = 0b0000_0100; |
||||
const GET = 0b0000_1000; |
||||
const SET = 0b0001_0000; |
||||
} |
||||
} |
||||
|
||||
impl SlotAttributes { |
||||
pub(crate) const fn is_accessor_descriptor(self) -> bool { |
||||
self.contains(Self::GET) || self.contains(Self::SET) |
||||
} |
||||
|
||||
pub(crate) const fn has_get(self) -> bool { |
||||
self.contains(Self::GET) |
||||
} |
||||
pub(crate) const fn has_set(self) -> bool { |
||||
self.contains(Self::SET) |
||||
} |
||||
|
||||
/// Check if slot type width matches, this can only happens,
|
||||
/// if they are both accessors, or both data properties.
|
||||
pub(crate) const fn width_match(self, other: Self) -> bool { |
||||
self.is_accessor_descriptor() == other.is_accessor_descriptor() |
||||
} |
||||
|
||||
/// Get the width of the slot.
|
||||
pub(crate) fn width(self) -> u32 { |
||||
// accessor take 2 positions in the storage to accomodate for the `get` and `set` fields.
|
||||
1 + u32::from(self.is_accessor_descriptor()) |
||||
} |
||||
} |
||||
|
||||
/// Represents an [`u32`] index and it's slot attributes of an element in a object storage.
|
||||
///
|
||||
/// Slots can have different width depending on its attributes, accessors properties have width `2`,
|
||||
/// while data properties have width `1`.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)] |
||||
pub(crate) struct Slot { |
||||
pub(crate) index: SlotIndex, |
||||
pub(crate) attributes: SlotAttributes, |
||||
} |
||||
|
||||
impl Slot { |
||||
/// Get the width of the slot.
|
||||
pub(crate) fn width(self) -> u32 { |
||||
self.attributes.width() |
||||
} |
||||
|
||||
/// Calculate next slot from previous one.
|
||||
///
|
||||
/// This is needed because slots do not have the same width.
|
||||
pub(crate) fn from_previous( |
||||
previous_slot: Option<Slot>, |
||||
new_attributes: SlotAttributes, |
||||
) -> Self { |
||||
// If there was no previous slot then return 0 as the index.
|
||||
let Some(previous_slot) = previous_slot else { |
||||
return Self { |
||||
index: 0, |
||||
attributes: new_attributes, |
||||
} |
||||
}; |
||||
|
||||
Self { |
||||
index: previous_slot.index + previous_slot.width(), |
||||
attributes: new_attributes, |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,241 @@
|
||||
use std::{cell::RefCell, fmt::Debug}; |
||||
|
||||
use boa_gc::{Finalize, Gc, GcRefCell, Trace}; |
||||
|
||||
use crate::property::PropertyKey; |
||||
|
||||
use super::{ |
||||
property_table::PropertyTableInner, shared_shape::TransitionKey, ChangeTransition, |
||||
ChangeTransitionAction, JsPrototype, Shape, Slot, |
||||
}; |
||||
|
||||
/// The internal representation of [`UniqueShape`].
|
||||
#[derive(Default, Debug, Trace, Finalize)] |
||||
struct Inner { |
||||
/// The property table that maps a [`PropertyKey`] to a slot in the objects storage.
|
||||
//
|
||||
// SAFETY: This is safe becasue nothing in this field needs tracing.
|
||||
#[unsafe_ignore_trace] |
||||
property_table: RefCell<PropertyTableInner>, |
||||
|
||||
/// The prototype of the shape.
|
||||
prototype: GcRefCell<JsPrototype>, |
||||
} |
||||
|
||||
/// Represents a [`Shape`] that is not shared with any other object.
|
||||
///
|
||||
/// This is useful for objects that are inherently unique like,
|
||||
/// the builtin object.
|
||||
///
|
||||
/// Cloning this does a shallow clone.
|
||||
#[derive(Default, Debug, Clone, Trace, Finalize)] |
||||
pub(crate) struct UniqueShape { |
||||
inner: Gc<Inner>, |
||||
} |
||||
|
||||
impl UniqueShape { |
||||
/// Create a new [`UniqueShape`].
|
||||
pub(crate) fn new(prototype: JsPrototype, property_table: PropertyTableInner) -> Self { |
||||
Self { |
||||
inner: Gc::new(Inner { |
||||
property_table: RefCell::new(property_table), |
||||
prototype: GcRefCell::new(prototype), |
||||
}), |
||||
} |
||||
} |
||||
|
||||
pub(crate) fn override_internal( |
||||
&self, |
||||
property_table: PropertyTableInner, |
||||
prototype: JsPrototype, |
||||
) { |
||||
*self.inner.property_table.borrow_mut() = property_table; |
||||
*self.inner.prototype.borrow_mut() = prototype; |
||||
} |
||||
|
||||
/// Get the prototype of the [`UniqueShape`].
|
||||
pub(crate) fn prototype(&self) -> JsPrototype { |
||||
self.inner.prototype.borrow().clone() |
||||
} |
||||
|
||||
/// Get the property table of the [`UniqueShape`].
|
||||
pub(crate) fn property_table(&self) -> &RefCell<PropertyTableInner> { |
||||
&self.inner.property_table |
||||
} |
||||
|
||||
/// Inserts a new property into the [`UniqueShape`].
|
||||
pub(crate) fn insert_property_transition(&self, key: TransitionKey) -> Self { |
||||
let mut property_table = self.property_table().borrow_mut(); |
||||
property_table.insert(key.property_key, key.attributes); |
||||
self.clone() |
||||
} |
||||
|
||||
/// Remove a property from the [`UniqueShape`].
|
||||
///
|
||||
/// This will cause the current shape to be invalidated, and a new [`UniqueShape`] will be returned.
|
||||
pub(crate) fn remove_property_transition(&self, key: &PropertyKey) -> Self { |
||||
let mut property_table = self.property_table().borrow_mut(); |
||||
let Some((index, _attributes)) = property_table.map.remove(key) else { |
||||
return self.clone(); |
||||
}; |
||||
|
||||
let index = index as usize; |
||||
|
||||
// shift elements
|
||||
property_table.keys.remove(index); |
||||
|
||||
// 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 mut property_table = std::mem::take(&mut *property_table); |
||||
|
||||
// If it is not the last property that was deleted,
|
||||
// then update all the property slots that are after it.
|
||||
if index != property_table.keys.len() { |
||||
// Get the previous value before the value at index,
|
||||
//
|
||||
// NOTE: calling wrapping_sub when usize index is 0 will wrap into usize::MAX
|
||||
// which will return None, avoiding unneeded checks.
|
||||
let mut previous_slot = property_table.keys.get(index.wrapping_sub(1)).map(|x| x.1); |
||||
|
||||
// Update all slot positions
|
||||
for (index, (key, slot)) in property_table.keys.iter_mut().enumerate().skip(index) { |
||||
*slot = Slot::from_previous(previous_slot, slot.attributes); |
||||
|
||||
let Some((map_index, map_slot)) = property_table.map.get_mut(key) else { |
||||
unreachable!("There should already be a property") |
||||
}; |
||||
*map_index = index as u32; |
||||
*map_slot = *slot; |
||||
|
||||
previous_slot = Some(*slot); |
||||
} |
||||
} |
||||
|
||||
let prototype = self.inner.prototype.borrow_mut().take(); |
||||
Self::new(prototype, property_table) |
||||
} |
||||
|
||||
/// Does a property lookup on the [`UniqueShape`] returning the [`Slot`] where it's
|
||||
/// located or [`None`] otherwise.
|
||||
pub(crate) fn lookup(&self, key: &PropertyKey) -> Option<Slot> { |
||||
let property_table = self.property_table().borrow(); |
||||
if let Some((_, slot)) = property_table.map.get(key) { |
||||
return Some(*slot); |
||||
} |
||||
|
||||
None |
||||
} |
||||
|
||||
/// Change the attributes of a property from the [`UniqueShape`].
|
||||
///
|
||||
/// This will cause the current shape to be invalidated, and a new [`UniqueShape`] will be returned.
|
||||
///
|
||||
/// NOTE: This assumes that the property had already been inserted.
|
||||
pub(crate) fn change_attributes_transition( |
||||
&self, |
||||
key: &TransitionKey, |
||||
) -> ChangeTransition<Shape> { |
||||
let mut property_table = self.property_table().borrow_mut(); |
||||
let Some((index, slot)) = property_table.map.get_mut(&key.property_key) else { |
||||
unreachable!("Attribute change can only happen on existing property") |
||||
}; |
||||
|
||||
let index = *index as usize; |
||||
|
||||
// If property does not change type, there is no need to shift.
|
||||
if slot.attributes.width_match(key.attributes) { |
||||
slot.attributes = key.attributes; |
||||
property_table.keys[index].1.attributes = key.attributes; |
||||
// TODO: invalidate the pointer.
|
||||
return ChangeTransition { |
||||
shape: Shape::unique(self.clone()), |
||||
action: ChangeTransitionAction::Nothing, |
||||
}; |
||||
} |
||||
slot.attributes = key.attributes; |
||||
|
||||
let mut previous_slot = *slot; |
||||
|
||||
property_table.keys[index].1.attributes = key.attributes; |
||||
|
||||
let action = if key.attributes.is_accessor_descriptor() { |
||||
// Data --> Accessor
|
||||
ChangeTransitionAction::Insert |
||||
} else { |
||||
// Accessor --> Data
|
||||
ChangeTransitionAction::Remove |
||||
}; |
||||
|
||||
// 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 mut property_table = std::mem::take(&mut *property_table); |
||||
|
||||
// Update all slot positions, after the target property.
|
||||
//
|
||||
// Example: Change 2nd one from accessor to data
|
||||
//
|
||||
// | Idx: 0, DATA | Idx: 1, ACCESSOR | Idx: 3, DATA |
|
||||
// \----- target
|
||||
//
|
||||
// Change attributes of target (given trough arguments).
|
||||
//
|
||||
// | Idx: 0, DATA | Idx: 1, DATA | Idx: 3, DATA |
|
||||
// \----- target
|
||||
//
|
||||
// The target becomes the previous pointer and next is target_index + 1,
|
||||
// continue until we reach the end.
|
||||
//
|
||||
// | Idx: 0, DATA | Idx: 1, DATA | Idx: 3, DATA |
|
||||
// previous ----/ \-------- next
|
||||
//
|
||||
// When next is out of range we quit, everything has been set.
|
||||
//
|
||||
// | Idx: 0, DATA | Idx: 1, DATA | Idx: 2, DATA | EMPTY |
|
||||
// previous ----/ \-------- next
|
||||
//
|
||||
let next = index + 1; |
||||
for (key, slot) in property_table.keys.iter_mut().skip(next) { |
||||
*slot = Slot::from_previous(Some(previous_slot), slot.attributes); |
||||
|
||||
let Some((_, map_slot)) = property_table.map.get_mut(key) else { |
||||
unreachable!("There should already be a property") |
||||
}; |
||||
*map_slot = *slot; |
||||
|
||||
previous_slot = *slot; |
||||
} |
||||
|
||||
let prototype = self.inner.prototype.borrow_mut().take(); |
||||
let shape = Self::new(prototype, property_table); |
||||
|
||||
ChangeTransition { |
||||
shape: Shape::unique(shape), |
||||
action, |
||||
} |
||||
} |
||||
|
||||
/// Change the prototype of the [`UniqueShape`].
|
||||
///
|
||||
/// This will cause the current shape to be invalidated, and a new [`UniqueShape`] will be returned.
|
||||
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(prototype, property_table) |
||||
} |
||||
|
||||
/// Gets all keys first strings then symbols in creation order.
|
||||
pub(crate) fn keys(&self) -> Vec<PropertyKey> { |
||||
self.property_table().borrow().keys() |
||||
} |
||||
|
||||
/// Return location in memory of the [`UniqueShape`].
|
||||
pub(crate) fn to_addr_usize(&self) -> usize { |
||||
let ptr: *const _ = self.inner.as_ref(); |
||||
ptr as usize |
||||
} |
||||
} |
@ -0,0 +1,220 @@
|
||||
# Shapes (Hidden Classes) |
||||
|
||||
The best way to explain object shapes is through examples. It all begins with the root shape. |
||||
|
||||
```mermaid |
||||
flowchart LR |
||||
classDef New style id2 fill:#bbf,stroke:#f66,stroke-width:2px,color:#fff,stroke-dasharray: 5 5 |
||||
style Root stroke:#000,stroke-width:5px |
||||
style PropertyTable fill:#071E22 |
||||
|
||||
Root(<b>Root Shape</b>\n<b>Prototype:</i> <i>None</i>) -->| Property Count 0 | PropertyTable(PropertyTable\n) |
||||
``` |
||||
|
||||
The root shape is where the transition chain starts from, it has a pointer to a `PropertyTable`, |
||||
we will explain what it is and does later on! |
||||
|
||||
**NOTE:** We will annotate the shapes with `S` followed by a number. |
||||
|
||||
If we have an example of JavaScript code like: |
||||
|
||||
```js |
||||
let o = {}; |
||||
``` |
||||
|
||||
The following chain is created: |
||||
|
||||
```mermaid |
||||
flowchart LR |
||||
classDef New style id2 fill:#bbf,stroke:#f66,stroke-width:2px,color:#fff,stroke-dasharray: 5 5 |
||||
style Root stroke:#000,stroke-width:5px |
||||
style PropertyTable fill:#071E22 |
||||
|
||||
Root(<b>S0: Root Shape</b>\n<b>Prototype:</b> <i>None</i>) -->|Property Count: 0| PropertyTable(PropertyTable\n) |
||||
|
||||
ObjectPrototype(<b>S1: Prototype Shape</b>\n<b>Prototype:</b> Object.prototype) -->|Property Count: 0|PropertyTable |
||||
ObjectPrototype:::New -->|Previous| Root |
||||
``` |
||||
|
||||
We transition, the object `o` has `S1` shape. The root shape does not have a prototype. So we transition into a shape that has the |
||||
`Object.prototype` as `__proto__`. We can see that the shapes inherited the `PropertyTable` from the `root`. |
||||
|
||||
Ok, Let us add a property `x`: |
||||
|
||||
```js |
||||
o.x = 100; // The value is not important! |
||||
``` |
||||
|
||||
Then this happens: |
||||
|
||||
```mermaid |
||||
flowchart LR |
||||
classDef New style id2 fill:#bbf,stroke:#f66,stroke-width:2px,color:#fff,stroke-dasharray: 5 5 |
||||
style Root stroke:#000,stroke-width:5px |
||||
style PropertyTable fill:#071E22 |
||||
|
||||
Root(<b>S0: Root Shape</b>\nPrototype: <i>None</i>) -->|Property Count: 0| PropertyTable(<b>PropertyTable</b>\nx: Slot 0, writable, configurable, enumerable) |
||||
|
||||
ObjectPrototype(<b>S1: Prototype Shape</b>\n<b>Prototype:</b> <i>Object.prototype</i>) -->|Property Count: 0|PropertyTable |
||||
ObjectPrototype -->|Previous| Root |
||||
|
||||
InsertX(<b>S2: Insert Shape</b>\n<b>Property:</b> '<i>x</i>') --> |Property Count: 1|PropertyTable |
||||
InsertX:::New -->|Previous| ObjectPrototype |
||||
``` |
||||
|
||||
The object `o` has shape `S2` shape now, we can see that it also inherited the `PropertyTable`, but it's property count is `1` and |
||||
an entry has been added into the `PropertyTable`. |
||||
|
||||
We can see that the property added is `writable`, `configurable`, and `enumerable`, but we also see `Slot 0`, |
||||
this is the index into the dense storage in the object itself. |
||||
|
||||
Here is how it would look with the `o` object: |
||||
|
||||
```mermaid |
||||
flowchart LR |
||||
classDef New style id2 fill:#bbf,stroke:#f66,stroke-width:2px,color:#fff,stroke-dasharray: 5 5 |
||||
style Root stroke:#000,stroke-width:5px |
||||
style PropertyTable fill:#071E22 |
||||
|
||||
Root(<b>S0: Root Shape</b>\n<b>Prototype:</b> <i>None</i>) -->| Property Count: 0 | PropertyTable(<b>PropertyTable</b>\nx: Slot 0, writable, configurable, enumerable) |
||||
|
||||
ObjectPrototype(<b>S1: Prototype Shape</b>\n<b>Prototype:</b> <i>Object.prototype</i>) -->| Property Count: 0 |PropertyTable |
||||
ObjectPrototype -->|Previous| Root |
||||
|
||||
InsertX(<b>S2: Insert Shape</b>\n<b>Property:</b> '<i>x</i>') --> | Property Count: 1 | PropertyTable |
||||
InsertX -->|Previous| ObjectPrototype |
||||
|
||||
O(<b>Object o</b>\n<b>Element 0:</b> JsValue: <i>100</i>) |
||||
O:::New --> InsertX |
||||
``` |
||||
|
||||
Let's define a getter and setter `y` |
||||
|
||||
```js |
||||
// What the getter/setter are not important! |
||||
Object.defineProperty(o, "y", { |
||||
enumerable: true, |
||||
configurable: true, |
||||
get: function () { |
||||
return this.x; |
||||
}, |
||||
set: function (value) { |
||||
this.x = value; |
||||
}, |
||||
}); |
||||
``` |
||||
|
||||
```mermaid |
||||
flowchart LR |
||||
classDef New style id2 fill:#bbf,stroke:#f66,stroke-width:2px,color:#fff,stroke-dasharray: 5 5 |
||||
style Root stroke:#000,stroke-width:5px |
||||
style PropertyTable fill:#071E22 |
||||
|
||||
Root(<b>S0: Root Shape</b>\n<b>Prototype:</b> <i>None</i>) -->|Property Count: 0| PropertyTable(<b>PropertyTable</b>\nx: Slot 0, writable, configurable, enumerable\ny: Slot 1, has_get, has_set, configurable, enumerable) |
||||
|
||||
ObjectPrototype(<b>S1: Prototype Shape</b>\n<b>Prototype:</b> <i>Object.prototype</i>) -->| Property Count: 0 |PropertyTable |
||||
ObjectPrototype -->|Previous| Root |
||||
|
||||
InsertX(<b>S2: Insert Shape</b>\n<b>Property:</b> '<i>x</i>') --> |Property Count: 1| PropertyTable |
||||
InsertX -->|Previous| ObjectPrototype |
||||
|
||||
InsertY(<b>S3: Insert Shape</b>\n<b>Property:</b> '<i>y</i>') --> |Property Count: 2| PropertyTable |
||||
InsertY:::New -->|Previous| InsertX |
||||
|
||||
O(<b>Object o\nElement 0:</b> JsValue: 100\n<b>Element 1:</b> JsValue: func\n<b>Element 2:</b> JsValue: func) --> InsertY |
||||
``` |
||||
|
||||
We can see that the property has been added into the property table, it has the `has_get` and `has_set` flags set, |
||||
in the object there are two elements added, the first is the `get` function and the second is the `set` function. |
||||
|
||||
Slots are varying in length, two for accessor properties and one for data properties, the index points to the first |
||||
value in the object storage. |
||||
|
||||
What would happen if an object had `S2` shape and we tried to access a property `y` how does it know if it |
||||
has or doesn't have a property named `y`? By the property count on the shape, it has property count `1`, |
||||
all the object in the `PropertyTable` are stored in a map that preserves the order and and can be indexed. |
||||
|
||||
When we do a lookup the on property table, if the index of the property is greater than the property count (`1`), |
||||
than it does not belong to the shape. |
||||
|
||||
Now, Let's create a new object `o2`, with property `x`: |
||||
|
||||
```js |
||||
let o2 = { x: 200 }; |
||||
``` |
||||
|
||||
After this `o2` would have the `S2` shape. |
||||
|
||||
How does the shape know that it can reuse `S1` then to go to `S2`? This is not the real structure! |
||||
Every shape has pointers to forward transitions that happened, these are weak pointers so we don't keep |
||||
alive unused shapes. The pointers have been omitted, so the diagrams are clearer (too many arrows). |
||||
|
||||
Ok, now let us define a property `z` instead of `y`: |
||||
|
||||
```js |
||||
o2.z = 300; |
||||
``` |
||||
|
||||
The following changes accure to the shape tree: |
||||
|
||||
```mermaid |
||||
flowchart LR |
||||
classDef New style id2 fill:#bbf,stroke:#f66,stroke-width:2px,color:#fff,stroke-dasharray: 5 5 |
||||
style Root stroke:#000,stroke-width:5px |
||||
style PropertyTable fill:#071E22 |
||||
style PropertyTableFork fill:#071E22 |
||||
|
||||
Root(<b>S0: Root Shape</b>\n<b>Prototype:</b> <i>None</i>) -->| Property Count: 0 | PropertyTable(<b>PropertyTable</b>\nx: Slot 0, writable, configurable, enumerable\ny: Slot 1, has_get, has_set, configurable, enumerable) |
||||
|
||||
ObjectPrototype(<b>S1: Prototype Shape\nPrototype:</b> <i>Object.prototype</i>) -->| Property Count: 0 |PropertyTable |
||||
ObjectPrototype -->|Previous| Root |
||||
|
||||
InsertX(<b>S2: Insert Shape\nProperty:</b> '<i>x</i>') --> | Property Count: 1 | PropertyTable |
||||
InsertX -->|Previous| ObjectPrototype |
||||
|
||||
InsertY(<b>S3: Insert Shape\nProperty:</b> '<i>y</i>') --> | Property Count: 2 | PropertyTable |
||||
InsertY -->|Previous| InsertX |
||||
|
||||
PropertyTableFork(<b>PropertyTable</b>\nx: Slot 0, writable, configurable, enumerable\nz: Slot 1, writable, configurable, enumerable) |
||||
InsertZ(<b>S4: Insert Shape\nProperty:</b> '<i>z</i>') --> | Property Count: 2 | PropertyTableFork:::New |
||||
InsertZ:::New -->|Previous| InsertX |
||||
|
||||
O(<b>Object o\nElement 0:</b> JsValue: 100\n<b>Element 1:</b> JsValue: func\n<b>Element 2:</b> JsValue: func) --> InsertY |
||||
O2(<b>Object o2\nElement 0:</b> JsValue: 200\n<b>Element 1:</b> JsValue: 300) |
||||
O2:::New --> InsertZ |
||||
``` |
||||
|
||||
Now `o2` has `S4` shape. We can also see that `PropertyTable` has been forked, because we can no longer add a property at position `1`. |
||||
|
||||
What would happen if we wanted to delete a property `x` from object `o`: |
||||
|
||||
```js |
||||
delete o.x; |
||||
``` |
||||
|
||||
```mermaid |
||||
flowchart LR |
||||
classDef New style id2 fill:#bbf,stroke:#f66,stroke-width:2px,color:#fff,stroke-dasharray: 5 5 |
||||
style Root stroke:#000,stroke-width:5px |
||||
style PropertyTable fill:#071E22 |
||||
style PropertyTableFork fill:#071E22 |
||||
|
||||
Root(<b>S0: Root Shape</b>\n<b>Prototype:</b> <i>None</i>) -->| Property Count: 0 | PropertyTable(<b>PropertyTable</b>\nx: Slot 0, writable, configurable, enumerable\ny: Slot 1, has_get, has_set, configurable, enumerable) |
||||
|
||||
ObjectPrototype(<b>S1: Prototype Shape\nPrototype:</b> <i>Object.prototype</i>) -->| Property Count: 0 |PropertyTable |
||||
ObjectPrototype -->|Previous| Root |
||||
|
||||
|
||||
PropertyTableFork(<b>PropertyTable</b>\ny: Slot 0, has_get, has_set, configurable, enumerable) |
||||
InsertYNew(<b>S3: Insert Shape\nProperty:</b> <i>y</i>) --> | Property Count: 1 |PropertyTableFork:::New |
||||
InsertYNew:::New -->|Previous| ObjectPrototype |
||||
|
||||
O(<b>Object o</b>\n<b>Element 0:</b> JsValue: func\n<b>Element 1:</b> JsValue: func) --> InsertYNew |
||||
O:::New |
||||
``` |
||||
|
||||
**NOTE:**: `o2` and its shape have been omitted from the diagram. |
||||
|
||||
When a deletion happens, we find the node in the chain where we added the property, and get it's parent (`base`), |
||||
we also remember that what transitions happened after the property insertion, then we apply them |
||||
one by one until we construct the chain and return the last shape in that chain. |
Loading…
Reference in new issue