diff --git a/Userland/Libraries/LibJS/Runtime/IteratorPrototype.cpp b/Userland/Libraries/LibJS/Runtime/IteratorPrototype.cpp index 594915304a..a084bba958 100644 --- a/Userland/Libraries/LibJS/Runtime/IteratorPrototype.cpp +++ b/Userland/Libraries/LibJS/Runtime/IteratorPrototype.cpp @@ -31,6 +31,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); + define_native_function(realm, vm.names.filter, filter, 1, attr); return {}; } @@ -103,4 +104,70 @@ JS_DEFINE_NATIVE_FUNCTION(IteratorPrototype::map) return result; } +// 3.1.3.3 Iterator.prototype.filter ( predicate ), https://tc39.es/proposal-iterator-helpers/#sec-iteratorprototype.filter +JS_DEFINE_NATIVE_FUNCTION(IteratorPrototype::filter) +{ + auto& realm = *vm.current_realm(); + + auto predicate = 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(predicate) is false, throw a TypeError exception. + if (!predicate.is_function()) + return vm.throw_completion(ErrorType::NotAFunction, "predicate"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 predicate and performs the following steps when called: + IteratorHelper::Closure closure = [predicate = NonnullGCPtr { predicate.as_function() }](auto& iterator) -> ThrowCompletionOr { + auto& vm = iterator.vm(); + + auto const& iterated = iterator.underlying_iterator(); + + // a. Let counter be 0. + + // b. Repeat, + while (true) { + // 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 selected be Completion(Call(predicate, undefined, « value, 𝔽(counter) »)). + auto selected = call(vm, *predicate, js_undefined(), value, Value { iterator.counter() }); + + // v. IfAbruptCloseIterator(selected, iterated). + if (selected.is_error()) + return iterator.close_result(selected.release_error()); + + // vii. Set counter to counter + 1. + // NOTE: We do this step early to ensure it occurs before returning. + iterator.increment_counter(); + + // vi. If ToBoolean(selected) is true, then + if (selected.value().to_boolean()) { + // 1. Let completion be Completion(Yield(value)). + // 2. IfAbruptCloseIterator(completion, iterated). + return iterator.result(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 e460400e32..e79830ae51 100644 --- a/Userland/Libraries/LibJS/Runtime/IteratorPrototype.h +++ b/Userland/Libraries/LibJS/Runtime/IteratorPrototype.h @@ -23,6 +23,7 @@ private: JS_DECLARE_NATIVE_FUNCTION(symbol_iterator); JS_DECLARE_NATIVE_FUNCTION(map); + JS_DECLARE_NATIVE_FUNCTION(filter); }; } diff --git a/Userland/Libraries/LibJS/Tests/builtins/Iterator/Iterator.prototype.filter.js b/Userland/Libraries/LibJS/Tests/builtins/Iterator/Iterator.prototype.filter.js new file mode 100644 index 0000000000..5828874030 --- /dev/null +++ b/Userland/Libraries/LibJS/Tests/builtins/Iterator/Iterator.prototype.filter.js @@ -0,0 +1,156 @@ +describe("errors", () => { + test("called with non-callable object", () => { + expect(() => { + Iterator.prototype.filter(Symbol.hasInstance); + }).toThrowWithMessage(TypeError, "predicate 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().filter(() => true); + 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().filter(() => true); + iterator.next(); + }).toThrow(TestError); + }); + + test("predicate function throws", () => { + function TestError() {} + + class TestIterator extends Iterator { + next() { + return { + done: false, + value: 1, + }; + } + } + + expect(() => { + const iterator = new TestIterator().filter(() => { + throw new TestError(); + }); + iterator.next(); + }).toThrow(TestError); + }); +}); + +describe("normal behavior", () => { + test("length is 1", () => { + expect(Iterator.prototype.filter).toHaveLength(1); + }); + + test("predicate function sees every value", () => { + function* generator() { + yield "a"; + yield "b"; + } + + let count = 0; + + const iterator = generator().filter((value, index) => { + ++count; + + switch (index) { + case 0: + expect(value).toBe("a"); + break; + case 1: + expect(value).toBe("b"); + break; + default: + expect().fail(`Unexpected predicate invocation: value=${value} index=${index}`); + break; + } + + return true; + }); + + for (const i of iterator) { + } + + expect(count).toBe(2); + }); + + test("predicate function can select values", () => { + function* generator() { + yield 1; + yield 0; + yield 2; + yield 0; + } + + const iterator = generator().filter(value => value > 0); + + let value = iterator.next(); + expect(value.value).toBe(1); + expect(value.done).toBeFalse(); + + value = iterator.next(); + expect(value.value).toBe(2); + expect(value.done).toBeFalse(); + + value = iterator.next(); + expect(value.value).toBeUndefined(); + expect(value.done).toBeTrue(); + }); + + test("predicates can be chained", () => { + function* generator() { + yield 1; + yield 0; + yield 2; + yield 0; + } + + let firstFilterCount = 0; + let secondFilterCount = 0; + + const iterator = generator() + .filter(value => { + ++firstFilterCount; + return value > 0; + }) + .filter(value => { + ++secondFilterCount; + return value > 1; + }); + + let value = iterator.next(); + expect(value.value).toBe(2); + expect(value.done).toBeFalse(); + + value = iterator.next(); + expect(value.value).toBeUndefined(); + expect(value.done).toBeTrue(); + + expect(firstFilterCount).toBe(4); + expect(secondFilterCount).toBe(2); + }); +});