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:
parent
114eeddea1
commit
879ac36e45
27 changed files with 1453 additions and 157 deletions
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
196
Libraries/LibJS/Bytecode/PropertyNameIterator.cpp
Normal file
196
Libraries/LibJS/Bytecode/PropertyNameIterator.cpp
Normal 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);
|
||||
}
|
||||
|
||||
}
|
||||
61
Libraries/LibJS/Bytecode/PropertyNameIterator.h
Normal file
61
Libraries/LibJS/Bytecode/PropertyNameIterator.h
Normal 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 };
|
||||
};
|
||||
|
||||
}
|
||||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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 ---
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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" });
|
||||
|
|
@ -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"]);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
Loading…
Reference in a new issue