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.
This commit is contained in:
Andreas Kling 2026-04-10 00:56:49 +02:00 committed by Andreas Kling
parent 114eeddea1
commit 879ac36e45
27 changed files with 1453 additions and 157 deletions

View file

@ -239,6 +239,8 @@ i64 asm_slow_path_get_global(Interpreter*, u32 pc);
i64 asm_slow_path_set_global(Interpreter*, u32 pc);
i64 asm_slow_path_call(Interpreter*, u32 pc);
i64 asm_slow_path_call_builtin(Interpreter*, u32 pc);
i64 asm_slow_path_get_object_property_iterator(Interpreter*, u32 pc);
i64 asm_slow_path_object_property_iterator_next(Interpreter*, u32 pc);
i64 asm_slow_path_call_construct(Interpreter*, u32 pc);
i64 asm_slow_path_new_object(Interpreter*, u32 pc);
i64 asm_slow_path_cache_object_shape(Interpreter*, u32 pc);
@ -414,6 +416,8 @@ i64 asm_fallback_handler(Interpreter* interp, u32 pc)
return execute_throwing<Op::GetMethod>(*interp, pc);
case Instruction::Type::GetObjectPropertyIterator:
return execute_throwing<Op::GetObjectPropertyIterator>(*interp, pc);
case Instruction::Type::ObjectPropertyIteratorNext:
return execute_throwing<Op::ObjectPropertyIteratorNext>(*interp, pc);
case Instruction::Type::HasPrivateId:
return execute_throwing<Op::HasPrivateId>(*interp, pc);
case Instruction::Type::ImportCall:
@ -697,6 +701,16 @@ i64 asm_slow_path_call(Interpreter* interp, u32 pc)
return slow_path_throwing<Op::Call>(*interp, pc);
}
i64 asm_slow_path_get_object_property_iterator(Interpreter* interp, u32 pc)
{
return slow_path_throwing<Op::GetObjectPropertyIterator>(*interp, pc);
}
i64 asm_slow_path_object_property_iterator_next(Interpreter* interp, u32 pc)
{
return slow_path_throwing<Op::ObjectPropertyIteratorNext>(*interp, pc);
}
i64 asm_slow_path_call_builtin(Interpreter* interp, u32 pc)
{
return slow_path_throwing<Op::CallBuiltin>(*interp, pc);

View file

@ -2022,6 +2022,102 @@ end
# chain walks, etc). Having them here avoids the generic fallback handler's
# overhead of saving/restoring all temporaries.
handler GetObjectPropertyIterator
call_slow_path asm_slow_path_get_object_property_iterator
end
handler ObjectPropertyIteratorNext
load_operand t1, m_iterator_object
extract_tag t2, t1
branch_ne t2, OBJECT_TAG, .slow
unbox_object t3, t1
load8 t4, [t3, PROPERTY_NAME_ITERATOR_FAST_PATH]
mov t0, OBJECT_PROPERTY_ITERATOR_FAST_PATH_NONE
branch_eq t4, t0, .slow
# These guards mirror PropertyNameIterator::fast_path_still_valid(). If the
# receiver or prototype chain no longer matches the cached snapshot, we drop
# to C++ and continue in deoptimized mode for the rest of the enumeration.
load64 t5, [t3, PROPERTY_NAME_ITERATOR_PROPERTY_CACHE]
load64 t6, [t3, PROPERTY_NAME_ITERATOR_OBJECT]
load64 t7, [t3, PROPERTY_NAME_ITERATOR_SHAPE]
load64 t8, [t6, OBJECT_SHAPE]
branch_ne t8, t7, .slow
load8 t2, [t3, PROPERTY_NAME_ITERATOR_SHAPE_IS_DICTIONARY]
branch_zero t2, .check_receiver
load32 t0, [t8, SHAPE_DICTIONARY_GENERATION]
load32 t2, [t3, PROPERTY_NAME_ITERATOR_SHAPE_DICTIONARY_GENERATION]
branch_ne t0, t2, .slow
.check_receiver:
mov t0, OBJECT_PROPERTY_ITERATOR_FAST_PATH_PACKED_INDEXED
branch_ne t4, t0, .check_proto
load8 t0, [t6, OBJECT_INDEXED_STORAGE_KIND]
mov t2, INDEXED_STORAGE_KIND_PACKED
branch_ne t0, t2, .slow
load32 t0, [t6, OBJECT_INDEXED_ARRAY_LIKE_SIZE]
load32 t2, [t3, PROPERTY_NAME_ITERATOR_INDEXED_PROPERTY_COUNT]
branch_ne t0, t2, .slow
.check_proto:
load64 t0, [t3, PROPERTY_NAME_ITERATOR_PROTOTYPE_CHAIN_VALIDITY]
branch_zero t0, .next_key
load8 t2, [t0, PROTOTYPE_CHAIN_VALIDITY_VALID]
branch_zero t2, .slow
.next_key:
# property_values is laid out as:
# [receiver packed index keys..., flattened named keys...]
load32 t0, [t3, PROPERTY_NAME_ITERATOR_NEXT_INDEXED_PROPERTY]
load32 t2, [t3, PROPERTY_NAME_ITERATOR_INDEXED_PROPERTY_COUNT]
branch_ge_unsigned t0, t2, .named
load64 t8, [t5, OBJECT_PROPERTY_ITERATOR_CACHE_DATA_PROPERTY_VALUES_DATA]
load64 t8, [t8, t0, 8]
add t0, 1
store32 [t3, PROPERTY_NAME_ITERATOR_NEXT_INDEXED_PROPERTY], t0
store_operand m_dst_value, t8
mov t0, BOOLEAN_FALSE
store_operand m_dst_done, t0
dispatch_next
.named:
load64 t0, [t3, PROPERTY_NAME_ITERATOR_NEXT_PROPERTY]
load64 t8, [t5, OBJECT_PROPERTY_ITERATOR_CACHE_DATA_PROPERTY_VALUES_SIZE]
load32 t2, [t3, PROPERTY_NAME_ITERATOR_INDEXED_PROPERTY_COUNT]
sub t8, t2
branch_ge_unsigned t0, t8, .done
mov t8, t0
add t8, t2
load64 t5, [t5, OBJECT_PROPERTY_ITERATOR_CACHE_DATA_PROPERTY_VALUES_DATA]
load64 t8, [t5, t8, 8]
add t0, 1
store64 [t3, PROPERTY_NAME_ITERATOR_NEXT_PROPERTY], t0
store_operand m_dst_value, t8
mov t0, BOOLEAN_FALSE
store_operand m_dst_done, t0
dispatch_next
.done:
load64 t5, [t3, PROPERTY_NAME_ITERATOR_ITERATOR_CACHE_SLOT]
branch_zero t5, .store_done
# Return the exhausted iterator object to the bytecode-site cache so the
# next execution of this loop can reset and reuse it.
mov t0, 0
store64 [t3, PROPERTY_NAME_ITERATOR_OBJECT], t0
store64 [t5, OBJECT_PROPERTY_ITERATOR_CACHE_REUSABLE_PROPERTY_NAME_ITERATOR], t3
store64 [t3, PROPERTY_NAME_ITERATOR_ITERATOR_CACHE_SLOT], t0
.store_done:
mov t0, BOOLEAN_TRUE
store_operand m_dst_done, t0
dispatch_next
.slow:
call_slow_path asm_slow_path_object_property_iterator_next
end
handler CallConstruct
call_slow_path asm_slow_path_call_construct
end

View file

@ -11,6 +11,7 @@
#include <LibJS/Bytecode/Builtins.h>
#include <LibJS/Bytecode/Executable.h>
#include <LibJS/Bytecode/Interpreter.h>
#include <LibJS/Bytecode/PropertyNameIterator.h>
#include <LibJS/Bytecode/PutKind.h>
#include <LibJS/Runtime/ArrayBuffer.h>
#include <LibJS/Runtime/DeclarativeEnvironment.h>
@ -82,6 +83,35 @@ int main()
outln("const PROPERTY_LOOKUP_CACHE_ENTRY0_PROTOTYPE = {}", plc_entries + offsetof(PropertyLookupCache::Entry, prototype));
outln("const PROPERTY_LOOKUP_CACHE_ENTRY0_PROTOTYPE_CHAIN_VALIDITY = {}", plc_entries + offsetof(PropertyLookupCache::Entry, prototype_chain_validity));
// ObjectPropertyIteratorCacheData layout
outln("\n# ObjectPropertyIteratorCacheData layout");
EMIT_OFFSET(OBJECT_PROPERTY_ITERATOR_CACHE_DATA_PROPERTIES, ObjectPropertyIteratorCacheData, m_properties);
EMIT_OFFSET(OBJECT_PROPERTY_ITERATOR_CACHE_DATA_PROPERTY_VALUES, ObjectPropertyIteratorCacheData, m_property_values);
EMIT_OFFSET(OBJECT_PROPERTY_ITERATOR_CACHE_DATA_SHAPE, ObjectPropertyIteratorCacheData, m_shape);
EMIT_OFFSET(OBJECT_PROPERTY_ITERATOR_CACHE_DATA_PROTOTYPE_CHAIN_VALIDITY, ObjectPropertyIteratorCacheData, m_prototype_chain_validity);
EMIT_OFFSET(OBJECT_PROPERTY_ITERATOR_CACHE_DATA_INDEXED_PROPERTY_COUNT, ObjectPropertyIteratorCacheData, m_indexed_property_count);
EMIT_OFFSET(OBJECT_PROPERTY_ITERATOR_CACHE_DATA_SHAPE_DICTIONARY_GENERATION, ObjectPropertyIteratorCacheData, m_shape_dictionary_generation);
EMIT_OFFSET(OBJECT_PROPERTY_ITERATOR_CACHE_DATA_FAST_PATH, ObjectPropertyIteratorCacheData, m_fast_path);
// ObjectPropertyIteratorCache layout
outln("\n# ObjectPropertyIteratorCache layout");
EMIT_OFFSET(OBJECT_PROPERTY_ITERATOR_CACHE_DATA_PTR, ObjectPropertyIteratorCache, data);
EMIT_OFFSET(OBJECT_PROPERTY_ITERATOR_CACHE_REUSABLE_PROPERTY_NAME_ITERATOR, ObjectPropertyIteratorCache, reusable_property_name_iterator);
// PropertyNameIterator layout
outln("\n# PropertyNameIterator layout");
EMIT_OFFSET(PROPERTY_NAME_ITERATOR_OBJECT, PropertyNameIterator, m_object);
EMIT_OFFSET(PROPERTY_NAME_ITERATOR_PROPERTY_CACHE, PropertyNameIterator, m_property_cache);
EMIT_OFFSET(PROPERTY_NAME_ITERATOR_SHAPE, PropertyNameIterator, m_shape);
EMIT_OFFSET(PROPERTY_NAME_ITERATOR_PROTOTYPE_CHAIN_VALIDITY, PropertyNameIterator, m_prototype_chain_validity);
EMIT_OFFSET(PROPERTY_NAME_ITERATOR_ITERATOR_CACHE_SLOT, PropertyNameIterator, m_iterator_cache_slot);
EMIT_OFFSET(PROPERTY_NAME_ITERATOR_INDEXED_PROPERTY_COUNT, PropertyNameIterator, m_indexed_property_count);
EMIT_OFFSET(PROPERTY_NAME_ITERATOR_NEXT_INDEXED_PROPERTY, PropertyNameIterator, m_next_indexed_property);
EMIT_OFFSET(PROPERTY_NAME_ITERATOR_NEXT_PROPERTY, PropertyNameIterator, m_next_property);
EMIT_OFFSET(PROPERTY_NAME_ITERATOR_SHAPE_IS_DICTIONARY, PropertyNameIterator, m_shape_is_dictionary);
EMIT_OFFSET(PROPERTY_NAME_ITERATOR_SHAPE_DICTIONARY_GENERATION, PropertyNameIterator, m_shape_dictionary_generation);
EMIT_OFFSET(PROPERTY_NAME_ITERATOR_FAST_PATH, PropertyNameIterator, m_fast_path);
// Executable layout
outln("\n# Executable layout");
EMIT_OFFSET(EXECUTABLE_PROPERTY_LOOKUP_CACHES, Executable, property_lookup_caches);
@ -111,6 +141,12 @@ int main()
outln("const INDEXED_STORAGE_KIND_HOLEY = {}", static_cast<u8>(IndexedStorageKind::Holey));
outln("const INDEXED_STORAGE_KIND_DICTIONARY = {}", static_cast<u8>(IndexedStorageKind::Dictionary));
// ObjectPropertyIteratorFastPath enum values
outln("\n# ObjectPropertyIteratorFastPath enum values");
outln("const OBJECT_PROPERTY_ITERATOR_FAST_PATH_NONE = {}", static_cast<u8>(ObjectPropertyIteratorFastPath::None));
outln("const OBJECT_PROPERTY_ITERATOR_FAST_PATH_PLAIN_NAMED = {}", static_cast<u8>(ObjectPropertyIteratorFastPath::PlainNamed));
outln("const OBJECT_PROPERTY_ITERATOR_FAST_PATH_PACKED_INDEXED = {}", static_cast<u8>(ObjectPropertyIteratorFastPath::PackedIndexed));
// Vector<Value> layout (used for bytecode)
outln("\n# Vector<Value> layout");
{
@ -123,6 +159,8 @@ int main()
// Composite offset for Executable.bytecode data pointer
outln("const EXECUTABLE_BYTECODE_DATA = {}", offsetof(Executable, bytecode) + vec_data);
outln("const OBJECT_PROPERTY_ITERATOR_CACHE_DATA_PROPERTY_VALUES_DATA = {}", offsetof(ObjectPropertyIteratorCacheData, m_property_values) + vec_data);
outln("const OBJECT_PROPERTY_ITERATOR_CACHE_DATA_PROPERTY_VALUES_SIZE = {}", offsetof(ObjectPropertyIteratorCacheData, m_property_values) + vec_size);
}
// PutKind enum

View file

@ -348,10 +348,9 @@ op GetNewTarget < Instruction
endop
op GetObjectPropertyIterator < Instruction
m_dst_iterator_object: Operand
m_dst_iterator_next: Operand
m_dst_iterator_done: Operand
m_dst_iterator: Operand
m_object: Operand
m_cache: ObjectPropertyIteratorCache*
endop
op GetPrivateById < Instruction
@ -468,6 +467,12 @@ op IteratorNextUnpack < Instruction
m_iterator_done: Operand
endop
op ObjectPropertyIteratorNext < Instruction
m_dst_value: Operand
m_dst_done: Operand
m_iterator_object: Operand
endop
op IteratorToArray < Instruction
m_dst: Operand
m_iterator_object: Operand
@ -950,4 +955,3 @@ op InitObjectLiteralProperty < Instruction
m_shape_cache_index: u32
m_property_slot: u32
endop

View file

@ -19,6 +19,38 @@
namespace JS::Bytecode {
GC_DEFINE_ALLOCATOR(Executable);
GC_DEFINE_ALLOCATOR(ObjectPropertyIteratorCacheData);
ObjectPropertyIteratorCacheData::ObjectPropertyIteratorCacheData(VM& vm, Vector<PropertyKey> properties, ObjectPropertyIteratorFastPath fast_path, u32 indexed_property_count, bool receiver_has_magical_length_property, GC::Ref<Shape> shape, GC::Ptr<PrototypeChainValidity> prototype_chain_validity)
: m_properties(move(properties))
, m_shape(shape)
, m_prototype_chain_validity(prototype_chain_validity)
, m_indexed_property_count(indexed_property_count)
, m_receiver_has_magical_length_property(receiver_has_magical_length_property)
, m_fast_path(fast_path)
{
// The iterator fast path returns JS Values directly, so materialize the
// cached key list once up front instead of converting PropertyKeys during
// every ObjectPropertyIteratorNext.
m_property_values.ensure_capacity(indexed_property_count + m_properties.size());
for (u32 i = 0; i < indexed_property_count; ++i)
m_property_values.append(PropertyKey { i }.to_value(vm));
for (auto const& key : m_properties)
m_property_values.append(key.to_value(vm));
if (m_shape->is_dictionary())
m_shape_dictionary_generation = m_shape->dictionary_generation();
}
void ObjectPropertyIteratorCacheData::visit_edges(Visitor& visitor)
{
Base::visit_edges(visitor);
visitor.visit(m_shape);
visitor.visit(m_prototype_chain_validity);
visitor.visit(m_property_values.span());
for (auto& key : m_properties)
key.visit_edges(visitor);
}
Executable::Executable(
Vector<u8> bytecode,
@ -32,6 +64,7 @@ Executable::Executable(
size_t number_of_global_variable_caches,
size_t number_of_template_object_caches,
size_t number_of_object_shape_caches,
size_t number_of_object_property_iterator_caches,
size_t number_of_registers,
Strict strict)
: GC::WeakContainer(heap())
@ -49,6 +82,7 @@ Executable::Executable(
global_variable_caches.resize(number_of_global_variable_caches);
template_object_caches.resize(number_of_template_object_caches);
object_shape_caches.resize(number_of_object_shape_caches);
object_property_iterator_caches.resize(number_of_object_property_iterator_caches);
}
Executable::~Executable() = default;
@ -61,7 +95,8 @@ void Executable::fixup_cache_pointers()
property_lookup_caches.span(),
global_variable_caches.span(),
template_object_caches.span(),
object_shape_caches.span());
object_shape_caches.span(),
object_property_iterator_caches.span());
}
}
@ -239,6 +274,10 @@ void Executable::visit_edges(Visitor& visitor)
visitor.visit(constants);
for (auto& cache : template_object_caches)
visitor.visit(cache.cached_template_object);
for (auto& cache : object_property_iterator_caches)
visitor.visit(cache.data);
for (auto& cache : object_property_iterator_caches)
visitor.visit(cache.reusable_property_name_iterator);
for (auto& data : shared_function_data)
visitor.visit(data);
for (auto& blueprint : class_blueprints) {

View file

@ -95,6 +95,47 @@ struct ObjectShapeCache {
Vector<u32> property_offsets;
};
enum class ObjectPropertyIteratorFastPath : u8 {
None,
PlainNamed,
PackedIndexed,
};
class JS_API ObjectPropertyIteratorCacheData final : public Cell {
GC_CELL(ObjectPropertyIteratorCacheData, Cell);
GC_DECLARE_ALLOCATOR(ObjectPropertyIteratorCacheData);
public:
ObjectPropertyIteratorCacheData(VM&, Vector<PropertyKey>, ObjectPropertyIteratorFastPath, u32 indexed_property_count, bool receiver_has_magical_length_property, GC::Ref<Shape>, GC::Ptr<PrototypeChainValidity> = nullptr);
virtual ~ObjectPropertyIteratorCacheData() override = default;
[[nodiscard]] ReadonlySpan<PropertyKey> properties() const { return m_properties.span(); }
[[nodiscard]] ReadonlySpan<Value> property_values() const { return m_property_values.span(); }
[[nodiscard]] ObjectPropertyIteratorFastPath fast_path() const { return m_fast_path; }
[[nodiscard]] u32 indexed_property_count() const { return m_indexed_property_count; }
[[nodiscard]] bool receiver_has_magical_length_property() const { return m_receiver_has_magical_length_property; }
[[nodiscard]] GC::Ptr<Shape> shape() const { return m_shape; }
[[nodiscard]] GC::Ptr<PrototypeChainValidity> prototype_chain_validity() const { return m_prototype_chain_validity; }
[[nodiscard]] u32 shape_dictionary_generation() const { return m_shape_dictionary_generation; }
private:
virtual void visit_edges(Visitor&) override;
Vector<PropertyKey> m_properties;
Vector<Value> m_property_values;
GC::Ptr<Shape> m_shape;
GC::Ptr<PrototypeChainValidity> m_prototype_chain_validity;
u32 m_indexed_property_count { 0 };
u32 m_shape_dictionary_generation { 0 };
bool m_receiver_has_magical_length_property { false };
ObjectPropertyIteratorFastPath m_fast_path { ObjectPropertyIteratorFastPath::None };
};
struct ObjectPropertyIteratorCache {
GC::Ptr<ObjectPropertyIteratorCacheData> data;
GC::Ptr<Object> reusable_property_name_iterator;
};
struct SourceRecord {
u32 source_start_offset {};
u32 source_end_offset {};
@ -124,6 +165,7 @@ public:
size_t number_of_global_variable_caches,
size_t number_of_template_object_caches,
size_t number_of_object_shape_caches,
size_t number_of_object_property_iterator_caches,
size_t number_of_registers,
Strict);
@ -135,6 +177,7 @@ public:
Vector<GlobalVariableCache> global_variable_caches;
Vector<TemplateObjectCache> template_object_caches;
Vector<ObjectShapeCache> object_shape_caches;
Vector<ObjectPropertyIteratorCache> object_property_iterator_caches;
NonnullOwnPtr<StringTable> string_table;
NonnullOwnPtr<IdentifierTable> identifier_table;
NonnullOwnPtr<PropertyKeyTable> property_key_table;

View file

@ -17,6 +17,7 @@
#include <LibJS/Bytecode/Label.h>
#include <LibJS/Bytecode/Op.h>
#include <LibJS/Bytecode/PropertyAccess.h>
#include <LibJS/Bytecode/PropertyNameIterator.h>
#include <LibJS/Export.h>
#include <LibJS/Runtime/AbstractOperations.h>
#include <LibJS/Runtime/Accessor.h>
@ -707,6 +708,7 @@ void Interpreter::run_bytecode(size_t entry_point)
HANDLE_INSTRUCTION(IteratorClose);
HANDLE_INSTRUCTION(IteratorNext);
HANDLE_INSTRUCTION(IteratorNextUnpack);
HANDLE_INSTRUCTION(ObjectPropertyIteratorNext);
HANDLE_INSTRUCTION(IteratorToArray);
HANDLE_INSTRUCTION_WITHOUT_EXCEPTION_CHECK(LeavePrivateEnvironment);
HANDLE_INSTRUCTION(LeftShift);
@ -1428,65 +1430,180 @@ inline ThrowCompletionOr<void> append(VM& vm, Value lhs, Value rhs, bool is_spre
return {};
}
class JS_API PropertyNameIterator final
: public Object
, public BuiltinIterator {
JS_OBJECT(PropertyNameIterator, Object);
GC_DECLARE_ALLOCATOR(PropertyNameIterator);
public:
virtual ~PropertyNameIterator() override = default;
BuiltinIterator* as_builtin_iterator_if_next_is_not_redefined(Value) override { return this; }
ThrowCompletionOr<void> next(VM& vm, bool& done, Value& value) override
{
while (true) {
if (m_iterator == m_properties.end()) {
done = true;
return {};
}
auto const& entry = *m_iterator;
ScopeGuard remove_first = [&] { ++m_iterator; };
// If the property is deleted, don't include it (invariant no. 2)
if (!TRY(m_object->has_property(entry)))
continue;
done = false;
value = entry.to_value(vm);
return {};
}
}
private:
PropertyNameIterator(JS::Realm& realm, GC::Ref<Object> object, Vector<PropertyKey> properties)
: Object(realm, nullptr)
, m_object(object)
, m_properties(move(properties))
, m_iterator(m_properties.begin())
{
}
virtual void visit_edges(Visitor& visitor) override
{
Base::visit_edges(visitor);
visitor.visit(m_object);
for (auto& key : m_properties)
key.visit_edges(visitor);
if (!m_iterator.is_end())
m_iterator->visit_edges(visitor);
}
GC::Ref<Object> m_object;
Vector<PropertyKey> m_properties;
decltype(m_properties.begin()) m_iterator;
struct FastPropertyNameIteratorData {
Vector<PropertyKey> properties;
PropertyNameIterator::FastPath fast_path { PropertyNameIterator::FastPath::None };
u32 indexed_property_count { 0 };
bool receiver_has_magical_length_property { false };
GC::Ptr<Shape> shape;
GC::Ptr<PrototypeChainValidity> prototype_chain_validity;
};
GC_DEFINE_ALLOCATOR(PropertyNameIterator);
static bool shape_has_enumerable_string_property(Shape const& shape)
{
for (auto const& [property_key, metadata] : shape.property_table()) {
if (property_key.is_string() && metadata.attributes.is_enumerable())
return true;
}
return false;
}
static bool property_name_iterator_fast_path_is_still_eligible(Object& object, PropertyNameIterator::FastPath fast_path, u32 indexed_property_count)
{
Object const* object_to_check = &object;
bool is_receiver = true;
while (object_to_check) {
if (!object_to_check->eligible_for_own_property_enumeration_fast_path())
return false;
if (is_receiver) {
if (fast_path == PropertyNameIterator::FastPath::PackedIndexed) {
if (object_to_check->indexed_storage_kind() != IndexedStorageKind::Packed)
return false;
if (object_to_check->indexed_array_like_size() != indexed_property_count)
return false;
} else if (object_to_check->indexed_array_like_size() != 0) {
return false;
}
} else if (object_to_check->indexed_array_like_size() != 0) {
return false;
}
object_to_check = object_to_check->prototype();
is_receiver = false;
}
return true;
}
static bool object_property_iterator_cache_matches(Object& object, ObjectPropertyIteratorCacheData const& cache)
{
// A cache entry represents the fully flattened key snapshot for one bytecode
// site. Reusing it is only valid while the receiver still has the same local
// state and the prototype chain validity token says nothing above it changed.
if (object.has_magical_length_property() != cache.receiver_has_magical_length_property())
return false;
auto& shape = object.shape();
if (&shape != cache.shape())
return false;
if (shape.is_dictionary() && shape.dictionary_generation() != cache.shape_dictionary_generation())
return false;
if (cache.prototype_chain_validity() && !cache.prototype_chain_validity()->is_valid())
return false;
return property_name_iterator_fast_path_is_still_eligible(object, cache.fast_path(), cache.indexed_property_count());
}
static ThrowCompletionOr<Optional<FastPropertyNameIteratorData>> try_get_fast_property_name_iterator_data(Object& object)
{
auto& vm = object.vm();
FastPropertyNameIteratorData result {};
result.fast_path = PropertyNameIterator::FastPath::PlainNamed;
result.receiver_has_magical_length_property = object.has_magical_length_property();
result.shape = &object.shape();
HashTable<GC::Ref<Object>> seen_objects;
size_t estimated_properties_count = 0;
bool prototype_chain_has_enumerable_named_properties = false;
for (auto object_to_check = GC::Ptr { &object }; object_to_check && !seen_objects.contains(*object_to_check); object_to_check = TRY(object_to_check->internal_get_prototype_of())) {
seen_objects.set(*object_to_check);
if (!object_to_check->eligible_for_own_property_enumeration_fast_path())
return Optional<FastPropertyNameIteratorData> {};
if (&object == object_to_check.ptr()) {
if (object_to_check->indexed_array_like_size() != 0) {
if (object_to_check->indexed_storage_kind() != IndexedStorageKind::Packed)
return Optional<FastPropertyNameIteratorData> {};
result.fast_path = PropertyNameIterator::FastPath::PackedIndexed;
result.indexed_property_count = object_to_check->indexed_array_like_size();
} else {
result.fast_path = PropertyNameIterator::FastPath::PlainNamed;
}
} else if (object_to_check->indexed_array_like_size() != 0) {
// The fast path only knows how to synthesize a packed indexed prefix
// for the receiver itself. As soon as indexed properties appear in
// the prototype chain, we fall back to the generic enumeration path.
return Optional<FastPropertyNameIteratorData> {};
} else if (!prototype_chain_has_enumerable_named_properties) {
prototype_chain_has_enumerable_named_properties = shape_has_enumerable_string_property(object_to_check->shape());
}
estimated_properties_count += object_to_check->shape().property_count();
}
seen_objects.clear_with_capacity();
if (auto* prototype = object.shape().prototype()) {
result.prototype_chain_validity = prototype->shape().prototype_chain_validity();
if (!result.prototype_chain_validity)
return Optional<FastPropertyNameIteratorData> {};
}
if (!prototype_chain_has_enumerable_named_properties) {
// Common case: only the receiver contributes enumerable string keys, so
// we can copy them straight from the shape without any shadowing work.
result.properties.ensure_capacity(object.shape().property_count());
for (auto const& [property_key, metadata] : object.shape().property_table()) {
if (property_key.is_string() && metadata.attributes.is_enumerable())
result.properties.append(property_key);
}
return result;
}
result.properties.ensure_capacity(estimated_properties_count);
HashTable<PropertyKey> seen_non_enumerable_properties;
Optional<HashTable<PropertyKey>> seen_properties;
auto ensure_seen_properties = [&] {
if (seen_properties.has_value())
return;
// Prototype shadowing ignores enumerability, so once we start looking
// above the receiver we need an explicit visited set for names we have
// already decided to expose from lower objects.
seen_properties = HashTable<PropertyKey> {};
seen_properties->ensure_capacity(result.properties.size());
for (auto const& property : result.properties)
seen_properties->set(property);
};
bool in_prototype_chain = false;
for (auto object_to_check = GC::Ptr { &object }; object_to_check && !seen_objects.contains(*object_to_check); object_to_check = TRY(object_to_check->internal_get_prototype_of())) {
seen_objects.set(*object_to_check);
// Arrays keep a non-enumerable magical `length` property outside the shape
// table, but it still shadows enumerable `length` properties higher up the
// prototype chain during for-in.
if (object_to_check->has_magical_length_property())
seen_non_enumerable_properties.set(vm.names.length);
for (auto const& [property_key, metadata] : object_to_check->shape().property_table()) {
if (!property_key.is_string())
continue;
bool enumerable = metadata.attributes.is_enumerable();
if (!enumerable)
seen_non_enumerable_properties.set(property_key);
if (in_prototype_chain && enumerable) {
if (seen_non_enumerable_properties.contains(property_key))
continue;
ensure_seen_properties();
if (seen_properties->contains(property_key))
continue;
}
if (enumerable)
result.properties.append(property_key);
if (seen_properties.has_value())
seen_properties->set(property_key);
}
in_prototype_chain = true;
}
return result;
}
// 14.7.5.9 EnumerateObjectProperties ( O ), https://tc39.es/ecma262/#sec-enumerate-object-properties
inline ThrowCompletionOr<IteratorRecordImpl> get_object_property_iterator(Interpreter& interpreter, Value value)
inline ThrowCompletionOr<GC::Ref<PropertyNameIterator>> get_object_property_iterator(Interpreter& interpreter, Value value, ObjectPropertyIteratorCache* cache = nullptr)
{
// While the spec does provide an algorithm, it allows us to implement it ourselves so long as we meet the following invariants:
// 1- Returned property keys do not include keys that are Symbols
@ -1507,6 +1624,43 @@ inline ThrowCompletionOr<IteratorRecordImpl> get_object_property_iterator(Interp
// Note: While the spec doesn't explicitly require these to be ordered, it says that the values should be retrieved via OwnPropertyKeys,
// so we just keep the order consistent anyway.
if (cache && cache->data) {
if (object_property_iterator_cache_matches(*object, *cache->data)) {
if (cache->reusable_property_name_iterator) {
// We keep one iterator object per bytecode site alive so hot
// loops can recycle it without allocating a new cell each time.
auto& iterator = static_cast<PropertyNameIterator&>(*cache->reusable_property_name_iterator);
cache->reusable_property_name_iterator = nullptr;
iterator.reset_with_cache_data(object, *cache->data, cache);
return iterator;
}
return PropertyNameIterator::create(interpreter.realm(), object, *cache->data, cache);
}
}
if (auto fast_iterator_data = TRY(try_get_fast_property_name_iterator_data(*object)); fast_iterator_data.has_value()) {
VERIFY(fast_iterator_data->shape);
auto cache_data = vm.heap().allocate<ObjectPropertyIteratorCacheData>(
vm,
move(fast_iterator_data->properties),
fast_iterator_data->fast_path,
fast_iterator_data->indexed_property_count,
fast_iterator_data->receiver_has_magical_length_property,
*fast_iterator_data->shape,
fast_iterator_data->prototype_chain_validity);
if (cache)
cache->data = cache_data;
if (cache && cache->reusable_property_name_iterator) {
auto& iterator = static_cast<PropertyNameIterator&>(*cache->reusable_property_name_iterator);
cache->reusable_property_name_iterator = nullptr;
iterator.reset_with_cache_data(object, cache_data, cache);
return iterator;
}
return PropertyNameIterator::create(interpreter.realm(), object, cache_data, cache);
}
size_t estimated_properties_count = 0;
HashTable<GC::Ref<Object>> seen_objects;
for (auto object_to_check = GC::Ptr { object.ptr() }; object_to_check && !seen_objects.contains(*object_to_check); object_to_check = TRY(object_to_check->internal_get_prototype_of())) {
@ -1552,8 +1706,7 @@ inline ThrowCompletionOr<IteratorRecordImpl> get_object_property_iterator(Interp
in_prototype_chain = true;
}
auto iterator = interpreter.realm().create<PropertyNameIterator>(interpreter.realm(), object, move(properties));
return IteratorRecordImpl { .done = false, .iterator = iterator, .next_method = js_undefined() };
return PropertyNameIterator::create(interpreter.realm(), object, move(properties));
}
ByteString Instruction::to_byte_string(Bytecode::Executable const& executable) const
@ -3152,10 +3305,8 @@ ThrowCompletionOr<void> GetMethod::execute_impl(Bytecode::Interpreter& interpret
NEVER_INLINE ThrowCompletionOr<void> GetObjectPropertyIterator::execute_impl(Bytecode::Interpreter& interpreter) const
{
auto iterator_record = TRY(get_object_property_iterator(interpreter, interpreter.get(m_object)));
interpreter.set(m_dst_iterator_object, iterator_record.iterator);
interpreter.set(m_dst_iterator_next, iterator_record.next_method);
interpreter.set(m_dst_iterator_done, Value(iterator_record.done));
auto* cache = bit_cast<ObjectPropertyIteratorCache*>(m_cache);
interpreter.set(m_dst_iterator, TRY(get_object_property_iterator(interpreter, interpreter.get(m_object), cache)));
return {};
}
@ -3205,6 +3356,18 @@ ThrowCompletionOr<void> IteratorNextUnpack::execute_impl(Bytecode::Interpreter&
return {};
}
ThrowCompletionOr<void> ObjectPropertyIteratorNext::execute_impl(Bytecode::Interpreter& interpreter) const
{
auto& iterator = static_cast<PropertyNameIterator&>(interpreter.get(m_iterator_object).as_object());
Value value;
bool done = false;
TRY(iterator.next(interpreter.vm(), done, value));
interpreter.set(m_dst_done, Value(done));
if (!done)
interpreter.set(m_dst_value, value);
return {};
}
NEVER_INLINE ThrowCompletionOr<void> NewClass::execute_impl(Bytecode::Interpreter& interpreter) const
{
Value super_class;

View file

@ -0,0 +1,196 @@
/*
* 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);
}
}

View file

@ -0,0 +1,61 @@
/*
* Copyright (c) 2026-present, the Ladybird developers.
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <LibJS/Bytecode/Executable.h>
#include <LibJS/Export.h>
#include <LibJS/Runtime/Iterator.h>
#include <LibJS/Runtime/Object.h>
namespace JS::Bytecode {
class JS_API PropertyNameIterator final
: public Object
, public BuiltinIterator {
JS_OBJECT(PropertyNameIterator, Object);
GC_DECLARE_ALLOCATOR(PropertyNameIterator);
public:
using FastPath = ObjectPropertyIteratorFastPath;
static GC::Ref<PropertyNameIterator> create(Realm&, GC::Ref<Object>, Vector<PropertyKey>, FastPath = FastPath::None, u32 indexed_property_count = 0, GC::Ptr<Shape> = nullptr, GC::Ptr<PrototypeChainValidity> = nullptr);
static GC::Ref<PropertyNameIterator> create(Realm&, GC::Ref<Object>, ObjectPropertyIteratorCacheData&, ObjectPropertyIteratorCache* = nullptr);
virtual ~PropertyNameIterator() override = default;
BuiltinIterator* as_builtin_iterator_if_next_is_not_redefined(Value) override { return this; }
ThrowCompletionOr<void> next(VM&, bool& done, Value& value) override;
void reset_with_cache_data(GC::Ref<Object>, ObjectPropertyIteratorCacheData&, ObjectPropertyIteratorCache*);
private:
PropertyNameIterator(Realm&, GC::Ref<Object>, Vector<PropertyKey>, FastPath, u32 indexed_property_count, GC::Ptr<Shape>, GC::Ptr<PrototypeChainValidity>);
PropertyNameIterator(Realm&, GC::Ref<Object>, ObjectPropertyIteratorCacheData&, ObjectPropertyIteratorCache*);
ReadonlySpan<PropertyKey> property_list() const;
ReadonlySpan<Value> property_value_list() const;
bool fast_path_still_valid() const;
void disable_fast_path();
virtual void visit_edges(Visitor&) override;
GC::Ptr<Object> m_object;
Vector<PropertyKey> m_owned_properties;
GC::Ptr<ObjectPropertyIteratorCacheData> m_property_cache;
GC::Ptr<Shape> m_shape;
GC::Ptr<PrototypeChainValidity> m_prototype_chain_validity;
ObjectPropertyIteratorCache* m_iterator_cache_slot { nullptr };
u32 m_indexed_property_count { 0 };
u32 m_next_indexed_property { 0 };
size_t m_next_property { 0 };
bool m_shape_is_dictionary { false };
u32 m_shape_dictionary_generation { 0 };
FastPath m_fast_path { FastPath::None };
};
}

View file

@ -143,7 +143,8 @@ pub fn field_type_info(ty: &str) -> FieldType {
"PropertyLookupCache*"
| "GlobalVariableCache*"
| "TemplateObjectCache*"
| "ObjectShapeCache*" => ("u64", 8, 8, "u64"),
| "ObjectShapeCache*"
| "ObjectPropertyIteratorCache*" => ("u64", 8, 8, "u64"),
_ => unreachable!("Unknown field type: {ty}"),
}
.into()

View file

@ -8,6 +8,7 @@ set(SOURCES
Bytecode/Instruction.cpp
Bytecode/Interpreter.cpp
Bytecode/Label.cpp
Bytecode/PropertyNameIterator.cpp
Bytecode/PropertyKeyTable.cpp
Bytecode/RegexTable.cpp
Bytecode/StringTable.cpp

View file

@ -1450,7 +1450,7 @@ ThrowCompletionOr<void> Object::for_each_own_property_with_enumerability(Functio
bool enumerable;
};
GC::ConservativeVector<OwnKey> keys { heap() };
keys.ensure_capacity(indexed_real_size() + shape().property_count());
keys.ensure_capacity(indexed_real_size() + shape().property_count() + (has_magical_length_property() ? 1 : 0));
{
auto indices = indexed_indices();
@ -1465,6 +1465,9 @@ ThrowCompletionOr<void> Object::for_each_own_property_with_enumerability(Functio
}
}
if (has_magical_length_property())
keys.unchecked_append({ PropertyKey(vm.names.length), false });
for (auto const& [property_key, metadata] : shape().property_table()) {
if (!property_key.is_string())
continue;
@ -1491,7 +1494,7 @@ ThrowCompletionOr<void> Object::for_each_own_property_with_enumerability(Functio
size_t Object::own_properties_count() const
{
return indexed_real_size() + shape().property_table().size();
return indexed_real_size() + shape().property_table().size() + (has_magical_length_property() ? 1 : 0);
}
// Simple side-effect free property lookup, following the prototype chain. Non-standard.

View file

@ -6538,18 +6538,17 @@ fn generate_for_in_statement(
// Create TDZ for lexical declarations before evaluating the RHS expression.
let entered_tdz = enter_for_in_of_head_tdz(generator, lhs);
// Evaluate RHS into `object`, allocate iterator registers, emit the
// null/undefined check + GetObjectPropertyIterator, then let `object`
// go out of scope so its register is freed before the loop body.
let (iterator_object, iterator_next_method, iterator_done) = {
// Evaluate RHS into `object`, allocate the internal property iterator
// register, emit the null/undefined check + GetObjectPropertyIterator,
// then let `object` go out of scope so its register is freed before the
// loop body.
let iterator_object = {
let object = generate_expression_or_undefined(rhs, generator, None);
if entered_tdz {
leave_for_in_of_head_tdz(generator);
}
let iterator_object = generator.allocate_register();
let iterator_next_method = generator.allocate_register();
let iterator_done = generator.allocate_register();
// Check for null/undefined
let nullish_block = generator.make_block();
@ -6566,14 +6565,14 @@ fn generate_for_in_statement(
generator.switch_to_basic_block(continue_block);
// Get property iterator
let cache = generator.next_object_property_iterator_cache();
generator.emit(Instruction::GetObjectPropertyIterator {
dst_iterator_object: iterator_object.operand(),
dst_iterator_next: iterator_next_method.operand(),
dst_iterator_done: iterator_done.operand(),
dst_iterator: iterator_object.operand(),
object: object.operand(),
cache: cache as u64,
});
(iterator_object, iterator_next_method, iterator_done)
iterator_object
};
// Body evaluation: completion, then jump to update block.
let completion = generator.allocate_completion_register();
@ -6586,12 +6585,10 @@ fn generate_for_in_statement(
generator.switch_to_basic_block(update_block);
let next_value = generator.allocate_register();
let done = generator.allocate_register();
generator.emit(Instruction::IteratorNextUnpack {
generator.emit(Instruction::ObjectPropertyIteratorNext {
dst_value: next_value.operand(),
dst_done: done.operand(),
iterator_object: iterator_object.operand(),
iterator_next: iterator_next_method.operand(),
iterator_done: iterator_done.operand(),
});
let loop_continue_block = generator.make_block();

View file

@ -189,6 +189,7 @@ pub struct FFIExecutableData {
pub global_variable_cache_count: u32,
pub template_object_cache_count: u32,
pub object_shape_cache_count: u32,
pub object_property_iterator_cache_count: u32,
pub number_of_registers: u32,
pub is_strict: bool,
pub length_identifier: FFIOptionalU32,
@ -562,6 +563,7 @@ pub unsafe fn create_executable(
global_variable_cache_count: generator.next_global_variable_cache,
template_object_cache_count: generator.next_template_object_cache,
object_shape_cache_count: generator.next_object_shape_cache,
object_property_iterator_cache_count: generator.next_object_property_iterator_cache,
number_of_registers: assembled.number_of_registers,
is_strict: generator.strict,
length_identifier: FFIOptionalU32::from(

View file

@ -172,6 +172,7 @@ pub struct Generator {
pub next_global_variable_cache: u32,
pub next_template_object_cache: u32,
pub next_object_shape_cache: u32,
pub next_object_property_iterator_cache: u32,
// --- Codegen state ---
pub strict: bool,
@ -316,6 +317,7 @@ impl Generator {
next_global_variable_cache: 0,
next_template_object_cache: 0,
next_object_shape_cache: 0,
next_object_property_iterator_cache: 0,
strict: false,
function_environment_needed: true,
enclosing_function_kind: FunctionKind::Normal,
@ -815,6 +817,10 @@ impl Generator {
next_cache_method!(next_global_variable_cache, next_global_variable_cache);
next_cache_method!(next_template_object_cache, next_template_object_cache);
next_cache_method!(next_object_shape_cache, next_object_shape_cache);
next_cache_method!(
next_object_property_iterator_cache,
next_object_property_iterator_cache
);
// --- Lexical environment helpers ---

View file

@ -768,6 +768,7 @@ extern "C" void* rust_create_executable(
data->global_variable_cache_count,
data->template_object_cache_count,
data->object_shape_cache_count,
data->object_property_iterator_cache_count,
data->number_of_registers,
data->is_strict ? JS::Strict::Yes : JS::Strict::No);

View file

@ -361,6 +361,7 @@ CACHE_POINTER_TYPES = {
"GlobalVariableCache*": "global_variable_caches",
"TemplateObjectCache*": "template_object_caches",
"ObjectShapeCache*": "object_shape_caches",
"ObjectPropertyIteratorCache*": "object_property_iterator_caches",
}
@ -608,7 +609,8 @@ def generate_fixup_cache_function(ops: List[OpDef]) -> str:
lines.append(" Span<PropertyLookupCache> property_lookup_caches,")
lines.append(" Span<GlobalVariableCache> global_variable_caches,")
lines.append(" Span<TemplateObjectCache> template_object_caches,")
lines.append(" Span<ObjectShapeCache> object_shape_caches)")
lines.append(" Span<ObjectShapeCache> object_shape_caches,")
lines.append(" Span<ObjectPropertyIteratorCache> object_property_iterator_caches)")
lines.append("{")
lines.append(' // Sentinel value used to indicate "no cache" (originally u32::MAX).')
lines.append(" static constexpr u64 NO_CACHE = NumericLimits<u32>::max();")
@ -704,7 +706,8 @@ void fixup_instruction_cache(
Span<PropertyLookupCache> property_lookup_caches,
Span<GlobalVariableCache> global_variable_caches,
Span<TemplateObjectCache> template_object_caches,
Span<ObjectShapeCache> object_shape_caches);
Span<ObjectShapeCache> object_shape_caches,
Span<ObjectPropertyIteratorCache> object_property_iterator_caches);
} // namespace JS::Bytecode
"""

View file

@ -13,7 +13,7 @@ block0:
foo$ad9db83a for-in-block-order.js:2:16
Registers: 10
Registers: 8
Blocks: 6
Locals: k~0
Constants:
@ -28,16 +28,16 @@ block1:
[ 28] End value:Undefined
block2:
[ 30] IteratorNextUnpack dst_value:reg8, dst_done:reg9, iterator_object:reg5, iterator_next:reg6, iterator_done:reg7
[ 48] JumpIf condition:reg9, true_target:block1, false_target:block5
[ 30] ObjectPropertyIteratorNext dst_value:reg6, dst_done:reg7, iterator_object:reg5
[ 40] JumpIf condition:reg7, true_target:block1, false_target:block5
block3:
[ 58] Jump target:block1
[ 50] Jump target:block1
block4:
[ 60] GetObjectPropertyIterator dst_iterator_object:reg5, dst_iterator_next:reg6, dst_iterator_done:reg7, object:arg0
[ 78] Jump target:block2
[ 58] GetObjectPropertyIterator dst_iterator:reg5, object:arg0
[ 70] Jump target:block2
block5:
[ 80] Mov dst:k~0, src:reg8
[ 90] Jump target:block2
[ 78] Mov dst:k~0, src:reg6
[ 88] Jump target:block2

View file

@ -1,5 +1,5 @@
$6f3aad7a for-in-lexical-scope.js:1:1
Registers: 14
Registers: 12
Blocks: 6
Constants:
[0] = Int32(0)
@ -19,27 +19,27 @@ block1:
[ 78] End value:reg6
block2:
[ 80] IteratorNextUnpack dst_value:reg9, dst_done:reg10, iterator_object:reg5, iterator_next:reg7, iterator_done:reg8
[ 98] JumpIf condition:reg10, true_target:block1, false_target:block5
[ 80] ObjectPropertyIteratorNext dst_value:reg7, dst_done:reg8, iterator_object:reg5
[ 90] JumpIf condition:reg8, true_target:block1, false_target:block5
block3:
[ a8] End value:reg6
[ a0] End value:reg6
block4:
[ b0] GetObjectPropertyIterator dst_iterator_object:reg5, dst_iterator_next:reg7, dst_iterator_done:reg8, object:reg6
[ c8] Mov dst:reg6, src:Undefined
[ d8] Jump target:block2
[ a8] GetObjectPropertyIterator dst_iterator:reg5, object:reg6
[ c0] Mov dst:reg6, src:Undefined
[ d0] Jump target:block2
block5:
[ e0] CreateLexicalEnvironment dst:reg11, parent:reg4, capacity:0
[ f0] CreateVariable `x`, is_immutable:false, is_global:false, is_strict:false
[ 100] InitializeLexicalBinding `x`, src:reg9
[ 118] CreateLexicalEnvironment dst:reg12, parent:reg11, capacity:0
[ 128] CreateMutableBinding environment:reg12, `probeDecl`, can_be_deleted:false
[ 138] NewFunction dst:reg13, shared_function_data_index:0
[ 150] InitializeLexicalBinding `probeDecl`, src:reg13
[ 168] GetBinding dst:reg13, `probeDecl`
[ 180] SetVariableBinding `probeDecl`, src:reg13
[ 198] SetLexicalEnvironment environment:reg11
[ 1a0] SetLexicalEnvironment environment:reg4
[ 1a8] Jump target:block2
[ d8] CreateLexicalEnvironment dst:reg9, parent:reg4, capacity:0
[ e8] CreateVariable `x`, is_immutable:false, is_global:false, is_strict:false
[ f8] InitializeLexicalBinding `x`, src:reg7
[ 110] CreateLexicalEnvironment dst:reg10, parent:reg9, capacity:0
[ 120] CreateMutableBinding environment:reg10, `probeDecl`, can_be_deleted:false
[ 130] NewFunction dst:reg11, shared_function_data_index:0
[ 148] InitializeLexicalBinding `probeDecl`, src:reg11
[ 160] GetBinding dst:reg11, `probeDecl`
[ 178] SetVariableBinding `probeDecl`, src:reg11
[ 190] SetLexicalEnvironment environment:reg9
[ 198] SetLexicalEnvironment environment:reg4
[ 1a0] Jump target:block2

View file

@ -0,0 +1,56 @@
$5af00ff4 for-in-object-property-iterator-next.js:8:1
Registers: 8
Blocks: 1
Constants:
[0] = Undefined
[1] = Int32(2)
[2] = String("two")
[3] = String("foo")
[4] = Int32(7)
[5] = String("seven")
block0:
[ 0] GetLexicalEnvironment dst:reg4
[ 8] GetGlobal dst:reg6, `collect`
[ 20] NewObject dst:reg7
[ 30] ToPrimitiveWithStringHint dst:Int32(2), value:Int32(2)
[ 40] PutByValue base:reg7, property:Int32(2), src:String("two"), kind:Own
[ 58] PutById base:reg7, `foo`, src:String("foo"), kind:Own
[ 80] ToPrimitiveWithStringHint dst:Int32(7), value:Int32(7)
[ 90] PutByValue base:reg7, property:Int32(7), src:String("seven"), kind:Own
[ a8] Call dst:reg5, callee:reg6, this_value:Undefined, collect, arguments:[reg7]
[ d0] End value:reg5
collect$4195dbe6 for-in-object-property-iterator-next.js:2:18
Registers: 12
Blocks: 6
Locals: key~0, keys~1
block0:
[ 0] GetLexicalEnvironment dst:reg4
[ 8] NewArray dst:keys~1
[ 18] JumpNullish condition:arg0, true_target:block3, false_target:block4
block1:
[ 28] Return value:keys~1
block2:
[ 30] ObjectPropertyIteratorNext dst_value:reg6, dst_done:reg7, iterator_object:reg5
[ 40] JumpIf condition:reg7, true_target:block1, false_target:block5
block3:
[ 50] Return value:keys~1
block4:
[ 58] GetObjectPropertyIterator dst_iterator:reg5, object:arg0
[ 70] Jump target:block2
block5:
[ 78] Mov dst:key~0, src:reg6
[ 88] GetById dst:reg9, base:keys~1, `push` (keys.push)
[ a8] Mov dst:reg10, src:keys~1
[ b8] ThrowIfTDZ src:key~0
[ c0] Mov dst:reg11, src:key~0
[ d0] Call dst:reg8, callee:reg9, this_value:reg10, keys.push, arguments:[reg11]
[ f8] Jump target:block2

View file

@ -1,5 +1,5 @@
$0d42cfa1 for-in-register-lifetime.js:1:1
Registers: 16
Registers: 14
Blocks: 6
Constants:
[0] = String("")
@ -24,26 +24,26 @@ block1:
[ c8] End value:reg5
block2:
[ d0] IteratorNextUnpack dst_value:reg9, dst_done:reg10, iterator_object:reg6, iterator_next:reg7, iterator_done:reg8
[ e8] JumpIf condition:reg10, true_target:block1, false_target:block5
[ d0] ObjectPropertyIteratorNext dst_value:reg7, dst_done:reg8, iterator_object:reg6
[ e0] JumpIf condition:reg8, true_target:block1, false_target:block5
block3:
[ f8] End value:reg5
[ f0] End value:reg5
block4:
[ 100] GetObjectPropertyIterator dst_iterator_object:reg6, dst_iterator_next:reg7, dst_iterator_done:reg8, object:reg5
[ 118] Mov dst:reg5, src:Undefined
[ 128] Jump target:block2
[ f8] GetObjectPropertyIterator dst_iterator:reg6, object:reg5
[ 110] Mov dst:reg5, src:Undefined
[ 120] Jump target:block2
block5:
[ 130] SetGlobal `k`, src:reg9
[ 148] GetGlobal dst:reg11, `result`
[ 160] GetGlobal dst:reg12, `k`
[ 178] GetGlobal dst:reg13, `obj`
[ 190] GetGlobal dst:reg14, `k`
[ 1a8] GetByValue dst:reg15, base:reg13, property:reg14 (obj[reg14])
[ 1c0] Add dst:reg13, lhs:reg12, rhs:reg15
[ 1d0] Add dst:reg12, lhs:reg11, rhs:reg13
[ 1e0] SetGlobal `result`, src:reg12
[ 1f8] Mov dst:reg5, src:reg12
[ 208] Jump target:block2
[ 128] SetGlobal `k`, src:reg7
[ 140] GetGlobal dst:reg9, `result`
[ 158] GetGlobal dst:reg10, `k`
[ 170] GetGlobal dst:reg11, `obj`
[ 188] GetGlobal dst:reg12, `k`
[ 1a0] GetByValue dst:reg13, base:reg11, property:reg12 (obj[reg12])
[ 1b8] Add dst:reg11, lhs:reg10, rhs:reg13
[ 1c8] Add dst:reg10, lhs:reg9, rhs:reg11
[ 1d8] SetGlobal `result`, src:reg10
[ 1f0] Mov dst:reg5, src:reg10
[ 200] Jump target:block2

View file

@ -12,7 +12,7 @@ block0:
fn$a115675f for-in-var-function-lhs-name.js:4:19
Registers: 10
Registers: 8
Blocks: 6
Locals: f~0
Constants:
@ -30,16 +30,16 @@ block1:
[ 60] End value:Undefined
block2:
[ 68] IteratorNextUnpack dst_value:reg5, dst_done:reg9, iterator_object:reg6, iterator_next:reg7, iterator_done:reg8
[ 80] JumpIf condition:reg9, true_target:block1, false_target:block5
[ 68] ObjectPropertyIteratorNext dst_value:reg5, dst_done:reg7, iterator_object:reg6
[ 78] JumpIf condition:reg7, true_target:block1, false_target:block5
block3:
[ 90] Jump target:block1
[ 88] Jump target:block1
block4:
[ 98] GetObjectPropertyIterator dst_iterator_object:reg6, dst_iterator_next:reg7, dst_iterator_done:reg8, object:reg5
[ b0] Jump target:block2
[ 90] GetObjectPropertyIterator dst_iterator:reg6, object:reg5
[ a8] Jump target:block2
block5:
[ b8] Mov dst:f~0, src:reg5
[ c8] Jump target:block2
[ b0] Mov dst:f~0, src:reg5
[ c0] Jump target:block2

View file

@ -1,5 +1,5 @@
$fa0f7b9b invalid-lhs-forin-no-dead-code.js:1:1
Registers: 13
Registers: 11
Blocks: 6
Constants:
[0] = Undefined
@ -13,19 +13,19 @@ block1:
[ 28] End value:reg5
block2:
[ 30] IteratorNextUnpack dst_value:reg9, dst_done:reg10, iterator_object:reg6, iterator_next:reg7, iterator_done:reg8
[ 48] JumpIf condition:reg10, true_target:block1, false_target:block5
[ 30] ObjectPropertyIteratorNext dst_value:reg7, dst_done:reg8, iterator_object:reg6
[ 40] JumpIf condition:reg8, true_target:block1, false_target:block5
block3:
[ 58] End value:reg5
[ 50] End value:reg5
block4:
[ 60] GetObjectPropertyIterator dst_iterator_object:reg6, dst_iterator_next:reg7, dst_iterator_done:reg8, object:reg5
[ 78] Mov dst:reg5, src:Undefined
[ 88] Jump target:block2
[ 58] GetObjectPropertyIterator dst_iterator:reg6, object:reg5
[ 70] Mov dst:reg5, src:Undefined
[ 80] Jump target:block2
block5:
[ 90] GetGlobal dst:reg12, `f`
[ a8] Call dst:reg11, callee:reg12, this_value:Undefined, f
[ c8] NewReferenceError dst:reg11, Invalid left-hand side in assignment
[ d8] Throw src:reg11
[ 88] GetGlobal dst:reg10, `f`
[ a0] Call dst:reg9, callee:reg10, this_value:Undefined, f
[ c0] NewReferenceError dst:reg9, Invalid left-hand side in assignment
[ d0] Throw src:reg9

View file

@ -86,7 +86,7 @@ block0:
forInTeardown$e8d7ea8e lexical-env-teardown.js:28:5
Registers: 10
Registers: 8
Blocks: 6
Locals: k~0, outer~1
Constants:
@ -107,20 +107,20 @@ block1:
[ 78] Return value:outer~1
block2:
[ 80] IteratorNextUnpack dst_value:reg5, dst_done:reg9, iterator_object:reg6, iterator_next:reg7, iterator_done:reg8
[ 98] JumpIf condition:reg9, true_target:block1, false_target:block5
[ 80] ObjectPropertyIteratorNext dst_value:reg5, dst_done:reg7, iterator_object:reg6
[ 90] JumpIf condition:reg7, true_target:block1, false_target:block5
block3:
[ a8] Return value:outer~1
[ a0] Return value:outer~1
block4:
[ b0] GetObjectPropertyIterator dst_iterator_object:reg6, dst_iterator_next:reg7, dst_iterator_done:reg8, object:reg5
[ c8] Jump target:block2
[ a8] GetObjectPropertyIterator dst_iterator:reg6, object:reg5
[ c0] Jump target:block2
block5:
[ d0] Mov dst:k~0, src:reg5
[ e0] ThrowIfTDZ src:k~0
[ e8] Jump target:block2
[ c8] Mov dst:k~0, src:reg5
[ d8] ThrowIfTDZ src:k~0
[ e0] Jump target:block2
forOfTeardown$b603f5d2 lexical-env-teardown.js:38:5

View file

@ -0,0 +1,8 @@
function collect(object) {
const keys = [];
for (const key in object)
keys.push(key);
return keys;
}
collect({ 2: "two", foo: "foo", 7: "seven" });

View file

@ -30,6 +30,101 @@ test("iterate through string", () => {
expect(a).toEqual(["0", "1", "2", "3", "4"]);
});
test("iterate through typed array", () => {
const a = [];
for (const property in new Uint8Array([1, 2])) {
a.push(property);
}
expect(a).toEqual(["0", "1"]);
});
test("array magical length shadows enumerable length higher in prototype chain", () => {
const tail = { length: 1 };
const array_prototype = [];
Object.setPrototypeOf(array_prototype, tail);
const array = [1, 2];
Object.setPrototypeOf(array, array_prototype);
const keys = [];
for (const key in array) {
keys.push(key);
}
expect(keys).toEqual(["0", "1"]);
});
test("array in prototype chain shadows enumerable length higher up", () => {
const tail = { length: 1 };
const array_prototype = [];
Object.setPrototypeOf(array_prototype, tail);
const object = Object.create(array_prototype);
object.foo = 1;
const keys = [];
for (const key in object) {
keys.push(key);
}
expect(keys).toEqual(["foo"]);
});
test("array magical length still shadows enumerable length on slow path", () => {
const tail = { length: 1 };
const proxy = new Proxy(Object.create(tail), {});
const array = [1, 2];
Object.setPrototypeOf(array, proxy);
const keys = [];
for (const key in array) {
keys.push(key);
}
expect(keys).toEqual(["0", "1"]);
});
test("same for-in site distinguishes arrays from plain objects with same prototype", () => {
function collect(object) {
const keys = [];
for (const key in object) keys.push(key);
return keys;
}
const proto = Object.create({ length: 1, z: 2 });
const array = [];
Object.setPrototypeOf(array, proto);
const object = {};
Object.setPrototypeOf(object, proto);
expect(collect(array)).toEqual(["z"]);
expect(collect(object)).toEqual(["length", "z"]);
expect(collect(array)).toEqual(["z"]);
});
test("same for-in site distinguishes plain objects from arrays with same prototype", () => {
function collect(object) {
const keys = [];
for (const key in object) keys.push(key);
return keys;
}
const proto = Object.create({ length: 1, z: 2 });
const array = [];
Object.setPrototypeOf(array, proto);
const object = {};
Object.setPrototypeOf(object, proto);
expect(collect(object)).toEqual(["length", "z"]);
expect(collect(array)).toEqual(["z"]);
expect(collect(object)).toEqual(["length", "z"]);
});
test("iterate through object", () => {
const a = [];
for (const property in { a: 1, b: 2, c: 2 }) {
@ -38,6 +133,22 @@ test("iterate through object", () => {
expect(a).toEqual(["a", "b", "c"]);
});
test("iterate through object with numeric-looking and named properties", () => {
const object = {};
object[7] = "seven";
object.foo = "foo";
object[2] = "two";
object.bar = "bar";
object[9] = "nine";
const keys = [];
for (const key in object) {
keys.push(key);
}
expect(keys).toEqual(["2", "7", "9", "foo", "bar"]);
});
test("iterate through undefined", () => {
for (const property in undefined) {
expect.fail();
@ -118,6 +229,44 @@ test("remove properties while iterating", () => {
expect(to).toEqual(["0", "1"]);
});
test("delete future packed index while iterating", () => {
const from = [1, 2, 3];
const to = [];
for (const prop in from) {
to.push(prop);
if (prop === "0") delete from[1];
}
expect(to).toEqual(["0", "2"]);
});
test("iterate through holey array", () => {
const from = [1, 2, 3];
delete from[1];
const to = [];
for (const prop in from) {
to.push(prop);
}
expect(to).toEqual(["0", "2"]);
});
test("iterate through sparse array", () => {
const from = [];
from[1] = 2;
from[100] = 3;
from.foo = 4;
const to = [];
for (const prop in from) {
to.push(prop);
}
expect(to).toEqual(["1", "100", "foo"]);
});
test("duplicated properties in prototype", () => {
const object = { a: 1 };
const proto = { a: 2 };
@ -128,3 +277,408 @@ test("duplicated properties in prototype", () => {
}
expect(a).toEqual(["a"]);
});
test("delete future own named property while iterating", () => {
const object = { a: 1, b: 2, c: 3 };
const keys = [];
for (const key in object) {
keys.push(key);
if (key === "a") delete object.b;
}
expect(keys).toEqual(["a", "c"]);
});
test("delete future prototype named property while iterating", () => {
const proto = { a: 1, b: 2 };
const object = Object.create(proto);
const keys = [];
for (const key in object) {
keys.push(key);
if (key === "a") delete proto.b;
}
expect(keys).toEqual(["a"]);
});
test("delete future own dictionary property while iterating", () => {
const object = {};
for (let i = 0; i < 70; i++) object["p" + i] = i;
const keys = [];
for (const key in object) {
keys.push(key);
if (key === "p0") delete object.p1;
if (key === "p2") delete object.p10;
}
expect(keys).not.toContain("p1");
expect(keys).not.toContain("p10");
expect(keys).toHaveLength(68);
});
test("delete future prototype dictionary property while iterating", () => {
const proto = {};
for (let i = 0; i < 70; i++) proto["p" + i] = i;
const object = Object.create(proto);
const keys = [];
for (const key in object) {
keys.push(key);
if (key === "p0") delete proto.p1;
if (key === "p2") delete proto.p10;
}
expect(keys).not.toContain("p1");
expect(keys).not.toContain("p10");
expect(keys).toHaveLength(68);
});
test("packed indices stay ahead of named properties", () => {
const array = [1, 2];
array.foo = 3;
array.bar = 4;
const keys = [];
for (const key in array) keys.push(key);
expect(keys).toEqual(["0", "1", "foo", "bar"]);
});
test("delete future own named property while iterating packed indices", () => {
const array = [1, 2];
array.foo = 3;
array.bar = 4;
const keys = [];
for (const key in array) {
keys.push(key);
if (key === "0") delete array.foo;
}
expect(keys).toEqual(["0", "1", "bar"]);
});
test("delete future prototype named property while iterating packed indices", () => {
const proto = Object.create(Array.prototype);
proto.foo = 1;
proto.bar = 2;
const array = [1, 2];
Object.setPrototypeOf(array, proto);
const keys = [];
for (const key in array) {
keys.push(key);
if (key === "0") delete proto.foo;
}
expect(keys).toEqual(["0", "1", "bar"]);
});
test("repeated for-in sees own named properties added between runs", () => {
function collect_twice(object, between_runs) {
const runs = [];
for (let pass = 0; pass < 2; ++pass) {
const keys = [];
for (const key in object) keys.push(key);
runs.push(keys);
if (pass === 0) between_runs();
}
return runs;
}
const object = { a: 1 };
const runs = collect_twice(object, () => {
object.b = 2;
});
expect(runs).toEqual([["a"], ["a", "b"]]);
});
test("repeated for-in sees prototype named properties added between runs", () => {
function collect_twice(object, between_runs) {
const runs = [];
for (let pass = 0; pass < 2; ++pass) {
const keys = [];
for (const key in object) keys.push(key);
runs.push(keys);
if (pass === 0) between_runs();
}
return runs;
}
const proto = {};
const object = Object.create(proto);
const runs = collect_twice(object, () => {
proto.foo = 1;
});
expect(runs).toEqual([[], ["foo"]]);
});
test("repeated for-in sees indexed receiver properties added between runs", () => {
function collect_twice(object, between_runs) {
const runs = [];
for (let pass = 0; pass < 2; ++pass) {
const keys = [];
for (const key in object) keys.push(key);
runs.push(keys);
if (pass === 0) between_runs();
}
return runs;
}
const object = { foo: 1 };
const runs = collect_twice(object, () => {
object[0] = 2;
});
expect(runs).toEqual([["foo"], ["0", "foo"]]);
});
test("repeated for-in sees indexed prototype properties added between runs", () => {
function collect_twice(object, between_runs) {
const runs = [];
for (let pass = 0; pass < 2; ++pass) {
const keys = [];
for (const key in object) keys.push(key);
runs.push(keys);
if (pass === 0) between_runs();
}
return runs;
}
const proto = {};
const object = Object.create(proto);
const runs = collect_twice(object, () => {
proto[0] = 1;
});
expect(runs).toEqual([[], ["0"]]);
});
test("repeated for-in sees packed receivers grow between runs", () => {
function collect_twice(object, between_runs) {
const runs = [];
for (let pass = 0; pass < 2; ++pass) {
const keys = [];
for (const key in object) keys.push(key);
runs.push(keys);
if (pass === 0) between_runs();
}
return runs;
}
const array = [1, 2];
const runs = collect_twice(array, () => {
array[2] = 3;
});
expect(runs).toEqual([
["0", "1"],
["0", "1", "2"],
]);
});
test("repeated for-in sees packed receivers become holey between runs", () => {
function collect_twice(object, between_runs) {
const runs = [];
for (let pass = 0; pass < 2; ++pass) {
const keys = [];
for (const key in object) keys.push(key);
runs.push(keys);
if (pass === 0) between_runs();
}
return runs;
}
const array = [1, 2, 3];
const runs = collect_twice(array, () => {
delete array[1];
});
expect(runs).toEqual([
["0", "1", "2"],
["0", "2"],
]);
});
test("recursive for-in re-entry keeps the active enumeration stable", () => {
function collect(object, nested_object) {
const keys = [];
for (const key in object) {
keys.push(key);
if (nested_object && key === "a") keys.push(...collect(nested_object));
}
return keys;
}
const outer = { a: 1, b: 2, c: 3 };
const inner = [1, 2];
expect(collect(outer, inner)).toEqual(["a", "0", "1", "b", "c"]);
});
test("repeated for-in after break still sees the full next enumeration", () => {
function collect_twice(object) {
const runs = [];
for (let pass = 0; pass < 2; ++pass) {
const keys = [];
for (const key in object) {
keys.push(key);
if (pass === 0) break;
}
runs.push(keys);
}
return runs;
}
expect(collect_twice({ a: 1, b: 2, c: 3 })).toEqual([["a"], ["a", "b", "c"]]);
});
test("repeated for-in after full named completion still sees the full next enumeration", () => {
function collect_twice(object) {
const runs = [];
for (let pass = 0; pass < 2; ++pass) {
const keys = [];
for (const key in object) keys.push(key);
runs.push(keys);
}
return runs;
}
expect(collect_twice({ a: 1, b: 2, c: 3 })).toEqual([
["a", "b", "c"],
["a", "b", "c"],
]);
});
test("completed cached for-in does not keep the last receiver alive", () => {
function exhaust(object) {
for (const key in object) {
}
}
function exhaust_and_drop_receiver() {
let receiver = { a: 1, b: 2, c: 3 };
let weak_ref = new WeakRef(receiver);
exhaust(receiver);
return weak_ref;
}
let weak_ref = exhaust_and_drop_receiver();
gc();
expect(weak_ref.deref()).toBeUndefined();
});
test("repeated for-in after full packed completion still sees the full next enumeration", () => {
function collect_twice(object) {
const runs = [];
for (let pass = 0; pass < 2; ++pass) {
const keys = [];
for (const key in object) keys.push(key);
runs.push(keys);
}
return runs;
}
const array = [1, 2];
array.foo = 3;
array.bar = 4;
expect(collect_twice(array)).toEqual([
["0", "1", "foo", "bar"],
["0", "1", "foo", "bar"],
]);
});
test("repeated for-in after full mixed numeric-looking and named completion stays stable", () => {
function collect_twice(object) {
const runs = [];
for (let pass = 0; pass < 2; ++pass) {
const keys = [];
for (const key in object) keys.push(key);
runs.push(keys);
}
return runs;
}
const object = {};
object[7] = "seven";
object.foo = "foo";
object[2] = "two";
object.bar = "bar";
object[9] = "nine";
expect(collect_twice(object)).toEqual([
["2", "7", "9", "foo", "bar"],
["2", "7", "9", "foo", "bar"],
]);
});
test("repeated empty for-in stays empty", () => {
function collect_twice(object) {
const runs = [];
for (let pass = 0; pass < 2; ++pass) {
const keys = [];
for (const key in object) keys.push(key);
runs.push(keys);
}
return runs;
}
expect(collect_twice({})).toEqual([[], []]);
});
test("shrink packed length while iterating", () => {
const array = [1, 2, 3];
const keys = [];
for (const key in array) {
keys.push(key);
if (key === "0") array.length = 1;
}
expect(keys).toEqual(["0"]);
});
test("indexed properties on prototype are still enumerated", () => {
const proto = [1, 2];
proto.foo = 3;
const object = Object.create(proto);
const keys = [];
for (const key in object) {
keys.push(key);
}
expect(keys).toEqual(["0", "1", "foo"]);
});
test("proxy in prototype chain is still enumerated", () => {
const proxy = new Proxy({ a: 1, b: 2 }, {});
const object = Object.create(proxy);
const keys = [];
for (const key in object) {
keys.push(key);
if (key === "a") delete proxy.b;
}
expect(keys).toEqual(["a"]);
});

View file

@ -148,6 +148,16 @@ describe("in- and exports", () => {
expect(result).toHaveProperty("namedVarValue", 3 + 3);
});
test("for-in can enumerate module namespace objects", () => {
const result = expectModulePassed("./basic-export-types.mjs");
const keys = [];
for (const key in result) {
keys.push(key);
}
expect(keys).toEqual(Object.keys(result));
});
test("default exports", () => {
const result = expectModulePassed("./module-with-default.mjs");
expect(result).toHaveProperty("defaultValue");