Skip to content

Commit a6b2312

Browse files
author
Alexey Shvayka
committed
[JSC] Iterator.prototype.reduce() doesn't properly forward return() call to underlying iterator
https://bugs.webkit.org/show_bug.cgi?id=280813 <rdar://problem/137181340> Reviewed by Yusuke Suzuki. If an exception occurs in `reducer` [1], for/of correctly retrieves "return" method from the underlying iterator (via forwarding getter on the wrapper), yet it calls the method with `this` value of the wrapper instead of underlying iterator, which for example makes Generator.prototype.return() throw a TypeError. This is a very subtle bug because if IteratorClose is called due to throw completion, it "swallows" throw completion of return() call [2]. While this can be fixed by rewriting iterator wrapper just like one in flatMap(), this patch instead utilizes @iteratorGenericNext() and @ifAbruptCloseIterator() to get rid of the wrapper for performance and code expressiveness reasons. [1]: https://tc39.es/proposal-iterator-helpers/#sec-iteratorprototype.reduce (steps 7.c-d) [2]: https://tc39.es/ecma262/#sec-iteratorclose (steps 5-6) * JSTests/stress/iterator-prototype-reduce.js: (sameValue.const.gen): (sameValue): * Source/JavaScriptCore/builtins/JSIteratorPrototype.js: (reduce): (reduce.wrapper.return.next): Deleted. (reduce.wrapper.return.get return): Deleted. (reduce.wrapper.iterator): Deleted. Canonical link: https://commits.webkit.org/284648@main
1 parent 2ad9fbc commit a6b2312

File tree

2 files changed

+35
-15
lines changed

2 files changed

+35
-15
lines changed

JSTests/stress/iterator-prototype-reduce.js

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,3 +136,27 @@ function shouldThrow(fn, error, message) {
136136
sameValue(nextGetCount, 1);
137137
sameValue(returnGetCount, 1);
138138
}
139+
140+
{
141+
let finallyReached = false;
142+
let yield3Reached = false;
143+
const gen = (function*() {
144+
yield 1;
145+
146+
try { yield 2; }
147+
finally { finallyReached = true; }
148+
149+
yield3Reached = true;
150+
yield 3;
151+
})();
152+
153+
shouldThrow(() => {
154+
gen.reduce((_, value) => {
155+
if (value === 2)
156+
throw new Error("my error");
157+
});
158+
}, Error, "my error");
159+
160+
assert(finallyReached, "Generator.prototype.return() invoked the generator function");
161+
sameValue(yield3Reached, false);
162+
}

Source/JavaScriptCore/builtins/JSIteratorPrototype.js

Lines changed: 11 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -283,33 +283,29 @@ function reduce(reducer /*, initialValue */)
283283

284284
var initialValue = @argument(1);
285285

286-
var iteratedIterator = this;
286+
var iterated = this;
287287
var iteratedNextMethod = this.next;
288288

289289
var accumulator;
290290
var counter = 0;
291291
if (initialValue === @undefined) {
292-
var result = iteratedNextMethod.@call(iteratedIterator);
293-
if (!@isObject(result))
294-
@throwTypeError("Iterator result interface is not an object.");
292+
var result = @iteratorGenericNext(iteratedNextMethod, iterated);
295293
if (result.done)
296294
@throwTypeError("Iterator.prototype.reduce requires an initial value or an iterator that is not done.");
297295
accumulator = result.value;
298296
counter = 1;
299297
} else
300298
accumulator = initialValue;
301299

302-
var wrapper = {
303-
@@iterator: function()
304-
{
305-
return {
306-
next: function() { return iteratedNextMethod.@call(iteratedIterator); },
307-
get return() { return iteratedIterator.return; },
308-
};
309-
},
310-
};
311-
for (var item of wrapper) {
312-
accumulator = reducer(accumulator, item, counter++);
300+
for (;;) {
301+
var result = @iteratorGenericNext(iteratedNextMethod, iterated);
302+
if (result.done)
303+
break;
304+
305+
var value = result.value;
306+
@ifAbruptCloseIterator(iterated, (
307+
accumulator = reducer(accumulator, value, counter++)
308+
));
313309
}
314310

315311
return accumulator;

0 commit comments

Comments
 (0)