//===-- W65816FrameLowering.cpp - W65816 Frame Information ----------------===//
//
// 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
//
//===----------------------------------------------------------------------===//
//
// Skeleton frame lowering.  The 65816 stack grows downward and has a 1-byte
// alignment; prologue and epilogue emission will be implemented alongside
// the full calling convention.
//
//===----------------------------------------------------------------------===//

#include "W65816FrameLowering.h"
#include "W65816InstrInfo.h"
#include "W65816MachineFunctionInfo.h"
#include "W65816Subtarget.h"
#include "llvm/CodeGen/MachineFrameInfo.h"
#include "llvm/CodeGen/MachineFunction.h"
#include "llvm/CodeGen/MachineInstrBuilder.h"
#include "llvm/CodeGen/MachineRegisterInfo.h"
#include "llvm/IR/Function.h"
#include "llvm/Support/CommandLine.h"
#include "llvm/Support/ErrorHandling.h"

using namespace llvm;

// Omit the per-function-entry `REP #$30`.  Every function is required by the
// C ABI to be entered in M=16/X=16, and the whole toolchain already upholds
// that invariant: crt0 sets it at startup, compiled code never makes a call
// or a return while in 8-bit mode (8-bit ops are short, block-local
// SEP/REP-wrapped windows that contain no calls), and the hand-written
// runtime relies on it too — libgcc's __mulhi3 / __umulhisi3 / __udivhi3 /
// __lshrsi3 carry NO entry REP and would already miscompile if any caller
// were in 8-bit mode.  Given that, the entry REP is redundant; dropping it
// saves 2 bytes + 3 cycles per function.  Default OFF: it is only sound if
// the ENTIRE linked program (including every hand-written asm entry point and
// any interrupt handler) maintains the invariant, so it must be a deliberate
// whole-program choice.
//
// Investigation (2026-06-30): measured gain is ~0.15% .text and ~3 cyc/call
// (this is the fib 1.06x-Calypsi case; the win is broad but small since it is
// only the per-call entry REP).  A whole-toolchain smoke build with this forced
// on was blocked only by asm-grep regression guards that assert the REP's
// presence as a correctness proxy (e.g. the sgnlt i8-signed-compare guard) --
// no runtime miscompile was reached, consistent with the invariant holding.
// This flag has NO per-function safety net: it trusts the invariant globally.
// Making the elision safe by default needs the design-doc 3.3 REP/SEP dataflow
// pass, which would prove per-function that mode is 16-bit at every call and
// return and keep the REP for anything it cannot prove -- that is the real fix
// and remains deferred (multi-week; the gain does not justify it while the
// geomean already beats Calypsi 0.62x).
static cl::opt<bool> OmitEntryRep(
    "w65816-omit-entry-rep",
    cl::desc("Omit the per-function-entry REP #$30 (relies on the whole-program "
             "M=16/X=16 ABI invariant; saves 2 bytes / 3 cycles per function)."),
    cl::init(false), cl::Hidden);

// (The pure-i8-detection helpers were removed when the prologue went
// to "always 16-bit M".  See emitPrologue comment.)
//
// (DBR-zero wrap was prototyped here — PHB at function entry to save
// caller's DBR, set DBR=0, restore at exit.  Two issues blocked it:
// (a) saving DBR to a DP slot ($F2/$F3) conflicts with libgcc's
// muldi3/divdi3 scratch — those routines use $F2..$F8 freely, so
// the saved DBR doesn't survive a libcall in the function body.
// (b) saving via PHB shifts SP, which means LowerFormalArguments
// would need to bump every arg's StackOffset by 1 — but at
// LowerFormalArguments time we don't know yet whether the function
// will need the wrap (indirect-Y emission is a later lowering
// choice).  Right approach is a per-function attribute the user
// opts into, plus PEI integration to add a fixed-size "saved DBR"
// slot.  Deferred — see STATUS.md.)

W65816FrameLowering::W65816FrameLowering(const W65816Subtarget &STI)
    : TargetFrameLowering(TargetFrameLowering::StackGrowsDown, Align(1), 0,
                          Align(1)) {}

bool W65816FrameLowering::hasFPImpl(const MachineFunction &MF) const {
  // The 65816 has no native frame pointer — direct page would be the
  // logical home for one but we don't reserve it.  Return false so
  // every function uses pure stack-relative addressing for locals.
  // Variable-size objects (alloca with non-constant size) and
  // frameaddress() intrinsics will need a real FP eventually.
  return false;
}

bool W65816FrameLowering::hasReservedCallFrame(const MachineFunction &MF) const {
  // Returning false is required for correctness: LowerCall pushes
  // outgoing args via PUSH16 (PHA), which incrementally shifts SP
  // between ADJCALLSTACKDOWN and ADJCALLSTACKUP.  With a reserved
  // call frame, PEI assumes SP is stable across calls and bakes
  // FrameOffset+StackSize into LDA_StackRel.  Then any FI access
  // that the scheduler interleaves with pushed args (e.g. loading
  // a *later* arg from the caller's frame to push it) reads from
  // the wrong offset — silently miscompiling 2+ arg libcalls.
  // hasReservedCallFrame=false makes PEI add the DOWN-amount to
  // FI offsets between ADJCALLSTACKDOWN and ADJCALLSTACKUP,
  // recovering correctness.
  return false;
}

void W65816FrameLowering::determineCalleeSaves(MachineFunction &MF,
                                               BitVector &SavedRegs,
                                               RegScavenger *RS) const {
  TargetFrameLowering::determineCalleeSaves(MF, SavedRegs, RS);
  // A VLA function caches its entry SP in DP $F4/$F5, which the epilogue
  // uses to unwind the (statically-unknown) VLA allocation and which the
  // post-call $F6 recompute derives from.  $F4 is a single global DP
  // location: a nested VLA callee overwrites it in its own prologue and
  // never restores it, so without a callee-side save/restore the caller's
  // $F4 is stale after the call -> wrong SP unwind + wrong $F6.  Reserve a
  // 2-byte frame slot here (before frame-object offsets are finalized) so
  // PEI includes it in the frame; emitPrologue/emitEpilogue fill and
  // restore it.  Only VLA functions pay this.
  if (MF.getFrameInfo().hasVarSizedObjects()) {
    int FI = MF.getFrameInfo().CreateStackObject(2, Align(2),
                                                 /*isSpillSlot=*/false);
    MF.getInfo<W65816MachineFunctionInfo>()->setF4SaveFrameIndex(FI);
  }
}

void W65816FrameLowering::emitPrologue(MachineFunction &MF,
                                       MachineBasicBlock &MBB) const {
  // Pick canonical mode based on whether the function uses any 8-bit
  // accumulator values.  Pure-i16 functions get REP #$30 (16-bit M+X);
  // any-i8 functions get REP #$10 + SEP #$20 (16-bit X, 8-bit A).
  // This is a coarse heuristic until the REP/SEP scheduling pass
  // (design doc §3.3) can insert mode transitions per-region.
  const W65816Subtarget &STI = MF.getSubtarget<W65816Subtarget>();
  const W65816InstrInfo &TII = *STI.getInstrInfo();
  const MachineRegisterInfo &MRI = MF.getRegInfo();
  MachineBasicBlock::iterator MBBI = MBB.begin();
  DebugLoc DL;

  // Always enter in 16-bit M+X (REP #$30).  Per-instruction i8 ops wrap
  // themselves with SEP #$20 / REP #$20 in their AsmPrinter expansion;
  // W65816SepRepCleanup coalesces adjacent toggles so back-to-back i8
  // ops collapse into a single SEP/REP region (recovering the byte-
  // heavy "pure-i8" prologue's efficiency without its hazards).
  //
  // The earlier "pure-i8" heuristic (REP #$10 + SEP #$20 prologue) was
  // a silent miscompile: late-stage i8→i16 sign extension and any other
  // i16 op the back-end emits *without* a wrap — `and #$ff`, `eor #$80`,
  // `adc #$ff80`, etc. — would assemble as 3-byte i16 immediates but
  // execute in M=1 where the CPU only reads the low byte.  The next
  // immediate byte then becomes the next opcode (often $00 = BRK).
  // Caught by tracing inc_g for `char inc_g(void) { g++; return g; }`.
  (void)MRI;
  MF.getInfo<W65816MachineFunctionInfo>()->setUsesAcc8(false);
  // The entry REP guarantees this function runs in 16-bit M/X even if a
  // caller somehow left 8-bit mode set.  Under -w65816-omit-entry-rep the
  // whole program is asserted to already maintain the M=16/X=16 invariant,
  // so this becomes redundant (see the OmitEntryRep comment above).
  if (!OmitEntryRep)
    BuildMI(MBB, MBBI, DL, TII.get(W65816::REP)).addImm(0x30);

  // Reserve stack space for locals/spills.
  //
  // Critical: arg0 lives in A on entry, so the prologue MUST NOT
  // clobber A.  The naive `TSC; SEC; SBC #N; TCS` sequence destroys A
  // (TSC overwrites A with SP) — used to silently corrupt arg0 in
  // every function with a stack frame, until this fix.
  //
  // Strategy (16-bit M):
  //   - Small frames (N <= 14 bytes): use N/2 `PHA` instructions.  PHA
  //     pushes A's value (whatever it is — including arg0) and only
  //     decrements S.  A is not modified.  N/2 bytes of code per call.
  //     Side-effect: the bytes pushed contain copies of arg0; the body's
  //     regalloc-inserted spills may overwrite them, which is fine.
  //   - Larger frames: TAY/TSC/.../TYA — 8 bytes total, preserves A
  //     through Y as a temporary.  Y is caller-saved by our (loose) ABI.
  //
  // Strategy (8-bit M): PHA in 8-bit M pushes 1 byte, so N PHAs for
  // N bytes.  Without this, spills land on top of the return address
  // and corrupt it (was a latent silent crash for 8-bit M functions
  // that needed any spilling).
  uint64_t StackSize = MF.getFrameInfo().getStackSize();
  bool HasVLA = MF.getFrameInfo().hasVarSizedObjects();

  // For VLA functions, save entry SP to DP $F4..$F5 BEFORE any frame
  // allocation so the epilogue can restore it directly (undoing both
  // the static frame and any dynamic_stackalloc bytes).  $F4 is the
  // saved-SP slot; $F0..$F1 is reserved for i64 return high-half;
  // $E0..$EF is libcall scratch.  TAY around the TSC preserves A
  // (which holds arg0).
  if (HasVLA) {
    BuildMI(MBB, MBBI, DL, TII.get(W65816::TAY));    // save A (arg0) in Y
    // Callee-save the caller's $F4 BEFORE we overwrite it below: stash it
    // in $E0 (libcall-scratch DP, free this early -- no calls yet) and
    // flush it to the reserved frame slot once $F6 is set (end of prologue).
    if (MF.getInfo<W65816MachineFunctionInfo>()->getF4SaveFrameIndex() >= 0) {
      BuildMI(MBB, MBBI, DL, TII.get(W65816::LDA_DP)).addImm(0xF4);
      BuildMI(MBB, MBBI, DL, TII.get(W65816::STA_DP)).addImm(0xE0);
    }
    BuildMI(MBB, MBBI, DL, TII.get(W65816::TSC));    // A = SP
    BuildMI(MBB, MBBI, DL, TII.get(W65816::STA_DP)).addImm(0xF4);
    BuildMI(MBB, MBBI, DL, TII.get(W65816::TYA));    // restore A (arg0)
  }

  if (StackSize > 0) {
    // Cycle math: each PHA is 4 cyc; the TSC-sequence (TAY+TSC+SEC+
    // SBC+TCS+TYA) is 13 cyc total.  N PHAs win on cycles when 4*N <= 13,
    // i.e. up to 3 PHAs (6-byte frame).  At N=4 (8 bytes): 16 cyc PHAs vs
    // 13 cyc TSC-seq → TSC wins.  Threshold at 6 bytes for speed.
    // (Bytes: N PHAs cost N bytes; TSC-seq costs 8 bytes.  We're
    // optimizing for speed per the project directive.)
    if (StackSize <= 6 && (StackSize % 2) == 0) {
      // Small frame: N/2 PHAs.  Preserves A.
      for (uint64_t i = 0; i < StackSize / 2; ++i)
        BuildMI(MBB, MBBI, DL, TII.get(W65816::PHA));
    } else {
      // Larger frame: TAY/TSC/.../TYA bracket.  Preserves A via Y as a
      // temp.
      BuildMI(MBB, MBBI, DL, TII.get(W65816::TAY));
      BuildMI(MBB, MBBI, DL, TII.get(W65816::TSC));
      BuildMI(MBB, MBBI, DL, TII.get(W65816::SEC));
      BuildMI(MBB, MBBI, DL, TII.get(W65816::SBC_Imm16))
          .addImm(StackSize);
      BuildMI(MBB, MBBI, DL, TII.get(W65816::TCS));
      // Frames > 256 bytes can't be addressed via 8-bit `,S` displacement.
      // Capture the post-allocation `S` into $F6/$F7 as a 16-bit DP frame
      // pointer; eliminateFrameIndex routes far accesses through
      // `LDA/STA ($F6),Y` (bank-0 implicit, since the stack is always
      // bank 0).  A holds the new S right after TCS — store it before
      // restoring A from Y.
      // Capture FP into $F6 when:
      //  - frame > 200 bytes (8-bit `,S` disp can't reach far slots), OR
      //  - function has VLAs (DYNAMIC_STACKALLOC shifts S, breaking
      //    static-frame `,s` access).
      if (StackSize > 200 || HasVLA) {
        MF.getInfo<W65816MachineFunctionInfo>()->setUsesDpFP(true);
        BuildMI(MBB, MBBI, DL, TII.get(W65816::STA_DP)).addImm(0xF6);
        // Bank byte at $F8 = 0.  expandFarFI uses `LDA/STA [dp],Y`
        // (long indirect Y, opcodes B7/97) which reads a 24-bit
        // pointer at $F6/$F7/$F8 and ignores DBR.  Without this
        // forced-bank-zero, callers that have switched DBR (e.g.
        // for I/O register access via `pha;plb`) silently corrupt
        // every FP-relative load and store in the callee.
        // sha256_transform exposed this — its 246-byte frame uses
        // FP-rel, and the test driver switched DBR to bank 2 for
        // probe writes before invoking it.
        BuildMI(MBB, MBBI, DL, TII.get(W65816::STZ_DP)).addImm(0xF8);
      }
      BuildMI(MBB, MBBI, DL, TII.get(W65816::TYA));
    }
  }
  // VLA function with no static frame (or PHA-only): still need FP.
  if (HasVLA &&
      !MF.getInfo<W65816MachineFunctionInfo>()->getUsesDpFP()) {
    MF.getInfo<W65816MachineFunctionInfo>()->setUsesDpFP(true);
    BuildMI(MBB, MBBI, DL, TII.get(W65816::TAY));
    BuildMI(MBB, MBBI, DL, TII.get(W65816::TSC));
    BuildMI(MBB, MBBI, DL, TII.get(W65816::STA_DP)).addImm(0xF6);
    BuildMI(MBB, MBBI, DL, TII.get(W65816::STZ_DP)).addImm(0xF8);
    BuildMI(MBB, MBBI, DL, TII.get(W65816::TYA));
  }

  // Flush the caller's $F4 (stashed in $E0 above) to its reserved frame
  // slot, now that $F6 (static base) is established.  Address the slot via
  // ($F6),Y exactly like eliminateFrameIndex's far path so the disp is
  // correct for any frame size.  ($F6),Y is SP-independent, so the PHA that
  // preserves arg0 does not perturb it.
  {
    int F4SaveFI =
        MF.getInfo<W65816MachineFunctionInfo>()->getF4SaveFrameIndex();
    if (HasVLA && F4SaveFI >= 0) {
      const MachineFrameInfo &MFI = MF.getFrameInfo();
      int FrameOffset = MFI.getObjectOffset(F4SaveFI);
      int FPOff = FrameOffset + (int)MFI.getStackSize();
      if (FrameOffset < 0)
        FPOff += 1;
      BuildMI(MBB, MBBI, DL, TII.get(W65816::PHA));                  // save arg0
      BuildMI(MBB, MBBI, DL, TII.get(W65816::LDA_DP)).addImm(0xE0);  // A = caller $F4
      BuildMI(MBB, MBBI, DL, TII.get(W65816::STY_DP)).addImm(0xFA)
          .addReg(W65816::Y, RegState::Implicit | RegState::Undef);
      BuildMI(MBB, MBBI, DL, TII.get(W65816::LDY_Imm16)).addImm(FPOff);
      BuildMI(MBB, MBBI, DL, TII.get(W65816::STA_DPIndLongY))
          .addImm(0xF6)
          .addReg(W65816::A, RegState::Implicit)
          .addReg(W65816::Y, RegState::Implicit);
      BuildMI(MBB, MBBI, DL, TII.get(W65816::LDY_DP)).addImm(0xFA);
      BuildMI(MBB, MBBI, DL, TII.get(W65816::PLA))
          .addReg(W65816::A, RegState::ImplicitDefine);              // restore arg0
    }
  }
}

void W65816FrameLowering::emitEpilogue(MachineFunction &MF,
                                       MachineBasicBlock &MBB) const {
  // Mirror image of the prologue: release any reserved frame bytes
  // before the RTL.
  uint64_t StackSize = MF.getFrameInfo().getStackSize();
  bool HasVLA = MF.getFrameInfo().hasVarSizedObjects();

  const W65816Subtarget &STI = MF.getSubtarget<W65816Subtarget>();
  const W65816InstrInfo &TII = *STI.getInstrInfo();
  MachineBasicBlock::iterator MBBI = MBB.getLastNonDebugInstr();
  // Insert before the terminator (the return).
  DebugLoc DL = MBBI != MBB.end() ? MBBI->getDebugLoc() : DebugLoc();

  if (StackSize == 0 && !HasVLA)
    return;

  // Detect whether the return live-out includes Y or X — for i64 returns
  // (Outs[0..2] -> A,X,Y), Y holds bits 32-47 and X holds bits 16-31, so
  // any TAY/PLY/TAX in the SP-restore would corrupt the return value.
  // The RTL terminator carries implicit-uses for every live-out return
  // register; scan them to decide which scratch we can use safely.
  bool YLive = false;
  bool XLive = false;
  if (MBBI != MBB.end() && MBBI->isReturn()) {
    for (const MachineOperand &MO : MBBI->operands()) {
      if (!MO.isReg() || !MO.isImplicit() || !MO.isUse()) continue;
      if (MO.getReg() == W65816::Y) YLive = true;
      else if (MO.getReg() == W65816::X) XLive = true;
    }
  }

  // VLA cleanup: restore entry SP from DP $F4 (saved in prologue).
  // This subsumes BOTH the static frame and any dynamic_stackalloc
  // bytes — we can skip the per-byte PLY/PLA loop entirely.  Preserve
  // A through TAY/TYA since it holds the return value.  For i64
  // returns where Y is also live, route the save through DP $E0
  // ($E0..$EF is libcall scratch — guaranteed dead by epilogue time).
  if (HasVLA) {
    int F4SaveFI =
        MF.getInfo<W65816MachineFunctionInfo>()->getF4SaveFrameIndex();
    if (F4SaveFI >= 0) {
      // Restore the caller's $F4 (a nested VLA call clobbered it), then
      // return.  Sequence:
      //   1) stash the return value A in $E2 -- X and Y are left untouched
      //      so i64/double returns survive without a YLive special case;
      //   2) unwind SP from THIS function's $F4 (its entry SP) -- must run
      //      BEFORE $F4 is overwritten;
      //   3) read the caller's saved $F4 from its slot via ($F6),Y ($F6 is
      //      valid here -- recomputed after every call, untouched by TCS,
      //      and the slot bytes below SP are intact until the RTL) and
      //      write it back to $F4;
      //   4) reload the return value.
      const MachineFrameInfo &MFI = MF.getFrameInfo();
      int FrameOffset = MFI.getObjectOffset(F4SaveFI);
      int FPOff = FrameOffset + (int)StackSize;
      if (FrameOffset < 0)
        FPOff += 1;
      BuildMI(MBB, MBBI, DL, TII.get(W65816::STA_DP)).addImm(0xE2);  // $E2 = ret A
      BuildMI(MBB, MBBI, DL, TII.get(W65816::LDA_DP)).addImm(0xF4);  // A = entry SP
      BuildMI(MBB, MBBI, DL, TII.get(W65816::TCS));                  // SP = entry SP
      BuildMI(MBB, MBBI, DL, TII.get(W65816::STY_DP)).addImm(0xFA)
          .addReg(W65816::Y, RegState::Implicit | RegState::Undef);
      BuildMI(MBB, MBBI, DL, TII.get(W65816::LDY_Imm16)).addImm(FPOff);
      BuildMI(MBB, MBBI, DL, TII.get(W65816::LDA_DPIndLongY)).addImm(0xF6)
          .addReg(W65816::A, RegState::ImplicitDefine)
          .addReg(W65816::Y, RegState::Implicit);
      BuildMI(MBB, MBBI, DL, TII.get(W65816::LDY_DP)).addImm(0xFA);
      BuildMI(MBB, MBBI, DL, TII.get(W65816::STA_DP)).addImm(0xF4);  // $F4 = caller val
      BuildMI(MBB, MBBI, DL, TII.get(W65816::LDA_DP)).addImm(0xE2);  // A = ret
      return;
    }
    // Leaf VLA (no reserved slot): $F4 still holds this function's entry SP.
    if (YLive) {
      BuildMI(MBB, MBBI, DL, TII.get(W65816::STA_DP)).addImm(0xE0);
      BuildMI(MBB, MBBI, DL, TII.get(W65816::LDA_DP)).addImm(0xF4);
      BuildMI(MBB, MBBI, DL, TII.get(W65816::TCS));
      BuildMI(MBB, MBBI, DL, TII.get(W65816::LDA_DP)).addImm(0xE0);
    } else {
      BuildMI(MBB, MBBI, DL, TII.get(W65816::TAY));
      BuildMI(MBB, MBBI, DL, TII.get(W65816::LDA_DP)).addImm(0xF4);
      BuildMI(MBB, MBBI, DL, TII.get(W65816::TCS));
      BuildMI(MBB, MBBI, DL, TII.get(W65816::TYA));
    }
    return;
  }

  // Prologue is always 16-bit M now (see emitPrologue).  No 8-bit
  // epilogue branch needed.

  // 16-bit M epilogue.  Mirror the prologue: A holds the return value
  // at this point and MUST be preserved.  Small frames release via
  // N/2 PLY (pop into Y, discard); larger frames use
  // TAY/TSC/CLC/ADC #N/TCS/TYA.
  // Mirror the prologue threshold (see comment there).
  if (StackSize <= 6 && (StackSize % 2) == 0 && !YLive) {
    // PLY clobbers Y, which is fine when Y isn't a return reg.
    for (uint64_t i = 0; i < StackSize / 2; ++i)
      BuildMI(MBB, MBBI, DL, TII.get(W65816::PLY));
    return;
  }
  if (YLive) {
    // Y is a return register (i64 / double).  Save A via DP $E0
    // instead of TAY so Y survives.  4 cyc slower than TAY/TYA but
    // correct.  X is allowed to be live too — none of these touch X.
    BuildMI(MBB, MBBI, DL, TII.get(W65816::STA_DP)).addImm(0xE0);
    BuildMI(MBB, MBBI, DL, TII.get(W65816::TSC));
    BuildMI(MBB, MBBI, DL, TII.get(W65816::CLC));
    BuildMI(MBB, MBBI, DL, TII.get(W65816::ADC_Imm16))
        .addImm(StackSize);
    BuildMI(MBB, MBBI, DL, TII.get(W65816::TCS));
    BuildMI(MBB, MBBI, DL, TII.get(W65816::LDA_DP)).addImm(0xE0);
    (void)XLive;
    return;
  }
  BuildMI(MBB, MBBI, DL, TII.get(W65816::TAY));
  BuildMI(MBB, MBBI, DL, TII.get(W65816::TSC));
  BuildMI(MBB, MBBI, DL, TII.get(W65816::CLC));
  BuildMI(MBB, MBBI, DL, TII.get(W65816::ADC_Imm16))
      .addImm(StackSize);
  BuildMI(MBB, MBBI, DL, TII.get(W65816::TCS));
  BuildMI(MBB, MBBI, DL, TII.get(W65816::TYA));
}

MachineBasicBlock::iterator W65816FrameLowering::eliminateCallFramePseudoInstr(
    MachineFunction &MF, MachineBasicBlock &MBB,
    MachineBasicBlock::iterator I) const {
  const W65816Subtarget &STI = MF.getSubtarget<W65816Subtarget>();
  const W65816InstrInfo &TII = *STI.getInstrInfo();

  // ADJCALLSTACKDOWN does nothing — we push args via PUSH16/PHA which
  // implicitly decrements SP, so no separate adjustment is needed.
  // ADJCALLSTACKUP releases all the pushed bytes after a call.
  //
  // Critical: A holds the callee's return value here, so this MUST NOT
  // clobber A.  PLY (small-N path) clobbers Y; TAY/.../TYA bracket
  // (large-N path) also clobbers Y.  Both are fine for i8/i16/i32
  // returns but DESTROY the return for i64/double (where X and Y hold
  // mid halves).  Detect i64-return calls by walking back to the JSL
  // and checking implicit-def $x/$y; in that case, save A via DP $E0
  // (libcall scratch, dead by call-up time) so X and Y survive.
  // Caught by `unsigned long long u64add(a,b)` through a noinline
  // boundary returning Y = b_hi (the last popped) instead of the
  // sum's mid-high.
  if (I->getOpcode() == W65816::ADJCALLSTACKUP) {
    int N = I->getOperand(0).getImm();
    // A function with a large (>200B) frame or VLAs caches its own SP in
    // the DP frame pointer $F6/$F7 (see emitPrologue) for far ($F6],Y)
    // stack access.  $F6 is NOT preserved across calls: any callee with
    // its own large frame overwrites $F6 in its prologue and never
    // restores it.  Because $F6 is merely a cache of SP, we recompute it
    // rather than save/restore it: after every call, re-store SP into
    // $F6 before the next FP-relative access reads a stale base.  Without
    // this, a far-loaded pointer (e.g. jlDataOpen's `mode` param, read
    // FP-relative after an snprintf whose vsnprintf clobbered $F6) arrives
    // as nondeterministic garbage.  This is needed even when N == 0 (a
    // call with no stack args still clobbers $F6).
    bool usesDpFP = MF.getFrameInfo().getStackSize() > 200 ||
                    MF.getFrameInfo().hasVarSizedObjects();
    if (N > 0 || usesDpFP) {
      DebugLoc DL = I->getDebugLoc();
      // Re-store the DP frame pointer $F6 for DpFP functions.  For a
      // plain large frame, $F6 == SP, and A already holds SP at each
      // call site below, so a single STA suffices.  For a VLA function
      // $F6 is the *static* frame base ($F4 entry-SP minus the static
      // frame size) and does NOT track SP once a VLA alloc has lowered
      // it -- recompute it from $F4 instead (clobbers A, which every
      // caller has already stashed in Y or $E0 and restores afterward).
      bool hasVLA = MF.getFrameInfo().hasVarSizedObjects();
      uint64_t frameSize = MF.getFrameInfo().getStackSize();
      auto reestablishFP = [&]() {
        if (!usesDpFP)
          return;
        if (hasVLA) {
          BuildMI(MBB, I, DL, TII.get(W65816::LDA_DP)).addImm(0xF4);
          BuildMI(MBB, I, DL, TII.get(W65816::SEC));
          BuildMI(MBB, I, DL, TII.get(W65816::SBC_Imm16)).addImm(frameSize);
          BuildMI(MBB, I, DL, TII.get(W65816::STA_DP)).addImm(0xF6);
        } else {
          BuildMI(MBB, I, DL, TII.get(W65816::STA_DP)).addImm(0xF6);
        }
      };
      bool YLive = false;
      bool XLive = false;
      // Walk forward looking for COPY %vreg = $x / $y — LowerCall's
      // pattern for materializing return halves.  JSLpseudo's tablegen
      // declares only `Defs=[A]`, so implicit-defs of X/Y aren't on
      // the call op itself.  We have to read what comes after.
      // Stop at the next call (re-clobbers everything) or at any def
      // of X/Y (cancels their post-call value).
      bool Stopped = false;
      for (auto J = std::next(I); J != MBB.end() && !Stopped; ++J) {
        if (J->isCall()) break;
        for (const MachineOperand &MO : J->operands()) {
          if (!MO.isReg()) continue;
          Register R = MO.getReg();
          if (R == W65816::Y) {
            if (MO.isUse()) YLive = true;
            else if (MO.isDef() && !YLive) Stopped = true;
          } else if (R == W65816::X) {
            if (MO.isUse()) XLive = true;
            else if (MO.isDef() && !XLive) Stopped = true;
          }
        }
        if (YLive && XLive) break;
      }
      if (YLive) {
        // i64 return: PLY would eat Y.  Route through DP $E0.  Worth
        // ~4 cyc more than PLY*N/2 but correctness wins.  X is not
        // touched by any of these insns either way, so XLive doesn't
        // change anything here — track it for symmetry.
        BuildMI(MBB, I, DL, TII.get(W65816::STA_DP)).addImm(0xE0);
        BuildMI(MBB, I, DL, TII.get(W65816::TSC));
        if (N > 0) {
          BuildMI(MBB, I, DL, TII.get(W65816::CLC));
          BuildMI(MBB, I, DL, TII.get(W65816::ADC_Imm16)).addImm(N);
          BuildMI(MBB, I, DL, TII.get(W65816::TCS));
        }
        // A holds SP here (frame SP after any arg release): cache it in $F6
        // (or recompute from $F4 for VLA functions).
        reestablishFP();
        BuildMI(MBB, I, DL, TII.get(W65816::LDA_DP)).addImm(0xE0);
        (void)XLive;
      } else if (N > 0 && N <= 14 && (N % 2) == 0 && !usesDpFP) {
        for (int i = 0; i < N / 2; ++i)
          BuildMI(MBB, I, DL, TII.get(W65816::PLY));
      } else {
        BuildMI(MBB, I, DL, TII.get(W65816::TAY));
        BuildMI(MBB, I, DL, TII.get(W65816::TSC));
        if (N > 0) {
          BuildMI(MBB, I, DL, TII.get(W65816::CLC));
          BuildMI(MBB, I, DL, TII.get(W65816::ADC_Imm16)).addImm(N);
          BuildMI(MBB, I, DL, TII.get(W65816::TCS));
        }
        // A holds SP here (frame SP after any arg release): cache it in $F6
        // (or recompute from $F4 for VLA functions).
        reestablishFP();
        BuildMI(MBB, I, DL, TII.get(W65816::TYA));
      }
    }
  }
  return MBB.erase(I);
}
