Files
Odin/core/rexcode/isa/arm32/printer.odin
Brendan Punsky 2b06f2f81f rexcode/arm32: make the NEON data type an instruction field
NEON reuses one operand shape across every element width, so `vadd.i8` and
`vadd.f32` are both DPR,DPR,DPR and only the type separates their encodings.
The type existed nowhere in the data: the encoder could reach the first form
of a shape and no other, and the printer reconstructed a suffix from the bit
pattern at print time. 489 of 1680 forms -- 29% of the table -- were
unreachable, and `inst_vadd(d0,d1,d2)` could only ever produce VFP vadd.f64.

Add `Data_Type` and carry `dt: [2]Data_Type` on Instruction, Encoding and
Decode_Entry. Two slots because the convert family names both ends
(`vcvt.s32.f32`); everything else leaves the second .NONE. In A64 the
arrangement belongs to each operand (`add v0.4s, v1.4s, v2.4s`); in A32 it
belongs to the instruction, which is why it goes here and not on Operand.

Instruction does not grow: it lands in bytes that were already padding, so
88 stays 88. Encoding and Decode_Entry go 21 -> 23, which is +3,360 B per
table, +6.7 KB in all.

The per-form type is derived from llvm-mc rather than hand-written: assemble
each form's canonical word, disassemble it, take the suffix. 942 forms carry
one, 38 carry two. (`.w` is the Thumb wide qualifier, not a type, and is
excluded.)

Effect: of 202 shape groups holding more than one form, 168 are now separated
by the type -- 429 of the 489 unreachable forms become selectable. `dt` left
at .NONE means "unspecified" and still takes the first matching form, so
every existing caller behaves exactly as before.

It also fixes printing. The old inference could only ever produce one type,
so the whole convert family printed `vcvt.f32` -- 13 forms sharing one string
that no assembler accepts. They now print `vcvt.f32.s32`, `vcvt.f64.f32`,
`vcvta.u32.f64`, and so on.

Verified: vadd.i8/i16/i32/i64/f32 encode to f2010802 / f2110802 / f2210802 /
f2310802 / f2010d02, matching llvm-mc exactly; all 11 rexcode suites are
identical to baseline.

Still unreachable, 60 forms in 34 groups: register lists (VLD2-4/VST2-4),
LDM/STM addressing modes, and a few lane-indexed and fixed-point convert
forms whose element size is not captured by the type alone.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-27 00:24:19 -04:00

589 lines
19 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.flags.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.
@(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:
write_register(sb, op.reg)
if op.shift_type != .NONE && op.shift_type != .RRX && op.shift_amt > 0 {
fmt.sbprintf(sb, ", %s #%d", SHIFT_NAMES[int(op.shift_type)], op.shift_amt)
}
if op.lane != 0 {
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)
if m.shift_type != .NONE && m.shift_amt > 0 {
fmt.sbprintf(sb, ", %s #%d", SHIFT_NAMES[int(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, "}")
}