ladybird/Libraries/LibJS/Bytecode/PropertyNameIterator.cpp
Andreas Kling 879ac36e45 LibJS: Cache stable for-in iteration at bytecode sites
Cache the flattened enumerable key snapshot for each `for..in` site and
reuse a `PropertyNameIterator` when the receiver shape, dictionary
generation, indexed storage kind and length, prototype chain
validity, and magical-length state still match.

Handle packed indexed receivers as well as plain named-property
objects. Teach `ObjectPropertyIteratorNext` in `asmint.asm` to return
cached property values directly and to fall back to the slow iterator
logic when any guard fails.

Treat arrays' hidden non-enumerable `length` property as a visited
name for for-in shadowing, and include the receiver's magical-length
state in the cache key so arrays and plain objects do not share
snapshots.

Add `test-js` and `test-js-bytecode` coverage for mixed numeric and
named keys, packed receiver transitions, re-entry, iterator reuse, GC
retention, array length shadowing, and same-site cache reuse.
2026-04-10 15:12:53 +02:00

196 lines
7.2 KiB
C++

/*
* Copyright (c) 2026-present, the Ladybird developers.
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <LibJS/Bytecode/PropertyNameIterator.h>
#include <LibJS/Runtime/Realm.h>
#include <LibJS/Runtime/Shape.h>
namespace JS::Bytecode {
GC_DEFINE_ALLOCATOR(PropertyNameIterator);
GC::Ref<PropertyNameIterator> PropertyNameIterator::create(Realm& realm, GC::Ref<Object> object, Vector<PropertyKey> properties, FastPath fast_path, u32 indexed_property_count, GC::Ptr<Shape> shape, GC::Ptr<PrototypeChainValidity> prototype_chain_validity)
{
return realm.create<PropertyNameIterator>(realm, object, move(properties), fast_path, indexed_property_count, shape, prototype_chain_validity);
}
GC::Ref<PropertyNameIterator> PropertyNameIterator::create(Realm& realm, GC::Ref<Object> object, ObjectPropertyIteratorCacheData& property_cache, ObjectPropertyIteratorCache* iterator_cache_slot)
{
VERIFY(property_cache.fast_path() != FastPath::None);
return realm.create<PropertyNameIterator>(realm, object, property_cache, iterator_cache_slot);
}
ThrowCompletionOr<void> PropertyNameIterator::next(VM& vm, bool& done, Value& value)
{
VERIFY(m_object);
while (true) {
if (m_next_indexed_property < m_indexed_property_count) {
auto current_index = m_next_indexed_property++;
auto entry = PropertyKey { current_index };
if (m_fast_path != FastPath::None && !fast_path_still_valid())
disable_fast_path();
if (m_fast_path == FastPath::None && !TRY(m_object->has_property(entry)))
continue;
done = false;
if (m_fast_path != FastPath::None && m_property_cache)
// Cache-backed iterators keep the return values pre-materialized
// so the asm fast path can hand back keys without converting a
// PropertyKey on every iteration.
value = property_value_list()[current_index];
else
value = entry.to_value(vm);
return {};
}
auto properties = property_list();
if (m_next_property >= properties.size()) {
if (m_iterator_cache_slot) {
// Once exhausted, hand the iterator object back to the bytecode
// site cache so the next execution of the same loop can reuse it.
m_object = nullptr;
m_iterator_cache_slot->reusable_property_name_iterator = this;
m_iterator_cache_slot = nullptr;
}
done = true;
return {};
}
auto current_index = m_next_property++;
auto const& entry = properties[current_index];
if (m_fast_path != FastPath::None && !fast_path_still_valid())
disable_fast_path();
// If the property is deleted, don't include it (invariant no. 2)
if (m_fast_path == FastPath::None && !TRY(m_object->has_property(entry)))
continue;
done = false;
if (m_fast_path != FastPath::None && m_property_cache)
value = property_value_list()[m_indexed_property_count + current_index];
else
value = entry.to_value(vm);
return {};
}
}
void PropertyNameIterator::reset_with_cache_data(GC::Ref<Object> object, ObjectPropertyIteratorCacheData& property_cache, ObjectPropertyIteratorCache* iterator_cache_slot)
{
VERIFY(property_cache.fast_path() != FastPath::None);
m_object = object;
m_owned_properties.clear();
m_property_cache = &property_cache;
m_shape = property_cache.shape();
m_prototype_chain_validity = property_cache.prototype_chain_validity();
m_indexed_property_count = property_cache.indexed_property_count();
m_next_indexed_property = 0;
m_next_property = 0;
m_shape_is_dictionary = property_cache.shape()->is_dictionary();
m_shape_dictionary_generation = property_cache.shape_dictionary_generation();
m_fast_path = property_cache.fast_path();
m_iterator_cache_slot = iterator_cache_slot;
VERIFY(m_property_cache);
VERIFY(m_shape);
}
PropertyNameIterator::PropertyNameIterator(Realm& realm, GC::Ref<Object> object, Vector<PropertyKey> properties, FastPath fast_path, u32 indexed_property_count, GC::Ptr<Shape> shape, GC::Ptr<PrototypeChainValidity> prototype_chain_validity)
: Object(realm, nullptr)
, m_object(object)
, m_owned_properties(move(properties))
, m_shape(shape)
, m_prototype_chain_validity(prototype_chain_validity)
, m_indexed_property_count(indexed_property_count)
, m_fast_path(fast_path)
{
if (m_shape)
m_shape_is_dictionary = m_shape->is_dictionary();
if (m_shape_is_dictionary)
m_shape_dictionary_generation = m_shape->dictionary_generation();
}
PropertyNameIterator::PropertyNameIterator(Realm& realm, GC::Ref<Object> object, ObjectPropertyIteratorCacheData& property_cache, ObjectPropertyIteratorCache* iterator_cache_slot)
: Object(realm, nullptr)
, m_object(object)
, m_property_cache(&property_cache)
, m_shape(property_cache.shape())
, m_prototype_chain_validity(property_cache.prototype_chain_validity())
, m_iterator_cache_slot(iterator_cache_slot)
, m_indexed_property_count(property_cache.indexed_property_count())
, m_shape_is_dictionary(property_cache.shape()->is_dictionary())
, m_shape_dictionary_generation(property_cache.shape_dictionary_generation())
, m_fast_path(property_cache.fast_path())
{
VERIFY(m_fast_path != FastPath::None);
VERIFY(m_property_cache);
VERIFY(m_shape);
}
ReadonlySpan<PropertyKey> PropertyNameIterator::property_list() const
{
if (m_property_cache)
return m_property_cache->properties();
return m_owned_properties.span();
}
ReadonlySpan<Value> PropertyNameIterator::property_value_list() const
{
VERIFY(m_property_cache);
return m_property_cache->property_values();
}
bool PropertyNameIterator::fast_path_still_valid() const
{
VERIFY(m_object);
VERIFY(m_shape);
// We revalidate on every next() call so active enumeration can deopt if the
// receiver or prototype chain changes underneath us. After deopting, the
// iterator resumes with has_property() checks for the remaining snapshot.
auto& shape = m_object->shape();
if (&shape != m_shape)
return false;
if (m_shape_is_dictionary && shape.dictionary_generation() != m_shape_dictionary_generation)
return false;
if (m_fast_path == FastPath::PackedIndexed) {
if (m_object->indexed_storage_kind() != IndexedStorageKind::Packed)
return false;
if (m_object->indexed_array_like_size() != m_indexed_property_count)
return false;
}
if (m_prototype_chain_validity && !m_prototype_chain_validity->is_valid())
return false;
return true;
}
void PropertyNameIterator::disable_fast_path()
{
m_fast_path = FastPath::None;
m_shape = nullptr;
m_prototype_chain_validity = nullptr;
m_shape_is_dictionary = false;
m_shape_dictionary_generation = 0;
}
void PropertyNameIterator::visit_edges(Visitor& visitor)
{
Base::visit_edges(visitor);
visitor.visit(m_object);
visitor.visit(m_property_cache);
visitor.visit(m_shape);
visitor.visit(m_prototype_chain_validity);
for (auto& key : m_owned_properties)
key.visit_edges(visitor);
}
}