Files
Odin/core/rexcode/isa/arm64/encoder.odin
Brendan Punsky 22292b9d35 rexcode/arm64: the last of the SVE and SME gaps
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
2026-08-28 19:36:08 -04:00

1021 lines
39 KiB
Odin

// rexcode · Brendan Punsky (dotbmp@github), original author
package rexcode_arm64
// =============================================================================
// AArch64 ENCODER
// =============================================================================
//
// Fixed-width 4-byte ISA. Two-pass design mirroring mips/encoder.odin /
// riscv/encoder.odin. The interesting bits vs other arches:
//
// * Compound operands: SHIFTED_REG (Rm + shift type + amount) and
// EXTENDED_REG (Rm + extend + amount) are packed by the RM encoder
// by inspecting the operand kind -- a plain REGISTER decays to
// LSL #0 / UXTX, amount=0.
//
// * Three split-immediate scatter patterns:
// BRANCH_PG21 -- 21-bit imm split as immlo[30:29] + immhi[23:5]
// (ADR / ADRP)
// TBZ_BIT -- 6-bit bit position split as b5[31] + b40[23:19]
// (TBZ / TBNZ)
// SYS_FIELD -- 15-bit (op0:op1:CRn:CRm:op2) at bits 19:5
//
// * Loads/stores with the unsigned-offset (LDR/STR) form scale the
// user displacement by data size (1/2/4/8) derived from bits[31:30]
// of the encoding. LDP/STP pair forms scale a signed 7-bit field.
//
// * Endianness: AArch64 standard mode stores instructions LE; BE-32
// (instructions stored big-endian) is legacy and rare. Parameter
// defaults to LITTLE.
MAX_INST_SIZE :: 4
encode_max_code_size :: #force_inline proc "contextless" (instructions: []Instruction) -> int {
return len(instructions) * MAX_INST_SIZE
}
encode_max_relocation_count :: #force_inline proc "contextless" (instructions: []Instruction) -> int {
return len(instructions)
}
// Pre-size the caller's encode outputs (code grown by length so code[:] is a
// valid emit target; relocs reserved by capacity) so the encode hot path never
// reallocates. Allocates no new buffers; pass nil to skip either array.
encode_reserve :: proc(code: ^[dynamic]u8, relocs: ^[dynamic]Relocation, instructions: []Instruction) {
if code != nil {
size := encode_max_code_size(instructions)
if len(code) < size {
resize(code, size)
}
}
if relocs != nil {
reserve(relocs, len(relocs) + encode_max_relocation_count(instructions))
}
}
encode :: proc(
instructions: []Instruction,
label_defs: []Label_Definition,
code: []u8,
relocs: ^[dynamic]Relocation,
errors: ^[dynamic]Error,
endianness: Endianness = .LITTLE,
resolve: bool = true,
base_address: u64 = 0,
) -> (byte_count: u32, ok: bool) {
n_inst := u32(len(instructions))
if u32(len(code)) < n_inst * 4 {
append(errors, Error{inst_idx = 0, code = .BUFFER_OVERFLOW})
return
}
errors_start := u32(len(errors))
pending_start := u32(len(relocs))
// ---- PASS 1 -----------------------------------------------------------
for i in 0..<n_inst {
inst := &instructions[i]
word := encode_one_inline(inst, byte_count, u16(i), relocs, errors) or_return
write_u32(code, byte_count, word, endianness)
byte_count += 4
}
// ---- PASS 1.5: fixed-width => *4 -------------------------------------
for &ld in label_defs {
if ld != LABEL_UNDEFINED {
ld = Label_Definition(u32(ld) * 4)
}
}
if !resolve {
ok = u32(len(errors)) == errors_start
return
}
// ---- PASS 2: resolve relocations -------------------------------------
n_relocs := u32(len(relocs))
write_idx := pending_start
for read_idx in pending_start..<n_relocs {
r := relocs[read_idx]
if resolve_relocation_inline(code, label_defs, &r, endianness, base_address, errors) {
continue
}
if write_idx != read_idx { relocs[write_idx] = r }
write_idx += 1
}
if write_idx != n_relocs { resize(relocs, int(write_idx)) }
ok = u32(len(errors)) == errors_start
return
}
// =============================================================================
// Internal: encode one instruction
// =============================================================================
@(private="file")
encode_one_inline :: #force_inline proc(
inst: ^Instruction,
pc: u32,
inst_idx: u16,
relocs: ^[dynamic]Relocation,
errors: ^[dynamic]Error,
) -> (word: u32, ok: bool) {
if inst.mnemonic == .INVALID {
append(errors, Error{inst_idx = u32(inst_idx), code = .INVALID_MNEMONIC})
return 0, false
}
forms := encoding_forms(inst.mnemonic)
if len(forms) == 0 {
append(errors, Error{inst_idx = u32(inst_idx), code = .INVALID_MNEMONIC})
return 0, false
}
form: ^Encoding
for &f in forms {
if encoding_matches_inline(inst, &f) { form = &f; break }
}
if form == nil {
append(errors, Error{inst_idx = u32(inst_idx), code = .NO_MATCHING_ENCODING})
return 0, false
}
word = form.bits
if form.enc[0] != .NONE { word |= pack_operand_inline(&inst.ops[0], form.enc[0], form, pc, inst_idx, relocs, inst) }
if form.enc[1] != .NONE { word |= pack_operand_inline(&inst.ops[1], form.enc[1], form, pc, inst_idx, relocs, inst) }
if form.enc[2] != .NONE { word |= pack_operand_inline(&inst.ops[2], form.enc[2], form, pc, inst_idx, relocs, inst) }
if form.enc[3] != .NONE { word |= pack_operand_inline(&inst.ops[3], form.enc[3], form, pc, inst_idx, relocs, inst) }
if form.enc[4] != .NONE { word |= pack_operand_inline(&inst.ops[4], form.enc[4], form, pc, inst_idx, relocs, inst) }
return word, true
}
@(private="file")
encoding_matches_inline :: #force_inline proc "contextless" (
inst: ^Instruction, form: ^Encoding,
) -> bool {
return operand_matches_inline(&inst.ops[0], form.ops[0], form) &&
operand_matches_inline(&inst.ops[1], form.ops[1], form) &&
operand_matches_inline(&inst.ops[2], form.ops[2], form) &&
operand_matches_inline(&inst.ops[3], form.ops[3], form) &&
operand_matches_inline(&inst.ops[4], form.ops[4], form)
}
@(private="file")
operand_matches_inline :: #force_inline proc "contextless" (
op: ^Operand, ot: Operand_Type, form: ^Encoding,
) -> bool {
switch ot {
case .NONE:
return op.kind == .NONE
case .W_REG:
return op.kind == .REGISTER && reg_class(op.reg) == REG_W
case .X_REG:
return op.kind == .REGISTER && reg_class(op.reg) == REG_X
case .WSP_REG:
return op.kind == .REGISTER && (reg_class(op.reg) == REG_W || reg_class(op.reg) == REG_WSP)
case .XSP_REG:
return op.kind == .REGISTER && (reg_class(op.reg) == REG_X || reg_class(op.reg) == REG_XSP)
case .W_SHIFTED:
if op.kind == .REGISTER { return reg_class(op.reg) == REG_W }
if op.kind == .SHIFTED_REG { return reg_class(op.shifted.reg) == REG_W }
return false
case .X_SHIFTED:
if op.kind == .REGISTER { return reg_class(op.reg) == REG_X }
if op.kind == .SHIFTED_REG { return reg_class(op.shifted.reg) == REG_X }
return false
case .W_EXTENDED:
// The extend type selects W vs X for the inner reg; accept either
// and let the encoder pack option = extend faithfully.
if op.kind == .REGISTER { return reg_class(op.reg) == REG_W }
if op.kind == .EXTENDED_REG {
c := reg_class(op.extended.reg)
return c == REG_W || c == REG_X
}
return false
case .X_EXTENDED:
if op.kind == .REGISTER { return reg_class(op.reg) == REG_X }
if op.kind == .EXTENDED_REG {
c := reg_class(op.extended.reg)
return c == REG_W || c == REG_X
}
return false
case .B_REG: return op.kind == .REGISTER && reg_class(op.reg) == REG_B
case .H_REG: return op.kind == .REGISTER && reg_class(op.reg) == REG_H
case .S_REG: return op.kind == .REGISTER && reg_class(op.reg) == REG_S
case .D_REG: return op.kind == .REGISTER && reg_class(op.reg) == REG_D
case .Q_REG: return op.kind == .REGISTER && reg_class(op.reg) == REG_Q
case .V_REG: return op.kind == .REGISTER && reg_class(op.reg) == REG_V
// NEON vector arrangement variants. The user encodes the arrangement
// via op.size: 8B=8, 16B=16, 4H=24, 8H=32, 2S=40, 4S=48, 1D=56, 2D=64.
// (lanes * elem_bytes; unique per arrangement). When op.size==0 the
// matcher accepts any V register (legacy / "first form wins") --
// callers using op_reg() get size=4 by default which matches the
// .V_4H form arithmetically; prefer the explicit op_v_*() builders.
case .V_8B:
return op.kind == .REGISTER && reg_class(op.reg) == REG_V && (op.size == 0 || op.size == 8)
case .V_16B:
return op.kind == .REGISTER && reg_class(op.reg) == REG_V && (op.size == 0 || op.size == 16)
case .V_4H:
return op.kind == .REGISTER && reg_class(op.reg) == REG_V && (op.size == 0 || op.size == 24)
case .V_8H, .V_8H_FP16:
return op.kind == .REGISTER && reg_class(op.reg) == REG_V && (op.size == 0 || op.size == 32)
case .V_2S:
return op.kind == .REGISTER && reg_class(op.reg) == REG_V && (op.size == 0 || op.size == 40)
case .V_4S:
return op.kind == .REGISTER && reg_class(op.reg) == REG_V && (op.size == 0 || op.size == 48)
case .V_1D:
return op.kind == .REGISTER && reg_class(op.reg) == REG_V && (op.size == 0 || op.size == 56)
case .V_2D:
return op.kind == .REGISTER && reg_class(op.reg) == REG_V && (op.size == 0 || op.size == 64)
case .V_1Q:
return op.kind == .REGISTER && reg_class(op.reg) == REG_V && (op.size == 0 || op.size == 72)
case .V_4H_FP16:
return op.kind == .REGISTER && reg_class(op.reg) == REG_V && (op.size == 0 || op.size == 24)
// Element-indexed V views: element size carried in op.size (B=1,H=2,S=4,
// D=8) so DUP/INS forms disambiguate. .S also accepts size 0 so a plain
// op_reg (as the hand-written SM3TT forms pass) still matches the .S slot.
// Element-view sizes are odd (1/3/5/7) so they can never be confused with
// an arrangement, which is always a multiple of 8 (see op_v_elem_*).
case .V_ELEM_B:
return op.kind == .REGISTER && reg_class(op.reg) == REG_V && op.size == 1
case .V_ELEM_H:
return op.kind == .REGISTER && reg_class(op.reg) == REG_V && op.size == 3
case .V_ELEM_S:
return op.kind == .REGISTER && reg_class(op.reg) == REG_V && (op.size == 5 || op.size == 0)
case .V_ELEM_D:
return op.kind == .REGISTER && reg_class(op.reg) == REG_V && op.size == 7
// SVE Z registers. Element size carried in op.size: B=1, H=2, S=4, D=8.
// op.size==0 (legacy / default-constructed) accepts any width.
case .Z_REG_B:
return op.kind == .REGISTER && reg_class(op.reg) == REG_Z && (op.size == 0 || op.size == 1)
case .Z_REG_H:
return op.kind == .REGISTER && reg_class(op.reg) == REG_Z && (op.size == 0 || op.size == 2)
case .Z_REG_S:
return op.kind == .REGISTER && reg_class(op.reg) == REG_Z && (op.size == 0 || op.size == 4)
case .Z_REG_D:
return op.kind == .REGISTER && reg_class(op.reg) == REG_Z && (op.size == 0 || op.size == 8)
case .P_REG, .P_REG_MERGE, .P_REG_ZERO, .P_REG_GOV,
.P_REG_B, .P_REG_H, .P_REG_S, .P_REG_D:
return op.kind == .REGISTER && reg_class(op.reg) == REG_P
case .PN_REG, .PN_REG_ZERO:
return op.kind == .REGISTER && reg_class(op.reg) == REG_PN
case .ZT_REG:
return op.kind == .REGISTER && reg_class(op.reg) == REG_ZT
case .ZA_ARRAY:
return op.kind == .ZA_SLICE
case .Z_LIST1_B, .Z_LIST1_H, .Z_LIST1_S, .Z_LIST1_D:
return op.kind == .REGISTER && reg_class(op.reg) == REG_Z && op.list_count <= 1
case .Z_LIST2_B:
return op.kind == .REGISTER && reg_class(op.reg) == REG_Z && op.list_count == 2
// The first register of a pair must be even, of a quad a multiple of four.
case .Z_REG_ANY:
return op.kind == .REGISTER && reg_class(op.reg) == REG_Z
case .Z_PAIR_B, .Z_PAIR_H, .Z_PAIR_S, .Z_PAIR_D:
return op.kind == .REGISTER && reg_class(op.reg) == REG_Z && (reg_hw(op.reg) & 0x1) == 0 &&
(op.size == 0 || op.size == z_elem_size(ot))
case .Z_QUAD_B, .Z_QUAD_H, .Z_QUAD_S, .Z_QUAD_D:
return op.kind == .REGISTER && reg_class(op.reg) == REG_Z && (reg_hw(op.reg) & 0x3) == 0 &&
(op.size == 0 || op.size == z_elem_size(ot))
// SME tile state (immediate-encoded tile number; user supplies the
// tile index as an immediate, e.g. 0 for ZA0.S, 3 for ZA3.S).
case .ZA_TILE_B, .ZA_TILE_H, .ZA_TILE_S, .ZA_TILE_D, .ZA_TILE_Q:
if op.kind == .REGISTER { return reg_class(op.reg) == REG_ZA }
return op.kind == .IMMEDIATE
// Misc immediate sub-types added in batch 3
case .FCMLA_ROT, .FCADD_ROT, .SVE_PRFOP, .LDRAA_IMM10:
return op.kind == .IMMEDIATE
case .LSL_SHIFT_W, .LSL_SHIFT_X, .ROR_SHIFT:
return op.kind == .IMMEDIATE
case .SME_PATTERN, .SVE_PATTERN:
return op.kind == .IMMEDIATE
// SME tile slice (packed immediate descriptor; see encoding_types.odin)
case .SME_SLICE_B, .SME_SLICE_H, .SME_SLICE_W, .SME_SLICE_D, .SME_SLICE_Q:
return op.kind == .ZA_SLICE
case .IMM_12, .IMM_16, .IMM_8, .IMM_6, .IMM_5, .IMM_4, .IMM_3, .IMM_2,
.NZCV_IMM, .PSTATE_FIELD, .HW_SHIFT, .LSE_SIZE, .VEC_SHIFT, .VEC_INDEX,
.IMM_MUL4:
return op.kind == .IMMEDIATE
case .SYS_REG:
return op.kind == .SYSTEM_REGISTER
case .BITMASK_IMM:
// The user passes the raw logical mask value; we validate that it
// fits the AArch64 bitmask-immediate encoding at the form's width.
return op.kind == .IMMEDIATE && is_valid_bitmask_imm(u64(op.immediate), form.flags.is_64)
case .REL_26, .REL_19, .REL_14, .REL_PG21:
return op.kind == .RELATIVE
// Memory: the addressing mode is what selects between the forms of one
// mnemonic (LDR [Xn,#i] / [Xn,#i]! / [Xn],#i / [Xn,Xm]), so it has to
// be matched, not just packed.
case .MEM_OFFSET:
return op.kind == .MEMORY && op.mem.mode == .OFFSET
case .MEM_PRE:
return op.kind == .MEMORY && op.mem.mode == .PRE_INDEXED
case .MEM_POST:
return op.kind == .MEMORY && op.mem.mode == .POST_INDEXED
case .MEM_REG:
return op.kind == .MEMORY && op.mem.mode == .REG_OFFSET
case .MEM_EXT:
return op.kind == .MEMORY && op.mem.mode == .EXT_REG_OFFSET
// SVE: the plain mode is not enough -- a gather's scalar+scalar and
// scalar+vector forms are both REG_OFFSET and differ only in whether
// the index (or base) is a Z register.
case .MEM_SVE_SS:
return op.kind == .MEMORY && op.mem.mode == .REG_OFFSET && reg_class(op.mem.index) != REG_Z
case .MEM_SVE_SI:
return op.kind == .MEMORY && op.mem.mode == .OFFSET && reg_class(op.mem.base) != REG_Z
case .MEM_SVE_VEC:
return op.kind == .MEMORY && op.mem.mode == .REG_OFFSET && reg_class(op.mem.index) == REG_Z
case .MEM_SVE_VB:
return op.kind == .MEMORY && op.mem.mode == .OFFSET && reg_class(op.mem.base) == REG_Z
case .COND:
return op.kind == .COND
case .COND_NOT_AL:
return op.kind == .COND && (op.cond & 0xE) != 0xE
}
return false
}
// =============================================================================
// Operand packer
// =============================================================================
@(private="file")
// Element size in bits for a NEON vector arrangement operand type.
vec_esize :: #force_inline proc "contextless" (ot: Operand_Type) -> u32 {
#partial switch ot {
case .V_8B, .V_16B: return 8
case .V_4H, .V_8H, .V_4H_FP16, .V_8H_FP16: return 16
case .V_2S, .V_4S: return 32
case .V_1D, .V_2D: return 64
case .Z_REG_B: return 8
case .Z_REG_H: return 16
case .Z_REG_S: return 32
case .Z_REG_D: return 64
}
return 8
}
@(private="file")
// Lane-index marker bit (log2 of element-size in bytes) for a DUP/INS form:
// derived from the V_ELEM_* operand the form carries. B=0, H=1, S=2, D=3.
vidx_markerbit :: #force_inline proc "contextless" (form: ^Encoding) -> u32 {
for ot in form.ops {
#partial switch ot {
case .V_ELEM_B: return 0
case .V_ELEM_H: return 1
case .V_ELEM_S: return 2
case .V_ELEM_D: return 3
}
}
return 0
}
pack_operand_inline :: #force_inline proc(
op: ^Operand,
enc: Operand_Encoding,
form: ^Encoding,
pc: u32,
inst_idx: u16,
relocs: ^[dynamic]Relocation,
// Most encodings pack one operand in isolation. SVE's tsz field does not:
// it holds an element size and a shift together, and the size belongs to a
// different operand than the shift.
inst: ^Instruction,
) -> u32 {
switch enc {
case .NONE, .IMPL:
return 0
// ---- Register slots ----------------------------------------------------
case .RD, .RT:
return (u32(reg_hw(op.reg)) & 0x1F) << 0
case .RN:
return (u32(reg_hw(op.reg)) & 0x1F) << 5
case .RN_RM:
r := u32(reg_hw(op.reg)) & 0x1F
return (r << 5) | (r << 16)
case .RT2, .RA:
return (u32(reg_hw(op.reg)) & 0x1F) << 10
case .RM:
// RM has three flavours per the operand kind:
// REGISTER -- plain Rm at bits 20-16
// SHIFTED_REG -- Rm + shift type (22:23) + amount (15:10)
// EXTENDED_REG -- Rm + extend (13:15) + amount (10:12)
switch op.kind {
case .REGISTER:
return (u32(reg_hw(op.reg)) & 0x1F) << 16
case .SHIFTED_REG:
return (u32(reg_hw(op.shifted.reg)) & 0x1F) << 16 |
(u32(op.shifted.type) & 0x3) << 22 |
(u32(op.shifted.amount) & 0x3F) << 10
case .EXTENDED_REG:
return (u32(reg_hw(op.extended.reg)) & 0x1F) << 16 |
(u32(op.extended.extend) & 0x7) << 13 |
(u32(op.extended.amount) & 0x7) << 10
case .NONE, .IMMEDIATE, .MEMORY, .RELATIVE, .COND, .SYSTEM_REGISTER, .ZA_SLICE:
return 0
}
// ---- Immediates --------------------------------------------------------
case .IMM12: return (u32(op.immediate) & 0xFFF) << 10
case .IMM16: return (u32(op.immediate) & 0xFFFF) << 5
case .IMM6: return (u32(op.immediate) & 0x3F) << 10
case .IMM9: return (u32(op.immediate) & 0x1FF) << 12
case .IMM_HW: return (u32(op.immediate) & 0x3) << 21
case .IMM_SH12: return (u32(op.immediate) & 0x1) << 22
case .SHIFT_TYPE: return (u32(op.immediate) & 0x3) << 22
case .EXT_OPT: return (u32(op.immediate) & 0x7) << 13
case .EXT_IMM3: return (u32(op.immediate) & 0x7) << 10
case .COND_HI:
// Condition payload may arrive as IMMEDIATE (raw) or COND kind.
c := u32(op.cond) if op.kind == .COND else u32(op.immediate)
return (c & 0xF) << 12
case .COND_HI_INV:
c := u32(op.cond) if op.kind == .COND else u32(op.immediate)
return ((c ~ 1) & 0xF) << 12
case .COND_LO:
c := u32(op.cond) if op.kind == .COND else u32(op.immediate)
return (c & 0xF) << 0
case .NZCV_FIELD:
return (u32(op.immediate) & 0xF) << 0
case .SYS_FIELD:
return (u32(op.sysreg) & 0x7FFF) << 5
case .HINT_FIELD:
return (u32(op.immediate) & 0x7F) << 5
case .BARRIER_FIELD:
return (u32(op.immediate) & 0xF) << 8
// ---- Memory operand variants ------------------------------------------
case .OFFSET_BASE_U12:
// Scaled unsigned 12-bit: imm12 = disp / data_size
// data_size derived from bits[31:30] of the form: 00=1, 01=2, 10=4, 11=8
size := u32(1) << ((form.bits >> 30) & 0x3)
base_bits := (u32(reg_hw(op.mem.base)) & 0x1F) << 5
imm_bits := (u32(op.mem.disp) / size) & 0xFFF
return base_bits | (imm_bits << 10)
case .OFFSET_BASE_S9:
// Signed 9-bit unscaled at bits 20-12.
base_bits := (u32(reg_hw(op.mem.base)) & 0x1F) << 5
imm_bits := u32(op.mem.disp) & 0x1FF
return base_bits | (imm_bits << 12)
case .OFFSET_BASE_PRE:
// Pre-index: bits[11:10] = 11, signed 9-bit at 20-12.
base_bits := (u32(reg_hw(op.mem.base)) & 0x1F) << 5
imm_bits := u32(op.mem.disp) & 0x1FF
return base_bits | (imm_bits << 12) | (0x3 << 10)
case .OFFSET_BASE_POST:
// Post-index: bits[11:10] = 01.
base_bits := (u32(reg_hw(op.mem.base)) & 0x1F) << 5
imm_bits := u32(op.mem.disp) & 0x1FF
return base_bits | (imm_bits << 12) | (0x1 << 10)
case .OFFSET_BASE_A:
// Atomic addressing: [Xn] only -- no displacement, no shift.
// Used by load/store exclusives, acquire/release, LSE atomics.
return (u32(reg_hw(op.mem.base)) & 0x1F) << 5
case .OFFSET_PAIR_4, .OFFSET_PAIR_8, .OFFSET_PAIR_16:
// LDP/STP: signed imm7, scaled by the transfer size, at bits 21:15.
// The pre/post marker is already in the form's bits, so nothing is
// ORed into bits 11:10 -- those belong to Rt2.
scale := i32(4)
if enc == .OFFSET_PAIR_8 {
scale = 8
} else if enc == .OFFSET_PAIR_16 {
scale = 16
}
base_bits := (u32(reg_hw(op.mem.base)) & 0x1F) << 5
imm_bits := u32((i32(op.mem.disp) / scale) & 0x7F)
return base_bits | (imm_bits << 15)
case .OFFSET_REG:
// [Xn, Xm{, LSL #s}]: option=011, S = shift!=0.
base_bits := (u32(reg_hw(op.mem.base)) & 0x1F) << 5
idx_bits := (u32(reg_hw(op.mem.index)) & 0x1F) << 16
option := u32(0x3) << 13
s_bit := op.mem.shift != 0 ? u32(1) << 12 : 0
return base_bits | idx_bits | option | s_bit | (0x2 << 10)
case .OFFSET_EXT:
// [Xn, Wm, SXTW|UXTW|SXTX #s]: option = ext, S = shift!=0.
base_bits := (u32(reg_hw(op.mem.base)) & 0x1F) << 5
idx_bits := (u32(reg_hw(op.mem.index)) & 0x1F) << 16
option := (u32(op.mem.extend) & 0x7) << 13
s_bit := op.mem.shift != 0 ? u32(1) << 12 : 0
return base_bits | idx_bits | option | s_bit | (0x2 << 10)
// ---- PC-relative branches ---------------------------------------------
case .BRANCH_26:
append(relocs, Relocation{
offset = pc, label_id = u32(op.relative),
type = .B26, size = 4, inst_idx = inst_idx,
})
return 0
case .BRANCH_19:
// Could be B.cond, CBZ/CBNZ, or LDR literal -- the relocation
// type for all three is the same B_COND19 (19-bit signed PC-rel
// scaled by 4) since the encoding field is identical.
append(relocs, Relocation{
offset = pc, label_id = u32(op.relative),
type = .B_COND19, size = 4, inst_idx = inst_idx,
})
return 0
case .BRANCH_14:
append(relocs, Relocation{
offset = pc, label_id = u32(op.relative),
type = .TBZ14, size = 4, inst_idx = inst_idx,
})
return 0
case .BRANCH_PG21:
// ADR / ADRP -- choose reloc type by the form's bits[31] (op flag).
ty: Relocation_Type = .ADR_PCREL21
if (form.bits >> 31) & 1 != 0 { ty = .ADRP_PCREL21 }
append(relocs, Relocation{
offset = pc, label_id = u32(op.relative),
type = ty, size = 4, inst_idx = inst_idx,
})
return 0
// ---- TBZ / TBNZ bit position split (b5 at bit 31, b40 at 23-19) -----
case .TBZ_BIT:
bit := u32(op.immediate) & 0x3F
return ((bit >> 5) & 1) << 31 | (bit & 0x1F) << 19
// ---- NEON / SIMD register slots (alias of RD/RN/RM/RA bit positions) --
case .VD, .VD_TSZ, .VD_LIST1, .VD_LIST2, .VD_LIST3, .VD_LIST4:
return (u32(reg_hw(op.reg)) & 0x1F) << 0
case .VN, .VN_TSZ, .VN_LIST1, .VN_LIST2, .VN_LIST3, .VN_LIST4:
return (u32(reg_hw(op.reg)) & 0x1F) << 5
case .VM:
return (u32(reg_hw(op.reg)) & 0x1F) << 16
case .VA:
return (u32(reg_hw(op.reg)) & 0x1F) << 10
// NEON shift-by-immediate: the element-size marker is already in `bits`;
// the operand drives only the low immh:immb bits at 22:16.
case .NEON_SHL_IMM:
return (u32(op.immediate) & 0x3F) << 16
case .NEON_SHR_IMM:
esize := vec_esize(form.ops[0])
return ((esize - u32(op.immediate)) & 0x3F) << 16
// NEON copy/permute index fields (element-size marker fixed in `bits`).
case .VN_VM_DUP:
hw := u32(reg_hw(op.reg)) & 0x1F
return (hw << 5) | (hw << 16)
case .NEON_IDX5:
mb := vidx_markerbit(form)
return (u32(op.immediate) << (mb + 1)) << 16
case .NEON_IDX4:
mb := vidx_markerbit(form)
return (u32(op.immediate) << mb) << 11
case .NEON_EXT_IDX:
return (u32(op.immediate) & 0xF) << 11
// CCMP/CCMN immediate (imm5 at 20:16) and MSR-immediate PSTATE selector.
case .IMM5_HI:
return (u32(op.immediate) & 0x1F) << 16
case .MSR_PSTATE:
v := u32(op.immediate)
return ((v >> 3) & 0x7) << 16 | (v & 0x7) << 5
case .FMOV_SCALAR_IMM:
return (u32(op.immediate) & 0xFF) << 13
// SVE alias duplicated predicate / Z fields + EXT byte index.
case .PG4_PM_DUP:
p := u32(reg_hw(op.reg)) & 0xF
return p << 10 | p << 16
case .PN_PM_DUP:
p := u32(reg_hw(op.reg)) & 0xF
return p << 5 | p << 16
case .PN_PG_PM_DUP:
p := u32(reg_hw(op.reg)) & 0xF
return p << 5 | p << 10 | p << 16
case .ZD_ZM_DUP:
z := u32(reg_hw(op.reg)) & 0x1F
return z << 0 | z << 16
case .SVE_EXT_IMM:
v := u32(op.immediate)
return ((v >> 3) & 0x1F) << 16 | (v & 0x7) << 10
case .ZA_TILE_LOW:
return (u32(op.immediate) & 0x7) << 0
// NEON single-structure lane index (Q at 30, S at 12, size at 11:10).
case .NEON_LANE_B:
i := u32(op.immediate)
return ((i >> 3) & 0x1) << 30 | ((i >> 2) & 0x1) << 12 | (i & 0x3) << 10
case .NEON_LANE_H:
i := u32(op.immediate)
return ((i >> 2) & 0x1) << 30 | ((i >> 1) & 0x1) << 12 | (i & 0x1) << 11
case .NEON_LANE_S:
i := u32(op.immediate)
return ((i >> 1) & 0x1) << 30 | (i & 0x1) << 12
case .NEON_LANE_D:
return (u32(op.immediate) & 0x1) << 30
// SVE2 XAR rotate amount: V = 2*esize - amount, split tszh:tszl:imm3.
case .SVE_XAR_SHIFT:
// The element size shares this field with the shift, so it cannot also
// select between forms -- there is one form, and the size comes from
// the register the caller passed.
esize := u32(inst.ops[0].size) * 8
if esize == 0 || esize > 64 { esize = vec_esize(form.ops[0]) }
v := sve_tsz_pack(esize, u32(op.immediate))
return ((v >> 5) & 0x3) << 22 | ((v >> 3) & 0x3) << 19 | (v & 0x7) << 16
// NEON MOVI/FMOV immediate split: abc at bits 18-16, defgh at bits 9-5.
case .NEON_IMM8_FMOV:
v := u32(op.immediate) & 0xFF
return ((v >> 5) & 0x7) << 16 | (v & 0x1F) << 5
case .NEON_INDEX_H:
// H lane index: H at bit 20, L at bit 21, M at bit 11 (3 bits total
// when ESize=H). v1 keeps the simpler layout: just bits 20-19.
return (u32(op.immediate) & 0x3) << 19
case .NEON_INDEX_S:
// S lane index: bits 11 (H) + 21 (L). v1: bit 11 + bit 21.
v := u32(op.immediate) & 0x3
return (v & 0x1) << 21 | ((v >> 1) & 0x1) << 11
case .NEON_INDEX_D:
return (u32(op.immediate) & 0x1) << 11
// LSE atomics share field positions with the standard load/store
// encoding (Rs at 16-20, Rt at 0-4, Rn at 5-9).
case .ATOMIC_RS:
return (u32(reg_hw(op.reg)) & 0x1F) << 16
case .ATOMIC_RT:
return (u32(reg_hw(op.reg)) & 0x1F) << 0
case .ATOMIC_RN:
// Memory operand carries the address register in mem.base.
if op.kind == .MEMORY {
return (u32(reg_hw(op.mem.base)) & 0x1F) << 5
}
return (u32(reg_hw(op.reg)) & 0x1F) << 5
// Bitmask logical immediate. The user passes the raw 32/64-bit mask
// value in op.immediate; the matcher has already validated that the
// value is encodable at the form's width, so encode_bitmask_imm
// cannot fail here.
case .BITMASK_FIELD:
n, immr, imms, _ := encode_bitmask_imm(u64(op.immediate), form.flags.is_64)
return (u32(n) << 22) | (u32(immr) << 16) | (u32(imms) << 10)
// SVE predicates (low 4 bits at 0/5/16; merge/zero via bit 14 etc.)
case .PD:
return (u32(reg_hw(op.reg)) & 0xF) << 0
case .PN:
return (u32(reg_hw(op.reg)) & 0xF) << 5
case .PM:
return (u32(reg_hw(op.reg)) & 0xF) << 16
case .SME_ZA_MASK:
return u32(op.immediate) & 0xFF
case .SME_ZA_ARRAY:
return ((u32(op.za.ws) & 0x3) << 13) | (u32(op.za.offset) & 0xF)
case .ENC_ZT0:
return 0
case .LUTI_IDX:
return (u32(op.immediate) & 0x3) << 15
case .PNG:
// The field holds pn - 8.
return ((u32(reg_hw(op.reg)) - 8) & 0x7) << 10
case .PG:
// Governing predicate (3-bit slot, P0..P7 only).
return (u32(reg_hw(op.reg)) & 0x7) << 10
case .PG4:
// 4-bit Pg slot (P0..P15) used by predicate-logical and a few
// SVE2 ops.
return (u32(reg_hw(op.reg)) & 0xF) << 10
case .PM3:
// 3-bit Pm at bits 15:13 (SME outer products FMOPA/SMOPA/etc.).
return (u32(reg_hw(op.reg)) & 0x7) << 13
// SVE immediates
case .SVE_IMM8:
// Signed 8-bit at bits 12-5 (DUP/CPY/ADD imm).
return (u32(op.immediate) & 0xFF) << 5
case .SVE_IMM5A:
// INDEX's first operand; SVE_IMM5 is the second, at bits 20:16.
return (u32(op.immediate) & 0x1F) << 5
case .SVE_IMM5:
// 5-bit at bits 20-16 (INDEX imm, etc.).
return (u32(op.immediate) & 0x1F) << 16
case .SVE_SHIFT_TSZ_IMM:
// tsz:imm3 at bits 22:16 -- caller passes the already-composed
// 7-bit field (tsz<6:3>:imm3<2:0>) in the IMMEDIATE.
return (u32(op.immediate) & 0x7F) << 16
case .SVE_PATTERN:
return (u32(op.immediate) & 0x1F) << 5
case .IMM_MUL4:
// SVE element-count multiplier "MUL #imm" (INCW/DECW/CNTW/SQINCW/...).
// imm4 field at bits 19:16 encodes (imm - 1); asm range MUL #1..#16
// maps to 0..15. The operand carries the architectural multiplier
// (1..16), so subtract one here, matching the ENC_LSL_IMM_* /
// NEON_SHR_IMM convention of encoding the adjusted value.
mul := u32(op.immediate)
if mul == 0 { mul = 1 }
return ((mul - 1) & 0xF) << 16
// SVE memory operands
case .SVE_OFFSET_BASE_SS, .SVE_OFFSET_BASE_SS1, .SVE_OFFSET_BASE_SS2, .SVE_OFFSET_BASE_SS3,
.SVE_OFFSET_BASE_SS4:
// [Xn, Xm, LSL #s] scalar+scalar. Base at 9:5, index at 20:16;
// shift is implicit in the encoding's static bits (per ESize).
base_bits := (u32(reg_hw(op.mem.base)) & 0x1F) << 5
idx_bits := (u32(reg_hw(op.mem.index)) & 0x1F) << 16
return base_bits | idx_bits
case .SVE_OFFSET_BASE_SI:
// [Xn{, #imm, MUL VL}] scalar+imm. Base at 9:5, signed 4-bit imm
// at bits 19:16 (caller passes signed disp as op.mem.disp).
base_bits := (u32(reg_hw(op.mem.base)) & 0x1F) << 5
imm_bits := (u32(op.mem.disp) & 0xF) << 16
return base_bits | imm_bits
// SME ZA tile number fields (position depends on element size).
case .ZA_TILE_NUM_B:
// The tile number sits in the low bits, as wide as the element
// size leaves room for.
return u32(reg_hw(op.reg)) & 0x0
case .ZA_TILE_NUM_H:
// The tile number sits in the low bits, as wide as the element
// size leaves room for.
return u32(reg_hw(op.reg)) & 0x1
case .ZA_TILE_NUM_S:
// The tile number sits in the low bits, as wide as the element
// size leaves room for.
return u32(reg_hw(op.reg)) & 0x3
case .ZA_TILE_NUM_D:
// The tile number sits in the low bits, as wide as the element
// size leaves room for.
return u32(reg_hw(op.reg)) & 0x7
case .SME_PATTERN_FIELD:
// 4-bit SME pattern/list at bits 8:5 (ZERO instruction list mask).
return (u32(op.immediate) & 0xF) << 5
// ---- SVE gather/scatter + vector-base memory --------------------------
case .SVE_OFFSET_BASE_VEC, .SVE_OFFSET_BASE_VEC_S, .SVE_OFFSET_BASE_VEC_D,
.SVE_OFFSET_BASE_VECST_S, .SVE_OFFSET_BASE_VECST_D:
// [Xn, Zm.S/D, extend] -- base GPR at 9:5, Zm at 20:16.
base := (u32(reg_hw(op.mem.base)) & 0x1F) << 5
idx := (u32(reg_hw(op.mem.index)) & 0x1F) << 16
return base | idx
case .SVE_OFFSET_VEC_BASE:
// [Zn.S/D, #imm5] -- vector base at 9:5, signed-5 imm at bits 20:16.
base := (u32(reg_hw(op.mem.base)) & 0x1F) << 5
imm := (u32(op.mem.disp) & 0x1F) << 16
return base | imm
// ---- SVE indexed lane field (FMLA Zda.T, Zn.T, Zm.T[i]) --------------
case .SVE_FMLA_IDX_H:
// i3 = (op.immediate >> 4) & 0x7? No -- user passes lane index
// (0..7) directly. Encoder packs i3 split as bit 22, bits 20:19,
// and Zm at bits 18:16 (low 8 regs only for indexed .H/.S).
// The instruction format we use accepts the lane index as a
// 3-bit immediate; the Zm register comes via .VM.
lane := u32(op.immediate) & 0x7
return ((lane >> 2) & 0x1) << 22 | (lane & 0x3) << 19
case .SVE_FMLA_IDX_S:
lane := u32(op.immediate) & 0x3
return lane << 19
case .SVE_FMLA_IDX_D:
lane := u32(op.immediate) & 0x1
return lane << 20
// ---- SME tile slice descriptor packing -------------------------------
//
// The slice descriptor (packed immediate) is unpacked into the
// instruction's bit positions per element size. The user-passed
// packed value carries:
// imm[3:0] | V[4] | Ws[6:5] | tile[10:7]
//
// Instruction layout (per LLVM golden tests):
// bit 15 = V flag (0=H, 1=V)
// bits 14:13 = Ws index (Ws is W12 + this)
// bits 3:0 = tile_num and imm packed (per element size):
// .B : imm[3:0] (single tile, ZA0.B)
// .H : tile[0]<<3 | imm[2:0] (2 tiles, 8 slices each)
// .W : tile[1:0]<<2 | imm[1:0] (4 tiles, 4 slices each)
// .D : tile[2:0]<<1 | imm[0] (8 tiles, 2 slices each)
// .Q : tile[3:0] (16 tiles, no imm)
case .SME_SLICE_B:
return (u32(op.za.vertical ? 1 : 0) << 15) | ((u32(op.za.ws) & 0x3) << 13) |
((u32(op.za.tile) << 4) & 0xF) | (u32(op.za.offset) & 0xF)
case .SME_SLICE_H:
return (u32(op.za.vertical ? 1 : 0) << 15) | ((u32(op.za.ws) & 0x3) << 13) |
((u32(op.za.tile) << 3) & 0xF) | (u32(op.za.offset) & 0x7)
case .SME_SLICE_W:
return (u32(op.za.vertical ? 1 : 0) << 15) | ((u32(op.za.ws) & 0x3) << 13) |
((u32(op.za.tile) << 2) & 0xF) | (u32(op.za.offset) & 0x3)
case .SME_SLICE_D:
return (u32(op.za.vertical ? 1 : 0) << 15) | ((u32(op.za.ws) & 0x3) << 13) |
((u32(op.za.tile) << 1) & 0xF) | (u32(op.za.offset) & 0x1)
case .SME_SLICE_Q:
return (u32(op.za.vertical ? 1 : 0) << 15) | ((u32(op.za.ws) & 0x3) << 13) |
((u32(op.za.tile) << 0) & 0xF) | (u32(op.za.offset) & 0x0)
case .NEON_IDX2:
return (u32(op.immediate) & 0x3) << 12
case .ENC_FCMLA_ROT:
// 2-bit rotation at bits 13:12 (0/1/2/3 = 0°/90°/180°/270°).
return (u32(op.immediate) & 0x3) << 12
case .ENC_FCADD_ROT:
// 1-bit rotation at bit 12 (0 = 90°, 1 = 270°).
return (u32(op.immediate) & 0x1) << 12
case .ENC_SVE_PRFOP:
// 4-bit SVE prefetch op at bits 3:0.
return u32(op.immediate) & 0xF
case .ENC_LDRAA_IMM10:
// Signed 10-bit immediate at bits 21:12 (the user passes a byte
// offset that must be a multiple of 8; we encode imm >> 3).
v := u32(i32(op.immediate) >> 3) & 0x3FF
return v << 12
// ---- Batch 5 composite-packed encodings ----
case .ENC_LSL_IMM_W:
// 32-bit LSL alias: immr = (-imm) & 31, imms = 31 - imm.
imm := u32(op.immediate) & 0x1F
immr := ((~imm + 1) & 0x1F)
imms := (31 - imm) & 0x1F
return (immr << 16) | (imms << 10)
case .ENC_LSL_IMM_X:
// 64-bit LSL alias: immr = (-imm) & 63, imms = 63 - imm.
imm := u32(op.immediate) & 0x3F
immr := ((~imm + 1) & 0x3F)
imms := (63 - imm) & 0x3F
return (immr << 16) | (imms << 10)
case .ENC_IMM6_LO:
// Signed 6-bit at bits 10:5 (RDSVL); range -32..31.
return (u32(op.immediate) & 0x3F) << 5
case .ENC_SHIFT_IMMR:
// LSR/ASR by immediate: immr = shift at bits 21:16. imms is the
// constant 31 / 63 already present in the form's bits, so unlike
// ENC_LSL_IMM_* there is nothing to compute for it. Mask to the
// register width so a W-form shift cannot spill into bit 21.
width := form.flags.is_64 ? u32(0x3F) : u32(0x1F)
return (u32(op.immediate) & width) << 16
case .ENC_DUAL_RN_RM:
// Pack the register at both Rn (9:5) AND Rm (20:16) slots
// (for ROR Rd, Rn, #imm = EXTR Rd, Rn, Rn, #imm).
hw := u32(reg_hw(op.reg)) & 0x1F
return (hw << 5) | (hw << 16)
case .ENC_ROR_SHIFT:
// imms (shift amount) at bits 15:10.
return (u32(op.immediate) & 0x3F) << 10
case .ENC_Z_PAIR_VD, .ENC_Z_QUAD_VD:
return u32(reg_hw(op.reg)) & (enc == .ENC_Z_PAIR_VD ? 0x1E : 0x1C)
case .ENC_Z_PAIR_VN, .ENC_Z_QUAD_VN:
return (u32(reg_hw(op.reg)) & 0x1F) << 5
case .ENC_Z_PAIR_VM, .ENC_Z_QUAD_VM:
return (u32(reg_hw(op.reg)) & 0x1F) << 16
}
return 0
}
// =============================================================================
// Pass 2 -- relocation resolver
// =============================================================================
@(private="file")
resolve_relocation_inline :: #force_inline proc(
code: []u8,
label_defs: []Label_Definition,
relocation: ^Relocation,
endianness: Endianness,
base_address: u64,
errors: ^[dynamic]Error,
) -> bool {
if int(relocation.label_id) >= len(label_defs) { return false }
ld := label_defs[relocation.label_id]
if ld == LABEL_UNDEFINED { return false }
target := u32(ld)
word := read_u32(code, relocation.offset, endianness)
switch relocation.type {
case .B26:
rel := i32(target) - i32(relocation.offset) + relocation.addend
if rel & 3 != 0 {
append(errors, Error{inst_idx = u32(relocation.inst_idx), code = .LABEL_OUT_OF_RANGE})
return true
}
words := rel >> 2
if words < -(1<<25) || words > (1<<25)-1 {
append(errors, Error{inst_idx = u32(relocation.inst_idx), code = .LABEL_OUT_OF_RANGE})
return true
}
word |= u32(words) & 0x03FFFFFF
case .B_COND19, .LDR_LITERAL19:
rel := i32(target) - i32(relocation.offset) + relocation.addend
if rel & 3 != 0 {
append(errors, Error{inst_idx = u32(relocation.inst_idx), code = .LABEL_OUT_OF_RANGE})
return true
}
words := rel >> 2
if words < -(1<<18) || words > (1<<18)-1 {
append(errors, Error{inst_idx = u32(relocation.inst_idx), code = .LABEL_OUT_OF_RANGE})
return true
}
word |= (u32(words) & 0x7FFFF) << 5
case .TBZ14:
rel := i32(target) - i32(relocation.offset) + relocation.addend
if rel & 3 != 0 {
append(errors, Error{inst_idx = u32(relocation.inst_idx), code = .LABEL_OUT_OF_RANGE})
return true
}
words := rel >> 2
if words < -(1<<13) || words > (1<<13)-1 {
append(errors, Error{inst_idx = u32(relocation.inst_idx), code = .LABEL_OUT_OF_RANGE})
return true
}
word |= (u32(words) & 0x3FFF) << 5
case .ADR_PCREL21:
// ADR: signed 21-bit byte offset (no scaling).
rel := i32(target) - i32(relocation.offset) + relocation.addend
if rel < -(1<<20) || rel > (1<<20)-1 {
append(errors, Error{inst_idx = u32(relocation.inst_idx), code = .LABEL_OUT_OF_RANGE})
return true
}
v := u32(rel) & 0x1FFFFF
word |= (v & 0x3) << 29 | ((v >> 2) & 0x7FFFF) << 5
case .ADRP_PCREL21:
// ADRP: difference of page (4KB-aligned) targets.
target_page := u64(target) & ~u64(0xFFF) + base_address & ~u64(0xFFF)
// Effective: ((target + base) >> 12) - ((pc + base) >> 12)
// Simpler: ((target + base) - (pc + base)) >> 12 when both are
// 4KB-aligned; but base + offset alignment is the caller's concern.
pc_page := (u64(relocation.offset) + base_address) & ~u64(0xFFF)
tg_page := target_page
diff := i64(tg_page) - i64(pc_page) + i64(relocation.addend)
if diff & 0xFFF != 0 {
append(errors, Error{inst_idx = u32(relocation.inst_idx), code = .LABEL_OUT_OF_RANGE})
return true
}
pages := diff >> 12
if pages < -(1<<20) || pages > (1<<20)-1 {
append(errors, Error{inst_idx = u32(relocation.inst_idx), code = .LABEL_OUT_OF_RANGE})
return true
}
v := u32(pages) & 0x1FFFFF
word |= (v & 0x3) << 29 | ((v >> 2) & 0x7FFFF) << 5
case .NONE, .PCREL_LO12_I, .PCREL_LO12_S, .ABS64, .ABS32, .ABS16:
// Linker-bound or assembler-layer; not auto-resolved here.
return false
}
write_u32(code, relocation.offset, word, endianness)
return true
}
// =============================================================================
// Endian-aware word I/O
// =============================================================================
@(private="package")
write_u32 :: #force_inline proc "contextless" (
code: []u8, offset: u32, word: u32, endianness: Endianness,
) {
if endianness == .LITTLE {
code[offset+0] = u8(word)
code[offset+1] = u8(word >> 8)
code[offset+2] = u8(word >> 16)
code[offset+3] = u8(word >> 24)
} else {
code[offset+0] = u8(word >> 24)
code[offset+1] = u8(word >> 16)
code[offset+2] = u8(word >> 8)
code[offset+3] = u8(word)
}
}
@(private="package")
read_u32 :: #force_inline proc "contextless" (
code: []u8, offset: u32, endianness: Endianness,
) -> u32 {
if endianness == .LITTLE {
return u32(code[offset+0]) |
(u32(code[offset+1]) << 8) |
(u32(code[offset+2]) << 16) |
(u32(code[offset+3]) << 24)
}
return (u32(code[offset+0]) << 24) |
(u32(code[offset+1]) << 16) |
(u32(code[offset+2]) << 8) |
u32(code[offset+3])
}
// The SVE element-width code an SME2 pair/quad operand type carries.
@(private="file", require_results)
z_elem_size :: #force_inline proc "contextless" (ot: Operand_Type) -> u8 {
#partial switch ot {
case .Z_PAIR_B, .Z_QUAD_B: return 1
case .Z_PAIR_H, .Z_QUAD_H: return 2
case .Z_PAIR_S, .Z_QUAD_S: return 4
}
return 8
}