From acc05480e832ba5a3ffb047a6f60d79e07c0ae70 Mon Sep 17 00:00:00 2001 From: Timothy Flynn Date: Sun, 25 Jun 2023 13:41:01 -0400 Subject: [PATCH] LibJS: Implement Iterator.prototype.reduce --- .../LibJS/Runtime/IteratorPrototype.cpp | 71 ++++++++++ .../LibJS/Runtime/IteratorPrototype.h | 1 + .../Iterator/Iterator.prototype.reduce.js | 124 ++++++++++++++++++ 3 files changed, 196 insertions(+) create mode 100644 Userland/Libraries/LibJS/Tests/builtins/Iterator/Iterator.prototype.reduce.js diff --git a/Userland/Libraries/LibJS/Runtime/IteratorPrototype.cpp b/Userland/Libraries/LibJS/Runtime/IteratorPrototype.cpp index a7a6d79561..b6a5dae112 100644 --- a/Userland/Libraries/LibJS/Runtime/IteratorPrototype.cpp +++ b/Userland/Libraries/LibJS/Runtime/IteratorPrototype.cpp @@ -35,6 +35,7 @@ ThrowCompletionOr IteratorPrototype::initialize(Realm& realm) define_native_function(realm, vm.names.take, take, 1, attr); define_native_function(realm, vm.names.drop, drop, 1, attr); define_native_function(realm, vm.names.flatMap, flat_map, 1, attr); + define_native_function(realm, vm.names.reduce, reduce, 1, attr); return {}; } @@ -445,4 +446,74 @@ JS_DEFINE_NATIVE_FUNCTION(IteratorPrototype::flat_map) return result; } +// 3.1.3.7 Iterator.prototype.reduce ( reducer [ , initialValue ] ), https://tc39.es/proposal-iterator-helpers/#sec-iteratorprototype.reduce +JS_DEFINE_NATIVE_FUNCTION(IteratorPrototype::reduce) +{ + auto reducer = 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(reducer) is false, throw a TypeError exception. + if (!reducer.is_function()) + return vm.throw_completion(ErrorType::NotAFunction, "reducer"sv); + + // 4. Let iterated be ? GetIteratorDirect(O). + auto iterated = TRY(get_iterator_direct(vm, object)); + + Value accumulator; + size_t counter = 0; + + // 5. If initialValue is not present, then + if (vm.argument_count() < 2) { + // a. Let next be ? IteratorStep(iterated). + auto next = TRY(iterator_step(vm, iterated)); + + // b. If next is false, throw a TypeError exception. + if (!next) + return vm.throw_completion(ErrorType::ReduceNoInitial); + + // c. Let accumulator be ? IteratorValue(next). + accumulator = TRY(iterator_value(vm, *next)); + + // d. Let counter be 1. + counter = 1; + } + // 6. Else, + else { + // a. Let accumulator be initialValue. + accumulator = vm.argument(1); + + // b. Let counter be 0. + counter = 0; + } + + // 7. Repeat, + while (true) { + // a. Let next be ? IteratorStep(iterated). + auto next = TRY(iterator_step(vm, iterated)); + + // b. If next is false, return accumulator. + if (!next) + return accumulator; + + // c. Let value be ? IteratorValue(next). + auto value = TRY(iterator_value(vm, *next)); + + // d. Let result be Completion(Call(reducer, undefined, « accumulator, value, 𝔽(counter) »)). + auto result = call(vm, reducer.as_function(), js_undefined(), accumulator, value, Value { counter }); + + // e. IfAbruptCloseIterator(result, iterated). + if (result.is_error()) + return *TRY(iterator_close(vm, iterated, result.release_error())); + + // f. Set accumulator to result.[[Value]]. + accumulator = result.release_value(); + + // g. Set counter to counter + 1. + ++counter; + } +} + } diff --git a/Userland/Libraries/LibJS/Runtime/IteratorPrototype.h b/Userland/Libraries/LibJS/Runtime/IteratorPrototype.h index 7cf718ba87..5785bdf6ba 100644 --- a/Userland/Libraries/LibJS/Runtime/IteratorPrototype.h +++ b/Userland/Libraries/LibJS/Runtime/IteratorPrototype.h @@ -27,6 +27,7 @@ private: JS_DECLARE_NATIVE_FUNCTION(take); JS_DECLARE_NATIVE_FUNCTION(drop); JS_DECLARE_NATIVE_FUNCTION(flat_map); + JS_DECLARE_NATIVE_FUNCTION(reduce); }; } diff --git a/Userland/Libraries/LibJS/Tests/builtins/Iterator/Iterator.prototype.reduce.js b/Userland/Libraries/LibJS/Tests/builtins/Iterator/Iterator.prototype.reduce.js new file mode 100644 index 0000000000..bea171d610 --- /dev/null +++ b/Userland/Libraries/LibJS/Tests/builtins/Iterator/Iterator.prototype.reduce.js @@ -0,0 +1,124 @@ +describe("errors", () => { + test("called with non-callable object", () => { + expect(() => { + Iterator.prototype.reduce(Symbol.hasInstance); + }).toThrowWithMessage(TypeError, "reducer is not a function"); + }); + + test("iterator's next method throws", () => { + function TestError() {} + + class TestIterator extends Iterator { + next() { + throw new TestError(); + } + } + + expect(() => { + new TestIterator().reduce(() => 0); + }).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(() => { + new TestIterator().reduce(() => 0); + }).toThrow(TestError); + }); + + test("reducer function throws", () => { + function TestError() {} + + class TestIterator extends Iterator { + next() { + return { + done: false, + value: 1, + }; + } + } + + expect(() => { + new TestIterator().reduce(() => { + throw new TestError(); + }); + }).toThrow(TestError); + }); + + test("no available initial value", () => { + function* generator() {} + + expect(() => { + generator().reduce(() => 0); + }).toThrowWithMessage(TypeError, "Reduce of empty array with no initial value"); + }); +}); + +describe("normal behavior", () => { + test("length is 1", () => { + expect(Iterator.prototype.reduce).toHaveLength(1); + }); + + test("reducer function sees every value", () => { + function* generator() { + yield "a"; + yield "b"; + } + + let count = 0; + + generator().reduce((accumulator, value, index) => { + ++count; + + switch (index) { + case 0: + expect(value).toBe("a"); + break; + case 1: + expect(value).toBe("b"); + break; + default: + expect().fail(`Unexpected reducer invocation: value=${value} index=${index}`); + break; + } + + return value; + }, ""); + + expect(count).toBe(2); + }); + + test("reducer uses first value as initial value", () => { + function* generator() { + yield 1; + yield 2; + yield 3; + } + + const result = generator().reduce((accumulator, value) => accumulator + value); + expect(result).toBe(6); + }); + + test("reducer uses provided value as initial value", () => { + function* generator() { + yield 1; + yield 2; + yield 3; + } + + const result = generator().reduce((accumulator, value) => accumulator + value, 10); + expect(result).toBe(16); + }); +});