//===--- HLSL.cpp - HLSL ToolChain Implementations --------------*- C++ -*-===//
//
// 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 "HLSL.h"
#include "clang/Driver/CommonArgs.h"
#include "clang/Driver/Compilation.h"
#include "clang/Driver/Job.h"
#include "llvm/ADT/StringSwitch.h"
#include "llvm/TargetParser/Triple.h"
#include <regex>

using namespace clang::driver;
using namespace clang::driver::tools;
using namespace clang::driver::toolchains;
using namespace clang;
using namespace llvm::opt;
using namespace llvm;

namespace {

const unsigned OfflineLibMinor = 0xF;

bool isLegalShaderModel(Triple &T) {
  if (T.getOS() != Triple::OSType::ShaderModel)
    return false;

  auto Version = T.getOSVersion();
  if (Version.getBuild())
    return false;
  if (Version.getSubminor())
    return false;

  auto Kind = T.getEnvironment();

  switch (Kind) {
  default:
    return false;
  case Triple::EnvironmentType::Vertex:
  case Triple::EnvironmentType::Hull:
  case Triple::EnvironmentType::Domain:
  case Triple::EnvironmentType::Geometry:
  case Triple::EnvironmentType::Pixel:
  case Triple::EnvironmentType::Compute: {
    VersionTuple MinVer(4, 0);
    return MinVer <= Version;
  } break;
  case Triple::EnvironmentType::Library: {
    VersionTuple SM6x(6, OfflineLibMinor);
    if (Version == SM6x)
      return true;

    VersionTuple MinVer(6, 3);
    return MinVer <= Version;
  } break;
  case Triple::EnvironmentType::Amplification:
  case Triple::EnvironmentType::Mesh: {
    VersionTuple MinVer(6, 5);
    return MinVer <= Version;
  } break;
  case Triple::EnvironmentType::RootSignature:
    VersionTuple MinVer(1, 0);
    VersionTuple MaxVer(1, 2);
    return MinVer <= Version && Version <= MaxVer;
  }
  return false;
}

std::optional<llvm::Triple> tryParseTriple(StringRef Profile) {
  // [ps|vs|gs|hs|ds|cs|ms|as]_[major]_[minor]
  SmallVector<StringRef, 3> Parts;
  Profile.split(Parts, "_");
  if (Parts.size() != 3)
    return std::nullopt;

  Triple::EnvironmentType Kind =
      StringSwitch<Triple::EnvironmentType>(Parts[0])
          .Case("ps", Triple::EnvironmentType::Pixel)
          .Case("vs", Triple::EnvironmentType::Vertex)
          .Case("gs", Triple::EnvironmentType::Geometry)
          .Case("hs", Triple::EnvironmentType::Hull)
          .Case("ds", Triple::EnvironmentType::Domain)
          .Case("cs", Triple::EnvironmentType::Compute)
          .Case("lib", Triple::EnvironmentType::Library)
          .Case("ms", Triple::EnvironmentType::Mesh)
          .Case("as", Triple::EnvironmentType::Amplification)
          .Case("rootsig", Triple::EnvironmentType::RootSignature)
          .Default(Triple::EnvironmentType::UnknownEnvironment);
  if (Kind == Triple::EnvironmentType::UnknownEnvironment)
    return std::nullopt;

  unsigned long long Major = 0;
  if (llvm::getAsUnsignedInteger(Parts[1], 0, Major))
    return std::nullopt;

  unsigned long long Minor = 0;
  if (Parts[2] == "x" && Kind == Triple::EnvironmentType::Library)
    Minor = OfflineLibMinor;
  else if (llvm::getAsUnsignedInteger(Parts[2], 0, Minor))
    return std::nullopt;

  // Determine DXIL version using the minor version number of Shader
  // Model version specified in target profile. Prior to decoupling DXIL version
  // numbering from that of Shader Model DXIL version 1.Y corresponds to SM 6.Y.
  // E.g., dxilv1.Y-unknown-shadermodelX.Y-hull
  llvm::Triple T;
  Triple::SubArchType SubArch = llvm::Triple::NoSubArch;
  switch (Minor) {
  case 0:
    SubArch = llvm::Triple::DXILSubArch_v1_0;
    break;
  case 1:
    SubArch = llvm::Triple::DXILSubArch_v1_1;
    break;
  case 2:
    SubArch = llvm::Triple::DXILSubArch_v1_2;
    break;
  case 3:
    SubArch = llvm::Triple::DXILSubArch_v1_3;
    break;
  case 4:
    SubArch = llvm::Triple::DXILSubArch_v1_4;
    break;
  case 5:
    SubArch = llvm::Triple::DXILSubArch_v1_5;
    break;
  case 6:
    SubArch = llvm::Triple::DXILSubArch_v1_6;
    break;
  case 7:
    SubArch = llvm::Triple::DXILSubArch_v1_7;
    break;
  case 8:
    SubArch = llvm::Triple::DXILSubArch_v1_8;
    break;
  case 9:
    SubArch = llvm::Triple::DXILSubArch_v1_9;
    break;
  case OfflineLibMinor:
    // Always consider minor version x as the latest supported DXIL version
    SubArch = llvm::Triple::LatestDXILSubArch;
    break;
  default:
    // No DXIL Version corresponding to specified Shader Model version found
    return std::nullopt;
  }
  T.setArch(Triple::ArchType::dxil, SubArch);
  T.setOSName(Triple::getOSTypeName(Triple::OSType::ShaderModel).str() +
              VersionTuple(Major, Minor).getAsString());
  T.setEnvironment(Kind);

  return T;
}

std::optional<std::string> tryParseProfile(StringRef Profile) {
  std::optional<llvm::Triple> MaybeT = tryParseTriple(Profile);
  if (MaybeT && isLegalShaderModel(*MaybeT))
    return MaybeT->getTriple();
  else
    return std::nullopt;
}

bool isLegalValidatorVersion(StringRef ValVersionStr, const Driver &D) {
  VersionTuple Version;
  if (Version.tryParse(ValVersionStr) || Version.getBuild() ||
      Version.getSubminor() || !Version.getMinor()) {
    D.Diag(diag::err_drv_invalid_format_dxil_validator_version)
        << ValVersionStr;
    return false;
  }

  uint64_t Major = Version.getMajor();
  uint64_t Minor = *Version.getMinor();
  if (Major == 0 && Minor != 0) {
    D.Diag(diag::err_drv_invalid_empty_dxil_validator_version) << ValVersionStr;
    return false;
  }
  VersionTuple MinVer(1, 0);
  if (Version < MinVer) {
    D.Diag(diag::err_drv_invalid_range_dxil_validator_version) << ValVersionStr;
    return false;
  }
  return true;
}

void getSpirvExtOperand(StringRef SpvExtensionArg, raw_ostream &out) {
  // The extensions that are commented out are supported in DXC, but the SPIR-V
  // backend does not know about them yet.
  static const std::vector<StringRef> DxcSupportedExtensions = {
      "SPV_KHR_16bit_storage",
      "SPV_KHR_device_group",
      "SPV_KHR_fragment_shading_rate",
      "SPV_KHR_multiview",
      "SPV_KHR_post_depth_coverage",
      "SPV_KHR_non_semantic_info",
      "SPV_KHR_shader_draw_parameters",
      "SPV_KHR_ray_tracing",
      "SPV_KHR_shader_clock",
      "SPV_EXT_demote_to_helper_invocation",
      "SPV_EXT_descriptor_indexing",
      "SPV_EXT_fragment_fully_covered",
      "SPV_EXT_fragment_invocation_density",
      "SPV_EXT_fragment_shader_interlock",
      "SPV_EXT_mesh_shader",
      "SPV_EXT_shader_stencil_export",
      "SPV_EXT_shader_viewport_index_layer",
      // "SPV_AMD_shader_early_and_late_fragment_tests",
      "SPV_GOOGLE_hlsl_functionality1",
      "SPV_GOOGLE_user_type",
      "SPV_KHR_ray_query",
      "SPV_EXT_shader_image_int64",
      "SPV_KHR_fragment_shader_barycentric",
      "SPV_KHR_physical_storage_buffer",
      "SPV_KHR_vulkan_memory_model",
      // "SPV_KHR_compute_shader_derivatives",
      "SPV_KHR_maximal_reconvergence",
      "SPV_KHR_float_controls",
      "SPV_NV_shader_subgroup_partitioned",
      // "SPV_KHR_quad_control"
  };

  if (SpvExtensionArg.starts_with("SPV_")) {
    out << "+" << SpvExtensionArg;
    return;
  }

  if (SpvExtensionArg.compare_insensitive("DXC") == 0) {
    bool first = true;
    for (StringRef E : DxcSupportedExtensions) {
      if (!first)
        out << ",";
      else
        first = false;
      out << "+" << E;
    }
    return;
  }
  out << SpvExtensionArg;
}

SmallString<1024> getSpirvExtArg(ArrayRef<std::string> SpvExtensionArgs) {
  if (SpvExtensionArgs.empty()) {
    return StringRef("-spirv-ext=all");
  }

  llvm::SmallString<1024> LlvmOption;
  raw_svector_ostream out(LlvmOption);

  out << "-spirv-ext=";
  getSpirvExtOperand(SpvExtensionArgs[0], out);

  SpvExtensionArgs = SpvExtensionArgs.slice(1);
  for (StringRef Extension : SpvExtensionArgs) {
    out << ",";
    getSpirvExtOperand(Extension, out);
  }
  return LlvmOption;
}

bool isValidSPIRVExtensionName(const std::string &str) {
  std::regex pattern("dxc|DXC|khr|KHR|SPV_[a-zA-Z0-9_]+");
  return std::regex_match(str, pattern);
}

// SPIRV extension names are of the form `SPV_[a-zA-Z0-9_]+`. We want to
// disallow obviously invalid names to avoid issues when parsing `spirv-ext`.
bool checkExtensionArgsAreValid(ArrayRef<std::string> SpvExtensionArgs,
                                const Driver &Driver) {
  bool AllValid = true;
  for (auto Extension : SpvExtensionArgs) {
    if (!isValidSPIRVExtensionName(Extension)) {
      Driver.Diag(diag::err_drv_invalid_value)
          << "-fspv-extension" << Extension;
      AllValid = false;
    }
  }
  return AllValid;
}

bool isRootSignatureTarget(StringRef Profile) {
  if (std::optional<llvm::Triple> T = tryParseTriple(Profile))
    return T->getEnvironment() == Triple::EnvironmentType::RootSignature;
  return false;
}

bool isRootSignatureTarget(DerivedArgList &Args) {
  if (const Arg *A = Args.getLastArg(options::OPT_target_profile))
    return isRootSignatureTarget(A->getValue());
  return false;
}

} // namespace

void tools::hlsl::Validator::ConstructJob(Compilation &C, const JobAction &JA,
                                          const InputInfo &Output,
                                          const InputInfoList &Inputs,
                                          const ArgList &Args,
                                          const char *LinkingOutput) const {
  std::string DxvPath = getToolChain().GetProgramPath("dxv");
  assert(DxvPath != "dxv" && "cannot find dxv");

  ArgStringList CmdArgs;
  assert(Inputs.size() == 1 && "Unable to handle multiple inputs.");
  const InputInfo &Input = Inputs[0];
  CmdArgs.push_back(Input.getFilename());
  CmdArgs.push_back("-o");
  CmdArgs.push_back(Output.getFilename());

  const char *Exec = Args.MakeArgString(DxvPath);
  C.addCommand(std::make_unique<Command>(JA, *this, ResponseFileSupport::None(),
                                         Exec, CmdArgs, Inputs, Input));
}

void tools::hlsl::MetalConverter::ConstructJob(
    Compilation &C, const JobAction &JA, const InputInfo &Output,
    const InputInfoList &Inputs, const ArgList &Args,
    const char *LinkingOutput) const {
  std::string MSCPath = getToolChain().GetProgramPath("metal-shaderconverter");
  ArgStringList CmdArgs;
  assert(Inputs.size() == 1 && "Unable to handle multiple inputs.");
  const InputInfo &Input = Inputs[0];
  CmdArgs.push_back(Input.getFilename());
  CmdArgs.push_back("-o");
  CmdArgs.push_back(Output.getFilename());

  const char *Exec = Args.MakeArgString(MSCPath);
  C.addCommand(std::make_unique<Command>(JA, *this, ResponseFileSupport::None(),
                                         Exec, CmdArgs, Inputs, Input));
}

void tools::hlsl::LLVMObjcopy::ConstructJob(Compilation &C, const JobAction &JA,
                                            const InputInfo &Output,
                                            const InputInfoList &Inputs,
                                            const ArgList &Args,
                                            const char *LinkingOutput) const {

  std::string ObjcopyPath = getToolChain().GetProgramPath("llvm-objcopy");
  const char *Exec = Args.MakeArgString(ObjcopyPath);

  ArgStringList CmdArgs;
  assert(Inputs.size() == 1 && "Unable to handle multiple inputs.");
  const InputInfo &Input = Inputs[0];
  CmdArgs.push_back(Input.getFilename());
  CmdArgs.push_back(Output.getFilename());

  if (Args.hasArg(options::OPT_dxc_strip_rootsignature)) {
    const char *StripRS = Args.MakeArgString("--remove-section=RTS0");
    CmdArgs.push_back(StripRS);
  }

  if (Arg *Arg = Args.getLastArg(options::OPT_dxc_Frs)) {
    const char *Frs =
        Args.MakeArgString("--extract-section=RTS0=" + Twine(Arg->getValue()));
    CmdArgs.push_back(Frs);
  }

  if (const Arg *A = Args.getLastArg(options::OPT_target_profile))
    if (isRootSignatureTarget(A->getValue())) {
      const char *Fos = Args.MakeArgString("--only-section=RTS0");
      CmdArgs.push_back(Fos);
    }

  assert(CmdArgs.size() > 2 && "Unnecessary invocation of objcopy.");

  C.addCommand(std::make_unique<Command>(JA, *this, ResponseFileSupport::None(),
                                         Exec, CmdArgs, Inputs, Input));
}

/// DirectX Toolchain
HLSLToolChain::HLSLToolChain(const Driver &D, const llvm::Triple &Triple,
                             const ArgList &Args)
    : ToolChain(D, Triple, Args) {
  if (Args.hasArg(options::OPT_dxc_validator_path_EQ))
    getProgramPaths().push_back(
        Args.getLastArgValue(options::OPT_dxc_validator_path_EQ).str());
}

Tool *clang::driver::toolchains::HLSLToolChain::getTool(
    Action::ActionClass AC) const {
  switch (AC) {
  case Action::BinaryAnalyzeJobClass:
    if (!Validator)
      Validator.reset(new tools::hlsl::Validator(*this));
    return Validator.get();
  case Action::BinaryTranslatorJobClass:
    if (!MetalConverter)
      MetalConverter.reset(new tools::hlsl::MetalConverter(*this));
    return MetalConverter.get();
  case Action::ObjcopyJobClass:
    if (!LLVMObjcopy)
      LLVMObjcopy.reset(new tools::hlsl::LLVMObjcopy(*this));
    return LLVMObjcopy.get();
  default:
    return ToolChain::getTool(AC);
  }
}

std::optional<std::string>
clang::driver::toolchains::HLSLToolChain::parseTargetProfile(
    StringRef TargetProfile) {
  return tryParseProfile(TargetProfile);
}

DerivedArgList *
HLSLToolChain::TranslateArgs(const DerivedArgList &Args, StringRef BoundArch,
                             Action::OffloadKind DeviceOffloadKind) const {
  DerivedArgList *DAL = new DerivedArgList(Args.getBaseArgs());

  const OptTable &Opts = getDriver().getOpts();

  if (Args.hasArg(options::OPT_dxc_col_major) &&
      Args.hasArg(options::OPT_dxc_row_major))
    getDriver().Diag(diag::err_drv_dxc_invalid_matrix_layout);

  for (Arg *A : Args) {
    if (A->getOption().getID() == options::OPT_dxc_all_resources_bound) {
      DAL->AddFlagArg(nullptr,
                      Opts.getOption(options::OPT_hlsl_all_resources_bound));
      A->claim();
      continue;
    }
    if (A->getOption().getID() == options::OPT_dxil_validator_version) {
      StringRef ValVerStr = A->getValue();
      if (!isLegalValidatorVersion(ValVerStr, getDriver()))
        continue;
    }
    if (A->getOption().getID() == options::OPT_dxc_entrypoint) {
      DAL->AddSeparateArg(nullptr, Opts.getOption(options::OPT_hlsl_entrypoint),
                          A->getValue());
      A->claim();
      continue;
    }
    if (A->getOption().getID() == options::OPT_dxc_rootsig_ver) {
      DAL->AddJoinedArg(nullptr,
                        Opts.getOption(options::OPT_fdx_rootsignature_version),
                        A->getValue());
      A->claim();
      continue;
    }
    if (A->getOption().getID() == options::OPT_dxc_rootsig_define) {
      DAL->AddJoinedArg(nullptr,
                        Opts.getOption(options::OPT_fdx_rootsignature_define),
                        A->getValue());
      A->claim();
      continue;
    }
    if (A->getOption().getID() == options::OPT__SLASH_O) {
      StringRef OStr = A->getValue();
      if (OStr == "d") {
        DAL->AddFlagArg(nullptr, Opts.getOption(options::OPT_O0));
        A->claim();
        continue;
      } else {
        DAL->AddJoinedArg(nullptr, Opts.getOption(options::OPT_O), OStr);
        A->claim();
        continue;
      }
    }
    if (A->getOption().getID() == options::OPT_emit_pristine_llvm) {
      // Translate -fcgl into -emit-llvm and -disable-llvm-passes.
      DAL->AddFlagArg(nullptr, Opts.getOption(options::OPT_emit_llvm));
      DAL->AddFlagArg(nullptr,
                      Opts.getOption(options::OPT_disable_llvm_passes));
      A->claim();
      continue;
    }
    if (A->getOption().getID() == options::OPT_dxc_hlsl_version) {
      // Translate -HV into -std for llvm
      // depending on the value given
      LangStandard::Kind LangStd = LangStandard::getHLSLLangKind(A->getValue());
      if (LangStd != LangStandard::lang_unspecified) {
        LangStandard l = LangStandard::getLangStandardForKind(LangStd);
        DAL->AddSeparateArg(nullptr, Opts.getOption(options::OPT_std_EQ),
                            l.getName());
      } else {
        getDriver().Diag(diag::err_drv_invalid_value) << "HV" << A->getValue();
      }

      A->claim();
      continue;
    }
    if (A->getOption().getID() == options::OPT_dxc_gis) {
      // Translate -Gis into -ffp_model_EQ=strict
      DAL->AddSeparateArg(nullptr, Opts.getOption(options::OPT_ffp_model_EQ),
                          "strict");
      A->claim();
      continue;
    }
    if (A->getOption().getID() == options::OPT_fvk_use_dx_layout) {
      // This is the only implemented layout so far.
      A->claim();
      continue;
    }

    if (A->getOption().getID() == options::OPT_fvk_use_scalar_layout) {
      getDriver().Diag(diag::err_drv_clang_unsupported) << A->getAsString(Args);
      A->claim();
      continue;
    }

    if (A->getOption().getID() == options::OPT_fvk_use_gl_layout) {
      getDriver().Diag(diag::err_drv_clang_unsupported) << A->getAsString(Args);
      A->claim();
      continue;
    }

    if (A->getOption().getID() == options::OPT_enable_16bit_types) {
      // Translate -enable-16bit-types into -fnative-half-type and
      // -fnative-int16-type
      DAL->AddFlagArg(nullptr, Opts.getOption(options::OPT_fnative_half_type));
      DAL->AddFlagArg(nullptr, Opts.getOption(options::OPT_fnative_int16_type));
      A->claim();
      continue;
    }
    if (A->getOption().getID() == options::OPT_dxc_col_major) {
      DAL->AddJoinedArg(nullptr,
                        Opts.getOption(options::OPT_fmatrix_memory_layout_EQ),
                        "column-major");
      A->claim();
      continue;
    }
    if (A->getOption().getID() == options::OPT_dxc_row_major) {
      DAL->AddJoinedArg(nullptr,
                        Opts.getOption(options::OPT_fmatrix_memory_layout_EQ),
                        "row-major");
      A->claim();
      continue;
    }

    DAL->append(A);
  }

  if (getArch() == llvm::Triple::spirv) {
    std::vector<std::string> SpvExtensionArgs =
        Args.getAllArgValues(options::OPT_fspv_extension_EQ);
    if (checkExtensionArgsAreValid(SpvExtensionArgs, getDriver())) {
      SmallString<1024> LlvmOption = getSpirvExtArg(SpvExtensionArgs);
      DAL->AddSeparateArg(nullptr, Opts.getOption(options::OPT_mllvm),
                          LlvmOption);
    }
    Args.claimAllArgs(options::OPT_fspv_extension_EQ);
  }

  if (!DAL->hasArg(options::OPT_O_Group)) {
    DAL->AddJoinedArg(nullptr, Opts.getOption(options::OPT_O), "3");
  }

  return DAL;
}

bool HLSLToolChain::requiresValidation(DerivedArgList &Args) const {
  if (!Args.hasArg(options::OPT_dxc_Fo))
    return false;

  if (Args.getLastArg(options::OPT_dxc_disable_validation))
    return false;

  std::string DxvPath = GetProgramPath("dxv");
  if (DxvPath != "dxv")
    return true;

  getDriver().Diag(diag::warn_drv_dxc_missing_dxv);
  return false;
}

bool HLSLToolChain::requiresBinaryTranslation(DerivedArgList &Args) const {
  return Args.hasArg(options::OPT_metal) && Args.hasArg(options::OPT_dxc_Fo);
}

bool HLSLToolChain::requiresObjcopy(DerivedArgList &Args) const {
  return Args.hasArg(options::OPT_dxc_Fo) &&
         (Args.hasArg(options::OPT_dxc_strip_rootsignature) ||
          Args.hasArg(options::OPT_dxc_Frs) || isRootSignatureTarget(Args));
}

bool HLSLToolChain::isLastJob(DerivedArgList &Args,
                              Action::ActionClass AC) const {
  // Note: we check in the reverse order of execution
  if (requiresBinaryTranslation(Args))
    return AC == Action::Action::BinaryTranslatorJobClass;
  if (requiresValidation(Args))
    return AC == Action::Action::BinaryAnalyzeJobClass;
  if (requiresObjcopy(Args))
    return AC == Action::Action::ObjcopyJobClass;

  // No translation, validation, or objcopy are required, so this action must
  // output to the result file.
  return true;
}

void HLSLToolChain::addClangWarningOptions(ArgStringList &CC1Args) const {
  CC1Args.push_back("-Wconversion");
}
