// Copyright 2021 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/objects/swiss-name-dictionary.h'

@doNotGenerateCppClass
extern class SwissNameDictionary extends HeapObject {
  hash: uint32;
  const capacity: int32;
  meta_table: ByteArray;
  data_table[Convert<intptr>(capacity) * 2]: JSAny|TheHole;
  ctrl_table[Convert<intptr>(capacity) + swiss_table::kGroupWidth]: uint8;
  property_details_table[Convert<intptr>(capacity)]: uint8;
}

namespace swiss_table {

const kDataTableEntryCount: constexpr intptr
    generates 'SwissNameDictionary::kDataTableEntryCount';

const kMax1ByteMetaTableCapacity: constexpr int32
    generates 'SwissNameDictionary::kMax1ByteMetaTableCapacity';

const kMax2ByteMetaTableCapacity: constexpr int32
    generates 'SwissNameDictionary::kMax2ByteMetaTableCapacity';

const kNotFoundSentinel:
    constexpr int32 generates 'SwissNameDictionary::kNotFoundSentinel';

extern macro LoadSwissNameDictionaryKey(SwissNameDictionary, intptr): Name;

extern macro StoreSwissNameDictionaryKeyAndValue(
    SwissNameDictionary, intptr, Object, Object): void;

extern macro SwissNameDictionarySetCtrl(
    SwissNameDictionary, intptr, intptr, uint8): void;

extern macro StoreSwissNameDictionaryPropertyDetails(
    SwissNameDictionary, intptr, intptr, uint8): void;

extern macro
SwissNameDictionaryIncreaseElementCountOrBailout(
    ByteArray, intptr, uint32): uint32 labels Bailout;

extern macro
StoreSwissNameDictionaryEnumToEntryMapping(
    SwissNameDictionary, intptr, intptr, int32): void;

extern macro
SwissNameDictionaryUpdateCountsForDeletion(ByteArray, intptr): uint32;

namespace runtime {
extern runtime SwissTableFindEntry(NoContext, SwissNameDictionary, Name): Smi;

extern runtime SwissTableAdd(
    NoContext, SwissNameDictionary, Name, Object, Smi): SwissNameDictionary;

extern runtime ShrinkSwissNameDictionary(
    NoContext, SwissNameDictionary): SwissNameDictionary;
}

// Counterpart for SwissNameDictionary::CapacityFor in C++.
@export
macro SwissNameDictionaryCapacityFor(atLeastSpaceFor: intptr): intptr {
  if (atLeastSpaceFor <= 4) {
    if (atLeastSpaceFor == 0) {
      return 0;
    } else if (atLeastSpaceFor < kSwissNameDictionaryInitialCapacity) {
      return 4;
    } else if (FromConstexpr<bool>(kGroupWidth == 16)) {
      dcheck(atLeastSpaceFor == 4);
      return 4;
    } else if (FromConstexpr<bool>(kGroupWidth == 8)) {
      dcheck(atLeastSpaceFor == 4);
      return 8;
    }
  }

  const nonNormalized = atLeastSpaceFor + atLeastSpaceFor / 7;
  return IntPtrRoundUpToPowerOfTwo32(nonNormalized);
}

// Counterpart for SwissNameDictionary::MaxUsableCapacity in C++.
@export
macro SwissNameDictionaryMaxUsableCapacity(capacity: intptr): intptr {
  dcheck(capacity == 0 || capacity >= kSwissNameDictionaryInitialCapacity);
  if (FromConstexpr<bool>(kGroupWidth == 8) && capacity == 4) {
    // If the group size is 16 we can fully utilize capacity 4: There will be
    // enough kEmpty entries in the ctrl table.
    return 3;
  }
  return capacity - capacity / 8;
}

// Counterpart for SwissNameDictionary::SizeFor in C++.
@export
macro SwissNameDictionarySizeFor(capacity: intptr): intptr {
  const constant: constexpr int32 = kHeapObjectHeaderSize + 8 + kTaggedSize;
  const dynamic: intptr =
      capacity * FromConstexpr<intptr>(2 * kTaggedSize + 2) +
      FromConstexpr<intptr>(kGroupWidth);
  return constant + dynamic;
}

// Counterpart for SwissNameDictionary::MetaTableSizePerEntryFor in C++.
@export
macro SwissNameDictionaryMetaTableSizePerEntryFor(capacity: intptr): intptr {
  if (capacity <= kMax1ByteMetaTableCapacity) {
    return 1;
  } else if (capacity <= kMax2ByteMetaTableCapacity) {
    return 2;
  } else {
    return 4;
  }
}

// Counterpart for SwissNameDictionary::MetaTableSizeFor in C++.
@export
macro SwissNameDictionaryMetaTableSizeFor(capacity: intptr): intptr {
  const perEntry: intptr =
      SwissNameDictionaryMetaTableSizePerEntryFor(capacity);
  const maxUsable: intptr =
      Convert<intptr>(SwissNameDictionaryMaxUsableCapacity(capacity));

  return (2 + maxUsable) * perEntry;
}

//
// Offsets. MT stands for "minus tag"
//

const kDataTableStartOffsetMT: constexpr intptr
    generates 'SwissNameDictionary::DataTableStartOffset() - kHeapObjectTag';

@export
macro SwissNameDictionaryDataTableStartOffsetMT(): intptr {
  return kDataTableStartOffsetMT;
}

@export
macro SwissNameDictionaryCtrlTableStartOffsetMT(capacity: intptr): intptr {
  return kDataTableStartOffsetMT +
      kDataTableEntryCount * FromConstexpr<intptr>(kTaggedSize) * capacity;
}

macro Probe(hash: uint32, mask: uint32): ProbeSequence {
  // Mask must be a power of 2 minus 1.
  dcheck(((mask + 1) & mask) == 0);

  return ProbeSequence{mask: mask, offset: H1(hash) & mask, index: 0};
}

macro FindEntry<GroupLoader: type>(
    table: SwissNameDictionary, key: Name): never labels
Found(intptr), NotFound {
  const hash: uint32 = LoadNameHash(key);
  const capacity: int32 = table.capacity;
  const nonZeroCapacity: int32 = capacity | Convert<int32>(capacity == 0);
  const mask: uint32 = Unsigned(nonZeroCapacity - 1);

  const ctrlTableStart: intptr =
      SwissNameDictionaryCtrlTableStartOffsetMT(Convert<intptr>(capacity)) +
      BitcastTaggedToWord(table);

  let seq = Probe(hash, mask);
  while (true) {
    const group =
        GroupLoader{}.LoadGroup(ctrlTableStart + Convert<intptr>(seq.offset));
    let match = group.Match(H2(hash));
    while (match.HasBitsSet()) {
      const inGroupIndex = match.LowestBitSet();
      const candidateEntry = Convert<intptr>(seq.Offset(inGroupIndex));
      const candidateKey: Object =
          LoadSwissNameDictionaryKey(table, candidateEntry);
      if (TaggedEqual(key, candidateKey)) {
        goto Found(candidateEntry);
      }
      match.ClearLowestSetBit();
    }
    if (group.MatchEmpty().HasBitsSet()) {
      goto NotFound;
    }
    seq.Next();
  }

  unreachable;
}

macro FindFirstEmpty<GroupLoader: type>(
    table: SwissNameDictionary, capacity: intptr, hash: uint32): int32 {
  const nonZeroCapacity: int32 =
      Convert<int32>(capacity) | Convert<int32>(capacity == 0);
  const mask: uint32 = Unsigned(nonZeroCapacity - 1);

  const ctrlTableStart: intptr =
      SwissNameDictionaryCtrlTableStartOffsetMT(capacity) +
      BitcastTaggedToWord(table);

  let seq = Probe(hash, mask);
  while (true) {
    const group =
        GroupLoader{}.LoadGroup(ctrlTableStart + Convert<intptr>(seq.offset));
    const match = group.MatchEmpty();
    if (match.HasBitsSet()) {
      const inGroupIndex = match.LowestBitSet();
      return Signed(seq.Offset(inGroupIndex));
    }
    seq.Next();
  }

  unreachable;
}

macro Add<GroupLoader: type>(
    table: SwissNameDictionary, key: Name, value: Object,
    propertyDetails: uint8): void labels Bailout {
  const capacity: intptr = Convert<intptr>(table.capacity);
  const maxUsable: uint32 =
      Unsigned(Convert<int32>(SwissNameDictionaryMaxUsableCapacity(capacity)));

  try {
    // We read the used capacity (present + deleted elements), compare it
    // against the max usable capacity to determine if a bailout is necessary,
    // and in case of no bailout increase the present element count all in one
    // go using the following macro. This way we don't have to do the branching
    // needed for meta table accesses multiple times.
    const used: uint32 = SwissNameDictionaryIncreaseElementCountOrBailout(
        table.meta_table, capacity, maxUsable) otherwise Bailout;

    const hash: uint32 = LoadNameHash(key);
    const newEntry32 = FindFirstEmpty<GroupLoader>(table, capacity, hash);
    const newEntry = Convert<intptr>(newEntry32);

    StoreSwissNameDictionaryKeyAndValue(table, newEntry, key, value);

    StoreSwissNameDictionaryEnumToEntryMapping(
        table, capacity, Convert<intptr>(used), newEntry32);

    const h2 = Convert<uint8>(Convert<intptr>(H2(hash)));
    SwissNameDictionarySetCtrl(table, capacity, newEntry, h2);

    StoreSwissNameDictionaryPropertyDetails(
        table, capacity, newEntry, propertyDetails);
  } label Bailout {
    goto Bailout;
  }
}

@export
macro SwissNameDictionaryDelete(table: SwissNameDictionary, entry: intptr):
    void labels Shrunk(SwissNameDictionary) {
  const capacity = Convert<intptr>(table.capacity);

  // Update present and deleted element counts at once, without needing to do
  // the meta table access related branching more than once.
  const newElementCount =
      SwissNameDictionaryUpdateCountsForDeletion(table.meta_table, capacity);

  StoreSwissNameDictionaryKeyAndValue(table, entry, TheHole, TheHole);

  const kDeleted = FromConstexpr<uint8>(ctrl::kDeleted);
  SwissNameDictionarySetCtrl(table, capacity, entry, kDeleted);

  // Same logic for deciding when to shrink as in SwissNameDictionary::Delete.
  if (Convert<intptr>(Signed(newElementCount)) < (capacity >> 2)) {
    const shrunkTable = runtime::ShrinkSwissNameDictionary(kNoContext, table);
    goto Shrunk(shrunkTable);
  }
}

// TODO(v8:11330) Ideally, we would like to implement
// CodeStubAssembler::SwissNameDictionaryFindEntry in Torque and do the
// necessary switching between the two implementations with if(kUseSimd) {...}
// else {...}. However, Torque currently generates a call to
// CodeAssembler::Branch which cannot guarantee that code for the "bad" path is
// not generated, even if the branch can be resolved at compile time. This means
// that we end up trying to generate unused code using unsupported instructions.
@export
macro SwissNameDictionaryFindEntrySIMD(table: SwissNameDictionary, key: Name):
    never labels Found(intptr), NotFound {
  FindEntry<GroupSse2Loader>(table, key)
      otherwise Found, NotFound;
}

@export
macro SwissNameDictionaryFindEntryPortable(
    table: SwissNameDictionary, key: Name): never labels
Found(intptr),
    NotFound {
  FindEntry<GroupPortableLoader>(table, key)
      otherwise Found, NotFound;
}

// TODO(v8:11330) Ideally, we would like to implement
// CodeStubAssembler::SwissNameDictionaryAdd in Torque and do the necessary
// switching between the two implementations with if(kUseSimd) {...} else {...}.
// However, Torque currently generates a call to CodeAssembler::Branch which
// cannot guarantee that code for the "bad" path is not generated, even if the
// branch can be resolved at compile time. This means that we end up trying to
// generate unused code using unsupported instructions.
@export
macro SwissNameDictionaryAddSIMD(
    table: SwissNameDictionary, key: Name, value: Object,
    propertyDetails: uint8): void labels Bailout {
  Add<GroupSse2Loader>(table, key, value, propertyDetails)
      otherwise Bailout;
}

@export
macro SwissNameDictionaryAddPortable(
    table: SwissNameDictionary, key: Name, value: Object,
    propertyDetails: uint8): void labels Bailout {
  Add<GroupPortableLoader>(table, key, value, propertyDetails)
      otherwise Bailout;
}
}