diff --git a/Userland/Libraries/LibJS/CMakeLists.txt b/Userland/Libraries/LibJS/CMakeLists.txt index 6de777d8ae..0296aab980 100644 --- a/Userland/Libraries/LibJS/CMakeLists.txt +++ b/Userland/Libraries/LibJS/CMakeLists.txt @@ -143,6 +143,8 @@ set(SOURCES Runtime/Intrinsics.cpp Runtime/Iterator.cpp Runtime/IteratorConstructor.cpp + Runtime/IteratorHelper.cpp + Runtime/IteratorHelperPrototype.cpp Runtime/IteratorOperations.cpp Runtime/IteratorPrototype.cpp Runtime/JSONObject.cpp diff --git a/Userland/Libraries/LibJS/Forward.h b/Userland/Libraries/LibJS/Forward.h index 8c25a872d7..7de394d16d 100644 --- a/Userland/Libraries/LibJS/Forward.h +++ b/Userland/Libraries/LibJS/Forward.h @@ -110,6 +110,7 @@ __JS_ENUMERATE(ArrayIterator, array_iterator) \ __JS_ENUMERATE(AsyncIterator, async_iterator) \ __JS_ENUMERATE(Intl::SegmentIterator, intl_segment_iterator) \ + __JS_ENUMERATE(IteratorHelper, iterator_helper) \ __JS_ENUMERATE(MapIterator, map_iterator) \ __JS_ENUMERATE(RegExpStringIterator, regexp_string_iterator) \ __JS_ENUMERATE(SetIterator, set_iterator) \ diff --git a/Userland/Libraries/LibJS/Runtime/Intrinsics.cpp b/Userland/Libraries/LibJS/Runtime/Intrinsics.cpp index 916a7a4499..564b6a3e77 100644 --- a/Userland/Libraries/LibJS/Runtime/Intrinsics.cpp +++ b/Userland/Libraries/LibJS/Runtime/Intrinsics.cpp @@ -64,6 +64,7 @@ #include #include #include +#include #include #include #include diff --git a/Userland/Libraries/LibJS/Runtime/IteratorHelper.cpp b/Userland/Libraries/LibJS/Runtime/IteratorHelper.cpp new file mode 100644 index 0000000000..6a63ed850f --- /dev/null +++ b/Userland/Libraries/LibJS/Runtime/IteratorHelper.cpp @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2023, Tim Flynn + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include +#include +#include +#include + +namespace JS { + +ThrowCompletionOr> IteratorHelper::create(Realm& realm, IteratorRecord underlying_iterator, Closure closure) +{ + return TRY(realm.heap().allocate(realm, realm.intrinsics().iterator_helper_prototype(), move(underlying_iterator), move(closure))); +} + +IteratorHelper::IteratorHelper(Object& prototype, IteratorRecord underlying_iterator, Closure closure) + : Object(ConstructWithPrototypeTag::Tag, prototype) + , m_underlying_iterator(move(underlying_iterator)) + , m_closure(move(closure)) +{ +} + +void IteratorHelper::visit_edges(Visitor& visitor) +{ + Base::visit_edges(visitor); + visitor.visit(m_underlying_iterator.iterator); +} + +Value IteratorHelper::result(Value value) +{ + if (value.is_undefined()) + m_done = true; + return value; +} + +ThrowCompletionOr IteratorHelper::close_result(Completion completion) +{ + m_done = true; + return *TRY(iterator_close(vm(), underlying_iterator(), move(completion))); +} + +} diff --git a/Userland/Libraries/LibJS/Runtime/IteratorHelper.h b/Userland/Libraries/LibJS/Runtime/IteratorHelper.h new file mode 100644 index 0000000000..01bf227966 --- /dev/null +++ b/Userland/Libraries/LibJS/Runtime/IteratorHelper.h @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2023, Tim Flynn + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include +#include +#include +#include + +namespace JS { + +class IteratorHelper final : public Object { + JS_OBJECT(IteratorHelper, Object); + +public: + using Closure = JS::SafeFunction(IteratorHelper&)>; + + static ThrowCompletionOr> create(Realm&, IteratorRecord, Closure); + + IteratorRecord const& underlying_iterator() const { return m_underlying_iterator; } + Closure& closure() { return m_closure; } + + size_t counter() const { return m_counter; } + void increment_counter() { ++m_counter; } + + Value result(Value); + ThrowCompletionOr close_result(Completion); + + bool done() const { return m_done; } + +private: + IteratorHelper(Object& prototype, IteratorRecord, Closure); + + virtual void visit_edges(Visitor&) override; + + IteratorRecord m_underlying_iterator; // [[UnderlyingIterator]] + Closure m_closure; + + size_t m_counter { 0 }; + bool m_done { false }; +}; + +} diff --git a/Userland/Libraries/LibJS/Runtime/IteratorHelperPrototype.cpp b/Userland/Libraries/LibJS/Runtime/IteratorHelperPrototype.cpp new file mode 100644 index 0000000000..48933a3568 --- /dev/null +++ b/Userland/Libraries/LibJS/Runtime/IteratorHelperPrototype.cpp @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2023, Tim Flynn + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include +#include +#include + +namespace JS { + +IteratorHelperPrototype::IteratorHelperPrototype(Realm& realm) + : PrototypeObject(realm.intrinsics().iterator_prototype()) +{ +} + +ThrowCompletionOr IteratorHelperPrototype::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); + + // 3.1.2.1.3 %IteratorHelperPrototype% [ @@toStringTag ], https://tc39.es/proposal-iterator-helpers/#sec-%iteratorhelperprototype%-@@tostringtag + define_direct_property(vm.well_known_symbol_to_string_tag(), MUST_OR_THROW_OOM(PrimitiveString::create(vm, "Iterator Helper"sv)), Attribute::Configurable); + + return {}; +} + +// 3.1.2.1.1 %IteratorHelperPrototype%.next ( ), https://tc39.es/proposal-iterator-helpers/#sec-%iteratorhelperprototype%.next +JS_DEFINE_NATIVE_FUNCTION(IteratorHelperPrototype::next) +{ + auto iterator = TRY(typed_this_object(vm)); + if (iterator->done()) + return create_iterator_result_object(vm, js_undefined(), true); + + // 1. Return ? GeneratorResume(this value, undefined, "Iterator Helper"). + auto result = TRY(iterator->closure()(*iterator)); + return create_iterator_result_object(vm, result, iterator->done()); +} + +// 3.1.2.1.2 %IteratorHelperPrototype%.return ( ), https://tc39.es/proposal-iterator-helpers/#sec-%iteratorhelperprototype%.return +JS_DEFINE_NATIVE_FUNCTION(IteratorHelperPrototype::return_) +{ + // 1. Let O be this value. + // 2. Perform ? RequireInternalSlot(O, [[UnderlyingIterator]]). + // 3. Assert: O has a [[GeneratorState]] slot. + // 4. If O.[[GeneratorState]] is suspendedStart, then + // a. Set O.[[GeneratorState]] to completed. + // b. NOTE: Once a generator enters the completed state it never leaves it and its associated execution context is never resumed. Any execution state associated with O can be discarded at this point. + // c. Perform ? IteratorClose(O.[[UnderlyingIterator]], NormalCompletion(unused)). + // d. Return CreateIterResultObject(undefined, true). + // 5. Let C be Completion { [[Type]]: return, [[Value]]: undefined, [[Target]]: empty }. + // 6. Return ? GeneratorResumeAbrupt(O, C, "Iterator Helper"). + + return vm.throw_completion(ErrorType::NotImplemented, "IteratorHelper.prototype.return"sv); +} + +} diff --git a/Userland/Libraries/LibJS/Runtime/IteratorHelperPrototype.h b/Userland/Libraries/LibJS/Runtime/IteratorHelperPrototype.h new file mode 100644 index 0000000000..269ba7a6ff --- /dev/null +++ b/Userland/Libraries/LibJS/Runtime/IteratorHelperPrototype.h @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2023, Tim Flynn + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include +#include + +namespace JS { + +class IteratorHelperPrototype final : public PrototypeObject { + JS_PROTOTYPE_OBJECT(IteratorHelperPrototype, IteratorHelper, IteratorHelper); + +public: + virtual ThrowCompletionOr initialize(Realm&) override; + +private: + explicit IteratorHelperPrototype(Realm&); + + JS_DECLARE_NATIVE_FUNCTION(next); + JS_DECLARE_NATIVE_FUNCTION(return_); +}; + +} diff --git a/Userland/Libraries/LibJS/Runtime/IteratorPrototype.cpp b/Userland/Libraries/LibJS/Runtime/IteratorPrototype.cpp index 561f8027bf..594915304a 100644 --- a/Userland/Libraries/LibJS/Runtime/IteratorPrototype.cpp +++ b/Userland/Libraries/LibJS/Runtime/IteratorPrototype.cpp @@ -5,14 +5,18 @@ */ #include +#include +#include #include +#include +#include #include namespace JS { // 27.1.2 The %IteratorPrototype% Object, https://tc39.es/ecma262/#sec-%iteratorprototype%-object IteratorPrototype::IteratorPrototype(Realm& realm) - : Object(ConstructWithPrototypeTag::Tag, realm.intrinsics().object_prototype()) + : PrototypeObject(realm.intrinsics().object_prototype()) { } @@ -26,6 +30,7 @@ ThrowCompletionOr IteratorPrototype::initialize(Realm& realm) u8 attr = Attribute::Writable | Attribute::Configurable; define_native_function(realm, vm.well_known_symbol_iterator(), symbol_iterator, 0, attr); + define_native_function(realm, vm.names.map, map, 1, attr); return {}; } @@ -37,4 +42,65 @@ JS_DEFINE_NATIVE_FUNCTION(IteratorPrototype::symbol_iterator) return vm.this_value(); } +// 3.1.3.2 Iterator.prototype.map ( mapper ), https://tc39.es/proposal-iterator-helpers/#sec-iteratorprototype.map +JS_DEFINE_NATIVE_FUNCTION(IteratorPrototype::map) +{ + auto& realm = *vm.current_realm(); + + auto mapper = vm.argument(0); + + // 1. Let O be the this value. + // 2. If O is not an Object, throw a TypeError exception. + auto object = TRY(this_object(vm)); + + // 3. If IsCallable(mapper) is false, throw a TypeError exception. + if (!mapper.is_function()) + return vm.throw_completion(ErrorType::NotAFunction, "mapper"sv); + + // 4. Let iterated be ? GetIteratorDirect(O). + auto iterated = TRY(get_iterator_direct(vm, object)); + + // 5. Let closure be a new Abstract Closure with no parameters that captures iterated and mapper and performs the following steps when called: + IteratorHelper::Closure closure = [mapper = NonnullGCPtr { mapper.as_function() }](auto& iterator) -> ThrowCompletionOr { + auto& vm = iterator.vm(); + + auto const& iterated = iterator.underlying_iterator(); + + // a. Let counter be 0. + // b. Repeat, + + // i. Let next be ? IteratorStep(iterated). + auto next = TRY(iterator_step(vm, iterated)); + + // ii. If next is false, return undefined. + if (!next) + return iterator.result(js_undefined()); + + // iii. Let value be ? IteratorValue(next). + auto value = TRY(iterator_value(vm, *next)); + + // iv. Let mapped be Completion(Call(mapper, undefined, « value, 𝔽(counter) »)). + auto mapped = call(vm, *mapper, js_undefined(), value, Value { iterator.counter() }); + + // v. IfAbruptCloseIterator(mapped, iterated). + if (mapped.is_error()) + return iterator.close_result(mapped.release_error()); + + // viii. Set counter to counter + 1. + // NOTE: We do this step early to ensure it occurs before returning. + iterator.increment_counter(); + + // vi. Let completion be Completion(Yield(mapped)). + // vii. IfAbruptCloseIterator(completion, iterated). + return iterator.result(mapped.release_value()); + }; + + // 6. Let result be CreateIteratorFromClosure(closure, "Iterator Helper", %IteratorHelperPrototype%, « [[UnderlyingIterator]] »). + // 7. Set result.[[UnderlyingIterator]] to iterated. + auto result = TRY(IteratorHelper::create(realm, move(iterated), move(closure))); + + // 8. Return result. + return result; +} + } diff --git a/Userland/Libraries/LibJS/Runtime/IteratorPrototype.h b/Userland/Libraries/LibJS/Runtime/IteratorPrototype.h index 5cb870070e..e460400e32 100644 --- a/Userland/Libraries/LibJS/Runtime/IteratorPrototype.h +++ b/Userland/Libraries/LibJS/Runtime/IteratorPrototype.h @@ -6,12 +6,13 @@ #pragma once -#include +#include +#include namespace JS { -class IteratorPrototype : public Object { - JS_OBJECT(IteratorPrototype, Object) +class IteratorPrototype : public PrototypeObject { + JS_PROTOTYPE_OBJECT(IteratorPrototype, Iterator, Iterator); public: virtual ThrowCompletionOr initialize(Realm&) override; @@ -21,6 +22,7 @@ private: IteratorPrototype(Realm&); JS_DECLARE_NATIVE_FUNCTION(symbol_iterator); + JS_DECLARE_NATIVE_FUNCTION(map); }; } diff --git a/Userland/Libraries/LibJS/Tests/builtins/Iterator/Iterator.prototype.map.js b/Userland/Libraries/LibJS/Tests/builtins/Iterator/Iterator.prototype.map.js new file mode 100644 index 0000000000..c3a14fcc09 --- /dev/null +++ b/Userland/Libraries/LibJS/Tests/builtins/Iterator/Iterator.prototype.map.js @@ -0,0 +1,144 @@ +describe("errors", () => { + test("called with non-callable object", () => { + expect(() => { + Iterator.prototype.map(Symbol.hasInstance); + }).toThrowWithMessage(TypeError, "mapper is not a function"); + }); + + test("iterator's next method throws", () => { + function TestError() {} + + class TestIterator extends Iterator { + next() { + throw new TestError(); + } + } + + expect(() => { + const iterator = new TestIterator().map(() => 0); + iterator.next(); + }).toThrow(TestError); + }); + + test("value returned by iterator's next method throws", () => { + function TestError() {} + + class TestIterator extends Iterator { + next() { + return { + done: false, + get value() { + throw new TestError(); + }, + }; + } + } + + expect(() => { + const iterator = new TestIterator().map(() => 0); + iterator.next(); + }).toThrow(TestError); + }); + + test("mapper function throws", () => { + function TestError() {} + + class TestIterator extends Iterator { + next() { + return { + done: false, + value: 1, + }; + } + } + + expect(() => { + const iterator = new TestIterator().map(() => { + throw new TestError(); + }); + iterator.next(); + }).toThrow(TestError); + }); +}); + +describe("normal behavior", () => { + test("length is 1", () => { + expect(Iterator.prototype.map).toHaveLength(1); + }); + + test("mapper function sees every value", () => { + function* generator() { + yield "a"; + yield "b"; + } + + let count = 0; + + const iterator = generator().map((value, index) => { + ++count; + + switch (index) { + case 0: + expect(value).toBe("a"); + break; + case 1: + expect(value).toBe("b"); + break; + default: + expect().fail(`Unexpected mapper invocation: value=${value} index=${index}`); + break; + } + + return value; + }); + + for (const i of iterator) { + } + + expect(count).toBe(2); + }); + + test("mapper function can modify values", () => { + function* generator() { + yield "a"; + yield "b"; + } + + const iterator = generator().map(value => value.toUpperCase()); + + let value = iterator.next(); + expect(value.value).toBe("A"); + expect(value.done).toBeFalse(); + + value = iterator.next(); + expect(value.value).toBe("B"); + expect(value.done).toBeFalse(); + + value = iterator.next(); + expect(value.value).toBeUndefined(); + expect(value.done).toBeTrue(); + }); + + test("mappers can be chained", () => { + function* generator() { + yield 1; + yield 2; + } + + const iterator = generator() + .map(value => value * 2) + .map(value => value + 10); + + let value = iterator.next(); + expect(value.value).toBe(12); + expect(value.done).toBeFalse(); + + value = iterator.next(); + expect(value.value).toBe(14); + expect(value.done).toBeFalse(); + + value = iterator.next(); + expect(value.value).toBeUndefined(); + expect(value.done).toBeTrue(); + }); +});