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

extern enum IterationKind extends uint31 { kKeys, kValues, kEntries }

extern class JSArrayIterator extends JSObject {
  iterated_object: JSReceiver;

  // [next_index]: The [[ArrayIteratorNextIndex]] inobject property.
  // The next_index is always a positive integer, and it points to
  // the next index that is to be returned by this iterator. It's
  // possible range is fixed depending on the [[iterated_object]]:
  //
  //   1. For JSArray's the next_index is always in Unsigned32
  //      range, and when the iterator reaches the end it's set
  //      to kMaxUInt32 to indicate that this iterator should
  //      never produce values anymore even if the "length"
  //      property of the JSArray changes at some later point.
  //   2. For JSTypedArray's the next_index is always in
  //      UnsignedSmall range, and when the iterator terminates
  //      it's set to Smi::kMaxValue.
  //   3. For all other JSReceiver's it's always between 0 and
  //      kMaxSafeInteger, and the latter value is used to mark
  //      termination.
  //
  // It's important that for 1. and 2. the value fits into the
  // Unsigned32 range (UnsignedSmall is a subset of Unsigned32),
  // since we use this knowledge in the fast-path for the array
  // iterator next calls in TurboFan (in the JSCallReducer) to
  // keep the index in Word32 representation. This invariant is
  // checked in JSArrayIterator::JSArrayIteratorVerify().
  next_index: Number;

  kind: SmiTagged<IterationKind>;
}

// Perform CreateArrayIterator (ES #sec-createarrayiterator).
@export
macro CreateArrayIterator(implicit context: NativeContext)(
    array: JSReceiver, kind: constexpr IterationKind): JSArrayIterator {
  return new JSArrayIterator{
    map: *NativeContextSlot(ContextSlot::INITIAL_ARRAY_ITERATOR_MAP_INDEX),
    properties_or_hash: kEmptyFixedArray,
    elements: kEmptyFixedArray,
    iterated_object: array,
    next_index: 0,
    kind: SmiTag<IterationKind>(kind)
  };
}

extern class JSArray extends JSObject {
  macro IsEmpty(): bool {
    return this.length == 0;
  }
  length: Number;
}

@doNotGenerateCast
extern class JSArrayConstructor extends JSFunction
    generates 'TNode<JSFunction>';

macro NewJSArray(implicit context: Context)(
    map: Map, elements: FixedArrayBase): JSArray {
  return new JSArray{
    map,
    properties_or_hash: kEmptyFixedArray,
    elements,
    length: elements.length
  };
}

macro NewJSArray(implicit context: Context)(): JSArray {
  return new JSArray{
    map: GetFastPackedSmiElementsJSArrayMap(),
    properties_or_hash: kEmptyFixedArray,
    elements: kEmptyFixedArray,
    length: 0
  };
}

// A HeapObject with a JSArray map, and either fast packed elements, or fast
// holey elements when the global NoElementsProtector is not invalidated.
transient type FastJSArray extends JSArray;

// A HeapObject with a JSArray map, and either fast packed elements, or fast
// holey elements or frozen, sealed elements when the global NoElementsProtector
// is not invalidated.
transient type FastJSArrayForRead extends JSArray;

// A FastJSArray when the global ArraySpeciesProtector is not invalidated.
transient type FastJSArrayForCopy extends FastJSArray;

// A FastJSArrayForCopy when the global IsConcatSpreadableProtector is not
// invalidated.
transient type FastJSArrayForConcat extends FastJSArrayForCopy;

// A FastJSArray when the global ArrayIteratorProtector is not invalidated.
transient type FastJSArrayWithNoCustomIteration extends FastJSArray;

// A FastJSArrayForRead when the global ArrayIteratorProtector is not
// invalidated.
transient type FastJSArrayForReadWithNoCustomIteration extends
    FastJSArrayForRead;

extern macro AllocateJSArray(
    constexpr ElementsKind, Map, intptr, Smi,
    constexpr AllocationFlag): JSArray;
extern macro AllocateJSArray(constexpr ElementsKind, Map, intptr, Smi): JSArray;
extern macro AllocateJSArray(constexpr ElementsKind, Map, Smi, Smi): JSArray;
extern macro AllocateJSArray(Map, FixedArrayBase, Smi): JSArray;

macro LoadElementNoHole<T : type extends FixedArrayBase>(
    a: JSArray, index: Smi): JSAny
    labels IfHole;

LoadElementNoHole<FixedArray>(implicit context: Context)(
    a: JSArray, index: Smi): JSAny
    labels IfHole {
  const elements: FixedArray =
      Cast<FixedArray>(a.elements) otherwise unreachable;
  const e = UnsafeCast<(JSAny | TheHole)>(elements.objects[index]);
  typeswitch (e) {
    case (TheHole): {
      goto IfHole;
    }
    case (e: JSAny): {
      return e;
    }
  }
}

LoadElementNoHole<FixedDoubleArray>(implicit context: Context)(
    a: JSArray, index: Smi): JSAny
    labels IfHole {
  const elements: FixedDoubleArray =
      Cast<FixedDoubleArray>(a.elements) otherwise unreachable;
  const e: float64 = elements.floats[index].Value() otherwise IfHole;
  return AllocateHeapNumberWithValue(e);
}

extern builtin ExtractFastJSArray(Context, JSArray, Smi, Smi): JSArray;

extern macro MoveElements(
    constexpr ElementsKind, FixedArrayBase, intptr, intptr, intptr): void;
macro TorqueMoveElementsSmi(
    elements: FixedArray, dstIndex: intptr, srcIndex: intptr,
    count: intptr): void {
  MoveElements(
      ElementsKind::HOLEY_SMI_ELEMENTS, elements, dstIndex, srcIndex, count);
}
macro TorqueMoveElements(
    elements: FixedArray, dstIndex: intptr, srcIndex: intptr,
    count: intptr): void {
  MoveElements(
      ElementsKind::HOLEY_ELEMENTS, elements, dstIndex, srcIndex, count);
}
macro TorqueMoveElements(
    elements: FixedDoubleArray, dstIndex: intptr, srcIndex: intptr,
    count: intptr): void {
  MoveElements(
      ElementsKind::HOLEY_DOUBLE_ELEMENTS, elements, dstIndex, srcIndex, count);
}

extern macro CopyElements(
    constexpr ElementsKind, FixedArrayBase, intptr, FixedArrayBase, intptr,
    intptr): void;
macro TorqueCopyElements(
    dstElements: FixedArray, dstIndex: intptr, srcElements: FixedArray,
    srcIndex: intptr, count: intptr): void {
  CopyElements(
      ElementsKind::HOLEY_ELEMENTS, dstElements, dstIndex, srcElements,
      srcIndex, count);
}
macro TorqueCopyElements(
    dstElements: FixedDoubleArray, dstIndex: intptr,
    srcElements: FixedDoubleArray, srcIndex: intptr, count: intptr): void {
  CopyElements(
      ElementsKind::HOLEY_DOUBLE_ELEMENTS, dstElements, dstIndex, srcElements,
      srcIndex, count);
}

extern builtin CloneFastJSArray(Context, FastJSArrayForCopy): JSArray;

struct FastJSArrayWitness {
  macro Get(): FastJSArray {
    return this.unstable;
  }

  macro Recheck(): void labels CastError {
    if (this.stable.map != this.map) goto CastError;
    // We don't need to check elements kind or whether the prototype
    // has changed away from the default JSArray prototype, because
    // if the map remains the same then those properties hold.
    //
    // However, we have to make sure there are no elements in the
    // prototype chain.
    if (IsNoElementsProtectorCellInvalid()) goto CastError;
    this.unstable = %RawDownCast<FastJSArray>(this.stable);
  }

  macro LoadElementNoHole(implicit context: Context)(k: Smi): JSAny
      labels FoundHole {
    if (this.hasDoubles) {
      return LoadElementNoHole<FixedDoubleArray>(this.unstable, k)
          otherwise FoundHole;
    } else {
      return LoadElementNoHole<FixedArray>(this.unstable, k)
          otherwise FoundHole;
    }
  }

  macro StoreHole(k: Smi): void {
    if (this.hasDoubles) {
      const elements = Cast<FixedDoubleArray>(this.unstable.elements)
          otherwise unreachable;
      elements.floats[k] = kDoubleHole;
    } else {
      const elements = Cast<FixedArray>(this.unstable.elements)
          otherwise unreachable;
      elements.objects[k] = TheHole;
    }
  }

  macro LoadElementOrUndefined(implicit context: Context)(k: Smi): JSAny {
    try {
      return this.LoadElementNoHole(k) otherwise FoundHole;
    } label FoundHole {
      return Undefined;
    }
  }

  macro EnsureArrayPushable(implicit context: Context)(): void labels Failed {
    EnsureArrayPushable(this.map) otherwise Failed;
    array::EnsureWriteableFastElements(this.unstable);
    this.arrayIsPushable = true;
  }

  macro ChangeLength(newLength: Smi): void {
    dcheck(this.arrayIsPushable);
    this.unstable.length = newLength;
  }

  macro Push(value: JSAny): void labels Failed {
    dcheck(this.arrayIsPushable);
    if (this.hasDoubles) {
      BuildAppendJSArray(
          ElementsKind::HOLEY_DOUBLE_ELEMENTS, this.unstable, value)
          otherwise Failed;
    } else if (this.hasSmis) {
      BuildAppendJSArray(ElementsKind::HOLEY_SMI_ELEMENTS, this.unstable, value)
          otherwise Failed;
    } else {
      dcheck(
          this.map.elements_kind == ElementsKind::HOLEY_ELEMENTS ||
          this.map.elements_kind == ElementsKind::PACKED_ELEMENTS);
      BuildAppendJSArray(ElementsKind::HOLEY_ELEMENTS, this.unstable, value)
          otherwise Failed;
    }
  }

  macro MoveElements(dst: intptr, src: intptr, length: intptr): void {
    dcheck(this.arrayIsPushable);
    if (this.hasDoubles) {
      const elements: FixedDoubleArray =
          Cast<FixedDoubleArray>(this.unstable.elements)
          otherwise unreachable;
      TorqueMoveElements(elements, dst, src, length);
    } else {
      const elements: FixedArray = Cast<FixedArray>(this.unstable.elements)
          otherwise unreachable;
      if (this.hasSmis) {
        TorqueMoveElementsSmi(elements, dst, src, length);
      } else {
        TorqueMoveElements(elements, dst, src, length);
      }
    }
  }

  const stable: JSArray;
  unstable: FastJSArray;
  const map: Map;
  const hasDoubles: bool;
  const hasSmis: bool;
  arrayIsPushable: bool;
}

macro NewFastJSArrayWitness(array: FastJSArray): FastJSArrayWitness {
  const kind = array.map.elements_kind;
  return FastJSArrayWitness{
    stable: array,
    unstable: array,
    map: array.map,
    hasDoubles: IsDoubleElementsKind(kind),
    hasSmis:
        IsElementsKindLessThanOrEqual(kind, ElementsKind::HOLEY_SMI_ELEMENTS),
    arrayIsPushable: false
  };
}

struct FastJSArrayForReadWitness {
  macro Get(): FastJSArrayForRead {
    return this.unstable;
  }

  macro Recheck(): void labels CastError {
    if (this.stable.map != this.map) goto CastError;
    // We don't need to check elements kind or whether the prototype
    // has changed away from the default JSArray prototype, because
    // if the map remains the same then those properties hold.
    //
    // However, we have to make sure there are no elements in the
    // prototype chain.
    if (IsNoElementsProtectorCellInvalid()) goto CastError;
    this.unstable = %RawDownCast<FastJSArrayForRead>(this.stable);
  }

  macro LoadElementNoHole(implicit context: Context)(k: Smi): JSAny
      labels FoundHole {
    if (this.hasDoubles) {
      return LoadElementNoHole<FixedDoubleArray>(this.unstable, k)
          otherwise FoundHole;
    } else {
      return LoadElementNoHole<FixedArray>(this.unstable, k)
          otherwise FoundHole;
    }
  }

  const stable: JSArray;
  unstable: FastJSArrayForRead;
  const map: Map;
  const hasDoubles: bool;
}

macro NewFastJSArrayForReadWitness(array: FastJSArrayForRead):
    FastJSArrayForReadWitness {
  const kind = array.map.elements_kind;
  return FastJSArrayForReadWitness{
    stable: array,
    unstable: array,
    map: array.map,
    hasDoubles: IsDoubleElementsKind(kind)
  };
}