Code generation fixes.

This commit is contained in:
Scott Duensing 2026-08-25 16:44:08 -05:00
parent d9b8a98da2
commit 035c393500
8 changed files with 289 additions and 16 deletions

View file

@ -1155,6 +1155,26 @@ typedef struct __sFILE {
unsigned short refNum; // GS/OS file reference (kind=GSOS only) unsigned short refNum; // GS/OS file reference (kind=GSOS only)
} FILE; } FILE;
// Diagnostic latch: the last FAILING GS/OS call made by this stdio bridge.
// Zero-cost when nothing fails; lets an app report WHY a write/read died
// (fwrite itself can only say "short") without a debugger on the target.
// __gsosLastOp values: 1=Open 2=Create 3=Read 4=Write 5=SetEOF 6=SetMark
// 7=Close 8=GetEOF 9=GetMark.
u16 __gsosLastErr;
u16 __gsosLastOp;
u16 __gsosLastRefNum;
unsigned long __gsosLastReq;
unsigned long __gsosLastXfer;
static void __gsosLatch(u16 op, u16 err, u16 refNum, unsigned long req, unsigned long xfer) {
__gsosLastOp = op;
__gsosLastErr = err;
__gsosLastRefNum = refNum;
__gsosLastReq = req;
__gsosLastXfer = xfer;
}
// Slots 0-2 are stdin/stdout/stderr; 3..MFS_MAX_FILES-1 are usable FILE* // Slots 0-2 are stdin/stdout/stderr; 3..MFS_MAX_FILES-1 are usable FILE*
// slots (both GS/OS and memory files draw from this table -- see fopen). // slots (both GS/OS and memory files draw from this table -- see fopen).
// At 8 that left only 5 concurrent opens, which starved apps that keep // At 8 that left only 5 concurrent opens, which starved apps that keep
@ -1574,7 +1594,8 @@ FILE *fopen(const char *path, const char *mode) {
// "default values". // "default values".
u16 access = (u16)(wantWrite ? (wantRead ? 3 : 2) : 1); u16 access = (u16)(wantWrite ? (wantRead ? 3 : 2) : 1);
__GsosOpenParm op = { 3, 0, &__gsosPathBuf, access }; __GsosOpenParm op = { 3, 0, &__gsosPathBuf, access };
if (gsosOpen(&op) != 0) return (FILE *)0; u16 orc = gsosOpen(&op);
if (orc != 0) { __gsosLatch(1, orc, 0, 0, 0); return (FILE *)0; }
f->kind = FILE_KIND_GSOS; f->kind = FILE_KIND_GSOS;
f->writable = (u8)(wantWrite ? 1 : 0); f->writable = (u8)(wantWrite ? 1 : 0);
@ -1591,7 +1612,8 @@ FILE *fopen(const char *path, const char *mode) {
if (truncate) { if (truncate) {
// "w" / "w+" — truncate to zero length. // "w" / "w+" — truncate to zero length.
__GsosSetPosRecGS e = { 3, op.refNum, 0, 0 }; __GsosSetPosRecGS e = { 3, op.refNum, 0, 0 };
if (gsosSetEOF(&e) != 0) f->err = 1; u16 erc = gsosSetEOF(&e);
if (erc != 0) { f->err = 1; __gsosLatch(5, erc, op.refNum, 0, 0); }
} }
if (append) { if (append) {
// "a" / "a+" — position at end-of-file. // "a" / "a+" — position at end-of-file.
@ -1630,7 +1652,7 @@ size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream) {
unsigned long got = offset + r.transferCount; unsigned long got = offset + r.transferCount;
if (rc != 0 || r.transferCount < total - offset) { if (rc != 0 || r.transferCount < total - offset) {
stream->eof = 1; stream->eof = 1;
if (rc != 0 && rc != 0x4C) stream->err = 1; // 0x4C = eofErr if (rc != 0 && rc != 0x4C) { stream->err = 1; __gsosLatch(3, rc, stream->refNum, total - offset, r.transferCount); }
} }
return (size_t)(got / size); return (size_t)(got / size);
} }
@ -1678,10 +1700,15 @@ size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream) {
if (!stream->writable) { stream->err = 1; return 0; } if (!stream->writable) { stream->err = 1; return 0; }
unsigned long total = (unsigned long)size * (unsigned long)nmemb; unsigned long total = (unsigned long)size * (unsigned long)nmemb;
__GsosIORecGS r = { 4, stream->refNum, (void *)in, total, 0 }; __GsosIORecGS r = { 4, stream->refNum, (void *)in, total, 0 };
if (gsosWrite(&r) != 0) { u16 wrc = gsosWrite(&r);
if (wrc != 0) {
stream->err = 1; stream->err = 1;
__gsosLatch(4, wrc, stream->refNum, total, r.transferCount);
return (size_t)(r.transferCount / size); return (size_t)(r.transferCount / size);
} }
if (r.transferCount < total) {
__gsosLatch(4, 0, stream->refNum, total, r.transferCount);
}
return (size_t)(r.transferCount / size); return (size_t)(r.transferCount / size);
} }
if (stream->kind != FILE_KIND_MEM) return 0; if (stream->kind != FILE_KIND_MEM) return 0;

View file

@ -1113,11 +1113,27 @@ void W65816AsmPrinter::emitInstruction(const MachineInstr *MI) {
return; return;
} }
case W65816::NEGA8: { case W65816::NEGA8: {
// EOR #$FF; INC A — same idea as NEGA16 but in 8-bit M. // SEP #$20 ; EOR #$FF ; INC A ; REP #$20 — same idea as NEGA16 but
// The function context is already 8-bit M when an i8-only path // through an explicit 8-bit M window, like LDA8absX/STA8absX.
// is selected, so no SEP/REP wrap is needed here. //
// The wrap is REQUIRED, not belt-and-braces. NEGA8 is selected for
// i8 subexpressions inside ordinary 16-bit-M functions (e.g. an i8
// negate feeding a sign-extended array index), and the previous
// unwrapped expansion assumed 8-bit M: in 16-bit M the CPU decodes
// the 2-byte `EOR #imm8` as a 3-byte `EOR #imm16`, swallowing the
// following INA opcode ($1A) as the immediate's high byte. The
// stream re-synchronizes, so nothing crashes — but the +1 of the
// two's-complement negate never executes and -x silently becomes ~x
// (RetroNet's IIgs SipHash corruption: every sealed frame's MAC came
// out one low in the rotate's first peeled iteration).
//
// Safe unconditionally: SepRepCleanup's isMNeutral whitelist does
// not include NEGA8, so no coalesced M=8 window ever spans this
// pseudo — the ambient mode here is always 16-bit M.
emitSepM();
emitOpImm(W65816::EOR_Imm8, 0xFF); emitOpImm(W65816::EOR_Imm8, 0xFF);
emitOp(W65816::INA); emitOp(W65816::INA);
emitRepM();
return; return;
} }
case W65816::NEGC16: { case W65816::NEGC16: {

View file

@ -467,6 +467,17 @@ MachineBasicBlock::iterator W65816FrameLowering::eliminateCallFramePseudoInstr(
} else { } else {
BuildMI(MBB, I, DL, TII.get(W65816::STA_DP)).addImm(0xF6); BuildMI(MBB, I, DL, TII.get(W65816::STA_DP)).addImm(0xF6);
} }
// Re-zero the BANK byte too, not just the 16-bit pointer. The
// [$F6],Y far-frame access reads a 24-bit pointer at $F6/$F7/$F8,
// and $F8 sits inside the libcall scratch range ($E0..$FF):
// libgcc's __muldi3 / __udivmoddi_core / __umulhisi3_qsq all
// store live values there. With only the prologue's STZ, the
// first mul/div anywhere in a callee chain left $F8 nonzero and
// every later FP-relative access in the caller silently read and
// wrote another bank -- the RetroNet IIgs corruption family
// (record-walk losing records, spilled call args arriving as
// garbage, wild jumps), whose victim moved with frame layout.
BuildMI(MBB, I, DL, TII.get(W65816::STZ_DP)).addImm(0xF8);
}; };
bool YLive = false; bool YLive = false;
bool XLive = false; bool XLive = false;

View file

@ -3849,6 +3849,67 @@ W65816TargetLowering::EmitInstrWithCustomInserter(MachineInstr &MI,
Register Ptr = MI.getOperand(1).getReg(); Register Ptr = MI.getOperand(1).getReg();
// PROVENANCE FIX: a deref of a MATERIALISED GLOBAL ADDRESS is exactly a
// DBR-relative absolute access to that global -- the same thing a direct
// `global` reference compiles to (LDAabs / STAabs). Detect it (the pointer
// traces, through COPYs, to an LDAi16imm of a global/external symbol) and
// emit the abs form, skipping the [dp],Y pointer deref entirely. The deref
// path forces the bank byte to 0 (STZ $E2), which is correct for a stack
// pointer (the 65816 stack is always bank 0) but WRONG for a global: under
// the GS/OS Loader a global lives in DBR's bank, not bank 0, and abs is
// DBR-relative so it lands in the right bank. Without this, a global whose
// address gets spilled/reloaded under register pressure (losing its Wrapper
// node, so it selects LDAptr instead of LDAabs) reads bank-0 garbage --
// observed live as RetroNet's gX.id read via a spilled global pointer
// returning 0xAD, so its FILE_REQUEST carried a junk transfer id. Note we
// canNOT just source the bank from $BE: $BE is the code segment's PBR, and
// in a multi-segment program the data (BSS) segment is a DIFFERENT bank, so
// only DBR (via abs) is correct. Only globals take this path; stack/heap
// pointers keep the deref. va_arg (ForceBank0) is a stack pointer, never a
// global, but gate on it anyway for clarity.
if (!ForceBank0) {
MachineRegisterInfo &MRI = MF->getRegInfo();
const MachineOperand *GlobalMO = nullptr;
Register P = Ptr;
for (int hops = 0; hops < 4 && P.isVirtual(); ++hops) {
MachineInstr *Def = MRI.getVRegDef(P);
if (!Def)
break;
unsigned DOp = Def->getOpcode();
if (DOp == W65816::LDAi16imm && Def->getNumOperands() >= 2 &&
(Def->getOperand(1).isGlobal() || Def->getOperand(1).isSymbol())) {
GlobalMO = &Def->getOperand(1);
break;
}
if (DOp == TargetOpcode::COPY && Def->getOperand(1).isReg() &&
Def->getOperand(1).getReg().isVirtual()) {
P = Def->getOperand(1).getReg();
continue;
}
break;
}
if (GlobalMO) {
MachineOperand G = *GlobalMO;
if (IsLoad) {
Register Dst = MI.getOperand(0).getReg();
BuildMI(*BB, MI.getIterator(), DL, TII.get(W65816::LDAabs), Dst)
.add(G);
} else if (IsByteStore) {
Register Val = MI.getOperand(0).getReg();
BuildMI(*BB, MI.getIterator(), DL, TII.get(TargetOpcode::COPY),
W65816::A).addReg(Val);
BuildMI(*BB, MI.getIterator(), DL, TII.get(W65816::STA8abs))
.addReg(W65816::A).add(G);
} else {
Register Val = MI.getOperand(0).getReg();
BuildMI(*BB, MI.getIterator(), DL, TII.get(W65816::STAabs))
.addReg(Val).add(G);
}
MI.eraseFromParent();
return BB;
}
}
// Why we spill the pointer to a fresh stack slot first: // Why we spill the pointer to a fresh stack slot first:
// a direct `COPY $a = ptr_vreg ; STA $E0` lets RA elide the COPY // a direct `COPY $a = ptr_vreg ; STA $E0` lets RA elide the COPY
// when ptr_vreg is already allocated to A. In a loop body where // when ptr_vreg is already allocated to A. In a loop body where

View file

@ -379,6 +379,15 @@ bool W65816RegisterInfo::eliminateFrameIndex(MachineBasicBlock::iterator II,
case W65816::IMG15: srcDP = 0xCE; break; case W65816::IMG15: srcDP = 0xCE; break;
default: break; default: break;
} }
// BYTE-SLOT STORE HAZARD. M=16 is the default mode, so `sta d,s` writes TWO bytes. When the target
// frame object is only 1 byte (an i8 spill / i8 arg-save), the second byte overflows the slot. For
// the TOP slot of a frame that overflow byte is the caller's saved RETURN ADDRESS low byte: the
// callee's RTL then jumps into hyperspace (observed live: rnClientOnFileRecord saving its uint8_t
// `op` clobbered its return 0x80->0x01, wedging the RetroNet IIgs DISPLAY walk). Wrap the store in
// SEP #$20 / REP #$20 so a 1-byte slot gets a genuine 8-bit store. Only the STORE is a hazard - a
// 16-bit load of the extra byte is a harmless read (the high byte of an i8 value is ignored), so
// LDAfi is left 16-bit. The load side never corrupts anything.
bool ByteSlot = MFI.getObjectSize(FI) == 1;
if (srcDP >= 0 || Src == W65816::X || Src == W65816::Y) { if (srcDP >= 0 || Src == W65816::X || Src == W65816::Y) {
// STAfi with non-A source: must clobber A to land the value in // STAfi with non-A source: must clobber A to land the value in
// A and then `sta d,s`. PHA-bracket so A's incoming value is // A and then `sta d,s`. PHA-bracket so A's incoming value is
@ -400,18 +409,26 @@ bool W65816RegisterInfo::eliminateFrameIndex(MachineBasicBlock::iterator II,
unsigned XferOp = (Src == W65816::X) ? W65816::TXA : W65816::TYA; unsigned XferOp = (Src == W65816::X) ? W65816::TXA : W65816::TYA;
BuildMI(*MI.getParent(), II, MI.getDebugLoc(), TII.get(XferOp)); BuildMI(*MI.getParent(), II, MI.getDebugLoc(), TII.get(XferOp));
} }
if (ByteSlot)
BuildMI(*MI.getParent(), II, MI.getDebugLoc(), TII.get(W65816::SEP)).addImm(0x20);
BuildMI(*MI.getParent(), II, MI.getDebugLoc(), BuildMI(*MI.getParent(), II, MI.getDebugLoc(),
TII.get(W65816::STA_StackRel)) TII.get(W65816::STA_StackRel))
.addImm(Offset + 2) // PHA shifted SP by 2 .addImm(Offset + 2) // PHA shifted SP by 2
.addReg(W65816::A, RegState::Implicit); .addReg(W65816::A, RegState::Implicit);
if (ByteSlot)
BuildMI(*MI.getParent(), II, MI.getDebugLoc(), TII.get(W65816::REP)).addImm(0x20);
BuildMI(*MI.getParent(), II, MI.getDebugLoc(), TII.get(W65816::PLA)); BuildMI(*MI.getParent(), II, MI.getDebugLoc(), TII.get(W65816::PLA));
} else { } else {
// Direct A source: simple sta d,s — A is the source, A is fine // Direct A source: simple sta d,s — A is the source, A is fine
// afterward (no implicit clobber). // afterward (no implicit clobber).
if (ByteSlot)
BuildMI(*MI.getParent(), II, MI.getDebugLoc(), TII.get(W65816::SEP)).addImm(0x20);
BuildMI(*MI.getParent(), II, MI.getDebugLoc(), BuildMI(*MI.getParent(), II, MI.getDebugLoc(),
TII.get(W65816::STA_StackRel)) TII.get(W65816::STA_StackRel))
.addImm(Offset) .addImm(Offset)
.addReg(W65816::A, RegState::Implicit); .addReg(W65816::A, RegState::Implicit);
if (ByteSlot)
BuildMI(*MI.getParent(), II, MI.getDebugLoc(), TII.get(W65816::REP)).addImm(0x20);
} }
MI.eraseFromParent(); MI.eraseFromParent();
return true; return true;

View file

@ -810,8 +810,14 @@ bool W65816StackSlotCleanup::runOnMachineFunction(MachineFunction &MF) {
SmallVector<MachineInstr *, 4> Branches; SmallVector<MachineInstr *, 4> Branches;
for (MachineInstr &MI : MBB) { for (MachineInstr &MI : MBB) {
unsigned Opc = MI.getOpcode(); unsigned Opc = MI.getOpcode();
// BCC/BCS/BVC/BVS are collected too, but ONLY for the carry/overflow
// corrupters (isCarryCorrupting below): an LDA-like op between a CMP
// and a BCC leaves C alone and needs no wrap, which is why they were
// never listed here before. ADDframe is different - see its note.
if (Opc == W65816::BEQ || Opc == W65816::BNE || if (Opc == W65816::BEQ || Opc == W65816::BNE ||
Opc == W65816::BMI || Opc == W65816::BPL) Opc == W65816::BMI || Opc == W65816::BPL ||
Opc == W65816::BCC || Opc == W65816::BCS ||
Opc == W65816::BVC || Opc == W65816::BVS)
Branches.push_back(&MI); Branches.push_back(&MI);
} }
auto isFlagPreserving = [VLAFunc](unsigned Opc) { auto isFlagPreserving = [VLAFunc](unsigned Opc) {
@ -878,6 +884,17 @@ bool W65816StackSlotCleanup::runOnMachineFunction(MachineFunction &MF) {
// COPY in the corrupting set forces the pass to walk past these // COPY in the corrupting set forces the pass to walk past these
// PHI-elim copies to find the real test (a CMP). // PHI-elim copies to find the real test (a CMP).
if (Opc == TargetOpcode::COPY) return true; if (Opc == TargetOpcode::COPY) return true;
// ADDframe (the LEA of a stack slot) expands at PEI to `TSC; CLC;
// ADC #disp`, which rewrites N/Z (TSC, ADC), C (CLC, ADC) and V
// (ADC). It has no Defs=[P] (deliberately, like the loads above)
// and isReMaterializable, so post-RA passes are free to place it
// between a CMP and its Bxx - which Machine Copy Propagation did
// in RetroNet's rnClient.c: `cmp #3; tsc; clc; adc #0xc1; bne`
// made an `act.kind == RN_WA_SUBMIT` test branch on (SP+0xC1)!=0,
// i.e. never true, so the form was never submitted on the IIgs.
// Before this line the walk below treated ADDframe as the flag-
// DEFINING test (isFlagDefining's default) and declined to wrap.
if (Opc == W65816::ADDframe) return true;
// Pure load / register-transfer instructions: only side effect on // Pure load / register-transfer instructions: only side effect on
// flags is N/Z from the loaded/transferred value. Never a "test" // flags is N/Z from the loaded/transferred value. Never a "test"
// — they just move data. Treated as corruption when between the // — they just move data. Treated as corruption when between the
@ -972,7 +989,16 @@ bool W65816StackSlotCleanup::runOnMachineFunction(MachineFunction &MF) {
Opc == W65816::CMPfi || Opc == W65816::CMPfi ||
Opc == W65816::ADDframe; Opc == W65816::ADDframe;
}; };
// The subset of corrupters that also rewrite C/V, and so must be
// wrapped even for a BCC/BCS/BVC/BVS. Only ADDframe today: every
// other entry in isLdaLike touches N/Z alone.
auto isCarryCorrupting = [](unsigned Opc) {
return Opc == W65816::ADDframe;
};
for (MachineInstr *Br : Branches) { for (MachineInstr *Br : Branches) {
unsigned BrOpc = Br->getOpcode();
bool TestsNZ = BrOpc == W65816::BEQ || BrOpc == W65816::BNE ||
BrOpc == W65816::BMI || BrOpc == W65816::BPL;
// Walk back from Br looking for the pattern: // Walk back from Br looking for the pattern:
// <test>; (mix of preserving + corrupting ops); Br // <test>; (mix of preserving + corrupting ops); Br
// where <test> is a flag-defining op (CMP/ORA/AND/ADC/...) and // where <test> is a flag-defining op (CMP/ORA/AND/ADC/...) and
@ -998,9 +1024,12 @@ bool W65816StackSlotCleanup::runOnMachineFunction(MachineFunction &MF) {
if (!MI.isDebugInstr()) { if (!MI.isDebugInstr()) {
if (isFlagPreserving(MI.getOpcode())) { if (isFlagPreserving(MI.getOpcode())) {
// skip // skip
} else if (isLdaLike(MI.getOpcode())) { } else if (isCarryCorrupting(MI.getOpcode()) ||
(TestsNZ && isLdaLike(MI.getOpcode()))) {
if (!LastCorrupt) LastCorrupt = &MI; if (!LastCorrupt) LastCorrupt = &MI;
FirstCorrupt = &MI; FirstCorrupt = &MI;
} else if (isLdaLike(MI.getOpcode())) {
// An N/Z-only corrupter under a C/V branch: harmless, walk on.
} else if (isFlagDefining(MI)) { } else if (isFlagDefining(MI)) {
Test = &MI; Test = &MI;
break; break;
@ -1056,7 +1085,11 @@ bool W65816StackSlotCleanup::runOnMachineFunction(MachineFunction &MF) {
// i32-libcall loops) get their FP-rel pseudos bumped, which // i32-libcall loops) get their FP-rel pseudos bumped, which
// shifts reads/writes by one byte and corrupts state at // shifts reads/writes by one byte and corrupts state at
// iteration N proportional to the i32-libcall count. // iteration N proportional to the i32-libcall count.
if (IsPseudo && UsesFPRel) { // ADDframe has no FP-relative expansion (eliminateFrameIndex always
// emits TSC + ADC #disp), so it is SP-relative in EVERY function and
// always needs the bump - exempting it here would leave its address
// one byte low inside the wrap in a large-frame function.
if (IsPseudo && UsesFPRel && Opc != W65816::ADDframe) {
const MachineFrameInfo &MFI = MF.getFrameInfo(); const MachineFrameInfo &MFI = MF.getFrameInfo();
if (It->getOperand(1).isFI()) { if (It->getOperand(1).isFI()) {
int FI = It->getOperand(1).getIndex(); int FI = It->getOperand(1).getIndex();

View file

@ -10,6 +10,7 @@
// //
//===----------------------------------------------------------------------===// //===----------------------------------------------------------------------===//
#include <cstdlib>
#include "W65816TargetMachine.h" #include "W65816TargetMachine.h"
#include "W65816.h" #include "W65816.h"
#include "W65816MachineFunctionInfo.h" #include "W65816MachineFunctionInfo.h"
@ -265,9 +266,16 @@ void W65816PassConfig::addPostRegAlloc() {
// then deletes still-adjacent redundant spills. A second SpillToX // then deletes still-adjacent redundant spills. A second SpillToX
// invocation collapses any TAX/TXA pair left adjacent by cleanup // invocation collapses any TAX/TXA pair left adjacent by cleanup
// (e.g. when an inner copy between bridge endpoints went away). // (e.g. when an inner copy between bridge endpoints went away).
addPass(createW65816SpillToX()); // Diagnostic lever: W65816_DISABLE_STACKOPT=1 turns off the stack-slot
addPass(createW65816StackSlotCleanup()); // rewriting passes (SpillToX / StackSlotCleanup / StackSlotMerge /
addPass(createW65816SpillToX()); // StackRelToImg) wholesale. Used to bisect wrong-VALUES corruption in
// large inlined frames down to this pass family without per-pass
// rebuilds; costs size/speed, never correctness.
if (!getenv("W65816_DISABLE_STACKOPT")) {
addPass(createW65816SpillToX());
addPass(createW65816StackSlotCleanup());
addPass(createW65816SpillToX());
}
// Disable MachineCopyPropagation: it eliminates `COPY $img = $a` // Disable MachineCopyPropagation: it eliminates `COPY $img = $a`
// thinking the IMG dest is dead (no explicit physreg use of $img // thinking the IMG dest is dead (no explicit physreg use of $img
// remains after PEI expands STAfi-with-Img16-source into LDA_DP). // remains after PEI expands STAfi-with-Img16-source into LDA_DP).
@ -287,7 +295,9 @@ void W65816PassConfig::addPreEmitPass() {
// physreg-COPY pseudos into the real TAX/TXA opcodes, adjacent // physreg-COPY pseudos into the real TAX/TXA opcodes, adjacent
// TXA;TAX pairs (which the earlier SpillToX invocations couldn't // TXA;TAX pairs (which the earlier SpillToX invocations couldn't
// see in COPY form) become collapsable. // see in COPY form) become collapsable.
addPass(createW65816SpillToX()); if (!getenv("W65816_DISABLE_STACKOPT")) {
addPass(createW65816SpillToX());
}
// Rewrite negative-Y indirect-Y stack-rel ops. Must run BEFORE // Rewrite negative-Y indirect-Y stack-rel ops. Must run BEFORE
// BranchExpand because the rewrite expands one instruction into // BranchExpand because the rewrite expands one instruction into
// several and shifts branch distances. The pass internally checks // several and shifts branch distances. The pass internally checks
@ -328,8 +338,12 @@ void W65816PassConfig::addPreEmitPass() {
// Saves 2 inst per PHI-copy occurrence (the memory copy round-trip // Saves 2 inst per PHI-copy occurrence (the memory copy round-trip
// collapses when X and Y are renamed to the same slot). See // collapses when X and Y are renamed to the same slot). See
// W65816StackSlotMerge.cpp. // W65816StackSlotMerge.cpp.
addPass(createW65816StackSlotMerge()); if (!getenv("W65816_DISABLE_STACKOPT") && !getenv("W65816_DISABLE_SLOTMERGE")) {
addPass(createW65816StackRelToImg()); addPass(createW65816StackSlotMerge());
}
if (!getenv("W65816_DISABLE_STACKOPT")) {
addPass(createW65816StackRelToImg());
}
} }
MachineFunctionInfo *W65816TargetMachine::createMachineFunctionInfo( MachineFunctionInfo *W65816TargetMachine::createMachineFunctionInfo(

View file

@ -0,0 +1,94 @@
# Pin: an ADDframe (LEA of a stack slot) scheduled between a CMP and the
# branch that consumes it must be PHP/PLP-wrapped by the stack-slot-cleanup
# pass (Pass -2.5), and its ImmOffset bumped by one for PHP's S decrement.
#
# ADDframe expands at PEI to `TSC; CLC; ADC #disp`, which rewrites N/Z, C
# and V. It carries no Defs=[P] and is rematerializable, so Machine Copy
# Propagation legitimately placed one between `cmp #3` and `bne` in
# RetroNet's rnClient.c - the `act.kind == RN_WA_SUBMIT` test then branched
# on (SP+disp) != 0 and the form was never submitted. The wrap pass used
# to treat ADDframe as the flag-DEFINING test and decline to wrap.
#
# The BCC case pins the second half of the fix: ADDframe corrupts C, so a
# carry branch needs the wrap too (LDA-like ops never did, and still don't).
#
# RUN: llc -mtriple=w65816 -run-pass=w65816-stack-slot-cleanup -o - %s | FileCheck %s
--- |
target triple = "w65816"
define void @nz() { ret void }
define void @carry() { ret void }
define void @lda_under_carry() { ret void }
...
---
# CHECK-LABEL: name: nz
# CHECK: CMPi16imm killed $a, 3, implicit-def $p
# CHECK-NEXT: PHP
# CHECK-NEXT: $a = ADDframe %stack.1, 1
# CHECK-NEXT: PLP
# CHECK-NEXT: BEQ %bb.1, implicit $p
name: nz
tracksRegLiveness: false
stack:
- { id: 0, size: 2, alignment: 1 }
- { id: 1, size: 6, alignment: 1 }
body: |
bb.0:
successors: %bb.1, %bb.2
$a = LDAfi %stack.0, 0
CMPi16imm killed $a, 3, implicit-def $p
$a = ADDframe %stack.1, 0
BEQ %bb.1, implicit $p
BRA %bb.2
bb.1:
RTL
bb.2:
RTL
...
---
# CHECK-LABEL: name: carry
# CHECK: CMPi16imm killed $a, 3, implicit-def $p
# CHECK-NEXT: PHP
# CHECK-NEXT: $a = ADDframe %stack.1, 1
# CHECK-NEXT: PLP
# CHECK-NEXT: BCC %bb.1, implicit $p
name: carry
tracksRegLiveness: false
stack:
- { id: 0, size: 2, alignment: 1 }
- { id: 1, size: 6, alignment: 1 }
body: |
bb.0:
successors: %bb.1, %bb.2
$a = LDAfi %stack.0, 0
CMPi16imm killed $a, 3, implicit-def $p
$a = ADDframe %stack.1, 0
BCC %bb.1, implicit $p
BRA %bb.2
bb.1:
RTL
bb.2:
RTL
...
---
# An N/Z-only corrupter under a carry branch stays unwrapped (no size cost).
# CHECK-LABEL: name: lda_under_carry
# CHECK: CMPi16imm killed $a, 3, implicit-def $p
# CHECK-NEXT: $a = LDAi16imm 7
# CHECK-NEXT: BCC %bb.1, implicit $p
name: lda_under_carry
tracksRegLiveness: false
stack:
- { id: 0, size: 2, alignment: 1 }
body: |
bb.0:
successors: %bb.1, %bb.2
$a = LDAfi %stack.0, 0
CMPi16imm killed $a, 3, implicit-def $p
$a = LDAi16imm 7
BCC %bb.1, implicit $p
BRA %bb.2
bb.1:
RTL
bb.2:
RTL
...