// 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): PromiseAllResolveElementContext { const resolveContext = %RawDownCast< PromiseAllResolveElementContext>(AllocateSyntheticFunctionContext( nativeContext, PromiseAllResolveElementContextSlots::kPromiseAllResolveElementLength)); InitContextSlot( resolveContext, PromiseAllResolveElementContextSlots:: kPromiseAllResolveElementRemainingSlot, 1); InitContextSlot( resolveContext, PromiseAllResolveElementContextSlots:: kPromiseAllResolveElementCapabilitySlot, capability); InitContextSlot( resolveContext, PromiseAllResolveElementContextSlots::kPromiseAllResolveElementValuesSlot, kEmptyFixedArray); return resolveContext; } macro CreatePromiseAllResolveElementFunction(implicit context: Context)( resolveElementContext: PromiseAllResolveElementContext, index: Smi, nativeContext: NativeContext, resolveFunction: SharedFunctionInfo): JSFunction { assert(index > 0); assert(index < kPropertyArrayHashFieldMax); const map = *ContextSlot( nativeContext, ContextSlot::STRICT_FUNCTION_WITHOUT_PROTOTYPE_MAP_INDEX); const resolve = AllocateFunctionWithMapAndContext( map, resolveFunction, resolveElementContext); assert(kPropertyArrayNoHashSentinel == 0); resolve.properties_or_hash = index; return resolve; } @export macro CreatePromiseResolvingFunctionsContext(implicit context: Context)( promise: JSPromise, debugEvent: Boolean, nativeContext: NativeContext): PromiseResolvingFunctionContext { const resolveContext = %RawDownCast<PromiseResolvingFunctionContext>( AllocateSyntheticFunctionContext( nativeContext, PromiseResolvingFunctionContextSlot::kPromiseContextLength)); InitContextSlot( resolveContext, PromiseResolvingFunctionContextSlot::kPromiseSlot, promise); InitContextSlot( resolveContext, PromiseResolvingFunctionContextSlot::kAlreadyResolvedSlot, False); InitContextSlot( resolveContext, PromiseResolvingFunctionContextSlot::kDebugEventSlot, debugEvent); static_assert( PromiseResolvingFunctionContextSlot::kPromiseContextLength == ContextSlot::MIN_CONTEXT_SLOTS + 3); return resolveContext; } macro IsPromiseThenLookupChainIntact(implicit context: Context)( nativeContext: NativeContext, receiverMap: Map): bool { if (IsForceSlowPath()) return false; if (!IsJSPromiseMap(receiverMap)) return false; if (receiverMap.prototype != *NativeContextSlot( nativeContext, ContextSlot::PROMISE_PROTOTYPE_INDEX)) { return false; } return !IsPromiseThenProtectorCellInvalid(); } struct PromiseAllResolveElementFunctor { macro Call(implicit context: Context)( resolveElementContext: PromiseAllResolveElementContext, nativeContext: NativeContext, index: Smi, _capability: PromiseCapability): Callable { return CreatePromiseAllResolveElementFunction( resolveElementContext, index, nativeContext, PromiseAllResolveElementSharedFunConstant()); } } struct PromiseAllRejectElementFunctor { macro Call(implicit context: Context)( _resolveElementContext: PromiseAllResolveElementContext, _nativeContext: NativeContext, _index: Smi, capability: PromiseCapability): Callable { return UnsafeCast<Callable>(capability.reject); } } struct PromiseAllSettledResolveElementFunctor { macro Call(implicit context: Context)( resolveElementContext: PromiseAllResolveElementContext, nativeContext: NativeContext, index: Smi, _capability: PromiseCapability): Callable { return CreatePromiseAllResolveElementFunction( resolveElementContext, index, nativeContext, PromiseAllSettledResolveElementSharedFunConstant()); } } struct PromiseAllSettledRejectElementFunctor { macro Call(implicit context: Context)( resolveElementContext: PromiseAllResolveElementContext, nativeContext: NativeContext, index: Smi, _capability: PromiseCapability): Callable { return CreatePromiseAllResolveElementFunction( resolveElementContext, index, nativeContext, PromiseAllSettledRejectElementSharedFunConstant()); } } transitioning macro PerformPromiseAll<F1: type, F2: type>( implicit context: Context)( nativeContext: NativeContext, iter: iterator::IteratorRecord, constructor: Constructor, capability: PromiseCapability, promiseResolveFunction: JSAny, createResolveElementFunctor: F1, createRejectElementFunctor: F2): JSAny labels Reject(Object) { 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; try { const fastIteratorResultMap = *NativeContextSlot( nativeContext, ContextSlot::ITERATOR_RESULT_MAP_INDEX); while (true) { let nextValue: JSAny; try { // 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); } catch (e) { goto Reject(e); } // 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 below 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 anyway. ThrowRangeError( MessageTemplate::kTooManyElementsInPromiseCombinator, 'all'); } // Set remainingElementsCount.[[Value]] to // remainingElementsCount.[[Value]] + 1. *ContextSlot( resolveElementContext, PromiseAllResolveElementContextSlots:: kPromiseAllResolveElementRemainingSlot) += 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 "then" lookup on the result of the "resolve" call and // immediately chain the 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)) { // Let nextPromise be ? Call(constructor, _promiseResolve_, « // nextValue »). const nextPromise = CallResolve(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); } } else { PerformPromiseThenImpl( UnsafeCast<JSPromise>(nextValue), resolveElementFun, rejectElementFun, Undefined); } // Set index to index + 1. index += 1; } } catch (e) deferred { iterator::IteratorCloseOnException(iter); goto Reject(e); } label Done {} // Set iteratorRecord.[[Done]] to true. // Set remainingElementsCount.[[Value]] to // remainingElementsCount.[[Value]] - 1. const remainingElementsCount = -- *ContextSlot( resolveElementContext, PromiseAllResolveElementContextSlots:: kPromiseAllResolveElementRemainingSlot); check(remainingElementsCount >= 0); if (remainingElementsCount > 0) { // Pre-allocate the backing store for the {values} to the desired // capacity. We may already have elements in "values" - this happens // when the Thenable calls the resolve callback immediately. const valuesRef:&FixedArray = ContextSlot( resolveElementContext, PromiseAllResolveElementContextSlots:: kPromiseAllResolveElementValuesSlot); const values = *valuesRef; // 'index' is a 1-based index and incremented after every Promise. Later we // use 'values' as a 0-based array, so capacity 'index - 1' is enough. const newCapacity = SmiUntag(index) - 1; const oldCapacity = values.length_intptr; if (oldCapacity < newCapacity) { *valuesRef = ExtractFixedArray(values, 0, oldCapacity, newCapacity); } } else deferred { // If remainingElementsCount.[[Value]] is 0, then // Let valuesArray be CreateArrayFromList(values). // Perform ? Call(resultCapability.[[Resolve]], undefined, // « valuesArray »). const values: FixedArray = *ContextSlot( resolveElementContext, PromiseAllResolveElementContextSlots:: kPromiseAllResolveElementValuesSlot); const arrayMap = *NativeContextSlot( nativeContext, ContextSlot::JS_ARRAY_PACKED_ELEMENTS_MAP_INDEX); const valuesArray = NewJSArray(arrayMap, values); 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 { const nativeContext = LoadNativeContext(context); // 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); // NewPromiseCapability guarantees that receiver is Constructor. assert(Is<Constructor>(receiver)); const constructor = UnsafeCast<Constructor>(receiver); try { // Let promiseResolve be GetPromiseResolve(C). // IfAbruptRejectPromise(promiseResolve, promiseCapability). const promiseResolveFunction = GetPromiseResolve(nativeContext, constructor); // 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( nativeContext, i, constructor, capability, promiseResolveFunction, 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{}); } extern macro PromiseAllResolveElementSharedFunConstant(): SharedFunctionInfo; extern macro PromiseAllSettledRejectElementSharedFunConstant(): SharedFunctionInfo; extern macro PromiseAllSettledResolveElementSharedFunConstant(): SharedFunctionInfo; }