//===-- W65816SpillToX.cpp - Replace stack spills with TAX/TXA -----------===//
//
// 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
//
//===----------------------------------------------------------------------===//
//
// Post-RA peephole: replace stack-spill/reload pairs with TAX/TXA (or
// TAY/TYA) when the index register is dead during the spill window.
//
// Fast regalloc spills A to stack via STAfi/LDAfi, costing ~12 cycles
// per round-trip (sta is 5 cycles + lda is 5 cycles + the displacement
// dispatch).  But the W65816 has TAX (2 cycles) + TXA (2 cycles), a
// 3x speedup if X is free during the spill window.
//
// We scan each basic block for the pattern:
//
//     STAfi $a, slot, 0
//     ...   (instructions that don't touch X or A's slot, don't kill A)
//     LDAfi $a, slot, 0
//
// If no instruction in the gap reads or writes X (or P-flags-dependent
// X side effects, etc.), we rewrite the pair as:
//
//     TAX
//     ...
//     TXA
//
// This saves 4 bytes (stack-rel addressing is 2 bytes per op vs TAX/TXA
// at 1 byte each) AND saves the memory traffic.  Net: ~8 cycles per
// converted pair.
//
// Conservative liveness: we treat X as "in use" if ANY instruction in
// the gap references W65816::X (def or use).  False positives mean
// we keep the slow stack form; false negatives are correctness bugs.
//
//===----------------------------------------------------------------------===//

#include "W65816.h"
#include "W65816InstrInfo.h"
#include "W65816Subtarget.h"
#include "llvm/ADT/SmallSet.h"
#include "llvm/CodeGen/MachineFrameInfo.h"
#include "llvm/CodeGen/MachineFunction.h"
#include "llvm/CodeGen/MachineFunctionPass.h"
#include "llvm/CodeGen/MachineInstr.h"
#include "llvm/CodeGen/MachineInstrBuilder.h"
#include "llvm/CodeGen/TargetRegisterInfo.h"

using namespace llvm;

#define DEBUG_TYPE "w65816-spill-to-x"

namespace {

class W65816SpillToX : public MachineFunctionPass {
public:
  static char ID;
  W65816SpillToX() : MachineFunctionPass(ID) {}
  StringRef getPassName() const override {
    return "W65816 spill-to-X peephole";
  }

  void getAnalysisUsage(AnalysisUsage &AU) const override {
    AU.setPreservesCFG();
    MachineFunctionPass::getAnalysisUsage(AU);
  }

  bool runOnMachineFunction(MachineFunction &MF) override;
};

} // namespace

char W65816SpillToX::ID = 0;

INITIALIZE_PASS(W65816SpillToX, DEBUG_TYPE, "W65816 spill-to-X peephole",
                false, false)

FunctionPass *llvm::createW65816SpillToX() {
  return new W65816SpillToX();
}

// Classifies how an MI interacts with X.
enum XEffect { XNone = 0, XReads = 1, XDefs = 2, XBoth = 3 };

// Most W65816 transfer/index opcodes (TAX, INX, LDX, STX, CPX, etc.)
// are tablegen'd as `InstImplied` with no Defs/Uses metadata, so the
// MCInstrDesc carries no implicit X operand and a generic operand
// scan misses them.  We hard-code the X-effect per opcode instead.
// Calls clobber X under our caller-saved-X ABI.
static XEffect xEffect(const MachineInstr &MI, const TargetRegisterInfo *TRI) {
  switch (MI.getOpcode()) {
  case W65816::TAX:                        // X := A
  case W65816::TYX:                        // X := Y
  case W65816::TSX:                        // X := SP
  case W65816::PLX:                        // X := pop
    return XDefs;
  case W65816::TXA:                        // A := X
  case W65816::TXY:                        // Y := X
  case W65816::TXS:                        // SP := X
  case W65816::PHX:                        // push X
    return XReads;
  case W65816::INX:                        // X := X+1
  case W65816::DEX:                        // X := X-1
    return XBoth;
  default:
    break;
  }
  if (MI.isCall()) return XBoth;           // caller-clobbered X
  // Generic operand scan for opcodes that carry X explicitly (LDX/STX/CPX
  // pseudos) or any properly-modelled implicit defs/uses.
  int eff = XNone;
  for (const auto &MO : MI.operands()) {
    if (!MO.isReg()) continue;
    Register R = MO.getReg();
    if (!R.isPhysical()) continue;
    bool isX = R == W65816::X || (TRI && TRI->regsOverlap(R, W65816::X));
    if (!isX) continue;
    if (MO.isDef()) eff |= XDefs; else eff |= XReads;
  }
  return (XEffect)eff;
}

// Convenience wrapper: returns true if MI references X in any way.
static bool touchesX(const MachineInstr &MI, const TargetRegisterInfo *TRI) {
  return xEffect(MI, TRI) != XNone;
}

// Returns FI if MI is `STAfi $a, slot, 0`, else -1.
static int matchSTAfi(const MachineInstr &MI) {
  if (MI.getOpcode() != W65816::STAfi) return -1;
  if (MI.getNumOperands() < 3) return -1;
  if (!MI.getOperand(0).isReg() || MI.getOperand(0).getReg() != W65816::A)
    return -1;
  if (!MI.getOperand(1).isFI()) return -1;
  if (!MI.getOperand(2).isImm() || MI.getOperand(2).getImm() != 0) return -1;
  return MI.getOperand(1).getIndex();
}

// Returns FI if MI is `LDAfi slot, 0` defining $a, else -1.
static int matchLDAfi(const MachineInstr &MI) {
  if (MI.getOpcode() != W65816::LDAfi) return -1;
  if (MI.getNumOperands() < 3) return -1;
  if (!MI.getOperand(0).isReg() || MI.getOperand(0).getReg() != W65816::A)
    return -1;
  if (!MI.getOperand(1).isFI()) return -1;
  if (!MI.getOperand(2).isImm() || MI.getOperand(2).getImm() != 0) return -1;
  return MI.getOperand(1).getIndex();
}

// Returns true if MI reads or writes the slot at FrameIndex FI.
static bool referencesSlot(const MachineInstr &MI, int FI) {
  for (const auto &MO : MI.operands()) {
    if (MO.isFI() && MO.getIndex() == FI) return true;
  }
  return false;
}

bool W65816SpillToX::runOnMachineFunction(MachineFunction &MF) {
  const W65816Subtarget &STI = MF.getSubtarget<W65816Subtarget>();
  const W65816InstrInfo *TII = STI.getInstrInfo();
  const TargetRegisterInfo *TRI = STI.getRegisterInfo();
  bool Changed = false;
  // Slots whose last reference we erased — candidates for reclamation.
  SmallSet<int, 8> SlotsTouched;

  for (auto &MBB : MF) {
    // Pass 1: collect (STAfi, slot) entries.
    SmallVector<std::pair<MachineInstr *, int>, 8> Stas;
    for (auto &MI : MBB) {
      int FI = matchSTAfi(MI);
      if (FI != -1) Stas.push_back({&MI, FI});
    }

    // For each STAfi, scan forward for the matching LDAfi with no
    // intervening X touch or slot reference.  Process in REVERSE
    // order so any nested pair is converted first; the outer pair's
    // gap scan then sees the inner TAX/TXA (which touches X) and
    // bails — preventing a mid-bridge X clobber.
    for (auto It = Stas.rbegin(); It != Stas.rend(); ++It) {
      auto [StaMI, FI] = *It;
      bool xTouched = false;
      bool gapEmpty = true;
      MachineInstr *LdaMI = nullptr;
      for (auto Scan = std::next(MachineBasicBlock::iterator(StaMI));
           Scan != MBB.end(); ++Scan) {
        MachineInstr &MI2 = *Scan;
        if (MI2.isDebugInstr()) continue;

        // Look for the matching LDAfi.  TAX preserves A so we don't
        // need to check A liveness — only whether X was free.
        if (matchLDAfi(MI2) == FI) { LdaMI = &MI2; break; }

        // Bail if X is touched (use or def, including implicit on
        // calls) or if the slot is referenced by something else
        // (which would invalidate the saved value).
        if (touchesX(MI2, TRI)) { xTouched = true; break; }
        if (referencesSlot(MI2, FI)) break;
        gapEmpty = false;
      }

      // Defer empty-gap pairs to StackSlotCleanup, which deletes both
      // (A still holds the stored value across an empty gap).  That
      // beats our TAX+TXA conversion (0 instr vs 2 instr).
      if (!LdaMI || xTouched || gapEmpty) continue;

      // X-live-after-LDA check: TXA (the LDAfi replacement) clobbers X.
      // If anything downstream of the LDA reads X — including the next
      // JSL's implicit $x — then we'd silently corrupt X.  Caught by
      // i32 first-arg functions where $x is live-in (= arg0_hi) and
      // a libcall later in the block expects $x intact.  Scan from just
      // past LDA to end-of-block; if any instr uses X, bail.
      bool xUsedAfter = false;
      for (auto Scan = std::next(MachineBasicBlock::iterator(LdaMI));
           Scan != MBB.end(); ++Scan) {
        const MachineInstr &MI3 = *Scan;
        if (MI3.isDebugInstr()) continue;
        XEffect eff = xEffect(MI3, TRI);
        if (eff & XReads) { xUsedAfter = true; break; }
        if (eff & XDefs) break;  // X redefined; no longer live
      }
      // Also bail if X is live-in to MBB and nothing has defined X
      // between MBB start and STA — the live-in value is needed past
      // the LDA point.
      if (!xUsedAfter && MBB.isLiveIn(W65816::X)) {
        bool xRedefBeforeSta = false;
        for (auto Scan = MBB.begin();
             Scan != MachineBasicBlock::iterator(StaMI); ++Scan) {
          const MachineInstr &MI3 = *Scan;
          if (MI3.isDebugInstr()) continue;
          if (xEffect(MI3, TRI) & XDefs) { xRedefBeforeSta = true; break; }
        }
        if (!xRedefBeforeSta) xUsedAfter = true;
      }
      if (xUsedAfter) continue;

      // Cross-block use check: if the slot is referenced anywhere
      // OUTSIDE the [STA, LDA] window (including other blocks), the
      // STA we'd erase is feeding those other reads — eliding it
      // would silently corrupt them.  Caught by sumTable() returning
      // a stale phi value because the loop's STA-to-merge-slot was
      // eliminated; the merge block's LDA then read the bb.0-init 0
      // instead of the loop's accumulated sum.
      bool externalUse = false;
      for (auto &OtherMBB : MF) {
        for (auto &OtherMI : OtherMBB) {
          if (&OtherMI == StaMI || &OtherMI == LdaMI) continue;
          // Walk inside-window range and skip those refs.
          if (&OtherMBB == &MBB) {
            // We already verified the gap doesn't reference FI; only
            // STA/LDA themselves are allowed users in this block.
          }
          if (referencesSlot(OtherMI, FI)) {
            externalUse = true;
            break;
          }
        }
        if (externalUse) break;
      }
      if (externalUse) continue;

      // Replace STAfi with TAX, LDAfi with TXA.
      //
      // DBG_VALUE preservation: STAfi/LDAfi are MC-level memory ops
      // with no vreg defs that a DBG_VALUE could reference — at this
      // post-PEI stage, debug operands have already been resolved to
      // <frame-reg, offset> expressions baked into DIExpression.  The
      // frame slot itself is reclaimed below (MFI.RemoveStackObject),
      // and DBG_VALUEs that pointed at it now refer to a slot that's
      // no longer written but the value flows through X.  Tracking
      // value-through-register-transfers is beyond Phase 1.5 scope;
      // worst case the user sees the variable as <optimized out> at
      // the affected PC range rather than a wrong value.
      DebugLoc StaDL = StaMI->getDebugLoc();
      DebugLoc LdaDL = LdaMI->getDebugLoc();
      MachineBasicBlock *MBB2 = StaMI->getParent();
      auto StaIt = MachineBasicBlock::iterator(StaMI);
      auto LdaIt = MachineBasicBlock::iterator(LdaMI);
      BuildMI(*MBB2, StaIt, StaDL, TII->get(W65816::TAX));
      BuildMI(*MBB2, LdaIt, LdaDL, TII->get(W65816::TXA))
          .addReg(W65816::A, RegState::ImplicitDefine);
      StaMI->eraseFromParent();
      LdaMI->eraseFromParent();
      SlotsTouched.insert(FI);
      Changed = true;
    }

    // Post-pass: collapse `TAX ; TXA` (or `TXA ; TAX`) pairs whose
    // observable effect is dead.  These appear when an inner STA/LDA
    // pair (originally between an outer pair we converted) was deleted
    // by StackSlotCleanup or coalesced by stack-slot-coloring, leaving
    // our TAX/TXA bookends adjacent.
    //
    // Distinct effect per ordering:
    //   TAX;TXA  : net effect is `X := A` (A unchanged, X clobbered).
    //              Removable iff X dead afterwards.
    //   TXA;TAX  : net effect is `A := X` (X unchanged, A clobbered).
    //              Removable iff A dead afterwards.
    //
    // The earlier code mis-handled TXA;TAX as if it clobbered X; in
    // fact X comes through the pair unchanged.
    auto It = MBB.begin();
    while (It != MBB.end()) {
      auto Next = std::next(It);
      if (Next == MBB.end()) break;
      bool isTaxThenTxa = It->getOpcode() == W65816::TAX &&
                          Next->getOpcode() == W65816::TXA;
      bool isTxaThenTax = It->getOpcode() == W65816::TXA &&
                          Next->getOpcode() == W65816::TAX;
      if (!isTaxThenTxa && !isTxaThenTax) { ++It; continue; }

      // Choose which physreg's liveness matters based on which value
      // the pair clobbers.
      Register Clobbered = isTaxThenTxa ? W65816::X : W65816::A;

      bool observed = false;
      bool killedByDef = false;
      for (auto Tail = std::next(Next); Tail != MBB.end(); ++Tail) {
        if (Tail->isDebugInstr()) continue;
        if (Tail->readsRegister(Clobbered, TRI)) { observed = true; break; }
        // Calls clobber both A and X (caller-saved).
        if (Tail->isCall()) { killedByDef = true; break; }
        // Opcode-based defs (TAX/TXA tablegen has no Defs metadata).
        if (Clobbered == W65816::X) {
          XEffect E = xEffect(*Tail, TRI);
          if (E & XReads) { observed = true; break; }
          if (E & XDefs)  { killedByDef = true; break; }
        } else {
          // For A: any LDA*/PLA/TXA/TYA/INA/DEA/arith op redefines A.
          unsigned Op = Tail->getOpcode();
          if (Op == W65816::TXA || Op == W65816::TYA ||
              Op == W65816::INA || Op == W65816::DEA ||
              Op == W65816::PLA) { killedByDef = true; break; }
          if (Tail->modifiesRegister(W65816::A, TRI)) {
            killedByDef = true; break;
          }
        }
      }
      if (observed) { ++It; continue; }
      if (!killedByDef) {
        bool liveOut = false;
        for (MachineBasicBlock *Succ : MBB.successors()) {
          if (Succ->isLiveIn(Clobbered)) { liveOut = true; break; }
        }
        if (liveOut) { ++It; continue; }
      }

      auto Erase1 = It++;
      auto Erase2 = It++;
      Erase1->eraseFromParent();
      Erase2->eraseFromParent();
      Changed = true;
    }
  }

  // Reclaim frame slots whose last reference we just erased.  Without
  // this, PEI still allocates space for them and emits the prologue
  // PHA, even though the slot is unused — wastes 1 PHA (4 cyc) and
  // 1 PLY per call.  RemoveStackObject marks the slot dead by setting
  // its size to ~0ULL; PEI ignores those when computing frame size.
  if (!SlotsTouched.empty()) {
    MachineFrameInfo &MFI = MF.getFrameInfo();
    for (int FI : SlotsTouched) {
      bool stillUsed = false;
      for (auto &MBB : MF) {
        for (auto &MI : MBB) {
          if (referencesSlot(MI, FI)) { stillUsed = true; break; }
        }
        if (stillUsed) break;
      }
      if (!stillUsed) MFI.RemoveStackObject(FI);
    }
  }
  return Changed;
}
