From d9d245faa7adbb731007db1feb33f79075e4860f Mon Sep 17 00:00:00 2001 From: Timothy Flynn Date: Sat, 24 Jun 2023 11:27:30 -0400 Subject: [PATCH] LibJS: Implement Iterator.from and the WrapForValidIteratorPrototype Iterator.from creates an Iterator from either an existing iterator or an iterator-like object. In the latter case, it sets the prototype of the returned iterator to WrapForValidIteratorPrototype to wrap around the iterator-like object's iteration methods. --- Userland/Libraries/LibJS/CMakeLists.txt | 1 + Userland/Libraries/LibJS/Forward.h | 1 + .../Libraries/LibJS/Runtime/Intrinsics.cpp | 3 + Userland/Libraries/LibJS/Runtime/Intrinsics.h | 2 + Userland/Libraries/LibJS/Runtime/Iterator.cpp | 45 ++++++++ Userland/Libraries/LibJS/Runtime/Iterator.h | 3 + .../LibJS/Runtime/IteratorConstructor.cpp | 34 ++++++ .../LibJS/Runtime/IteratorConstructor.h | 2 + .../Runtime/WrapForValidIteratorPrototype.cpp | 71 ++++++++++++ .../Runtime/WrapForValidIteratorPrototype.h | 28 +++++ .../Tests/builtins/Iterator/Iterator.from.js | 109 ++++++++++++++++++ 11 files changed, 299 insertions(+) create mode 100644 Userland/Libraries/LibJS/Runtime/WrapForValidIteratorPrototype.cpp create mode 100644 Userland/Libraries/LibJS/Runtime/WrapForValidIteratorPrototype.h create mode 100644 Userland/Libraries/LibJS/Tests/builtins/Iterator/Iterator.from.js diff --git a/Userland/Libraries/LibJS/CMakeLists.txt b/Userland/Libraries/LibJS/CMakeLists.txt index c3d39f99500..6de777d8ae6 100644 --- a/Userland/Libraries/LibJS/CMakeLists.txt +++ b/Userland/Libraries/LibJS/CMakeLists.txt @@ -257,6 +257,7 @@ set(SOURCES Runtime/WeakSet.cpp Runtime/WeakSetConstructor.cpp Runtime/WeakSetPrototype.cpp + Runtime/WrapForValidIteratorPrototype.cpp Runtime/WrappedFunction.cpp Script.cpp SourceCode.cpp diff --git a/Userland/Libraries/LibJS/Forward.h b/Userland/Libraries/LibJS/Forward.h index d3e846f9d9c..8c25a872d73 100644 --- a/Userland/Libraries/LibJS/Forward.h +++ b/Userland/Libraries/LibJS/Forward.h @@ -229,6 +229,7 @@ class AsyncFromSyncIteratorPrototype; class AsyncGenerator; class AsyncGeneratorPrototype; class GeneratorPrototype; +class WrapForValidIteratorPrototype; class TypedArrayConstructor; class TypedArrayPrototype; diff --git a/Userland/Libraries/LibJS/Runtime/Intrinsics.cpp b/Userland/Libraries/LibJS/Runtime/Intrinsics.cpp index a15b431a2ed..916a7a44993 100644 --- a/Userland/Libraries/LibJS/Runtime/Intrinsics.cpp +++ b/Userland/Libraries/LibJS/Runtime/Intrinsics.cpp @@ -126,6 +126,7 @@ #include #include #include +#include namespace JS { @@ -199,6 +200,7 @@ ThrowCompletionOr Intrinsics::initialize_intrinsics(Realm& realm) m_async_generator_prototype = heap().allocate(realm, realm).release_allocated_value_but_fixme_should_propagate_errors(); m_generator_prototype = heap().allocate(realm, realm).release_allocated_value_but_fixme_should_propagate_errors(); m_intl_segments_prototype = heap().allocate(realm, realm).release_allocated_value_but_fixme_should_propagate_errors(); + m_wrap_for_valid_iterator_prototype = heap().allocate(realm, realm).release_allocated_value_but_fixme_should_propagate_errors(); // These must be initialized before allocating... // - AggregateErrorPrototype, which uses ErrorPrototype as its prototype @@ -356,6 +358,7 @@ void Intrinsics::visit_edges(Visitor& visitor) visitor.visit(m_async_generator_prototype); visitor.visit(m_generator_prototype); visitor.visit(m_intl_segments_prototype); + visitor.visit(m_wrap_for_valid_iterator_prototype); visitor.visit(m_eval_function); visitor.visit(m_is_finite_function); visitor.visit(m_is_nan_function); diff --git a/Userland/Libraries/LibJS/Runtime/Intrinsics.h b/Userland/Libraries/LibJS/Runtime/Intrinsics.h index 27402d0fabc..be8d7bf516d 100644 --- a/Userland/Libraries/LibJS/Runtime/Intrinsics.h +++ b/Userland/Libraries/LibJS/Runtime/Intrinsics.h @@ -29,6 +29,7 @@ public: NonnullGCPtr async_from_sync_iterator_prototype() { return *m_async_from_sync_iterator_prototype; } NonnullGCPtr async_generator_prototype() { return *m_async_generator_prototype; } NonnullGCPtr generator_prototype() { return *m_generator_prototype; } + NonnullGCPtr wrap_for_valid_iterator_prototype() { return *m_wrap_for_valid_iterator_prototype; } // Alias for the AsyncGenerator Prototype Object used by the spec (%AsyncGeneratorFunction.prototype.prototype%) NonnullGCPtr async_generator_function_prototype_prototype() { return *m_async_generator_prototype; } @@ -128,6 +129,7 @@ private: GCPtr m_async_from_sync_iterator_prototype; GCPtr m_async_generator_prototype; GCPtr m_generator_prototype; + GCPtr m_wrap_for_valid_iterator_prototype; // Not included in JS_ENUMERATE_INTL_OBJECTS due to missing distinct constructor GCPtr m_intl_segments_prototype; diff --git a/Userland/Libraries/LibJS/Runtime/Iterator.cpp b/Userland/Libraries/LibJS/Runtime/Iterator.cpp index 86793d51656..9adee1e5ecd 100644 --- a/Userland/Libraries/LibJS/Runtime/Iterator.cpp +++ b/Userland/Libraries/LibJS/Runtime/Iterator.cpp @@ -4,7 +4,9 @@ * SPDX-License-Identifier: BSD-2-Clause */ +#include #include +#include namespace JS { @@ -24,4 +26,47 @@ Iterator::Iterator(Object& prototype) { } +// 2.1.1 GetIteratorDirect ( obj ), https://tc39.es/proposal-iterator-helpers/#sec-getiteratorflattenable +ThrowCompletionOr get_iterator_direct(VM& vm, Object& object) +{ + // 1. Let nextMethod be ? Get(obj, "next"). + auto next_method = TRY(object.get(vm.names.next)); + + // 2. Let iteratorRecord be Record { [[Iterator]]: obj, [[NextMethod]]: nextMethod, [[Done]]: false }. + IteratorRecord iterator_record { .iterator = object, .next_method = next_method, .done = false }; + + // 3. Return iteratorRecord. + return iterator_record; +} + +ThrowCompletionOr get_iterator_flattenable(VM& vm, Value object) +{ + // 1. If obj is not an Object, throw a TypeError exception. + if (!object.is_object()) + return vm.throw_completion(ErrorType::NotAnObject, "obj"sv); + + // 2. Let method be ? GetMethod(obj, @@iterator). + auto method = TRY(object.get_method(vm, vm.well_known_symbol_iterator())); + + Value iterator; + + // 3. If method is undefined, then + if (!method) { + // a. Let iterator be obj. + iterator = object; + } + // 4. Else, + else { + // a. Let iterator be ? Call(method, obj). + iterator = TRY(call(vm, method, object)); + } + + // 5. If iterator is not an Object, throw a TypeError exception. + if (!iterator.is_object()) + return vm.throw_completion(ErrorType::NotAnObject, "iterator"sv); + + // 6. Return ? GetIteratorDirect(iterator). + return TRY(get_iterator_direct(vm, iterator.as_object())); +} + } diff --git a/Userland/Libraries/LibJS/Runtime/Iterator.h b/Userland/Libraries/LibJS/Runtime/Iterator.h index dc5e2723361..4a0c1874f65 100644 --- a/Userland/Libraries/LibJS/Runtime/Iterator.h +++ b/Userland/Libraries/LibJS/Runtime/Iterator.h @@ -35,4 +35,7 @@ private: IteratorRecord m_iterated; // [[Iterated]] }; +ThrowCompletionOr get_iterator_direct(VM&, Object&); +ThrowCompletionOr get_iterator_flattenable(VM&, Value); + } diff --git a/Userland/Libraries/LibJS/Runtime/IteratorConstructor.cpp b/Userland/Libraries/LibJS/Runtime/IteratorConstructor.cpp index 3f222ac7042..09f97baf73b 100644 --- a/Userland/Libraries/LibJS/Runtime/IteratorConstructor.cpp +++ b/Userland/Libraries/LibJS/Runtime/IteratorConstructor.cpp @@ -29,6 +29,9 @@ ThrowCompletionOr IteratorConstructor::initialize(Realm& realm) // 3.1.1.2.1 Iterator.prototype, https://tc39.es/proposal-iterator-helpers/#sec-iterator.prototype define_direct_property(vm.names.prototype, realm.intrinsics().iterator_prototype(), 0); + u8 attr = Attribute::Writable | Attribute::Configurable; + define_native_function(realm, vm.names.from, from, 1, attr); + define_direct_property(vm.names.length, Value(0), Attribute::Configurable); return {}; @@ -56,4 +59,35 @@ ThrowCompletionOr> IteratorConstructor::construct(FunctionO return TRY(ordinary_create_from_constructor(vm, new_target, &Intrinsics::iterator_prototype)); } +// 3.1.1.2.2 Iterator.from ( O ), https://tc39.es/proposal-iterator-helpers/#sec-iterator.from +JS_DEFINE_NATIVE_FUNCTION(IteratorConstructor::from) +{ + auto& realm = *vm.current_realm(); + + auto object = vm.argument(0); + + // 1. If O is a String, set O to ! ToObject(O). + if (object.is_string()) + object = MUST_OR_THROW_OOM(object.to_object(vm)); + + // 2. Let iteratorRecord be ? GetIteratorFlattenable(O). + auto iterator_record = TRY(get_iterator_flattenable(vm, object)); + + // 3. Let hasInstance be ? OrdinaryHasInstance(%Iterator%, iteratorRecord.[[Iterator]]). + auto has_instance = TRY(ordinary_has_instance(vm, iterator_record.iterator, realm.intrinsics().iterator_constructor())); + + // 4. If hasInstance is true, then + if (has_instance.is_boolean() && has_instance.as_bool()) { + // a. Return iteratorRecord.[[Iterator]]. + return iterator_record.iterator; + } + + // 5. Let wrapper be OrdinaryObjectCreate(%WrapForValidIteratorPrototype%, « [[Iterated]] »). + // 6. Set wrapper.[[Iterated]] to iteratorRecord. + auto wrapper = MUST_OR_THROW_OOM(Iterator::create(realm, realm.intrinsics().wrap_for_valid_iterator_prototype(), move(iterator_record))); + + // 7. Return wrapper. + return wrapper; +} + } diff --git a/Userland/Libraries/LibJS/Runtime/IteratorConstructor.h b/Userland/Libraries/LibJS/Runtime/IteratorConstructor.h index 8b924c958d2..fd5baa65e17 100644 --- a/Userland/Libraries/LibJS/Runtime/IteratorConstructor.h +++ b/Userland/Libraries/LibJS/Runtime/IteratorConstructor.h @@ -24,6 +24,8 @@ private: explicit IteratorConstructor(Realm&); virtual bool has_constructor() const override { return true; } + + JS_DECLARE_NATIVE_FUNCTION(from); }; } diff --git a/Userland/Libraries/LibJS/Runtime/WrapForValidIteratorPrototype.cpp b/Userland/Libraries/LibJS/Runtime/WrapForValidIteratorPrototype.cpp new file mode 100644 index 00000000000..167c1646958 --- /dev/null +++ b/Userland/Libraries/LibJS/Runtime/WrapForValidIteratorPrototype.cpp @@ -0,0 +1,71 @@ +/* + * Copyright (c) 2023, Tim Flynn + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include +#include +#include + +namespace JS { + +// 3.1.1.2.2.1 The %WrapForValidIteratorPrototype% Object, https://tc39.es/proposal-iterator-helpers/#sec-wrapforvaliditeratorprototype-object +WrapForValidIteratorPrototype::WrapForValidIteratorPrototype(Realm& realm) + : PrototypeObject(realm.intrinsics().iterator_prototype()) +{ +} + +ThrowCompletionOr WrapForValidIteratorPrototype::initialize(Realm& realm) +{ + auto& vm = this->vm(); + MUST_OR_THROW_OOM(Base::initialize(realm)); + + u8 attr = Attribute::Writable | Attribute::Configurable; + define_native_function(realm, vm.names.next, next, 0, attr); + define_native_function(realm, vm.names.return_, return_, 0, attr); + + return {}; +} + +// 3.1.1.2.2.1.1 %WrapForValidIteratorPrototype%.next ( ), https://tc39.es/proposal-iterator-helpers/#sec-wrapforvaliditeratorprototype.next +JS_DEFINE_NATIVE_FUNCTION(WrapForValidIteratorPrototype::next) +{ + // 1. Let O be this value. + // 2. Perform ? RequireInternalSlot(O, [[Iterated]]). + auto object = TRY(typed_this_object(vm)); + + // 3. Let iteratorRecord be O.[[Iterated]]. + auto const& iterator_record = object->iterated(); + + // 4. Return ? Call(iteratorRecord.[[NextMethod]], iteratorRecord.[[Iterator]]). + return TRY(call(vm, iterator_record.next_method, iterator_record.iterator)); +} + +// 3.1.1.2.2.1.2 %WrapForValidIteratorPrototype%.return ( ), https://tc39.es/proposal-iterator-helpers/#sec-wrapforvaliditeratorprototype.return +JS_DEFINE_NATIVE_FUNCTION(WrapForValidIteratorPrototype::return_) +{ + // 1. Let O be this value. + // 2. Perform ? RequireInternalSlot(O, [[Iterated]]). + auto object = TRY(typed_this_object(vm)); + + // 3. Let iterator be O.[[Iterated]].[[Iterator]]. + auto iterator = object->iterated().iterator; + + // 4. Assert: iterator is an Object. + VERIFY(iterator); + + // 5. Let returnMethod be ? GetMethod(iterator, "return"). + auto return_method = TRY(Value { iterator }.get_method(vm, vm.names.return_)); + + // 6. If returnMethod is undefined, then + if (!return_method) { + // a. Return CreateIterResultObject(undefined, true). + return create_iterator_result_object(vm, js_undefined(), true); + } + + // 7. Return ? Call(returnMethod, iterator). + return TRY(call(vm, return_method, iterator)); +} + +} diff --git a/Userland/Libraries/LibJS/Runtime/WrapForValidIteratorPrototype.h b/Userland/Libraries/LibJS/Runtime/WrapForValidIteratorPrototype.h new file mode 100644 index 00000000000..5c3bcdeb073 --- /dev/null +++ b/Userland/Libraries/LibJS/Runtime/WrapForValidIteratorPrototype.h @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2023, Tim Flynn + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include +#include +#include + +namespace JS { + +class WrapForValidIteratorPrototype final : public PrototypeObject { + JS_PROTOTYPE_OBJECT(WrapForValidIteratorPrototype, Iterator, Iterator); + +public: + virtual ThrowCompletionOr initialize(Realm&) override; + +private: + explicit WrapForValidIteratorPrototype(Realm&); + + JS_DECLARE_NATIVE_FUNCTION(next); + JS_DECLARE_NATIVE_FUNCTION(return_); +}; + +} diff --git a/Userland/Libraries/LibJS/Tests/builtins/Iterator/Iterator.from.js b/Userland/Libraries/LibJS/Tests/builtins/Iterator/Iterator.from.js new file mode 100644 index 00000000000..d471ab48fcf --- /dev/null +++ b/Userland/Libraries/LibJS/Tests/builtins/Iterator/Iterator.from.js @@ -0,0 +1,109 @@ +describe("errors", () => { + test("called with non-Object", () => { + expect(() => { + Iterator.from(Symbol.hasInstance); + }).toThrowWithMessage(TypeError, "obj is not an object"); + }); + + test("@@iterator is not callable", () => { + const iterable = {}; + iterable[Symbol.iterator] = 12389; + + expect(() => { + Iterator.from(iterable); + }).toThrowWithMessage(TypeError, "12389 is not a function"); + }); + + test("@@iterator throws an exception", () => { + function TestError() {} + + const iterable = {}; + iterable[Symbol.iterator] = () => { + throw new TestError(); + }; + + expect(() => { + Iterator.from(iterable); + }).toThrow(TestError); + }); + + test("@@iterator return a non-Object", () => { + const iterable = {}; + iterable[Symbol.iterator] = () => { + return Symbol.hasInstance; + }; + + expect(() => { + Iterator.from(iterable); + }).toThrowWithMessage(TypeError, "iterator is not an object"); + }); +}); + +describe("normal behavior", () => { + test("length is 1", () => { + expect(Iterator.from).toHaveLength(1); + }); + + test("create Iterator from a string", () => { + const iterator = Iterator.from("ab"); + + let result = iterator.next(); + expect(result.value).toBe("a"); + expect(result.done).toBeFalse(); + + result = iterator.next(); + expect(result.value).toBe("b"); + expect(result.done).toBeFalse(); + + result = iterator.next(); + expect(result.value).toBeUndefined(); + expect(result.done).toBeTrue(); + }); + + test("create Iterator from generator", () => { + function* generator() { + yield 1; + yield 2; + } + + const iterator = Iterator.from(generator()); + + let result = iterator.next(); + expect(result.value).toBe(1); + expect(result.done).toBeFalse(); + + result = iterator.next(); + expect(result.value).toBe(2); + expect(result.done).toBeFalse(); + + result = iterator.next(); + expect(result.value).toBeUndefined(); + expect(result.done).toBeTrue(); + }); + + test("create Iterator from iterator-like object", () => { + class TestIterator { + next() { + if (this.#first) { + this.#first = false; + return { value: 1, done: false }; + } + + return { value: undefined, done: true }; + } + + #first = true; + } + + const testIterator = new TestIterator(); + const iterator = Iterator.from(testIterator); + + let result = iterator.next(); + expect(result.value).toBe(1); + expect(result.done).toBeFalse(); + + result = iterator.next(); + expect(result.value).toBeUndefined(); + expect(result.done).toBeTrue(); + }); +});