// 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 runtime {
extern transitioning runtime
RejectPromise(implicit context: Context)(JSPromise, JSAny, Boolean): JSAny;

extern transitioning runtime
PromiseRevokeReject(implicit context: Context)(JSPromise): JSAny;

extern transitioning runtime
PromiseRejectAfterResolved(implicit context: Context)(JSPromise, JSAny): JSAny;

extern transitioning runtime
PromiseResolveAfterResolved(implicit context: Context)(JSPromise, JSAny): JSAny;

extern transitioning runtime
PromiseRejectEventFromStack(implicit context: Context)(JSPromise, JSAny): JSAny;
}

// https://tc39.es/ecma262/#sec-promise-abstract-operations
namespace promise {

extern macro PromiseForwardingHandlerSymbolConstant(): Symbol;
const kPromiseForwardingHandlerSymbol: Symbol =
    PromiseForwardingHandlerSymbolConstant();
extern macro PromiseHandledBySymbolConstant(): Symbol;
const kPromiseHandledBySymbol: Symbol = PromiseHandledBySymbolConstant();
extern macro ResolveStringConstant(): String;
const kResolveString: String = ResolveStringConstant();
extern macro IsPromiseResolveProtectorCellInvalid(): bool;

extern macro AllocateFunctionWithMapAndContext(
    Map, SharedFunctionInfo, FunctionContext): JSFunction;

extern macro PromiseReactionMapConstant(): Map;
extern macro PromiseFulfillReactionJobTaskMapConstant(): Map;
extern macro PromiseRejectReactionJobTaskMapConstant(): Map;
extern transitioning builtin
ResolvePromise(Context, JSPromise, JSAny): JSAny;

extern transitioning builtin
EnqueueMicrotask(Context, Microtask): Undefined;

macro
ExtractHandlerContextInternal(implicit context: Context)(
    handler: Callable|Undefined): Context labels NotFound {
  let iter: JSAny = handler;
  while (true) {
    typeswitch (iter) {
      case (b: JSBoundFunction): {
        iter = b.bound_target_function;
      }
      case (p: JSProxy): {
        iter = p.target;
      }
      case (f: JSFunction): {
        return f.context;
      }
      case (JSAny): {
        break;
      }
    }
  }
  goto NotFound;
}

macro
ExtractHandlerContext(implicit context: Context)(handler: Callable|
                                                 Undefined): Context {
  try {
    return ExtractHandlerContextInternal(handler) otherwise NotFound;
  } label NotFound deferred {
    return context;
  }
}

macro
ExtractHandlerContext(implicit context: Context)(
    primary: Callable|Undefined, secondary: Callable|Undefined): Context {
  try {
    return ExtractHandlerContextInternal(primary) otherwise NotFound;
  } label NotFound deferred {
    return ExtractHandlerContextInternal(secondary) otherwise Default;
  } label Default deferred {
    return context;
  }
}

transitioning macro MorphAndEnqueuePromiseReaction(implicit context: Context)(
    promiseReaction: PromiseReaction, argument: JSAny,
    reactionType: constexpr PromiseReactionType): void {
  let primaryHandler: Callable|Undefined;
  let secondaryHandler: Callable|Undefined;
  if constexpr (reactionType == kPromiseReactionFulfill) {
    primaryHandler = promiseReaction.fulfill_handler;
    secondaryHandler = promiseReaction.reject_handler;
  } else {
    static_assert(reactionType == kPromiseReactionReject);
    primaryHandler = promiseReaction.reject_handler;
    secondaryHandler = promiseReaction.fulfill_handler;
  }

  // According to HTML, we use the context of the appropriate handler as the
  // context of the microtask. See step 3 of HTML's EnqueueJob:
  // https://html.spec.whatwg.org/C/#enqueuejob(queuename,-job,-arguments)
  const handlerContext: Context =
      ExtractHandlerContext(primaryHandler, secondaryHandler);

  // Morph {current} from a PromiseReaction into a PromiseReactionJobTask
  // and schedule that on the microtask queue. We try to minimize the number
  // of stores here to avoid write barrier overhead.
  static_assert(
      kPromiseReactionSize ==
      kPromiseReactionJobTaskSizeOfAllPromiseReactionJobTasks);
  if constexpr (reactionType == kPromiseReactionFulfill) {
    *UnsafeConstCast(&promiseReaction.map) =
        PromiseFulfillReactionJobTaskMapConstant();
    const promiseReactionJobTask =
        UnsafeCast<PromiseFulfillReactionJobTask>(promiseReaction);
    promiseReactionJobTask.argument = argument;
    promiseReactionJobTask.context = handlerContext;
    EnqueueMicrotask(handlerContext, promiseReactionJobTask);
    static_assert(
        kPromiseReactionFulfillHandlerOffset ==
        kPromiseReactionJobTaskHandlerOffset);
    static_assert(
        kPromiseReactionPromiseOrCapabilityOffset ==
        kPromiseReactionJobTaskPromiseOrCapabilityOffset);
  } else {
    static_assert(reactionType == kPromiseReactionReject);
    *UnsafeConstCast(&promiseReaction.map) =
        PromiseRejectReactionJobTaskMapConstant();
    const promiseReactionJobTask =
        UnsafeCast<PromiseRejectReactionJobTask>(promiseReaction);
    promiseReactionJobTask.argument = argument;
    promiseReactionJobTask.context = handlerContext;
    promiseReactionJobTask.handler = primaryHandler;
    EnqueueMicrotask(handlerContext, promiseReactionJobTask);
    static_assert(
        kPromiseReactionPromiseOrCapabilityOffset ==
        kPromiseReactionJobTaskPromiseOrCapabilityOffset);
  }
}

// https://tc39.es/ecma262/#sec-triggerpromisereactions
transitioning macro TriggerPromiseReactions(implicit context: Context)(
    reactions: Zero|PromiseReaction, argument: JSAny,
    reactionType: constexpr PromiseReactionType): void {
  // We need to reverse the {reactions} here, since we record them on the
  // JSPromise in the reverse order.
  let current = reactions;
  let reversed: Zero|PromiseReaction = kZero;

  // As an additional safety net against misuse of the V8 Extras API, we
  // sanity check the {reactions} to make sure that they are actually
  // PromiseReaction instances and not actual JavaScript values (which
  // would indicate that we're rejecting or resolving an already settled
  // promise), see https://crbug.com/931640 for details on this.
  while (true) {
    typeswitch (current) {
      case (Zero): {
        break;
      }
      case (currentReaction: PromiseReaction): {
        current = currentReaction.next;
        currentReaction.next = reversed;
        reversed = currentReaction;
      }
    }
  }
  // Morph the {reactions} into PromiseReactionJobTasks and push them
  // onto the microtask queue.
  current = reversed;
  while (true) {
    typeswitch (current) {
      case (Zero): {
        break;
      }
      case (currentReaction: PromiseReaction): {
        current = currentReaction.next;
        MorphAndEnqueuePromiseReaction(currentReaction, argument, reactionType);
      }
    }
  }
}

// https://tc39.es/ecma262/#sec-fulfillpromise
transitioning builtin
FulfillPromise(implicit context: Context)(
    promise: JSPromise, value: JSAny): Undefined {
  // Assert: The value of promise.[[PromiseState]] is "pending".
  dcheck(promise.Status() == PromiseState::kPending);

  RunContextPromiseHookResolve(promise);

  // 2. Let reactions be promise.[[PromiseFulfillReactions]].
  const reactions =
      UnsafeCast<(Zero | PromiseReaction)>(promise.reactions_or_result);

  // 3. Set promise.[[PromiseResult]] to value.
  // 4. Set promise.[[PromiseFulfillReactions]] to undefined.
  // 5. Set promise.[[PromiseRejectReactions]] to undefined.
  promise.reactions_or_result = value;

  // 6. Set promise.[[PromiseState]] to "fulfilled".
  promise.SetStatus(PromiseState::kFulfilled);

  // 7. Return TriggerPromiseReactions(reactions, value).
  TriggerPromiseReactions(reactions, value, kPromiseReactionFulfill);
  return Undefined;
}

extern macro PromiseBuiltinsAssembler::
    IsIsolatePromiseHookEnabledOrDebugIsActiveOrHasAsyncEventDelegate(): bool;

extern macro PromiseBuiltinsAssembler::
    IsIsolatePromiseHookEnabledOrDebugIsActiveOrHasAsyncEventDelegate(uint32):
        bool;

// https://tc39.es/ecma262/#sec-rejectpromise
transitioning builtin
RejectPromise(implicit context: Context)(
    promise: JSPromise, reason: JSAny, debugEvent: Boolean): JSAny {
  const promiseHookFlags = PromiseHookFlags();

  // If promise hook is enabled or the debugger is active, let
  // the runtime handle this operation, which greatly reduces
  // the complexity here and also avoids a couple of back and
  // forth between JavaScript and C++ land.
  if (IsIsolatePromiseHookEnabledOrDebugIsActiveOrHasAsyncEventDelegate(
          promiseHookFlags) ||
      !promise.HasHandler()) {
    // 7. If promise.[[PromiseIsHandled]] is false, perform
    //    HostPromiseRejectionTracker(promise, "reject").
    // We don't try to handle rejecting {promise} without handler
    // here, but we let the C++ code take care of this completely.
    return runtime::RejectPromise(promise, reason, debugEvent);
  }

  RunContextPromiseHookResolve(promise, promiseHookFlags);

  // 2. Let reactions be promise.[[PromiseRejectReactions]].
  const reactions =
      UnsafeCast<(Zero | PromiseReaction)>(promise.reactions_or_result);

  // 3. Set promise.[[PromiseResult]] to reason.
  // 4. Set promise.[[PromiseFulfillReactions]] to undefined.
  // 5. Set promise.[[PromiseRejectReactions]] to undefined.
  promise.reactions_or_result = reason;

  // 6. Set promise.[[PromiseState]] to "rejected".
  promise.SetStatus(PromiseState::kRejected);

  // 8. Return TriggerPromiseReactions(reactions, reason).
  TriggerPromiseReactions(reactions, reason, kPromiseReactionReject);
  return Undefined;
}

const kPromiseCapabilitySize:
    constexpr int31 generates 'PromiseCapability::kSize';

type PromiseResolvingFunctionContext extends FunctionContext;
extern enum PromiseResolvingFunctionContextSlot extends intptr
constexpr 'PromiseBuiltins::PromiseResolvingFunctionContextSlot' {
  kPromiseSlot: Slot<PromiseResolvingFunctionContext, JSPromise>,
  kAlreadyResolvedSlot: Slot<PromiseResolvingFunctionContext, Boolean>,
  kDebugEventSlot: Slot<PromiseResolvingFunctionContext, Boolean>,
  kPromiseContextLength
}

type PromiseCapabilitiesExecutorContext extends FunctionContext;
extern enum FunctionContextSlot extends intptr
constexpr 'PromiseBuiltins::FunctionContextSlot' {
  kCapabilitySlot: Slot<PromiseCapabilitiesExecutorContext, PromiseCapability>,
  kCapabilitiesContextLength
}

@export
macro CreatePromiseCapabilitiesExecutorContext(
    nativeContext: NativeContext, capability: PromiseCapability):
    PromiseCapabilitiesExecutorContext {
  const executorContext = %RawDownCast<PromiseCapabilitiesExecutorContext>(
      AllocateSyntheticFunctionContext(
          nativeContext, FunctionContextSlot::kCapabilitiesContextLength));

  InitContextSlot(
      executorContext, FunctionContextSlot::kCapabilitySlot, capability);
  return executorContext;
}

@export
macro CreatePromiseCapability(
    promise: JSReceiver|Undefined, resolve: JSFunction|Undefined,
    reject: JSFunction|Undefined): PromiseCapability {
  return new PromiseCapability{
    map: kPromiseCapabilityMap,
    promise: promise,
    resolve: resolve,
    reject: reject
  };
}

@export
struct PromiseResolvingFunctions {
  resolve: JSFunction;
  reject: JSFunction;
}

@export
macro CreatePromiseResolvingFunctions(implicit context: Context)(
    promise: JSPromise, debugEvent: Boolean, nativeContext: NativeContext):
    PromiseResolvingFunctions {
  const promiseContext = CreatePromiseResolvingFunctionsContext(
      promise, debugEvent, nativeContext);
  const map = *NativeContextSlot(
      nativeContext, ContextSlot::STRICT_FUNCTION_WITHOUT_PROTOTYPE_MAP_INDEX);
  const resolveInfo = PromiseCapabilityDefaultResolveSharedFunConstant();

  const resolve: JSFunction =
      AllocateFunctionWithMapAndContext(map, resolveInfo, promiseContext);
  const rejectInfo = PromiseCapabilityDefaultRejectSharedFunConstant();
  const reject: JSFunction =
      AllocateFunctionWithMapAndContext(map, rejectInfo, promiseContext);
  return PromiseResolvingFunctions{resolve: resolve, reject: reject};
}

transitioning macro
InnerNewPromiseCapability(implicit context: Context)(
    constructor: HeapObject, debugEvent: Boolean): PromiseCapability {
  const nativeContext = LoadNativeContext(context);
  if (constructor ==
      *NativeContextSlot(nativeContext, ContextSlot::PROMISE_FUNCTION_INDEX)) {
    const promise = NewJSPromise();

    const pair =
        CreatePromiseResolvingFunctions(promise, debugEvent, nativeContext);

    return CreatePromiseCapability(promise, pair.resolve, pair.reject);
  } else {
    // We have to create the capability before the associated promise
    // because the builtin PromiseConstructor uses the executor.
    const capability = CreatePromiseCapability(Undefined, Undefined, Undefined);
    const executorContext =
        CreatePromiseCapabilitiesExecutorContext(nativeContext, capability);

    const executorInfo = PromiseGetCapabilitiesExecutorSharedFunConstant();
    const functionMap =
        *NativeContextSlot(
        nativeContext,
        ContextSlot::STRICT_FUNCTION_WITHOUT_PROTOTYPE_MAP_INDEX);
    const executor = AllocateFunctionWithMapAndContext(
        functionMap, executorInfo, executorContext);

    const promiseConstructor = UnsafeCast<Constructor>(constructor);
    const promise = Construct(promiseConstructor, executor);
    capability.promise = promise;

    if (!Is<Callable>(capability.resolve) || !Is<Callable>(capability.reject)) {
      ThrowTypeError(MessageTemplate::kPromiseNonCallable);
    }
    return capability;
  }
}

// https://tc39.es/ecma262/#sec-newpromisecapability
transitioning builtin
NewPromiseCapability(implicit context: Context)(
    maybeConstructor: Object, debugEvent: Boolean): PromiseCapability {
  typeswitch (maybeConstructor) {
    case (Smi): {
      ThrowTypeError(MessageTemplate::kNotConstructor, maybeConstructor);
    }
    case (constructor: HeapObject): {
      if (!IsConstructor(constructor)) {
        ThrowTypeError(MessageTemplate::kNotConstructor, maybeConstructor);
      }
      return InnerNewPromiseCapability(constructor, debugEvent);
    }
  }
}

// https://tc39.es/ecma262/#sec-promise-reject-functions
transitioning javascript builtin
PromiseCapabilityDefaultReject(
    js-implicit context: Context, receiver: JSAny)(reason: JSAny): JSAny {
  const context = %RawDownCast<PromiseResolvingFunctionContext>(context);
  // 2. Let promise be F.[[Promise]].
  const promise =
      *ContextSlot(context, PromiseResolvingFunctionContextSlot::kPromiseSlot);

  // 3. Let alreadyResolved be F.[[AlreadyResolved]].
  const alreadyResolved = *ContextSlot(
      context, PromiseResolvingFunctionContextSlot::kAlreadyResolvedSlot);

  // 4. If alreadyResolved.[[Value]] is true, return undefined.
  if (alreadyResolved == True) {
    return runtime::PromiseRejectAfterResolved(promise, reason);
  }

  // 5. Set alreadyResolved.[[Value]] to true.
  *ContextSlot(
      context, PromiseResolvingFunctionContextSlot::kAlreadyResolvedSlot) =
      True;

  // 6. Return RejectPromise(promise, reason).
  const debugEvent = *ContextSlot(
      context, PromiseResolvingFunctionContextSlot::kDebugEventSlot);
  return RejectPromise(promise, reason, debugEvent);
}

// https://tc39.es/ecma262/#sec-promise-resolve-functions
transitioning javascript builtin
PromiseCapabilityDefaultResolve(
    js-implicit context: Context, receiver: JSAny)(resolution: JSAny): JSAny {
  const context = %RawDownCast<PromiseResolvingFunctionContext>(context);
  // 2. Let promise be F.[[Promise]].
  const promise: JSPromise =
      *ContextSlot(context, PromiseResolvingFunctionContextSlot::kPromiseSlot);

  // 3. Let alreadyResolved be F.[[AlreadyResolved]].
  const alreadyResolved: Boolean = *ContextSlot(
      context, PromiseResolvingFunctionContextSlot::kAlreadyResolvedSlot);

  // 4. If alreadyResolved.[[Value]] is true, return undefined.
  if (alreadyResolved == True) {
    return runtime::PromiseResolveAfterResolved(promise, resolution);
  }

  // 5. Set alreadyResolved.[[Value]] to true.
  *ContextSlot(
      context, PromiseResolvingFunctionContextSlot::kAlreadyResolvedSlot) =
      True;

  // The rest of the logic (and the catch prediction) is
  // encapsulated in the dedicated ResolvePromise builtin.
  return ResolvePromise(context, promise, resolution);
}

@export
transitioning macro PerformPromiseThenImpl(implicit context: Context)(
    promise: JSPromise, onFulfilled: Callable|Undefined,
    onRejected: Callable|Undefined,
    resultPromiseOrCapability: JSPromise|PromiseCapability|Undefined): void {
  if (promise.Status() == PromiseState::kPending) {
    // The {promise} is still in "Pending" state, so we just record a new
    // PromiseReaction holding both the onFulfilled and onRejected callbacks.
    // Once the {promise} is resolved we decide on the concrete handler to
    // push onto the microtask queue.
    const handlerContext = ExtractHandlerContext(onFulfilled, onRejected);
    const promiseReactions =
        UnsafeCast<(Zero | PromiseReaction)>(promise.reactions_or_result);
    const reaction = NewPromiseReaction(
        handlerContext, promiseReactions, resultPromiseOrCapability,
        onFulfilled, onRejected);
    promise.reactions_or_result = reaction;
  } else {
    const reactionsOrResult = promise.reactions_or_result;
    let microtask: PromiseReactionJobTask;
    let handlerContext: Context;
    if (promise.Status() == PromiseState::kFulfilled) {
      handlerContext = ExtractHandlerContext(onFulfilled, onRejected);
      microtask = NewPromiseFulfillReactionJobTask(
          handlerContext, reactionsOrResult, onFulfilled,
          resultPromiseOrCapability);
    } else
      deferred {
        dcheck(promise.Status() == PromiseState::kRejected);
        handlerContext = ExtractHandlerContext(onRejected, onFulfilled);
        microtask = NewPromiseRejectReactionJobTask(
            handlerContext, reactionsOrResult, onRejected,
            resultPromiseOrCapability);
        if (!promise.HasHandler()) {
          runtime::PromiseRevokeReject(promise);
        }
      }
    EnqueueMicrotask(handlerContext, microtask);
  }
  promise.SetHasHandler();
}

// https://tc39.es/ecma262/#sec-performpromisethen
transitioning builtin
PerformPromiseThen(implicit context: Context)(
    promise: JSPromise, onFulfilled: Callable|Undefined,
    onRejected: Callable|Undefined, resultPromise: JSPromise|Undefined): JSAny {
  PerformPromiseThenImpl(promise, onFulfilled, onRejected, resultPromise);
  return resultPromise;
}

// https://tc39.es/ecma262/#sec-promise-reject-functions
transitioning javascript builtin
PromiseReject(
    js-implicit context: NativeContext, receiver: JSAny)(reason: JSAny): JSAny {
  // 1. Let C be the this value.
  // 2. If Type(C) is not Object, throw a TypeError exception.
  const receiver = Cast<JSReceiver>(receiver) otherwise
  ThrowTypeError(MessageTemplate::kCalledOnNonObject, 'PromiseReject');

  const promiseFun = *NativeContextSlot(ContextSlot::PROMISE_FUNCTION_INDEX);
  if (promiseFun == receiver) {
    const promise = NewJSPromise(PromiseState::kRejected, reason);
    runtime::PromiseRejectEventFromStack(promise, reason);
    return promise;
  } else {
    // 3. Let promiseCapability be ? NewPromiseCapability(C).
    const capability = NewPromiseCapability(receiver, True);

    // 4. Perform ? Call(promiseCapability.[[Reject]], undefined, « r »).
    const reject = UnsafeCast<Callable>(capability.reject);
    Call(context, reject, Undefined, reason);

    // 5. Return promiseCapability.[[Promise]].
    return capability.promise;
  }
}

const kPromiseExecutorAlreadyInvoked: constexpr MessageTemplate
    generates 'MessageTemplate::kPromiseExecutorAlreadyInvoked';

// https://tc39.es/ecma262/#sec-getcapabilitiesexecutor-functions
transitioning javascript builtin
PromiseGetCapabilitiesExecutor(js-implicit context: Context, receiver: JSAny)(
    resolve: JSAny, reject: JSAny): JSAny {
  const context = %RawDownCast<PromiseCapabilitiesExecutorContext>(context);
  const capability: PromiseCapability =
      *ContextSlot(context, FunctionContextSlot::kCapabilitySlot);
  if (capability.resolve != Undefined || capability.reject != Undefined)
    deferred {
      ThrowTypeError(kPromiseExecutorAlreadyInvoked);
    }

  capability.resolve = resolve;
  capability.reject = reject;
  return Undefined;
}

macro IsPromiseResolveLookupChainIntact(implicit context: Context)(
    nativeContext: NativeContext, constructor: JSReceiver): bool {
  if (IsForceSlowPath()) return false;
  const promiseFun =
      *NativeContextSlot(nativeContext, ContextSlot::PROMISE_FUNCTION_INDEX);
  return promiseFun == constructor && !IsPromiseResolveProtectorCellInvalid();
}

// https://tc39.es/ecma262/#sec-getpromiseresolve
transitioning macro GetPromiseResolve(implicit context: Context)(
    nativeContext: NativeContext, constructor: Constructor): JSAny {
  // 1. Assert: IsConstructor(constructor) is true.

  // 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. In this case, promiseResolveFunction is undefined,
  // and when CallResolve is called with it later, it will call Promise.resolve.
  let promiseResolveFunction: JSAny = Undefined;

  if (!IsPromiseResolveLookupChainIntact(nativeContext, constructor)) {
    let promiseResolve: JSAny;

    // 2. Let promiseResolve be ? Get(constructor, "resolve").
    promiseResolve = GetProperty(constructor, kResolveString);

    // 3. If IsCallable(promiseResolve) is false, throw a TypeError exception.
    promiseResolveFunction =
        Cast<Callable>(promiseResolve) otherwise ThrowTypeError(
            MessageTemplate::kCalledNonCallable, 'resolve');
  }
  // 4. return promiseResolve.
  return promiseResolveFunction;
}

transitioning macro CallResolve(implicit context: Context)(
    constructor: Constructor, resolve: JSAny, value: JSAny): JSAny {
  // Undefined can never be a valid value for the resolve function,
  // instead it is used as a special marker for the fast path.
  if (resolve == Undefined) {
    return PromiseResolve(constructor, value);
  } else
    deferred {
      return Call(context, UnsafeCast<Callable>(resolve), constructor, value);
    }
}

transitioning javascript builtin
PromiseConstructorLazyDeoptContinuation(
    js-implicit context: NativeContext, receiver: JSAny)(
    promise: JSAny, reject: JSAny, exception: JSAny|TheHole,
    _result: JSAny): JSAny {
  typeswitch (exception) {
    case (TheHole): {
    }
    case (e: JSAny): {
      Call(context, reject, Undefined, e);
    }
  }
  return promise;
}

extern macro PromiseCapabilityDefaultRejectSharedFunConstant():
    SharedFunctionInfo;
extern macro PromiseCapabilityDefaultResolveSharedFunConstant():
    SharedFunctionInfo;
extern macro PromiseGetCapabilitiesExecutorSharedFunConstant():
    SharedFunctionInfo;
}