From 75c9377766326589faa844a95d5997a156f6aed0 Mon Sep 17 00:00:00 2001 From: janekptacijarabaci Date: Thu, 15 Mar 2018 21:11:35 +0100 Subject: Close iterator after error in {Map,Set,WeakMap,WeakSet} constructors Issue #17 --- js/src/builtin/Map.js | 11 +- js/src/builtin/Set.js | 7 +- js/src/builtin/Utilities.js | 23 ++ js/src/builtin/WeakMap.js | 11 +- js/src/builtin/WeakSet.js | 7 +- .../tests/ecma_6/Map/constructor-iterator-close.js | 253 +++++++++++++++++++++ 6 files changed, 306 insertions(+), 6 deletions(-) create mode 100644 js/src/tests/ecma_6/Map/constructor-iterator-close.js (limited to 'js') diff --git a/js/src/builtin/Map.js b/js/src/builtin/Map.js index 432364614..27a12bfff 100644 --- a/js/src/builtin/Map.js +++ b/js/src/builtin/Map.js @@ -40,11 +40,18 @@ function MapConstructorInit(iterable) { var nextItem = next.value; // Step 8.d. - if (!IsObject(nextItem)) + if (!IsObject(nextItem)) { + IteratorCloseThrow(iter); ThrowTypeError(JSMSG_INVALID_MAP_ITERABLE, "Map"); + } // Steps 8.e-j. - callContentFunction(adder, map, nextItem[0], nextItem[1]); + try { + callContentFunction(adder, map, nextItem[0], nextItem[1]); + } catch (e) { + IteratorCloseThrow(iter); + throw e; + } } } diff --git a/js/src/builtin/Set.js b/js/src/builtin/Set.js index accc70120..c61a49ef8 100644 --- a/js/src/builtin/Set.js +++ b/js/src/builtin/Set.js @@ -40,7 +40,12 @@ function SetConstructorInit(iterable) { var nextValue = next.value; // Steps 8.d-e. - callContentFunction(adder, set, nextValue); + try { + callContentFunction(adder, set, nextValue); + } catch (e) { + IteratorCloseThrow(iter); + throw e; + } } } diff --git a/js/src/builtin/Utilities.js b/js/src/builtin/Utilities.js index bfb1fe7f4..2dece3801 100644 --- a/js/src/builtin/Utilities.js +++ b/js/src/builtin/Utilities.js @@ -154,6 +154,29 @@ function GetIterator(obj, method) { return iterator; } +// ES2017 draft rev 7.4.6. +// When completion.[[Type]] is throw. +function IteratorCloseThrow(iter) { + // Steps 1-2 (implicit) + + // Step 3. + var returnMethod = GetMethod(iter, "return"); + + // Step 4 (done in caller). + if (returnMethod === undefined) + return; + + try { + // Step 5. + callContentFunction(returnMethod, iter); + } catch (e) { + } + + // Step 6 (done in caller). + + // Steps 7-9 (skipped). +} + var _builtinCtorsCache = {__proto__: null}; function GetBuiltinConstructor(builtinName) { diff --git a/js/src/builtin/WeakMap.js b/js/src/builtin/WeakMap.js index 066a72bfe..708be8424 100644 --- a/js/src/builtin/WeakMap.js +++ b/js/src/builtin/WeakMap.js @@ -40,10 +40,17 @@ function WeakMapConstructorInit(iterable) { var nextItem = next.value; // Step 8.d. - if (!IsObject(nextItem)) + if (!IsObject(nextItem)) { + IteratorCloseThrow(iter); ThrowTypeError(JSMSG_INVALID_MAP_ITERABLE, "WeakMap"); + } // Steps 8.e-j. - callContentFunction(adder, map, nextItem[0], nextItem[1]); + try { + callContentFunction(adder, map, nextItem[0], nextItem[1]); + } catch (e) { + IteratorCloseThrow(iter); + throw e; + } } } diff --git a/js/src/builtin/WeakSet.js b/js/src/builtin/WeakSet.js index eb7c2378f..8589f9dc6 100644 --- a/js/src/builtin/WeakSet.js +++ b/js/src/builtin/WeakSet.js @@ -40,7 +40,12 @@ function WeakSetConstructorInit(iterable) { var nextValue = next.value; // Steps 8.d-e. - callContentFunction(adder, set, nextValue); + try { + callContentFunction(adder, set, nextValue); + } catch (e) { + IteratorCloseThrow(iter); + throw e; + } } } diff --git a/js/src/tests/ecma_6/Map/constructor-iterator-close.js b/js/src/tests/ecma_6/Map/constructor-iterator-close.js new file mode 100644 index 000000000..ba4bee4a7 --- /dev/null +++ b/js/src/tests/ecma_6/Map/constructor-iterator-close.js @@ -0,0 +1,253 @@ +var BUGNUMBER = 1180306; +var summary = 'Map/Set/WeakMap/WeakSet constructor should close iterator on error'; + +print(BUGNUMBER + ": " + summary); + +function test(ctors, { nextVal=undefined, + nextThrowVal=undefined, + modifier=undefined, + exceptionVal=undefined, + exceptionType=undefined, + closed=true }) { + function getIterable() { + let iterable = { + closed: false, + [Symbol.iterator]() { + let iterator = { + first: true, + next() { + if (this.first) { + this.first = false; + if (nextThrowVal) + throw nextThrowVal; + return nextVal; + } + return { value: undefined, done: true }; + }, + return() { + iterable.closed = true; + return {}; + } + }; + if (modifier) + modifier(iterator, iterable); + + return iterator; + } + }; + return iterable; + } + + for (let ctor of ctors) { + let iterable = getIterable(); + if (exceptionVal) { + let caught = false; + try { + new ctor(iterable); + } catch (e) { + assertEq(e, exceptionVal); + caught = true; + } + assertEq(caught, true); + } else if (exceptionType) { + assertThrowsInstanceOf(() => new ctor(iterable), exceptionType); + } else { + new ctor(iterable); + } + assertEq(iterable.closed, closed); + } +} + +// == Error cases with close == + +// ES 2017 draft 23.1.1.1 Map step 8.d.ii. +// ES 2017 draft 23.3.1.1 WeakMap step 8.d.ii. +test([Map, WeakMap], { + nextVal: { value: "non object", done: false }, + exceptionType: TypeError, + closed: true, +}); + +// ES 2017 draft 23.1.1.1 Map step 8.f. +// ES 2017 draft 23.3.1.1 WeakMap step 8.f. +test([Map, WeakMap], { + nextVal: { value: { get 0() { throw "0 getter throws"; } }, done: false }, + exceptionVal: "0 getter throws", + closed: true, +}); + +// ES 2017 draft 23.1.1.1 Map step 8.h. +// ES 2017 draft 23.3.1.1 WeakMap step 8.h. +test([Map, WeakMap], { + nextVal: { value: { 0: {}, get 1() { throw "1 getter throws"; } }, done: false }, + exceptionVal: "1 getter throws", + closed: true, +}); + +// ES 2017 draft 23.1.1.1 Map step 8.j. +// ES 2017 draft 23.3.1.1 WeakMap step 8.j. +class MyMap extends Map { + set(k, v) { + throw "setter throws"; + } +} +class MyWeakMap extends WeakMap { + set(k, v) { + throw "setter throws"; + } +} +test([MyMap, MyWeakMap], { + nextVal: { value: [{}, {}], done: false }, + exceptionVal: "setter throws", + closed: true, +}); + +// ES 2017 draft 23.2.1.1 Set step 8.e. +// ES 2017 draft 23.4.1.1 WeakSet step 8.e. +class MySet extends Set { + add(v) { + throw "adder throws"; + } +} +class MyWeakSet extends WeakSet { + add(v) { + throw "adder throws"; + } +} +test([MySet, MyWeakSet], { + nextVal: { value: {}, done: false }, + exceptionVal: "adder throws", + closed: true, +}); + +// ES 2017 draft 7.4.6 step 3. +// if GetMethod fails, the thrown value should be used. +test([MyMap, MySet, MyWeakMap, MyWeakSet], { + nextVal: { value: [{}, {}], done: false }, + modifier: (iterator, iterable) => { + Object.defineProperty(iterator, "return", { + get: function() { + iterable.closed = true; + throw "return getter throws"; + } + }); + }, + exceptionVal: "return getter throws", + closed: true, +}); +test([MyMap, MySet, MyWeakMap, MyWeakSet], { + nextVal: { value: [{}, {}], done: false }, + modifier: (iterator, iterable) => { + Object.defineProperty(iterator, "return", { + get: function() { + iterable.closed = true; + return "non object"; + } + }); + }, + exceptionType: TypeError, + closed: true, +}); +test([MyMap, MySet, MyWeakMap, MyWeakSet], { + nextVal: { value: [{}, {}], done: false }, + modifier: (iterator, iterable) => { + Object.defineProperty(iterator, "return", { + get: function() { + iterable.closed = true; + // Non callable. + return {}; + } + }); + }, + exceptionType: TypeError, + closed: true, +}); + +// ES 2017 draft 7.4.6 steps 6. +// if return method throws, the thrown value should be ignored. +test([MyMap, MyWeakMap], { + nextVal: { value: [{}, {}], done: false }, + modifier: (iterator, iterable) => { + iterator.return = function() { + iterable.closed = true; + throw "return throws"; + }; + }, + exceptionVal: "setter throws", + closed: true, +}); +test([MySet, MyWeakSet], { + nextVal: { value: [{}, {}], done: false }, + modifier: (iterator, iterable) => { + iterator.return = function() { + iterable.closed = true; + throw "return throws"; + }; + }, + exceptionVal: "adder throws", + closed: true, +}); + +test([MyMap, MyWeakMap], { + nextVal: { value: [{}, {}], done: false }, + modifier: (iterator, iterable) => { + iterator.return = function() { + iterable.closed = true; + return "non object"; + }; + }, + exceptionVal: "setter throws", + closed: true, +}); +test([MySet, MyWeakSet], { + nextVal: { value: [{}, {}], done: false }, + modifier: (iterator, iterable) => { + iterator.return = function() { + iterable.closed = true; + return "non object"; + }; + }, + exceptionVal: "adder throws", + closed: true, +}); + +// == Error cases without close == + +// ES 2017 draft 23.1.1.1 Map step 8.a. +// ES 2017 draft 23.3.1.1 WeakMap step 8.a. +// ES 2017 draft 23.2.1.1 Set step 8.a. +// ES 2017 draft 23.4.1.1 WeakSet step 8.a. +test([Map, WeakMap, Set, WeakSet], { + nextThrowVal: "next throws", + exceptionVal: "next throws", + closed: false, +}); +test([Map, WeakMap, Set, WeakSet], { + nextVal: { value: {}, get done() { throw "done getter throws"; } }, + exceptionVal: "done getter throws", + closed: false, +}); + +// ES 2017 draft 23.1.1.1 Map step 8.c. +// ES 2017 draft 23.3.1.1 WeakMap step 8.c. +// ES 2017 draft 23.2.1.1 Set step 8.c. +// ES 2017 draft 23.4.1.1 WeakSet step 8.c. +test([Map, WeakMap, Set, WeakSet], { + nextVal: { get value() { throw "value getter throws"; }, done: false }, + exceptionVal: "value getter throws", + closed: false, +}); + +// == Successful cases == + +test([Map, WeakMap], { + nextVal: { value: [{}, {}], done: false }, + closed: false, +}); +test([Set, WeakSet], { + nextVal: { value: {}, done: false }, + closed: false, +}); + +if (typeof reportCompare === 'function') + reportCompare(true, true); -- cgit v1.2.3 From 25550ce903d01f31bead59de945e4adf86819440 Mon Sep 17 00:00:00 2001 From: janekptacijarabaci Date: Thu, 15 Mar 2018 21:12:03 +0100 Subject: Close iterator after error in Array.from Issue #17 --- js/src/builtin/Array.js | 46 ++++-- js/src/tests/ecma_6/Array/from-iterator-close.js | 183 +++++++++++++++++++++++ 2 files changed, 215 insertions(+), 14 deletions(-) create mode 100644 js/src/tests/ecma_6/Array/from-iterator-close.js (limited to 'js') diff --git a/js/src/builtin/Array.js b/js/src/builtin/Array.js index 54b47b72f..5ab0b71be 100644 --- a/js/src/builtin/Array.js +++ b/js/src/builtin/Array.js @@ -784,7 +784,7 @@ function ArrayKeys() { return CreateArrayIterator(this, ITEM_KIND_KEY); } -// ES6 draft rev31 (2015/01/15) 22.1.2.1 Array.from(source[, mapfn[, thisArg]]). +// ES 2017 draft 0f10dba4ad18de92d47d421f378233a2eae8f077 22.1.2.1 function ArrayFrom(items, mapfn=undefined, thisArg=undefined) { // Step 1. var C = this; @@ -795,43 +795,61 @@ function ArrayFrom(items, mapfn=undefined, thisArg=undefined) { ThrowTypeError(JSMSG_NOT_FUNCTION, DecompileArg(1, mapfn)); var T = thisArg; - // Steps 4-5. + // Step 4. var usingIterator = GetMethod(items, std_iterator); - // Step 6. + // Step 5. if (usingIterator !== undefined) { - // Steps 6.a-c. + // Steps 5.a-c. var A = IsConstructor(C) ? new C() : []; - // Steps 6.d-e. + // Step 5.c. var iterator = GetIterator(items, usingIterator); - // Step 6.f. + // Step 5.d. var k = 0; - // Step 6.g. + // Step 5.e. // These steps cannot be implemented using a for-of loop. // See . while (true) { - // Steps 6.g.i-iii. + // Step 5.e.i. + // Disabled for performance reason. We won't hit this case on + // normal array, since _DefineDataProperty will throw before it. + // We could hit this when |A| is a proxy and it ignores + // |_DefineDataProperty|, but it happens only after too long loop. + /* + if (k >= 0x1fffffffffffff) { + IteratorCloseThrow(iterator); + ThrowTypeError(JSMSG_TOO_LONG_ARRAY); + } + */ + + // Step 5.e.iii. var next = callContentFunction(iterator.next, iterator); if (!IsObject(next)) ThrowTypeError(JSMSG_NEXT_RETURNED_PRIMITIVE); - // Step 6.g.iv. + // Step 5.e.iv. if (next.done) { A.length = k; return A; } - // Steps 6.g.v-vi. + // Steps 5.e.v. var nextValue = next.value; - // Steps 6.g.vii-viii. - var mappedValue = mapping ? callContentFunction(mapfn, thisArg, nextValue, k) : nextValue; + // Steps 5.e.vi-vii. + try { + var mappedValue = mapping ? callContentFunction(mapfn, thisArg, nextValue, k) : nextValue; - // Steps 6.g.ix-xi. - _DefineDataProperty(A, k++, mappedValue); + // Steps 5.e.ii (reordered), 5.e.viii. + _DefineDataProperty(A, k++, mappedValue); + } catch (e) { + // Steps 5.e.vi.2, 5.e.ix. + IteratorCloseThrow(iterator); + throw e; + } } } diff --git a/js/src/tests/ecma_6/Array/from-iterator-close.js b/js/src/tests/ecma_6/Array/from-iterator-close.js new file mode 100644 index 000000000..fa97ea282 --- /dev/null +++ b/js/src/tests/ecma_6/Array/from-iterator-close.js @@ -0,0 +1,183 @@ +var BUGNUMBER = 1180306; +var summary = 'Array.from should close iterator on error'; + +print(BUGNUMBER + ": " + summary); + +function test(ctor, { mapVal=undefined, + nextVal=undefined, + nextThrowVal=undefined, + modifier=undefined, + exceptionVal=undefined, + exceptionType=undefined, + closed=true }) { + let iterable = { + closed: false, + [Symbol.iterator]() { + let iterator = { + first: true, + next() { + if (this.first) { + this.first = false; + if (nextThrowVal) + throw nextThrowVal; + return nextVal; + } + return { value: undefined, done: true }; + }, + return() { + iterable.closed = true; + return {}; + } + }; + if (modifier) + modifier(iterator, iterable); + + return iterator; + } + }; + if (exceptionVal) { + let caught = false; + try { + ctor.from(iterable, mapVal); + } catch (e) { + assertEq(e, exceptionVal); + caught = true; + } + assertEq(caught, true); + } else if (exceptionType) { + assertThrowsInstanceOf(() => ctor.from(iterable, mapVal), exceptionType); + } else { + ctor.from(iterable, mapVal); + } + assertEq(iterable.closed, closed); +} + +// == Error cases with close == + +// ES 2017 draft 22.1.2.1 step 5.e.i.1. +// Cannot test. + +// ES 2017 draft 22.1.2.1 step 5.e.vi.2. +test(Array, { + mapVal: () => { throw "map throws"; }, + nextVal: { value: 1, done: false }, + exceptionVal: "map throws", + closed: true, +}); + +// ES 2017 draft 22.1.2.1 step 5.e.ix. +class MyArray extends Array { + constructor() { + return new Proxy({}, { + defineProperty() { + throw "defineProperty throws"; + } + }); + } +} +test(MyArray, { + nextVal: { value: 1, done: false }, + exceptionVal: "defineProperty throws", + closed: true, +}); + +// ES 2017 draft 7.4.6 step 3. +// if GetMethod fails, the thrown value should be used. +test(MyArray, { + nextVal: { value: 1, done: false }, + modifier: (iterator, iterable) => { + Object.defineProperty(iterator, "return", { + get: function() { + iterable.closed = true; + throw "return getter throws"; + } + }); + }, + exceptionVal: "return getter throws", + closed: true, +}); +test(MyArray, { + nextVal: { value: 1, done: false }, + modifier: (iterator, iterable) => { + Object.defineProperty(iterator, "return", { + get: function() { + iterable.closed = true; + return "non object"; + } + }); + }, + exceptionType: TypeError, + closed: true, +}); +test(MyArray, { + nextVal: { value: 1, done: false }, + modifier: (iterator, iterable) => { + Object.defineProperty(iterator, "return", { + get: function() { + iterable.closed = true; + // Non callable. + return {}; + } + }); + }, + exceptionType: TypeError, + closed: true, +}); + +// ES 2017 draft 7.4.6 steps 6. +// if return method throws, the thrown value should be ignored. +test(MyArray, { + nextVal: { value: 1, done: false }, + modifier: (iterator, iterable) => { + iterator.return = function() { + iterable.closed = true; + throw "return throws"; + }; + }, + exceptionVal: "defineProperty throws", + closed: true, +}); + +test(MyArray, { + nextVal: { value: 1, done: false }, + modifier: (iterator, iterable) => { + iterator.return = function() { + iterable.closed = true; + return "non object"; + }; + }, + exceptionVal: "defineProperty throws", + closed: true, +}); + +// == Error cases without close == + +// ES 2017 draft 22.1.2.1 step 5.e.iii. +test(Array, { + nextThrowVal: "next throws", + exceptionVal: "next throws", + closed: false, +}); + +test(Array, { + nextVal: { value: {}, get done() { throw "done getter throws"; } }, + exceptionVal: "done getter throws", + closed: false, +}); + +// ES 2017 draft 22.1.2.1 step 5.e.v. +test(Array, { + nextVal: { get value() { throw "value getter throws"; }, done: false }, + exceptionVal: "value getter throws", + closed: false, +}); + +// == Successful cases == + +test(Array, { + nextVal: { value: 1, done: false }, + closed: false, +}); + +if (typeof reportCompare === 'function') + reportCompare(true, true); -- cgit v1.2.3 From 114794557687aebca601c38ba0f0a52a43b44d4a Mon Sep 17 00:00:00 2001 From: janekptacijarabaci Date: Thu, 15 Mar 2018 21:12:39 +0100 Subject: Close iterator after error in Promise.{all,race} Issue #17 --- js/src/builtin/Promise.cpp | 53 ++++-- js/src/js.msg | 3 + js/src/jsapi.h | 6 + js/src/tests/ecma_6/Promise/iterator-close.js | 234 ++++++++++++++++++++++++++ js/src/vm/ForOfIterator.cpp | 51 ++++++ 5 files changed, 330 insertions(+), 17 deletions(-) create mode 100644 js/src/tests/ecma_6/Promise/iterator-close.js (limited to 'js') diff --git a/js/src/builtin/Promise.cpp b/js/src/builtin/Promise.cpp index 59c97e529..c781a336d 100644 --- a/js/src/builtin/Promise.cpp +++ b/js/src/builtin/Promise.cpp @@ -1369,7 +1369,8 @@ PromiseObject::create(JSContext* cx, HandleObject executor, HandleObject proto / static MOZ_MUST_USE bool PerformPromiseAll(JSContext *cx, JS::ForOfIterator& iterator, HandleObject C, HandleObject promiseObj, - HandleObject resolve, HandleObject reject); + HandleObject resolve, HandleObject reject, + bool* done); // ES2016, 25.4.4.1. static bool @@ -1410,12 +1411,14 @@ Promise_static_all(JSContext* cx, unsigned argc, Value* vp) // Step 6 (implicit). // Step 7. - bool result = PerformPromiseAll(cx, iter, C, resultPromise, resolve, reject); + bool done; + bool result = PerformPromiseAll(cx, iter, C, resultPromise, resolve, reject, &done); // Step 8. if (!result) { // Step 8.a. - // TODO: implement iterator closing. + if (!done) + iter.closeThrow(); // Step 8.b. return AbruptRejectPromise(cx, args, resultPromise, reject); @@ -1598,8 +1601,11 @@ RunResolutionFunction(JSContext *cx, HandleObject resolutionFun, HandleValue res // ES2016, 25.4.4.1.1. static MOZ_MUST_USE bool PerformPromiseAll(JSContext *cx, JS::ForOfIterator& iterator, HandleObject C, - HandleObject promiseObj, HandleObject resolve, HandleObject reject) + HandleObject promiseObj, HandleObject resolve, HandleObject reject, + bool* done) { + *done = false; + RootedObject unwrappedPromiseObj(cx); if (IsWrapper(promiseObj)) { unwrappedPromiseObj = CheckedUnwrap(promiseObj); @@ -1666,14 +1672,19 @@ PerformPromiseAll(JSContext *cx, JS::ForOfIterator& iterator, HandleObject C, RootedValue rejectFunVal(cx, ObjectOrNullValue(reject)); while (true) { - bool done; - // Steps a, b, c, e, f, g. - if (!iterator.next(&nextValue, &done)) + // Steps a-c, e-g. + if (!iterator.next(&nextValue, done)) { + // Steps b, f. + *done = true; + + // Steps c, g. return false; + } // Step d. - if (done) { + if (*done) { // Step d.i (implicit). + // Step d.ii. int32_t remainingCount = dataHolder->decreaseRemainingCount(); @@ -1822,7 +1833,8 @@ PromiseAllResolveElementFunction(JSContext* cx, unsigned argc, Value* vp) static MOZ_MUST_USE bool PerformPromiseRace(JSContext *cx, JS::ForOfIterator& iterator, HandleObject C, HandleObject promiseObj, - HandleObject resolve, HandleObject reject); + HandleObject resolve, HandleObject reject, + bool* done); // ES2016, 25.4.4.3. static bool @@ -1863,12 +1875,14 @@ Promise_static_race(JSContext* cx, unsigned argc, Value* vp) // Step 6 (implicit). // Step 7. - bool result = PerformPromiseRace(cx, iter, C, resultPromise, resolve, reject); + bool done; + bool result = PerformPromiseRace(cx, iter, C, resultPromise, resolve, reject, &done); // Step 8. if (!result) { // Step 8.a. - // TODO: implement iterator closing. + if (!done) + iter.closeThrow(); // Step 8.b. return AbruptRejectPromise(cx, args, resultPromise, reject); @@ -1882,25 +1896,30 @@ Promise_static_race(JSContext* cx, unsigned argc, Value* vp) // ES2016, 25.4.4.3.1. static MOZ_MUST_USE bool PerformPromiseRace(JSContext *cx, JS::ForOfIterator& iterator, HandleObject C, - HandleObject promiseObj, HandleObject resolve, HandleObject reject) + HandleObject promiseObj, HandleObject resolve, HandleObject reject, + bool* done) { + *done = false; MOZ_ASSERT(C->isConstructor()); RootedValue CVal(cx, ObjectValue(*C)); RootedValue nextValue(cx); RootedValue resolveFunVal(cx, ObjectOrNullValue(resolve)); RootedValue rejectFunVal(cx, ObjectOrNullValue(reject)); - bool done; while (true) { // Steps a-c, e-g. - if (!iterator.next(&nextValue, &done)) + if (!iterator.next(&nextValue, done)) { + // Steps b, f. + *done = true; + + // Steps c, g. return false; + } // Step d. - if (done) { - // Step d.i. - // TODO: implement iterator closing. + if (*done) { + // Step d.i (implicit). // Step d.ii. return true; diff --git a/js/src/js.msg b/js/src/js.msg index cb5fc383b..2a818056f 100644 --- a/js/src/js.msg +++ b/js/src/js.msg @@ -579,3 +579,6 @@ MSG_DEF(JSMSG_PROMISE_CAPABILITY_HAS_SOMETHING_ALREADY, 0, JSEXN_TYPEERR, "GetCa MSG_DEF(JSMSG_PROMISE_RESOLVE_FUNCTION_NOT_CALLABLE, 0, JSEXN_TYPEERR, "A Promise subclass passed a non-callable value as the resolve function.") MSG_DEF(JSMSG_PROMISE_REJECT_FUNCTION_NOT_CALLABLE, 0, JSEXN_TYPEERR, "A Promise subclass passed a non-callable value as the reject function.") MSG_DEF(JSMSG_PROMISE_ERROR_IN_WRAPPED_REJECTION_REASON,0, JSEXN_INTERNALERR, "Promise rejection value is a non-unwrappable cross-compartment wrapper.") + +// Iterator +MSG_DEF(JSMSG_RETURN_NOT_CALLABLE, 0, JSEXN_TYPEERR, "property 'return' of iterator is not callable") diff --git a/js/src/jsapi.h b/js/src/jsapi.h index 2d6ff462c..a93852fa5 100644 --- a/js/src/jsapi.h +++ b/js/src/jsapi.h @@ -6166,6 +6166,12 @@ class MOZ_STACK_CLASS JS_PUBLIC_API(ForOfIterator) { */ bool next(JS::MutableHandleValue val, bool* done); + /** + * Close the iterator. + * For the case that completion type is throw. + */ + void closeThrow(); + /** * If initialized with throwOnNonCallable = false, check whether * the value is iterable. diff --git a/js/src/tests/ecma_6/Promise/iterator-close.js b/js/src/tests/ecma_6/Promise/iterator-close.js new file mode 100644 index 000000000..f17260d05 --- /dev/null +++ b/js/src/tests/ecma_6/Promise/iterator-close.js @@ -0,0 +1,234 @@ +// |reftest| skip-if(!xulRuntime.shell) -- needs drainJobQueue + +var BUGNUMBER = 1180306; +var summary = 'Promise.{all,race} should close iterator on error'; + +print(BUGNUMBER + ": " + summary); + +function test(ctor, props, { nextVal=undefined, + nextThrowVal=undefined, + modifier=undefined, + rejectReason=undefined, + rejectType=undefined, + closed=true }) { + function getIterable() { + let iterable = { + closed: false, + [Symbol.iterator]() { + let iterator = { + first: true, + next() { + if (this.first) { + this.first = false; + if (nextThrowVal) + throw nextThrowVal; + return nextVal; + } + return { value: undefined, done: true }; + }, + return() { + iterable.closed = true; + return {}; + } + }; + if (modifier) + modifier(iterator, iterable); + + return iterator; + } + }; + return iterable; + } + for (let prop of props) { + let iterable = getIterable(); + let e; + ctor[prop](iterable).catch(e_ => { e = e_; }); + drainJobQueue(); + if(rejectType) + assertEq(e instanceof rejectType, true); + else + assertEq(e, rejectReason); + assertEq(iterable.closed, closed); + } +} + +// == Error cases with close == + +// ES 2017 draft 25.4.4.1.1 step 6.i. +// ES 2017 draft 25.4.4.3.1 step 3.h. +class MyPromiseStaticResolveGetterThrows extends Promise { + static get resolve() { + throw "static resolve getter throws"; + } +}; +test(MyPromiseStaticResolveGetterThrows, ["all", "race"], { + nextVal: { value: Promise.resolve(1), done: false }, + rejectReason: "static resolve getter throws", + closed: true, +}); + +class MyPromiseStaticResolveThrows extends Promise { + static resolve() { + throw "static resolve throws"; + } +}; +test(MyPromiseStaticResolveThrows, ["all", "race"], { + nextVal: { value: Promise.resolve(1), done: false }, + rejectReason: "static resolve throws", + closed: true, +}); + +// ES 2017 draft 25.4.4.1.1 step 6.q. +// ES 2017 draft 25.4.4.3.1 step 3.i. +class MyPromiseThenGetterThrows extends Promise { + static resolve() { + return { + get then() { + throw "then getter throws"; + } + }; + } +}; +test(MyPromiseThenGetterThrows, ["all", "race"], { + nextVal: { value: Promise.resolve(1), done: false }, + rejectReason: "then getter throws", + closed: true, +}); + +class MyPromiseThenThrows extends Promise { + static resolve() { + return { + then() { + throw "then throws"; + } + }; + } +}; +test(MyPromiseThenThrows, ["all", "race"], { + nextVal: { value: Promise.resolve(1), done: false }, + rejectReason: "then throws", + closed: true, +}); + +// ES 2017 draft 7.4.6 step 3. +// if GetMethod fails, the thrown value should be used. +test(MyPromiseThenThrows, ["all", "race"], { + nextVal: { value: Promise.resolve(1), done: false }, + modifier: (iterator, iterable) => { + Object.defineProperty(iterator, "return", { + get: function() { + iterable.closed = true; + throw "return getter throws"; + } + }); + }, + rejectReason: "return getter throws", + closed: true, +}); +test(MyPromiseThenThrows, ["all", "race"], { + nextVal: { value: Promise.resolve(1), done: false }, + modifier: (iterator, iterable) => { + Object.defineProperty(iterator, "return", { + get: function() { + iterable.closed = true; + return "non object"; + } + }); + }, + rejectType: TypeError, + closed: true, +}); +test(MyPromiseThenThrows, ["all", "race"], { + nextVal: { value: Promise.resolve(1), done: false }, + modifier: (iterator, iterable) => { + Object.defineProperty(iterator, "return", { + get: function() { + iterable.closed = true; + // Non callable. + return {}; + } + }); + }, + rejectType: TypeError, + closed: true, +}); + +// ES 2017 draft 7.4.6 steps 6. +// if return method throws, the thrown value should be ignored. +test(MyPromiseThenThrows, ["all", "race"], { + nextVal: { value: Promise.resolve(1), done: false }, + modifier: (iterator, iterable) => { + iterator.return = function() { + iterable.closed = true; + throw "return throws"; + }; + }, + rejectReason: "then throws", + closed: true, +}); + +test(MyPromiseThenThrows, ["all", "race"], { + nextVal: { value: Promise.resolve(1), done: false }, + modifier: (iterator, iterable) => { + iterator.return = function() { + iterable.closed = true; + return "non object"; + }; + }, + rejectReason: "then throws", + closed: true, +}); + +// == Error cases without close == + +// ES 2017 draft 25.4.4.1.1 step 6.a. +test(Promise, ["all", "race"], { + nextThrowVal: "next throws", + rejectReason: "next throws", + closed: false, +}); + +test(Promise, ["all", "race"], { + nextVal: { value: {}, get done() { throw "done getter throws"; } }, + rejectReason: "done getter throws", + closed: false, +}); + +// ES 2017 draft 25.4.4.1.1 step 6.e. +test(Promise, ["all", "race"], { + nextVal: { get value() { throw "value getter throws"; }, done: false }, + rejectReason: "value getter throws", + closed: false, +}); + +// ES 2017 draft 25.4.4.1.1 step 6.d.iii.2. +let first = true; +class MyPromiseResolveThrows extends Promise { + constructor(executer) { + if (first) { + first = false; + super((resolve, reject) => { + executer(() => { + throw "resolve throws"; + }, reject); + }); + return; + } + super(executer); + } +}; +test(MyPromiseResolveThrows, ["all"], { + nextVal: { value: undefined, done: true }, + rejectReason: "resolve throws", + closed: false, +}); + +// == Successful cases == + +test(Promise, ["all", "race"], { + nextVal: { value: Promise.resolve(1), done: false }, + closed: false, +}); + +if (typeof reportCompare === 'function') + reportCompare(true, true); diff --git a/js/src/vm/ForOfIterator.cpp b/js/src/vm/ForOfIterator.cpp index 7bd521a6a..a67b36774 100644 --- a/js/src/vm/ForOfIterator.cpp +++ b/js/src/vm/ForOfIterator.cpp @@ -151,6 +151,57 @@ ForOfIterator::next(MutableHandleValue vp, bool* done) return GetProperty(cx_, resultObj, resultObj, cx_->names().value, vp); } +// ES 2017 draft 0f10dba4ad18de92d47d421f378233a2eae8f077 7.4.6. +// When completion.[[Type]] is throw. +void +ForOfIterator::closeThrow() +{ + MOZ_ASSERT(iterator); + + RootedValue completionException(cx_); + if (cx_->isExceptionPending()) { + if (!GetAndClearException(cx_, &completionException)) + completionException.setUndefined(); + } + + // Steps 1-2 (implicit) + + // Step 3 (partial). + RootedValue returnVal(cx_); + if (!GetProperty(cx_, iterator, iterator, cx_->names().return_, &returnVal)) + return; + + // Step 4. + if (returnVal.isUndefined()) { + cx_->setPendingException(completionException); + return; + } + + // Step 3 (remaining part) + if (!returnVal.isObject()) { + JS_ReportErrorNumberASCII(cx_, GetErrorMessage, nullptr, JSMSG_RETURN_NOT_CALLABLE); + return; + } + RootedObject returnObj(cx_, &returnVal.toObject()); + if (!returnObj->isCallable()) { + JS_ReportErrorNumberASCII(cx_, GetErrorMessage, nullptr, JSMSG_RETURN_NOT_CALLABLE); + return; + } + + // Step 5. + RootedValue innerResultValue(cx_); + if (!js::Call(cx_, returnVal, iterator, &innerResultValue)) { + if (cx_->isExceptionPending()) + cx_->clearPendingException(); + } + + // Step 6. + cx_->setPendingException(completionException); + + // Steps 7-9 (skipped). + return; +} + bool ForOfIterator::materializeArrayIterator() { -- cgit v1.2.3