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

#include 'src/builtins/builtins-regexp-gen.h'

namespace regexp {

extern macro RegExpBuiltinsAssembler::BranchIfFastRegExpForMatch(
    implicit context: Context)(HeapObject): never labels IsFast,
    IsSlow;
macro IsFastRegExpForMatch(implicit context: Context)(o: HeapObject): bool {
  BranchIfFastRegExpForMatch(o) otherwise return true, return false;
}

extern macro RegExpBuiltinsAssembler::BranchIfFastRegExpForSearch(
    implicit context: Context)(HeapObject): never labels IsFast,
    IsSlow;
macro IsFastRegExpForSearch(implicit context: Context)(o: HeapObject): bool {
  BranchIfFastRegExpForSearch(o) otherwise return true, return false;
}

extern macro RegExpBuiltinsAssembler::BranchIfFastRegExp_Strict(
    implicit context: Context)(HeapObject): never labels IsFast,
    IsSlow;
macro IsFastRegExpStrict(implicit context: Context)(o: HeapObject): bool {
  BranchIfFastRegExp_Strict(o) otherwise return true, return false;
}

extern macro RegExpBuiltinsAssembler::BranchIfFastRegExp_Permissive(
    implicit context: Context)(HeapObject): never labels IsFast,
    IsSlow;

@export
macro IsFastRegExpPermissive(implicit context: Context)(o: HeapObject): bool {
  BranchIfFastRegExp_Permissive(o) otherwise return true, return false;
}

// ES#sec-regexpexec Runtime Semantics: RegExpExec ( R, S )
@export
transitioning macro RegExpExec(implicit context: Context)(
    receiver: JSReceiver, string: String): JSAny {
  // Take the slow path of fetching the exec property, calling it, and
  // verifying its return value.

  const exec = GetProperty(receiver, 'exec');

  // Is {exec} callable?
  typeswitch (exec) {
    case (execCallable: Callable): {
      const result = Call(context, execCallable, receiver, string);
      if (result != Null) {
        ThrowIfNotJSReceiver(
            result, MessageTemplate::kInvalidRegExpExecResult, '');
      }
      return result;
    }
    case (Object): {
      const regexp = Cast<JSRegExp>(receiver) otherwise ThrowTypeError(
          MessageTemplate::kIncompatibleMethodReceiver, 'RegExp.prototype.exec',
          receiver);
      return RegExpPrototypeExecSlow(regexp, string);
    }
  }
}

extern macro RegExpBuiltinsAssembler::ConstructNewResultFromMatchInfo(
    implicit context: Context)(JSRegExp, RegExpMatchInfo, String, Number):
    JSRegExpResult|JSRegExpResultWithIndices;

const kGlobalOrSticky: constexpr int31
    generates 'JSRegExp::kGlobal | JSRegExp::kSticky';

extern macro RegExpBuiltinsAssembler::RegExpExecInternal(
    implicit context: Context)(
    JSRegExp, String, Number, RegExpMatchInfo): HeapObject;

// ES#sec-regexp.prototype.exec
// RegExp.prototype.exec ( string )
// Implements the core of RegExp.prototype.exec but without actually
// constructing the JSRegExpResult. Returns a fixed array containing match
// indices as returned by RegExpExecStub on successful match, and jumps to
// IfDidNotMatch otherwise.
transitioning macro RegExpPrototypeExecBodyWithoutResult(
    implicit context: Context)(
    regexp: JSRegExp, string: String, regexpLastIndex: Number,
    isFastPath: constexpr bool): RegExpMatchInfo labels IfDidNotMatch {
  if (isFastPath) {
    dcheck(HasInitialRegExpMap(regexp));
  } else {
    IncrementUseCounter(context, SmiConstant(kRegExpExecCalledOnSlowRegExp));
  }

  let lastIndex = regexpLastIndex;

  // Check whether the regexp is global or sticky, which determines whether we
  // update last index later on.
  const flags = UnsafeCast<Smi>(regexp.flags);
  const isGlobalOrSticky: intptr =
      SmiUntag(flags) & IntPtrConstant(kGlobalOrSticky);
  const shouldUpdateLastIndex: bool = isGlobalOrSticky != 0;

  // Grab and possibly update last index.
  if (shouldUpdateLastIndex) {
    if (!TaggedIsSmi(lastIndex) || (lastIndex > string.length_smi)) {
      StoreLastIndex(regexp, SmiConstant(0), isFastPath);
      goto IfDidNotMatch;
    }
  } else {
    lastIndex = SmiConstant(0);
  }

  const lastMatchInfo: RegExpMatchInfo = GetRegExpLastMatchInfo();

  const matchIndices =
      RegExpExecInternal(regexp, string, lastIndex, lastMatchInfo);

  // {match_indices} is either null or the RegExpMatchInfo array.
  // Return early if exec failed, possibly updating last index.
  if (matchIndices != Null) {
    const matchIndicesRegExpMatchInfo =
        UnsafeCast<RegExpMatchInfo>(matchIndices);
    if (shouldUpdateLastIndex) {
      // Update the new last index from {match_indices}.
      const newLastIndex: Smi = matchIndicesRegExpMatchInfo.GetEndOfCapture(0);
      StoreLastIndex(regexp, newLastIndex, isFastPath);
    }
    return matchIndicesRegExpMatchInfo;
  }
  if (shouldUpdateLastIndex) {
    StoreLastIndex(regexp, SmiConstant(0), isFastPath);
  }
  goto IfDidNotMatch;
}

@export
transitioning macro RegExpPrototypeExecBodyWithoutResultFast(
    implicit context: Context)(regexp: JSRegExp, string: String):
    RegExpMatchInfo labels IfDidNotMatch {
  const lastIndex = LoadLastIndexAsLength(regexp, true);
  return RegExpPrototypeExecBodyWithoutResult(regexp, string, lastIndex, true)
      otherwise IfDidNotMatch;
}

transitioning macro RegExpPrototypeExecBodyWithoutResultFast(
    implicit context: Context)(
    regexp: JSRegExp, string: String,
    lastIndex: Number): RegExpMatchInfo labels IfDidNotMatch {
  return RegExpPrototypeExecBodyWithoutResult(regexp, string, lastIndex, true)
      otherwise IfDidNotMatch;
}

// ES#sec-regexp.prototype.exec
// RegExp.prototype.exec ( string )
transitioning macro RegExpPrototypeExecBody(implicit context: Context)(
    receiver: JSReceiver, string: String, isFastPath: constexpr bool): JSAny {
  let regexp: JSRegExp;
  if constexpr (isFastPath) {
    regexp = UnsafeCast<JSRegExp>(receiver);
  } else {
    regexp = Cast<JSRegExp>(receiver) otherwise ThrowTypeError(
        MessageTemplate::kIncompatibleMethodReceiver, 'RegExp.prototype.exec',
        receiver);
  }
  const lastIndex = LoadLastIndexAsLength(regexp, isFastPath);
  const matchIndices: RegExpMatchInfo = RegExpPrototypeExecBodyWithoutResult(
      regexp, string, lastIndex, isFastPath) otherwise return Null;
  return ConstructNewResultFromMatchInfo(
      regexp, matchIndices, string, lastIndex);
}

macro LoadRegExpFunction(nativeContext: NativeContext): JSFunction {
  return *NativeContextSlot(nativeContext, ContextSlot::REGEXP_FUNCTION_INDEX);
}

// Note this doesn't guarantee const-ness of object properties, just
// unchanged object layout.
macro HasInitialRegExpMap(implicit context: Context)(o: HeapObject): bool {
  const nativeContext = LoadNativeContext(context);
  const function = LoadRegExpFunction(nativeContext);
  const initialMap = UnsafeCast<Map>(function.prototype_or_initial_map);
  return initialMap == o.map;
}

macro IsReceiverInitialRegExpPrototype(implicit context: Context)(
    receiver: Object): bool {
  const nativeContext = LoadNativeContext(context);
  const regexpFun = LoadRegExpFunction(nativeContext);
  const initialMap = UnsafeCast<Map>(regexpFun.prototype_or_initial_map);
  const initialPrototype: HeapObject = initialMap.prototype;
  return TaggedEqual(receiver, initialPrototype);
}

extern enum Flag constexpr 'JSRegExp::Flag' {
  kNone,
  kGlobal,
  kIgnoreCase,
  kMultiline,
  kSticky,
  kUnicode,
  kDotAll,
  kHasIndices,
  kLinear
}

const kNoCounterFlagGetter: constexpr int31 = -1;
const kRegExpPrototypeStickyGetter: constexpr int31
    generates 'v8::Isolate::kRegExpPrototypeStickyGetter';
const kRegExpPrototypeUnicodeGetter: constexpr int31
    generates 'v8::Isolate::kRegExpPrototypeUnicodeGetter';

extern macro RegExpBuiltinsAssembler::FastFlagGetter(
    JSRegExp, constexpr Flag): bool;
extern runtime IncrementUseCounter(Context, Smi): void;

macro FlagGetter(implicit context: Context)(
    receiver: Object, flag: constexpr Flag, counter: constexpr int31,
    methodName: constexpr string): JSAny {
  typeswitch (receiver) {
    case (receiver: JSRegExp): {
      return SelectBooleanConstant(FastFlagGetter(receiver, flag));
    }
    case (Object): {
    }
  }
  if (!IsReceiverInitialRegExpPrototype(receiver)) {
    ThrowTypeError(MessageTemplate::kRegExpNonRegExp, methodName);
  }
  if constexpr (counter != -1) {
    IncrementUseCounter(context, SmiConstant(counter));
  }
  return Undefined;
}

// ES6 21.2.5.4.
// ES #sec-get-regexp.prototype.global
transitioning javascript builtin RegExpPrototypeGlobalGetter(
    js-implicit context: NativeContext, receiver: JSAny)(): JSAny {
  return FlagGetter(
      receiver, Flag::kGlobal, kNoCounterFlagGetter, 'RegExp.prototype.global');
}

// ES6 21.2.5.5.
// ES #sec-get-regexp.prototype.ignorecase
transitioning javascript builtin RegExpPrototypeIgnoreCaseGetter(
    js-implicit context: NativeContext, receiver: JSAny)(): JSAny {
  return FlagGetter(
      receiver, Flag::kIgnoreCase, kNoCounterFlagGetter,
      'RegExp.prototype.ignoreCase');
}

// ES6 21.2.5.7.
// ES #sec-get-regexp.prototype.multiline
transitioning javascript builtin RegExpPrototypeMultilineGetter(
    js-implicit context: NativeContext, receiver: JSAny)(): JSAny {
  return FlagGetter(
      receiver, Flag::kMultiline, kNoCounterFlagGetter,
      'RegExp.prototype.multiline');
}

transitioning javascript builtin RegExpPrototypeHasIndicesGetter(
    js-implicit context: NativeContext, receiver: JSAny)(): JSAny {
  return FlagGetter(
      receiver, Flag::kHasIndices, kNoCounterFlagGetter,
      'RegExp.prototype.hasIndices');
}

transitioning javascript builtin RegExpPrototypeLinearGetter(
    js-implicit context: NativeContext, receiver: JSAny)(): JSAny {
  return FlagGetter(
      receiver, Flag::kLinear, kNoCounterFlagGetter, 'RegExp.prototype.linear');
}

// ES #sec-get-regexp.prototype.dotAll
transitioning javascript builtin RegExpPrototypeDotAllGetter(
    js-implicit context: NativeContext, receiver: JSAny)(): JSAny {
  return FlagGetter(
      receiver, Flag::kDotAll, kNoCounterFlagGetter, 'RegExp.prototype.dotAll');
}

// ES6 21.2.5.12.
// ES #sec-get-regexp.prototype.sticky
transitioning javascript builtin RegExpPrototypeStickyGetter(
    js-implicit context: NativeContext, receiver: JSAny)(): JSAny {
  return FlagGetter(
      receiver, Flag::kSticky, kRegExpPrototypeStickyGetter,
      'RegExp.prototype.sticky');
}

// ES6 21.2.5.15.
// ES #sec-get-regexp.prototype.unicode
transitioning javascript builtin RegExpPrototypeUnicodeGetter(
    js-implicit context: NativeContext, receiver: JSAny)(): JSAny {
  return FlagGetter(
      receiver, Flag::kUnicode, kRegExpPrototypeUnicodeGetter,
      'RegExp.prototype.unicode');
}

extern transitioning macro
RegExpBuiltinsAssembler::FlagsGetter(implicit context: Context)(
    Object, constexpr bool): String;

transitioning macro
FastFlagsGetter(implicit context: Context)(receiver: FastJSRegExp): String {
  return FlagsGetter(receiver, true);
}

transitioning macro SlowFlagsGetter(implicit context: Context)(receiver: JSAny):
    String {
  return FlagsGetter(receiver, false);
}

// ES #sec-get-regexp.prototype.flags
// TFJ(RegExpPrototypeFlagsGetter, 0, kReceiver) \
transitioning javascript builtin RegExpPrototypeFlagsGetter(
    js-implicit context: NativeContext, receiver: JSAny)(): String {
  ThrowIfNotJSReceiver(
      receiver, MessageTemplate::kRegExpNonObject, 'RegExp.prototype.flags');

  // The check is strict because the following code relies on individual flag
  // getters on the regexp prototype (e.g.: global, sticky, ...). We don't
  // bother to check these individually.
  const fastRegexp = Cast<FastJSRegExp>(receiver)
      otherwise return SlowFlagsGetter(receiver);
  return FastFlagsGetter(fastRegexp);
}

extern transitioning macro RegExpBuiltinsAssembler::SlowLoadLastIndex(
    implicit context: Context)(JSAny): JSAny;
extern transitioning macro RegExpBuiltinsAssembler::SlowStoreLastIndex(
    implicit context: Context)(JSAny, JSAny): void;

extern macro RegExpBuiltinsAssembler::FastLoadLastIndex(JSRegExp): Smi;
extern macro RegExpBuiltinsAssembler::FastStoreLastIndex(JSRegExp, Smi): void;

@export
transitioning macro LoadLastIndex(implicit context: Context)(
    regexp: JSAny, isFastPath: constexpr bool): JSAny {
  return isFastPath ? FastLoadLastIndex(UnsafeCast<JSRegExp>(regexp)) :
                      SlowLoadLastIndex(regexp);
}

@export
transitioning macro LoadLastIndexAsLength(implicit context: Context)(
    regexp: JSRegExp, isFastPath: constexpr bool): Number {
  const lastIndex = LoadLastIndex(regexp, isFastPath);
  if (isFastPath) {
    // ToLength on a positive smi is a nop and can be skipped.
    return UnsafeCast<PositiveSmi>(lastIndex);
  } else {
    // Omit ToLength if last_index is a non-negative smi.
    typeswitch (lastIndex) {
      case (i: PositiveSmi): {
        return i;
      }
      case (o: JSAny): {
        return ToLength_Inline(o);
      }
    }
  }
}

@export
transitioning macro StoreLastIndex(implicit context: Context)(
    regexp: JSAny, value: Number, isFastPath: constexpr bool): void {
  if (isFastPath) {
    FastStoreLastIndex(UnsafeCast<JSRegExp>(regexp), UnsafeCast<Smi>(value));
  } else {
    SlowStoreLastIndex(regexp, value);
  }
}

extern macro RegExpBuiltinsAssembler::AdvanceStringIndex(
    String, Number, bool, constexpr bool): Number;
extern macro
RegExpBuiltinsAssembler::AdvanceStringIndexFast(String, Smi, bool): Smi;
extern macro
RegExpBuiltinsAssembler::AdvanceStringIndexSlow(String, Number, bool): Smi;

type UseCounterFeature extends int31
constexpr 'v8::Isolate::UseCounterFeature';
const kRegExpMatchIsTrueishOnNonJSRegExp: constexpr UseCounterFeature
    generates 'v8::Isolate::kRegExpMatchIsTrueishOnNonJSRegExp';
const kRegExpMatchIsFalseishOnJSRegExp: constexpr UseCounterFeature
    generates 'v8::Isolate::kRegExpMatchIsFalseishOnJSRegExp';
const kRegExpExecCalledOnSlowRegExp: constexpr UseCounterFeature
    generates 'v8::Isolate::kRegExpExecCalledOnSlowRegExp';

// ES#sec-isregexp IsRegExp ( argument )
@export
transitioning macro IsRegExp(implicit context: Context)(obj: JSAny): bool {
  const receiver = Cast<JSReceiver>(obj) otherwise return false;

  // Check @match.
  const value = GetProperty(receiver, MatchSymbolConstant());
  if (value == Undefined) {
    return Is<JSRegExp>(receiver);
  }

  dcheck(value != Undefined);
  // The common path. Symbol.match exists, equals the RegExpPrototypeMatch
  // function (and is thus trueish), and the receiver is a JSRegExp.
  if (ToBoolean(value)) {
    if (!Is<JSRegExp>(receiver)) {
      IncrementUseCounter(
          context, SmiConstant(kRegExpMatchIsTrueishOnNonJSRegExp));
    }
    return true;
  }

  dcheck(!ToBoolean(value));
  if (Is<JSRegExp>(receiver)) {
    IncrementUseCounter(context, SmiConstant(kRegExpMatchIsFalseishOnJSRegExp));
  }
  return false;
}

extern runtime RegExpInitializeAndCompile(
    Context, JSRegExp, String, String): JSAny;

@export
transitioning macro RegExpCreate(implicit context: Context)(
    nativeContext: NativeContext, maybeString: JSAny, flags: String): JSAny {
  const regexpFun = LoadRegExpFunction(nativeContext);
  const initialMap = UnsafeCast<Map>(regexpFun.prototype_or_initial_map);
  return RegExpCreate(initialMap, maybeString, flags);
}

@export
transitioning macro RegExpCreate(implicit context: Context)(
    initialMap: Map, maybeString: JSAny, flags: String): JSAny {
  const pattern: String =
      maybeString == Undefined ? kEmptyString : ToString_Inline(maybeString);
  const regexp =
      UnsafeCast<JSRegExp>(AllocateFastOrSlowJSObjectFromMap(initialMap));
  return RegExpInitializeAndCompile(context, regexp, pattern, flags);
}
}