mirror of
https://github.com/odin-lang/Odin.git
synced 2026-09-02 10:13:35 +00:00
No assembler spells these `vld1_lane` or `vmov_lane`; they are `vld1`
and `vmov` with a lane-indexed operand. All nine are now their base
mnemonic. Merging them meant fixing what the separate names had been
hiding.
VLD1_LANE and VST1_LANE were byte-for-byte duplicates of forms VLD1 and
VST1 already had, with a looser mask, and neither encoded the lane --
both used .VD_D, which has no lane field. `vld1.8 {d0[3]}, [r0]`
disassembled as `vld1.8 d0, [r0]`. VMOV had the same shape: its own
DPR_ELEM form dropped the index, while VMOV_LANE's three forms carried
the per-size encodings that actually work. The lane-dropping forms are
gone and the working ones now sit under the base mnemonic.
VLD2-4/VST2-4's single-lane forms left bits 9:8 free, and that field is
what separates VLD1/2/3/4 -- so VLD4's encoding matched VLD2's entry and
disassembled as the wrong instruction.
Two more things the lane could not survive. A lane index of 0 printed
nothing, because 0 doubled as "no lane" -- `vmov.32 d0[0], r0` came out
as `vmov.32 d0, r0`, a different instruction. And a lane inside a
register list was dropped entirely, so `{d0[1], d1[1]}` printed as
`{d0, d1}`.
The whole set is byte-exact against llvm-mc: 27 single-lane forms across
VLD1-4/VST1-4 and all three VMOV element sizes. arm32 is 1649/1649 on
the sweep and 436/436 on the table checks, with the stale expectations
for the forms this corrected updated to the verified values.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018UmHLRF11EoWwNWCJ7JGaA
629 lines
20 KiB
Odin
629 lines
20 KiB
Odin
// rexcode · Brendan Punsky (dotbmp@github), original author
|
|
|
|
package rexcode_arm32
|
|
|
|
import "core:strings"
|
|
import "core:fmt"
|
|
import "core:io"
|
|
import "core:os"
|
|
import "core:reflect"
|
|
import "core:rexcode/isa"
|
|
|
|
// =============================================================================
|
|
// AArch32 PRINTER
|
|
// =============================================================================
|
|
//
|
|
// Canonical UAL syntax:
|
|
//
|
|
// ADD{<c>}{S} <Rd>, <Rn>, #<imm> A32 / T32 data-proc imm
|
|
// ADD{<c>}{S} <Rd>, <Rn>, <Rm>{, <shift>} A32 / T32 data-proc reg
|
|
// LDR{<c>} <Rt>, [<Rn>, #±<imm>] load/store immediate
|
|
// LDR{<c>} <Rt>, [<Rn>, ±<Rm>{, <shift>}] load/store reg-offset
|
|
// PUSH {R0, R1, R4-R7, LR} register list
|
|
// B{<c>} <label> branch
|
|
// VADD.<dt> <Vd>, <Vn>, <Vm> VFP/NEON
|
|
//
|
|
// Mnemonic is uppercased by default (configurable); condition code suffix
|
|
// is appended (EQ/NE/...) after the mnemonic when cond != AL.
|
|
|
|
Token :: isa.Token
|
|
Token_Kind :: isa.Token_Kind
|
|
Print_Options :: isa.Print_Options
|
|
Print_Result :: isa.Print_Result
|
|
DEFAULT_PRINT_OPTIONS :: isa.DEFAULT_PRINT_OPTIONS
|
|
|
|
@(rodata, private="file")
|
|
COND_SUFFIX := [16]string{
|
|
"eq", "ne", "cs", "cc", "mi", "pl", "vs", "vc",
|
|
"hi", "ls", "ge", "lt", "gt", "le", "", "", // 14=AL (no suffix), 15=NV/unconditional
|
|
}
|
|
|
|
@(rodata, private="file")
|
|
GPR_NAMES := [16]string{
|
|
"r0", "r1", "r2", "r3", "r4", "r5", "r6", "r7",
|
|
"r8", "r9", "r10", "fp", "ip", "sp", "lr", "pc",
|
|
}
|
|
|
|
@(rodata, private="file")
|
|
SHIFT_NAMES := [5]string{"lsl", "lsr", "asr", "ror", "rrx"}
|
|
|
|
mnemonic_to_string :: proc(m: Mnemonic, lowercase: bool = true, allocator := context.temp_allocator) -> string {
|
|
sb := strings.builder_make(allocator)
|
|
write_mnemonic(&sb, m, 14, false, !lowercase)
|
|
return strings.to_string(sb)
|
|
}
|
|
|
|
register_name :: proc(r: Register, lowercase: bool = true, allocator := context.temp_allocator) -> string {
|
|
sb := strings.builder_make(allocator)
|
|
write_register(&sb, r, !lowercase)
|
|
return strings.to_string(sb)
|
|
}
|
|
|
|
// =============================================================================
|
|
// Core sbprint
|
|
// =============================================================================
|
|
|
|
sbprint :: proc(
|
|
sb: ^strings.Builder,
|
|
instructions: []Instruction,
|
|
inst_info: []Instruction_Info,
|
|
label_defs: []Label_Definition,
|
|
tokens: ^[dynamic]Token = nil,
|
|
options: ^Print_Options = nil,
|
|
label_names: ^isa.Label_Names = nil,
|
|
) {
|
|
opts := options
|
|
if opts == nil {
|
|
@(static) defaults := DEFAULT_PRINT_OPTIONS
|
|
opts = &defaults
|
|
}
|
|
|
|
// Display-side label naming: numbers in ADDRESS order (independent of the internal ids'
|
|
// allocation order), caller names keyed by byte offset (isa.Label_Display).
|
|
display: isa.Label_Display
|
|
isa.label_display_init(&display, label_defs, label_names)
|
|
defer isa.label_display_destroy(&display)
|
|
|
|
for i in 0..<len(instructions) {
|
|
inst := &instructions[i]
|
|
offset := u32(i) * 4
|
|
if i < len(inst_info) {
|
|
offset = inst_info[i].offset
|
|
}
|
|
|
|
// A displayable label at this offset — a definition, or a caller-named offset
|
|
if isa.label_display_at(&display, offset) {
|
|
isa.label_display_write(&display, sb, offset, opts.label_prefix)
|
|
strings.write_string(sb, ":\n")
|
|
}
|
|
|
|
if opts.show_offsets {
|
|
fmt.sbprintf(sb, "%08x: ", offset)
|
|
}
|
|
|
|
if inst.mnemonic == .INVALID {
|
|
strings.write_string(sb, " .word 0xINVALID\n")
|
|
continue
|
|
}
|
|
|
|
strings.write_string(sb, " ")
|
|
|
|
// The `.<dt>` suffix. It is data on the instruction now, so there is
|
|
// nothing to reconstruct: read it straight off. This is also what
|
|
// makes the convert family print correctly -- `vcvt.s32.f32` names
|
|
// both ends, and the old bit-pattern inference only ever produced one.
|
|
dt_suffix := ""
|
|
dt := inst.dt
|
|
if dt[0] == .NONE && i < len(inst_info) {
|
|
de_idx := int(inst_info[i].decode_entry)
|
|
if de_idx < len(DECODE_ENTRIES) {
|
|
dt = DECODE_ENTRIES[de_idx].dt
|
|
}
|
|
}
|
|
if dt[0] == .NONE {
|
|
dt_suffix = infer_dt_suffix_from_inst(inst)
|
|
}
|
|
|
|
write_mnemonic(sb, inst.mnemonic, inst.cond, inst.sets_flags, opts.uppercase)
|
|
if dt[0] != .NONE {
|
|
write_data_type(sb, dt, opts.uppercase)
|
|
} else if dt_suffix != "" {
|
|
strings.write_string(sb, dt_suffix)
|
|
}
|
|
|
|
if inst.operand_count > 0 {
|
|
strings.write_string(sb, " ")
|
|
for k in 0..<inst.operand_count {
|
|
if k > 0 { strings.write_string(sb, ", ") }
|
|
write_operand(sb, &inst.ops[k], inst, offset, &display, opts)
|
|
}
|
|
}
|
|
strings.write_string(sb, "\n")
|
|
}
|
|
}
|
|
|
|
// =============================================================================
|
|
// Data-type suffix inference
|
|
// =============================================================================
|
|
//
|
|
// UAL syntax for VFP/NEON ops: VADD.F32 / VADD.I16 / VADD.F64 etc.
|
|
// The suffix is determined by the matched encoding form's:
|
|
// * Feature flag (HALF_FP -> .F16, NEON_HALF_FP -> .F16, VFPV2 -> .F32/.F64)
|
|
// * Opcode bits (11:8 within the NEON 3-reg-same family)
|
|
// * Size bits (21:20 select element width for integer NEON)
|
|
// * U bit (24) for signed/unsigned (e.g. .S16 vs .U16)
|
|
// * Register class of operand 0 (SPR -> single, DPR -> double or NEON D, QPR -> NEON Q)
|
|
|
|
@(private="file")
|
|
infer_dt_suffix :: proc(form: ^Decode_Entry, inst: ^Instruction) -> string {
|
|
op0 := form.ops[0]
|
|
feat := form.feature
|
|
|
|
// VFP scalar single/double/half by register class + feature
|
|
if op0 == .SPR && (feat == .VFPV2 || feat == .VFPV3 || feat == .VFPV4 ||
|
|
feat == .V8 || feat == .DIV) {
|
|
return ".f32"
|
|
}
|
|
if op0 == .DPR && (feat == .VFPV2 || feat == .VFPV3 || feat == .VFPV4 ||
|
|
feat == .V8) && !is_neon_class_op(form) {
|
|
return ".f64"
|
|
}
|
|
if (op0 == .SPR || op0 == .DPR) && feat == .HALF_FP {
|
|
return ".f16"
|
|
}
|
|
if feat == .NEON_HALF_FP {
|
|
return ".f16"
|
|
}
|
|
|
|
// MVE FP forms
|
|
if feat == .MVE_FP {
|
|
// MVE bit 20 distinguishes F16 (1) from F32 (0)
|
|
if (form.bits >> 20) & 1 != 0 { return ".f16" }
|
|
return ".f32"
|
|
}
|
|
if feat == .MVE_INT {
|
|
sz := (form.bits >> 20) & 0x3
|
|
switch sz {
|
|
case 0: return ".i8"
|
|
case 1: return ".i16"
|
|
case 2: return ".i32"
|
|
case 3: return ".i64"
|
|
}
|
|
}
|
|
|
|
// NEON integer / FP by opcode bits 11:8 + size bits 21:20
|
|
if feat == .NEON && (op0 == .DPR || op0 == .QPR) {
|
|
return neon_3reg_suffix(form)
|
|
}
|
|
|
|
// BF16 / DOT / FCMA / FHM
|
|
if feat == .BF16 { return ".bf16" }
|
|
if feat == .DOT { return ".s8" } // VSDOT / VUDOT default suffix
|
|
if feat == .FHM { return ".f16" }
|
|
if feat == .FCMA { return ".f32" }
|
|
|
|
return ""
|
|
}
|
|
|
|
@(private="file")
|
|
infer_dt_suffix_from_inst :: proc(inst: ^Instruction) -> string {
|
|
if inst.operand_count == 0 { return "" }
|
|
op0 := &inst.ops[0]
|
|
if op0.kind != .REGISTER { return "" }
|
|
switch reg_class(op0.reg) {
|
|
case REG_SPR: return ".f32"
|
|
case REG_DPR: return ".f64"
|
|
case REG_QPR: return "" // can't tell integer vs FP from operand alone
|
|
}
|
|
return ""
|
|
}
|
|
|
|
@(private="file")
|
|
is_neon_class_op :: proc(form: ^Decode_Entry) -> bool {
|
|
// NEON A32 unconditional class top byte is F2/F3; T32 is E2/E3 (after bit-28 swap).
|
|
top := (form.bits >> 24) & 0xFF
|
|
if top == 0xF2 || top == 0xF3 { return true }
|
|
if top == 0xE2 || top == 0xE3 { return true }
|
|
return false
|
|
}
|
|
|
|
@(private="file")
|
|
neon_3reg_suffix :: proc(form: ^Decode_Entry) -> string {
|
|
// For 3-reg-same family: bits 11:8 = opcode, bit 4 = subtype, bit 24 = U
|
|
op_bits := (form.bits >> 8) & 0xF
|
|
sz := (form.bits >> 20) & 0x3
|
|
u := (form.bits >> 24) & 1
|
|
|
|
switch op_bits {
|
|
case 0xD: // FP add/sub/mul/abd
|
|
return ".f32"
|
|
case 0xF: // FP max/min/recps/rsqrts/etc.
|
|
return ".f32"
|
|
case 0x1: // VAND/VBIC/VORR/VORN/VEOR/VBSL/VBIT/VBIF (no size suffix)
|
|
return ""
|
|
}
|
|
|
|
// Integer ops -- size from bits 21:20, signed/unsigned from U
|
|
prefix := u == 1 ? ".u" : ".s"
|
|
// Some ops are size-agnostic (.I8/.I16/etc. when signedness doesn't matter)
|
|
#partial switch form.mnemonic {
|
|
case .VADD, .VSUB, .VMUL, .VMLA, .VMLS, .VEXT, .VCEQ, .VTST:
|
|
prefix = ".i"
|
|
}
|
|
switch sz {
|
|
case 0: return strings.concatenate({prefix, "8"}, context.temp_allocator)
|
|
case 1: return strings.concatenate({prefix, "16"}, context.temp_allocator)
|
|
case 2: return strings.concatenate({prefix, "32"}, context.temp_allocator)
|
|
case 3: return strings.concatenate({prefix, "64"}, context.temp_allocator)
|
|
}
|
|
return ""
|
|
}
|
|
|
|
sbprintln :: proc(
|
|
sb: ^strings.Builder,
|
|
instructions: []Instruction,
|
|
inst_info: []Instruction_Info,
|
|
label_defs: []Label_Definition,
|
|
tokens: ^[dynamic]Token = nil,
|
|
options: ^Print_Options = nil,
|
|
label_names: ^isa.Label_Names = nil,
|
|
) {
|
|
sbprint(sb, instructions, inst_info, label_defs, tokens, options, label_names)
|
|
strings.write_byte(sb, '\n')
|
|
}
|
|
|
|
// =============================================================================
|
|
// Sink wrappers (cross-arch naming contract -- see docs/cross_arch_design.md §6)
|
|
// =============================================================================
|
|
|
|
print :: proc(
|
|
instructions: []Instruction, inst_info: []Instruction_Info, label_defs: []Label_Definition,
|
|
tokens: ^[dynamic]Token = nil, options: ^Print_Options = nil, label_names: ^isa.Label_Names = nil,
|
|
) {
|
|
sb := strings.builder_make(context.temp_allocator)
|
|
sbprint(&sb, instructions, inst_info, label_defs, tokens, options, label_names)
|
|
os.write_string(os.stdout, strings.to_string(sb))
|
|
}
|
|
|
|
println :: proc(
|
|
instructions: []Instruction, inst_info: []Instruction_Info, label_defs: []Label_Definition,
|
|
tokens: ^[dynamic]Token = nil, options: ^Print_Options = nil, label_names: ^isa.Label_Names = nil,
|
|
) {
|
|
sb := strings.builder_make(context.temp_allocator)
|
|
sbprintln(&sb, instructions, inst_info, label_defs, tokens, options, label_names)
|
|
os.write_string(os.stdout, strings.to_string(sb))
|
|
}
|
|
|
|
aprint :: proc(
|
|
instructions: []Instruction, inst_info: []Instruction_Info, label_defs: []Label_Definition,
|
|
tokens: ^[dynamic]Token = nil, options: ^Print_Options = nil, label_names: ^isa.Label_Names = nil,
|
|
allocator := context.allocator,
|
|
) -> string {
|
|
sb := strings.builder_make(allocator)
|
|
sbprint(&sb, instructions, inst_info, label_defs, tokens, options, label_names)
|
|
return strings.to_string(sb)
|
|
}
|
|
|
|
aprintln :: proc(
|
|
instructions: []Instruction, inst_info: []Instruction_Info, label_defs: []Label_Definition,
|
|
tokens: ^[dynamic]Token = nil, options: ^Print_Options = nil, label_names: ^isa.Label_Names = nil,
|
|
allocator := context.allocator,
|
|
) -> string {
|
|
sb := strings.builder_make(allocator)
|
|
sbprintln(&sb, instructions, inst_info, label_defs, tokens, options, label_names)
|
|
return strings.to_string(sb)
|
|
}
|
|
|
|
tprint :: proc(
|
|
instructions: []Instruction, inst_info: []Instruction_Info, label_defs: []Label_Definition,
|
|
tokens: ^[dynamic]Token = nil, options: ^Print_Options = nil, label_names: ^isa.Label_Names = nil,
|
|
) -> string {
|
|
sb := strings.builder_make(context.temp_allocator)
|
|
sbprint(&sb, instructions, inst_info, label_defs, tokens, options, label_names)
|
|
return strings.to_string(sb)
|
|
}
|
|
|
|
tprintln :: proc(
|
|
instructions: []Instruction, inst_info: []Instruction_Info, label_defs: []Label_Definition,
|
|
tokens: ^[dynamic]Token = nil, options: ^Print_Options = nil, label_names: ^isa.Label_Names = nil,
|
|
) -> string {
|
|
sb := strings.builder_make(context.temp_allocator)
|
|
sbprintln(&sb, instructions, inst_info, label_defs, tokens, options, label_names)
|
|
return strings.to_string(sb)
|
|
}
|
|
|
|
bprint :: proc(
|
|
buf: []u8,
|
|
instructions: []Instruction, inst_info: []Instruction_Info, label_defs: []Label_Definition,
|
|
tokens: ^[dynamic]Token = nil, options: ^Print_Options = nil, label_names: ^isa.Label_Names = nil,
|
|
) -> string {
|
|
sb := strings.builder_from_bytes(buf)
|
|
sbprint(&sb, instructions, inst_info, label_defs, tokens, options, label_names)
|
|
return strings.to_string(sb)
|
|
}
|
|
|
|
bprintln :: proc(
|
|
buf: []u8,
|
|
instructions: []Instruction, inst_info: []Instruction_Info, label_defs: []Label_Definition,
|
|
tokens: ^[dynamic]Token = nil, options: ^Print_Options = nil, label_names: ^isa.Label_Names = nil,
|
|
) -> string {
|
|
sb := strings.builder_from_bytes(buf)
|
|
sbprintln(&sb, instructions, inst_info, label_defs, tokens, options, label_names)
|
|
return strings.to_string(sb)
|
|
}
|
|
|
|
fprint :: proc(
|
|
fd: ^os.File,
|
|
instructions: []Instruction, inst_info: []Instruction_Info, label_defs: []Label_Definition,
|
|
tokens: ^[dynamic]Token = nil, options: ^Print_Options = nil, label_names: ^isa.Label_Names = nil,
|
|
) {
|
|
sb := strings.builder_make(context.temp_allocator)
|
|
sbprint(&sb, instructions, inst_info, label_defs, tokens, options, label_names)
|
|
os.write_string(fd, strings.to_string(sb))
|
|
}
|
|
|
|
fprintln :: proc(
|
|
fd: ^os.File,
|
|
instructions: []Instruction, inst_info: []Instruction_Info, label_defs: []Label_Definition,
|
|
tokens: ^[dynamic]Token = nil, options: ^Print_Options = nil, label_names: ^isa.Label_Names = nil,
|
|
) {
|
|
sb := strings.builder_make(context.temp_allocator)
|
|
sbprintln(&sb, instructions, inst_info, label_defs, tokens, options, label_names)
|
|
os.write_string(fd, strings.to_string(sb))
|
|
}
|
|
|
|
wprint :: proc(
|
|
w: io.Writer,
|
|
instructions: []Instruction, inst_info: []Instruction_Info, label_defs: []Label_Definition,
|
|
tokens: ^[dynamic]Token = nil, options: ^Print_Options = nil, label_names: ^isa.Label_Names = nil,
|
|
) {
|
|
sb := strings.builder_make(context.temp_allocator)
|
|
sbprint(&sb, instructions, inst_info, label_defs, tokens, options, label_names)
|
|
io.write_string(w, strings.to_string(sb))
|
|
}
|
|
|
|
wprintln :: proc(
|
|
w: io.Writer,
|
|
instructions: []Instruction, inst_info: []Instruction_Info, label_defs: []Label_Definition,
|
|
tokens: ^[dynamic]Token = nil, options: ^Print_Options = nil, label_names: ^isa.Label_Names = nil,
|
|
) {
|
|
sb := strings.builder_make(context.temp_allocator)
|
|
sbprintln(&sb, instructions, inst_info, label_defs, tokens, options, label_names)
|
|
io.write_string(w, strings.to_string(sb))
|
|
}
|
|
|
|
// =============================================================================
|
|
// Writers
|
|
// =============================================================================
|
|
|
|
// `.i32`, `.s32.f32`, `.8`. Both slots print when the second is set.
|
|
// A shift suffix on a register operand. Three spellings, and the enum covers
|
|
// all of them: `, lsl #3` for an immediate amount, `, lsl r3` when the count
|
|
// comes from a register (Shift_Type.LSL_REG..ROR_REG keep the Rs index in
|
|
// shift_amt), and a bare `, rrx`, which takes no amount at all.
|
|
//
|
|
// SHIFT_NAMES only holds LSL..RRX, so the register-shifted variants have to be
|
|
// folded back onto it. Indexing it with the raw enum value ran off the end --
|
|
// LSL_REG is 6 against a 5-entry table -- and crashed the printer outright.
|
|
@(private="file")
|
|
write_shift :: proc(sb: ^strings.Builder, st: Shift_Type, amt: u8) {
|
|
switch st {
|
|
case .LSL, .LSR, .ASR, .ROR:
|
|
if amt == 0 {
|
|
return
|
|
}
|
|
fmt.sbprintf(sb, ", %s #%d", SHIFT_NAMES[int(st)], amt)
|
|
case .RRX:
|
|
strings.write_string(sb, ", rrx")
|
|
case .NONE:
|
|
// no shift to print
|
|
case .LSL_REG, .LSR_REG, .ASR_REG, .ROR_REG:
|
|
fmt.sbprintf(sb, ", %s %s", SHIFT_NAMES[int(st) - int(Shift_Type.LSL_REG)], GPR_NAMES[amt & 0xF])
|
|
}
|
|
}
|
|
|
|
@(private="file")
|
|
write_data_type :: proc(sb: ^strings.Builder, dt: Data_Types, uppercase: bool) {
|
|
for d in dt {
|
|
if d == .NONE { continue }
|
|
strings.write_byte(sb, '.')
|
|
name := DATA_TYPE_NAMES[d]
|
|
for i in 0 ..< len(name) {
|
|
c := name[i]
|
|
if uppercase && c >= 'a' && c <= 'z' {
|
|
strings.write_byte(sb, c - 'a' + 'A')
|
|
} else {
|
|
strings.write_byte(sb, c)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
@(rodata, private="file")
|
|
DATA_TYPE_NAMES := [Data_Type]string{
|
|
.NONE = "",
|
|
.S8 = "s8", .S16 = "s16", .S32 = "s32", .S64 = "s64",
|
|
.U8 = "u8", .U16 = "u16", .U32 = "u32", .U64 = "u64",
|
|
.I8 = "i8", .I16 = "i16", .I32 = "i32", .I64 = "i64",
|
|
.F16 = "f16", .F32 = "f32", .F64 = "f64",
|
|
.P8 = "p8", .P16 = "p16", .BF16 = "bf16",
|
|
.SZ8 = "8", .SZ16 = "16", .SZ32 = "32", .SZ64 = "64",
|
|
}
|
|
|
|
@(private="file")
|
|
write_mnemonic :: proc(sb: ^strings.Builder, m: Mnemonic, cond: u8, sets_flags: bool, uppercase: bool) {
|
|
name, _ := reflect.enum_name_from_value(m)
|
|
// PSB_CSYNC / TSB_CSYNC are the only names left holding an underscore:
|
|
// assemblers write them as a mnemonic plus an operand token, `psb csync`,
|
|
// so that underscore is a space. No other mnemonic has one.
|
|
split := -1
|
|
if len(name) > 4 && (name[:4] == "PSB_" || name[:4] == "TSB_") {
|
|
split = 3
|
|
}
|
|
for i in 0..<len(name) {
|
|
c := name[i]
|
|
if i == split {
|
|
strings.write_byte(sb, ' ')
|
|
} else if !uppercase && c >= 'A' && c <= 'Z' {
|
|
strings.write_byte(sb, c - 'A' + 'a')
|
|
} else {
|
|
strings.write_byte(sb, c)
|
|
}
|
|
}
|
|
if sets_flags {
|
|
strings.write_string(sb, uppercase ? "S" : "s")
|
|
}
|
|
if cond != 14 && cond != 15 {
|
|
strings.write_string(sb, uppercase ? COND_SUFFIX[cond] : COND_SUFFIX[cond])
|
|
// (Both forms identical for cond; we keep the table lowercase and
|
|
// post-process if uppercase requested.)
|
|
}
|
|
}
|
|
|
|
@(private="file")
|
|
write_register :: proc(sb: ^strings.Builder, r: Register, uppercase: bool = false) {
|
|
cls := reg_class(r)
|
|
hw := reg_hw(r)
|
|
switch cls {
|
|
case REG_GPR:
|
|
strings.write_string(sb, GPR_NAMES[hw & 0xF])
|
|
case REG_SPR:
|
|
fmt.sbprintf(sb, "s%d", hw)
|
|
case REG_DPR:
|
|
fmt.sbprintf(sb, "d%d", hw)
|
|
case REG_QPR:
|
|
fmt.sbprintf(sb, "q%d", hw)
|
|
case REG_SREG:
|
|
switch hw {
|
|
case 0: strings.write_string(sb, "apsr")
|
|
case 1: strings.write_string(sb, "cpsr")
|
|
case 2: strings.write_string(sb, "spsr")
|
|
case: fmt.sbprintf(sb, "psr%d", hw)
|
|
}
|
|
case REG_FPSC:
|
|
switch hw {
|
|
case 0: strings.write_string(sb, "fpsid")
|
|
case 1: strings.write_string(sb, "fpscr")
|
|
case 8: strings.write_string(sb, "fpexc")
|
|
case: fmt.sbprintf(sb, "fpsc%d", hw)
|
|
}
|
|
case REG_COPROC:
|
|
fmt.sbprintf(sb, "c%d", hw)
|
|
case:
|
|
fmt.sbprintf(sb, "?%d", hw)
|
|
}
|
|
}
|
|
|
|
@(private="file")
|
|
write_operand :: proc(
|
|
sb: ^strings.Builder,
|
|
op: ^Operand,
|
|
inst: ^Instruction,
|
|
offset: u32,
|
|
display: ^isa.Label_Display,
|
|
opts: ^Print_Options,
|
|
) {
|
|
switch op.kind {
|
|
case .NONE:
|
|
return
|
|
case .REGISTER:
|
|
// A VFP/NEON list is a contiguous run written in braces, and every
|
|
// register is named -- `{d1, d2, d3}`, not a range.
|
|
if op.list.count > 0 {
|
|
strings.write_string(sb, "{")
|
|
step := u16(max(op.list.stride, 1))
|
|
for n in 0 ..< u16(op.list.count) {
|
|
if n > 0 { strings.write_string(sb, ", ") }
|
|
write_register(sb, Register(reg_class(op.reg) | ((reg_hw(op.reg) + n * step) & 0x1F)))
|
|
// Every member of a single-lane list carries the index:
|
|
// `{d0[1], d1[1]}`. `{d0[]}` is the to-all-lanes form.
|
|
if op.list.all_lanes {
|
|
strings.write_string(sb, "[]")
|
|
} else if op.has_lane {
|
|
fmt.sbprintf(sb, "[%d]", op.lane)
|
|
}
|
|
}
|
|
strings.write_string(sb, "}")
|
|
return
|
|
}
|
|
write_register(sb, op.reg)
|
|
write_shift(sb, op.shift_type, op.shift_amt)
|
|
if op.has_lane {
|
|
fmt.sbprintf(sb, "[%d]", op.lane)
|
|
}
|
|
case .IMMEDIATE:
|
|
fmt.sbprintf(sb, "#%d", op.immediate)
|
|
case .MEMORY:
|
|
write_memory(sb, op.mem)
|
|
case .RELATIVE:
|
|
// Resolve to label if possible
|
|
target := u32(i64(offset) + op.relative)
|
|
if isa.label_display_at(display, target) {
|
|
isa.label_display_write(display, sb, target, opts.label_prefix)
|
|
} else {
|
|
// raw absolute
|
|
fmt.sbprintf(sb, "0x%x", target)
|
|
}
|
|
case .REG_LIST:
|
|
write_reg_list(sb, u16(op.immediate))
|
|
}
|
|
}
|
|
|
|
@(private="file")
|
|
write_memory :: proc(sb: ^strings.Builder, m: Memory) {
|
|
strings.write_string(sb, "[")
|
|
write_register(sb, m.base)
|
|
if reg_class(m.index) == REG_GPR && reg_hw(m.index) != 0 {
|
|
// Register offset
|
|
switch m.mode {
|
|
case .OFFSET:
|
|
strings.write_string(sb, ", ")
|
|
if m.sign < 0 { strings.write_string(sb, "-") }
|
|
write_register(sb, m.index)
|
|
write_shift(sb, m.shift_type, m.shift_amt)
|
|
strings.write_string(sb, "]")
|
|
case .PRE_INDEX:
|
|
strings.write_string(sb, ", ")
|
|
if m.sign < 0 { strings.write_string(sb, "-") }
|
|
write_register(sb, m.index)
|
|
strings.write_string(sb, "]!")
|
|
case .POST_INDEX:
|
|
strings.write_string(sb, "], ")
|
|
if m.sign < 0 { strings.write_string(sb, "-") }
|
|
write_register(sb, m.index)
|
|
}
|
|
} else if m.disp != 0 {
|
|
// Immediate offset
|
|
switch m.mode {
|
|
case .OFFSET: fmt.sbprintf(sb, ", #%d]", m.disp)
|
|
case .PRE_INDEX: fmt.sbprintf(sb, ", #%d]!", m.disp)
|
|
case .POST_INDEX: fmt.sbprintf(sb, "], #%d", m.disp)
|
|
}
|
|
} else {
|
|
strings.write_string(sb, "]")
|
|
}
|
|
}
|
|
|
|
@(private="file")
|
|
write_reg_list :: proc(sb: ^strings.Builder, mask: u16) {
|
|
strings.write_string(sb, "{")
|
|
first := true
|
|
range_start: int = -1
|
|
for b in 0..<16 {
|
|
bit := mask & (1 << u32(b)) != 0
|
|
next_bit := b < 15 && mask & (1 << u32(b + 1)) != 0
|
|
if bit && range_start < 0 { range_start = b }
|
|
if bit && !next_bit {
|
|
if !first { strings.write_string(sb, ", ") }
|
|
first = false
|
|
if range_start == b {
|
|
strings.write_string(sb, GPR_NAMES[b])
|
|
} else {
|
|
fmt.sbprintf(sb, "%s-%s", GPR_NAMES[range_start], GPR_NAMES[b])
|
|
}
|
|
range_start = -1
|
|
}
|
|
}
|
|
strings.write_string(sb, "}")
|
|
}
|