//===--- SIMDIntrinsicsCheck.cpp - clang-tidy------------------------------===//
//
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
// See https://llvm.org/LICENSE.txt for license information.
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
//
//===----------------------------------------------------------------------===//

#include "SIMDIntrinsicsCheck.h"
#include "clang/AST/ASTContext.h"
#include "clang/ASTMatchers/ASTMatchFinder.h"
#include "llvm/ADT/StringMap.h"
#include "llvm/ADT/Triple.h"
#include "llvm/Support/Regex.h"

using namespace clang::ast_matchers;

namespace clang {
namespace tidy {
namespace portability {

namespace {

// If the callee has parameter of VectorType or pointer to VectorType,
// or the return type is VectorType, we consider it a vector function
// and a candidate for checking.
AST_MATCHER(FunctionDecl, isVectorFunction) {
  bool IsVector = Node.getReturnType()->isVectorType();
  for (const ParmVarDecl *Parm : Node.parameters()) {
    QualType Type = Parm->getType();
    if (Type->isPointerType())
      Type = Type->getPointeeType();
    if (Type->isVectorType())
      IsVector = true;
  }
  return IsVector;
}

} // namespace

static StringRef TrySuggestPPC(StringRef Name) {
  if (!Name.consume_front("vec_"))
    return {};

  static const llvm::StringMap<StringRef> Mapping{
    // [simd.alg]
    {"max", "$std::max"},
    {"min", "$std::min"},

    // [simd.binary]
    {"add", "operator+ on $simd objects"},
    {"sub", "operator- on $simd objects"},
    {"mul", "operator* on $simd objects"},
  };

  auto It = Mapping.find(Name);
  if (It != Mapping.end())
    return It->second;
  return {};
}

static StringRef TrySuggestX86(StringRef Name) {
  if (!(Name.consume_front("_mm_") || Name.consume_front("_mm256_") ||
        Name.consume_front("_mm512_")))
    return {};

  // [simd.alg]
  if (Name.startswith("max_"))
    return "$simd::max";
  if (Name.startswith("min_"))
    return "$simd::min";

  // [simd.binary]
  if (Name.startswith("add_"))
    return "operator+ on $simd objects";
  if (Name.startswith("sub_"))
    return "operator- on $simd objects";
  if (Name.startswith("mul_"))
    return "operator* on $simd objects";

  return {};
}

SIMDIntrinsicsCheck::SIMDIntrinsicsCheck(StringRef Name,
                                         ClangTidyContext *Context)
    : ClangTidyCheck(Name, Context), Std(Options.get("Std", "")),
      Suggest(Options.get("Suggest", 0) != 0) {}

void SIMDIntrinsicsCheck::storeOptions(ClangTidyOptions::OptionMap &Opts) {
  Options.store(Opts, "Std", "");
  Options.store(Opts, "Suggest", 0);
}

void SIMDIntrinsicsCheck::registerMatchers(MatchFinder *Finder) {
  if (!getLangOpts().CPlusPlus11)
    return;
  // If Std is not specified, infer it from the language options.
  // libcxx implementation backports it to C++11 std::experimental::simd.
  if (Std.empty())
    Std = getLangOpts().CPlusPlus2a ? "std" : "std::experimental";

  Finder->addMatcher(callExpr(callee(functionDecl(
                                  matchesName("^::(_mm_|_mm256_|_mm512_|vec_)"),
                                  isVectorFunction())),
                              unless(isExpansionInSystemHeader()))
                         .bind("call"),
                     this);
}

void SIMDIntrinsicsCheck::check(const MatchFinder::MatchResult &Result) {
  const auto *Call = Result.Nodes.getNodeAs<CallExpr>("call");
  assert(Call != nullptr);
  const FunctionDecl *Callee = Call->getDirectCallee();
  if (!Callee)
    return;

  StringRef Old = Callee->getName();
  StringRef New;
  llvm::Triple::ArchType Arch =
      Result.Context->getTargetInfo().getTriple().getArch();

  // We warn or suggest if this SIMD intrinsic function has a std::simd
  // replacement.
  switch (Arch) {
  default:
    break;
  case llvm::Triple::ppc:
  case llvm::Triple::ppc64:
  case llvm::Triple::ppc64le:
    New = TrySuggestPPC(Old);
    break;
  case llvm::Triple::x86:
  case llvm::Triple::x86_64:
    New = TrySuggestX86(Old);
    break;
  }

  // We have found a std::simd replacement.
  if (!New.empty()) {
    std::string Message;
    // If Suggest is true, give a P0214 alternative, otherwise point it out it
    // is non-portable.
    if (Suggest) {
      Message = (Twine("'") + Old + "' can be replaced by " + New).str();
      Message = llvm::Regex("\\$std").sub(Std, Message);
      Message =
          llvm::Regex("\\$simd").sub((Std.str() + "::simd").str(), Message);
    } else {
      Message = (Twine("'") + Old + "' is a non-portable " +
                 llvm::Triple::getArchTypeName(Arch) + " intrinsic function")
                    .str();
    }
    diag(Call->getExprLoc(), Message);
  }
}

} // namespace portability
} // namespace tidy
} // namespace clang
