mirror of
https://github.com/odin-lang/Odin.git
synced 2026-09-02 02:03:35 +00:00
x86 keeps Instruction at 64 bytes -- one cache line -- by packing its memory
operand into a bit_field u64 rather than a struct. arm32 and arm64 both used
a 12-byte Memory struct, and since Memory sits in every Operand that width is
multiplied by four in every Instruction. Adopting x86's trick, plus two
smaller things, takes both ARM ISAs under the cache line.
Instruction ops[4] Operand Memory
x86 64 48 12 8
arm32 88 -> 48 72->40 18->10 12->8
arm64 64 -> 48 56->40 14->10 12->8
Memory -> bit_field u64, both ISAs. Field syntax and composite literals are
unchanged, so callers see nothing. Registers keep their type: an arm64
Register never exceeds 0x0C1F and an arm32 one never exceeds 0x401F, but the
arm64 NONE sentinel is 0xFFFF, so arm64 gives them the full 16 bits and arm32
15. What is left goes to `disp`: 23 bits on arm64 (worst case 65,520, from
LDR Q, [Xn, #imm12*16]) and 19 on arm32 (worst case 4,095, an A32 imm12) --
64x and 32x headroom respectively.
arm32 Operand also carried four tail bytes arm64 does not. `cond` was dead:
nine builders wrote it and nothing in the package ever read it, and
Instruction.cond already exists. shift_type/shift_amt/lane now ride inside
the union alongside the register they describe -- they only ever apply to a
register operand -- via a `using` bit_field, so op.reg, op.shift_type,
op.shift_amt and op.lane still read and write exactly as before.
arm32 Instruction packs cond, operand_count, mode, length and the two flag
bits into one 16-bit word; they need 13 bits between them and were spending
six bytes. `using` again keeps the field names, with the one exception that
inst.flags.sets_flags is now inst.sets_flags (five call sites).
Verified: every rexcode suite matches baseline; both generators stay
idempotent; arm64 is 73/73 byte-exact against llvm-mc on both encode and
decode round-trip; arm32's 1680/1680 sweep still passes and a memory/shift
encode spot-check is byte-identical to what the same code produced before
this commit.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
168 lines
6.2 KiB
Odin
168 lines
6.2 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)
|
|
}
|
|
|
|
// ---- 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 Register's raw value never exceeds REG_QPR|31 = 0x401F, 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 | 5, // 0..31 immediate shift
|
|
mode: Index_Mode | 2,
|
|
sign: i8 | 3, // +1 or -1 (U bit)
|
|
disp: i32 | 19, // immediate displacement (sign-extended)
|
|
// 1 bit spare
|
|
}
|
|
#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 {
|
|
reg: Register | 15,
|
|
shift_type: Shift_Type | 4, // GPR_SHIFTED; .LSL/0 when plain
|
|
shift_amt: u8 | 5, // 0..31, or the Rs index for RSR
|
|
lane: u8 | 5, // SIMD lane for DPR_ELEM / QPR_ELEM
|
|
// 3 bits spare
|
|
},
|
|
mem: Memory,
|
|
immediate: i64,
|
|
relative: i64, // label id (pre) or signed byte offset (post)
|
|
},
|
|
kind: Operand_Kind,
|
|
size: u8,
|
|
}
|
|
#assert(size_of(Operand) == 10)
|
|
|
|
// ---- 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_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}
|
|
}
|
|
@(require_results)
|
|
op_dpr_lane :: #force_inline proc "contextless" (d: Register, idx: u8) -> Operand {
|
|
return Operand{reg = d, kind = .REGISTER, size = 4, lane = idx}
|
|
}
|
|
@(require_results)
|
|
op_qpr_lane :: #force_inline proc "contextless" (q: Register, idx: u8) -> Operand {
|
|
return Operand{reg = q, kind = .REGISTER, size = 4, lane = idx}
|
|
}
|