From 0e63d04a35153e74e68b0f5badb4e896117f7513 Mon Sep 17 00:00:00 2001 From: Timothy Flynn Date: Sun, 16 Jul 2023 16:06:06 -0400 Subject: [PATCH] LibJS: Implement %IteratorHelperPrototype%.return --- .../LibJS/Runtime/IteratorHelperPrototype.cpp | 24 ++++++--- .../Iterator/Iterator.prototype.drop.js | 49 +++++++++++++++++++ .../Iterator/Iterator.prototype.filter.js | 49 +++++++++++++++++++ .../Iterator/Iterator.prototype.flatMap.js | 49 +++++++++++++++++++ .../Iterator/Iterator.prototype.map.js | 49 +++++++++++++++++++ .../Iterator/Iterator.prototype.take.js | 49 +++++++++++++++++++ 6 files changed, 263 insertions(+), 6 deletions(-) diff --git a/Userland/Libraries/LibJS/Runtime/IteratorHelperPrototype.cpp b/Userland/Libraries/LibJS/Runtime/IteratorHelperPrototype.cpp index bc2edae1ab..cd406e7510 100644 --- a/Userland/Libraries/LibJS/Runtime/IteratorHelperPrototype.cpp +++ b/Userland/Libraries/LibJS/Runtime/IteratorHelperPrototype.cpp @@ -44,16 +44,28 @@ JS_DEFINE_NATIVE_FUNCTION(IteratorHelperPrototype::return_) { // 1. Let O be this value. // 2. Perform ? RequireInternalSlot(O, [[UnderlyingIterator]]). + auto iterator = TRY(typed_this_object(vm)); + // 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). + if (iterator->generator_state() == GeneratorObject::GeneratorState::SuspendedStart) { + // a. Set O.[[GeneratorState]] to completed. + iterator->set_generator_state(GeneratorObject::GeneratorState::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)). + TRY(iterator_close(vm, iterator->underlying_iterator(), normal_completion({}))); + + // d. Return CreateIterResultObject(undefined, true). + return create_iterator_result_object(vm, js_undefined(), true); + } + // 5. Let C be Completion { [[Type]]: return, [[Value]]: undefined, [[Target]]: empty }. + Completion completion { Completion::Type::Return, js_undefined(), {} }; + // 6. Return ? GeneratorResumeAbrupt(O, C, "Iterator Helper"). - - return vm.throw_completion(ErrorType::NotImplemented, "IteratorHelper.prototype.return"sv); + return TRY(iterator->resume_abrupt(vm, move(completion), "Iterator Helper"sv)); } } diff --git a/Userland/Libraries/LibJS/Tests/builtins/Iterator/Iterator.prototype.drop.js b/Userland/Libraries/LibJS/Tests/builtins/Iterator/Iterator.prototype.drop.js index db2aecb3ea..c9f360ce16 100644 --- a/Userland/Libraries/LibJS/Tests/builtins/Iterator/Iterator.prototype.drop.js +++ b/Userland/Libraries/LibJS/Tests/builtins/Iterator/Iterator.prototype.drop.js @@ -49,6 +49,28 @@ describe("errors", () => { iterator.next(); }).toThrow(TestError); }); + + test("iterator's return method throws", () => { + function TestError() {} + + class TestIterator extends Iterator { + next() { + return { + done: false, + value: 1, + }; + } + + return() { + throw new TestError(); + } + } + + expect(() => { + const iterator = new TestIterator().drop(1); + iterator.return(); + }).toThrow(TestError); + }); }); describe("normal behavior", () => { @@ -115,4 +137,31 @@ describe("normal behavior", () => { expect(value.value).toBeUndefined(); expect(value.done).toBeTrue(); }); + + test("return is forwarded to the underlying iterator's return method", () => { + let returnCount = 0; + + class TestIterator extends Iterator { + next() { + return { + done: false, + value: 1, + }; + } + + return() { + ++returnCount; + return {}; + } + } + + const iterator = new TestIterator().drop(1); + expect(returnCount).toBe(0); + + iterator.return(); + expect(returnCount).toBe(1); + + iterator.return(); + expect(returnCount).toBe(1); + }); }); diff --git a/Userland/Libraries/LibJS/Tests/builtins/Iterator/Iterator.prototype.filter.js b/Userland/Libraries/LibJS/Tests/builtins/Iterator/Iterator.prototype.filter.js index 5828874030..a40cb2dfdc 100644 --- a/Userland/Libraries/LibJS/Tests/builtins/Iterator/Iterator.prototype.filter.js +++ b/Userland/Libraries/LibJS/Tests/builtins/Iterator/Iterator.prototype.filter.js @@ -40,6 +40,28 @@ describe("errors", () => { }).toThrow(TestError); }); + test("iterator's return method throws", () => { + function TestError() {} + + class TestIterator extends Iterator { + next() { + return { + done: false, + value: 1, + }; + } + + return() { + throw new TestError(); + } + } + + expect(() => { + const iterator = new TestIterator().filter(() => true); + iterator.return(); + }).toThrow(TestError); + }); + test("predicate function throws", () => { function TestError() {} @@ -153,4 +175,31 @@ describe("normal behavior", () => { expect(firstFilterCount).toBe(4); expect(secondFilterCount).toBe(2); }); + + test("return is forwarded to the underlying iterator's return method", () => { + let returnCount = 0; + + class TestIterator extends Iterator { + next() { + return { + done: false, + value: 1, + }; + } + + return() { + ++returnCount; + return {}; + } + } + + const iterator = new TestIterator().filter(() => true); + expect(returnCount).toBe(0); + + iterator.return(); + expect(returnCount).toBe(1); + + iterator.return(); + expect(returnCount).toBe(1); + }); }); diff --git a/Userland/Libraries/LibJS/Tests/builtins/Iterator/Iterator.prototype.flatMap.js b/Userland/Libraries/LibJS/Tests/builtins/Iterator/Iterator.prototype.flatMap.js index 495d42a194..f70023d181 100644 --- a/Userland/Libraries/LibJS/Tests/builtins/Iterator/Iterator.prototype.flatMap.js +++ b/Userland/Libraries/LibJS/Tests/builtins/Iterator/Iterator.prototype.flatMap.js @@ -40,6 +40,28 @@ describe("errors", () => { }).toThrow(TestError); }); + test("iterator's return method throws", () => { + function TestError() {} + + class TestIterator extends Iterator { + next() { + return { + done: false, + value: 1, + }; + } + + return() { + throw new TestError(); + } + } + + expect(() => { + const iterator = new TestIterator().flatMap(() => 0); + iterator.return(); + }).toThrow(TestError); + }); + test("mapper function throws", () => { function TestError() {} @@ -138,4 +160,31 @@ describe("normal behavior", () => { expect(value.value).toBeUndefined(); expect(value.done).toBeTrue(); }); + + test("return is forwarded to the underlying iterator's return method", () => { + let returnCount = 0; + + class TestIterator extends Iterator { + next() { + return { + done: false, + value: 1, + }; + } + + return() { + ++returnCount; + return {}; + } + } + + const iterator = new TestIterator().flatMap(() => 0); + expect(returnCount).toBe(0); + + iterator.return(); + expect(returnCount).toBe(1); + + iterator.return(); + expect(returnCount).toBe(1); + }); }); diff --git a/Userland/Libraries/LibJS/Tests/builtins/Iterator/Iterator.prototype.map.js b/Userland/Libraries/LibJS/Tests/builtins/Iterator/Iterator.prototype.map.js index c3a14fcc09..9fde57ff25 100644 --- a/Userland/Libraries/LibJS/Tests/builtins/Iterator/Iterator.prototype.map.js +++ b/Userland/Libraries/LibJS/Tests/builtins/Iterator/Iterator.prototype.map.js @@ -40,6 +40,28 @@ describe("errors", () => { }).toThrow(TestError); }); + test("iterator's return method throws", () => { + function TestError() {} + + class TestIterator extends Iterator { + next() { + return { + done: false, + value: 1, + }; + } + + return() { + throw new TestError(); + } + } + + expect(() => { + const iterator = new TestIterator().map(() => 0); + iterator.return(); + }).toThrow(TestError); + }); + test("mapper function throws", () => { function TestError() {} @@ -141,4 +163,31 @@ describe("normal behavior", () => { expect(value.value).toBeUndefined(); expect(value.done).toBeTrue(); }); + + test("return is forwarded to the underlying iterator's return method", () => { + let returnCount = 0; + + class TestIterator extends Iterator { + next() { + return { + done: false, + value: 1, + }; + } + + return() { + ++returnCount; + return {}; + } + } + + const iterator = new TestIterator().map(() => 0); + expect(returnCount).toBe(0); + + iterator.return(); + expect(returnCount).toBe(1); + + iterator.return(); + expect(returnCount).toBe(1); + }); }); diff --git a/Userland/Libraries/LibJS/Tests/builtins/Iterator/Iterator.prototype.take.js b/Userland/Libraries/LibJS/Tests/builtins/Iterator/Iterator.prototype.take.js index 390af4252f..850b68f81b 100644 --- a/Userland/Libraries/LibJS/Tests/builtins/Iterator/Iterator.prototype.take.js +++ b/Userland/Libraries/LibJS/Tests/builtins/Iterator/Iterator.prototype.take.js @@ -49,6 +49,28 @@ describe("errors", () => { iterator.next(); }).toThrow(TestError); }); + + test("iterator's return method throws", () => { + function TestError() {} + + class TestIterator extends Iterator { + next() { + return { + done: false, + value: 1, + }; + } + + return() { + throw new TestError(); + } + } + + expect(() => { + const iterator = new TestIterator().take(1); + iterator.return(); + }).toThrow(TestError); + }); }); describe("normal behavior", () => { @@ -120,4 +142,31 @@ describe("normal behavior", () => { expect(value.value).toBeUndefined(); expect(value.done).toBeTrue(); }); + + test("return is forwarded to the underlying iterator's return method", () => { + let returnCount = 0; + + class TestIterator extends Iterator { + next() { + return { + done: false, + value: 1, + }; + } + + return() { + ++returnCount; + return {}; + } + } + + const iterator = new TestIterator().take(1); + expect(returnCount).toBe(0); + + iterator.return(); + expect(returnCount).toBe(1); + + iterator.return(); + expect(returnCount).toBe(1); + }); });