mirror of
https://github.com/odin-lang/Odin.git
synced 2026-09-02 10:13:35 +00:00
TBL2 is gone: SVE2's two-table lookup is spelled `tbl`, and the reason
it could not merge before was that the arity is the caller's choice, so
the matcher needs it in the operand type rather than the encoding. It
has that now, and both forms stay reachable. TBX, which takes no braces,
was briefly given them and is back to plain.
ZERO's operand is a mask with one bit per .d tile, and it is written as
the list of the largest tiles that exactly cover it -- a .s tile being
two .d tiles four apart, a .h tile four two apart. A mask of every bit
is just `{za}`. It had been printing the raw number.
ZA array vectors (`ldr za[w12, 0], [x0]`) are their own operand shape,
with no tile and no element size; they printed as a bare immediate.
The pair and quad register fields were read a bit too wide. A pair
starts on an even register and a quad on a multiple of four, and ZIP and
UZP tell themselves apart with the bits below that -- so every UZP
decoded as naming the register one or two above the one it writes.
SVE/SME2 against llvm-mc: 703 byte-exact of 704, with nothing
mismatched. The one left is a reserved encoding -- XAR with a tsz of
zero names no element size, and llvm-mc calls it invalid too.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018UmHLRF11EoWwNWCJ7JGaA
458 lines
17 KiB
Odin
458 lines
17 KiB
Odin
// rexcode · Brendan Punsky (dotbmp@github), original author
|
|
|
|
package rexcode_arm64
|
|
|
|
// =============================================================================
|
|
// AArch64 OPERANDS
|
|
// =============================================================================
|
|
//
|
|
// AArch64 has a rich addressing repertoire:
|
|
//
|
|
// [Xn] OFFSET with imm=0
|
|
// [Xn, #imm] OFFSET (signed 9 or unsigned scaled 12)
|
|
// [Xn, #imm]! PRE_INDEXED (writeback before)
|
|
// [Xn], #imm POST_INDEXED (writeback after)
|
|
// [Xn, Xm{, LSL #s}] REG_OFFSET (shift = log2(size) when present)
|
|
// [Xn, Wm, SXTW|UXTW|SXTX #s] EXT_REG_OFFSET
|
|
// label LITERAL (PC-relative for LDR literal)
|
|
//
|
|
// `Shift_Type` and `Extend` enumerate the shifter/extender flavours that
|
|
// data-processing register and memory operand encodings need.
|
|
|
|
Operand_Kind :: enum u8 {
|
|
NONE,
|
|
REGISTER,
|
|
IMMEDIATE,
|
|
MEMORY,
|
|
RELATIVE,
|
|
SHIFTED_REG, // X reg + shift type + shift amount
|
|
EXTENDED_REG, // X/W reg + extend + amount
|
|
COND, // 4-bit condition code (EQ/NE/.../AL/NV)
|
|
SYSTEM_REGISTER, // MRS/MSR target, as a packed 15-bit field
|
|
ZA_SLICE, // `za0h.b[w12, 0]` -- a row or column of an SME tile
|
|
}
|
|
|
|
// One slice of an SME accumulator tile: which tile, taken along the rows (h)
|
|
// or the columns (v), addressed by one of W12..W15 plus a fixed offset.
|
|
ZA_Slice :: bit_field u32 {
|
|
tile: u8 | 4,
|
|
vertical: bool | 1,
|
|
ws: u8 | 2, // 0..3, meaning W12..W15
|
|
offset: u8 | 4,
|
|
elem: u8 | 5, // the ZSHAPE_* code the tile is viewed at
|
|
}
|
|
|
|
Shift_Type :: enum u8 {
|
|
LSL = 0,
|
|
LSR = 1,
|
|
ASR = 2,
|
|
ROR = 3,
|
|
}
|
|
|
|
Extend :: enum u8 {
|
|
UXTB = 0,
|
|
UXTH = 1,
|
|
UXTW = 2,
|
|
UXTX = 3,
|
|
SXTB = 4,
|
|
SXTH = 5,
|
|
SXTW = 6,
|
|
SXTX = 7,
|
|
}
|
|
|
|
Address_Mode :: enum u8 {
|
|
OFFSET, // [Xn, #imm] (imm may be 0)
|
|
PRE_INDEXED, // [Xn, #imm]!
|
|
POST_INDEXED, // [Xn], #imm
|
|
REG_OFFSET, // [Xn, Xm{, LSL #s}]
|
|
EXT_REG_OFFSET, // [Xn, Wm, SXTW|UXTW|SXTX #s]
|
|
LITERAL, // PC-rel target (LDR literal)
|
|
}
|
|
|
|
// Memory operand packed into one word: base + optional index + signed disp +
|
|
// addressing metadata. Index is `NONE` for non-register-offset modes.
|
|
//
|
|
// A bit_field rather than a struct because this sits in every Operand, so its
|
|
// width is multiplied by four in every Instruction. Field syntax is unchanged
|
|
// (`m.base`, `m.disp`) and composite literals still work, so this is invisible
|
|
// to callers.
|
|
//
|
|
// Widths: registers get the full 16 bits because the NONE sentinel is 0xFFFF.
|
|
// That leaves 23 bits for `disp` (+/-4.19M) against a worst case of 65,520 --
|
|
// LDR Q, [Xn, #imm12*16] -- the largest displacement any A64 addressing mode
|
|
// can encode, so there is ~64x headroom.
|
|
Memory :: bit_field u64 {
|
|
base: Register | 16,
|
|
index: Register | 16, // NONE for OFFSET/PRE/POST/LITERAL
|
|
disp: i32 | 23,
|
|
extend: Extend | 3, // for EXT_REG_OFFSET; UXTX otherwise
|
|
shift: u8 | 3, // 0..4 for register-offset / extended
|
|
mode: Address_Mode | 3,
|
|
// Full: 16 + 16 + 23 + 3 + 3 + 3 = 64.
|
|
}
|
|
#assert(size_of(Memory) == 8)
|
|
|
|
Shifted_Reg :: struct #packed {
|
|
reg: Register, // 2
|
|
type: Shift_Type, // 1
|
|
amount: u8, // 1 (0..63 for 64-bit; 0..31 for 32-bit)
|
|
}
|
|
#assert(size_of(Shifted_Reg) == 4)
|
|
|
|
Extended_Reg :: struct #packed {
|
|
reg: Register, // 2
|
|
extend: Extend, // 1
|
|
amount: u8, // 1 (0..4)
|
|
}
|
|
#assert(size_of(Extended_Reg) == 4)
|
|
|
|
// 16-byte tagged operand. The union holds whichever payload matches `kind`.
|
|
Operand :: struct #packed {
|
|
using _: struct #raw_union #packed {
|
|
reg: Register, // 2
|
|
mem: Memory, // 8
|
|
immediate: i64, // 8
|
|
relative: i64, // 8
|
|
shifted: Shifted_Reg, // 8
|
|
extended: Extended_Reg, // 8
|
|
cond: u8, // 1
|
|
sysreg: System_Register, // 2
|
|
za: ZA_Slice, // 4
|
|
}, // 12 total because of alignment
|
|
kind: Operand_Kind, // 1
|
|
size: u8, // 1 -- carried width info; meaning varies
|
|
// How many consecutive registers the syntax writes as a list, starting at
|
|
// `reg`: `{v0.16b, v1.16b}` is 2. Zero means the operand is a plain
|
|
// register. The count belongs to the instruction form rather than to the
|
|
// caller -- LD2 always names two -- so it comes from the encoding.
|
|
list_count: u8, // 1
|
|
}
|
|
#assert(size_of(Operand) == 11)
|
|
|
|
// -----------------------------------------------------------------------------
|
|
// Constructors -- generic
|
|
// -----------------------------------------------------------------------------
|
|
|
|
@(require_results)
|
|
op_reg :: #force_inline proc "contextless" (r: Register) -> Operand {
|
|
return Operand{reg = r, kind = .REGISTER, size = 4}
|
|
}
|
|
@(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_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_mem :: #force_inline proc "contextless" (m: Memory) -> Operand {
|
|
return Operand{mem = m, kind = .MEMORY, size = 4}
|
|
}
|
|
|
|
@(require_results)
|
|
op_shifted :: #force_inline proc "contextless" (r: Register, type: Shift_Type, amount: u8) -> Operand {
|
|
return Operand{shifted = Shifted_Reg{reg = r, type = type, amount = amount}, kind = .SHIFTED_REG, size = 4}
|
|
}
|
|
|
|
@(require_results)
|
|
op_extended :: #force_inline proc "contextless" (r: Register, ext: Extend, amount: u8) -> Operand {
|
|
return Operand{extended = Extended_Reg{reg = r, extend = ext, amount = amount}, kind = .EXTENDED_REG, size = 4}
|
|
}
|
|
|
|
@(require_results)
|
|
op_cond :: #force_inline proc "contextless" (c: Cond) -> Operand {
|
|
return Operand{cond = u8(c), kind = .COND, size = 1}
|
|
}
|
|
|
|
// A vector lane index. It is a plain immediate in the encoding, but it prints
|
|
// glued to the register it indexes (`v2.s[3]`) rather than as a separate
|
|
// operand, so it is marked to tell it apart from an immediate that really is
|
|
// one -- EXT's byte index, for instance, is written `#3`.
|
|
LANE_INDEX :: u8(0xFF)
|
|
|
|
// SVE writes its element-count pattern by name (`vl8`, `mul3`, `all`) and its
|
|
// multiplier as `mul #N`, so both need telling apart from a plain immediate.
|
|
SVE_PATTERN_IMM :: u8(0xFE)
|
|
SVE_MUL_IMM :: u8(0xFD)
|
|
|
|
// ZERO's operand is an 8-bit mask, one bit per .d tile, written as the list of
|
|
// the largest tiles that exactly cover it: a .s tile is two .d tiles four
|
|
// apart, a .h tile is four two apart, and the single .b tile is all eight.
|
|
ZA_TILE_MASK :: u8(0xFC)
|
|
|
|
// The 32 SVE element-count patterns; the gaps are reserved and print as a
|
|
// bare number.
|
|
@(rodata)
|
|
SVE_PATTERN_NAMES := [32]string{
|
|
"pow2", "vl1", "vl2", "vl3", "vl4", "vl5", "vl6", "vl7",
|
|
"vl8", "vl16", "vl32", "vl64", "vl128", "vl256", "", "",
|
|
"", "", "", "", "", "", "", "",
|
|
"", "", "", "", "", "mul4", "mul3", "all",
|
|
}
|
|
|
|
@(require_results)
|
|
op_lane_index :: #force_inline proc "contextless" (index: i64) -> Operand {
|
|
return Operand{immediate = index, kind = .IMMEDIATE, size = LANE_INDEX}
|
|
}
|
|
|
|
op_sysreg :: #force_inline proc "contextless" (sr: System_Register) -> Operand {
|
|
return Operand{sysreg = sr, kind = .SYSTEM_REGISTER, size = 2}
|
|
}
|
|
|
|
@(require_results)
|
|
op_za_slice :: #force_inline proc "contextless" (tile, ws, offset, elem: u8, vertical := false) -> Operand {
|
|
return Operand{
|
|
za = ZA_Slice{tile = tile, vertical = vertical, ws = ws, offset = offset, elem = elem},
|
|
kind = .ZA_SLICE, size = elem,
|
|
}
|
|
}
|
|
|
|
// -----------------------------------------------------------------------------
|
|
// SVE Z-register builders -- encode the element arrangement in op.size
|
|
// (B=1, H=2, S=4, D=8). Matcher uses op.size to disambiguate the right
|
|
// table form when multiple element sizes share a base mnemonic.
|
|
// -----------------------------------------------------------------------------
|
|
|
|
@(require_results)
|
|
op_z_b :: #force_inline proc "contextless" (n: u8) -> Operand {
|
|
return Operand{reg = Register(REG_Z | u16(n & 0x1F)), kind = .REGISTER, size = 1}
|
|
}
|
|
@(require_results)
|
|
op_z_h :: #force_inline proc "contextless" (n: u8) -> Operand {
|
|
return Operand{reg = Register(REG_Z | u16(n & 0x1F)), kind = .REGISTER, size = 2}
|
|
}
|
|
@(require_results)
|
|
op_z_s :: #force_inline proc "contextless" (n: u8) -> Operand {
|
|
return Operand{reg = Register(REG_Z | u16(n & 0x1F)), kind = .REGISTER, size = 4}
|
|
}
|
|
@(require_results)
|
|
op_z_d :: #force_inline proc "contextless" (n: u8) -> Operand {
|
|
return Operand{reg = Register(REG_Z | u16(n & 0x1F)), kind = .REGISTER, size = 8}
|
|
}
|
|
|
|
// SVE packs an element size and a shift amount into one field, `tszh:tszl:imm3`,
|
|
// as `V = 2*esize - shift`. Because the shift is in [1, esize], V lands in
|
|
// [esize, 2*esize), so the four element sizes occupy disjoint ranges and the
|
|
// position of the highest set bit names the size:
|
|
//
|
|
// .b V in [ 8, 15] .s V in [32, 63]
|
|
// .h V in [16, 31] .d V in [64, 127]
|
|
//
|
|
// Encoder and decoder both need this, and it is written once here so the two
|
|
// cannot drift apart.
|
|
|
|
// The element width, in bits, that a packed tsz value names.
|
|
@(require_results)
|
|
sve_tsz_esize :: #force_inline proc "contextless" (v: u32) -> u32 {
|
|
switch {
|
|
case v >= 64: return 64
|
|
case v >= 32: return 32
|
|
case v >= 16: return 16
|
|
}
|
|
return 8
|
|
}
|
|
|
|
// The shift amount a packed tsz value names.
|
|
@(require_results)
|
|
sve_tsz_shift :: #force_inline proc "contextless" (v: u32) -> u32 {
|
|
return 2 * sve_tsz_esize(v) - v
|
|
}
|
|
|
|
// The packed tsz value for an element width and shift.
|
|
@(require_results)
|
|
sve_tsz_pack :: #force_inline proc "contextless" (esize, shift: u32) -> u32 {
|
|
return (2 * esize - shift) & 0x7F
|
|
}
|
|
|
|
// The SVE element-width code (the `size` an operand carries) for a width in
|
|
// bits: .b = 1, .h = 2, .s = 4, .d = 8.
|
|
@(require_results)
|
|
sve_esize_code :: #force_inline proc "contextless" (esize: u32) -> u8 {
|
|
return u8(esize / 8)
|
|
}
|
|
|
|
// A predicate register's governing qualifier, carried in Operand.size and read
|
|
// only when the register's class is REG_P. SVE writes it as a suffix -- `p0/z`
|
|
// zeroes the inactive lanes, `p0/m` leaves them alone -- and an assembler will
|
|
// not take the instruction without it where the form calls for one.
|
|
PQUAL_ZERO :: u8(1)
|
|
PQUAL_MERGE :: u8(2)
|
|
PQUAL_NONE :: u8(4)
|
|
|
|
// A predicate that is an instruction's DESTINATION is written with an element
|
|
// size instead -- `cmpge p0.b, p1/z, ...` -- so those codes have to live apart
|
|
// from the qualifiers above, and apart from the neutral 4 a plain op_reg gives.
|
|
PSHAPE_B :: u8(20)
|
|
PSHAPE_H :: u8(21)
|
|
PSHAPE_S :: u8(22)
|
|
PSHAPE_D :: u8(23)
|
|
|
|
// Arrangement codes carried in Operand.size. Element views are odd and
|
|
// arrangements are multiples of 8, so the two can never be confused; 4 is the
|
|
// neutral "no vector shape" value every scalar class uses.
|
|
VSHAPE_NONE :: u8(4)
|
|
VSHAPE_8B :: u8(8)
|
|
VSHAPE_16B :: u8(16)
|
|
VSHAPE_4H :: u8(24)
|
|
VSHAPE_8H :: u8(32)
|
|
VSHAPE_2S :: u8(40)
|
|
VSHAPE_4S :: u8(48)
|
|
VSHAPE_1D :: u8(56)
|
|
VSHAPE_2D :: u8(64)
|
|
VSHAPE_1Q :: u8(72)
|
|
VSHAPE_ELEM_B :: u8(1)
|
|
VSHAPE_ELEM_H :: u8(3)
|
|
VSHAPE_ELEM_S :: u8(5)
|
|
VSHAPE_ELEM_D :: u8(7)
|
|
|
|
// SVE element-width codes carried in Operand.size for a Z register.
|
|
ZSHAPE_B :: u8(1)
|
|
ZSHAPE_H :: u8(2)
|
|
ZSHAPE_S :: u8(4)
|
|
ZSHAPE_D :: u8(8)
|
|
ZSHAPE_Q :: u8(16)
|
|
|
|
// -----------------------------------------------------------------------------
|
|
// NEON V-register arrangement builders. These take the register the caller
|
|
// actually has, not its number: rebuilding one from `reg_hw` would relabel an
|
|
// X register as a V register, and the matcher -- which checks reg_class --
|
|
// would never get to reject it. Passing a non-V register here now simply
|
|
// matches no form, and encode reports it.
|
|
//
|
|
// op.size encodes lanes*elem-bytes:
|
|
// .8B = 8 .16B = 16
|
|
// .4H = 24 .8H = 32
|
|
// .2S = 40 .4S = 48
|
|
// .1D = 56 .2D = 64
|
|
// (Encoded so that no two arrangements collide and so the value is easy
|
|
// to inspect.)
|
|
// -----------------------------------------------------------------------------
|
|
|
|
@(require_results)
|
|
op_v_8b :: #force_inline proc "contextless" (r: Register) -> Operand {
|
|
return Operand{reg = r, kind = .REGISTER, size = 8}
|
|
}
|
|
@(require_results)
|
|
op_v_16b :: #force_inline proc "contextless" (r: Register) -> Operand {
|
|
return Operand{reg = r, kind = .REGISTER, size = 16}
|
|
}
|
|
@(require_results)
|
|
op_v_4h :: #force_inline proc "contextless" (r: Register) -> Operand {
|
|
return Operand{reg = r, kind = .REGISTER, size = 24}
|
|
}
|
|
@(require_results)
|
|
op_v_8h :: #force_inline proc "contextless" (r: Register) -> Operand {
|
|
return Operand{reg = r, kind = .REGISTER, size = 32}
|
|
}
|
|
@(require_results)
|
|
op_v_2s :: #force_inline proc "contextless" (r: Register) -> Operand {
|
|
return Operand{reg = r, kind = .REGISTER, size = 40}
|
|
}
|
|
@(require_results)
|
|
op_v_4s :: #force_inline proc "contextless" (r: Register) -> Operand {
|
|
return Operand{reg = r, kind = .REGISTER, size = 48}
|
|
}
|
|
@(require_results)
|
|
op_v_1d :: #force_inline proc "contextless" (r: Register) -> Operand {
|
|
return Operand{reg = r, kind = .REGISTER, size = 56}
|
|
}
|
|
@(require_results)
|
|
op_v_2d :: #force_inline proc "contextless" (r: Register) -> Operand {
|
|
return Operand{reg = r, kind = .REGISTER, size = 64}
|
|
}
|
|
// .1q breaks the lanes*elem-bytes rule the others follow (1*16 would collide
|
|
// with 16B), so it gets the next free multiple of 8.
|
|
@(require_results)
|
|
op_v_1q :: #force_inline proc "contextless" (r: Register) -> Operand {
|
|
return Operand{reg = r, kind = .REGISTER, size = 72}
|
|
}
|
|
// A run of `count` consecutive registers written as a list, `{v0.16b, v1.16b}`.
|
|
// `shape` is one of the VSHAPE_* codes.
|
|
@(require_results)
|
|
op_v_list :: #force_inline proc "contextless" (first: Register, shape, count: u8) -> Operand {
|
|
return Operand{reg = first, kind = .REGISTER, size = shape, list_count = count}
|
|
}
|
|
|
|
// Element-indexed V views (V0.B[i]/.H[i]/.S[i]/.D[i]). The element size rides
|
|
// in op.size so the matcher can disambiguate DUP/INS forms; the lane index is
|
|
// a separate immediate operand.
|
|
//
|
|
// The codes are ODD (1/3/5/7) on purpose: arrangement operands above use
|
|
// multiples of 8, so a size can never mean both. They used to be 1/2/4/8,
|
|
// which made an element-D view indistinguishable from an 8B arrangement --
|
|
// the printer cannot tell `.d` from `.8b` if both are size 8.
|
|
@(require_results)
|
|
op_v_elem_b :: #force_inline proc "contextless" (r: Register) -> Operand {
|
|
return Operand{reg = r, kind = .REGISTER, size = 1}
|
|
}
|
|
@(require_results)
|
|
op_v_elem_h :: #force_inline proc "contextless" (r: Register) -> Operand {
|
|
return Operand{reg = r, kind = .REGISTER, size = 3}
|
|
}
|
|
@(require_results)
|
|
op_v_elem_s :: #force_inline proc "contextless" (r: Register) -> Operand {
|
|
return Operand{reg = r, kind = .REGISTER, size = 5}
|
|
}
|
|
@(require_results)
|
|
op_v_elem_d :: #force_inline proc "contextless" (r: Register) -> Operand {
|
|
return Operand{reg = r, kind = .REGISTER, size = 7}
|
|
}
|
|
|
|
// -----------------------------------------------------------------------------
|
|
// Memory constructors (one per addressing mode)
|
|
// -----------------------------------------------------------------------------
|
|
|
|
@(require_results)
|
|
mem_offset :: #force_inline proc "contextless" (base: Register, disp: i32 = 0) -> Memory {
|
|
return Memory{base = base, index = NONE, disp = disp, mode = .OFFSET}
|
|
}
|
|
@(require_results)
|
|
mem_pre :: #force_inline proc "contextless" (base: Register, disp: i32) -> Memory {
|
|
return Memory{base = base, index = NONE, disp = disp, mode = .PRE_INDEXED}
|
|
}
|
|
@(require_results)
|
|
mem_post :: #force_inline proc "contextless" (base: Register, disp: i32) -> Memory {
|
|
return Memory{base = base, index = NONE, disp = disp, mode = .POST_INDEXED}
|
|
}
|
|
@(require_results)
|
|
mem_reg :: #force_inline proc "contextless" (base, index: Register, shift_amount: u8 = 0) -> Memory {
|
|
return Memory{base = base, index = index, mode = .REG_OFFSET, shift = shift_amount, extend = .UXTX}
|
|
}
|
|
@(require_results)
|
|
mem_ext :: #force_inline proc "contextless" (base, index: Register, ext: Extend, shift_amount: u8 = 0) -> Memory {
|
|
return Memory{base = base, index = index, mode = .EXT_REG_OFFSET, extend = ext, shift = shift_amount}
|
|
}
|
|
|
|
// -----------------------------------------------------------------------------
|
|
// Condition codes
|
|
// -----------------------------------------------------------------------------
|
|
|
|
Cond :: enum u8 {
|
|
EQ = 0x0,
|
|
NE = 0x1,
|
|
CS = 0x2, // unsigned higher or same (alias HS)
|
|
CC = 0x3, // unsigned lower (alias LO)
|
|
MI = 0x4,
|
|
PL = 0x5,
|
|
VS = 0x6,
|
|
VC = 0x7,
|
|
HI = 0x8,
|
|
LS = 0x9,
|
|
GE = 0xA,
|
|
LT = 0xB,
|
|
GT = 0xC,
|
|
LE = 0xD,
|
|
AL = 0xE,
|
|
NV = 0xF,
|
|
}
|
|
|
|
// Architectural aliases for the two carry-style conditions.
|
|
COND_HS :: Cond.CS
|
|
COND_LO :: Cond.CC
|