blob: 19a16d8da858f164dabdebe893c8debc8fff1e8a [file] [log] [blame]
// Copyright 2019 the V8 project authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include 'src/builtins/builtins-promise.h'
#include 'src/builtins/builtins-promise-gen.h'
namespace promise {
const kPromiseBuiltinsPromiseContextLength: constexpr int31
generates 'PromiseBuiltins::kPromiseContextLength';
// Creates the context used by all Promise.all resolve element closures,
// together with the values array. Since all closures for a single Promise.all
// call use the same context, we need to store the indices for the individual
// closures somewhere else (we put them into the identity hash field of the
// closures), and we also need to have a separate marker for when the closure
// was called already (we slap the native context onto the closure in that
// case to mark it's done).
macro CreatePromiseAllResolveElementContext(implicit context: Context)(
capability: PromiseCapability, nativeContext: NativeContext): Context {
// TODO(bmeurer): Manually fold this into a single allocation.
const arrayMap = UnsafeCast<Map>(
nativeContext[NativeContextSlot::JS_ARRAY_PACKED_ELEMENTS_MAP_INDEX]);
const valuesArray = AllocateJSArray(
ElementsKind::PACKED_ELEMENTS, arrayMap, IntPtrConstant(0),
SmiConstant(0));
const resolveContext = AllocateSyntheticFunctionContext(
nativeContext,
PromiseAllResolveElementContextSlots::kPromiseAllResolveElementLength);
resolveContext[PromiseAllResolveElementContextSlots::
kPromiseAllResolveElementRemainingSlot] = SmiConstant(1);
resolveContext[PromiseAllResolveElementContextSlots::
kPromiseAllResolveElementCapabilitySlot] = capability;
resolveContext[PromiseAllResolveElementContextSlots::
kPromiseAllResolveElementValuesArraySlot] = valuesArray;
return resolveContext;
}
macro CreatePromiseAllResolveElementFunction(implicit context: Context)(
resolveElementContext: Context, index: Smi, nativeContext: NativeContext,
slotIndex: constexpr NativeContextSlot): JSFunction {
assert(index > 0);
assert(index < kPropertyArrayHashFieldMax);
const map = UnsafeCast<Map>(
nativeContext
[NativeContextSlot::STRICT_FUNCTION_WITHOUT_PROTOTYPE_MAP_INDEX]);
const resolveInfo =
UnsafeCast<SharedFunctionInfo>(nativeContext[slotIndex]);
const resolve = AllocateFunctionWithMapAndContext(
map, resolveInfo, resolveElementContext);
assert(kPropertyArrayNoHashSentinel == 0);
resolve.properties_or_hash = index;
return resolve;
}
@export
macro CreatePromiseResolvingFunctionsContext(implicit context: Context)(
promise: JSPromise, debugEvent: Object, nativeContext: NativeContext):
Context {
const resolveContext = AllocateSyntheticFunctionContext(
nativeContext, kPromiseBuiltinsPromiseContextLength);
resolveContext[kPromiseBuiltinsPromiseSlot] = promise;
resolveContext[kPromiseBuiltinsAlreadyResolvedSlot] = False;
resolveContext[kPromiseBuiltinsDebugEventSlot] = debugEvent;
return resolveContext;
}
macro IsPromiseThenLookupChainIntact(implicit context: Context)(
nativeContext: NativeContext, receiverMap: Map): bool {
if (IsForceSlowPath()) return false;
if (!IsJSPromiseMap(receiverMap)) return false;
if (receiverMap.prototype !=
nativeContext[NativeContextSlot::PROMISE_PROTOTYPE_INDEX])
return false;
return !IsPromiseThenProtectorCellInvalid();
}
struct PromiseAllResolveElementFunctor {
macro Call(implicit context: Context)(
resolveElementContext: Context, nativeContext: NativeContext,
index: Smi, _capability: PromiseCapability): Callable {
return CreatePromiseAllResolveElementFunction(
resolveElementContext, index, nativeContext,
NativeContextSlot::PROMISE_ALL_RESOLVE_ELEMENT_SHARED_FUN);
}
}
struct PromiseAllRejectElementFunctor {
macro Call(implicit context: Context)(
_resolveElementContext: Context, _nativeContext: NativeContext,
_index: Smi, capability: PromiseCapability): Callable {
return UnsafeCast<Callable>(capability.reject);
}
}
struct PromiseAllSettledResolveElementFunctor {
macro Call(implicit context: Context)(
resolveElementContext: Context, nativeContext: NativeContext,
index: Smi, _capability: PromiseCapability): Callable {
return CreatePromiseAllResolveElementFunction(
resolveElementContext, index, nativeContext,
NativeContextSlot::PROMISE_ALL_SETTLED_RESOLVE_ELEMENT_SHARED_FUN);
}
}
struct PromiseAllSettledRejectElementFunctor {
macro Call(implicit context: Context)(
resolveElementContext: Context, nativeContext: NativeContext,
index: Smi, _capability: PromiseCapability): Callable {
return CreatePromiseAllResolveElementFunction(
resolveElementContext, index, nativeContext,
NativeContextSlot::PROMISE_ALL_SETTLED_REJECT_ELEMENT_SHARED_FUN);
}
}
transitioning macro PerformPromiseAll<F1: type, F2: type>(implicit context:
Context)(
constructor: JSReceiver, capability: PromiseCapability,
iter: iterator::IteratorRecord, createResolveElementFunctor: F1,
createRejectElementFunctor: F2): JSAny labels Reject(Object) {
const nativeContext = LoadNativeContext(context);
const promise = capability.promise;
const resolve = capability.resolve;
const reject = capability.reject;
// For catch prediction, don't treat the .then calls as handling it;
// instead, recurse outwards.
if (IsDebugActive()) deferred {
SetPropertyStrict(
context, reject, kPromiseForwardingHandlerSymbol, True);
}
const resolveElementContext =
CreatePromiseAllResolveElementContext(capability, nativeContext);
let index: Smi = 1;
// We can skip the "resolve" lookup on {constructor} if it's the
// Promise constructor and the Promise.resolve protector is intact,
// as that guards the lookup path for the "resolve" property on the
// Promise constructor.
let promiseResolveFunction: JSAny = Undefined;
try {
try {
if (!IsPromiseResolveLookupChainIntact(nativeContext, constructor)) {
// 5. Let _promiseResolve_ be ? Get(_constructor_, `"resolve"`).
let promiseResolve: JSAny;
try {
promiseResolve = GetProperty(constructor, kResolveString);
} catch (e) deferred {
iterator::IteratorCloseOnException(iter, e) otherwise Reject;
}
// 6. If IsCallable(_promiseResolve_) is *false*, throw a *TypeError*
// exception.
promiseResolveFunction = Cast<Callable>(promiseResolve)
otherwise ThrowTypeError(
MessageTemplate::kCalledNonCallable, 'resolve');
}
const fastIteratorResultMap = UnsafeCast<Map>(
nativeContext[NativeContextSlot::ITERATOR_RESULT_MAP_INDEX]);
while (true) {
let nextValue: JSAny;
// Let next be IteratorStep(iteratorRecord.[[Iterator]]).
// If next is an abrupt completion, set iteratorRecord.[[Done]] to
// true. ReturnIfAbrupt(next).
const next: JSReceiver = iterator::IteratorStep(
iter, fastIteratorResultMap) otherwise goto Done;
// Let nextValue be IteratorValue(next).
// If nextValue is an abrupt completion, set iteratorRecord.[[Done]]
// to true.
// ReturnIfAbrupt(nextValue).
nextValue = iterator::IteratorValue(next, fastIteratorResultMap);
// Check if we reached the limit.
if (index == kPropertyArrayHashFieldMax) {
// If there are too many elements (currently more than 2**21-1),
// raise a RangeError here (which is caught directly and turned into
// a rejection) of the resulting promise. We could gracefully handle
// this case as well and support more than this number of elements
// by going to a separate function and pass the larger indices via a
// separate context, but it doesn't seem likely that we need this,
// and it's unclear how the rest of the system deals with 2**21 live
// Promises anyways.
try {
ThrowRangeError(MessageTemplate::kTooManyElementsInPromiseAll);
} catch (e) deferred {
iterator::IteratorCloseOnException(iter, e) otherwise Reject;
}
}
// Set remainingElementsCount.[[Value]] to
// remainingElementsCount.[[Value]] + 1.
const remainingElementsCount =
UnsafeCast<Smi>(resolveElementContext
[PromiseAllResolveElementContextSlots::
kPromiseAllResolveElementRemainingSlot]);
resolveElementContext[PromiseAllResolveElementContextSlots::
kPromiseAllResolveElementRemainingSlot] =
remainingElementsCount + 1;
// Let resolveElement be CreateBuiltinFunction(steps,
// « [[AlreadyCalled]],
// [[Index]],
// [[Values]],
// [[Capability]],
// [[RemainingElements]]
// »).
// Set resolveElement.[[AlreadyCalled]] to a Record { [[Value]]: false
// }. Set resolveElement.[[Index]] to index. Set
// resolveElement.[[Values]] to values. Set
// resolveElement.[[Capability]] to resultCapability. Set
// resolveElement.[[RemainingElements]] to remainingElementsCount.
const resolveElementFun = createResolveElementFunctor.Call(
resolveElementContext, nativeContext, index, capability);
const rejectElementFun = createRejectElementFunctor.Call(
resolveElementContext, nativeContext, index, capability);
// We can skip the "resolve" lookup on the {constructor} as well as
// the "then" lookup on the result of the "resolve" call, and
// immediately chain continuation onto the {next_value} if:
//
// (a) The {constructor} is the intrinsic %Promise% function, and
// looking up "resolve" on {constructor} yields the initial
// Promise.resolve() builtin, and
// (b) the promise @@species protector cell is valid, meaning that
// no one messed with the Symbol.species property on any
// intrinsic promise or on the Promise.prototype, and
// (c) the {next_value} is a JSPromise whose [[Prototype]] field
// contains the intrinsic %PromisePrototype%, and
// (d) we're not running with async_hooks or DevTools enabled.
//
// In that case we also don't need to allocate a chained promise for
// the PromiseReaction (aka we can pass undefined to
// PerformPromiseThen), since this is only necessary for DevTools and
// PromiseHooks.
if (promiseResolveFunction != Undefined ||
IsPromiseHookEnabledOrDebugIsActiveOrHasAsyncEventDelegate() ||
IsPromiseSpeciesProtectorCellInvalid() || Is<Smi>(nextValue) ||
!IsPromiseThenLookupChainIntact(
nativeContext, UnsafeCast<HeapObject>(nextValue).map)) {
try {
// Let nextPromise be ? Call(constructor, _promiseResolve_, «
// nextValue »).
const nextPromise = CallResolve(
UnsafeCast<Constructor>(constructor), promiseResolveFunction,
nextValue);
// Perform ? Invoke(nextPromise, "then", « resolveElement,
// resultCapability.[[Reject]] »).
const then = GetProperty(nextPromise, kThenString);
const thenResult = Call(
nativeContext, then, nextPromise, resolveElementFun,
rejectElementFun);
// For catch prediction, mark that rejections here are
// semantically handled by the combined Promise.
if (IsDebugActive() && Is<JSPromise>(thenResult)) deferred {
SetPropertyStrict(
context, thenResult, kPromiseHandledBySymbol, promise);
}
} catch (e) deferred {
iterator::IteratorCloseOnException(iter, e) otherwise Reject;
}
} else {
PerformPromiseThenImpl(
UnsafeCast<JSPromise>(nextValue), resolveElementFun,
rejectElementFun, Undefined);
}
// Set index to index + 1.
index += 1;
}
}
}
label Done {}
// Set iteratorRecord.[[Done]] to true.
// Set remainingElementsCount.[[Value]] to
// remainingElementsCount.[[Value]] - 1.
let remainingElementsCount = UnsafeCast<Smi>(
resolveElementContext[PromiseAllResolveElementContextSlots::
kPromiseAllResolveElementRemainingSlot]);
remainingElementsCount -= 1;
resolveElementContext[PromiseAllResolveElementContextSlots::
kPromiseAllResolveElementRemainingSlot] =
remainingElementsCount;
if (remainingElementsCount > 0) {
// Pre-allocate the backing store for the {values_array} to the desired
// capacity here. We may already have elements here in case of some
// fancy Thenable that calls the resolve callback immediately, so we need
// to handle that correctly here.
const valuesArray = UnsafeCast<JSArray>(
resolveElementContext[PromiseAllResolveElementContextSlots::
kPromiseAllResolveElementValuesArraySlot]);
const oldElements = UnsafeCast<FixedArray>(valuesArray.elements);
const oldCapacity = oldElements.length_intptr;
const newCapacity = SmiUntag(index);
if (oldCapacity < newCapacity) {
valuesArray.elements =
ExtractFixedArray(oldElements, 0, oldCapacity, newCapacity);
}
} else
deferred {
// If remainingElementsCount.[[Value]] is 0, then
// Let valuesArray be CreateArrayFromList(values).
// Perform ? Call(resultCapability.[[Resolve]], undefined,
// « valuesArray »).
assert(remainingElementsCount == 0);
const valuesArray = UnsafeCast<JSAny>(
resolveElementContext
[PromiseAllResolveElementContextSlots::
kPromiseAllResolveElementValuesArraySlot]);
Call(nativeContext, UnsafeCast<JSAny>(resolve), Undefined, valuesArray);
}
// Return resultCapability.[[Promise]].
return promise;
}
transitioning macro GeneratePromiseAll<F1: type, F2: type>(implicit context:
Context)(
receiver: JSAny, iterable: JSAny, createResolveElementFunctor: F1,
createRejectElementFunctor: F2): JSAny {
// Let C be the this value.
// If Type(C) is not Object, throw a TypeError exception.
const receiver = Cast<JSReceiver>(receiver)
otherwise ThrowTypeError(
MessageTemplate::kCalledOnNonObject, 'Promise.all');
// Let promiseCapability be ? NewPromiseCapability(C).
// Don't fire debugEvent so that forwarding the rejection through all does
// not trigger redundant ExceptionEvents
const capability = NewPromiseCapability(receiver, False);
try {
try {
// Let iterator be GetIterator(iterable).
// IfAbruptRejectPromise(iterator, promiseCapability).
let i = iterator::GetIterator(iterable);
// Let result be PerformPromiseAll(iteratorRecord, C,
// promiseCapability). If result is an abrupt completion, then
// If iteratorRecord.[[Done]] is false, let result be
// IteratorClose(iterator, result).
// IfAbruptRejectPromise(result, promiseCapability).
return PerformPromiseAll(
receiver, capability, i, createResolveElementFunctor,
createRejectElementFunctor) otherwise Reject;
} catch (e) deferred {
goto Reject(e);
}
}
label Reject(e: Object) deferred {
// Exception must be bound to a JS value.
const e = UnsafeCast<JSAny>(e);
const reject = UnsafeCast<JSAny>(capability.reject);
Call(context, reject, Undefined, e);
return capability.promise;
}
}
// ES#sec-promise.all
transitioning javascript builtin PromiseAll(
js-implicit context: Context, receiver: JSAny)(iterable: JSAny): JSAny {
return GeneratePromiseAll(
receiver, iterable, PromiseAllResolveElementFunctor{},
PromiseAllRejectElementFunctor{});
}
// ES#sec-promise.allsettled
// Promise.allSettled ( iterable )
transitioning javascript builtin PromiseAllSettled(
js-implicit context: Context, receiver: JSAny)(iterable: JSAny): JSAny {
return GeneratePromiseAll(
receiver, iterable, PromiseAllSettledResolveElementFunctor{},
PromiseAllSettledRejectElementFunctor{});
}
}