LibJS: Allow advancing built-in iterators without result object creation

Expose a method on built-in iterators that allows retrieving the next
iteration result without allocating a JS::Object. This change is a
preparation for optimizing for..of and for..in loops.
This commit is contained in:
Aliaksandr Kalenik 2025-04-30 16:29:41 +03:00 committed by Andreas Kling
parent c72d5943e6
commit ab52d86a69
Notes: github-actions[bot] 2025-04-30 18:52:44 +00:00
15 changed files with 234 additions and 169 deletions

View file

@ -158,6 +158,7 @@ class Agent;
struct AsyncGeneratorRequest; struct AsyncGeneratorRequest;
class BigInt; class BigInt;
class BoundFunction; class BoundFunction;
class BuiltinIterator;
struct CachedSourceRange; struct CachedSourceRange;
class Cell; class Cell;
class ClassExpression; class ClassExpression;

View file

@ -4,8 +4,9 @@
* SPDX-License-Identifier: BSD-2-Clause * SPDX-License-Identifier: BSD-2-Clause
*/ */
#include <LibJS/Runtime/Array.h>
#include <LibJS/Runtime/ArrayIterator.h> #include <LibJS/Runtime/ArrayIterator.h>
#include <LibJS/Runtime/GlobalObject.h> #include <LibJS/Runtime/TypedArray.h>
namespace JS { namespace JS {
@ -35,4 +36,108 @@ void ArrayIterator::visit_edges(Cell::Visitor& visitor)
visitor.visit(m_array); visitor.visit(m_array);
} }
ThrowCompletionOr<void> ArrayIterator::next(VM& vm, bool& done, Value& value)
{
// 1. Let O be the this value.
// 2. If O is not an Object, throw a TypeError exception.
// 3. If O does not have all of the internal slots of an Array Iterator Instance (23.1.5.3), throw a TypeError exception.
// 4. Let array be O.[[IteratedArrayLike]].
auto target_array = m_array;
// 5. If array is undefined, return CreateIteratorResultObject(undefined, true).
if (target_array.is_undefined()) {
value = js_undefined();
done = true;
return {};
}
VERIFY(target_array.is_object());
auto& array = target_array.as_object();
// 6. Let index be O.[[ArrayLikeNextIndex]].
auto index = m_index;
// 7. Let kind be O.[[ArrayLikeIterationKind]].
auto kind = m_iteration_kind;
size_t length = 0;
// 8. If array has a [[TypedArrayName]] internal slot, then
if (array.is_typed_array()) {
auto& typed_array = static_cast<TypedArrayBase&>(array);
// a. Let taRecord be MakeTypedArrayWithBufferWitnessRecord(array, SEQ-CST).
auto typed_array_record = make_typed_array_with_buffer_witness_record(typed_array, ArrayBuffer::SeqCst);
// b. If IsTypedArrayOutOfBounds(taRecord) is true, throw a TypeError exception.
if (is_typed_array_out_of_bounds(typed_array_record))
return vm.throw_completion<TypeError>(ErrorType::BufferOutOfBounds, "TypedArray"sv);
// c. Let len be TypedArrayLength(taRecord).
length = typed_array_length(typed_array_record);
}
// 9. Else,
else {
// a. Let len be ? LengthOfArrayLike(array).
length = TRY(length_of_array_like(vm, array));
}
// 10. If index ≥ len, then
if (index >= length) {
// a. Set O.[[IteratedArrayLike]] to undefined.
m_array = js_undefined();
// b. Return CreateIteratorResultObject(undefined, true).
value = js_undefined();
done = true;
return {};
}
// 11. Set O.[[ArrayLikeNextIndex]] to index + 1.
m_index++;
// 12. Let indexNumber be 𝔽(index).
Value result;
// 13. If kind is KEY, then
if (kind == PropertyKind::Key) {
// a. Let result be indexNumber.
result = Value { static_cast<i32>(index) };
}
// 14. Else,
else {
// a. Let elementKey be ! ToString(indexNumber).
// b. Let elementValue be ? Get(array, elementKey).
auto element_value = TRY([&]() -> ThrowCompletionOr<Value> {
// OPTIMIZATION: For objects that don't interfere with indexed property access, we try looking directly at storage.
if (!array.may_interfere_with_indexed_property_access() && array.indexed_properties().has_index(index)) {
if (auto value = array.indexed_properties().get(index)->value; !value.is_accessor())
return value;
}
return array.get(index);
}());
// c. If kind is VALUE, then
if (kind == PropertyKind::Value) {
// i. Let result be elementValue.
result = element_value;
}
// d. Else,
else {
// i. Assert: kind is KEY+VALUE.
VERIFY(kind == PropertyKind::KeyAndValue);
// ii. Let result be CreateArrayFromList(« indexNumber, elementValue »).
result = Array::create_from(*vm.current_realm(), { Value(static_cast<i32>(index)), element_value });
}
}
// 15. Return CreateIteratorResultObject(result, false).
value = result;
return {};
}
} }

View file

@ -6,11 +6,13 @@
#pragma once #pragma once
#include <LibJS/Runtime/Iterator.h>
#include <LibJS/Runtime/Object.h> #include <LibJS/Runtime/Object.h>
namespace JS { namespace JS {
class ArrayIterator final : public Object { class ArrayIterator final : public Object
, public BuiltinIterator {
JS_OBJECT(ArrayIterator, Object); JS_OBJECT(ArrayIterator, Object);
GC_DECLARE_ALLOCATOR(ArrayIterator); GC_DECLARE_ALLOCATOR(ArrayIterator);
@ -19,13 +21,8 @@ public:
virtual ~ArrayIterator() override = default; virtual ~ArrayIterator() override = default;
Value array() const { return m_array; } BuiltinIterator* as_builtin_iterator() override { return this; }
void set_array(Value array) { m_array = array; } ThrowCompletionOr<void> next(VM&, bool& done, Value& value) override;
Object::PropertyKind iteration_kind() const { return m_iteration_kind; }
size_t index() const { return m_index; }
void set_index(size_t index) { m_index = index; }
private: private:
ArrayIterator(Value array, Object::PropertyKind iteration_kind, Object& prototype); ArrayIterator(Value array, Object::PropertyKind iteration_kind, Object& prototype);

View file

@ -35,103 +35,13 @@ void ArrayIteratorPrototype::initialize(Realm& realm)
// 23.1.5.2.1 %ArrayIteratorPrototype%.next ( ), https://tc39.es/ecma262/#sec-%arrayiteratorprototype%.next // 23.1.5.2.1 %ArrayIteratorPrototype%.next ( ), https://tc39.es/ecma262/#sec-%arrayiteratorprototype%.next
JS_DEFINE_NATIVE_FUNCTION(ArrayIteratorPrototype::next) JS_DEFINE_NATIVE_FUNCTION(ArrayIteratorPrototype::next)
{ {
auto& realm = *vm.current_realm();
// 1. Let O be the this value.
// 2. If O is not an Object, throw a TypeError exception.
// 3. If O does not have all of the internal slots of an Array Iterator Instance (23.1.5.3), throw a TypeError exception.
auto iterator = TRY(typed_this_value(vm)); auto iterator = TRY(typed_this_value(vm));
// 4. Let array be O.[[IteratedArrayLike]]. Value value;
auto target_array = iterator->array(); bool done = false;
TRY(iterator->next(vm, done, value));
// 5. If array is undefined, return CreateIteratorResultObject(undefined, true). return create_iterator_result_object(vm, value, done);
if (target_array.is_undefined())
return create_iterator_result_object(vm, js_undefined(), true);
VERIFY(target_array.is_object());
auto& array = target_array.as_object();
// 6. Let index be O.[[ArrayLikeNextIndex]].
auto index = iterator->index();
// 7. Let kind be O.[[ArrayLikeIterationKind]].
auto kind = iterator->iteration_kind();
size_t length = 0;
// 8. If array has a [[TypedArrayName]] internal slot, then
if (array.is_typed_array()) {
auto& typed_array = static_cast<TypedArrayBase&>(array);
// a. Let taRecord be MakeTypedArrayWithBufferWitnessRecord(array, SEQ-CST).
auto typed_array_record = make_typed_array_with_buffer_witness_record(typed_array, ArrayBuffer::SeqCst);
// b. If IsTypedArrayOutOfBounds(taRecord) is true, throw a TypeError exception.
if (is_typed_array_out_of_bounds(typed_array_record))
return vm.throw_completion<TypeError>(ErrorType::BufferOutOfBounds, "TypedArray"sv);
// c. Let len be TypedArrayLength(taRecord).
length = typed_array_length(typed_array_record);
}
// 9. Else,
else {
// a. Let len be ? LengthOfArrayLike(array).
length = TRY(length_of_array_like(vm, array));
}
// 10. If index ≥ len, then
if (index >= length) {
// a. Set O.[[IteratedArrayLike]] to undefined.
iterator->set_array(js_undefined());
// b. Return CreateIteratorResultObject(undefined, true).
return create_iterator_result_object(vm, js_undefined(), true);
}
// 11. Set O.[[ArrayLikeNextIndex]] to index + 1.
iterator->set_index(index + 1);
// 12. Let indexNumber be 𝔽(index).
Value result;
// 13. If kind is KEY, then
if (kind == PropertyKind::Key) {
// a. Let result be indexNumber.
result = Value { static_cast<i32>(index) };
}
// 14. Else,
else {
// a. Let elementKey be ! ToString(indexNumber).
// b. Let elementValue be ? Get(array, elementKey).
auto element_value = TRY([&]() -> ThrowCompletionOr<Value> {
// OPTIMIZATION: For objects that don't interfere with indexed property access, we try looking directly at storage.
if (!array.may_interfere_with_indexed_property_access() && array.indexed_properties().has_index(index)) {
if (auto value = array.indexed_properties().get(index)->value; !value.is_accessor())
return value;
}
return array.get(index);
}());
// c. If kind is VALUE, then
if (kind == PropertyKind::Value) {
// i. Let result be elementValue.
result = element_value;
}
// d. Else,
else {
// i. Assert: kind is KEY+VALUE.
VERIFY(kind == PropertyKind::KeyAndValue);
// ii. Let result be CreateArrayFromList(« indexNumber, elementValue »).
result = Array::create_from(realm, { Value(static_cast<i32>(index)), element_value });
}
}
// 15. Return CreateIteratorResultObject(result, false).
return create_iterator_result_object(vm, result, false);
} }
} }

View file

@ -67,6 +67,12 @@ enum class PrimitiveHandling {
RejectPrimitives, RejectPrimitives,
}; };
class BuiltinIterator {
public:
virtual ~BuiltinIterator() = default;
virtual ThrowCompletionOr<void> next(VM&, bool& done, Value& value) = 0;
};
// 7.4.12 IfAbruptCloseIterator ( value, iteratorRecord ), https://tc39.es/ecma262/#sec-ifabruptcloseiterator // 7.4.12 IfAbruptCloseIterator ( value, iteratorRecord ), https://tc39.es/ecma262/#sec-ifabruptcloseiterator
#define TRY_OR_CLOSE_ITERATOR(vm, iterator_record, expression) \ #define TRY_OR_CLOSE_ITERATOR(vm, iterator_record, expression) \
({ \ ({ \

View file

@ -4,7 +4,7 @@
* SPDX-License-Identifier: BSD-2-Clause * SPDX-License-Identifier: BSD-2-Clause
*/ */
#include <LibJS/Runtime/GlobalObject.h> #include <LibJS/Runtime/Array.h>
#include <LibJS/Runtime/MapIterator.h> #include <LibJS/Runtime/MapIterator.h>
namespace JS { namespace JS {
@ -30,4 +30,34 @@ void MapIterator::visit_edges(Cell::Visitor& visitor)
visitor.visit(m_map); visitor.visit(m_map);
} }
ThrowCompletionOr<void> MapIterator::next(VM& vm, bool& done, Value& value)
{
if (m_done) {
done = true;
value = js_undefined();
return {};
}
if (m_iterator.is_end()) {
m_done = true;
done = true;
value = js_undefined();
return {};
}
auto entry = *m_iterator;
++m_iterator;
if (m_iteration_kind == Object::PropertyKind::Key) {
value = entry.key;
return {};
}
if (m_iteration_kind == Object::PropertyKind::Value) {
value = entry.value;
return {};
}
value = Array::create_from(*vm.current_realm(), { entry.key, entry.value });
return {};
}
} }

View file

@ -6,12 +6,14 @@
#pragma once #pragma once
#include <LibJS/Runtime/Iterator.h>
#include <LibJS/Runtime/Map.h> #include <LibJS/Runtime/Map.h>
#include <LibJS/Runtime/Object.h> #include <LibJS/Runtime/Object.h>
namespace JS { namespace JS {
class MapIterator final : public Object { class MapIterator final : public Object
, public BuiltinIterator {
JS_OBJECT(MapIterator, Object); JS_OBJECT(MapIterator, Object);
GC_DECLARE_ALLOCATOR(MapIterator); GC_DECLARE_ALLOCATOR(MapIterator);
@ -20,9 +22,8 @@ public:
virtual ~MapIterator() override = default; virtual ~MapIterator() override = default;
Map& map() const { return m_map; } BuiltinIterator* as_builtin_iterator() override { return this; }
bool done() const { return m_done; } ThrowCompletionOr<void> next(VM&, bool& done, Value& value) override;
Object::PropertyKind iteration_kind() const { return m_iteration_kind; }
private: private:
friend class MapIteratorPrototype; friend class MapIteratorPrototype;

View file

@ -6,8 +6,6 @@
#include <AK/TypeCasts.h> #include <AK/TypeCasts.h>
#include <LibJS/Runtime/Array.h> #include <LibJS/Runtime/Array.h>
#include <LibJS/Runtime/Error.h>
#include <LibJS/Runtime/GlobalObject.h>
#include <LibJS/Runtime/Iterator.h> #include <LibJS/Runtime/Iterator.h>
#include <LibJS/Runtime/MapIteratorPrototype.h> #include <LibJS/Runtime/MapIteratorPrototype.h>
@ -32,27 +30,13 @@ void MapIteratorPrototype::initialize(Realm& realm)
// 24.1.5.2.1 %MapIteratorPrototype%.next ( ), https://tc39.es/ecma262/#sec-%mapiteratorprototype%.next // 24.1.5.2.1 %MapIteratorPrototype%.next ( ), https://tc39.es/ecma262/#sec-%mapiteratorprototype%.next
JS_DEFINE_NATIVE_FUNCTION(MapIteratorPrototype::next) JS_DEFINE_NATIVE_FUNCTION(MapIteratorPrototype::next)
{ {
auto& realm = *vm.current_realm(); auto iterator = TRY(typed_this_value(vm));
auto map_iterator = TRY(typed_this_value(vm)); Value value;
if (map_iterator->done()) bool done = false;
return create_iterator_result_object(vm, js_undefined(), true); TRY(iterator->next(vm, done, value));
if (map_iterator->m_iterator.is_end()) { return create_iterator_result_object(vm, value, done);
map_iterator->m_done = true;
return create_iterator_result_object(vm, js_undefined(), true);
}
auto iteration_kind = map_iterator->iteration_kind();
auto entry = *map_iterator->m_iterator;
++map_iterator->m_iterator;
if (iteration_kind == Object::PropertyKind::Key)
return create_iterator_result_object(vm, entry.key, false);
if (iteration_kind == Object::PropertyKind::Value)
return create_iterator_result_object(vm, entry.value, false);
return create_iterator_result_object(vm, Array::create_from(realm, { entry.key, entry.value }), false);
} }
} }

View file

@ -212,6 +212,8 @@ public:
virtual bool is_array_iterator() const { return false; } virtual bool is_array_iterator() const { return false; }
virtual bool is_raw_json_object() const { return false; } virtual bool is_raw_json_object() const { return false; }
virtual BuiltinIterator* as_builtin_iterator() { return nullptr; }
// B.3.7 The [[IsHTMLDDA]] Internal Slot, https://tc39.es/ecma262/#sec-IsHTMLDDA-internal-slot // B.3.7 The [[IsHTMLDDA]] Internal Slot, https://tc39.es/ecma262/#sec-IsHTMLDDA-internal-slot
virtual bool is_htmldda() const { return false; } virtual bool is_htmldda() const { return false; }

View file

@ -4,7 +4,7 @@
* SPDX-License-Identifier: BSD-2-Clause * SPDX-License-Identifier: BSD-2-Clause
*/ */
#include <LibJS/Runtime/GlobalObject.h> #include <LibJS/Runtime/Array.h>
#include <LibJS/Runtime/SetIterator.h> #include <LibJS/Runtime/SetIterator.h>
namespace JS { namespace JS {
@ -30,4 +30,31 @@ void SetIterator::visit_edges(Cell::Visitor& visitor)
visitor.visit(m_set); visitor.visit(m_set);
} }
ThrowCompletionOr<void> SetIterator::next(VM& vm, bool& done, Value& value)
{
if (m_done) {
done = true;
value = js_undefined();
return {};
}
if (m_iterator == m_set->end()) {
m_done = true;
done = true;
value = js_undefined();
return {};
}
VERIFY(m_iteration_kind != Object::PropertyKind::Key);
value = (*m_iterator).key;
++m_iterator;
if (m_iteration_kind == Object::PropertyKind::Value) {
return {};
}
value = Array::create_from(*vm.current_realm(), { value, value });
return {};
}
} }

View file

@ -6,13 +6,14 @@
#pragma once #pragma once
#include <AK/HashTable.h> #include <LibJS/Runtime/Iterator.h>
#include <LibJS/Runtime/Object.h> #include <LibJS/Runtime/Object.h>
#include <LibJS/Runtime/Set.h> #include <LibJS/Runtime/Set.h>
namespace JS { namespace JS {
class SetIterator final : public Object { class SetIterator final : public Object
, public BuiltinIterator {
JS_OBJECT(SetIterator, Object); JS_OBJECT(SetIterator, Object);
GC_DECLARE_ALLOCATOR(SetIterator); GC_DECLARE_ALLOCATOR(SetIterator);
@ -21,9 +22,8 @@ public:
virtual ~SetIterator() override = default; virtual ~SetIterator() override = default;
Set& set() const { return m_set; } BuiltinIterator* as_builtin_iterator() override { return this; }
bool done() const { return m_done; } ThrowCompletionOr<void> next(VM&, bool& done, Value& value) override;
Object::PropertyKind iteration_kind() const { return m_iteration_kind; }
private: private:
friend class SetIteratorPrototype; friend class SetIteratorPrototype;

View file

@ -34,27 +34,13 @@ void SetIteratorPrototype::initialize(Realm& realm)
// 24.2.5.2.1 %SetIteratorPrototype%.next ( ), https://tc39.es/ecma262/#sec-%setiteratorprototype%.next // 24.2.5.2.1 %SetIteratorPrototype%.next ( ), https://tc39.es/ecma262/#sec-%setiteratorprototype%.next
JS_DEFINE_NATIVE_FUNCTION(SetIteratorPrototype::next) JS_DEFINE_NATIVE_FUNCTION(SetIteratorPrototype::next)
{ {
auto& realm = *vm.current_realm(); auto iterator = TRY(typed_this_value(vm));
auto set_iterator = TRY(typed_this_value(vm)); Value value;
if (set_iterator->done()) bool done = false;
return create_iterator_result_object(vm, js_undefined(), true); TRY(iterator->next(vm, done, value));
auto& set = set_iterator->set(); return create_iterator_result_object(vm, value, done);
if (set_iterator->m_iterator == set.end()) {
set_iterator->m_done = true;
return create_iterator_result_object(vm, js_undefined(), true);
}
auto iteration_kind = set_iterator->iteration_kind();
VERIFY(iteration_kind != Object::PropertyKind::Key);
auto value = (*set_iterator->m_iterator).key;
++set_iterator->m_iterator;
if (iteration_kind == Object::PropertyKind::Value)
return create_iterator_result_object(vm, value, false);
return create_iterator_result_object(vm, Array::create_from(realm, { value, value }), false);
} }
} }

View file

@ -24,4 +24,26 @@ StringIterator::StringIterator(String string, Object& prototype)
{ {
} }
ThrowCompletionOr<void> StringIterator::next(VM& vm, bool& done, Value& value)
{
if (m_done) {
done = true;
value = js_undefined();
return {};
}
if (m_iterator.done()) {
m_done = true;
done = true;
value = js_undefined();
return {};
}
auto code_point = String::from_code_point(*m_iterator);
++m_iterator;
value = PrimitiveString::create(vm, move(code_point));
return {};
}
} }

View file

@ -8,11 +8,13 @@
#include <AK/String.h> #include <AK/String.h>
#include <AK/Utf8View.h> #include <AK/Utf8View.h>
#include <LibJS/Runtime/Iterator.h>
#include <LibJS/Runtime/Object.h> #include <LibJS/Runtime/Object.h>
namespace JS { namespace JS {
class StringIterator final : public Object { class StringIterator final : public Object
, public BuiltinIterator {
JS_OBJECT(StringIterator, Object); JS_OBJECT(StringIterator, Object);
GC_DECLARE_ALLOCATOR(StringIterator); GC_DECLARE_ALLOCATOR(StringIterator);
@ -21,8 +23,8 @@ public:
virtual ~StringIterator() override = default; virtual ~StringIterator() override = default;
Utf8CodePointIterator& iterator() { return m_iterator; } BuiltinIterator* as_builtin_iterator() override { return this; }
bool done() const { return m_done; } ThrowCompletionOr<void> next(VM&, bool& done, Value& value) override;
private: private:
explicit StringIterator(String string, Object& prototype); explicit StringIterator(String string, Object& prototype);

View file

@ -33,20 +33,12 @@ void StringIteratorPrototype::initialize(Realm& realm)
JS_DEFINE_NATIVE_FUNCTION(StringIteratorPrototype::next) JS_DEFINE_NATIVE_FUNCTION(StringIteratorPrototype::next)
{ {
auto iterator = TRY(typed_this_value(vm)); auto iterator = TRY(typed_this_value(vm));
if (iterator->done())
return create_iterator_result_object(vm, js_undefined(), true);
auto& utf8_iterator = iterator->iterator(); Value value;
bool done = false;
TRY(iterator->next(vm, done, value));
if (utf8_iterator.done()) { return create_iterator_result_object(vm, value, done);
iterator->m_done = true;
return create_iterator_result_object(vm, js_undefined(), true);
}
auto code_point = String::from_code_point(*utf8_iterator);
++utf8_iterator;
return create_iterator_result_object(vm, PrimitiveString::create(vm, move(code_point)), false);
} }
} }