mirror of
https://github.com/LadybirdBrowser/ladybird.git
synced 2026-06-24 18:30:23 +00:00
Use the PrototypeChainValidity cell as the prototype-shape tag, since prototype shapes already always own one and non-prototype shapes do not. Remove the separate m_is_prototype_shape bit and its extra invariant.
489 lines
16 KiB
C++
489 lines
16 KiB
C++
/*
|
|
* Copyright (c) 2020-2024, Andreas Kling <andreas@ladybird.org>
|
|
* Copyright (c) 2025, Aliaksandr Kalenik <kalenik.aliaksandr@gmail.com>
|
|
*
|
|
* SPDX-License-Identifier: BSD-2-Clause
|
|
*/
|
|
|
|
#include <LibGC/DeferGC.h>
|
|
#include <LibJS/Runtime/DescriptorArray.h>
|
|
#include <LibJS/Runtime/ExternalMemory.h>
|
|
#include <LibJS/Runtime/Realm.h>
|
|
#include <LibJS/Runtime/Shape.h>
|
|
#include <LibJS/Runtime/VM.h>
|
|
|
|
namespace JS {
|
|
|
|
GC_DEFINE_ALLOCATOR(Shape);
|
|
GC_DEFINE_ALLOCATOR(PrototypeChainValidity);
|
|
|
|
Shape::~Shape() = default;
|
|
|
|
void Shape::finalize()
|
|
{
|
|
Base::finalize();
|
|
if (m_dictionary)
|
|
m_property_storage.property_table.~PropertyTablePtr();
|
|
}
|
|
|
|
GC::Ptr<DescriptorArray> Shape::descriptors() const
|
|
{
|
|
VERIFY(!m_dictionary);
|
|
return m_property_storage.descriptors;
|
|
}
|
|
|
|
void Shape::set_descriptors(GC::Ptr<DescriptorArray> descriptors)
|
|
{
|
|
VERIFY(!m_dictionary);
|
|
m_property_storage.descriptors = descriptors;
|
|
}
|
|
|
|
Shape::PropertyTable& Shape::property_table()
|
|
{
|
|
VERIFY(m_dictionary);
|
|
VERIFY(m_property_storage.property_table);
|
|
return *m_property_storage.property_table;
|
|
}
|
|
|
|
Shape::PropertyTable const& Shape::property_table() const
|
|
{
|
|
VERIFY(m_dictionary);
|
|
VERIFY(m_property_storage.property_table);
|
|
return *m_property_storage.property_table;
|
|
}
|
|
|
|
void Shape::become_dictionary_shape()
|
|
{
|
|
VERIFY(!m_dictionary);
|
|
new (&m_property_storage.property_table) PropertyTablePtr();
|
|
m_property_storage.property_table = make<PropertyTable>();
|
|
m_dictionary = true;
|
|
}
|
|
|
|
size_t Shape::external_memory_size() const
|
|
{
|
|
size_t size = 0;
|
|
if (m_dictionary)
|
|
size += hash_map_external_memory_size(property_table());
|
|
if (m_forward_transitions)
|
|
size += hash_map_external_memory_size(*m_forward_transitions);
|
|
if (m_prototype_transitions)
|
|
size += hash_map_external_memory_size(*m_prototype_transitions);
|
|
if (m_delete_transitions)
|
|
size += hash_map_external_memory_size(*m_delete_transitions);
|
|
if (m_child_prototype_shapes)
|
|
size += vector_external_memory_size(*m_child_prototype_shapes);
|
|
return size;
|
|
}
|
|
|
|
GC::Ref<Shape> Shape::create_dictionary_transition()
|
|
{
|
|
auto new_shape = heap().allocate<Shape>(m_realm);
|
|
new_shape->become_dictionary_shape();
|
|
new_shape->m_has_parameter_map = m_has_parameter_map;
|
|
new_shape->m_prototype = m_prototype;
|
|
invalidate_prototype_if_needed_for_new_prototype(new_shape);
|
|
copy_properties_to_dictionary_shape(*new_shape);
|
|
return new_shape;
|
|
}
|
|
|
|
GC::Ptr<Shape> Shape::get_or_prune_cached_forward_transition(TransitionKey const& key)
|
|
{
|
|
if (is_prototype_shape())
|
|
return nullptr;
|
|
if (!m_forward_transitions)
|
|
return nullptr;
|
|
auto it = m_forward_transitions->find(key);
|
|
if (it == m_forward_transitions->end())
|
|
return nullptr;
|
|
if (!it->value) {
|
|
// The cached forward transition has gone stale (from garbage collection). Prune it.
|
|
m_forward_transitions->remove(it);
|
|
return nullptr;
|
|
}
|
|
return it->value.ptr();
|
|
}
|
|
|
|
GC::Ptr<Shape> Shape::get_or_prune_cached_delete_transition(PropertyKey const& key)
|
|
{
|
|
if (is_prototype_shape())
|
|
return nullptr;
|
|
if (!m_delete_transitions)
|
|
return nullptr;
|
|
auto it = m_delete_transitions->find(key);
|
|
if (it == m_delete_transitions->end())
|
|
return nullptr;
|
|
if (!it->value) {
|
|
// The cached delete transition has gone stale (from garbage collection). Prune it.
|
|
m_delete_transitions->remove(it);
|
|
return nullptr;
|
|
}
|
|
return it->value.ptr();
|
|
}
|
|
|
|
GC::Ptr<Shape> Shape::get_or_prune_cached_prototype_transition(Object* prototype)
|
|
{
|
|
if (is_prototype_shape())
|
|
return nullptr;
|
|
if (!m_prototype_transitions)
|
|
return nullptr;
|
|
auto it = m_prototype_transitions->find(prototype);
|
|
if (it == m_prototype_transitions->end())
|
|
return nullptr;
|
|
if (!it->value) {
|
|
// The cached prototype transition has gone stale (from garbage collection). Prune it.
|
|
m_prototype_transitions->remove(it);
|
|
return nullptr;
|
|
}
|
|
return it->value.ptr();
|
|
}
|
|
|
|
GC::Ref<Shape> Shape::create_put_transition(PropertyKey const& property_key, PropertyAttributes attributes)
|
|
{
|
|
TransitionKey key { property_key, attributes };
|
|
if (auto existing_shape = get_or_prune_cached_forward_transition(key))
|
|
return *existing_shape;
|
|
auto new_shape = heap().allocate<Shape>(*this, PropertyCountChange::Increment);
|
|
|
|
if (descriptors() && descriptors()->size() == m_property_count) {
|
|
descriptors()->set(property_key, { m_property_count, attributes }, m_property_count);
|
|
new_shape->set_descriptors(descriptors());
|
|
} else {
|
|
new_shape->set_descriptors(copy_descriptors());
|
|
new_shape->descriptors()->set(property_key, { m_property_count, attributes }, m_property_count);
|
|
}
|
|
invalidate_prototype_if_needed_for_new_prototype(new_shape);
|
|
if (!is_prototype_shape()) {
|
|
if (!m_forward_transitions)
|
|
m_forward_transitions = make<HashMap<TransitionKey, GC::Weak<Shape>>>();
|
|
m_forward_transitions->set(key, new_shape);
|
|
}
|
|
return new_shape;
|
|
}
|
|
|
|
GC::Ref<Shape> Shape::create_configure_transition(PropertyKey const& property_key, PropertyAttributes attributes)
|
|
{
|
|
TransitionKey key { property_key, attributes };
|
|
if (auto existing_shape = get_or_prune_cached_forward_transition(key))
|
|
return *existing_shape;
|
|
auto new_shape = heap().allocate<Shape>(*this, PropertyCountChange::Preserve);
|
|
new_shape->set_descriptors(copy_descriptors());
|
|
new_shape->descriptors()->set_attributes(property_key, attributes, m_property_count);
|
|
invalidate_prototype_if_needed_for_new_prototype(new_shape);
|
|
if (!is_prototype_shape()) {
|
|
if (!m_forward_transitions)
|
|
m_forward_transitions = make<HashMap<TransitionKey, GC::Weak<Shape>>>();
|
|
m_forward_transitions->set(key, new_shape.ptr());
|
|
}
|
|
return new_shape;
|
|
}
|
|
|
|
GC::Ref<Shape> Shape::create_prototype_transition(Object* new_prototype)
|
|
{
|
|
if (new_prototype)
|
|
new_prototype->convert_to_prototype_if_needed();
|
|
if (auto existing_shape = get_or_prune_cached_prototype_transition(new_prototype))
|
|
return *existing_shape;
|
|
auto new_shape = heap().allocate<Shape>(*this, new_prototype);
|
|
if (m_dictionary && m_property_count > DescriptorArray::max_descriptor_count) {
|
|
new_shape->become_dictionary_shape();
|
|
copy_properties_to_dictionary_shape(*new_shape);
|
|
} else if (m_dictionary) {
|
|
new_shape->set_descriptors(copy_descriptors());
|
|
} else {
|
|
new_shape->set_descriptors(descriptors());
|
|
}
|
|
invalidate_prototype_if_needed_for_new_prototype(new_shape);
|
|
if (!is_prototype_shape()) {
|
|
if (!m_prototype_transitions)
|
|
m_prototype_transitions = make<HashMap<GC::Ptr<Object>, GC::Weak<Shape>>>();
|
|
m_prototype_transitions->set(new_prototype, new_shape.ptr());
|
|
}
|
|
return new_shape;
|
|
}
|
|
|
|
Shape::Shape(Realm& realm)
|
|
: m_realm(realm)
|
|
{
|
|
}
|
|
|
|
Shape::Shape(Shape& previous_shape, PropertyCountChange property_count_change)
|
|
: m_has_parameter_map(previous_shape.m_has_parameter_map)
|
|
, m_realm(previous_shape.m_realm)
|
|
, m_prototype(previous_shape.m_prototype)
|
|
, m_property_count(previous_shape.m_property_count)
|
|
{
|
|
switch (property_count_change) {
|
|
case PropertyCountChange::Preserve:
|
|
break;
|
|
case PropertyCountChange::Increment:
|
|
VERIFY(m_property_count < NumericLimits<u32>::max());
|
|
++m_property_count;
|
|
break;
|
|
case PropertyCountChange::Decrement:
|
|
VERIFY(m_property_count > 0);
|
|
--m_property_count;
|
|
break;
|
|
}
|
|
}
|
|
|
|
Shape::Shape(Shape& previous_shape, Object* new_prototype)
|
|
: m_has_parameter_map(previous_shape.m_has_parameter_map)
|
|
, m_realm(previous_shape.m_realm)
|
|
, m_prototype(new_prototype)
|
|
, m_property_count(previous_shape.m_property_count)
|
|
{
|
|
}
|
|
|
|
void Shape::visit_edges(Cell::Visitor& visitor)
|
|
{
|
|
Base::visit_edges(visitor);
|
|
visitor.visit(m_realm);
|
|
if (!m_dictionary)
|
|
visitor.visit(m_property_storage.descriptors);
|
|
visitor.visit(m_prototype);
|
|
|
|
visitor.ignore(m_prototype_transitions);
|
|
|
|
// Child prototype-shape weak refs need no marking; pruning is lazy.
|
|
visitor.ignore(m_child_prototype_shapes);
|
|
|
|
// FIXME: The forward transition keys should be weak, but we have to mark them for now in case they go stale.
|
|
if (m_forward_transitions) {
|
|
for (auto& it : *m_forward_transitions)
|
|
it.key.property_key.visit_edges(visitor);
|
|
}
|
|
|
|
// FIXME: The delete transition keys should be weak, but we have to mark them for now in case they go stale.
|
|
if (m_delete_transitions) {
|
|
for (auto& it : *m_delete_transitions)
|
|
it.key.visit_edges(visitor);
|
|
}
|
|
|
|
visitor.visit(m_prototype_chain_validity);
|
|
|
|
// Descriptor arrays mark their own keys; dictionary tables are not cells, so Shape marks their keys directly.
|
|
if (m_dictionary) {
|
|
for (auto& it : property_table())
|
|
it.key.visit_edges(visitor);
|
|
}
|
|
}
|
|
|
|
Optional<PropertyMetadata> Shape::lookup(PropertyKey const& property_key) const
|
|
{
|
|
if (m_property_count == 0)
|
|
return {};
|
|
if (m_dictionary) {
|
|
auto property = property_table().get(property_key);
|
|
if (!property.has_value())
|
|
return {};
|
|
return property;
|
|
}
|
|
if (!descriptors())
|
|
return {};
|
|
return descriptors()->lookup(property_key, m_property_count);
|
|
}
|
|
|
|
void Shape::for_each_property_in_insertion_order(Function<void(PropertyKey const&, PropertyMetadata const&)> const& callback) const
|
|
{
|
|
if (m_dictionary) {
|
|
for (auto const& [property_key, metadata] : property_table())
|
|
callback(property_key, metadata);
|
|
return;
|
|
}
|
|
if (!descriptors())
|
|
return;
|
|
descriptors()->for_each_in_insertion_order(callback, m_property_count);
|
|
}
|
|
|
|
void Shape::ensure_descriptor_array()
|
|
{
|
|
VERIFY(!m_dictionary);
|
|
if (descriptors())
|
|
return;
|
|
set_descriptors(heap().allocate<DescriptorArray>());
|
|
}
|
|
|
|
GC::Ref<DescriptorArray> Shape::copy_descriptors() const
|
|
{
|
|
VERIFY(m_property_count <= DescriptorArray::max_descriptor_count);
|
|
if (!m_dictionary && descriptors())
|
|
return heap().allocate<DescriptorArray>(*descriptors(), m_property_count);
|
|
|
|
auto descriptors = heap().allocate<DescriptorArray>();
|
|
for_each_property_in_insertion_order([&](auto const& property_key, auto const& metadata) {
|
|
descriptors->set(property_key, metadata, descriptors->size());
|
|
});
|
|
return descriptors;
|
|
}
|
|
|
|
void Shape::copy_properties_to_dictionary_shape(Shape& shape) const
|
|
{
|
|
VERIFY(shape.m_dictionary);
|
|
for_each_property_in_insertion_order([&](auto const& property_key, auto const& metadata) {
|
|
shape.property_table().set(property_key, metadata);
|
|
});
|
|
shape.m_property_count = shape.property_table().size();
|
|
}
|
|
|
|
GC::Ref<Shape> Shape::create_delete_transition(PropertyKey const& property_key)
|
|
{
|
|
if (auto existing_shape = get_or_prune_cached_delete_transition(property_key))
|
|
return *existing_shape;
|
|
auto new_shape = heap().allocate<Shape>(*this, PropertyCountChange::Decrement);
|
|
new_shape->set_descriptors(copy_descriptors());
|
|
new_shape->descriptors()->remove(property_key, m_property_count);
|
|
invalidate_prototype_if_needed_for_new_prototype(new_shape);
|
|
if (!m_delete_transitions)
|
|
m_delete_transitions = make<HashMap<PropertyKey, GC::Weak<Shape>>>();
|
|
m_delete_transitions->set(property_key, new_shape.ptr());
|
|
return new_shape;
|
|
}
|
|
|
|
void Shape::add_property_without_transition(PropertyKey const& property_key, PropertyAttributes attributes)
|
|
{
|
|
invalidate_prototype_if_needed_for_change_without_transition();
|
|
if (m_dictionary) {
|
|
if (property_table().set(property_key, { m_property_count, attributes }) == AK::HashSetResult::InsertedNewEntry) {
|
|
VERIFY(m_property_count < NumericLimits<u32>::max());
|
|
++m_property_count;
|
|
++m_dictionary_generation;
|
|
}
|
|
return;
|
|
}
|
|
|
|
ensure_descriptor_array();
|
|
if (!descriptors()->lookup(property_key, m_property_count).has_value()) {
|
|
VERIFY(m_property_count < NumericLimits<u32>::max());
|
|
descriptors()->set(property_key, { m_property_count, attributes }, m_property_count);
|
|
++m_property_count;
|
|
++m_dictionary_generation;
|
|
return;
|
|
}
|
|
descriptors()->set(property_key, { m_property_count, attributes }, m_property_count);
|
|
}
|
|
|
|
void Shape::set_property_attributes_without_transition(PropertyKey const& property_key, PropertyAttributes attributes)
|
|
{
|
|
invalidate_prototype_if_needed_for_change_without_transition();
|
|
VERIFY(is_dictionary());
|
|
auto it = property_table().find(property_key);
|
|
VERIFY(it != property_table().end());
|
|
it->value.attributes = attributes;
|
|
property_table().set(property_key, it->value);
|
|
++m_dictionary_generation;
|
|
}
|
|
|
|
void Shape::remove_property_without_transition(PropertyKey const& property_key, u32 offset)
|
|
{
|
|
invalidate_prototype_if_needed_for_change_without_transition();
|
|
VERIFY(is_dictionary());
|
|
if (property_table().remove(property_key))
|
|
--m_property_count;
|
|
for (auto& it : property_table()) {
|
|
VERIFY(it.value.offset != offset);
|
|
if (it.value.offset > offset)
|
|
--it.value.offset;
|
|
}
|
|
++m_dictionary_generation;
|
|
}
|
|
|
|
GC::Ref<Shape> Shape::clone_for_prototype()
|
|
{
|
|
VERIFY(!is_prototype_shape());
|
|
VERIFY(!m_prototype_chain_validity);
|
|
auto new_shape = heap().allocate<Shape>(m_realm);
|
|
new_shape->m_has_parameter_map = m_has_parameter_map;
|
|
new_shape->m_prototype = m_prototype;
|
|
if (m_dictionary && m_property_count > DescriptorArray::max_descriptor_count) {
|
|
new_shape->become_dictionary_shape();
|
|
copy_properties_to_dictionary_shape(*new_shape);
|
|
} else {
|
|
new_shape->set_descriptors(copy_descriptors());
|
|
new_shape->m_property_count = m_property_count;
|
|
}
|
|
new_shape->m_prototype_chain_validity = heap().allocate<PrototypeChainValidity>();
|
|
if (new_shape->m_prototype)
|
|
new_shape->m_prototype->shape().add_child_prototype_shape(*new_shape);
|
|
return new_shape;
|
|
}
|
|
|
|
void Shape::set_prototype_without_transition(Object* new_prototype)
|
|
{
|
|
VERIFY(new_prototype);
|
|
new_prototype->convert_to_prototype_if_needed();
|
|
m_prototype = new_prototype;
|
|
}
|
|
|
|
void Shape::set_prototype_shape()
|
|
{
|
|
VERIFY(!is_prototype_shape());
|
|
m_prototype_chain_validity = heap().allocate<PrototypeChainValidity>();
|
|
if (m_prototype)
|
|
m_prototype->shape().add_child_prototype_shape(*this);
|
|
}
|
|
|
|
void Shape::add_child_prototype_shape(GC::Ref<Shape> child)
|
|
{
|
|
VERIFY(is_prototype_shape());
|
|
VERIFY(child->is_prototype_shape());
|
|
if (!m_child_prototype_shapes)
|
|
m_child_prototype_shapes = make<Vector<GC::Weak<Shape>>>();
|
|
m_child_prototype_shapes->append(GC::Weak<Shape> { *child });
|
|
}
|
|
|
|
void Shape::invalidate_prototype_if_needed_for_new_prototype(GC::Ref<Shape> new_prototype_shape)
|
|
{
|
|
if (!is_prototype_shape())
|
|
return;
|
|
new_prototype_shape->set_prototype_shape();
|
|
m_prototype_chain_validity->set_valid(false);
|
|
|
|
invalidate_all_prototype_chains_leading_to_this();
|
|
|
|
// The owning object is keeping the same [[Prototype]], so its existing
|
|
// children descend from new_prototype_shape going forward.
|
|
new_prototype_shape->m_child_prototype_shapes = move(m_child_prototype_shapes);
|
|
}
|
|
|
|
void Shape::invalidate_prototype_if_needed_for_change_without_transition()
|
|
{
|
|
if (!is_prototype_shape())
|
|
return;
|
|
m_prototype_chain_validity->set_valid(false);
|
|
m_prototype_chain_validity = heap().allocate<PrototypeChainValidity>();
|
|
|
|
invalidate_all_prototype_chains_leading_to_this();
|
|
}
|
|
|
|
void Shape::invalidate_all_prototype_chains_leading_to_this()
|
|
{
|
|
if (!m_child_prototype_shapes || m_child_prototype_shapes->is_empty())
|
|
return;
|
|
|
|
HashTable<Shape*> shapes_to_invalidate;
|
|
Vector<Shape*> worklist;
|
|
auto enqueue_children_of = [&](Shape& shape) {
|
|
if (!shape.m_child_prototype_shapes)
|
|
return;
|
|
// Prune dead weak refs and enqueue the live ones in one pass.
|
|
shape.m_child_prototype_shapes->remove_all_matching([&](GC::Weak<Shape> const& weak) {
|
|
auto child = weak.ptr();
|
|
if (!child)
|
|
return true;
|
|
if (shapes_to_invalidate.set(child.ptr()) == HashSetResult::InsertedNewEntry)
|
|
worklist.append(child.ptr());
|
|
return false;
|
|
});
|
|
};
|
|
enqueue_children_of(*this);
|
|
while (!worklist.is_empty())
|
|
enqueue_children_of(*worklist.take_last());
|
|
|
|
for (auto* shape : shapes_to_invalidate) {
|
|
shape->m_prototype_chain_validity->set_valid(false);
|
|
shape->m_prototype_chain_validity = heap().allocate<PrototypeChainValidity>();
|
|
}
|
|
}
|
|
|
|
}
|