//===----------------------------------------------------------------------===//
//
// 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 "llvm/DWARFCFIChecker/DWARFCFIAnalysis.h"
#include "Registers.h"
#include "llvm/ADT/ArrayRef.h"
#include "llvm/ADT/SmallSet.h"
#include "llvm/ADT/SmallVector.h"
#include "llvm/ADT/Twine.h"
#include "llvm/DWARFCFIChecker/DWARFCFIState.h"
#include "llvm/DebugInfo/DWARF/LowLevel/DWARFUnwindTable.h"
#include "llvm/MC/MCAsmInfo.h"
#include "llvm/MC/MCContext.h"
#include "llvm/MC/MCDwarf.h"
#include "llvm/MC/MCExpr.h"
#include "llvm/MC/MCInst.h"
#include "llvm/MC/MCInstrInfo.h"
#include "llvm/MC/MCRegister.h"
#include "llvm/MC/MCRegisterInfo.h"
#include "llvm/MC/MCStreamer.h"
#include "llvm/MC/MCSubtargetInfo.h"
#include "llvm/MC/TargetRegistry.h"
#include "llvm/Support/ErrorHandling.h"
#include "llvm/Support/FormatVariadic.h"
#include <optional>

using namespace llvm;

struct CFARegOffsetInfo {
  DWARFRegNum Reg;
  int64_t Offset;

  CFARegOffsetInfo(DWARFRegNum Reg, int64_t Offset)
      : Reg(Reg), Offset(Offset) {}

  bool operator==(const CFARegOffsetInfo &RHS) const {
    return Reg == RHS.Reg && Offset == RHS.Offset;
  }
};

static std::optional<CFARegOffsetInfo>
getCFARegOffsetInfo(const dwarf::UnwindRow &UnwindRow) {
  auto CFALocation = UnwindRow.getCFAValue();
  if (CFALocation.getLocation() !=
      dwarf::UnwindLocation::Location::RegPlusOffset)
    return std::nullopt;

  return CFARegOffsetInfo(CFALocation.getRegister(), CFALocation.getOffset());
}

static SmallSet<DWARFRegNum, 4>
getUnwindRuleRegSet(const dwarf::UnwindRow &UnwindRow, DWARFRegNum Reg) {
  auto MaybeLoc = UnwindRow.getRegisterLocations().getRegisterLocation(Reg);
  assert(MaybeLoc && "the register should be included in the unwinding row");
  auto Loc = *MaybeLoc;

  switch (Loc.getLocation()) {
  case dwarf::UnwindLocation::Location::Unspecified:
  case dwarf::UnwindLocation::Location::Undefined:
  case dwarf::UnwindLocation::Location::Constant:
  case dwarf::UnwindLocation::Location::CFAPlusOffset:
    // [CFA + offset] does not depend on any register because the CFA value is
    // constant throughout the entire frame; only the way to calculate it might
    // change.
  case dwarf::UnwindLocation::Location::DWARFExpr:
    // TODO: Expressions are not supported yet, but if they were to be
    // supported, all the registers used in an expression should extracted and
    // returned here.
    return {};
  case dwarf::UnwindLocation::Location::Same:
    return {Reg};
  case dwarf::UnwindLocation::Location::RegPlusOffset:
    return {Loc.getRegister()};
  }
  llvm_unreachable("Unknown dwarf::UnwindLocation::Location enum");
}

DWARFCFIAnalysis::DWARFCFIAnalysis(MCContext *Context, MCInstrInfo const &MCII,
                                   bool IsEH,
                                   ArrayRef<MCCFIInstruction> Prologue)
    : State(Context), Context(Context), MCII(MCII),
      MCRI(Context->getRegisterInfo()), IsEH(IsEH) {

  for (auto LLVMReg : getTrackingRegs(MCRI)) {
    if (MCRI->get(LLVMReg).IsArtificial || MCRI->get(LLVMReg).IsConstant)
      continue;

    DWARFRegNum Reg = MCRI->getDwarfRegNum(LLVMReg, IsEH);
    // TODO: this should be `undefined` instead of `same_value`, but because
    // initial frame state doesn't have any directives about callee saved
    // registers, every register is tracked. After initial frame state is
    // corrected, this should be changed.
    State.update(MCCFIInstruction::createSameValue(nullptr, Reg));
  }

  // TODO: Ignoring PC should be in the initial frame state.
  State.update(MCCFIInstruction::createUndefined(
      nullptr, MCRI->getDwarfRegNum(MCRI->getProgramCounter(), IsEH)));

  for (auto &&InitialFrameStateCFIDirective :
       Context->getAsmInfo()->getInitialFrameState())
    State.update(InitialFrameStateCFIDirective);

  auto MaybeCurrentRow = State.getCurrentUnwindRow();
  assert(MaybeCurrentRow && "there should be at least one row");
  auto MaybeCFA = getCFARegOffsetInfo(*MaybeCurrentRow);
  assert(MaybeCFA &&
         "the CFA information should be describable in [reg + offset] in here");
  auto CFA = *MaybeCFA;

  // TODO: CFA register callee value is CFA's value, this should be in initial
  // frame state.
  State.update(MCCFIInstruction::createOffset(nullptr, CFA.Reg, 0));

  // Applying the prologue after default assumptions to overwrite them.
  for (auto &&Directive : Prologue)
    State.update(Directive);
}

void DWARFCFIAnalysis::update(const MCInst &Inst,
                              ArrayRef<MCCFIInstruction> Directives) {
  const MCInstrDesc &MCInstInfo = MCII.get(Inst.getOpcode());

  auto MaybePrevRow = State.getCurrentUnwindRow();
  assert(MaybePrevRow && "the analysis should have initialized the "
                         "state with at least one row by now");
  auto PrevRow = *MaybePrevRow;

  for (auto &&Directive : Directives)
    State.update(Directive);

  SmallSet<DWARFRegNum, 4> Writes, Reads;
  for (unsigned I = 0; I < MCInstInfo.NumImplicitUses; I++)
    Reads.insert(MCRI->getDwarfRegNum(
        getSuperReg(MCRI, MCInstInfo.implicit_uses()[I]), IsEH));
  for (unsigned I = 0; I < MCInstInfo.NumImplicitDefs; I++)
    Writes.insert(MCRI->getDwarfRegNum(
        getSuperReg(MCRI, MCInstInfo.implicit_defs()[I]), IsEH));

  for (unsigned I = 0; I < Inst.getNumOperands(); I++) {
    auto &&Op = Inst.getOperand(I);
    if (Op.isReg()) {
      if (I < MCInstInfo.getNumDefs())
        Writes.insert(
            MCRI->getDwarfRegNum(getSuperReg(MCRI, Op.getReg()), IsEH));
      else if (Op.getReg())
        Reads.insert(
            MCRI->getDwarfRegNum(getSuperReg(MCRI, Op.getReg()), IsEH));
    }
  }

  auto MaybeNextRow = State.getCurrentUnwindRow();
  assert(MaybeNextRow && "previous row existed, so should the current row");
  auto NextRow = *MaybeNextRow;

  checkCFADiff(Inst, PrevRow, NextRow, Reads, Writes);

  for (auto LLVMReg : getTrackingRegs(MCRI)) {
    DWARFRegNum Reg = MCRI->getDwarfRegNum(LLVMReg, IsEH);

    checkRegDiff(Inst, Reg, PrevRow, NextRow, Reads, Writes);
  }
}

void DWARFCFIAnalysis::checkRegDiff(const MCInst &Inst, DWARFRegNum Reg,
                                    const dwarf::UnwindRow &PrevRow,
                                    const dwarf::UnwindRow &NextRow,
                                    const SmallSet<DWARFRegNum, 4> &Reads,
                                    const SmallSet<DWARFRegNum, 4> &Writes) {
  auto MaybePrevLoc = PrevRow.getRegisterLocations().getRegisterLocation(Reg);
  auto MaybeNextLoc = NextRow.getRegisterLocations().getRegisterLocation(Reg);

  // All the tracked registers are added during initiation. So if a register is
  // not added, should stay the same during execution and vice versa.
  if (!MaybePrevLoc) {
    assert(!MaybeNextLoc && "the register unwind info suddenly appeared here");
    return;
  }
  assert(MaybeNextLoc && "the register unwind info suddenly vanished here");

  auto PrevLoc = MaybePrevLoc.value();
  auto NextLoc = MaybeNextLoc.value();

  auto MaybeLLVMReg = MCRI->getLLVMRegNum(Reg, IsEH);
  if (!MaybeLLVMReg) {
    if (!(PrevLoc == NextLoc))
      Context->reportWarning(
          Inst.getLoc(),
          formatv("the dwarf register {0} does not have a LLVM number, but its "
                  "unwind info changed. Ignoring this change",
                  Reg));
    return;
  }
  const char *RegName = MCRI->getName(*MaybeLLVMReg);

  // Each case is annotated with its corresponding number as described in
  // `llvm/include/llvm/DWARFCFIChecker/DWARFCFIAnalysis.h`.

  // TODO: Expressions are not supported yet, but if they were to be supported,
  // note that structure equality for expressions is defined as follows: Two
  // expressions are structurally equal if they become the same after you
  // replace every operand with a placeholder.

  if (PrevLoc == NextLoc) { // Case 1
    for (DWARFRegNum UsedReg : getUnwindRuleRegSet(PrevRow, Reg))
      if (Writes.count(UsedReg)) { // Case 1.b
        auto MaybeLLVMUsedReg = MCRI->getLLVMRegNum(UsedReg, IsEH);
        assert(MaybeLLVMUsedReg && "instructions will always write to a "
                                   "register that has an LLVM register number");
        Context->reportError(
            Inst.getLoc(),
            formatv("changed register {1}, that register {0}'s unwinding rule "
                    "uses, but there is no CFI directives about it",
                    RegName, MCRI->getName(*MaybeLLVMUsedReg)));
        return;
      }
    return; // Case 1.a
  }
  // Case 2
  if (PrevLoc.getLocation() != NextLoc.getLocation()) { // Case 2.a
    Context->reportWarning(
        Inst.getLoc(),
        formatv("validating changes happening to register {0} unwinding "
                "rule structure is not implemented yet",
                RegName));
    return;
  }
  auto &&PrevRegSet = getUnwindRuleRegSet(PrevRow, Reg);
  if (PrevRegSet != getUnwindRuleRegSet(NextRow, Reg)) { // Case 2.b
    Context->reportWarning(
        Inst.getLoc(),
        formatv("validating changes happening to register {0} unwinding "
                "rule register set is not implemented yet",
                RegName));
    return;
  }
  // Case 2.c
  for (DWARFRegNum UsedReg : PrevRegSet)
    if (Writes.count(UsedReg)) { // Case 2.c.i
      Context->reportWarning(
          Inst.getLoc(),
          formatv("register {0} unwinding rule's offset is changed, and one of "
                  "the rule's registers is modified, but validating the "
                  "modification amount is not implemented yet",
                  RegName));
      return;
    }
  // Case 2.c.ii
  Context->reportError(
      Inst.getLoc(), formatv("register {0} unwinding rule's offset is changed, "
                             "but not any of the rule's registers are modified",
                             RegName));
}

void DWARFCFIAnalysis::checkCFADiff(const MCInst &Inst,
                                    const dwarf::UnwindRow &PrevRow,
                                    const dwarf::UnwindRow &NextRow,
                                    const SmallSet<DWARFRegNum, 4> &Reads,
                                    const SmallSet<DWARFRegNum, 4> &Writes) {

  auto MaybePrevCFA = getCFARegOffsetInfo(PrevRow);
  auto MaybeNextCFA = getCFARegOffsetInfo(NextRow);

  if (!MaybePrevCFA) {
    if (MaybeNextCFA) {
      Context->reportWarning(Inst.getLoc(),
                             "CFA rule changed to [reg + offset], this "
                             "transition will not be checked");
      return;
    }

    Context->reportWarning(Inst.getLoc(),
                           "CFA rule is not [reg + offset], not checking it");
    return;
  }

  if (!MaybeNextCFA) {
    Context->reportWarning(Inst.getLoc(),
                           "CFA rule changed from [reg + offset], this "
                           "transition will not be checked");
    return;
  }

  auto PrevCFA = *MaybePrevCFA;
  auto NextCFA = *MaybeNextCFA;

  auto MaybeLLVMPrevReg = MCRI->getLLVMRegNum(PrevCFA.Reg, IsEH);
  const char *PrevCFARegName =
      MaybeLLVMPrevReg ? MCRI->getName(*MaybeLLVMPrevReg) : "";
  auto MaybeLLVMNextReg = MCRI->getLLVMRegNum(NextCFA.Reg, IsEH);
  const char *NextCFARegName =
      MaybeLLVMNextReg ? MCRI->getName(*MaybeLLVMNextReg) : "";

  if (PrevCFA == NextCFA) {         // Case 1
    if (!Writes.count(PrevCFA.Reg)) // Case 1.a
      return;
    // Case 1.b
    Context->reportError(
        Inst.getLoc(),
        formatv("modified CFA register {0} but not changed CFA rule",
                PrevCFARegName));
    return;
  }

  if (PrevCFA.Reg != NextCFA.Reg) { // Case 2.b
    Context->reportWarning(
        Inst.getLoc(),
        formatv("CFA register changed from register {0} to register {1}, "
                "validating this change is not implemented yet",
                PrevCFARegName, NextCFARegName));
    return;
  }
  // Case 2.c
  if (Writes.count(PrevCFA.Reg)) { // Case 2.c.i
    Context->reportWarning(
        Inst.getLoc(), formatv("CFA offset is changed from {0} to {1}, and CFA "
                               "register {2} is modified, but validating the "
                               "modification amount is not implemented yet",
                               PrevCFA.Offset, NextCFA.Offset, PrevCFARegName));
    return;
  }
  // Case 2.c.ii
  Context->reportError(
      Inst.getLoc(),
      formatv("did not modify CFA register {0} but changed CFA rule",
              PrevCFARegName));
}
