Bug 1539694 - Part 2: Implement Promise.allSettled stage 3 proposal. r=jorendorff

Nightly-only for now because the proposal only just reached stage 3.

Promise.allSettled is similar to Promise.all, except that each element also has
an own reject handler. Apart from that most code from Promise.all can be reused
for Promise.allSettled, including calling the `CommonPerformPromiseAllRace`
helper function.

Because each element has an own reject handler and we need to track if either
function of the resolve/reject handler pair was already called, it's not
possible to reuse the same trick as in `PromiseAllResolveElementFunction` where
the data-holder slot is used to track if the handler was already called. Instead
`PromiseAllSettledElementFunction` uses the values array to check if the
current index position is still set to `undefined` as a mean to verify that the
resolving functions for each element weren't already called.

Differential Revision: https://phabricator.services.mozilla.com/D25209

--HG--
extra : moz-landing-system : lando
This commit is contained in:
André Bargull 2019-04-11 12:22:43 +00:00
Родитель 675d1e61cb
Коммит e89cfcac3e
3 изменённых файлов: 401 добавлений и 2 удалений

Просмотреть файл

@ -97,6 +97,11 @@ enum PromiseAllResolveElementFunctionSlots {
PromiseAllResolveElementFunctionSlot_ElementIndex,
};
enum PromiseAllSettledElementFunctionSlots {
PromiseAllSettledElementFunctionSlot_Data = 0,
PromiseAllSettledElementFunctionSlot_ElementIndex,
};
enum ReactionJobSlots {
ReactionJobSlot_ReactionRecord = 0,
};
@ -2272,17 +2277,26 @@ static MOZ_MUST_USE bool PerformPromiseAll(
JSContext* cx, PromiseForOfIterator& iterator, HandleObject C,
Handle<PromiseCapability> resultCapability, bool* done);
static MOZ_MUST_USE bool PerformPromiseAllSettled(
JSContext* cx, PromiseForOfIterator& iterator, HandleObject C,
Handle<PromiseCapability> resultCapability, bool* done);
static MOZ_MUST_USE bool PerformPromiseRace(
JSContext* cx, PromiseForOfIterator& iterator, HandleObject C,
Handle<PromiseCapability> resultCapability, bool* done);
enum class IterationMode { All, Race };
enum class IterationMode { All, AllSettled, Race };
// ES2020 draft rev a09fc232c137800dbf51b6204f37fdede4ba1646
//
// Unified implementation of
// 25.6.4.1 Promise.all ( iterable )
// 25.6.4.3 Promise.race ( iterable )
//
// Promise.allSettled (Stage 3 proposal)
// https://tc39.github.io/proposal-promise-allSettled/
//
// Promise.allSettled ( iterable )
static MOZ_MUST_USE bool CommonStaticAllRace(JSContext* cx, CallArgs& args,
IterationMode mode) {
HandleValue iterable = args.get(0);
@ -2295,6 +2309,9 @@ static MOZ_MUST_USE bool CommonStaticAllRace(JSContext* cx, CallArgs& args,
case IterationMode::All:
message = "Receiver of Promise.all call";
break;
case IterationMode::AllSettled:
message = "Receiver of Promise.allSettled call";
break;
case IterationMode::Race:
message = "Receiver of Promise.race call";
break;
@ -2325,6 +2342,9 @@ static MOZ_MUST_USE bool CommonStaticAllRace(JSContext* cx, CallArgs& args,
case IterationMode::All:
message = "Argument of Promise.all";
break;
case IterationMode::AllSettled:
message = "Argument of Promise.allSettled";
break;
case IterationMode::Race:
message = "Argument of Promise.race";
break;
@ -2342,6 +2362,9 @@ static MOZ_MUST_USE bool CommonStaticAllRace(JSContext* cx, CallArgs& args,
case IterationMode::All:
result = PerformPromiseAll(cx, iter, C, promiseCapability, &done);
break;
case IterationMode::AllSettled:
result = PerformPromiseAllSettled(cx, iter, C, promiseCapability, &done);
break;
case IterationMode::Race:
result = PerformPromiseRace(cx, iter, C, promiseCapability, &done);
break;
@ -2583,6 +2606,10 @@ static bool IsPromiseSpecies(JSContext* cx, JSFunction* species);
// ES2019 draft rev dd269df67d37409a6f2099a842b8f5c75ee6fc24
// 25.6.4.1.1 Runtime Semantics: PerformPromiseAll, step 6.
// 25.6.4.3.1 Runtime Semantics: PerformPromiseRace, step 3.
//
// Promise.allSettled (Stage 3 proposal)
// https://tc39.github.io/proposal-promise-allSettled/
// Runtime Semantics: PerformPromiseAllSettled, step 6.
template <typename T>
static MOZ_MUST_USE bool CommonPerformPromiseAllRace(
JSContext* cx, PromiseForOfIterator& iterator, HandleObject C,
@ -3117,6 +3144,285 @@ static MOZ_MUST_USE bool PerformPromiseRace(
isDefaultResolveFn, getResolveAndReject);
}
enum class PromiseAllSettledElementFunctionKind { Resolve, Reject };
// Promise.allSettled (Stage 3 proposal)
// https://tc39.github.io/proposal-promise-allSettled/
//
// Promise.allSettled Resolve Element Functions
// Promise.allSettled Reject Element Functions
template <PromiseAllSettledElementFunctionKind Kind>
static bool PromiseAllSettledElementFunction(JSContext* cx, unsigned argc,
Value* vp);
// Promise.allSettled (Stage 3 proposal)
// https://tc39.github.io/proposal-promise-allSettled/
//
// Promise.allSettled ( iterable )
static bool Promise_static_allSettled(JSContext* cx, unsigned argc, Value* vp) {
CallArgs args = CallArgsFromVp(argc, vp);
return CommonStaticAllRace(cx, args, IterationMode::AllSettled);
}
// Promise.allSettled (Stage 3 proposal)
// https://tc39.github.io/proposal-promise-allSettled/
//
// PerformPromiseAllSettled ( iteratorRecord, constructor, resultCapability )
static MOZ_MUST_USE bool PerformPromiseAllSettled(
JSContext* cx, PromiseForOfIterator& iterator, HandleObject C,
Handle<PromiseCapability> resultCapability, bool* done) {
*done = false;
// Step 1.
MOZ_ASSERT(C->isConstructor());
// Step 2 (omitted).
// Step 3.
// See the big comment in PerformPromiseAll about which objects should be
// created in which compartments.
RootedArrayObject valuesArray(cx);
RootedValue valuesArrayVal(cx);
if (IsWrapper(resultCapability.promise())) {
JSObject* unwrappedPromiseObj =
CheckedUnwrapStatic(resultCapability.promise());
MOZ_ASSERT(unwrappedPromiseObj);
{
AutoRealm ar(cx, unwrappedPromiseObj);
valuesArray = NewDenseEmptyArray(cx);
if (!valuesArray) {
return false;
}
}
valuesArrayVal.setObject(*valuesArray);
if (!cx->compartment()->wrap(cx, &valuesArrayVal)) {
return false;
}
} else {
valuesArray = NewDenseEmptyArray(cx);
if (!valuesArray) {
return false;
}
valuesArrayVal.setObject(*valuesArray);
}
// Step 4.
// Create our data holder that holds all the things shared across every step
// of the iterator. In particular, this holds the remainingElementsCount
// (as an integer reserved slot), the array of values, and the resolve
// function from our PromiseCapability.
Rooted<PromiseAllDataHolder*> dataHolder(cx);
dataHolder =
NewPromiseAllDataHolder(cx, resultCapability.promise(), valuesArrayVal,
resultCapability.resolve());
if (!dataHolder) {
return false;
}
// Step 5.
uint32_t index = 0;
auto getResolveAndReject = [cx, &valuesArray, &dataHolder, &index](
MutableHandleValue resolveFunVal,
MutableHandleValue rejectFunVal) {
// Step 6.h.
{ // Scope for the AutoRealm we need to work with valuesArray. We
// mostly do this for performance; we could go ahead and do the define via
// a cross-compartment proxy instead...
AutoRealm ar(cx, valuesArray);
if (!NewbornArrayPush(cx, valuesArray, UndefinedValue())) {
return false;
}
}
auto PromiseAllSettledResolveElementFunction =
PromiseAllSettledElementFunction<
PromiseAllSettledElementFunctionKind::Resolve>;
auto PromiseAllSettledRejectElementFunction =
PromiseAllSettledElementFunction<
PromiseAllSettledElementFunctionKind::Reject>;
// Steps 6.j-m.
JSFunction* resolveFunc = NewNativeFunction(
cx, PromiseAllSettledResolveElementFunction, 1, nullptr,
gc::AllocKind::FUNCTION_EXTENDED, GenericObject);
if (!resolveFunc) {
return false;
}
// Steps 6.o-q.
resolveFunc->setExtendedSlot(PromiseAllSettledElementFunctionSlot_Data,
ObjectValue(*dataHolder));
// Step 6.n.
resolveFunc->setExtendedSlot(
PromiseAllSettledElementFunctionSlot_ElementIndex, Int32Value(index));
// Steps 6.r-t.
JSFunction* rejectFunc = NewNativeFunction(
cx, PromiseAllSettledRejectElementFunction, 1, nullptr,
gc::AllocKind::FUNCTION_EXTENDED, GenericObject);
if (!rejectFunc) {
return false;
}
// Steps 6.v-x.
rejectFunc->setExtendedSlot(PromiseAllSettledElementFunctionSlot_Data,
ObjectValue(*dataHolder));
// Step 6.u.
rejectFunc->setExtendedSlot(
PromiseAllSettledElementFunctionSlot_ElementIndex, Int32Value(index));
// Step 6.y.
dataHolder->increaseRemainingCount();
// Step 6.aa.
index++;
MOZ_ASSERT(index > 0);
resolveFunVal.setObject(*resolveFunc);
rejectFunVal.setObject(*rejectFunc);
return true;
};
// Step 6.
if (!CommonPerformPromiseAllRace(cx, iterator, C, resultCapability.promise(),
done, true, getResolveAndReject)) {
return false;
}
// Step 6.d.ii.
int32_t remainingCount = dataHolder->decreaseRemainingCount();
// Steps 6.d.iii-iv.
if (remainingCount == 0) {
return RunResolutionFunction(cx, resultCapability.resolve(), valuesArrayVal,
ResolveMode, resultCapability.promise());
}
return true;
}
// Promise.allSettled (Stage 3 proposal)
// https://tc39.github.io/proposal-promise-allSettled/
//
// Promise.allSettled Resolve Element Functions
// Promise.allSettled Reject Element Functions
template <PromiseAllSettledElementFunctionKind Kind>
static bool PromiseAllSettledElementFunction(JSContext* cx, unsigned argc,
Value* vp) {
CallArgs args = CallArgsFromVp(argc, vp);
HandleValue valueOrReason = args.get(0);
// Step 1.
JSFunction* resolve = &args.callee().as<JSFunction>();
Rooted<PromiseAllDataHolder*> data(
cx, &resolve->getExtendedSlot(PromiseAllSettledElementFunctionSlot_Data)
.toObject()
.as<PromiseAllDataHolder>());
// Steps 2-4 (moved below).
// Step 5.
int32_t index =
resolve
->getExtendedSlot(PromiseAllSettledElementFunctionSlot_ElementIndex)
.toInt32();
// Step 6.
RootedValue valuesVal(cx, data->valuesArray());
RootedObject valuesObj(cx, &valuesVal.toObject());
bool needsWrapping = false;
if (IsProxy(valuesObj)) {
// See comment for PerformPromiseAllSettled, step 3 for why we unwrap here.
valuesObj = UncheckedUnwrap(valuesObj);
if (JS_IsDeadWrapper(valuesObj)) {
JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
JSMSG_DEAD_OBJECT);
return false;
}
needsWrapping = true;
}
HandleNativeObject values = valuesObj.as<NativeObject>();
// Steps 2-3.
// We use the element value as a signal for whether the Promise was already
// fulfilled. Upon resolution, it's set to the result object created below.
if (!values->getDenseElement(index).isUndefined()) {
args.rval().setUndefined();
return true;
}
// Steps 7-8 (moved below).
// Step 9.
RootedPlainObject obj(cx, NewBuiltinClassInstance<PlainObject>(cx));
if (!obj) {
return false;
}
// Step 10.
RootedId id(cx, NameToId(cx->names().status));
RootedValue statusValue(cx);
if (Kind == PromiseAllSettledElementFunctionKind::Resolve) {
statusValue.setString(cx->names().fulfilled);
} else {
statusValue.setString(cx->names().rejected);
}
if (!NativeDefineDataProperty(cx, obj, id, statusValue, JSPROP_ENUMERATE)) {
return false;
}
// Step 11.
if (Kind == PromiseAllSettledElementFunctionKind::Resolve) {
id = NameToId(cx->names().value);
} else {
id = NameToId(cx->names().reason);
}
if (!NativeDefineDataProperty(cx, obj, id, valueOrReason, JSPROP_ENUMERATE)) {
return false;
}
RootedValue objVal(cx, ObjectValue(*obj));
if (needsWrapping) {
AutoRealm ar(cx, valuesObj);
if (!cx->compartment()->wrap(cx, &objVal)) {
return false;
}
}
// Steps 4, 12.
values->setDenseElement(index, objVal);
// Steps 8, 13.
uint32_t remainingCount = data->decreaseRemainingCount();
// Step 14.
if (remainingCount == 0) {
// Step 14.a. (Omitted, happened in PerformPromiseAllSettled.)
// Step 14.b.
// Step 7 (Adapted to work with PromiseAllDataHolder's layout).
RootedObject resolveAllFun(cx, data->resolveObj());
RootedObject promiseObj(cx, data->promiseObj());
if (!RunResolutionFunction(cx, resolveAllFun, valuesVal, ResolveMode,
promiseObj)) {
return false;
}
}
// Step 15.
args.rval().setUndefined();
return true;
}
// https://tc39.github.io/ecma262/#sec-promise.reject
//
// Unified implementation of
@ -5392,9 +5698,13 @@ static const JSPropertySpec promise_properties[] = {
static const JSFunctionSpec promise_static_methods[] = {
JS_FN("all", Promise_static_all, 1, 0),
#ifdef NIGHTLY_BUILD
JS_FN("allSettled", Promise_static_allSettled, 1, 0),
#endif
JS_FN("race", Promise_static_race, 1, 0),
JS_FN("reject", Promise_reject, 1, 0),
JS_FN("resolve", Promise_static_resolve, 1, 0), JS_FS_END};
JS_FN("resolve", Promise_static_resolve, 1, 0),
JS_FS_END};
static const JSPropertySpec promise_static_properties[] = {
JS_SYM_GET(species, Promise_static_species, 0), JS_PS_END};

Просмотреть файл

@ -0,0 +1,88 @@
// |reftest| skip-if(!Promise.allSettled)
// Smoke test for `Promise.allSettled`, test262 should cover the function in
// more detail.
// Empty elements.
Promise.allSettled([]).then(v => {
assertDeepEq(v, []);
});
// Single element.
Promise.allSettled([Promise.resolve(0)]).then(v => {
assertDeepEq(v, [
{"status": "fulfilled", "value": 0},
]);
});
Promise.allSettled([Promise.reject(1)]).then(v => {
assertDeepEq(v, [
{"status": "rejected", "reason": 1},
]);
});
// Multiple elements.
Promise.allSettled([Promise.resolve(1), Promise.resolve(2)]).then(v => {
assertDeepEq(v, [
{"status": "fulfilled", "value": 1},
{"status": "fulfilled", "value": 2},
]);
});
Promise.allSettled([Promise.resolve(3), Promise.reject(4)]).then(v => {
assertDeepEq(v, [
{"status": "fulfilled", "value": 3},
{"status": "rejected", "reason": 4},
]);
});
Promise.allSettled([Promise.reject(5), Promise.resolve(6)]).then(v => {
assertDeepEq(v, [
{"status": "rejected", "reason": 5},
{"status": "fulfilled", "value": 6},
]);
});
Promise.allSettled([Promise.reject(7), Promise.reject(8)]).then(v => {
assertDeepEq(v, [
{"status": "rejected", "reason": 7},
{"status": "rejected", "reason": 8},
]);
});
// Cross-Realm tests.
//
// Note: When |g| is a cross-compartment global, Promise.allSettled creates
// the result array in |g|'s Realm. This doesn't follow the spec, but the code
// in js/src/builtin/Promise.cpp claims this is useful when the Promise
// compartment is less-privileged. This means for this test we can't use
// assertDeepEq below, because the result array may have the wrong prototype.
let g = newGlobal();
if (typeof isSameCompartment !== "function") {
var isSameCompartment = SpecialPowers.Cu.getJSTestingFunctions().isSameCompartment;
}
// Test wrapping when neither Promise.allSettled element function is called.
Promise.allSettled.call(g.Promise, []).then(v => {
assertEq(isSameCompartment(v, g), true);
assertEq(v.length, 0);
});
// Test wrapping in `Promise.allSettled Resolve Element Function`.
Promise.allSettled.call(g.Promise, [Promise.resolve(0)]).then(v => {
assertEq(isSameCompartment(v, g), true);
assertEq(v.length, 1);
assertEq(v[0].status, "fulfilled");
assertEq(v[0].value, 0);
});
// Test wrapping in `Promise.allSettled Reject Element Function`.
Promise.allSettled.call(g.Promise, [Promise.reject(0)]).then(v => {
assertEq(isSameCompartment(v, g), true);
assertEq(v.length, 1);
assertEq(v[0].status, "rejected");
assertEq(v[0].reason, 0);
});
if (typeof reportCompare === "function")
reportCompare(0, 0);

Просмотреть файл

@ -383,6 +383,7 @@
MACRO(startTimestamp, startTimestamp, "startTimestamp") \
MACRO(state, state, "state") \
MACRO(static, static_, "static") \
MACRO(status, status, "status") \
MACRO(std_Function_apply, std_Function_apply, "std_Function_apply") \
MACRO(sticky, sticky, "sticky") \
MACRO(StringIterator, StringIterator, "String Iterator") \