/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*-
 * vim: set ts=8 sw=2 et tw=0 ft=c:
 *
 * 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 "frontend/ObjLiteral.h"

#include "mozilla/DebugOnly.h"  // mozilla::DebugOnly
#include "mozilla/HashTable.h"  // mozilla::HashSet

#include "NamespaceImports.h"  // ValueVector

#include "builtin/Array.h"  // NewDenseCopiedArray
#include "frontend/CompilationStencil.h"  // frontend::{CompilationStencil, CompilationAtomCache}
#include "frontend/ParserAtom.h"                   // frontend::ParserAtomTable
#include "frontend/TaggedParserAtomIndexHasher.h"  // TaggedParserAtomIndexHasher
#include "gc/AllocKind.h"                          // gc::AllocKind
#include "js/Id.h"                                 // JS::PropertyKey
#include "js/Printer.h"                            // js::Fprinter
#include "js/RootingAPI.h"                         // Rooted
#include "js/TypeDecls.h"                          // RootedId, RootedValue
#include "vm/JSObject.h"                           // TenuredObject
#include "vm/JSONPrinter.h"                        // js::JSONPrinter
#include "vm/NativeObject.h"                       // NativeDefineDataProperty
#include "vm/PlainObject.h"                        // PlainObject

#include "gc/ObjectKind-inl.h"    // gc::GetGCObjectKind
#include "vm/JSAtomUtils-inl.h"   // AtomToId
#include "vm/JSObject-inl.h"      // NewBuiltinClassInstance
#include "vm/NativeObject-inl.h"  // AddDataPropertyNonDelegate

namespace js {

bool ObjLiteralWriter::checkForDuplicatedNames(FrontendContext* fc) {
  if (!mightContainDuplicatePropertyNames_) {
    return true;
  }

  // If possible duplicate property names are detected by bloom-filter,
  // check again with hash-set.

  mozilla::HashSet<frontend::TaggedParserAtomIndex,
                   frontend::TaggedParserAtomIndexHasher>
      propNameSet;

  if (!propNameSet.reserve(propertyCount_)) {
    js::ReportOutOfMemory(fc);
    return false;
  }

  ObjLiteralReader reader(getCode());

  while (true) {
    ObjLiteralInsn insn;
    if (!reader.readInsn(&insn)) {
      break;
    }

    if (insn.getKey().isArrayIndex()) {
      continue;
    }

    auto propName = insn.getKey().getAtomIndex();

    auto p = propNameSet.lookupForAdd(propName);
    if (p) {
      flags_.setFlag(ObjLiteralFlag::HasIndexOrDuplicatePropName);
      break;
    }

    // Already reserved above and doesn't fail.
    MOZ_ALWAYS_TRUE(propNameSet.add(p, propName));
  }

  return true;
}

static void InterpretObjLiteralValue(
    JSContext* cx, const frontend::CompilationAtomCache& atomCache,
    const ObjLiteralInsn& insn, MutableHandleValue valOut) {
  switch (insn.getOp()) {
    case ObjLiteralOpcode::ConstValue:
      valOut.set(insn.getConstValue());
      return;
    case ObjLiteralOpcode::ConstString: {
      frontend::TaggedParserAtomIndex index = insn.getAtomIndex();
      JSString* str = atomCache.getExistingStringAt(cx, index);
      MOZ_ASSERT(str);
      valOut.setString(str);
      return;
    }
    case ObjLiteralOpcode::Null:
      valOut.setNull();
      return;
    case ObjLiteralOpcode::Undefined:
      valOut.setUndefined();
      return;
    case ObjLiteralOpcode::True:
      valOut.setBoolean(true);
      return;
    case ObjLiteralOpcode::False:
      valOut.setBoolean(false);
      return;
    default:
      MOZ_CRASH("Unexpected object-literal instruction opcode");
  }
}

static uint32_t CountNonIndexPropertiesUpTo(
    const mozilla::Span<const uint8_t> literalInsns, uint32_t limit) {
  ObjLiteralReader reader(literalInsns);

  uint32_t count = 0;
  while (count < limit) {
    // Make sure `insn` doesn't live across GC.
    ObjLiteralInsn insn;
    if (!reader.readInsn(&insn)) {
      break;
    }
    if (!insn.getKey().isArrayIndex()) {
      count++;
    }
  }

  return count;
}

enum class PropertySetKind {
  UniqueNames,
  Normal,
};

template <PropertySetKind kind>
static gc::AllocKind AllocKindForObjectLiteral(
    const mozilla::Span<const uint8_t> literalInsns, uint32_t propCount) {
  // Use NewObjectGCKind for empty object literals to reserve some fixed slots
  // for new properties. This improves performance for common patterns such as
  // |Object.assign({}, ...)|.
  if (propCount == 0) {
    return NewObjectGCKind();
  }

  // Don't reserve object slots for index properties that don't use them.
  if (kind == PropertySetKind::Normal) {
    propCount =
        CountNonIndexPropertiesUpTo(literalInsns, gc::MaxGCObjectFixedSlots);
  }

  return gc::GetGCObjectKind(propCount);
}

template <PropertySetKind kind>
JSObject* InterpretObjLiteralObj(
    JSContext* cx, const frontend::CompilationAtomCache& atomCache,
    const mozilla::Span<const uint8_t> literalInsns, uint32_t propertyCount) {
  gc::AllocKind allocKind =
      AllocKindForObjectLiteral<kind>(literalInsns, propertyCount);

  Rooted<PlainObject*> obj(
      cx, NewPlainObjectWithAllocKind(cx, allocKind, TenuredObject));
  if (!obj) {
    return nullptr;
  }

  ObjLiteralReader reader(literalInsns);

  RootedId propId(cx);
  RootedValue propVal(cx);
  while (true) {
    // Make sure `insn` doesn't live across GC.
    ObjLiteralInsn insn;
    if (!reader.readInsn(&insn)) {
      break;
    }
    MOZ_ASSERT(insn.isValid());
    MOZ_ASSERT_IF(kind == PropertySetKind::UniqueNames,
                  !insn.getKey().isArrayIndex());

    if (kind == PropertySetKind::Normal && insn.getKey().isArrayIndex()) {
      propId = PropertyKey::Int(insn.getKey().getArrayIndex());
    } else {
      JSAtom* jsatom =
          atomCache.getExistingAtomAt(cx, insn.getKey().getAtomIndex());
      MOZ_ASSERT(jsatom);
      propId = AtomToId(jsatom);
    }

    InterpretObjLiteralValue(cx, atomCache, insn, &propVal);

    if constexpr (kind == PropertySetKind::UniqueNames) {
      if (!AddDataPropertyToPlainObject(cx, obj, propId, propVal)) {
        return nullptr;
      }
    } else {
      if (!NativeDefineDataProperty(cx, obj, propId, propVal,
                                    JSPROP_ENUMERATE)) {
        return nullptr;
      }
    }
  }

  return obj;
}

static JSObject* InterpretObjLiteralObj(
    JSContext* cx, const frontend::CompilationAtomCache& atomCache,
    const mozilla::Span<const uint8_t> literalInsns, ObjLiteralFlags flags,
    uint32_t propertyCount) {
  if (!flags.hasFlag(ObjLiteralFlag::HasIndexOrDuplicatePropName)) {
    return InterpretObjLiteralObj<PropertySetKind::UniqueNames>(
        cx, atomCache, literalInsns, propertyCount);
  }

  return InterpretObjLiteralObj<PropertySetKind::Normal>(
      cx, atomCache, literalInsns, propertyCount);
}

static JSObject* InterpretObjLiteralArray(
    JSContext* cx, const frontend::CompilationAtomCache& atomCache,
    const mozilla::Span<const uint8_t> literalInsns, uint32_t propertyCount) {
  ObjLiteralReader reader(literalInsns);
  ObjLiteralInsn insn;

  Rooted<ValueVector> elements(cx, ValueVector(cx));
  if (!elements.reserve(propertyCount)) {
    return nullptr;
  }

  RootedValue propVal(cx);
  while (reader.readInsn(&insn)) {
    MOZ_ASSERT(insn.isValid());

    InterpretObjLiteralValue(cx, atomCache, insn, &propVal);
    elements.infallibleAppend(propVal);
  }

  return NewDenseCopiedArray(cx, elements.length(), elements.begin(),
                             NewObjectKind::TenuredObject);
}

// ES2023 draft rev ee74c9cb74dbfa23e62b486f5226102c345c678e
//
// GetTemplateObject ( templateLiteral )
// https://tc39.es/ecma262/#sec-gettemplateobject
//
// Steps 8-16.
static JSObject* InterpretObjLiteralCallSiteObj(
    JSContext* cx, const frontend::CompilationAtomCache& atomCache,
    const mozilla::Span<const uint8_t> literalInsns, uint32_t propertyCount) {
  ObjLiteralReader reader(literalInsns);
  ObjLiteralInsn insn;

  // We have to read elements for two arrays. The 'cooked' values are followed
  // by the 'raw' values. Both arrays have the same length.
  MOZ_ASSERT((propertyCount % 2) == 0);
  uint32_t count = propertyCount / 2;

  Rooted<ValueVector> elements(cx, ValueVector(cx));
  if (!elements.reserve(count)) {
    return nullptr;
  }

  RootedValue propVal(cx);
  auto readElements = [&](uint32_t count) {
    MOZ_ASSERT(elements.empty());

    for (size_t i = 0; i < count; i++) {
      MOZ_ALWAYS_TRUE(reader.readInsn(&insn));
      MOZ_ASSERT(insn.isValid());

      InterpretObjLiteralValue(cx, atomCache, insn, &propVal);
      MOZ_ASSERT(propVal.isString() || propVal.isUndefined());
      elements.infallibleAppend(propVal);
    }
  };

  // Create cooked array.
  readElements(count);
  Rooted<ArrayObject*> cso(
      cx, NewDenseCopiedArray(cx, elements.length(), elements.begin(),
                              NewObjectKind::TenuredObject));
  if (!cso) {
    return nullptr;
  }
  elements.clear();

  // Create raw array.
  readElements(count);
  Rooted<ArrayObject*> raw(
      cx, NewDenseCopiedArray(cx, elements.length(), elements.begin(),
                              NewObjectKind::TenuredObject));
  if (!raw) {
    return nullptr;
  }

  // Define .raw property and freeze both arrays.
  RootedValue rawValue(cx, ObjectValue(*raw));
  if (!DefineDataProperty(cx, cso, cx->names().raw, rawValue, 0)) {
    return nullptr;
  }
  if (!FreezeObject(cx, raw)) {
    return nullptr;
  }
  if (!FreezeObject(cx, cso)) {
    return nullptr;
  }

  return cso;
}

template <PropertySetKind kind>
Shape* InterpretObjLiteralShape(JSContext* cx,
                                const frontend::CompilationAtomCache& atomCache,
                                const mozilla::Span<const uint8_t> literalInsns,
                                uint32_t propertyCount) {
  gc::AllocKind allocKind =
      AllocKindForObjectLiteral<kind>(literalInsns, propertyCount);
  uint32_t numFixedSlots = GetGCKindSlots(allocKind);

  ObjLiteralReader reader(literalInsns);

  Rooted<SharedPropMap*> map(cx);
  uint32_t mapLength = 0;
  ObjectFlags objectFlags;

  uint32_t slot = 0;
  RootedId propId(cx);
  while (true) {
    // Make sure `insn` doesn't live across GC.
    ObjLiteralInsn insn;
    if (!reader.readInsn(&insn)) {
      break;
    }
    MOZ_ASSERT(insn.isValid());
    MOZ_ASSERT(!insn.getKey().isArrayIndex());
    MOZ_ASSERT(insn.getOp() == ObjLiteralOpcode::Undefined);

    JSAtom* jsatom =
        atomCache.getExistingAtomAt(cx, insn.getKey().getAtomIndex());
    MOZ_ASSERT(jsatom);
    propId = AtomToId(jsatom);

    // Assert or check property names are unique.
    if constexpr (kind == PropertySetKind::UniqueNames) {
      mozilla::DebugOnly<uint32_t> index;
      MOZ_ASSERT_IF(map, !map->lookupPure(mapLength, propId, &index));
    } else {
      uint32_t index;
      if (map && map->lookupPure(mapLength, propId, &index)) {
        continue;
      }
    }

    constexpr PropertyFlags propFlags = PropertyFlags::defaultDataPropFlags;

    if (!SharedPropMap::addPropertyWithKnownSlot(cx, &PlainObject::class_, &map,
                                                 &mapLength, propId, propFlags,
                                                 slot, &objectFlags)) {
      return nullptr;
    }

    slot++;
  }

  JSObject* proto = &cx->global()->getObjectPrototype();
  return SharedShape::getInitialOrPropMapShape(
      cx, &PlainObject::class_, cx->realm(), TaggedProto(proto), numFixedSlots,
      map, mapLength, objectFlags);
}

static Shape* InterpretObjLiteralShape(
    JSContext* cx, const frontend::CompilationAtomCache& atomCache,
    const mozilla::Span<const uint8_t> literalInsns, ObjLiteralFlags flags,
    uint32_t propertyCount) {
  if (!flags.hasFlag(ObjLiteralFlag::HasIndexOrDuplicatePropName)) {
    return InterpretObjLiteralShape<PropertySetKind::UniqueNames>(
        cx, atomCache, literalInsns, propertyCount);
  }
  return InterpretObjLiteralShape<PropertySetKind::Normal>(
      cx, atomCache, literalInsns, propertyCount);
}

JS::GCCellPtr ObjLiteralStencil::create(
    JSContext* cx, const frontend::CompilationAtomCache& atomCache) const {
  switch (kind()) {
    case ObjLiteralKind::Array: {
      JSObject* obj =
          InterpretObjLiteralArray(cx, atomCache, code_, propertyCount_);
      if (!obj) {
        return JS::GCCellPtr();
      }
      return JS::GCCellPtr(obj);
    }
    case ObjLiteralKind::CallSiteObj: {
      JSObject* obj =
          InterpretObjLiteralCallSiteObj(cx, atomCache, code_, propertyCount_);
      if (!obj) {
        return JS::GCCellPtr();
      }
      return JS::GCCellPtr(obj);
    }
    case ObjLiteralKind::Object: {
      JSObject* obj =
          InterpretObjLiteralObj(cx, atomCache, code_, flags(), propertyCount_);
      if (!obj) {
        return JS::GCCellPtr();
      }
      return JS::GCCellPtr(obj);
    }
    case ObjLiteralKind::Shape: {
      Shape* shape = InterpretObjLiteralShape(cx, atomCache, code_, flags(),
                                              propertyCount_);
      if (!shape) {
        return JS::GCCellPtr();
      }
      return JS::GCCellPtr(shape);
    }
    case ObjLiteralKind::Invalid:
      break;
  }
  MOZ_CRASH("Invalid kind");
}

#ifdef DEBUG
bool ObjLiteralStencil::isContainedIn(const LifoAlloc& alloc) const {
  return alloc.contains(code_.data());
}
#endif

#if defined(DEBUG) || defined(JS_JITSPEW)

static void DumpObjLiteralFlagsItems(js::JSONPrinter& json,
                                     ObjLiteralFlags flags) {
  if (flags.hasFlag(ObjLiteralFlag::HasIndexOrDuplicatePropName)) {
    json.value("HasIndexOrDuplicatePropName");
    flags.clearFlag(ObjLiteralFlag::HasIndexOrDuplicatePropName);
  }

  if (!flags.isEmpty()) {
    json.value("Unknown(%x)", flags.toRaw());
  }
}

static const char* ObjLiteralKindToString(ObjLiteralKind kind) {
  switch (kind) {
    case ObjLiteralKind::Object:
      return "Object";
    case ObjLiteralKind::Array:
      return "Array";
    case ObjLiteralKind::CallSiteObj:
      return "CallSiteObj";
    case ObjLiteralKind::Shape:
      return "Shape";
    case ObjLiteralKind::Invalid:
      break;
  }
  MOZ_CRASH("Invalid kind");
}

static void DumpObjLiteral(js::JSONPrinter& json,
                           const frontend::CompilationStencil* stencil,
                           mozilla::Span<const uint8_t> code,
                           ObjLiteralKind kind, const ObjLiteralFlags& flags,
                           uint32_t propertyCount) {
  json.property("kind", ObjLiteralKindToString(kind));

  json.beginListProperty("flags");
  DumpObjLiteralFlagsItems(json, flags);
  json.endList();

  json.beginListProperty("code");
  ObjLiteralReader reader(code);
  ObjLiteralInsn insn;
  while (reader.readInsn(&insn)) {
    json.beginObject();

    if (insn.getKey().isNone()) {
      json.nullProperty("key");
    } else if (insn.getKey().isAtomIndex()) {
      frontend::TaggedParserAtomIndex index = insn.getKey().getAtomIndex();
      json.beginObjectProperty("key");
      DumpTaggedParserAtomIndex(json, index, stencil);
      json.endObject();
    } else if (insn.getKey().isArrayIndex()) {
      uint32_t index = insn.getKey().getArrayIndex();
      json.formatProperty("key", "ArrayIndex(%u)", index);
    }

    switch (insn.getOp()) {
      case ObjLiteralOpcode::ConstValue: {
        const Value& v = insn.getConstValue();
        json.formatProperty("op", "ConstValue(%f)", v.toNumber());
        break;
      }
      case ObjLiteralOpcode::ConstString: {
        frontend::TaggedParserAtomIndex index = insn.getAtomIndex();
        json.beginObjectProperty("op");
        DumpTaggedParserAtomIndex(json, index, stencil);
        json.endObject();
        break;
      }
      case ObjLiteralOpcode::Null:
        json.property("op", "Null");
        break;
      case ObjLiteralOpcode::Undefined:
        json.property("op", "Undefined");
        break;
      case ObjLiteralOpcode::True:
        json.property("op", "True");
        break;
      case ObjLiteralOpcode::False:
        json.property("op", "False");
        break;
      default:
        json.formatProperty("op", "Invalid(%x)", uint8_t(insn.getOp()));
        break;
    }

    json.endObject();
  }
  json.endList();

  json.property("propertyCount", propertyCount);
}

void ObjLiteralWriter::dump() const {
  js::Fprinter out(stderr);
  js::JSONPrinter json(out);
  dump(json, nullptr);
}

void ObjLiteralWriter::dump(js::JSONPrinter& json,
                            const frontend::CompilationStencil* stencil) const {
  json.beginObject();
  dumpFields(json, stencil);
  json.endObject();
}

void ObjLiteralWriter::dumpFields(
    js::JSONPrinter& json, const frontend::CompilationStencil* stencil) const {
  DumpObjLiteral(json, stencil, getCode(), kind_, flags_, propertyCount_);
}

void ObjLiteralStencil::dump() const {
  js::Fprinter out(stderr);
  js::JSONPrinter json(out);
  dump(json, nullptr);
}

void ObjLiteralStencil::dump(
    js::JSONPrinter& json, const frontend::CompilationStencil* stencil) const {
  json.beginObject();
  dumpFields(json, stencil);
  json.endObject();
}

void ObjLiteralStencil::dumpFields(
    js::JSONPrinter& json, const frontend::CompilationStencil* stencil) const {
  DumpObjLiteral(json, stencil, code_, kind(), flags(), propertyCount_);
}

#endif  // defined(DEBUG) || defined(JS_JITSPEW)

}  // namespace js
