/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*-
 * vim: set ts=8 sts=2 et sw=2 tw=80:
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

#include "vm/PromiseLookup.h"

#include "mozilla/Assertions.h"  // MOZ_ASSERT

#include "jspubtd.h"  // JSProto_*

#include "builtin/Promise.h"  // js::Promise_then, js::Promise_static_resolve, js::Promise_static_species
#include "js/HeapAPI.h"  // js::gc::IsInsideNursery
#include "js/Id.h"       // SYMBOL_TO_JSID
#include "js/Value.h"    // JS::Value, JS::ObjectValue
#include "util/Poison.h"  // js::AlwaysPoison, JS_RESET_VALUE_PATTERN, MemCheckKind
#include "vm/GlobalObject.h"  // js::GlobalObject
#include "vm/JSContext.h"     // JSContext
#include "vm/JSFunction.h"    // JSFunction
#include "vm/JSObject.h"      // JSObject
#include "vm/NativeObject.h"  // js::NativeObject
#include "vm/Runtime.h"       // js::WellKnownSymbols
#include "vm/Shape.h"         // js::Shape

#include "vm/JSObject-inl.h"  // js::IsFunctionObject, js::IsNativeFunction

using JS::ObjectValue;
using JS::Value;

using js::NativeObject;

JSFunction* js::PromiseLookup::getPromiseConstructor(JSContext* cx) {
  JSObject* obj = cx->global()->maybeGetConstructor(JSProto_Promise);
  return obj ? &obj->as<JSFunction>() : nullptr;
}

NativeObject* js::PromiseLookup::getPromisePrototype(JSContext* cx) {
  JSObject* obj = cx->global()->maybeGetPrototype(JSProto_Promise);
  return obj ? &obj->as<NativeObject>() : nullptr;
}

bool js::PromiseLookup::isDataPropertyNative(JSContext* cx, NativeObject* obj,
                                             uint32_t slot, JSNative native) {
  JSFunction* fun;
  if (!IsFunctionObject(obj->getSlot(slot), &fun)) {
    return false;
  }
  return fun->maybeNative() == native && fun->realm() == cx->realm();
}

bool js::PromiseLookup::isAccessorPropertyNative(JSContext* cx,
                                                 NativeObject* holder,
                                                 uint32_t getterSlot,
                                                 JSNative native) {
  JSObject* getter = holder->getGetter(getterSlot);
  return getter && IsNativeFunction(getter, native) &&
         getter->as<JSFunction>().realm() == cx->realm();
}

void js::PromiseLookup::initialize(JSContext* cx) {
  MOZ_ASSERT(state_ == State::Uninitialized);

  // Get the canonical Promise.prototype.
  NativeObject* promiseProto = getPromisePrototype(cx);

  // Check condition 1:
  // Leave the cache uninitialized if the Promise class itself is not yet
  // initialized.
  if (!promiseProto) {
    return;
  }

  // Get the canonical Promise constructor.
  JSFunction* promiseCtor = getPromiseConstructor(cx);
  MOZ_ASSERT(promiseCtor,
             "The Promise constructor is initialized iff Promise.prototype is "
             "initialized");

  // Shortcut returns below means Promise[@@species] will never be
  // optimizable, set to disabled now, and clear it later when we succeed.
  state_ = State::Disabled;

  // Check condition 2:
  // Look up Promise.prototype.constructor and ensure it's a data property.
  mozilla::Maybe<PropertyInfo> ctorProp =
      promiseProto->lookup(cx, cx->names().constructor);
  if (ctorProp.isNothing() || !ctorProp->isDataProperty()) {
    return;
  }

  // Get the referred value, and ensure it holds the canonical Promise
  // constructor.
  JSFunction* ctorFun;
  if (!IsFunctionObject(promiseProto->getSlot(ctorProp->slot()), &ctorFun)) {
    return;
  }
  if (ctorFun != promiseCtor) {
    return;
  }

  // Check condition 3:
  // Look up Promise.prototype.then and ensure it's a data property.
  mozilla::Maybe<PropertyInfo> thenProp =
      promiseProto->lookup(cx, cx->names().then);
  if (thenProp.isNothing() || !thenProp->isDataProperty()) {
    return;
  }

  // Get the referred value, and ensure it holds the canonical "then"
  // function.
  if (!isDataPropertyNative(cx, promiseProto, thenProp->slot(), Promise_then)) {
    return;
  }

  // Check condition 4:
  // Look up the '@@species' value on Promise.
  mozilla::Maybe<PropertyInfo> speciesProp = promiseCtor->lookup(
      cx, PropertyKey::Symbol(cx->wellKnownSymbols().species));
  if (speciesProp.isNothing() || !promiseCtor->hasGetter(*speciesProp)) {
    return;
  }

  // Get the referred value, ensure it holds the canonical Promise[@@species]
  // function.
  uint32_t speciesGetterSlot = speciesProp->slot();
  if (!isAccessorPropertyNative(cx, promiseCtor, speciesGetterSlot,
                                Promise_static_species)) {
    return;
  }

  // Check condition 5:
  // Look up Promise.resolve and ensure it's a data property.
  mozilla::Maybe<PropertyInfo> resolveProp =
      promiseCtor->lookup(cx, cx->names().resolve);
  if (resolveProp.isNothing() || !resolveProp->isDataProperty()) {
    return;
  }

  // Get the referred value, and ensure it holds the canonical "resolve"
  // function.
  if (!isDataPropertyNative(cx, promiseCtor, resolveProp->slot(),
                            Promise_static_resolve)) {
    return;
  }

  // Store raw pointers below. This is okay to do here, because all objects
  // are in the tenured heap.
  MOZ_ASSERT(!gc::IsInsideNursery(promiseCtor->shape()));
  MOZ_ASSERT(!gc::IsInsideNursery(promiseProto->shape()));

  state_ = State::Initialized;
  promiseConstructorShape_ = promiseCtor->shape();
  promiseProtoShape_ = promiseProto->shape();
  promiseSpeciesGetterSlot_ = speciesGetterSlot;
  promiseResolveSlot_ = resolveProp->slot();
  promiseProtoConstructorSlot_ = ctorProp->slot();
  promiseProtoThenSlot_ = thenProp->slot();
}

void js::PromiseLookup::reset() {
  AlwaysPoison(this, JS_RESET_VALUE_PATTERN, sizeof(*this),
               MemCheckKind::MakeUndefined);
  state_ = State::Uninitialized;
}

bool js::PromiseLookup::isPromiseStateStillSane(JSContext* cx) {
  MOZ_ASSERT(state_ == State::Initialized);

  NativeObject* promiseProto = getPromisePrototype(cx);
  MOZ_ASSERT(promiseProto);

  NativeObject* promiseCtor = getPromiseConstructor(cx);
  MOZ_ASSERT(promiseCtor);

  // Ensure that Promise.prototype still has the expected shape.
  if (promiseProto->shape() != promiseProtoShape_) {
    return false;
  }

  // Ensure that Promise still has the expected shape.
  if (promiseCtor->shape() != promiseConstructorShape_) {
    return false;
  }

  // Ensure that Promise.prototype.constructor is the canonical constructor.
  if (promiseProto->getSlot(promiseProtoConstructorSlot_) !=
      ObjectValue(*promiseCtor)) {
    return false;
  }

  // Ensure that Promise.prototype.then is the canonical "then" function.
  if (!isDataPropertyNative(cx, promiseProto, promiseProtoThenSlot_,
                            Promise_then)) {
    return false;
  }

  // Ensure the species getter contains the canonical @@species function.
  if (!isAccessorPropertyNative(cx, promiseCtor, promiseSpeciesGetterSlot_,
                                Promise_static_species)) {
    return false;
  }

  // Ensure that Promise.resolve is the canonical "resolve" function.
  if (!isDataPropertyNative(cx, promiseCtor, promiseResolveSlot_,
                            Promise_static_resolve)) {
    return false;
  }

  return true;
}

bool js::PromiseLookup::ensureInitialized(JSContext* cx,
                                          Reinitialize reinitialize) {
  if (state_ == State::Uninitialized) {
    // If the cache is not initialized, initialize it.
    initialize(cx);
  } else if (state_ == State::Initialized) {
    if (reinitialize == Reinitialize::Allowed) {
      if (!isPromiseStateStillSane(cx)) {
        // If the promise state is no longer sane, reinitialize.
        reset();
        initialize(cx);
      }
    } else {
      // When we're not allowed to reinitialize, the promise state must
      // still be sane if the cache is already initialized.
      MOZ_ASSERT(isPromiseStateStillSane(cx));
    }
  }

  // If the cache is disabled or still uninitialized, don't bother trying to
  // optimize.
  if (state_ != State::Initialized) {
    return false;
  }

  // By the time we get here, we should have a sane promise state.
  MOZ_ASSERT(isPromiseStateStillSane(cx));

  return true;
}

bool js::PromiseLookup::isDefaultPromiseState(JSContext* cx) {
  // Promise and Promise.prototype are in their default states iff the
  // lookup cache was successfully initialized.
  return ensureInitialized(cx, Reinitialize::Allowed);
}

bool js::PromiseLookup::hasDefaultProtoAndNoShadowedProperties(
    JSContext* cx, PromiseObject* promise) {
  // Ensure |promise|'s prototype is the actual Promise.prototype.
  if (promise->staticPrototype() != getPromisePrototype(cx)) {
    return false;
  }

  // Ensure |promise| doesn't define any own properties. This serves as a
  // quick check to make sure |promise| doesn't define an own "constructor"
  // or "then" property which may shadow Promise.prototype.constructor or
  // Promise.prototype.then.
  return promise->empty();
}

bool js::PromiseLookup::isDefaultInstance(JSContext* cx, PromiseObject* promise,
                                          Reinitialize reinitialize) {
  // Promise and Promise.prototype must be in their default states.
  if (!ensureInitialized(cx, reinitialize)) {
    return false;
  }

  // The object uses the default properties from Promise.prototype.
  return hasDefaultProtoAndNoShadowedProperties(cx, promise);
}
