/* 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/. */

#ifndef vm_PropMap_inl_h
#define vm_PropMap_inl_h

#include "vm/PropMap.h"

#include "gc/Cell.h"
#include "gc/Zone.h"
#include "vm/JSContext.h"

#include "gc/GCContext-inl.h"

namespace js {

inline AutoKeepPropMapTables::AutoKeepPropMapTables(JSContext* cx)
    : cx_(cx), prev_(cx->zone()->keepPropMapTables()) {
  cx->zone()->setKeepPropMapTables(true);
}

inline AutoKeepPropMapTables::~AutoKeepPropMapTables() {
  cx_->zone()->setKeepPropMapTables(prev_);
}

// static
MOZ_ALWAYS_INLINE PropMap* PropMap::lookupLinear(uint32_t mapLength,
                                                 PropertyKey key,
                                                 uint32_t* index) {
  MOZ_ASSERT(mapLength > 0);
  MOZ_ASSERT(mapLength <= Capacity);

  // This function is very hot, so we use a macro to manually unroll the lookups
  // below. Some compilers are able to unroll the equivalent loops, but they're
  // not very consistent about this. The code below results in reasonable code
  // with all compilers we tested.

  static_assert(PropMap::Capacity == 8,
                "Code below needs to change when capacity changes");

#define LOOKUP_KEY(idx)                        \
  if (mapLength > idx && getKey(idx) == key) { \
    *index = idx;                              \
    return this;                               \
  }
  LOOKUP_KEY(0);
  LOOKUP_KEY(1);
  LOOKUP_KEY(2);
  LOOKUP_KEY(3);
  LOOKUP_KEY(4);
  LOOKUP_KEY(5);
  LOOKUP_KEY(6);
  LOOKUP_KEY(7);
#undef LOOKUP_KEY

  PropMap* map = this;
  while (map->hasPrevious()) {
    map = map->asLinked()->previous();
#define LOOKUP_KEY(idx)          \
  if (map->getKey(idx) == key) { \
    *index = idx;                \
    return map;                  \
  }
    LOOKUP_KEY(0);
    LOOKUP_KEY(1);
    LOOKUP_KEY(2);
    LOOKUP_KEY(3);
    LOOKUP_KEY(4);
    LOOKUP_KEY(5);
    LOOKUP_KEY(6);
    LOOKUP_KEY(7);
#undef LOOKUP_KEY
  }

  return nullptr;
}

MOZ_ALWAYS_INLINE PropMap* PropMapTable::lookup(PropMap* map,
                                                uint32_t mapLength,
                                                PropertyKey key,
                                                uint32_t* index) {
  JS::AutoCheckCannotGC nogc;
  MOZ_ASSERT(map->asLinked()->maybeTable(nogc) == this);

  PropMapAndIndex entry;
  if (lookupInCache(key, &entry)) {
    if (entry.isNone()) {
      return nullptr;
    }
  } else {
    auto p = lookupRaw(key);
    addToCache(key, p);
    if (!p) {
      return nullptr;
    }
    entry = *p;
  }

  // For the last map, only properties in [0, mapLength) are part of the object.
  if (entry.map() == map && entry.index() >= mapLength) {
    return nullptr;
  }

  *index = entry.index();
  return entry.map();
}

// static
MOZ_ALWAYS_INLINE PropMap* PropMap::lookupPure(uint32_t mapLength,
                                               PropertyKey key,
                                               uint32_t* index) {
  if (canHaveTable()) {
    JS::AutoCheckCannotGC nogc;
    if (PropMapTable* table = asLinked()->maybeTable(nogc)) {
      return table->lookup(this, mapLength, key, index);
    }
  }

  return lookupLinear(mapLength, key, index);
}

// static
MOZ_ALWAYS_INLINE PropMap* PropMap::lookup(JSContext* cx, uint32_t mapLength,
                                           PropertyKey key, uint32_t* index) {
  if (canHaveTable()) {
    JS::AutoCheckCannotGC nogc;
    if (PropMapTable* table = asLinked()->ensureTable(cx, nogc);
        MOZ_LIKELY(table)) {
      return table->lookup(this, mapLength, key, index);
    }
    // OOM. Do a linear lookup.
    cx->recoverFromOutOfMemory();
  }

  return lookupLinear(mapLength, key, index);
}

// static
inline void SharedPropMap::getPrevious(MutableHandle<SharedPropMap*> map,
                                       uint32_t* mapLength) {
  // Update the map/mapLength pointers to "remove" the last property. In most
  // cases we can simply decrement *mapLength, but if *mapLength is 1 we have to
  // either start at the previous map or set map/mapLength to nullptr/zero
  // (if there is just one property).

  MOZ_ASSERT(map);
  MOZ_ASSERT(*mapLength > 0);

  if (*mapLength > 1) {
    *mapLength -= 1;
    return;
  }

  if (map->hasPrevious()) {
    map.set(map->asNormal()->previous());
    *mapLength = PropMap::Capacity;
    return;
  }

  map.set(nullptr);
  *mapLength = 0;
}

// static
inline bool PropMap::lookupForRemove(JSContext* cx, PropMap* map,
                                     uint32_t mapLength, PropertyKey key,
                                     const AutoKeepPropMapTables& keep,
                                     PropMap** propMap, uint32_t* propIndex,
                                     PropMapTable** table,
                                     PropMapTable::Ptr* ptr) {
  if (map->isDictionary()) {
    *table = map->asLinked()->ensureTable(cx, keep);
    if (!*table) {
      return false;
    }
    *ptr = (*table)->lookupRaw(key);
    *propMap = *ptr ? (*ptr)->map() : nullptr;
    *propIndex = *ptr ? (*ptr)->index() : 0;
    return true;
  }

  *table = nullptr;
  *propMap = map->lookup(cx, mapLength, key, propIndex);
  return true;
}

MOZ_ALWAYS_INLINE bool SharedPropMap::shouldConvertToDictionaryForAdd() const {
  if (MOZ_LIKELY(numPreviousMaps() < NumPrevMapsConsiderDictionary)) {
    return false;
  }
  if (numPreviousMaps() >= NumPrevMapsAlwaysDictionary) {
    return true;
  }

  // More heuristics: if one of the last two maps has had a dictionary
  // conversion before, or is branchy (indicated by parent != previous), convert
  // to dictionary.
  const SharedPropMap* curMap = this;
  for (size_t i = 0; i < 2; i++) {
    if (curMap->hadDictionaryConversion()) {
      return true;
    }
    if (curMap->treeDataRef().parent.map() != curMap->asNormal()->previous()) {
      return true;
    }
    curMap = curMap->asNormal()->previous();
  }
  return false;
}

inline void SharedPropMap::sweep(JS::GCContext* gcx) {
  // We detach the child from the parent if the parent is reachable.
  //
  // This test depends on PropMap arenas not being freed until after we finish
  // incrementally sweeping them. If that were not the case the parent pointer
  // could point to a marked cell that had been deallocated and then
  // reallocated, since allocating a cell in a zone that is being marked will
  // set the mark bit for that cell.

  MOZ_ASSERT(zone()->isGCSweeping());
  MOZ_ASSERT_IF(hasPrevious(), asLinked()->previous()->zone() == zone());

  SharedPropMapAndIndex parent = treeDataRef().parent;
  if (!parent.isNone() && TenuredThingIsMarkedAny(parent.map())) {
    parent.map()->removeChild(gcx, this);
  }
}

inline void SharedPropMap::finalize(JS::GCContext* gcx) {
  if (canHaveTable() && asLinked()->hasTable()) {
    asLinked()->purgeTable(gcx);
  }
  if (hasChildrenSet()) {
    SharedChildrenPtr& childrenRef = treeDataRef().children;
    gcx->delete_(this, childrenRef.toChildrenSet(), MemoryUse::PropMapChildren);
    childrenRef.setNone();
  }
}

inline void DictionaryPropMap::finalize(JS::GCContext* gcx) {
  if (asLinked()->hasTable()) {
    asLinked()->purgeTable(gcx);
  }
}

}  // namespace js

#endif /* vm_PropMap_inl_h */
