| // 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{}); |
| } |
| } |