mirror of
https://github.com/odin-lang/Odin.git
synced 2026-09-03 10:40:20 +00:00
Three unrelated things the A32 sweep turned up, all of them in what an
instruction prints rather than what it encodes.
VTBL and VTBX read their table from a run of one to four D registers,
and the run length is a fixed pattern bit of the form rather than
something the operand encodes. They printed a bare `d0`, which is not
the syntax -- the table is a list even when it holds one register. The
length now rides in the operand encoding, the way the NEON
structure-list lengths already do, and the encoder checks it, so
`{d0}` and `{d0, d1}` select different forms instead of both landing on
whichever sorted first.
The NEON compare-with-zero forms and VCMP/VCMPE against zero take a
literal `#0` that no field encodes. Modelling it as an implicit operand
is enough for it to print; nothing is emitted for it.
VCMP left bit 7 out of its mask. That bit is the E, which is the only
thing separating VCMP from VCMPE, so VCMP matched both and VCMPE could
never be decoded at all.
Also: VPUSH and VPOP take no data type, but the printer's fallback
guessed one from the register bank whenever the table gave none, and
produced `vpop.f64 {d0}`. A leading register list now suppresses it --
the load/store-multiple forms are exactly the ones that lead with a
list.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018UmHLRF11EoWwNWCJ7JGaA
646 lines
21 KiB
Odin
646 lines
21 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)
|
|
// LDM/STM write the updated base back, and the syntax marks
|
|
// that with a `!` on the base register itself.
|
|
if k == 0 && inst.writeback { strings.write_byte(sb, '!') }
|
|
}
|
|
}
|
|
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 "" }
|
|
// A register list leads the load/store-multiple forms -- VPUSH, VPOP and
|
|
// friends -- and none of them take a data type. Guessing one from the
|
|
// bank produced `vpop.f64 {d0}`, which is not accepted syntax.
|
|
if op0.list.count > 0 { 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)
|
|
// An immediate-only form leaves index at Register(0), whose class is not
|
|
// REG_GPR -- so the class alone says whether there is an index. Excluding
|
|
// hw 0 as well made r0 unusable as one.
|
|
if reg_class(m.index) == REG_GPR {
|
|
// 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 {
|
|
// An indexed form writes its displacement even when it is zero: the
|
|
// writeback is the point, and `[r0]` alone is the plain offset form.
|
|
// The U bit also survives a zero displacement, and an assembler needs
|
|
// to see it -- `#-0` and `#0` are different words.
|
|
neg := m.disp == 0 && m.sign < 0 ? "-" : ""
|
|
switch m.mode {
|
|
case .OFFSET:
|
|
if m.disp != 0 || m.sign < 0 {
|
|
fmt.sbprintf(sb, ", #%s%d]", neg, m.disp)
|
|
} else {
|
|
strings.write_string(sb, "]")
|
|
}
|
|
case .PRE_INDEX: fmt.sbprintf(sb, ", #%s%d]!", neg, m.disp)
|
|
case .POST_INDEX: fmt.sbprintf(sb, "], #%s%d", neg, m.disp)
|
|
}
|
|
}
|
|
}
|
|
|
|
@(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, "}")
|
|
}
|