Skip to main content

boa_engine/object/shape/shared_shape/
mod.rs

1mod forward_transition;
2pub(crate) mod template;
3
4#[cfg(test)]
5mod tests;
6
7use std::{collections::hash_map::RandomState, hash::Hash};
8
9use bitflags::bitflags;
10use boa_gc::{Finalize, Gc, Trace, WeakGc, empty_trace};
11use indexmap::IndexMap;
12
13use crate::{JsObject, object::JsPrototype, property::PropertyKey};
14
15use self::forward_transition::ForwardTransition;
16
17use super::{
18    ChangeTransition, ChangeTransitionAction, Slot, UniqueShape, property_table::PropertyTable,
19    slot::SlotAttributes,
20};
21
22/// Represent a [`SharedShape`] property transition.
23#[derive(Debug, Finalize, Clone, PartialEq, Eq, Hash)]
24pub(crate) struct TransitionKey {
25    pub(crate) property_key: PropertyKey,
26    pub(crate) attributes: SlotAttributes,
27}
28
29// SAFETY: Non of the member of this struct are garbage collected,
30//         so this should be fine.
31unsafe impl Trace for TransitionKey {
32    empty_trace!();
33}
34
35const INSERT_PROPERTY_TRANSITION_TYPE: u8 = 0b0000_0000;
36const CONFIGURE_PROPERTY_TRANSITION_TYPE: u8 = 0b0000_0001;
37const PROTOTYPE_TRANSITION_TYPE: u8 = 0b0000_0010;
38
39// Reserved for future use!
40#[allow(unused)]
41const RESERVED_TRANSITION_TYPE: u8 = 0b0000_0011;
42
43bitflags! {
44    /// Flags of a shape.
45    #[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Finalize)]
46    pub struct ShapeFlags: u8 {
47        /// Represents the transition type of a [`SharedShape`].
48        const TRANSITION_TYPE = 0b0000_0011;
49    }
50}
51
52impl ShapeFlags {
53    // NOTE: Remove type bits and set the new ones.
54    fn insert_property_transition_from(previous: Self) -> Self {
55        previous.difference(Self::TRANSITION_TYPE)
56            | Self::from_bits_retain(INSERT_PROPERTY_TRANSITION_TYPE)
57    }
58    fn configure_property_transition_from(previous: Self) -> Self {
59        previous.difference(Self::TRANSITION_TYPE)
60            | Self::from_bits_retain(CONFIGURE_PROPERTY_TRANSITION_TYPE)
61    }
62    fn prototype_transition_from(previous: Self) -> Self {
63        previous.difference(Self::TRANSITION_TYPE)
64            | Self::from_bits_retain(PROTOTYPE_TRANSITION_TYPE)
65    }
66
67    const fn is_insert_transition_type(self) -> bool {
68        self.intersection(Self::TRANSITION_TYPE).bits() == INSERT_PROPERTY_TRANSITION_TYPE
69    }
70    const fn is_prototype_transition_type(self) -> bool {
71        self.intersection(Self::TRANSITION_TYPE).bits() == PROTOTYPE_TRANSITION_TYPE
72    }
73}
74
75// SAFETY: Non of the member of this struct are garbage collected,
76//         so this should be fine.
77unsafe impl Trace for ShapeFlags {
78    empty_trace!();
79}
80
81/// The internal representation of a [`SharedShape`].
82#[derive(Debug, Trace, Finalize)]
83struct Inner {
84    /// See [`ForwardTransition`].
85    forward_transitions: ForwardTransition,
86
87    /// The count of how many properties this [`SharedShape`] holds.
88    property_count: u32,
89
90    /// Instance prototype `__proto__`.
91    prototype: JsPrototype,
92
93    // SAFETY: This is safe because nothing in [`PropertyTable`]
94    //         needs tracing
95    #[unsafe_ignore_trace]
96    property_table: PropertyTable,
97
98    /// The previous shape in the transition chain.
99    ///
100    /// [`None`] if it is the root shape.
101    previous: Option<SharedShape>,
102
103    /// How many transitions have happened from the root node.
104    transition_count: u16,
105
106    /// Flags about the shape.
107    flags: ShapeFlags,
108}
109
110/// Represents a shared object shape.
111#[derive(Debug, Trace, Finalize, Clone)]
112pub struct SharedShape {
113    inner: Gc<Inner>,
114}
115
116impl SharedShape {
117    fn property_table(&self) -> &PropertyTable {
118        &self.inner.property_table
119    }
120    /// Return the property count that this shape owns in the [`PropertyTable`].
121    fn property_count(&self) -> u32 {
122        self.inner.property_count
123    }
124    /// Return the index to the property in the [`PropertyTable`].
125    fn property_index(&self) -> u32 {
126        self.inner.property_count.saturating_sub(1)
127    }
128    /// Getter for the transition count field.
129    #[must_use]
130    pub fn transition_count(&self) -> u16 {
131        self.inner.transition_count
132    }
133    /// Getter for the previous field.
134    #[must_use]
135    pub fn previous(&self) -> Option<&Self> {
136        self.inner.previous.as_ref()
137    }
138    /// Get the prototype of the shape.
139    #[must_use]
140    pub fn prototype(&self) -> JsPrototype {
141        self.inner.prototype.clone()
142    }
143    /// Get the property this [`SharedShape`] refers to.
144    pub(crate) fn property(&self) -> (PropertyKey, Slot) {
145        let inner = self.property_table().inner().borrow();
146        let (key, slot) = inner
147            .keys
148            .get(self.property_index() as usize)
149            .expect("There should be a property");
150        (key.clone(), *slot)
151    }
152    /// Get the flags of the shape.
153    fn flags(&self) -> ShapeFlags {
154        self.inner.flags
155    }
156    /// Getter for the [`ForwardTransition`] field.
157    fn forward_transitions(&self) -> &ForwardTransition {
158        &self.inner.forward_transitions
159    }
160    /// Check if the shape has the given prototype.
161    #[must_use]
162    pub fn has_prototype(&self, prototype: &JsObject) -> bool {
163        self.inner.prototype.as_ref() == Some(prototype)
164    }
165
166    /// Create a new [`SharedShape`].
167    fn new(inner: Inner) -> Self {
168        Self {
169            inner: Gc::new(inner),
170        }
171    }
172
173    /// Create a root [`SharedShape`].
174    #[must_use]
175    pub(crate) fn root() -> Self {
176        Self::new(Inner {
177            forward_transitions: ForwardTransition::default(),
178            prototype: None,
179            property_count: 0,
180            // Most of the time the root shape initiates with between 1-4 properties.
181            property_table: PropertyTable::with_capacity(4),
182            previous: None,
183            flags: ShapeFlags::default(),
184            transition_count: 0,
185        })
186    }
187
188    /// Create a [`SharedShape`] change prototype transition.
189    pub(crate) fn change_prototype_transition(&self, prototype: JsPrototype) -> Self {
190        if let Some(shape) = self.forward_transitions().get_prototype(&prototype) {
191            if let Some(inner) = shape.upgrade() {
192                return Self { inner };
193            }
194
195            self.forward_transitions().prune_prototype_transitions();
196        }
197        let new_inner_shape = Inner {
198            forward_transitions: ForwardTransition::default(),
199            prototype: prototype.clone(),
200            property_table: self.property_table().clone(),
201            property_count: self.property_count(),
202            previous: Some(self.clone()),
203            transition_count: self.transition_count() + 1,
204            flags: ShapeFlags::prototype_transition_from(self.flags()),
205        };
206        let new_shape = Self::new(new_inner_shape);
207
208        self.forward_transitions()
209            .insert_prototype(prototype, &new_shape.inner);
210
211        new_shape
212    }
213
214    /// Create a [`SharedShape`] insert property transition.
215    pub(crate) fn insert_property_transition(&self, key: TransitionKey) -> Self {
216        // Check if we have already created such a transition, if so use it!
217        if let Some(shape) = self.forward_transitions().get_property(&key) {
218            if let Some(inner) = shape.upgrade() {
219                return Self { inner };
220            }
221
222            self.forward_transitions().prune_property_transitions();
223        }
224
225        let property_table = self.property_table().add_property_deep_clone_if_needed(
226            key.property_key.clone(),
227            key.attributes,
228            self.property_count(),
229        );
230        let new_inner_shape = Inner {
231            prototype: self.prototype(),
232            forward_transitions: ForwardTransition::default(),
233            property_table,
234            property_count: self.property_count() + 1,
235            previous: Some(self.clone()),
236            transition_count: self.transition_count() + 1,
237            flags: ShapeFlags::insert_property_transition_from(self.flags()),
238        };
239        let new_shape = Self::new(new_inner_shape);
240
241        self.forward_transitions()
242            .insert_property(key, &new_shape.inner);
243
244        new_shape
245    }
246
247    /// Create a [`SharedShape`] change prototype transition, returning [`ChangeTransition`].
248    pub(crate) fn change_attributes_transition(
249        &self,
250        key: TransitionKey,
251    ) -> ChangeTransition<Self> {
252        let slot = self.property_table().get_expect(&key.property_key);
253
254        // Check if we have already created such a transition, if so use it!
255        if let Some(shape) = self.forward_transitions().get_property(&key) {
256            if let Some(inner) = shape.upgrade() {
257                let action = if slot.attributes.width_match(key.attributes) {
258                    ChangeTransitionAction::Nothing
259                } else if slot.attributes.is_accessor_descriptor() {
260                    // Accessor property --> Data property
261                    ChangeTransitionAction::Remove
262                } else {
263                    // Data property --> Accessor property
264                    ChangeTransitionAction::Insert
265                };
266
267                return ChangeTransition {
268                    shape: Self { inner },
269                    action,
270                };
271            }
272
273            self.forward_transitions().prune_property_transitions();
274        }
275
276        // The attribute change transitions, didn't change from accessor to data property or vice-versa.
277        if slot.attributes.width_match(key.attributes) {
278            let property_table = self.property_table().deep_clone_all();
279            property_table.set_attributes_at_index(&key.property_key, key.attributes);
280            let inner_shape = Inner {
281                forward_transitions: ForwardTransition::default(),
282                prototype: self.prototype(),
283                property_table,
284                property_count: self.property_count(),
285                previous: Some(self.clone()),
286                transition_count: self.transition_count() + 1,
287                flags: ShapeFlags::configure_property_transition_from(self.flags()),
288            };
289            let shape = Self::new(inner_shape);
290
291            self.forward_transitions()
292                .insert_property(key, &shape.inner);
293
294            return ChangeTransition {
295                shape,
296                action: ChangeTransitionAction::Nothing,
297            };
298        }
299
300        // Rollback before the property has added.
301        let (mut base, prototype, transitions) = self.rollback_before(&key.property_key);
302
303        // Apply prototype transition, if it was found.
304        if let Some(prototype) = prototype {
305            base = base.change_prototype_transition(prototype);
306        }
307
308        // Apply this property.
309        base = base.insert_property_transition(key);
310
311        // Apply previous properties.
312        for (property_key, attributes) in transitions.into_iter().rev() {
313            let transition = TransitionKey {
314                property_key,
315                attributes,
316            };
317            base = base.insert_property_transition(transition);
318        }
319
320        // Determine action to be performed on the storage.
321        let action = if slot.attributes.is_accessor_descriptor() {
322            // Accessor property --> Data property
323            ChangeTransitionAction::Remove
324        } else {
325            // Data property --> Accessor property
326            ChangeTransitionAction::Insert
327        };
328
329        ChangeTransition {
330            shape: base,
331            action,
332        }
333    }
334
335    /// Rollback to shape before the insertion of the [`PropertyKey`] that is provided.
336    ///
337    /// This returns the shape before the insertion, if it sees a prototype transition it will return the latest one,
338    /// ignoring any others, [`None`] otherwise. It also will return the property transitions ordered from
339    /// latest to oldest that it sees.
340    ///
341    /// NOTE: In the transitions it does not include the property that we are rolling back.
342    ///
343    /// NOTE: The prototype transitions if it sees a property insert and then later an attribute change it will condense
344    /// into one property insert transition with the new attribute in the change attribute transition,
345    /// in the same place that the property was inserted initially.
346    //
347    // For example with the following chain:
348    //
349    //        INSERT(x)             INSERT(y)                INSERT(z)
350    // { }  ------------>  { x }  ------------>  { x, y }  ------------>  { x, y, z }
351    //
352    // Then we call rollback on `y`:
353    //
354    //        INSERT(x)             INSERT(y)                INSERT(z)
355    // { }  ------------>  { x }  ------------>  { x, y }  ------------>  { x, y, z }
356    //                       ^
357    //                       \--- base (with array of transitions to be performed: INSERT(z),
358    //                                                 and protortype: None )
359    fn rollback_before(
360        &self,
361        key: &PropertyKey,
362    ) -> (
363        Self,
364        Option<JsPrototype>,
365        IndexMap<PropertyKey, SlotAttributes>,
366    ) {
367        let mut prototype = None;
368        let mut transitions: IndexMap<PropertyKey, SlotAttributes, RandomState> =
369            IndexMap::default();
370
371        let mut current = Some(self);
372        let base = loop {
373            let Some(current_shape) = current else {
374                unreachable!("The chain should have insert transition type!")
375            };
376
377            // We only take the latest prototype change it, if it exists.
378            if current_shape.flags().is_prototype_transition_type() {
379                if prototype.is_none() {
380                    prototype = Some(current_shape.prototype().clone());
381                }
382
383                // Skip when it is a prototype transition.
384                current = current_shape.previous();
385                continue;
386            }
387
388            let (current_property_key, slot) = current_shape.property();
389
390            if current_shape.flags().is_insert_transition_type() && &current_property_key == key {
391                let base = if let Some(base) = current_shape.previous() {
392                    base.clone()
393                } else {
394                    // It's the root, because it doesn't have previous.
395                    current_shape.clone()
396                };
397                break base;
398            }
399
400            // Do not add property that we are trying to delete.
401            // this can happen if a configure was called after inserting it into the shape
402            if &current_property_key != key {
403                // Only take the latest changes to a property. To try to build a smaller tree.
404                transitions
405                    .entry(current_property_key)
406                    .or_insert(slot.attributes);
407            }
408
409            current = current_shape.previous();
410        };
411
412        (base, prototype, transitions)
413    }
414
415    /// Remove a property from [`SharedShape`], returning the new [`SharedShape`].
416    pub(crate) fn remove_property_transition(&self, key: &PropertyKey) -> Self {
417        let (mut base, prototype, transitions) = self.rollback_before(key);
418
419        // Apply prototype transition, if it was found.
420        if let Some(prototype) = prototype {
421            base = base.change_prototype_transition(prototype);
422        }
423
424        for (property_key, attributes) in transitions.into_iter().rev() {
425            let transition = TransitionKey {
426                property_key,
427                attributes,
428            };
429            base = base.insert_property_transition(transition);
430        }
431
432        base
433    }
434
435    /// Do a property lookup, returns [`None`] if property not found.
436    pub(crate) fn lookup(&self, key: &PropertyKey) -> Option<Slot> {
437        let property_count = self.property_count();
438        if property_count == 0 {
439            return None;
440        }
441
442        let property_table_inner = self.property_table().inner().borrow();
443        // Check if we are trying to access properties that belong to another shape.
444        if let Some((property_table_index, slot)) = property_table_inner.map.get(key)
445            && *property_table_index < self.property_count()
446        {
447            return Some(*slot);
448        }
449        None
450    }
451
452    /// Gets all keys first strings then symbols in creation order.
453    pub(crate) fn keys(&self) -> Vec<PropertyKey> {
454        let property_table = self.property_table().inner().borrow();
455        property_table.keys_cloned_n(self.property_count())
456    }
457
458    /// Returns a new [`UniqueShape`] with the properties of the [`SharedShape`].
459    pub(crate) fn to_unique(&self) -> UniqueShape {
460        UniqueShape::new(
461            self.prototype(),
462            self.property_table()
463                .inner()
464                .borrow()
465                .clone_count(self.property_count()),
466        )
467    }
468
469    /// Return location in memory of the [`SharedShape`].
470    pub(crate) fn to_addr_usize(&self) -> usize {
471        let ptr: *const _ = self.inner.as_ref();
472        ptr as usize
473    }
474}
475
476/// Represents a weak reference to [`SharedShape`].
477#[derive(Debug, Trace, Finalize, Clone, PartialEq)]
478pub(crate) struct WeakSharedShape {
479    inner: WeakGc<Inner>,
480}
481
482impl WeakSharedShape {
483    /// Upgrade returns a [`SharedShape`] pointer for the internal value if the pointer is still live,
484    /// or [`None`] if the value was already garbage collected.
485    #[inline]
486    #[must_use]
487    pub(crate) fn upgrade(&self) -> Option<SharedShape> {
488        Some(SharedShape {
489            inner: self.inner.upgrade()?,
490        })
491    }
492}
493
494impl From<&SharedShape> for WeakSharedShape {
495    fn from(value: &SharedShape) -> Self {
496        WeakSharedShape {
497            inner: WeakGc::new(&value.inner),
498        }
499    }
500}