//===-- W65816AsmPrinter.cpp - W65816 LLVM assembly writer ----------------===//
//
// 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
//
//===----------------------------------------------------------------------===//
//
// W65816 assembly printer.  Owns the late pseudo-expansion path
// (MCInst lowering for the IR-pseudo opcodes that we keep through PEI
// because their machine encoding depends on AsmPrinter-time peepholes
// or runtime ABI knowledge -- BRK_pseudo, LDAi16imm_bank, JSLpseudo,
// the SEP/REP-wrapped i8 forms, etc.), plus a small set of mode-aware
// peepholes (PEA / PEI substitution for LDA+PUSH16 chains, STZ
// folding, etc.) that prefer to run after the rest of codegen has
// stabilised the MIR.
//
//===----------------------------------------------------------------------===//

#include "MCTargetDesc/W65816InstPrinter.h"
#include "W65816MachineFunctionInfo.h"
#include "W65816MCInstLower.h"
#include "W65816TargetMachine.h"
#include "TargetInfo/W65816TargetInfo.h"
#include "llvm/BinaryFormat/ELF.h"
#include "llvm/CodeGen/AsmPrinter.h"
#include "llvm/CodeGen/MachineFrameInfo.h"
#include "llvm/CodeGen/MachineFunction.h"
#include "llvm/CodeGen/MachineInstr.h"
#include "llvm/IR/GlobalValue.h"
#include "llvm/MC/MCContext.h"
#include "llvm/MC/MCExpr.h"
#include "llvm/MC/MCInst.h"
#include "llvm/MC/MCInstBuilder.h"
#include "llvm/MC/MCSectionELF.h"
#include "llvm/MC/MCStreamer.h"
#include "llvm/MC/MCSymbol.h"
#include "llvm/MC/TargetRegistry.h"
#include "llvm/Support/Compiler.h"

using namespace llvm;

#define DEBUG_TYPE "asm-printer"

// W65816 processor-status flag masks used by SEP/REP wrapping.
// (See W65816 datasheet 6.10.)  M = accumulator width (1 = 8-bit,
// 0 = 16-bit); X = index width (same convention).  The wraps in this
// file toggle M only; X never changes in normal codegen.
static constexpr unsigned kPStatusM = 0x20;
[[maybe_unused]] static constexpr unsigned kPStatusX = 0x10;

// IIgs runtime DP slots referenced from emitted code.  Both are part of
// the runtime ABI -- AsmPrinter / ISelLowering / libgcc must agree.
//   kRuntimePbrStashDP -- crt0 stashes the runtime PBR here so
//                         LDAi16imm_bank can emit `lda $BE` (PBR-byte
//                         load) for &symbol values in non-bank-0 placements.
//   kRuntimeIndirTargetDP -- __indirTarget vector used by the
//                            JMP (abs) indirect-call thunk.
static constexpr unsigned kRuntimePbrStashDP    = 0xBE;
static constexpr unsigned kRuntimeIndirTargetDP = 0x00B8;

// Trap probe DP slot — the BRK_pseudo expansion writes the trap sentinel
// here so external observers (smoke tests, runViaFinder $70 probe) can
// detect that __builtin_trap fired.  Kept in sync with the $70 convention
// across crt0, runInGno --check, and the demos/probe harnesses.
static constexpr unsigned kTrapStatusDP = 0x70;

// Sentinel value materialised into A and stored at $70 before BRK by
// BRK_pseudo.  High byte 0x00 is bank pad, low byte 0xBE is the trap
// marker (matches kRuntimePbrStashDP coincidentally, but they have
// different semantic roles: this is data, that is an address).
static constexpr unsigned kBrkPseudoSentinel = 0x00BE;

// DP scratch byte used by ADJCALLSTACKUP / ALLOCAfi to save A across a
// TSC/TCS bracket.  Lives in the project-wide $E0..$DF DP scratch
// range; coordinate with W65816ISelLowering / W65816RegisterInfo if
// the layout changes.
static constexpr unsigned kDpScratch0 = 0xE0;

// IIgs bank-byte mask: a 24-bit address whose top 8 bits are non-zero
// is in a non-zero bank and must be encoded via the LONG form.
static constexpr uint64_t kBankByteMask = 0xFF0000;

// ADJCALLSTACKUP fan-out limit: PLY (1 byte / 4 cyc per pair-pop) wins
// over the 8-byte / ~14-cyc TAY/TSC/CLC/ADC/TCS/TYA bracket up through
// N = 14 even bytes; beyond that the bracket is cheaper.  See the
// dispatch in the ADJCALLSTACKUP expansion.
static constexpr int kAdjStackUpPlyMaxN = 14;

// Per-function frame sidecar.  We emit one fixed-size record per
// function into a private DWARF-adjacent section so the host-side
// debugger (scripts/mameDebug.py --repl `bt`) can walk parent JSL
// return frames.  The record layout is:
//
//   uint32_t fnPcStart  (R_W65816_DATA32 to the function-entry label)
//   uint32_t fnPcEnd    (R_W65816_DATA32 to a private function-end label)
//   uint16_t fnFrameSizeBytes  (MachineFrameInfo::getStackSize)
//   uint8_t  fnRtlBytes        (3 for our JSL/RTL ABI; reserved for
//                               future inline JSR/RTS subroutines)
//   uint8_t  pad               (zero — keeps the record at 12 bytes,
//                               aligned for trivial pointer arithmetic
//                               in the parser)
//
// Total: 12 bytes per record.  Records are emitted in object-file
// order; link816's `.debug_*` concat-and-relocate path forwards them
// unchanged into the sidecar.  The fnPcStart fields end up holding
// 24-bit final-image addresses (top 8 bits are zero for bank-0
// programs but reserved for future multi-bank placements).
static constexpr unsigned kFrameRecordSize  = 12;
static constexpr unsigned kJslRtlBytes      = 3;
static constexpr const char *kFrameSection  = ".debug_frame_w65816";

// Phase B — opt-in far globals.  A global declared via
// `__attribute__((section("X")))` where X contains ".far" (canonically
// .rodata.far / .data.far / .bss.far) routes through LDA_Long / STA_Long
// instead of LDA_Abs / STA_Abs at AsmPrinter pseudo expansion, so its
// bank byte is encoded explicitly and the linker patches the full
// 24-bit address via R_W65816_IMM24.  This is the opt-in path for
// programs whose data overflows the small-data bank (DBR is pinned at
// the BSS-seg-0 bank by crt0; large rodata/BSS in other banks needs
// long-form addressing).
static bool isFarGlobalOperand(const llvm::MachineOperand &MO) {
  if (!MO.isGlobal()) return false;
  const llvm::GlobalValue *GV = MO.getGlobal();
  if (!GV->hasSection()) return false;
  return GV->getSection().find(".far") != llvm::StringRef::npos;
}

namespace {

class W65816AsmPrinter : public AsmPrinter {
public:
  W65816AsmPrinter(TargetMachine &TM, std::unique_ptr<MCStreamer> Streamer)
      : AsmPrinter(TM, std::move(Streamer), ID) {}

  StringRef getPassName() const override { return "W65816 Assembly Printer"; }

  void emitInstruction(const MachineInstr *MI) override;

  // Emit `SEP #kPStatusM` — open an i8 wrap.  Shared by every i8-pseudo
  // expansion (LDAi8imm, STA8abs/long, LDA8abs/long/AbsX, CMPi8imm,
  // ADCi8imm/SBCi8imm, ANDi8imm/ORAi8imm/EORi8imm).
  void emitSepM() {
    EmitToStreamer(*OutStreamer,
                   MCInstBuilder(W65816::SEP).addImm(kPStatusM));
  }
  // Emit `REP #kPStatusM` — close an i8 wrap, restoring M=16.
  void emitRepM() {
    EmitToStreamer(*OutStreamer,
                   MCInstBuilder(W65816::REP).addImm(kPStatusM));
  }
  // Emit a single-operand instruction with one immediate value (most
  // common 65816 pseudo-lowering shape).
  void emitOpImm(unsigned Opcode, int64_t Imm) {
    EmitToStreamer(*OutStreamer, MCInstBuilder(Opcode).addImm(Imm));
  }
  // Emit an implied / accumulator-only instruction (no operands).
  void emitOp(unsigned Opcode) {
    EmitToStreamer(*OutStreamer, MCInstBuilder(Opcode));
  }

  // Clear all peephole skip flags.  Called at function-body-end and
  // basic-block-start to make sure a half-applied peephole at the end
  // of one region never reaches into the next one — e.g. a stale
  // SkipNextSepImm from the LDAi8imm-collapse in the previous MBB
  // could swallow the first SEP of the next MBB.
  void clearSkipFlags() {
    SkipNextSepImm = -1;
    SkipNextStaAbs = false;
    SkipNextPush16 = false;
    SkipNextSta8Wrap = false;
  }
  // Reset per-function state (defensive — see clearSkipFlags) and
  // emit one .debug_frame_w65816 record for the function we just
  // finished printing.  AsmPrinter::emitFunctionBody invokes this hook
  // after the last instruction and before the auto-generated function
  // end label; we synthesise our own end label (a private temp sym)
  // so the record can carry both endpoints regardless of whether the
  // generic code path also emits a CurrentFnEnd label.
  void emitFunctionBodyEnd() override {
    clearSkipFlags();
    emitFrameRecord();
  }
  void emitFrameRecord();

  // Counter for the per-function private end-label symbols.  Unique
  // names sidestep any clash with hand-written `.Lw65_*` labels in
  // crt0/libgcc assembly.
  unsigned FrameRecordIdx = 0;
  // Reset on MBB entry too — labels emit before the MIs of a new MBB.
  void emitBasicBlockStart(const MachineBasicBlock &MBB) override {
    clearSkipFlags();
    AsmPrinter::emitBasicBlockStart(MBB);
  }

  // When >=0, the next SEP #imm instruction with this immediate is
  // dropped (consumed) and this field reset to -1.  Used by the
  // LDAi8imm expansion to elide a redundant REP/SEP pair when the
  // following MI is the byte-store wrap's opening SEP — the LDAi8imm
  // already left M=8, so the wrap's SEP would be a no-op.
  int SkipNextSepImm = -1;

  // When true, the next STAabs is consumed (already replaced with STZ
  // by the LDAi16imm-0 peephole).
  bool SkipNextStaAbs = false;

  // When true, the next PUSH16 is consumed (already replaced with PEA
  // by the LDAi16imm + PUSH16 peephole).
  bool SkipNextPush16 = false;

  // When true, the next STA8abs / STA8long should skip emitting its
  // opening SEP (we already entered M=8 via the preceding LDAi8imm
  // collapse) and skip its closing REP (the LDAi8imm consumer will
  // restore M=16 itself).  Avoids 4 B / 6 cyc per byte-store-of-imm.
  bool SkipNextSta8Wrap = false;

  static char ID;
};

// Emit one fixed-size frame record into .debug_frame_w65816.  See the
// kFrameRecordSize comment near the top of the file for the layout
// rationale.  Skip pseudo-functions whose stack size hasn't been
// stabilised by PEI (CurrentFnSym null is the canonical guard) so we
// don't emit dangling records that would resolve to addr=0 at link
// time.
void W65816AsmPrinter::emitFrameRecord() {
  if (!MF || !CurrentFnSym)
    return;
  const MachineFrameInfo &MFI = MF->getFrameInfo();
  uint64_t StackSize = MFI.getStackSize();
  // Clip to 16 bits.  Frames > 64KB are physically unrepresentable on
  // the 65816 (the S register itself is 16 bits) so any larger value
  // is a backend bug worth catching elsewhere — here we just truncate
  // to keep the record format fixed-width.
  if (StackSize > 0xFFFFu)
    StackSize = 0xFFFFu;

  // Save the current section so we can switch back after writing the
  // record.  Without the save/restore, the trailing function-size
  // computation in AsmPrinter::emitFunctionBody would emit the size
  // .quad into our debug section instead of .text.
  MCSection *PrevSection = OutStreamer->getCurrentSectionOnly();

  // Synthesise the function-end label in the current text section,
  // *before* switching to the debug section.  emitFunctionBodyEnd is
  // called from AsmPrinter::emitFunctionBody after the last MI and
  // before the CurrentFnEnd label (which we can't reference here
  // because it's emitted later — see AsmPrinter.cpp ~line 2479).
  MCSymbol *FnEnd =
      OutContext.createTempSymbol("w65_fnEnd" + Twine(FrameRecordIdx++));
  OutStreamer->emitLabel(FnEnd);

  // Switch to the frame-info section.  ELF type SHT_PROGBITS, no
  // SHF_ALLOC (sidecar-only — never loaded at runtime).  link816's
  // sidecar emitter forwards any section whose name starts with
  // `.debug_` regardless of flags.
  MCSection *FrameSec =
      OutContext.getELFSection(kFrameSection, ELF::SHT_PROGBITS, 0);
  OutStreamer->switchSection(FrameSec);

  // Record layout (12 bytes — see the kFrameRecordSize comment near
  // the top of the file):
  //
  //   +0  uint32_t fnPcStart   R_W65816_DATA32 -> CurrentFnSym
  //   +4  uint32_t fnPcEnd     R_W65816_DATA32 -> FnEnd
  //   +8  uint16_t frameSize
  //   +10 uint8_t  rtlBytes
  //   +11 uint8_t  pad
  const MCExpr *StartExpr = MCSymbolRefExpr::create(CurrentFnSym, OutContext);
  const MCExpr *EndExpr   = MCSymbolRefExpr::create(FnEnd, OutContext);
  OutStreamer->emitValue(StartExpr, /*Size=*/4);
  OutStreamer->emitValue(EndExpr,   /*Size=*/4);
  OutStreamer->emitInt16(static_cast<uint16_t>(StackSize));
  OutStreamer->emitInt8(kJslRtlBytes);
  OutStreamer->emitInt8(0);

  // Restore the prior section so the rest of AsmPrinter::emitFunctionBody
  // (CurrentFnEnd label, .size directive, etc.) emits into .text.
  if (PrevSection)
    OutStreamer->switchSection(PrevSection);
}

} // end anonymous namespace

// Convert a single MachineOperand to an MCOperand using the standard
// rules (register / immediate / global / external / etc.).  Used by
// pseudo expansion to lift symbolic operands; mirrors the per-operand
// path in W65816MCInstLower::Lower.
static MCOperand lowerOperand(const MachineOperand &MO,
                              W65816MCInstLower &Lower) {
  switch (MO.getType()) {
  case MachineOperand::MO_Register:
    return MCOperand::createReg(MO.getReg());
  case MachineOperand::MO_Immediate:
    return MCOperand::createImm(MO.getImm());
  case MachineOperand::MO_GlobalAddress:
    return Lower.LowerSymbolOperand(MO, Lower.GetGlobalAddressSymbol(MO));
  case MachineOperand::MO_ExternalSymbol:
    return Lower.LowerSymbolOperand(MO, Lower.GetExternalSymbolSymbol(MO));
  case MachineOperand::MO_BlockAddress:
    return Lower.LowerSymbolOperand(MO, Lower.GetBlockAddressSymbol(MO));
  case MachineOperand::MO_JumpTableIndex:
    return Lower.LowerSymbolOperand(MO, Lower.GetJumpTableSymbol(MO));
  case MachineOperand::MO_ConstantPoolIndex:
    return Lower.LowerSymbolOperand(MO, Lower.GetConstantPoolIndexSymbol(MO));
  default:
    llvm_unreachable("W65816: unsupported operand type in pseudo lower");
  }
}

void W65816AsmPrinter::emitInstruction(const MachineInstr *MI) {
  W65816_MC::verifyInstructionPredicates(MI->getOpcode(),
                                         getSubtargetInfo().getFeatureBits());

  // Drop a SEP that the previous LDAi8imm expansion marked redundant.
  // The LDAi8imm peephole leaves M=8 set when its successor (or the
  // next non-mode-neutral MI) is a SEP #$20 — that SEP would re-set
  // the same flag, so we elide it.  Mode-neutral MIs (X-flag-only
  // index ops, branches, transfers that don't touch A) pass through
  // freely without invalidating the skip.
  if (SkipNextSepImm >= 0 && !MI->isDebugInstr()) {
    if (MI->getOpcode() == W65816::SEP &&
        MI->getNumOperands() >= 1 && MI->getOperand(0).isImm() &&
        MI->getOperand(0).getImm() == SkipNextSepImm) {
      SkipNextSepImm = -1;
      return;  // consume the SEP, don't emit
    }
    // Check if MI is mode-neutral; if so, pass through and KEEP the skip.
    bool isMNeutral = false;
    if (MI->isBranch() || MI->isReturn()) isMNeutral = true;
    else switch (MI->getOpcode()) {
    case W65816::LDX_Imm16: case W65816::LDX_DP: case W65816::LDX_Abs:
    case W65816::LDX_DPY:   case W65816::LDX_AbsY:
    case W65816::LDY_Imm16: case W65816::LDY_DP: case W65816::LDY_Abs:
    case W65816::LDY_DPX:   case W65816::LDY_AbsX:
    case W65816::STX_DP:    case W65816::STX_Abs: case W65816::STX_DPY:
    case W65816::STY_DP:    case W65816::STY_Abs: case W65816::STY_DPX:
    case W65816::INX:       case W65816::DEX:
    case W65816::INY:       case W65816::DEY:
    case W65816::CPX_Imm16: case W65816::CPX_DP: case W65816::CPX_Abs:
    case W65816::CPY_Imm16: case W65816::CPY_DP: case W65816::CPY_Abs:
    case W65816::PHX:       case W65816::PHY:
    case W65816::PLX:       case W65816::PLY:
    case W65816::NOP:
      isMNeutral = true; break;
    default: break;
    }
    // Anything else invalidates the elision (might re-clear M, push/pop
    // 8-bit P that observes mode, call out, etc.).
    if (!isMNeutral) SkipNextSepImm = -1;
  }

  // Drop the STAabs that the LDAi16imm-0 peephole replaced with STZ.
  if (SkipNextStaAbs && !MI->isDebugInstr()) {
    if (MI->getOpcode() == W65816::STAabs) {
      SkipNextStaAbs = false;
      return;  // consume, already emitted as STZ
    }
    // Anything other than the expected STAabs cancels the elision.
    SkipNextStaAbs = false;
  }

  // Drop the PUSH16 that the LDAi16imm + PUSH16 peephole replaced
  // with PEA.
  if (SkipNextPush16 && !MI->isDebugInstr()) {
    if (MI->getOpcode() == W65816::PUSH16) {
      SkipNextPush16 = false;
      return;  // consume, already emitted as PEA
    }
    SkipNextPush16 = false;
  }

  W65816MCInstLower MCInstLowering(OutContext, *this);

  // Expand codegen pseudos into their MC-layer realisations.  Keep this
  // switch in sync with the pseudo defs at the bottom of
  // W65816InstrInfo.td.  All single-output pseudos here have operand
  // layout (outs $dst, ins $value); we drop $dst (it lives implicitly in
  // A) and lift $value into the MC instruction's single source operand.
  switch (MI->getOpcode()) {
  default:
    break;
  case W65816::INC_HI_IF_CARRY_StackRel: {
    // Conditional-increment of an i32's hi half on stack at (off, S).
    // Z is presumed set/clear from the preceding LDA-INA-STA on the
    // lo half: Z=1 means lo wrapped 0xFFFF→0, so hi must be incremented;
    // Z=0 means no overflow, skip.  STA preserves N/Z so the gap
    // between the lo's INA and our BNE is OK.
    //
    // Emits:
    //   bne <skip>
    //   lda $off, s
    //   inc a
    //   sta $off, s
    //   <skip>:
    int64_t Off = MI->getOperand(0).getImm();
    MCSymbol *SkipSym = OutContext.createTempSymbol();
    EmitToStreamer(*OutStreamer,
        MCInstBuilder(W65816::BNE).addExpr(
            MCSymbolRefExpr::create(SkipSym, OutContext)));
    emitOpImm(W65816::LDA_StackRel, Off);
    emitOp(W65816::INA);
    emitOpImm(W65816::STA_StackRel, Off);
    OutStreamer->emitLabel(SkipSym);
    return;
  }
  case W65816::ADJCALLSTACKDOWN: {
    // DOWN is a no-op in our scheme — the PUSH16 sequence in LowerCall
    // already shifted SP incrementally as args were pushed.  Nothing
    // to emit; PEI may or may not have processed it, either is fine.
    return;
  }
  case W65816::ADJCALLSTACKUP: {
    // PEI's eliminateCallFramePseudoInstr handles UP whenever the
    // function has any frame work (StackSize > 0 or any FI use).
    // Frame-less functions — e.g. `unsigned short sqr(unsigned short
    // x) { return x*x; }` lowers to PUSH16 + jsl __mulhi3 + RTL with
    // no locals — get skipped by PEI's call-frame phase, leaving
    // ADJCALLSTACKUP as a pseudo all the way to here.  Previously we
    // silently dropped it, which left SP off by N bytes after the
    // call and corrupted the caller's stack frame (caught by sqr(x)
    // segfaulting MAME).  Emit the SP fixup ourselves: PLY*N/2 for
    // small even N, otherwise the TAY/TSC-ADC/TYA bracket.
    int N = MI->getOperand(0).getImm();
    if (N == 0) return;
    // A holds the callee's return value; preserve it.  Walk forward
    // looking for X/Y uses (i64-return halves) — same logic as
    // eliminateCallFramePseudoInstr.
    bool YLive = false;
    for (auto J = std::next(MI->getIterator()); J != MI->getParent()->end();
         ++J) {
      if (J->isCall()) break;
      bool yDef = false;
      for (const MachineOperand &MO : J->operands()) {
        if (!MO.isReg()) continue;
        if (MO.getReg() == W65816::Y) {
          if (MO.isUse()) { YLive = true; break; }
          if (MO.isDef()) yDef = true;
        }
      }
      if (YLive || yDef) break;
    }
    if (YLive) {
      // Route through DP $E0 to preserve both A and Y.
      emitOpImm(W65816::STA_DP, kDpScratch0);
      emitOp(W65816::TSC);
      emitOp(W65816::CLC);
      emitOpImm(W65816::ADC_Imm16, N);
      emitOp(W65816::TCS);
      emitOpImm(W65816::LDA_DP, kDpScratch0);
    } else if (N <= kAdjStackUpPlyMaxN && (N % 2) == 0) {
      // Repeated PLY (1 byte / 4 cyc each) wins over the TAY/TSC/CLC/
      // ADC/TCS/TYA bracket (8 bytes / ~14 cyc fixed) for N <= 14;
      // beyond that the bracket is cheaper.  Must be even (PLY pops
      // 16-bit pairs).
      for (int i = 0; i < N / 2; ++i) {
        emitOp(W65816::PLY);
      }
    } else {
      emitOp(W65816::TAY);
      emitOp(W65816::TSC);
      emitOp(W65816::CLC);
      emitOpImm(W65816::ADC_Imm16, N);
      emitOp(W65816::TCS);
      emitOp(W65816::TYA);
    }
    return;
  }
  case W65816::LDXi16imm: {
    EmitToStreamer(*OutStreamer,
        MCInstBuilder(W65816::LDX_Imm16)
            .addOperand(lowerOperand(MI->getOperand(1), MCInstLowering)));
    return;
  }
  case W65816::LDAi16imm_bank: {
    // Bank-byte materialization for the HIGH half of a `&symbol` 32-bit
    // pointer.  At runtime crt0 sets $00:00BE = PBR (placement bank)
    // and $00:00BF = 0 (pad), so `lda $BE` in 16-bit M reads the 16-bit
    // value $00PBR — exactly the bank|pad word we want.
    //
    // Why not a fixup_bank16 reloc the OMF Loader patches?  The whole
    // BANK16 pipeline IS wired (LDA_Imm16_Bank pseudo -> fixup_bank16 ->
    // R_W65816_BANK16 -> link816 BANK16 reloc -> cRELOC ByteCnt=2
    // BitShift=16) and currently dormant.  Empirically retested 2026-05-29
    // against real GS/OS 6.0.4 via scripts/runViaFinder.sh + a one-shot
    // demos/bankProbe.c (writes (uint32_t)&probe >> 16 to $70 and the
    // crt0-stashed $BE to $72):
    //
    //   control build (lda $BE):   $70 = 0x08, $72 = 0x08   -- placed bank
    //   patched build (BANK16):    $70 = 0x00, $72 = 0x08   -- LOADER BUG
    //
    // The Loader's BitShift=16 formula truncates `segPlacedBase` to 16
    // bits before adding offsetRef and shifting, so the result is always
    // 0 for our segment sizes.  The sibling IMM24 path (ByteCnt=3,
    // BitShift=0, e.g. `jsl main`) delivers the placed 24-bit address
    // correctly, so this is specifically a >>16 truncation in the bank
    // extraction.  OMF RELOC (0xE2) uses the identical formula and is
    // no help; INTERSEG is for cross-segment refs.  Reading PBR at
    // runtime via $BE is the only working mechanism on real hardware.
    //
    // EXCEPTION — external-weak symbols: a `&weakfn` whose definition may
    // be absent at link time must be representable as NULL.  If we emit
    // `lda $BE` for its bank, the runtime PBR (non-zero when GS/OS loads
    // the segment in a non-zero bank) makes the pointer always non-null,
    // so `if (weakfn) weakfn()` is always taken and jumps to the
    // unresolved (=0) target — a crash.  For an external-weak global the
    // bank must be a literal 0 instead, so an undefined-weak ref is a
    // genuine null and a strong link-time def still works (its low16 is
    // non-zero so the null test passes and the direct call uses the
    // symbol's own reloc, not this bank byte).  Defined/normal globals
    // and compiler external symbols (libgcc helpers) keep `lda $BE`.
    const MachineOperand &Sym = MI->getOperand(1);
    bool WeakUndef = Sym.isGlobal() &&
                     Sym.getGlobal()->hasExternalWeakLinkage();
    if (WeakUndef) {
      emitOpImm(W65816::LDA_Imm16, 0);
    } else {
      emitOpImm(W65816::LDA_DP, kRuntimePbrStashDP);
    }
    return;
  }
  case W65816::LDAi16imm: {
    // Peek at the next non-debug MI for two peephole patterns:
    //   (1) LDAi16imm 0 + STAabs $g  -> STZ_Abs $g (saves 3B)
    //   (2) LDAi16imm $imm + PUSH16  -> PEA #$imm  (saves 1B)
    // Both replace the LDA+next pair with a single 3- or 4-byte op
    // that achieves the same memory effect.
    auto It = std::next(MI->getIterator());
    while (It != MI->getParent()->end() && It->isDebugInstr()) ++It;

    bool IsZero = MI->getOperand(1).isImm() &&
                  MI->getOperand(1).getImm() == 0;
    // STAabs operand layout: (value:Acc16, addr).  Operand 0 is the A
    // register; only fire when this STA kills A — otherwise dropping
    // the LDA leaves a later A-consumer without its value.  E.g. SDAG
    // CSE'd `g16 = 0; g32 = 0;` shares one LDAi16imm 0 across multiple
    // STAabs; only the LAST one kills A, so the peephole would have
    // miscompiled the earlier two by deleting the LDA but not the
    // remaining STAs.
    if (IsZero && It != MI->getParent()->end() &&
        It->getOpcode() == W65816::STAabs &&
        It->getOperand(0).isReg() && It->getOperand(0).isKill()) {
      // Only collapse to STZ when the address fits in 16 bits.  STZ
      // has no long-form (24-bit) variant — STZ_Abs is DBR-relative
      // 3-byte and would drop a non-zero bank byte.  When the user
      // writes `*(uint16 *)0xE19E00UL = 0` we MUST keep the
      // LDA #0 + STA_Long pair so the bank-explicit form survives.
      bool AddrFitsIn16 = !It->getOperand(1).isImm() ||
                          (It->getOperand(1).getImm() & kBankByteMask) == 0;
      if (AddrFitsIn16) {
        EmitToStreamer(*OutStreamer,
            MCInstBuilder(W65816::STZ_Abs)
                .addOperand(lowerOperand(It->getOperand(1), MCInstLowering)));
        SkipNextStaAbs = true;
        return;
      }
    }

    // PEA peephole: LDAi16imm + PUSH16 -> PEA.  Safe iff A is dead
    // after the PUSH16 — the next instruction must redefine A WITHOUT
    // reading A first.  JSL has implicit-def $a (caller-save) AND
    // implicit-use $a when A is an arg (the W65816 calling convention).
    // The peephole must NOT fire when the next op USES A: dropping
    // the LDA would leave A undefined going into the callee.  Same
    // shape as the PEI peephole below.
    if (It != MI->getParent()->end() && It->getOpcode() == W65816::PUSH16) {
      auto It2 = std::next(It);
      while (It2 != MI->getParent()->end() && It2->isDebugInstr()) ++It2;
      // If PUSH16 is the last MI in the BB we leave the peephole as a
      // no-op (conservative): the PUSH chain almost always feeds a JSL
      // within the same BB, and proving A-dead at BB exit via successor
      // live-in scan is not worth the bookkeeping.
      bool ADead = false;
      if (It2 != MI->getParent()->end()) {
        const TargetRegisterInfo *TRI =
            MI->getParent()->getParent()->getSubtarget().getRegisterInfo();
        if (It2->modifiesRegister(W65816::A, TRI) &&
            !It2->readsRegister(W65816::A, TRI))
          ADead = true;
      }
      if (ADead) {
        EmitToStreamer(*OutStreamer,
            MCInstBuilder(W65816::PEA)
                .addOperand(lowerOperand(MI->getOperand(1), MCInstLowering)));
        SkipNextPush16 = true;
        return;
      }
    }

    EmitToStreamer(*OutStreamer,
        MCInstBuilder(W65816::LDA_Imm16)
            .addOperand(lowerOperand(MI->getOperand(1), MCInstLowering)));
    return;
  }
  case W65816::LDAi8imm: {
    // i8 immediate — requires M=1 so the CPU reads only 1 immediate
    // byte.  The function runs in M=0 (prologue convention), so wrap
    // with SEP/REP.  Adjacent i8 ops collapse via W65816SepRepCleanup.
    //
    // Lookahead optimization: if the next MI is `SEP #$20` (the
    // STBptr inserter's byte-store wrap opener, hoisted above this
    // LDAi8imm by W65816SepRepCleanup), the closing REP here would
    // immediately be cancelled by that SEP.  Skip both — the byte
    // STA runs in our LDAi8imm-set M=8 mode.  Saves 4B / 6cyc per
    // hit.  We mark the next-SEP-to-skip via a per-AsmPrinter flag
    // so the SEP visit drops it.
    emitSepM();
    emitOpImm(W65816::LDA_Imm8, MI->getOperand(1).getImm() & 0xFF);
    bool SkipRep = false;
    // Walk past mode-neutral MIs (X-flag-only ops, branches, transfers
    // that don't touch A) to find the next SEP/REP — same idea as the
    // pre-emit REP/SEP scheduler, but applied to LDAi8imm's closing
    // REP.  If a SEP #$20 sits there, we can elide the REP+SEP pair.
    auto isMNeutralMI = [](const MachineInstr &MI) -> bool {
      if (MI.isDebugInstr()) return true;
      if (MI.isBranch() || MI.isReturn()) return true;
      unsigned O = MI.getOpcode();
      switch (O) {
      case W65816::LDX_Imm16: case W65816::LDX_DP: case W65816::LDX_Abs:
      case W65816::LDX_DPY:   case W65816::LDX_AbsY:
      case W65816::LDY_Imm16: case W65816::LDY_DP: case W65816::LDY_Abs:
      case W65816::LDY_DPX:   case W65816::LDY_AbsX:
      case W65816::STX_DP:    case W65816::STX_Abs: case W65816::STX_DPY:
      case W65816::STY_DP:    case W65816::STY_Abs: case W65816::STY_DPX:
      case W65816::INX:       case W65816::DEX:
      case W65816::INY:       case W65816::DEY:
      case W65816::CPX_Imm16: case W65816::CPX_DP: case W65816::CPX_Abs:
      case W65816::CPY_Imm16: case W65816::CPY_DP: case W65816::CPY_Abs:
      case W65816::PHX:       case W65816::PHY:
      case W65816::PLX:       case W65816::PLY:
      case W65816::CLC:       case W65816::SEC:
      case W65816::PHP:       case W65816::PLP:
      case W65816::NOP:
        return true;
      default:
        return false;
      }
    };
    auto It = std::next(MI->getIterator());
    while (It != MI->getParent()->end() && isMNeutralMI(*It)) ++It;
    if (It != MI->getParent()->end() &&
        It->getOpcode() == W65816::SEP &&
        It->getNumOperands() >= 1 && It->getOperand(0).isImm() &&
        It->getOperand(0).getImm() == kPStatusM) {
      SkipRep = true;
      SkipNextSepImm = static_cast<int>(kPStatusM);
    }
    // STA8abs / STA8long don't expose their SEP at MIR — the wrap is
    // emitted at MC layer.  Detect them here so we can elide the
    // closing REP and the store's opening SEP+REP wrap entirely:
    // the 4 emitted bytes (REP/SEP between LDAi8imm and STA, plus
    // REP after STA) all collapse, leaving SEP/LDA/STA/REP as the
    // tight sequence the user wrote.
    else if (It != MI->getParent()->end() &&
             (It->getOpcode() == W65816::STA8abs ||
              It->getOpcode() == W65816::STA8long)) {
      SkipRep = true;
      SkipNextSta8Wrap = true;
    }
    if (!SkipRep) {
      emitRepM();
    }
    return;
  }
  case W65816::LDAabs:
  case W65816::STAabs: {
    // Pick addressing form by address range (same dispatch for LDA/STA):
    //   $00..$FF      -> DP-form   (DP-relative; DP=0 in our runtime
    //                               makes this bank-0 fixed regardless
    //                               of DBR — correct for const literals
    //                               like *(uint16*)0x70).
    //   $100..$FFFF   -> Abs-form  (DBR-relative).
    //   non-zero bank -> Long-form (4-byte, bank-explicit).
    //   Far global    -> Long-form (Phase B opt-in: a global declared
    //                               via __attribute__((section("X")))
    //                               where X contains ".far" — usually
    //                               .rodata.far, .data.far, or .bss.far
    //                               — gets bank-explicit 24-bit access
    //                               regardless of DBR.  This is how a
    //                               program with > 60 KB of data spreads
    //                               it across multiple banks while keeping
    //                               DBR pinned at the small-data bank for
    //                               the rest of the program.)
    // The DP pickup is what makes the bank-0-anchored __indirTarget
    // slot at $00:00B8 (see runtime/src/libgcc.s) addressable correctly
    // from codegen-emitted indirect-call sequences when DBR != 0 (crt0
    // sets DBR=PBR for the small-data model; under non-bank-0 OMF
    // placement that's not 0).  STAabs uses the same dispatch so
    // `sta __indirTarget` writes to the bank-0-anchored slot regardless
    // of placement.
    bool IsLoad = MI->getOpcode() == W65816::LDAabs;
    unsigned OpDP   = IsLoad ? W65816::LDA_DP   : W65816::STA_DP;
    unsigned OpAbs  = IsLoad ? W65816::LDA_Abs  : W65816::STA_Abs;
    unsigned OpLong = IsLoad ? W65816::LDA_Long : W65816::STA_Long;
    const MachineOperand &AddrOp = MI->getOperand(1);
    if (AddrOp.isImm()) {
      uint64_t A = AddrOp.getImm();
      if ((A & ~0xFFULL) == 0) {
        emitOpImm(OpDP, A);
        return;
      }
      if ((A & kBankByteMask) != 0) {
        EmitToStreamer(*OutStreamer,
            MCInstBuilder(OpLong)
                .addOperand(lowerOperand(AddrOp, MCInstLowering)));
        return;
      }
    }
    bool IsFarGlobal = AddrOp.isGlobal() &&
                       AddrOp.getGlobal()->hasSection() &&
                       AddrOp.getGlobal()->getSection().find(".far") !=
                           StringRef::npos;
    EmitToStreamer(*OutStreamer,
        MCInstBuilder(IsFarGlobal ? OpLong : OpAbs)
            .addOperand(lowerOperand(AddrOp, MCInstLowering)));
    return;
  }
  case W65816::LDA_DP: {
    // PEI peephole: LDA_DP $dp + PUSH16 -> PEI $dp.  PEI pushes the
    // 16-bit value at direct page $dp directly onto the stack without
    // touching A, saving 1 byte (PEI=2B vs LDA_DP+PHA=3B).  Safe iff
    // A is dead after the PUSH16, same as the LDAi16imm+PUSH16
    // peephole.  Common case: i64-libcall-return forwarding —
    // copyPhysReg(A=DPF0) emits LDA $F0; the next op is PUSH16 to
    // forward the i64 high-half into a downstream call's args; the
    // chained call's first op then redefines A.
    auto It = std::next(MI->getIterator());
    while (It != MI->getParent()->end() && It->isDebugInstr()) ++It;
    if (It != MI->getParent()->end() &&
        It->getOpcode() == W65816::PUSH16) {
      auto It2 = std::next(It);
      while (It2 != MI->getParent()->end() && It2->isDebugInstr()) ++It2;
      bool ADead = false;
      if (It2 != MI->getParent()->end()) {
        const TargetRegisterInfo *TRI =
            MI->getParent()->getParent()->getSubtarget().getRegisterInfo();
        // PEI doesn't load A, so the LDA's value-set is needed if
        // the next instruction READS A.  JSL has implicit-def $a
        // (caller-save) AND implicit-use $a (when A is an arg) —
        // modifiesRegister returns true for both, but readsRegister
        // is what tells us if A's value is consumed.  Drop the LDA
        // ONLY when the next op modifies A WITHOUT reading it.
        if (It2->modifiesRegister(W65816::A, TRI) &&
            !It2->readsRegister(W65816::A, TRI))
          ADead = true;
      }
      if (ADead) {
        EmitToStreamer(*OutStreamer,
            MCInstBuilder(W65816::PEI_DP)
                .addOperand(lowerOperand(MI->getOperand(0), MCInstLowering)));
        SkipNextPush16 = true;
        return;
      }
    }
    // Fall through to default emit (no peephole opportunity).
    break;
  }
  case W65816::ADCi16imm:
  case W65816::SBCi16imm: {
    bool IsSub = MI->getOpcode() == W65816::SBCi16imm;
    emitOp(IsSub ? W65816::SEC : W65816::CLC);
    EmitToStreamer(*OutStreamer,
        MCInstBuilder(IsSub ? W65816::SBC_Imm16 : W65816::ADC_Imm16)
            .addOperand(lowerOperand(MI->getOperand(2), MCInstLowering)));
    return;
  }
  case W65816::ADCEi16imm:
  case W65816::SBCEi16imm: {
    // Chained ADC/SBC: no CLC/SEC prefix — the carry/borrow from the
    // previous addc/adde/subc/sube is already in P.  See ADCi16imm
    // comment in W65816InstrInfo.td.
    bool IsSub = MI->getOpcode() == W65816::SBCEi16imm;
    EmitToStreamer(*OutStreamer,
        MCInstBuilder(IsSub ? W65816::SBC_Imm16 : W65816::ADC_Imm16)
            .addOperand(lowerOperand(MI->getOperand(2), MCInstLowering)));
    return;
  }
  case W65816::ADCi8imm:
  case W65816::SBCi8imm: {
    bool IsSub = MI->getOpcode() == W65816::SBCi8imm;
    // SEP/REP wrap (see LDAi8imm comment).
    emitSepM();
    emitOp(IsSub ? W65816::SEC : W65816::CLC);
    emitOpImm(IsSub ? W65816::SBC_Imm8 : W65816::ADC_Imm8,
              MI->getOperand(2).getImm() & 0xFF);
    emitRepM();
    return;
  }
  case W65816::ANDi8imm:
  case W65816::ORAi8imm:
  case W65816::EORi8imm: {
    unsigned mc = 0;
    switch (MI->getOpcode()) {
    case W65816::ANDi8imm: mc = W65816::AND_Imm8; break;
    case W65816::ORAi8imm: mc = W65816::ORA_Imm8; break;
    case W65816::EORi8imm: mc = W65816::EOR_Imm8; break;
    }
    // Mask to 8 bits so the printer doesn't show the sign-extended
    // i8 value as a wider hex literal (e.g. -16 → 0xFFF0); the
    // encoder only takes the low byte anyway.  SEP/REP wrap as
    // documented at LDAi8imm.
    emitSepM();
    emitOpImm(mc, MI->getOperand(2).getImm() & 0xFF);
    emitRepM();
    return;
  }
  case W65816::LDA8abs:
  case W65816::LDA8long: {
    // i8 absolute load — same M=8 wrap as LDA_Abs; LDA8long uses
    // LDA_Long (0xAF, bank-explicit) for const-int MMIO addresses.
    // A global declared in a `.far*` section also routes through
    // LDA_Long so its bank byte is encoded explicitly (Phase B).
    bool IsLong = MI->getOpcode() == W65816::LDA8long;
    const MachineOperand &AddrMO = MI->getOperand(1);
    IsLong = IsLong || isFarGlobalOperand(AddrMO);
    MCOperand Addr = lowerOperand(AddrMO, MCInstLowering);
    if (IsLong && Addr.isImm()) {
      // Sign-extended 16-bit pointer -> low 16 (bank 0); genuine 24-bit
      // constant address -> keep low 24 (bank preserved).  See STA8long.
      int64_t Imm = Addr.getImm();
      int16_t Lo16 = static_cast<int16_t>(Imm);
      bool IsSext16 =
          (Imm == static_cast<int64_t>(Lo16)) ||
          (static_cast<uint64_t>(Imm) ==
           static_cast<uint64_t>(static_cast<uint32_t>(static_cast<int32_t>(Lo16))));
      Addr = MCOperand::createImm(
          IsSext16 ? (static_cast<uint64_t>(Imm) & 0xFFFFu)
                   : (static_cast<uint64_t>(Imm) & 0xFFFFFFu));
    }
    emitSepM();
    EmitToStreamer(*OutStreamer,
        MCInstBuilder(IsLong ? W65816::LDA_Long : W65816::LDA_Abs)
            .addOperand(Addr));
    emitRepM();
    return;
  }
  case W65816::LDA8absX: {
    // i8 indexed-global load: SEP #0x20 ; LDA <addr>, X ; REP #0x20
    // X holds the index (set up by CopyToReg before this MI).
    // A `.far*` global routes through LDA_LongX (0xBF) for explicit
    // 24-bit bank addressing (Phase B).
    const MachineOperand &AddrMO = MI->getOperand(0);
    bool IsFar = isFarGlobalOperand(AddrMO);
    emitSepM();
    EmitToStreamer(*OutStreamer,
        MCInstBuilder(IsFar ? W65816::LDA_LongX : W65816::LDA_AbsX)
            .addOperand(lowerOperand(AddrMO, MCInstLowering)));
    emitRepM();
    return;
  }
  case W65816::STA8absX: {
    // i8 indexed-global store: SEP #0x20 ; STA <addr>, X ; REP #0x20
    // A holds the value, X holds the index.  `.far*` global routes
    // through STA_LongX (0x9F) for explicit 24-bit bank addressing.
    const MachineOperand &AddrMO = MI->getOperand(0);
    bool IsFar = isFarGlobalOperand(AddrMO);
    emitSepM();
    EmitToStreamer(*OutStreamer,
        MCInstBuilder(IsFar ? W65816::STA_LongX : W65816::STA_AbsX)
            .addOperand(lowerOperand(AddrMO, MCInstLowering)));
    emitRepM();
    return;
  }
  case W65816::STA8abs:
  case W65816::STA8long: {
    // STA_Abs / STA_Long are 16-bit when M=0, 8-bit when M=1.  Pure-i8
    // functions run with M=1 and a bare STA is correct.  M=0 functions
    // need an SEP/REP wrap so the STA stores only one byte — without
    // it, the store clobbers the byte at addr+1.  STA8long differs
    // from STA8abs only in the underlying opcode (0x8F vs 0x8D): long
    // is bank-explicit, abs is DBR-relative.  Long is required for
    // const-int MMIO addresses since the data bank is non-zero under
    // GS/OS Loader.  Far globals (`.far*` section) also route through
    // STA_Long for explicit bank addressing (Phase B).
    bool IsLong = MI->getOpcode() == W65816::STA8long ||
                  isFarGlobalOperand(MI->getOperand(1));
    bool UsesAcc8 = MI->getMF()
                        ->getInfo<W65816MachineFunctionInfo>()
                        ->getUsesAcc8();
    // SkipOpenSep: LDAi8imm collapse already left M=8, so skip our
    // opening SEP — but we still own the closing REP since LDAi8imm
    // dropped its.
    bool SkipOpenSep = SkipNextSta8Wrap;
    SkipNextSta8Wrap = false;
    if (!UsesAcc8 && !SkipOpenSep) {
      emitSepM();
    }
    MCOperand Addr = lowerOperand(MI->getOperand(1), MCInstLowering);
    // STA_Long takes a 24-bit absolute address.  Two cases reach here as
    // an immediate:
    //  (a) a const-int cast through a 16-bit pointer, which TableGen
    //      SIGN-EXTENDS into the wider imm (0xC035 -> ...FFFFC035); the
    //      intended bank is 0, so recover the low 16 bits.
    //  (b) a genuine >16-bit constant address such as 0xE12000 (a fixed
    //      hardware register in bank $E1); this MUST keep its bank byte.
    // Distinguish by the sign-extension round-trip: if the value equals
    // the sign-extension of its low 16 bits it is case (a) -> low 16;
    // otherwise it is case (b) -> keep the low 24 (bank included), dropping
    // any above-bit-23 garbage.  The check tolerates both i32-in-i64 and
    // full-i64 sign-extended representations of the immediate.
    if (IsLong && Addr.isImm()) {
      int64_t Imm = Addr.getImm();
      int16_t Lo16 = static_cast<int16_t>(Imm);
      bool IsSext16 =
          (Imm == static_cast<int64_t>(Lo16)) ||
          (static_cast<uint64_t>(Imm) ==
           static_cast<uint64_t>(static_cast<uint32_t>(static_cast<int32_t>(Lo16))));
      Addr = MCOperand::createImm(
          IsSext16 ? (static_cast<uint64_t>(Imm) & 0xFFFFu)
                   : (static_cast<uint64_t>(Imm) & 0xFFFFFFu));
    }
    EmitToStreamer(*OutStreamer,
        MCInstBuilder(IsLong ? W65816::STA_Long : W65816::STA_Abs)
            .addOperand(Addr));
    if (!UsesAcc8) {
      emitRepM();
    }
    return;
  }
  case W65816::ADCabs:
  case W65816::SBCabs: {
    bool IsSub = MI->getOpcode() == W65816::SBCabs;
    emitOp(IsSub ? W65816::SEC : W65816::CLC);
    bool IsFar = isFarGlobalOperand(MI->getOperand(2));
    unsigned Mc = IsSub
        ? (IsFar ? W65816::SBC_Long : W65816::SBC_Abs)
        : (IsFar ? W65816::ADC_Long : W65816::ADC_Abs);
    EmitToStreamer(*OutStreamer,
        MCInstBuilder(Mc)
            .addOperand(lowerOperand(MI->getOperand(2), MCInstLowering)));
    return;
  }
  case W65816::ADCEabs:
  case W65816::SBCEabs: {
    // Chained variant — no CLC/SEC prefix.
    bool IsSub = MI->getOpcode() == W65816::SBCEabs;
    bool IsFar = isFarGlobalOperand(MI->getOperand(2));
    unsigned Mc = IsSub
        ? (IsFar ? W65816::SBC_Long : W65816::SBC_Abs)
        : (IsFar ? W65816::ADC_Long : W65816::ADC_Abs);
    EmitToStreamer(*OutStreamer,
        MCInstBuilder(Mc)
            .addOperand(lowerOperand(MI->getOperand(2), MCInstLowering)));
    return;
  }
  case W65816::CMPi16imm: {
    // CMPi16imm has (outs), (ins Acc16:$lhs, i16imm:$rhs); MC needs only
    // the immediate.
    EmitToStreamer(*OutStreamer,
        MCInstBuilder(W65816::CMP_Imm16)
            .addOperand(lowerOperand(MI->getOperand(1), MCInstLowering)));
    return;
  }
  case W65816::CMPi8imm: {
    // i8 immediate compare — needs M=1 so the CPU only reads 1 byte
    // for the immediate.  See LDAi8imm comment for the wrap rationale.
    emitSepM();
    emitOpImm(W65816::CMP_Imm8, MI->getOperand(1).getImm() & 0xFF);
    emitRepM();
    return;
  }
  case W65816::CMPabs: {
    bool IsFar = isFarGlobalOperand(MI->getOperand(1));
    EmitToStreamer(*OutStreamer,
        MCInstBuilder(IsFar ? W65816::CMP_Long : W65816::CMP_Abs)
            .addOperand(lowerOperand(MI->getOperand(1), MCInstLowering)));
    return;
  }
  // Bitwise immediate / memory pseudos: simple opcode swap, no carry
  // prefix.  Operand 0 = $dst (output, tied), 1 = $src (use), 2 = imm/addr.
  case W65816::ANDi16imm:
  case W65816::ORAi16imm:
  case W65816::EORi16imm: {
    unsigned mc = 0;
    switch (MI->getOpcode()) {
    case W65816::ANDi16imm: mc = W65816::AND_Imm16; break;
    case W65816::ORAi16imm: mc = W65816::ORA_Imm16; break;
    case W65816::EORi16imm: mc = W65816::EOR_Imm16; break;
    }
    EmitToStreamer(*OutStreamer,
        MCInstBuilder(mc).addOperand(
            lowerOperand(MI->getOperand(2), MCInstLowering)));
    return;
  }
  case W65816::ANDabs:
  case W65816::ORAabs:
  case W65816::EORabs: {
    bool IsFar = isFarGlobalOperand(MI->getOperand(2));
    unsigned mc = 0;
    switch (MI->getOpcode()) {
    case W65816::ANDabs: mc = IsFar ? W65816::AND_Long : W65816::AND_Abs; break;
    case W65816::ORAabs: mc = IsFar ? W65816::ORA_Long : W65816::ORA_Abs; break;
    case W65816::EORabs: mc = IsFar ? W65816::EOR_Long : W65816::EOR_Abs; break;
    }
    EmitToStreamer(*OutStreamer,
        MCInstBuilder(mc).addOperand(
            lowerOperand(MI->getOperand(2), MCInstLowering)));
    return;
  }
  case W65816::JSLpseudo:
  case W65816::JSLpseudo32: {
    EmitToStreamer(*OutStreamer,
        MCInstBuilder(W65816::JSL_Long)
            .addOperand(lowerOperand(MI->getOperand(0), MCInstLowering)));
    return;
  }
  case W65816::PUSH16: {
    emitOp(W65816::PHA);
    return;
  }
  case W65816::BRINDpseudo: {
    // BRIND (computed-goto / indirect-branch terminator).  The target's
    // 16-bit offset was pre-stored to $00B8 (the shared __indirTarget
    // slot, see libgcc.s) by LowerBRIND's chained store.  Emit a single
    // `jmp ($00B8)` (opcode 0x6C, JMP_AbsInd) — bank-0 vector fetch is
    // unconditional on the 65816, so this dispatches correctly even
    // when the program's segment is placed in a non-zero bank by the
    // GS/OS Loader.
    emitOpImm(W65816::JMP_AbsInd, kRuntimeIndirTargetDP);
    return;
  }
  case W65816::BRK_pseudo: {
    // ISD::TRAP / __builtin_trap() / -fsanitize-trap=undefined.
    // Three-part expansion:
    //   1. Materialise the 0xBE sentinel into A (matches the existing
    //      crt0 $BE-stash convention: high byte = bank pad, low byte =
    //      trap marker).  We're in M=16 (default ABI) so the LDA is a
    //      2-byte word write but only the low byte at $70 is the meaningful
    //      sentinel — $71 lands in DP scratch and is harmless.  Sanitizers
    //      that want byte-precise marking can SEP/REP-wrap if it ever
    //      matters; today the $70-marker probe convention reads a single
    //      byte so this is fine.
    //   2. BRK #$00.  Some emulators (raw 65816 cores, e.g. snes9x debug)
    //      vector cleanly through $00FFE6 and a host-side handler can
    //      observe the trap.  Headless MAME's apple2gs mis-vectors BRK to
    //      $0000 and wild-jumps — see crt0.s halt comment — so we cannot
    //      rely on BRK alone to actually stop execution.
    //   3. BRA .self — tight loop.  This is the actual halt for headless
    //      MAME and the general bare-metal case.  Mirrors crt0.s's
    //      `.Lhalt: bra .Lhalt` convention.  Under -debug, MAME's debugger
    //      will see the spin and can step out; under no-debug it idles
    //      forever (IRQs masked by crt0).
    MCSymbol *HaltSym = OutContext.createTempSymbol("trap_halt");
    EmitToStreamer(*OutStreamer,
        MCInstBuilder(W65816::LDA_Imm16).addImm(kBrkPseudoSentinel));
    EmitToStreamer(*OutStreamer,
        MCInstBuilder(W65816::STA_DP).addImm(kTrapStatusDP));
    EmitToStreamer(*OutStreamer, MCInstBuilder(W65816::BRK).addImm(0));
    OutStreamer->emitLabel(HaltSym);
    EmitToStreamer(*OutStreamer,
        MCInstBuilder(W65816::BRA).addExpr(
            MCSymbolRefExpr::create(HaltSym, OutContext)));
    return;
  }
  case W65816::ALLOCAfi: {
    // VLA / dynamic_stackalloc: A holds size on entry; on exit A holds
    // pointer to the allocated region.
    //   TSC                ; A = SP
    //   SEC                ; clear borrow
    //   SBC size  (in $E0) ; A = SP - size
    //   TCS                ; SP = A
    //   INC A              ; A = SP + 1, the lowest byte of the region
    // Size is in A on entry — but we need A=SP after TSC, so first
    // stash the size to DP scratch.
    emitOpImm(W65816::STA_DP, kDpScratch0);
    emitOp(W65816::TSC);
    emitOp(W65816::SEC);
    emitOpImm(W65816::SBC_DP, kDpScratch0);
    emitOp(W65816::TCS);
    emitOp(W65816::INA);
    return;
  }
  case W65816::PUSH16X: {
    emitOp(W65816::PHX);
    return;
  }
  case W65816::ASLA16:
  case W65816::ASLA8: {
    emitOp(W65816::ASL_A);
    return;
  }
  case W65816::LSRA16:
  case W65816::LSRA8: {
    emitOp(W65816::LSR_A);
    return;
  }
  case W65816::ASRA16: {
    // PHA ; ASL A (sets carry from sign bit) ; PLA ; ROR A
    emitOp(W65816::PHA);
    emitOp(W65816::ASL_A);
    emitOp(W65816::PLA);
    emitOp(W65816::ROR_A);
    return;
  }
  case W65816::XBA16: {
    emitOp(W65816::XBA);
    return;
  }
  case W65816::INA_PSEUDO:
  case W65816::INA_PSEUDO8: {
    emitOp(W65816::INA);
    return;
  }
  case W65816::DEA_PSEUDO:
  case W65816::DEA_PSEUDO8: {
    emitOp(W65816::DEA);
    return;
  }
  case W65816::NEGA16: {
    // EOR #$FFFF; INC A.
    emitOpImm(W65816::EOR_Imm16, 0xFFFF);
    emitOp(W65816::INA);
    return;
  }
  case W65816::NEGA8: {
    // EOR #$FF; INC A — same idea as NEGA16 but in 8-bit M.
    // The function context is already 8-bit M when an i8-only path
    // is selected, so no SEP/REP wrap is needed here.
    emitOpImm(W65816::EOR_Imm8, 0xFF);
    emitOp(W65816::INA);
    return;
  }
  case W65816::NEGC16: {
    // (subc 0, x) — lo half of multi-precision negate.
    // EOR #$FFFF; CLC; ADC #1.  C-out = 1 iff result = 0 (i.e. x was 0),
    // matching SBC's "no borrow" convention.
    emitOpImm(W65816::EOR_Imm16, 0xFFFF);
    emitOp(W65816::CLC);
    emitOpImm(W65816::ADC_Imm16, 1);
    return;
  }
  case W65816::SRL15A: {
    // ASL A; LDA #0; ROL A — extract bit 15 to bit 0.
    emitOp(W65816::ASL_A);
    emitOpImm(W65816::LDA_Imm16, 0);
    emitOp(W65816::ROL_A);
    return;
  }
  case W65816::SHL15A: {
    // LSR A; LDA #0; ROR A — move bit 0 to bit 15.
    emitOp(W65816::LSR_A);
    emitOpImm(W65816::LDA_Imm16, 0);
    emitOp(W65816::ROR_A);
    return;
  }
  case W65816::SRL8A: {
    // XBA; AND #$00FF — high byte to low byte, zero high.
    emitOp(W65816::XBA);
    emitOpImm(W65816::AND_Imm16, 0x00FF);
    return;
  }
  case W65816::SHL8A: {
    // XBA; AND #$FF00 — low byte to high byte, zero low.
    emitOp(W65816::XBA);
    emitOpImm(W65816::AND_Imm16, 0xFF00);
    return;
  }
  case W65816::SRA15A: {
    // ASL A; LDA #0; ADC #-1; EOR #-1 — sign-fill from bit 15.
    // ASL: C = bit 15 of input (the sign).
    // LDA #0: A = 0, C unchanged.
    // ADC #-1: A = 0 + (-1) + C = -1 + C.  If C=1 (neg): A = 0; if
    //   C=0 (pos): A = -1.  Inverted from what we want.
    // EOR #-1: flip bits — A = -1 (neg) or 0 (pos), correct.
    emitOp(W65816::ASL_A);
    emitOpImm(W65816::LDA_Imm16, 0);
    emitOpImm(W65816::ADC_Imm16, 0xFFFF);
    emitOpImm(W65816::EOR_Imm16, 0xFFFF);
    return;
  }
  case W65816::NEGE16: {
    // (sube 0, x) — hi half of multi-precision negate.
    // EOR #$FFFF; ADC #0.  Carry-in from the previous subc/sube is
    // already in P; ADC #0 propagates it as ~x + C, which matches
    // 0 - x - !C in two's complement.
    emitOpImm(W65816::EOR_Imm16, 0xFFFF);
    emitOpImm(W65816::ADC_Imm16, 0);
    return;
  }
  }

  MCInst TmpInst;
  MCInstLowering.Lower(MI, TmpInst);
  EmitToStreamer(*OutStreamer, TmpInst);
}

char W65816AsmPrinter::ID = 0;

INITIALIZE_PASS(W65816AsmPrinter, "w65816-asm-printer",
                "W65816 Assembly Printer", false, false)

extern "C" LLVM_ABI LLVM_EXTERNAL_VISIBILITY void
LLVMInitializeW65816AsmPrinter() {
  RegisterAsmPrinter<W65816AsmPrinter> X(getTheW65816Target());
}
