More fixes.

This commit is contained in:
Scott Duensing 2026-10-17 19:46:25 -05:00
parent d626c54e92
commit a6dc3eb147
7 changed files with 134 additions and 16 deletions

View file

@ -935,6 +935,32 @@ The `.byte` form is needed when llvm-mc can't yet parse an opcode
literally (some 65816 addressing modes still have gaps in the literally (some 65816 addressing modes still have gaps in the
assembler). Hand-encoding is a stopgap; report opcodes that need it. assembler). Hand-encoding is a stopgap; report opcodes that need it.
### Operand widths (inline asm and `.s` files)
The 65816 encodes an operand's width in the opcode, and the assembler
picks the narrowest form a constant fits:
| You write | You get | Addresses |
|---|---|---|
| `sta 0x12` / `sta 0x12, x` | direct page (2 bytes) | bank 0, through D |
| `sta 0x1234` / `sta 0x1234, x` | absolute (3 bytes) | the data bank (DBR) |
| `sta 0x012000` | long (4 bytes) | that bank |
| `sta \|0x12, x` | absolute, forced | the data bank (DBR) |
| `sta >0x12` | long, forced | bank 0 |
So a bare `sta 0, x` with X holding an address is a direct-page store
into bank 0, not the DBR-relative absolute store it may look like;
write `sta |0, x`. Symbols (labels, globals) always take the absolute
form unless prefixed. `llc -S` and `llvm-mc` print the prefixes back
where a constant would otherwise narrow, so their text assembles to the
same bytes. (`lda`/`sta n, y` has no direct-page form and is always
absolute.)
Immediates follow the assembler's idea of the M and X flags, not the
`sep`/`rep` instructions before them: after `sep #0x20`, write `.a8`
(and `.a16` after `rep #0x20`; `.i8`/`.i16` for X) or `lda #0` still
assembles as a 3-byte immediate.
--- ---
## Tools reference ## Tools reference

View file

@ -547,10 +547,12 @@ void mmioStore(unsigned char v) { *(volatile unsigned char *)0xC035 = v; }
unsigned char mmioLoad(void) { return *(volatile unsigned char *)0xC035; } unsigned char mmioLoad(void) { return *(volatile unsigned char *)0xC035; }
EOF EOF
"$CLANG" --target=w65816 -O2 -S "$cFileC" -o "$sFileC" "$CLANG" --target=w65816 -O2 -S "$cFileC" -o "$sFileC"
# Must contain `sta 0xc035` AND `lda 0xc035` (long form chosen by addr). # Must contain `sta >0xc035` AND `lda >0xc035` (long form chosen by
# Must NOT contain `sta [` or `lda [` (the old [dp],Y route). # addr; the printer's `>` keeps a long under 0x10000 long when the text
if ! grep -qE 'sta 0xc035' "$sFileC" \ # is assembled again). Must NOT contain `sta [` or `lda [` (the old
|| ! grep -qE 'lda 0xc035' "$sFileC" \ # [dp],Y route).
if ! grep -qE 'sta >0xc035' "$sFileC" \
|| ! grep -qE 'lda >0xc035' "$sFileC" \
|| grep -qE '(sta|lda) \[' "$sFileC"; then || grep -qE '(sta|lda) \[' "$sFileC"; then
cat "$sFileC" >&2 cat "$sFileC" >&2
die "i8 const-addr store/load: expected STA8long/LDA8long, got [dp],Y route" die "i8 const-addr store/load: expected STA8long/LDA8long, got [dp],Y route"

View file

@ -100,6 +100,13 @@ class W65816Operand : public MCParsedAsmOperand {
// narrowed to STA_DP/STA_Abs by value fit. // narrowed to STA_DP/STA_Abs by value fit.
bool ForceLong = false; bool ForceLong = false;
// Set only for k_Addr operands carrying an explicit force-absolute prefix
// (`|expr`, ORCA/M's and WDC's spelling). When true the operand matches
// ONLY the 16-bit absolute class (AddrAbs), so `sta |0,x` reaches
// STA_AbsX (0x9D), DBR-relative, instead of being narrowed by value fit
// to STA_DPX (0x95), which addresses bank 0 through D.
bool ForceAbs = false;
public: public:
W65816Operand(StringRef T, SMLoc S) W65816Operand(StringRef T, SMLoc S)
: Kind(k_Tok), Tok({T.data(), (unsigned)T.size()}), Start(S), : Kind(k_Tok), Tok({T.data(), (unsigned)T.size()}), Start(S),
@ -107,8 +114,8 @@ public:
W65816Operand(unsigned R, SMLoc S, SMLoc E) W65816Operand(unsigned R, SMLoc S, SMLoc E)
: Kind(k_Reg), RegNum(R), Start(S), End(E) {} : Kind(k_Reg), RegNum(R), Start(S), End(E) {}
W65816Operand(KindTy K, const MCExpr *E, SMLoc S, SMLoc E2, W65816Operand(KindTy K, const MCExpr *E, SMLoc S, SMLoc E2,
bool ForceLong = false) bool ForceLong = false, bool ForceAbs = false)
: Kind(K), Start(S), End(E2), ForceLong(ForceLong) { : Kind(K), Start(S), End(E2), ForceLong(ForceLong), ForceAbs(ForceAbs) {
if (K == k_Imm) if (K == k_Imm)
Imm = E; Imm = E;
else else
@ -133,8 +140,10 @@ public:
static std::unique_ptr<W65816Operand> createAddr(const MCExpr *Val, SMLoc S, static std::unique_ptr<W65816Operand> createAddr(const MCExpr *Val, SMLoc S,
SMLoc E, SMLoc E,
bool ForceLong = false) { bool ForceLong = false,
return std::make_unique<W65816Operand>(k_Addr, Val, S, E, ForceLong); bool ForceAbs = false) {
return std::make_unique<W65816Operand>(k_Addr, Val, S, E, ForceLong,
ForceAbs);
} }
// --- MCParsedAsmOperand hooks ------------------------------------------ // --- MCParsedAsmOperand hooks ------------------------------------------
@ -165,7 +174,8 @@ public:
// for absolute addresses and PC-relative branches, falling through // for absolute addresses and PC-relative branches, falling through
// narrower variants only when the value is a constant that fits. // narrower variants only when the value is a constant that fits.
// 24-bit `long` and `pcrel16` are opt-in (only constants in the wider // 24-bit `long` and `pcrel16` are opt-in (only constants in the wider
// range, or future explicit addressing-mode syntax). // range, or the `>` prefix); the `|` prefix keeps a small constant on the
// 16-bit absolute form.
static bool constFitsUnsigned(const MCExpr *Expr, unsigned width) { static bool constFitsUnsigned(const MCExpr *Expr, unsigned width) {
if (const auto *CE = dyn_cast<MCConstantExpr>(Expr)) { if (const auto *CE = dyn_cast<MCConstantExpr>(Expr)) {
@ -198,7 +208,7 @@ public:
} }
bool isAddrDP() const { bool isAddrDP() const {
return Kind == k_Addr && !ForceLong && return Kind == k_Addr && !ForceLong && !ForceAbs &&
isConstant(Addr) && constFitsUnsigned(Addr, 8); isConstant(Addr) && constFitsUnsigned(Addr, 8);
} }
bool isAddrAbs() const { bool isAddrAbs() const {
@ -206,7 +216,7 @@ public:
(!isConstant(Addr) || constFitsUnsigned(Addr, 16)); (!isConstant(Addr) || constFitsUnsigned(Addr, 16));
} }
bool isAddrLong() const { bool isAddrLong() const {
if (Kind != k_Addr) if (Kind != k_Addr || ForceAbs)
return false; return false;
// Explicit `>expr` prefix forces the long form for any value that fits // Explicit `>expr` prefix forces the long form for any value that fits
// in 24 bits (or any symbol), and the narrower AddrDP/AddrAbs predicates // in 24 bits (or any symbol), and the narrower AddrDP/AddrAbs predicates
@ -617,6 +627,16 @@ bool W65816AsmParser::parseOperand(OperandVector &Operands) {
ForceLong = true; ForceLong = true;
} }
// Force-absolute prefix: `|expr` selects the 16-bit absolute form even
// when the value fits in a byte, where the matcher would otherwise narrow
// to direct page -- a different bank (DBR vs 0) for the indexed forms.
// ('|' cannot begin an expression either.)
bool ForceAbs = false;
if (!ForceLong && getLexer().is(AsmToken::Pipe)) {
Parser.Lex(); // eat '|'
ForceAbs = true;
}
// Anything else is an address / branch-target / block-move byte. We // Anything else is an address / branch-target / block-move byte. We
// rely on the standard LLVM asm expression parser, which accepts // rely on the standard LLVM asm expression parser, which accepts
// identifiers (labels), decimal integers, hex ($... via MCAsmInfo // identifiers (labels), decimal integers, hex ($... via MCAsmInfo
@ -626,7 +646,10 @@ bool W65816AsmParser::parseOperand(OperandVector &Operands) {
if (Parser.parseExpression(Val)) if (Parser.parseExpression(Val))
return Error(S, "expected expression"); return Error(S, "expected expression");
SMLoc E = getLexer().getLoc(); SMLoc E = getLexer().getLoc();
Operands.push_back(W65816Operand::createAddr(Val, S, E, ForceLong)); if (ForceAbs && isa<MCConstantExpr>(Val) &&
!W65816Operand::constFitsUnsigned(Val, 16))
return Error(S, "absolute address does not fit in 16 bits");
Operands.push_back(W65816Operand::createAddr(Val, S, E, ForceLong, ForceAbs));
return false; return false;
} }

View file

@ -67,12 +67,22 @@ void W65816InstPrinter::printOperand(const MCInst *MI, unsigned OpNo,
// the stock LLVM expression lexer) can round-trip our output. Teaching // the stock LLVM expression lexer) can round-trip our output. Teaching
// the lexer about `$` is a future cleanup; until then the printed form // the lexer about `$` is a future cleanup; until then the printed form
// is compatible-with-itself rather than idiomatic WDC syntax. // is compatible-with-itself rather than idiomatic WDC syntax.
//
// A constant that would fit a narrower form carries the prefix that keeps
// this one when the text is assembled again: `|` (absolute) below 0x100,
// `>` (long) below 0x10000. The parser narrows a bare constant by value,
// and the narrower forms address another bank (dp: bank 0 through D).
static void printAddrImm(const MCOperand &Op, unsigned width, raw_ostream &O, static void printAddrImm(const MCOperand &Op, unsigned width, raw_ostream &O,
const MCAsmInfo &MAI) { const MCAsmInfo &MAI) {
if (Op.isImm()) { if (Op.isImm()) {
uint64_t mask = width >= 64 ? ~uint64_t(0) : ((uint64_t(1) << width) - 1); uint64_t mask = width >= 64 ? ~uint64_t(0) : ((uint64_t(1) << width) - 1);
uint64_t value = Op.getImm() & mask;
if (width == 16 && value < 0x100)
O << "|";
else if (width == 24 && value < 0x10000)
O << ">";
O << "0x"; O << "0x";
O.write_hex(Op.getImm() & mask); O.write_hex(value);
return; return;
} }
assert(Op.isExpr() && "unknown address operand kind"); assert(Op.isExpr() && "unknown address operand kind");
@ -89,6 +99,17 @@ void W65816InstPrinter::printAddrAbs(const MCInst *MI, unsigned OpNo,
printAddrImm(MI->getOperand(OpNo), 16, O, MAI); printAddrImm(MI->getOperand(OpNo), 16, O, MAI);
} }
void W65816InstPrinter::printAddrAbsInd(const MCInst *MI, unsigned OpNo,
raw_ostream &O) {
const MCOperand &Op = MI->getOperand(OpNo);
if (Op.isImm()) {
O << "0x";
O.write_hex(Op.getImm() & 0xffff);
return;
}
printAddrImm(Op, 16, O, MAI);
}
void W65816InstPrinter::printAddrLong(const MCInst *MI, unsigned OpNo, void W65816InstPrinter::printAddrLong(const MCInst *MI, unsigned OpNo,
raw_ostream &O) { raw_ostream &O) {
printAddrImm(MI->getOperand(OpNo), 24, O, MAI); printAddrImm(MI->getOperand(OpNo), 24, O, MAI);

View file

@ -40,6 +40,7 @@ private:
void printOperand(const MCInst *MI, unsigned OpNo, raw_ostream &O); void printOperand(const MCInst *MI, unsigned OpNo, raw_ostream &O);
void printAddrDP(const MCInst *MI, unsigned OpNo, raw_ostream &O); void printAddrDP(const MCInst *MI, unsigned OpNo, raw_ostream &O);
void printAddrAbs(const MCInst *MI, unsigned OpNo, raw_ostream &O); void printAddrAbs(const MCInst *MI, unsigned OpNo, raw_ostream &O);
void printAddrAbsInd(const MCInst *MI, unsigned OpNo, raw_ostream &O);
void printAddrLong(const MCInst *MI, unsigned OpNo, raw_ostream &O); void printAddrLong(const MCInst *MI, unsigned OpNo, raw_ostream &O);
void printPCRel8(const MCInst *MI, uint64_t Address, unsigned OpNo, void printPCRel8(const MCInst *MI, uint64_t Address, unsigned OpNo,
raw_ostream &O); raw_ostream &O);

View file

@ -129,6 +129,18 @@ def addrAbs : Operand<i16> {
let DecoderMethod = "decodeAddr16"; let DecoderMethod = "decodeAddr16";
} }
// addrAbs in an indirect jump (JMP/JSR (abs), (abs, x), [abs]): parsed
// the same, but no narrower form of these exists, so it prints bare,
// without the `|` printAddrAbs keeps a small constant absolute with.
def addrAbsInd : Operand<i16> {
let ParserMatchClass = W65816AddrOp<"AddrAbs">;
let OperandType = "OPERAND_MEMORY";
let Type = i16;
let PrintMethod = "printAddrAbsInd";
let EncoderMethod = "encodeAddr16";
let DecoderMethod = "decodeAddr16";
}
def addrLong : Operand<i32> { def addrLong : Operand<i32> {
let ParserMatchClass = W65816AddrOp<"AddrLong">; let ParserMatchClass = W65816AddrOp<"AddrLong">;
let OperandType = "OPERAND_MEMORY"; let OperandType = "OPERAND_MEMORY";
@ -344,7 +356,7 @@ class InstDPIndX<bits<8> op, string mnem>
// Used by the indirect-call trampoline in crt0/libgcc to dispatch // Used by the indirect-call trampoline in crt0/libgcc to dispatch
// through a function-pointer slot. // through a function-pointer slot.
class InstAbsInd<bits<8> op, string mnem> class InstAbsInd<bits<8> op, string mnem>
: W65816Inst<(outs), (ins addrAbs:$addr), : W65816Inst<(outs), (ins addrAbsInd:$addr),
!strconcat(mnem, "\t(${addr})")> { !strconcat(mnem, "\t(${addr})")> {
let Size = 3; let Size = 3;
bits<16> addr; bits<16> addr;
@ -358,7 +370,7 @@ class InstAbsInd<bits<8> op, string mnem>
// jumps there. Useful for jump tables when the base address is known // jumps there. Useful for jump tables when the base address is known
// at assembly time. // at assembly time.
class InstAbsIndX<bits<8> op, string mnem> class InstAbsIndX<bits<8> op, string mnem>
: W65816Inst<(outs), (ins addrAbs:$addr), : W65816Inst<(outs), (ins addrAbsInd:$addr),
!strconcat(mnem, "\t(${addr}, x)")> { !strconcat(mnem, "\t(${addr}, x)")> {
let Size = 3; let Size = 3;
bits<16> addr; bits<16> addr;
@ -372,7 +384,7 @@ class InstAbsIndX<bits<8> op, string mnem>
// offset address. Square brackets distinguish from the 16-bit // offset address. Square brackets distinguish from the 16-bit
// indirect form. // indirect form.
class InstAbsIndLong<bits<8> op, string mnem> class InstAbsIndLong<bits<8> op, string mnem>
: W65816Inst<(outs), (ins addrAbs:$addr), : W65816Inst<(outs), (ins addrAbsInd:$addr),
!strconcat(mnem, "\t[${addr}]")> { !strconcat(mnem, "\t[${addr}]")> {
let Size = 3; let Size = 3;
bits<16> addr; bits<16> addr;

View file

@ -0,0 +1,33 @@
; The `|` prefix keeps a constant that fits in a byte on the 16-bit
; absolute form: `sta |0,x` is STA abs,X (0x9D), DBR-relative, where a bare
; `sta 0,x` narrows to STA dp,X (0x95), bank 0 through D. Without it no
; abs,X under 0x100 could be written: JoeyLib's iigsIndexed.s, X a
; surface's address, stored to bank 0 instead of the surface's bank. The
; printer puts the prefixes back (`|` on an absolute constant under 0x100,
; `>` on a long one under 0x10000), so its text assembles to the same
; bytes again.
;
; RUN: llvm-mc -triple=w65816 -show-encoding %s | FileCheck %s
; RUN: llvm-mc -triple=w65816 %s | llvm-mc -triple=w65816 -show-encoding | FileCheck %s
; RUN: not llvm-mc -triple=w65816 --defsym=TOOBIG=1 %s 2>&1 | FileCheck --check-prefix=ERR %s
.text
rep #0x30
.a16
.i16
; CHECK: sta |0x0, x {{.*}} encoding: [0x9d,0x00,0x00]
sta |0,x
; CHECK: lda |0x0, x {{.*}} encoding: [0xbd,0x00,0x00]
lda |0,x
; CHECK: sta 0x0, x {{.*}} encoding: [0x95,0x00]
sta 0,x
; CHECK: lda |0x10 {{.*}} encoding: [0xad,0x10,0x00]
lda |0x10
; CHECK: sta 0x1234, y {{.*}} encoding: [0x99,0x34,0x12]
sta |0x1234,y
; CHECK: sta >0x71 {{.*}} encoding: [0x8f,0x71,0x00,0x00]
sta >0x71
.ifdef TOOBIG
; ERR: error: absolute address does not fit in 16 bits
sta |0x12345
.endif