mirror of
https://github.com/odin-lang/Odin.git
synced 2026-09-02 02:03:35 +00:00
The last of the A32 sweep's disagreements, and they were mostly the same shape: an operand the syntax names but no field encodes, left printing as `#0`. MRS names APSR or SPSR by the R bit; VMRS and VMSR name FPSCR and its neighbours out of bits 19:16, which their masks had pinned shut so the register could not vary at all; SETEND names LE or BE by the E bit. Those are bare tokens in the syntax, which is what the special-register classes already model, so the endian pair joins them as a register class of its own. DBG read the whole eight-bit hint field where its option is only the low four, so it printed the fixed bits above it. RFE and SRS name their addressing mode the way LDM and STM do, so they are four mnemonics each rather than one, and the P and U bits that pick the mode are fixed bits of each form -- they had been left out of the mask entirely, so every one of the eight words decoded as the DA form. The writeback bit rides in the base register for RFE and in the implicit SP for SRS. VORR and VBIC against a modified immediate had no forms at all. Those words fell through to the shift-by-immediate family that sits beside them, and decoded as VSRA, VQSHRN and VQRSHRN with a shift of zero -- which is not a shift any of them can take. The register field in an operand was fifteen bits, on the reasoning that a register's raw value never passes 0x401F. Two classes do: the coprocessor registers at 0x8000, and now the endian tokens at 0x9000. Both were truncating silently. The field is sixteen bits, which fills the word exactly. A memory base or index is always a GPR, so those stay as they are. The A32 sweep now round-trips 984 of its 1183 entries byte-exact through llvm-mc, with nothing left that llvm and this disagree on: 159 are words llvm's own disassembly cannot assemble back, 38 are reserved encodings, and the last two are PSB CSYNC and TSB CSYNC, which llvm does not implement for AArch32 at all. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018UmHLRF11EoWwNWCJ7JGaA
206 lines
8.1 KiB
Odin
206 lines
8.1 KiB
Odin
// rexcode · Brendan Punsky (dotbmp@github), original author
|
|
|
|
package rexcode_arm32
|
|
|
|
// =============================================================================
|
|
// AArch32 OPERANDS
|
|
// =============================================================================
|
|
//
|
|
// Kind-tagged operand, same shape as other arches. Variations specific to
|
|
// AArch32:
|
|
//
|
|
// * Memory operands carry a much richer payload than RISC-V: base GPR +
|
|
// {imm | reg}-offset + shift {LSL/LSR/ASR/ROR/RRX} + pre/post indexing
|
|
// + sign. We pack these into a single Memory struct.
|
|
//
|
|
// * REGISTER operands always store a single Register byte; lane and shape
|
|
// hints (e.g. D[idx]) ride in the `lane`/`shift_type` fields.
|
|
//
|
|
// * IMMEDIATE i64 is wide enough for sign-extended branch displacements,
|
|
// 16-bit MOVW/MOVT immediates, modified-immediate raw values, and the
|
|
// packed CDE/coproc imm fields.
|
|
//
|
|
// * RELATIVE = label id (pre-resolution) or signed byte offset (post).
|
|
//
|
|
// * REG_LIST is a 16-bit GPR bitmask packed into the immediate slot.
|
|
|
|
Operand_Kind :: enum u8 {
|
|
NONE,
|
|
REGISTER,
|
|
IMMEDIATE,
|
|
MEMORY,
|
|
RELATIVE,
|
|
REG_LIST, // LDM/STM/PUSH/POP bitmask (low 16 bits = R0..R15)
|
|
// A modified immediate is a bit pattern an 8-bit field expands into, and
|
|
// assemblers write it as one: in hex, or as a float when the expansion
|
|
// produced one. The value is stored expanded either way -- for the float
|
|
// it is the 32-bit pattern, which is what the encoder needs back.
|
|
HEX_IMMEDIATE,
|
|
FLOAT_IMMEDIATE,
|
|
}
|
|
|
|
// ---- Shift / addressing-mode helpers ---------------------------------------
|
|
|
|
Shift_Type :: enum u8 {
|
|
LSL = 0,
|
|
LSR = 1,
|
|
ASR = 2,
|
|
ROR = 3,
|
|
RRX = 4, // pseudo: encoded as ROR #0
|
|
NONE = 5,
|
|
// Register-shifted-register markers: the shift count comes from the Rs
|
|
// register stored in shift_amt (0..15), not from an immediate. Encoder
|
|
// packs bits 11..8 = Rs, 6..5 = (type - LSL_REG) low 2 bits, bit 4 = 1.
|
|
LSL_REG = 6,
|
|
LSR_REG = 7,
|
|
ASR_REG = 8,
|
|
ROR_REG = 9,
|
|
}
|
|
|
|
Index_Mode :: enum u8 {
|
|
OFFSET = 0, // [Rn, #imm] -- no writeback
|
|
PRE_INDEX = 1, // [Rn, #imm]! -- writeback after addr calc
|
|
POST_INDEX = 2, // [Rn], #imm -- writeback, base = Rn pre-update
|
|
}
|
|
|
|
// Packed into one word: this sits in every Operand, so its width is
|
|
// multiplied by four in every Instruction. Field syntax and composite
|
|
// literals are unchanged, so this is invisible to callers.
|
|
//
|
|
// A memory base or index is always a GPR, whose raw value never exceeds
|
|
// REG_GPR|15 = 0x100F, so 15 bits hold one losslessly (index uses
|
|
// Register(0), not a high sentinel, for "absent").
|
|
// `disp` gets 19 bits (+/-262,143) against a worst case of 4,095 -- the imm12
|
|
// of an A32 load -- so there is ~64x headroom.
|
|
Memory :: bit_field u64 {
|
|
base: Register | 15, // GPR base register
|
|
index: Register | 15, // GPR, or Register(0) for imm-only forms
|
|
shift_type: Shift_Type | 4,
|
|
shift_amt: u8 | 6, // 0..32 -- LSR and ASR reach 32 through a zero field
|
|
mode: Index_Mode | 2,
|
|
sign: i8 | 3, // +1 or -1 (U bit)
|
|
disp: i32 | 19, // immediate displacement (sign-extended)
|
|
}
|
|
#assert(size_of(Memory) == 8)
|
|
|
|
@(require_results)
|
|
mem_imm :: #force_inline proc "contextless" (base: Register, disp: i32) -> Memory {
|
|
return Memory{base = base, disp = disp, sign = 1, mode = .OFFSET}
|
|
}
|
|
@(require_results)
|
|
mem_imm_pre :: #force_inline proc "contextless" (base: Register, disp: i32) -> Memory {
|
|
return Memory{base = base, disp = disp, sign = 1, mode = .PRE_INDEX}
|
|
}
|
|
@(require_results)
|
|
mem_imm_post :: #force_inline proc "contextless" (base: Register, disp: i32) -> Memory {
|
|
return Memory{base = base, disp = disp, sign = 1, mode = .POST_INDEX}
|
|
}
|
|
@(require_results)
|
|
mem_reg :: #force_inline proc "contextless" (base, index: Register, sign: i8 = 1) -> Memory {
|
|
return Memory{base = base, index = index, sign = sign, mode = .OFFSET}
|
|
}
|
|
@(require_results)
|
|
mem_reg_shift :: #force_inline proc "contextless" (
|
|
base, index: Register, st: Shift_Type, amt: u8, sign: i8 = 1,
|
|
) -> Memory {
|
|
return Memory{base = base, index = index, shift_type = st, shift_amt = amt, sign = sign, mode = .OFFSET}
|
|
}
|
|
|
|
// ---- Operand structure -----------------------------------------------------
|
|
|
|
Operand :: struct #packed {
|
|
using _: struct #raw_union #packed {
|
|
// A register operand's shift and lane ride WITH the register instead
|
|
// of in the tail -- that is what keeps Operand at 10 bytes, and they
|
|
// only ever apply to a register anyway. `using` means op.reg,
|
|
// op.shift_type, op.shift_amt and op.lane still read and write
|
|
// exactly as they did when these were separate fields.
|
|
using _: bit_field u32 {
|
|
// Sixteen bits, not fifteen: the class nibble reaches 0x9000 for
|
|
// the endian tokens and 0x8000 for the coprocessor registers,
|
|
// and both were silently truncating.
|
|
reg: Register | 16,
|
|
shift_type: Shift_Type | 4, // GPR_SHIFTED; .LSL/0 when plain
|
|
shift_amt: u8 | 6, // 0..32, or the Rs index for RSR
|
|
lane: u8 | 5, // SIMD lane for DPR_ELEM / QPR_ELEM
|
|
// Whether `lane` means anything. Lane 0 is a real index -- `d0[0]`
|
|
// is not `d0` -- so it cannot be spelled by lane == 0.
|
|
has_lane: bool | 1,
|
|
},
|
|
mem: Memory,
|
|
immediate: i64,
|
|
relative: i64, // label id (pre) or signed byte offset (post)
|
|
},
|
|
kind: Operand_Kind,
|
|
size: u8,
|
|
// How the syntax writes this register as a list. `count` 0 means a plain
|
|
// register. NEON structure loads also come in a spaced form that steps two
|
|
// registers at a time -- `{d2, d4}` -- and a to-all-lanes form written
|
|
// `{d2[]}`. A GPR list (`{r4, lr}`) is not a run at all and stays a
|
|
// bitmask under REG_LIST.
|
|
list: List_Shape,
|
|
}
|
|
|
|
List_Shape :: bit_field u8 {
|
|
count: u8 | 3, // 0 = not a list, else 1..4
|
|
stride: u8 | 3, // 1 for {d2, d3}, 2 for {d2, d4}
|
|
all_lanes: bool | 1, // `{d2[]}` -- loaded to every lane
|
|
}
|
|
#assert(size_of(Operand) == 11)
|
|
|
|
// ---- Operand builders ------------------------------------------------------
|
|
|
|
@(require_results)
|
|
op_reg :: #force_inline proc "contextless" (r: Register) -> Operand {
|
|
return Operand{reg = r, kind = .REGISTER, size = 4}
|
|
}
|
|
@(require_results)
|
|
op_reg_shifted :: #force_inline proc "contextless" (
|
|
r: Register, st: Shift_Type, amt: u8,
|
|
) -> Operand {
|
|
return Operand{reg = r, kind = .REGISTER, size = 4, shift_type = st, shift_amt = amt}
|
|
}
|
|
@(require_results)
|
|
op_imm :: #force_inline proc "contextless" (v: i64, size: u8 = 4) -> Operand {
|
|
return Operand{immediate = v, kind = .IMMEDIATE, size = size}
|
|
}
|
|
@(require_results)
|
|
op_hex_imm :: #force_inline proc "contextless" (v: u32) -> Operand {
|
|
return Operand{immediate = i64(v), kind = .HEX_IMMEDIATE, size = 4}
|
|
}
|
|
@(require_results)
|
|
op_float_imm :: #force_inline proc "contextless" (bits: u32) -> Operand {
|
|
return Operand{immediate = i64(bits), kind = .FLOAT_IMMEDIATE, size = 4}
|
|
}
|
|
@(require_results)
|
|
op_mem :: #force_inline proc "contextless" (m: Memory) -> Operand {
|
|
return Operand{mem = m, kind = .MEMORY, size = 4}
|
|
}
|
|
@(require_results)
|
|
op_label :: #force_inline proc "contextless" (label_id: u32, size: u8 = 4) -> Operand {
|
|
return Operand{relative = i64(label_id), kind = .RELATIVE, size = size}
|
|
}
|
|
@(require_results)
|
|
op_rel_offset :: #force_inline proc "contextless" (off: i64) -> Operand {
|
|
return Operand{relative = off, kind = .RELATIVE, size = 4}
|
|
}
|
|
@(require_results)
|
|
op_reg_list :: #force_inline proc "contextless" (mask: u16) -> Operand {
|
|
return Operand{immediate = i64(mask), kind = .REG_LIST, size = 2}
|
|
}
|
|
|
|
// The head of a register list, written `{d1, d2, d3}` (stride 1) or
|
|
// `{d2, d4}` (stride 2).
|
|
@(require_results)
|
|
op_reg_run :: #force_inline proc "contextless" (first: Register, count: u8, stride: u8 = 1, all_lanes := false) -> Operand {
|
|
return Operand{reg = first, kind = .REGISTER, list = {count = count, stride = stride, all_lanes = all_lanes}}
|
|
}
|
|
@(require_results)
|
|
op_dpr_lane :: #force_inline proc "contextless" (d: Register, idx: u8) -> Operand {
|
|
return Operand{reg = d, kind = .REGISTER, size = 4, lane = idx, has_lane = true}
|
|
}
|
|
@(require_results)
|
|
op_qpr_lane :: #force_inline proc "contextless" (q: Register, idx: u8) -> Operand {
|
|
return Operand{reg = q, kind = .REGISTER, size = 4, lane = idx, has_lane = true}
|
|
}
|