Merge branch 'bill/rexcode' of https://github.com/odin-lang/Odin into bill/rexcode

This commit is contained in:
gingerBill
2026-06-15 13:35:28 +01:00
319 changed files with 43305 additions and 14840 deletions

7
.gitignore vendored
View File

@@ -279,6 +279,13 @@ demo.bin
libLLVM*.so*
*.a
# core:rexcode commits its generated encode/decode tables: each arch's tablegen
# emits human-readable Odin, serialized to tables/*.bin, which the library
# #loads at compile time. The blobs (and the rexcode x86 package, caught by the
# broad x86/ rule above) must be tracked.
!/core/rexcode/x86/
!core/rexcode/*/tables/*.bin
# WASM
*.wasm

View File

@@ -1,3 +1,5 @@
// rexcode · Brendan Punsky (dotbmp@github), original author
package rexcode_arm32
import "../isa"

File diff suppressed because it is too large Load Diff

View File

@@ -1,3 +1,5 @@
// rexcode · Brendan Punsky (dotbmp@github), original author
package rexcode_arm32
// =============================================================================
@@ -115,7 +117,7 @@ encode_one_inline :: #force_inline proc(
return 0, 0, false
}
forms := ENCODING_TABLE[inst.mnemonic]
forms := encoding_forms(inst.mnemonic)
if len(forms) == 0 {
append(errors, Error{inst_idx = u32(inst_idx), code = .INVALID_MNEMONIC})
return 0, 0, false

View File

@@ -1,3 +1,5 @@
// rexcode · Brendan Punsky (dotbmp@github), original author
package rexcode_arm32
import "../isa"

View File

@@ -1,3 +1,5 @@
// rexcode · Brendan Punsky (dotbmp@github), original author
package rexcode_arm32
// =============================================================================

View File

@@ -1,3 +1,5 @@
// rexcode · Brendan Punsky (dotbmp@github), original author
package rexcode_arm32
// =============================================================================

View File

@@ -1,3 +1,5 @@
// rexcode · Brendan Punsky (dotbmp@github), original author
package rexcode_arm32
// =============================================================================

View File

@@ -1,3 +1,5 @@
// rexcode · Brendan Punsky (dotbmp@github), original author
package rexcode_arm32
// =============================================================================

View File

@@ -1,3 +1,5 @@
// rexcode · Brendan Punsky (dotbmp@github), original author
package rexcode_arm32
import "core:strings"

View File

@@ -1,3 +1,5 @@
// rexcode · Brendan Punsky (dotbmp@github), original author
package rexcode_arm32
// =============================================================================

View File

@@ -1,3 +1,5 @@
// rexcode · Brendan Punsky (dotbmp@github), original author
package rexcode_arm32
// =============================================================================

View File

@@ -1,4 +1,6 @@
package rexcode_arm32
// rexcode · Brendan Punsky (dotbmp@github), original author
package rexcode_arm32_tablegen
// =============================================================================
// AArch32 ENCODING TABLE

View File

@@ -0,0 +1,477 @@
// rexcode · Brendan Punsky (dotbmp@github), original author
package rexcode_arm32_tablegen
// =============================================================================
// AArch32 TABLE GENERATOR (Stage A)
// =============================================================================
//
// Reads the single-source-of-truth ENCODING_TABLE (encoding_table.odin, this
// package) and emits human-readable, type-checked Odin into ./generated/:
//
// generated/encode_tables.odin ENCODE_FORMS + ENCODE_RUNS (flattened encode)
// generated/decode_tables.odin DECODE_ENTRIES + DECODE_FORM_IDX +
// DECODE_BUCKET_LIST + A32/T32/T16/T32_SUB index
// generated/writer.odin Stage B: serialize those globals to ../../tables/*.bin
//
// It also re-emits the library loader ../tables.odin. Run:
// odin run arm32/tablegen # Stage A
// odin run arm32/tablegen/generated # Stage B
//
// AArch32 is fixed-width-per-mode (A32 = 4 bytes, T16 = 2 bytes, T32 = 4 bytes
// packed as low_halfword | high_halfword << 16). Three primary dispatch tables
// (one per mode/size) with a secondary T32 sub-bucket; the decode bucketing
// below is ported verbatim from the old tools/gen_decode_tables.odin.
import "core:fmt"
import "core:os"
import "core:strings"
import "core:slice"
import "core:reflect"
import "core:math/bits"
import lib "../"
// Package-scope aliases so the moved SoT resolves Mnemonic/Encoding unqualified.
Encoding :: lib.Encoding
Mnemonic :: lib.Mnemonic
Blob :: struct { global, file, typ: string }
BLOBS := [?]Blob{
{"ENCODE_FORMS", "arm32.encode_forms.bin", "Encoding"},
{"ENCODE_RUNS", "arm32.encode_runs.bin", "Encode_Run"},
{"DECODE_ENTRIES", "arm32.entries.bin", "Decode_Entry"},
{"DECODE_FORM_IDX", "arm32.form_idx.bin", "u16"},
{"DECODE_BUCKET_LIST", "arm32.bucket_list.bin", "u16"},
{"DECODE_INDEX_A32", "arm32.idx_a32.bin", "Decode_Index"},
{"DECODE_INDEX_T32", "arm32.idx_t32.bin", "Decode_Index"},
{"DECODE_INDEX_T16", "arm32.idx_t16.bin", "Decode_Index"},
{"DECODE_INDEX_T32_SUB", "arm32.idx_t32_sub.bin", "Decode_Index"},
}
DIR_GEN :: #directory + "/generated/"
PATH_LOADER :: #directory + "/../tables.odin"
A32_BUCKETS :: 256 // bits[27:20]
T32_BUCKETS :: 128 // bits[31:25]
T16_BUCKETS :: 64 // bits[15:10]
T32_SUB_BUCKETS :: 32 // bits[24:20] of u32
Entry :: struct {
mnemonic: lib.Mnemonic,
ops: [4]lib.Operand_Type,
enc: [4]lib.Operand_Encoding,
bits: u32,
mask: u32,
feature: lib.Feature,
mode: lib.Mode,
flags: lib.Encoding_Flags,
is_thumb32: bool,
key: u16, // primary dispatch key (8 bits A32, 7 bits T32, 6 bits T16)
ilen: u8,
form_idx: u16, // index of this form within ENCODING_TABLE[mnemonic]
}
Range :: struct { start: u16, count: u16 }
main :: proc() {
n := emit_encode_tables()
ne := emit_decode_tables()
emit_writer()
emit_loader()
fmt.printfln("arm32 tablegen: %d encode forms, %d decode entries", n, ne)
}
// -----------------------------------------------------------------------------
// Encode side
// -----------------------------------------------------------------------------
emit_encode_tables :: proc() -> (total: int) {
sb := strings.builder_make()
strings.write_string(&sb, "package rexcode_arm32_generated\n\n")
strings.write_string(&sb, "// GENERATED by ../gen.odin -- DO NOT EDIT.\n")
strings.write_string(&sb, "// Flattened encode forms + per-mnemonic run index (source: ENCODING_TABLE).\n\n")
strings.write_string(&sb, "import lib \"../..\"\n\n")
for m in Mnemonic { total += len(ENCODING_TABLE[m]) }
fmt.sbprintfln(&sb, "ENCODE_FORMS := [%d]lib.Encoding{{", total)
for m in Mnemonic {
forms := ENCODING_TABLE[m]
if len(forms) == 0 { continue }
fmt.sbprintfln(&sb, "\t// .%v", m)
for f in forms {
write_row(&sb, f.mnemonic, f.ops, f.enc, f.bits, f.mask, f.feature, f.mode, f.flags)
}
}
strings.write_string(&sb, "}\n\n")
run_w := 0
for m in Mnemonic { run_w = max(run_w, len(reflect.enum_string(m))) }
strings.write_string(&sb, "ENCODE_RUNS := [lib.Mnemonic]lib.Encode_Run{\n")
start := 0
for m in Mnemonic {
c := len(ENCODING_TABLE[m])
name := reflect.enum_string(m)
fmt.sbprintf(&sb, "\t.%s", name)
for _ in 0..<run_w-len(name) { strings.write_byte(&sb, ' ') }
fmt.sbprintfln(&sb, " = {{% 5d, % 3d}},", start, c)
start += c
}
strings.write_string(&sb, "}\n")
emit_file(DIR_GEN + "encode_tables.odin", &sb)
return
}
// -----------------------------------------------------------------------------
// Decode side (ported from old tools/gen_decode_tables.odin)
// -----------------------------------------------------------------------------
mode_rank :: proc(e: Entry) -> int {
if e.mode == .A32 { return 0 }
if e.is_thumb32 || e.ilen == 4 { return 1 }
return 2
}
// Variable bits within the bucket-key range. For each combination of those
// variable bits we emit a separate bucket key, so a single entry is reachable
// via every word that can match its mask. fixed_key must be sanitized via
// `bits & mask` — entry bits often carry a default value at variable positions
// (e.g. U=1 in LDR's base 0x05900000), and those defaults must not pre-set bits
// in the key or we'd skip the zero-side bucket during enumeration.
enumerate_keys :: proc(bits, mask: u32, key_shift: u32, key_bits: u32, out: ^[dynamic]u16) {
clear(out)
key_mask := (u32(1) << key_bits) - 1
fixed_key := ((bits & mask) >> key_shift) & key_mask
var_bits := (~mask >> key_shift) & key_mask
// Enumerate submasks of var_bits via the classic Gosper-style walk.
sub: u32 = 0
for {
append(out, u16(fixed_key | sub))
if var_bits == 0 { break }
if sub == var_bits { break }
sub = (sub - var_bits) & var_bits // next non-zero submask
}
}
emit_decode_tables :: proc() -> (total: int) {
all: [dynamic]Entry
defer delete(all)
for mn in Mnemonic {
for f, fi in ENCODING_TABLE[mn] {
ilen := lib.inst_size_from_bits(f.bits, f.mode)
e := Entry{
mnemonic = mn,
ops = f.ops,
enc = f.enc,
bits = f.bits,
mask = f.mask,
feature = f.feature,
mode = f.mode,
flags = f.flags,
is_thumb32 = f.flags.thumb32,
ilen = ilen,
form_idx = u16(fi),
}
// Compute dispatch key per mode/size.
if e.mode == .A32 {
e.key = u16((f.bits >> 20) & 0xFF)
} else if e.is_thumb32 || ilen == 4 {
// T32 32-bit: bits[31:25] of packed u32 (top 7 bits of high halfword)
e.key = u16((f.bits >> 25) & 0x7F)
} else {
// T16: bits[15:10] of the halfword (stored in low 16 of u32)
e.key = u16((f.bits >> 10) & 0x3F)
}
append(&all, e)
}
}
// Sort: by mode group (A32 first, then T32-wide, then T16), then by key,
// then by mask popcount descending so more-specific forms match first.
slice.sort_by(all[:], proc(x, y: Entry) -> bool {
mx := mode_rank(x)
my := mode_rank(y)
if mx != my { return mx < my }
if x.key != y.key { return x.key < y.key }
xc := bits.count_ones(x.mask)
yc := bits.count_ones(y.mask)
if xc != yc { return xc > yc }
return u16(x.mnemonic) < u16(y.mnemonic)
})
// First pass: collect (entry_idx, bucket_key) pairs across modes, expanding
// variable bits within the bucket-key range. Then group by bucket.
A32_Pair :: struct { bucket: u16, entry_idx: u16 }
a32_pairs: [dynamic]A32_Pair
t32_pairs: [dynamic]A32_Pair
t16_pairs: [dynamic]A32_Pair
t32_sub_pairs: [dynamic]A32_Pair
defer delete(a32_pairs); defer delete(t32_pairs)
defer delete(t16_pairs); defer delete(t32_sub_pairs)
keys: [dynamic]u16
defer delete(keys)
for e, i in all {
if e.mode == .A32 {
enumerate_keys(e.bits, e.mask, 20, 8, &keys)
for k in keys { append(&a32_pairs, A32_Pair{bucket = k, entry_idx = u16(i)}) }
} else if e.is_thumb32 || e.ilen == 4 {
enumerate_keys(e.bits, e.mask, 25, 7, &keys)
for k in keys { append(&t32_pairs, A32_Pair{bucket = k, entry_idx = u16(i)}) }
// Sub-bucket: bits 24:20 of word
sub_keys: [dynamic]u16
defer delete(sub_keys)
enumerate_keys(e.bits, e.mask, 20, 5, &sub_keys)
for k in keys {
for sk in sub_keys {
append(&t32_sub_pairs, A32_Pair{
bucket = k * T32_SUB_BUCKETS + sk,
entry_idx = u16(i),
})
}
}
} else {
enumerate_keys(e.bits, e.mask, 10, 6, &keys)
for k in keys { append(&t16_pairs, A32_Pair{bucket = k, entry_idx = u16(i)}) }
}
}
// Within each bucket we want most-specific (highest mask popcount) first,
// tiebreak by mnemonic, so the decoder's linear scan picks the most specific
// encoding before falling through to a more general one. Encode
// (bucket, -popcount, mnemonic) into a single u64 sort key.
Sort_Pair :: struct { sort_key: u64, entry_idx: u16, bucket: u16 }
rebuild :: proc(pairs: ^[dynamic]A32_Pair, all: []Entry) {
sortable := make([dynamic]Sort_Pair, 0, len(pairs))
defer delete(sortable)
for p in pairs^ {
e := all[p.entry_idx]
pop := u64(bits.count_ones(e.mask))
key := (u64(p.bucket) << 48) | ((255 - pop) << 32) | u64(e.mnemonic)
append(&sortable, Sort_Pair{
sort_key = key, entry_idx = p.entry_idx, bucket = p.bucket,
})
}
slice.sort_by_key(sortable[:], proc(s: Sort_Pair) -> u64 { return s.sort_key })
clear(pairs)
for s in sortable { append(pairs, A32_Pair{bucket = s.bucket, entry_idx = s.entry_idx}) }
}
rebuild(&a32_pairs, all[:])
rebuild(&t32_pairs, all[:])
rebuild(&t16_pairs, all[:])
rebuild(&t32_sub_pairs, all[:])
// Build a flat u16 dispatch list (DECODE_BUCKET_LIST). Each bucket points to
// a contiguous run of entry indices in that list.
a32_idx: [A32_BUCKETS]Range
t32_idx: [T32_BUCKETS]Range
t16_idx: [T16_BUCKETS]Range
t32_sub_idx: [T32_BUCKETS * T32_SUB_BUCKETS]Range
bucket_list: [dynamic]u16
defer delete(bucket_list)
emit_pairs :: proc(pairs: []A32_Pair, idx: []Range, list: ^[dynamic]u16) {
prev_bucket: i32 = -1
for p in pairs {
cur_bucket := i32(p.bucket)
if cur_bucket != prev_bucket {
idx[cur_bucket].start = u16(len(list))
idx[cur_bucket].count = 0
prev_bucket = cur_bucket
}
append(list, p.entry_idx)
idx[cur_bucket].count += 1
}
}
emit_pairs(a32_pairs[:], a32_idx[:], &bucket_list)
emit_pairs(t32_pairs[:], t32_idx[:], &bucket_list)
emit_pairs(t16_pairs[:], t16_idx[:], &bucket_list)
emit_pairs(t32_sub_pairs[:], t32_sub_idx[:], &bucket_list)
sb := strings.builder_make()
strings.write_string(&sb, "package rexcode_arm32_generated\n\n")
strings.write_string(&sb, "// GENERATED by ../gen.odin -- DO NOT EDIT.\n")
strings.write_string(&sb, "// Reverse decode tables (source: ENCODING_TABLE), keyed by mode + primary key.\n\n")
strings.write_string(&sb, "import lib \"../..\"\n\n")
fmt.sbprintfln(&sb, "DECODE_ENTRIES := [%d]lib.Decode_Entry{{", len(all))
for e in all {
write_row(&sb, e.mnemonic, e.ops, e.enc, e.bits, e.mask, e.feature, e.mode, e.flags)
}
strings.write_string(&sb, "}\n\n")
emit_u16_array(&sb, "DECODE_FORM_IDX", form_idx_slice(all[:]))
emit_u16_array(&sb, "DECODE_BUCKET_LIST", bucket_list[:])
emit_range(&sb, "DECODE_INDEX_A32", a32_idx[:])
emit_range(&sb, "DECODE_INDEX_T32", t32_idx[:])
emit_range(&sb, "DECODE_INDEX_T16", t16_idx[:])
emit_range(&sb, "DECODE_INDEX_T32_SUB", t32_sub_idx[:])
emit_file(DIR_GEN + "decode_tables.odin", &sb)
return len(all)
}
form_idx_slice :: proc(entries: []Entry) -> []u16 {
out := make([]u16, len(entries), context.temp_allocator)
for e, i in entries { out[i] = e.form_idx }
return out
}
emit_u16_array :: proc(sb: ^strings.Builder, name: string, items: []u16) {
fmt.sbprintfln(sb, "%s := [%d]u16{{", name, len(items))
for v, i in items {
if i % 16 == 0 {
if i > 0 { strings.write_string(sb, "\n") }
strings.write_string(sb, "\t")
}
fmt.sbprintf(sb, " %d,", v)
}
strings.write_string(sb, "\n}\n\n")
}
emit_range :: proc(sb: ^strings.Builder, name: string, ranges: []Range) {
fmt.sbprintfln(sb, "%s := [%d]lib.Decode_Index{{", name, len(ranges))
for r, i in ranges {
if r.count != 0 {
fmt.sbprintfln(sb, "\t0x%02X = {{% 5d, % 3d}},", i, r.start, r.count)
}
}
strings.write_string(sb, "}\n\n")
}
// -----------------------------------------------------------------------------
// Shared row + flags formatting
// -----------------------------------------------------------------------------
write_row :: proc(sb: ^strings.Builder, mn: lib.Mnemonic, ops: [4]lib.Operand_Type,
enc: [4]lib.Operand_Encoding, bits, mask: u32,
feature: lib.Feature, mode: lib.Mode, flags: lib.Encoding_Flags) {
fmt.sbprintf(sb, "\t{{ .%v, {{.%v,.%v,.%v,.%v}}, {{.%v,.%v,.%v,.%v}}, 0x%08X, 0x%08X, .%v, .%v, {{%s}} }},\n",
mn, ops[0], ops[1], ops[2], ops[3], enc[0], enc[1], enc[2], enc[3],
bits, mask, feature, mode, flags_lit(flags))
}
flags_lit :: proc(f: lib.Encoding_Flags) -> string {
parts: [dynamic]string
defer delete(parts)
if f.sets_flags { append(&parts, "sets_flags=true") }
if f.cond_in_28 { append(&parts, "cond_in_28=true") }
if f.branch { append(&parts, "branch=true") }
if f.cond_branch { append(&parts, "cond_branch=true") }
if f.writes_pc { append(&parts, "writes_pc=true") }
if f.thumb32 { append(&parts, "thumb32=true") }
if f.deprecated { append(&parts, "deprecated=true") }
return strings.join(parts[:], ", ", context.temp_allocator)
}
// -----------------------------------------------------------------------------
// Stage B writer + the library loader
// -----------------------------------------------------------------------------
emit_writer :: proc() {
sb := strings.builder_make()
strings.write_string(&sb, "package rexcode_arm32_generated\n\n")
strings.write_string(&sb, "// GENERATED by ../gen.odin -- DO NOT EDIT.\n")
strings.write_string(&sb, "// Stage B: serialize the typed tables above to raw blobs under ../../tables/.\n\n")
strings.write_string(&sb, "import \"core:os\"\nimport \"core:fmt\"\n\n")
strings.write_string(&sb, "TABLES :: #directory + \"/../../tables/\"\n\n")
strings.write_string(&sb, "raw :: #force_inline proc \"contextless\" (p: rawptr, n: int) -> []u8 {\n\treturn (cast([^]u8)p)[:n]\n}\n\n")
strings.write_string(&sb, "w :: proc(file: string, data: []u8) {\n")
strings.write_string(&sb, "\tif err := os.write_entire_file(file, data); err != nil {\n")
strings.write_string(&sb, "\t\tfmt.eprintfln(\"rexcode tablegen: failed to write %s: %v\", file, err)\n\t\tos.exit(1)\n\t}\n}\n\n")
strings.write_string(&sb, "main :: proc() {\n")
for b in BLOBS {
fmt.sbprintfln(&sb, "\tw(TABLES + \"%s\", raw(&%s, size_of(%s)))", b.file, b.global, b.global)
}
strings.write_string(&sb, "}\n")
emit_file(DIR_GEN + "writer.odin", &sb)
}
LOADER_TYPES :: `// -----------------------------------------------------------------------------
// Subsidiary table types (generated scaffolding)
// -----------------------------------------------------------------------------
// Companion run index: ENCODE_RUNS[mnemonic] -> contiguous run in ENCODE_FORMS.
Encode_Run :: struct {
start: u32,
count: u32,
}
Decode_Entry :: struct #packed {
mnemonic: Mnemonic, // 2
ops: [4]Operand_Type, // 4
enc: [4]Operand_Encoding, // 4
bits: u32, // 4
mask: u32, // 4
feature: Feature, // 1
mode: Mode, // 1
flags: Encoding_Flags, // 1
}
#assert(size_of(Decode_Entry) == 21)
Decode_Index :: struct #packed {
start: u16,
count: u16,
}
#assert(size_of(Decode_Index) == 4)
DECODE_T32_SUB_BUCKETS :: 32
`
LOADER_ACCESSORS :: `// -----------------------------------------------------------------------------
// Accessors
// -----------------------------------------------------------------------------
// Per-mnemonic encode forms: the run of ENCODE_FORMS belonging to ` + "`m`" + `.
// Replaces the old ENCODING_TABLE[m] slice; the returned view is into rodata.
@(private, require_results)
encoding_forms :: #force_inline proc "contextless" (m: Mnemonic) -> []Encoding {
r := ENCODE_RUNS[u16(m)]
return ENCODE_FORMS[r.start:][:r.count]
}
`
emit_loader :: proc() {
sb := strings.builder_make()
strings.write_string(&sb, "package rexcode_arm32\n\n")
strings.write_string(&sb, "// =============================================================================\n")
strings.write_string(&sb, "// GENERATED FILE - DO NOT EDIT\n")
strings.write_string(&sb, "// =============================================================================\n")
strings.write_string(&sb, "//\n")
strings.write_string(&sb, "// Loads the flat binary encode/decode tables into @(rodata). Produced by tablegen:\n")
strings.write_string(&sb, "//\n")
strings.write_string(&sb, "// odin run tablegen # Stage A: ENCODING_TABLE -> generated/ + this file\n")
strings.write_string(&sb, "// odin run tablegen/generated # Stage B: typed Odin literals -> tables/*.bin\n")
strings.write_string(&sb, "//\n")
strings.write_string(&sb, "// The .bin blobs are raw, host-endian, packed struct images.\n\n")
strings.write_string(&sb, LOADER_TYPES)
strings.write_string(&sb, "\n// -----------------------------------------------------------------------------\n")
strings.write_string(&sb, "// Loaded tables (rodata, embedded from tables/*.bin at compile time)\n")
strings.write_string(&sb, "// -----------------------------------------------------------------------------\n\n")
gmax, fmax := 0, 0
for b in BLOBS { gmax = max(gmax, len(b.global)); fmax = max(fmax, len(b.file)) }
for b in BLOBS {
fmt.sbprintf(&sb, "@(rodata) %s", b.global)
for _ in 0..<gmax-len(b.global) { strings.write_byte(&sb, ' ') }
path := fmt.tprintf("\"tables/%s\",", b.file)
fmt.sbprintf(&sb, " := #load(%s", path)
for _ in 0..<fmax-len(b.file) { strings.write_byte(&sb, ' ') }
fmt.sbprintfln(&sb, " []%s)", b.typ)
}
strings.write_string(&sb, "\n")
strings.write_string(&sb, LOADER_ACCESSORS)
emit_file(PATH_LOADER, &sb)
}
GEN_ATTRIB :: "// rexcode · Brendan Punsky (dotbmp@github), original author\n\n"
emit_file :: proc(path: string, sb: ^strings.Builder) {
if err := os.write_entire_file(path, transmute([]u8)strings.concatenate({GEN_ATTRIB, strings.to_string(sb^)})); err != nil {
fmt.eprintfln("rexcode tablegen: failed to write %s: %v", path, err)
os.exit(1)
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,34 @@
// rexcode · Brendan Punsky (dotbmp@github), original author
package rexcode_arm32_generated
// GENERATED by ../gen.odin -- DO NOT EDIT.
// Stage B: serialize the typed tables above to raw blobs under ../../tables/.
import "core:os"
import "core:fmt"
TABLES :: #directory + "/../../tables/"
raw :: #force_inline proc "contextless" (p: rawptr, n: int) -> []u8 {
return (cast([^]u8)p)[:n]
}
w :: proc(file: string, data: []u8) {
if err := os.write_entire_file(file, data); err != nil {
fmt.eprintfln("rexcode tablegen: failed to write %s: %v", file, err)
os.exit(1)
}
}
main :: proc() {
w(TABLES + "arm32.encode_forms.bin", raw(&ENCODE_FORMS, size_of(ENCODE_FORMS)))
w(TABLES + "arm32.encode_runs.bin", raw(&ENCODE_RUNS, size_of(ENCODE_RUNS)))
w(TABLES + "arm32.entries.bin", raw(&DECODE_ENTRIES, size_of(DECODE_ENTRIES)))
w(TABLES + "arm32.form_idx.bin", raw(&DECODE_FORM_IDX, size_of(DECODE_FORM_IDX)))
w(TABLES + "arm32.bucket_list.bin", raw(&DECODE_BUCKET_LIST, size_of(DECODE_BUCKET_LIST)))
w(TABLES + "arm32.idx_a32.bin", raw(&DECODE_INDEX_A32, size_of(DECODE_INDEX_A32)))
w(TABLES + "arm32.idx_t32.bin", raw(&DECODE_INDEX_T32, size_of(DECODE_INDEX_T32)))
w(TABLES + "arm32.idx_t16.bin", raw(&DECODE_INDEX_T16, size_of(DECODE_INDEX_T16)))
w(TABLES + "arm32.idx_t32_sub.bin", raw(&DECODE_INDEX_T32_SUB, size_of(DECODE_INDEX_T32_SUB)))
}

View File

@@ -0,0 +1,70 @@
// rexcode · Brendan Punsky (dotbmp@github), original author
package rexcode_arm32
// =============================================================================
// GENERATED FILE - DO NOT EDIT
// =============================================================================
//
// Loads the flat binary encode/decode tables into @(rodata). Produced by tablegen:
//
// odin run tablegen # Stage A: ENCODING_TABLE -> generated/ + this file
// odin run tablegen/generated # Stage B: typed Odin literals -> tables/*.bin
//
// The .bin blobs are raw, host-endian, packed struct images.
// -----------------------------------------------------------------------------
// Subsidiary table types (generated scaffolding)
// -----------------------------------------------------------------------------
// Companion run index: ENCODE_RUNS[mnemonic] -> contiguous run in ENCODE_FORMS.
Encode_Run :: struct {
start: u32,
count: u32,
}
Decode_Entry :: struct #packed {
mnemonic: Mnemonic, // 2
ops: [4]Operand_Type, // 4
enc: [4]Operand_Encoding, // 4
bits: u32, // 4
mask: u32, // 4
feature: Feature, // 1
mode: Mode, // 1
flags: Encoding_Flags, // 1
}
#assert(size_of(Decode_Entry) == 21)
Decode_Index :: struct #packed {
start: u16,
count: u16,
}
#assert(size_of(Decode_Index) == 4)
DECODE_T32_SUB_BUCKETS :: 32
// -----------------------------------------------------------------------------
// Loaded tables (rodata, embedded from tables/*.bin at compile time)
// -----------------------------------------------------------------------------
@(rodata) ENCODE_FORMS := #load("tables/arm32.encode_forms.bin", []Encoding)
@(rodata) ENCODE_RUNS := #load("tables/arm32.encode_runs.bin", []Encode_Run)
@(rodata) DECODE_ENTRIES := #load("tables/arm32.entries.bin", []Decode_Entry)
@(rodata) DECODE_FORM_IDX := #load("tables/arm32.form_idx.bin", []u16)
@(rodata) DECODE_BUCKET_LIST := #load("tables/arm32.bucket_list.bin", []u16)
@(rodata) DECODE_INDEX_A32 := #load("tables/arm32.idx_a32.bin", []Decode_Index)
@(rodata) DECODE_INDEX_T32 := #load("tables/arm32.idx_t32.bin", []Decode_Index)
@(rodata) DECODE_INDEX_T16 := #load("tables/arm32.idx_t16.bin", []Decode_Index)
@(rodata) DECODE_INDEX_T32_SUB := #load("tables/arm32.idx_t32_sub.bin", []Decode_Index)
// -----------------------------------------------------------------------------
// Accessors
// -----------------------------------------------------------------------------
// Per-mnemonic encode forms: the run of ENCODE_FORMS belonging to `m`.
// Replaces the old ENCODING_TABLE[m] slice; the returned view is into rodata.
@(private, require_results)
encoding_forms :: #force_inline proc "contextless" (m: Mnemonic) -> []Encoding {
r := ENCODE_RUNS[u16(m)]
return ENCODE_FORMS[r.start:][:r.count]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -1,3 +1,5 @@
// rexcode · Brendan Punsky (dotbmp@github), original author
package rexcode_arm32_tests
import "core:fmt"

View File

@@ -1,3 +1,5 @@
// rexcode · Brendan Punsky (dotbmp@github), original author
package rexcode_arm32_tests
import "core:fmt"
@@ -8,7 +10,8 @@ ok_count, fail_count: int
@(private="file")
check :: proc(name: string, mn: a.Mnemonic, idx: int, want_bits, want_mask: u32) {
enc := a.ENCODING_TABLE[mn]
_run := a.ENCODE_RUNS[u16(mn)]
enc := a.ENCODE_FORMS[_run.start:][:_run.count]
if idx >= len(enc) {
fmt.printf(" [FAIL] %s: entry %d not present (have %d entries)\n", name, idx, len(enc))
fail_count += 1

View File

@@ -1,3 +1,5 @@
// rexcode · Brendan Punsky (dotbmp@github), original author
package rexcode_arm32_tests
import "core:fmt"
@@ -41,7 +43,8 @@ run_sweep_tests :: proc() {
only_print_kind: string = ""
for mn in a.Mnemonic {
forms := a.ENCODING_TABLE[mn]
_run := a.ENCODE_RUNS[u16(mn)]
forms := a.ENCODE_FORMS[_run.start:][:_run.count]
for &f, idx in forms {
ilen := a.inst_size_from_bits(f.bits, f.mode)

View File

@@ -1,3 +1,5 @@
// rexcode · Brendan Punsky (dotbmp@github), original author
package main
// =============================================================================
@@ -39,7 +41,8 @@ main :: proc() {
n_a32, n_t32, n_t16 := 0, 0, 0
for mn in a.Mnemonic {
for &f in a.ENCODING_TABLE[mn] {
_run := a.ENCODE_RUNS[u16(mn)]
for &f in a.ENCODE_FORMS[_run.start:][:_run.count] {
bits := fill_safe_operands(&f)
ilen := a.inst_size_from_bits(f.bits, f.mode)
if f.mode == .A32 {

View File

@@ -1,381 +0,0 @@
package main
// =============================================================================
// AArch32 DECODE-TABLE GENERATOR
// =============================================================================
//
// Three primary dispatch tables (one per Mode/size):
//
// A32 (4-byte instructions):
// key = bits[27:20] of the 32-bit word (256 buckets).
// This is the natural major-opcode + S-bit + sub-op field.
//
// T32 32-bit (Thumb-2 wide):
// key = bits[31:25] of the packed u32 (high halfword top 7 bits;
// 128 buckets). Top 5 bits of the first halfword are 11101, 11110,
// or 11111 -- T32 32-bit identifier.
//
// T16 (Thumb-1, 16-bit):
// key = bits[15:10] of the halfword (64 buckets). Adequate spread for
// the ~120 T16 forms.
//
// Within each bucket, entries are sorted by mask-popcount descending so
// the most-specific encoding wins on first match. Linear scan inside the
// bucket is fine (worst-case ~50 entries on A32 data-proc, ~30 on T32).
//
// Run with: cd arm32 && odin run tools/gen_decode_tables.odin -file
// Output: ./decoding_tables.odin
import "core:fmt"
import "core:os"
import "core:slice"
import "core:strings"
import "core:math/bits"
import a "../"
Entry :: struct {
mnemonic: a.Mnemonic,
ops: [4]a.Operand_Type,
enc: [4]a.Operand_Encoding,
bits: u32,
mask: u32,
feature: a.Feature,
mode: a.Mode,
flags: a.Encoding_Flags,
is_thumb32: bool,
key: u16, // primary dispatch key (8 bits A32, 7 bits T32, 6 bits T16)
ilen: u8,
form_idx: u16, // index of this form within ENCODING_TABLE[mnemonic]
}
Range :: struct {
start: u16,
count: u16,
}
A32_BUCKETS :: 256 // bits[27:20]
T32_BUCKETS :: 128 // bits[31:25]
T16_BUCKETS :: 64 // bits[15:10]
// T32 wide instructions cluster around top-bit patterns 11101/11110/11111
// (bits[31:27] in {0x1D, 0x1E, 0x1F}). The primary bucket can hit ~100
// entries; we sub-bucket the densest primary buckets on bits[24:20] (32
// values) to bring per-bucket scan to <= 10.
T32_SUB_BUCKETS :: 32 // bits[24:20] of u32
main :: proc() {
fmt.println("Generating AArch32 decoder tables from ENCODING_TABLE...")
all: [dynamic]Entry
defer delete(all)
for mn in a.Mnemonic {
for f, fi in a.ENCODING_TABLE[mn] {
ilen := a.inst_size_from_bits(f.bits, f.mode)
e := Entry{
mnemonic = mn,
ops = f.ops,
enc = f.enc,
bits = f.bits,
mask = f.mask,
feature = f.feature,
mode = f.mode,
flags = f.flags,
is_thumb32 = f.flags.thumb32,
ilen = ilen,
form_idx = u16(fi),
}
// Compute dispatch key per mode/size.
if e.mode == .A32 {
e.key = u16((f.bits >> 20) & 0xFF)
} else if e.is_thumb32 || ilen == 4 {
// T32 32-bit: bits[31:25] of packed u32 (i.e. top 7 bits of
// the high halfword)
e.key = u16((f.bits >> 25) & 0x7F)
} else {
// T16: bits[15:10] of the halfword (stored in low 16 of u32)
e.key = u16((f.bits >> 10) & 0x3F)
}
append(&all, e)
}
}
// Sort: by mode group (A32 first, then T32-wide, then T16), then by key,
// then by mask popcount descending so more-specific forms match first.
slice.sort_by(all[:], proc(x, y: Entry) -> bool {
mx := mode_rank(x)
my := mode_rank(y)
if mx != my { return mx < my }
if x.key != y.key { return x.key < y.key }
xc := bits.count_ones(x.mask)
yc := bits.count_ones(y.mask)
if xc != yc { return xc > yc }
return u16(x.mnemonic) < u16(y.mnemonic)
})
// First pass: collect (entry_idx, bucket_key) pairs across modes, expanding
// variable bits within the bucket-key range. Then group by bucket.
// Secondary T32 index keyed on bits 24:20 of u32 (the densest primary
// T32 bucket has > 100 entries; this brings the inner scan to ~10).
A32_Pair :: struct { bucket: u16, entry_idx: u16 }
a32_pairs: [dynamic]A32_Pair
t32_pairs: [dynamic]A32_Pair
t16_pairs: [dynamic]A32_Pair
t32_sub_pairs: [dynamic]A32_Pair
defer delete(a32_pairs); defer delete(t32_pairs)
defer delete(t16_pairs); defer delete(t32_sub_pairs)
enumerate_keys :: proc(bits, mask: u32, key_shift: u32, key_bits: u32, out: ^[dynamic]u16) {
clear(out)
// Variable bits within the bucket-key range. For each combination of
// those variable bits we emit a separate bucket key, so a single entry
// is reachable via every word that can match its mask. fixed_key must
// be sanitized via `bits & mask` — entry bits often carry a default
// value at variable positions (e.g. U=1 in LDR's base 0x05900000), and
// those defaults must not pre-set bits in the key or we'd skip the
// zero-side bucket during enumeration.
key_mask := (u32(1) << key_bits) - 1
fixed_key := ((bits & mask) >> key_shift) & key_mask
var_bits := (~mask >> key_shift) & key_mask
// Enumerate submasks of var_bits via the classic Gosper-style walk.
sub: u32 = 0
for {
append(out, u16(fixed_key | sub))
if var_bits == 0 { break }
if sub == var_bits { break }
sub = (sub - var_bits) & var_bits // next non-zero submask
}
}
keys: [dynamic]u16
defer delete(keys)
for e, i in all {
if e.mode == .A32 {
enumerate_keys(e.bits, e.mask, 20, 8, &keys)
for k in keys { append(&a32_pairs, A32_Pair{bucket = k, entry_idx = u16(i)}) }
} else if e.is_thumb32 || e.ilen == 4 {
enumerate_keys(e.bits, e.mask, 25, 7, &keys)
for k in keys { append(&t32_pairs, A32_Pair{bucket = k, entry_idx = u16(i)}) }
// Sub-bucket: bits 24:20 of word
sub_keys: [dynamic]u16
defer delete(sub_keys)
enumerate_keys(e.bits, e.mask, 20, 5, &sub_keys)
for k in keys {
for sk in sub_keys {
append(&t32_sub_pairs, A32_Pair{
bucket = k * T32_SUB_BUCKETS + sk,
entry_idx = u16(i),
})
}
}
} else {
enumerate_keys(e.bits, e.mask, 10, 6, &keys)
for k in keys { append(&t16_pairs, A32_Pair{bucket = k, entry_idx = u16(i)}) }
}
}
// The original entry array `all` is already sorted by (mode, key, popcount,
// mnemonic). We need to emit a single linear DECODE_ENTRIES array where
// each bucket points to a contiguous slice. Because an entry can appear in
// multiple buckets, we duplicate entries in the emitted array — bucket
// (start, count) addresses the duplicated region.
// Within each bucket we want most-specific (highest mask popcount) first,
// tiebreak by mnemonic, so the decoder's linear scan picks the most
// specific encoding before falling through to a more general one. Encode
// (bucket, -popcount, mnemonic) into a single u64 sort key so we don't
// need a closure-capturing comparator.
Sort_Pair :: struct { sort_key: u64, entry_idx: u16, bucket: u16 }
rebuild :: proc(pairs: ^[dynamic]A32_Pair, all: []Entry) {
sortable := make([dynamic]Sort_Pair, 0, len(pairs))
defer delete(sortable)
for p in pairs^ {
e := all[p.entry_idx]
pop := u64(bits.count_ones(e.mask))
// bucket << 48 | (255 - pop) << 32 | mnemonic
key := (u64(p.bucket) << 48) | ((255 - pop) << 32) | u64(e.mnemonic)
append(&sortable, Sort_Pair{
sort_key = key, entry_idx = p.entry_idx, bucket = p.bucket,
})
}
slice.sort_by_key(sortable[:], proc(s: Sort_Pair) -> u64 { return s.sort_key })
clear(pairs)
for s in sortable { append(pairs, A32_Pair{bucket = s.bucket, entry_idx = s.entry_idx}) }
}
rebuild(&a32_pairs, all[:])
rebuild(&t32_pairs, all[:])
rebuild(&t16_pairs, all[:])
rebuild(&t32_sub_pairs, all[:])
// Build a flat u16 dispatch list (DECODE_BUCKET_LIST). Each bucket
// points to a contiguous run of entry indices in that list. Duplicating
// small u16 indices instead of full 21-byte entries keeps the LLVM
// initializer manageable (the previous "duplicate full entries" approach
// produced ~108KB of initializer and broke codegen).
a32_idx: [A32_BUCKETS]Range
t32_idx: [T32_BUCKETS]Range
t16_idx: [T16_BUCKETS]Range
t32_sub_idx: [T32_BUCKETS * T32_SUB_BUCKETS]Range
bucket_list: [dynamic]u16
defer delete(bucket_list)
emit_pairs :: proc(
pairs: []A32_Pair, idx: []Range, list: ^[dynamic]u16,
) {
prev_bucket: i32 = -1
for p in pairs {
cur_bucket := i32(p.bucket)
if cur_bucket != prev_bucket {
idx[cur_bucket].start = u16(len(list))
idx[cur_bucket].count = 0
prev_bucket = cur_bucket
}
append(list, p.entry_idx)
idx[cur_bucket].count += 1
}
}
emit_pairs(a32_pairs[:], a32_idx[:], &bucket_list)
emit_pairs(t32_pairs[:], t32_idx[:], &bucket_list)
emit_pairs(t16_pairs[:], t16_idx[:], &bucket_list)
emit_pairs(t32_sub_pairs[:], t32_sub_idx[:], &bucket_list)
sb: strings.Builder
strings.builder_init(&sb)
defer strings.builder_destroy(&sb)
emit_header(&sb)
emit_entries(&sb, all[:])
emit_form_idx(&sb, all[:])
emit_bucket_list(&sb, bucket_list[:])
emit_range_table(&sb, "DECODE_INDEX_A32", a32_idx[:])
emit_range_table(&sb, "DECODE_INDEX_T32", t32_idx[:])
emit_range_table(&sb, "DECODE_INDEX_T16", t16_idx[:])
emit_range_table(&sb, "DECODE_INDEX_T32_SUB", t32_sub_idx[:])
err := os.write_entire_file("decoding_tables.odin", transmute([]u8)strings.to_string(sb))
if err != nil {
fmt.eprintfln("FAILED to write decoding_tables.odin: %v", err)
os.exit(1)
}
max_a32, max_t32, max_t16: u16
pop_a32, pop_t32, pop_t16: int
for r in a32_idx { if r.count > max_a32 { max_a32 = r.count }; if r.count > 0 { pop_a32 += 1 } }
for r in t32_idx { if r.count > max_t32 { max_t32 = r.count }; if r.count > 0 { pop_t32 += 1 } }
for r in t16_idx { if r.count > max_t16 { max_t16 = r.count }; if r.count > 0 { pop_t16 += 1 } }
fmt.printfln("OK -- %d entries: A32 %d buckets (max=%d); T32 %d buckets (max=%d); T16 %d buckets (max=%d)",
len(all), pop_a32, max_a32, pop_t32, max_t32, pop_t16, max_t16)
}
mode_rank :: proc(e: Entry) -> int {
if e.mode == .A32 { return 0 }
if e.is_thumb32 || e.ilen == 4 { return 1 }
return 2
}
push_range :: proc(r: ^Range, i: u16) {
if r.count == 0 { r.start = i }
r.count += 1
}
emit_header :: proc(sb: ^strings.Builder) {
strings.write_string(sb, `package rexcode_arm32
// =============================================================================
// GENERATED FILE - DO NOT EDIT
// =============================================================================
//
// Generated by tools/gen_decode_tables.odin from ENCODING_TABLE.
// Regenerate with: cd arm32 && odin run tools/gen_decode_tables.odin -file
//
Decode_Entry :: struct #packed {
mnemonic: Mnemonic,
ops: [4]Operand_Type,
enc: [4]Operand_Encoding,
bits: u32,
mask: u32,
feature: Feature,
mode: Mode,
flags: Encoding_Flags,
}
#assert(size_of(Decode_Entry) == 21)
Decode_Index :: struct #packed {
start: u16,
count: u16,
}
#assert(size_of(Decode_Index) == 4)
DECODE_T32_SUB_BUCKETS :: 32
`)
}
emit_entries :: proc(sb: ^strings.Builder, entries: []Entry) {
fmt.sbprintfln(sb, "")
fmt.sbprintfln(sb, "@(rodata)")
fmt.sbprintfln(sb, "DECODE_ENTRIES := [%d]Decode_Entry{{", len(entries))
for e in entries {
flags_str := encode_flags_literal(e.flags)
fmt.sbprintfln(sb,
"\t{{.%v, {{.%v, .%v, .%v, .%v}}, {{.%v, .%v, .%v, .%v}}, 0x%08X, 0x%08X, .%v, .%v, {{%s}}}},",
e.mnemonic,
e.ops[0], e.ops[1], e.ops[2], e.ops[3],
e.enc[0], e.enc[1], e.enc[2], e.enc[3],
e.bits, e.mask, e.feature, e.mode, flags_str)
}
strings.write_string(sb, "}\n\n")
}
encode_flags_literal :: proc(f: a.Encoding_Flags) -> string {
sb: strings.Builder
strings.builder_init(&sb)
first := true
write := proc(sb: ^strings.Builder, first: ^bool, s: string) {
if !first^ { strings.write_string(sb, ", ") }
strings.write_string(sb, s)
first^ = false
}
if f.sets_flags { write(&sb, &first, "sets_flags=true") }
if f.cond_in_28 { write(&sb, &first, "cond_in_28=true") }
if f.branch { write(&sb, &first, "branch=true") }
if f.cond_branch { write(&sb, &first, "cond_branch=true") }
if f.writes_pc { write(&sb, &first, "writes_pc=true") }
if f.thumb32 { write(&sb, &first, "thumb32=true") }
if f.deprecated { write(&sb, &first, "deprecated=true") }
return strings.to_string(sb)
}
emit_range_table :: proc(sb: ^strings.Builder, name: string, ranges: []Range) {
fmt.sbprintfln(sb, "@(rodata)")
fmt.sbprintf(sb, "%s := [%d]Decode_Index{{\n", name, len(ranges))
for r, i in ranges {
if r.count != 0 {
fmt.sbprintf(sb, "\t0x%02X = {{%d, %d}},\n", i, r.start, r.count)
}
}
strings.write_string(sb, "}\n\n")
}
emit_form_idx :: proc(sb: ^strings.Builder, entries: []Entry) {
fmt.sbprintfln(sb, "@(rodata)")
fmt.sbprintf(sb, "DECODE_FORM_IDX := [%d]u16{{\n", len(entries))
for e, i in entries {
if i > 0 && i % 16 == 0 { strings.write_string(sb, "\n") }
fmt.sbprintf(sb, " %d,", e.form_idx)
}
strings.write_string(sb, "\n}\n\n")
}
emit_bucket_list :: proc(sb: ^strings.Builder, items: []u16) {
fmt.sbprintfln(sb, "@(rodata)")
fmt.sbprintf(sb, "DECODE_BUCKET_LIST := [%d]u16{{\n", len(items))
for v, i in items {
if i > 0 && i % 16 == 0 { strings.write_string(sb, "\n") }
fmt.sbprintf(sb, " %d,", v)
}
strings.write_string(sb, "\n}\n\n")
}

View File

@@ -1,4 +1,6 @@
#!/bin/bash
# rexcode · Brendan Punsky (dotbmp@github), original author
# Per-line llvm-mc disassembly wrapper.
#
# llvm-mc reads the entire stdin as a stream and decodes greedily, so a

View File

@@ -1,3 +1,5 @@
// rexcode · Brendan Punsky (dotbmp@github), original author
package main
// =============================================================================

View File

@@ -1,3 +1,5 @@
// rexcode · Brendan Punsky (dotbmp@github), original author
package rexcode_arm64
// =============================================================================

View File

@@ -1,3 +1,5 @@
// rexcode · Brendan Punsky (dotbmp@github), original author
package rexcode_arm64
import "../isa"

File diff suppressed because it is too large Load Diff

View File

@@ -1,3 +1,5 @@
// rexcode · Brendan Punsky (dotbmp@github), original author
package rexcode_arm64
// =============================================================================
@@ -104,7 +106,7 @@ encode_one_inline :: #force_inline proc(
append(errors, Error{inst_idx = u32(inst_idx), code = .INVALID_MNEMONIC})
return 0, false
}
forms := ENCODING_TABLE[inst.mnemonic]
forms := encoding_forms(inst.mnemonic)
if len(forms) == 0 {
append(errors, Error{inst_idx = u32(inst_idx), code = .INVALID_MNEMONIC})
return 0, false

View File

@@ -1,3 +1,5 @@
// rexcode · Brendan Punsky (dotbmp@github), original author
package rexcode_arm64
import "../isa"

View File

@@ -1,3 +1,5 @@
// rexcode · Brendan Punsky (dotbmp@github), original author
package rexcode_arm64
// =============================================================================

View File

@@ -1,3 +1,5 @@
// rexcode · Brendan Punsky (dotbmp@github), original author
package rexcode_arm64
// =============================================================================

View File

@@ -1,3 +1,5 @@
// rexcode · Brendan Punsky (dotbmp@github), original author
package rexcode_arm64
// =============================================================================

View File

@@ -1,3 +1,5 @@
// rexcode · Brendan Punsky (dotbmp@github), original author
package rexcode_arm64
import "core:strings"

View File

@@ -1,3 +1,5 @@
// rexcode · Brendan Punsky (dotbmp@github), original author
package rexcode_arm64
// =============================================================================

View File

@@ -1,3 +1,5 @@
// rexcode · Brendan Punsky (dotbmp@github), original author
package rexcode_arm64
// =============================================================================

View File

@@ -1,3 +1,5 @@
// rexcode · Brendan Punsky (dotbmp@github), original author
package rexcode_arm64
// =============================================================================

View File

@@ -1,4 +1,6 @@
package rexcode_arm64
// rexcode · Brendan Punsky (dotbmp@github), original author
package rexcode_arm64_tablegen
// =============================================================================
// AArch64 ENCODING_TABLE (v1: base integer + FP scalar)

View File

@@ -0,0 +1,293 @@
// rexcode · Brendan Punsky (dotbmp@github), original author
package rexcode_arm64_tablegen
// =============================================================================
// AArch64 TABLE GENERATOR (Stage A)
// =============================================================================
//
// Reads the single-source-of-truth ENCODING_TABLE (encoding_table.odin, this
// package) and emits human-readable, type-checked Odin into ./generated/:
//
// generated/encode_tables.odin ENCODE_FORMS + ENCODE_RUNS (flattened encode)
// generated/decode_tables.odin DECODE_ENTRIES + DECODE_INDEX_OP0 index table
// generated/writer.odin Stage B: serialize those globals to ../../tables/*.bin
//
// It also re-emits the library loader ../tables.odin. Run:
// odin run arm64/tablegen # Stage A
// odin run arm64/tablegen/generated # Stage B
import "core:fmt"
import "core:os"
import "core:strings"
import "core:slice"
import "core:reflect"
import "core:math/bits"
import lib "../"
// Package-scope aliases so the moved SoT resolves Mnemonic/Encoding unqualified.
Encoding :: lib.Encoding
Mnemonic :: lib.Mnemonic
Blob :: struct { global, file, typ: string }
BLOBS := [?]Blob{
{"ENCODE_FORMS", "arm64.encode_forms.bin", "Encoding"},
{"ENCODE_RUNS", "arm64.encode_runs.bin", "Encode_Run"},
{"DECODE_ENTRIES", "arm64.entries.bin", "Decode_Entry"},
{"DECODE_INDEX_OP0", "arm64.idx_op0.bin", "Decode_Index"},
}
DIR_GEN :: #directory + "/generated/"
PATH_LOADER :: #directory + "/../tables.odin"
Entry :: struct {
mnemonic: lib.Mnemonic,
ops: [4]lib.Operand_Type,
enc: [4]lib.Operand_Encoding,
bits: u32,
mask: u32,
feature: lib.Feature,
flags: lib.Encoding_Flags,
op0: u8, // bits[28:25]
}
Range :: struct { start: u16, count: u16 }
main :: proc() {
n := emit_encode_tables()
ne := emit_decode_tables()
emit_writer()
emit_loader()
fmt.printfln("arm64 tablegen: %d encode forms, %d decode entries", n, ne)
}
// -----------------------------------------------------------------------------
// Encode side
// -----------------------------------------------------------------------------
emit_encode_tables :: proc() -> (total: int) {
sb := strings.builder_make()
strings.write_string(&sb, "package rexcode_arm64_generated\n\n")
strings.write_string(&sb, "// GENERATED by ../gen.odin -- DO NOT EDIT.\n")
strings.write_string(&sb, "// Flattened encode forms + per-mnemonic run index (source: ENCODING_TABLE).\n\n")
strings.write_string(&sb, "import lib \"../..\"\n\n")
for m in Mnemonic { total += len(ENCODING_TABLE[m]) }
fmt.sbprintfln(&sb, "ENCODE_FORMS := [%d]lib.Encoding{{", total)
for m in Mnemonic {
forms := ENCODING_TABLE[m]
if len(forms) == 0 { continue }
fmt.sbprintfln(&sb, "\t// .%v", m)
for f in forms {
write_row(&sb, f.mnemonic, f.ops, f.enc, f.bits, f.mask, f.feature, f.flags)
}
}
strings.write_string(&sb, "}\n\n")
run_w := 0
for m in Mnemonic { run_w = max(run_w, len(reflect.enum_string(m))) }
strings.write_string(&sb, "ENCODE_RUNS := [lib.Mnemonic]lib.Encode_Run{\n")
start := 0
for m in Mnemonic {
c := len(ENCODING_TABLE[m])
name := reflect.enum_string(m)
fmt.sbprintf(&sb, "\t.%s", name)
for _ in 0..<run_w-len(name) { strings.write_byte(&sb, ' ') }
fmt.sbprintfln(&sb, " = {{% 5d, % 3d}},", start, c)
start += c
}
strings.write_string(&sb, "}\n")
emit_file(DIR_GEN + "encode_tables.odin", &sb)
return
}
// -----------------------------------------------------------------------------
// Decode side
// -----------------------------------------------------------------------------
//
// AArch64 has no single primary-opcode field; the ARM ARM divides the ISA by
// `op0` at bits[28:25] into a 16-slot dispatch index. Each encoding replicates
// across every op0 bucket whose value is consistent with the form's masked
// static pattern. Mask popcount descending sort lets the most-specific entry
// match first within a bucket.
emit_decode_tables :: proc() -> (total: int) {
all: [dynamic]Entry
defer delete(all)
for mn in Mnemonic {
for f in ENCODING_TABLE[mn] {
op0_static := u8((f.bits >> 25) & 0xF)
op0_mask := u8((f.mask >> 25) & 0xF)
// Enumerate every 4-bit bucket B such that (B & op0_mask) == op0_static.
for b: u8 = 0; b < 16; b += 1 {
if (b & op0_mask) != op0_static { continue }
append(&all, Entry{f.mnemonic, f.ops, f.enc, f.bits, f.mask, f.feature, f.flags, b})
}
}
}
slice.sort_by(all[:], proc(a, b: Entry) -> bool {
if a.op0 != b.op0 { return a.op0 < b.op0 }
ac := bits.count_ones(a.mask); bc := bits.count_ones(b.mask)
if ac != bc { return ac > bc }
return u16(a.mnemonic) < u16(b.mnemonic)
})
op0_idx: [16]Range
for e, i in all { push(&op0_idx[e.op0], u16(i)) }
sb := strings.builder_make()
strings.write_string(&sb, "package rexcode_arm64_generated\n\n")
strings.write_string(&sb, "// GENERATED by ../gen.odin -- DO NOT EDIT.\n")
strings.write_string(&sb, "// Reverse decode tables (source: ENCODING_TABLE), keyed by op0 (bits 28:25).\n\n")
strings.write_string(&sb, "import lib \"../..\"\n\n")
fmt.sbprintfln(&sb, "DECODE_ENTRIES := [%d]lib.Decode_Entry{{", len(all))
for e in all {
write_row(&sb, e.mnemonic, e.ops, e.enc, e.bits, e.mask, e.feature, e.flags)
}
strings.write_string(&sb, "}\n\n")
emit_range(&sb, "DECODE_INDEX_OP0", op0_idx[:])
emit_file(DIR_GEN + "decode_tables.odin", &sb)
return len(all)
}
push :: proc(r: ^Range, i: u16) { if r.count == 0 { r.start = i }; r.count += 1 }
emit_range :: proc(sb: ^strings.Builder, name: string, ranges: []Range) {
fmt.sbprintfln(sb, "%s := [%d]lib.Decode_Index{{", name, len(ranges))
for r, i in ranges {
if r.count != 0 {
fmt.sbprintfln(sb, "\t0x%02X = {{% 4d, % 3d}},", i, r.start, r.count)
}
}
strings.write_string(sb, "}\n\n")
}
// -----------------------------------------------------------------------------
// Shared row + flags formatting (compact, matching arm64's original generator)
// -----------------------------------------------------------------------------
write_row :: proc(sb: ^strings.Builder, mn: lib.Mnemonic, ops: [4]lib.Operand_Type,
enc: [4]lib.Operand_Encoding, bits, mask: u32, feature: lib.Feature, flags: lib.Encoding_Flags) {
fmt.sbprintf(sb, "\t{{ .%v, {{.%v,.%v,.%v,.%v}}, {{.%v,.%v,.%v,.%v}}, 0x%08X, 0x%08X, .%v, {{%s}} }},\n",
mn, ops[0], ops[1], ops[2], ops[3], enc[0], enc[1], enc[2], enc[3], bits, mask, feature, flags_lit(flags))
}
flags_lit :: proc(f: lib.Encoding_Flags) -> string {
parts: [dynamic]string
defer delete(parts)
if f.branch { append(&parts, "branch=true") }
if f.cond_branch { append(&parts, "cond_branch=true") }
if f.writes_pc { append(&parts, "writes_pc=true") }
if f.sets_flags { append(&parts, "sets_flags=true") }
if f.is_64 { append(&parts, "is_64=true") }
return strings.join(parts[:], ", ", context.temp_allocator)
}
// -----------------------------------------------------------------------------
// Stage B writer + the library loader
// -----------------------------------------------------------------------------
emit_writer :: proc() {
sb := strings.builder_make()
strings.write_string(&sb, "package rexcode_arm64_generated\n\n")
strings.write_string(&sb, "// GENERATED by ../gen.odin -- DO NOT EDIT.\n")
strings.write_string(&sb, "// Stage B: serialize the typed tables above to raw blobs under ../../tables/.\n\n")
strings.write_string(&sb, "import \"core:os\"\nimport \"core:fmt\"\n\n")
strings.write_string(&sb, "TABLES :: #directory + \"/../../tables/\"\n\n")
strings.write_string(&sb, "raw :: #force_inline proc \"contextless\" (p: rawptr, n: int) -> []u8 {\n\treturn (cast([^]u8)p)[:n]\n}\n\n")
strings.write_string(&sb, "w :: proc(file: string, data: []u8) {\n")
strings.write_string(&sb, "\tif err := os.write_entire_file(file, data); err != nil {\n")
strings.write_string(&sb, "\t\tfmt.eprintfln(\"rexcode tablegen: failed to write %s: %v\", file, err)\n\t\tos.exit(1)\n\t}\n}\n\n")
strings.write_string(&sb, "main :: proc() {\n")
for b in BLOBS {
fmt.sbprintfln(&sb, "\tw(TABLES + \"%s\", raw(&%s, size_of(%s)))", b.file, b.global, b.global)
}
strings.write_string(&sb, "}\n")
emit_file(DIR_GEN + "writer.odin", &sb)
}
LOADER_TYPES :: `// -----------------------------------------------------------------------------
// Subsidiary table types (generated scaffolding)
// -----------------------------------------------------------------------------
// Companion run index: ENCODE_RUNS[mnemonic] -> contiguous run in ENCODE_FORMS.
Encode_Run :: struct {
start: u32,
count: u32,
}
Decode_Entry :: struct #packed {
mnemonic: Mnemonic, // 2
ops: [4]Operand_Type, // 4
enc: [4]Operand_Encoding, // 4
bits: u32, // 4
mask: u32, // 4
feature: Feature, // 1
flags: Encoding_Flags, // 1
}
#assert(size_of(Decode_Entry) == 20)
Decode_Index :: struct #packed {
start: u16,
count: u16,
}
#assert(size_of(Decode_Index) == 4)
`
LOADER_ACCESSORS :: `// -----------------------------------------------------------------------------
// Accessors
// -----------------------------------------------------------------------------
// Per-mnemonic encode forms: the run of ENCODE_FORMS belonging to ` + "`m`" + `.
// Replaces the old ENCODING_TABLE[m] slice; the returned view is into rodata.
@(private, require_results)
encoding_forms :: #force_inline proc "contextless" (m: Mnemonic) -> []Encoding {
r := ENCODE_RUNS[u16(m)]
return ENCODE_FORMS[r.start:][:r.count]
}
`
emit_loader :: proc() {
sb := strings.builder_make()
strings.write_string(&sb, "package rexcode_arm64\n\n")
strings.write_string(&sb, "// =============================================================================\n")
strings.write_string(&sb, "// GENERATED FILE - DO NOT EDIT\n")
strings.write_string(&sb, "// =============================================================================\n")
strings.write_string(&sb, "//\n")
strings.write_string(&sb, "// Loads the flat binary encode/decode tables into @(rodata). Produced by tablegen:\n")
strings.write_string(&sb, "//\n")
strings.write_string(&sb, "// odin run tablegen # Stage A: ENCODING_TABLE -> generated/ + this file\n")
strings.write_string(&sb, "// odin run tablegen/generated # Stage B: typed Odin literals -> tables/*.bin\n")
strings.write_string(&sb, "//\n")
strings.write_string(&sb, "// The .bin blobs are raw, host-endian, packed struct images.\n\n")
strings.write_string(&sb, LOADER_TYPES)
strings.write_string(&sb, "\n// -----------------------------------------------------------------------------\n")
strings.write_string(&sb, "// Loaded tables (rodata, embedded from tables/*.bin at compile time)\n")
strings.write_string(&sb, "// -----------------------------------------------------------------------------\n\n")
gmax, fmax := 0, 0
for b in BLOBS { gmax = max(gmax, len(b.global)); fmax = max(fmax, len(b.file)) }
for b in BLOBS {
fmt.sbprintf(&sb, "@(rodata) %s", b.global)
for _ in 0..<gmax-len(b.global) { strings.write_byte(&sb, ' ') }
path := fmt.tprintf("\"tables/%s\",", b.file)
fmt.sbprintf(&sb, " := #load(%s", path)
for _ in 0..<fmax-len(b.file) { strings.write_byte(&sb, ' ') }
fmt.sbprintfln(&sb, " []%s)", b.typ)
}
strings.write_string(&sb, "\n")
strings.write_string(&sb, LOADER_ACCESSORS)
emit_file(PATH_LOADER, &sb)
}
GEN_ATTRIB :: "// rexcode · Brendan Punsky (dotbmp@github), original author\n\n"
emit_file :: proc(path: string, sb: ^strings.Builder) {
if err := os.write_entire_file(path, transmute([]u8)strings.concatenate({GEN_ATTRIB, strings.to_string(sb^)})); err != nil {
fmt.eprintfln("rexcode tablegen: failed to write %s: %v", path, err)
os.exit(1)
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,29 @@
// rexcode · Brendan Punsky (dotbmp@github), original author
package rexcode_arm64_generated
// GENERATED by ../gen.odin -- DO NOT EDIT.
// Stage B: serialize the typed tables above to raw blobs under ../../tables/.
import "core:os"
import "core:fmt"
TABLES :: #directory + "/../../tables/"
raw :: #force_inline proc "contextless" (p: rawptr, n: int) -> []u8 {
return (cast([^]u8)p)[:n]
}
w :: proc(file: string, data: []u8) {
if err := os.write_entire_file(file, data); err != nil {
fmt.eprintfln("rexcode tablegen: failed to write %s: %v", file, err)
os.exit(1)
}
}
main :: proc() {
w(TABLES + "arm64.encode_forms.bin", raw(&ENCODE_FORMS, size_of(ENCODE_FORMS)))
w(TABLES + "arm64.encode_runs.bin", raw(&ENCODE_RUNS, size_of(ENCODE_RUNS)))
w(TABLES + "arm64.entries.bin", raw(&DECODE_ENTRIES, size_of(DECODE_ENTRIES)))
w(TABLES + "arm64.idx_op0.bin", raw(&DECODE_INDEX_OP0, size_of(DECODE_INDEX_OP0)))
}

View File

@@ -0,0 +1,62 @@
// rexcode · Brendan Punsky (dotbmp@github), original author
package rexcode_arm64
// =============================================================================
// GENERATED FILE - DO NOT EDIT
// =============================================================================
//
// Loads the flat binary encode/decode tables into @(rodata). Produced by tablegen:
//
// odin run tablegen # Stage A: ENCODING_TABLE -> generated/ + this file
// odin run tablegen/generated # Stage B: typed Odin literals -> tables/*.bin
//
// The .bin blobs are raw, host-endian, packed struct images.
// -----------------------------------------------------------------------------
// Subsidiary table types (generated scaffolding)
// -----------------------------------------------------------------------------
// Companion run index: ENCODE_RUNS[mnemonic] -> contiguous run in ENCODE_FORMS.
Encode_Run :: struct {
start: u32,
count: u32,
}
Decode_Entry :: struct #packed {
mnemonic: Mnemonic, // 2
ops: [4]Operand_Type, // 4
enc: [4]Operand_Encoding, // 4
bits: u32, // 4
mask: u32, // 4
feature: Feature, // 1
flags: Encoding_Flags, // 1
}
#assert(size_of(Decode_Entry) == 20)
Decode_Index :: struct #packed {
start: u16,
count: u16,
}
#assert(size_of(Decode_Index) == 4)
// -----------------------------------------------------------------------------
// Loaded tables (rodata, embedded from tables/*.bin at compile time)
// -----------------------------------------------------------------------------
@(rodata) ENCODE_FORMS := #load("tables/arm64.encode_forms.bin", []Encoding)
@(rodata) ENCODE_RUNS := #load("tables/arm64.encode_runs.bin", []Encode_Run)
@(rodata) DECODE_ENTRIES := #load("tables/arm64.entries.bin", []Decode_Entry)
@(rodata) DECODE_INDEX_OP0 := #load("tables/arm64.idx_op0.bin", []Decode_Index)
// -----------------------------------------------------------------------------
// Accessors
// -----------------------------------------------------------------------------
// Per-mnemonic encode forms: the run of ENCODE_FORMS belonging to `m`.
// Replaces the old ENCODING_TABLE[m] slice; the returned view is into rodata.
@(private, require_results)
encoding_forms :: #force_inline proc "contextless" (m: Mnemonic) -> []Encoding {
r := ENCODE_RUNS[u16(m)]
return ENCODE_FORMS[r.start:][:r.count]
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -1,3 +1,5 @@
// rexcode · Brendan Punsky (dotbmp@github), original author
package rexcode_arm64_tests
// End-to-end AArch64 pipeline tests: encode -> decode -> print round-trips

View File

@@ -1,3 +1,5 @@
// rexcode · Brendan Punsky (dotbmp@github), original author
package rexcode_arm64_tests
// Spot-check ENCODING_TABLE entries against canonical bit patterns from
@@ -15,7 +17,8 @@ import a "../"
@(private="file")
check :: proc(name: string, m: a.Mnemonic, idx: int, want_bits, want_mask: u32) {
encs := a.ENCODING_TABLE[m]
_run := a.ENCODE_RUNS[u16(m)]
encs := a.ENCODE_FORMS[_run.start:][:_run.count]
if idx >= len(encs) {
fmt.printfln(" [FAIL] %s: no encoding at idx %d", name, idx)
failures += 1

View File

@@ -1,3 +1,5 @@
// rexcode · Brendan Punsky (dotbmp@github), original author
package main
// =============================================================================
@@ -33,7 +35,8 @@ main :: proc() {
count := 0
for mn in a.Mnemonic {
for f in a.ENCODING_TABLE[mn] {
_run := a.ENCODE_RUNS[u16(mn)]
for f in a.ENCODE_FORMS[_run.start:][:_run.count] {
b0 := u8( f.bits & 0xFF)
b1 := u8((f.bits >> 8) & 0xFF)
b2 := u8((f.bits >> 16) & 0xFF)

View File

@@ -1,185 +0,0 @@
package main
// =============================================================================
// AArch64 DECODE-TABLE GENERATOR
// =============================================================================
//
// AArch64 doesn't have a single primary-opcode field; the ARM ARM divides
// the ISA by `op0` at bits[28:25] into 8 top-level encoding classes plus
// SVE/reserved. For v1 we use that 4-bit field directly as a 16-slot
// dispatch index and linear-scan within each bucket. Mask popcount
// descending sort lets the most-specific entry match first.
//
// 16 buckets * ~7 entries average in the v1 table is fine; the densest
// bucket (DPR at op0=x101) holds ~35 entries which is one cache line of
// linear scan -- under 100 cycles per decode worst case. Sub-bucketing
// can be added later if profiling shows it matters.
//
// Run with: cd arm64 && odin run tools/gen_decode_tables.odin -file
// Output: ./decoding_tables.odin
import "core:fmt"
import "core:os"
import "core:slice"
import "core:strings"
import "core:math/bits"
import a "../"
Entry :: struct {
mnemonic: a.Mnemonic,
ops: [4]a.Operand_Type,
enc: [4]a.Operand_Encoding,
bits: u32,
mask: u32,
feature: a.Feature,
flags: a.Encoding_Flags,
op0: u8, // bits[28:25]
}
Range :: struct {
start: u16,
count: u16,
}
main :: proc() {
fmt.println("Generating AArch64 decoder tables from ENCODING_TABLE...")
all: [dynamic]Entry
defer delete(all)
// For each encoding, enumerate every op0 bucket (bits 28:25) that
// matches the entry's static pattern. Entries with mask bits *outside*
// the op0 range fixed (e.g. B/BL with mask 0xFC000000 = bits 31:26
// static, bit 25 part of imm26) replicate across all matching buckets.
for mn in a.Mnemonic {
for f in a.ENCODING_TABLE[mn] {
op0_static := u8((f.bits >> 25) & 0xF)
op0_mask := u8((f.mask >> 25) & 0xF)
// Enumerate every 4-bit bucket B such that (B & op0_mask) == op0_static.
for b: u8 = 0; b < 16; b += 1 {
if (b & op0_mask) != op0_static { continue }
append(&all, Entry{
mnemonic = mn,
ops = f.ops,
enc = f.enc,
bits = f.bits,
mask = f.mask,
feature = f.feature,
flags = f.flags,
op0 = b,
})
}
}
}
slice.sort_by(all[:], proc(a, b: Entry) -> bool {
if a.op0 != b.op0 { return a.op0 < b.op0 }
ac := bits.count_ones(a.mask)
bc := bits.count_ones(b.mask)
if ac != bc { return ac > bc }
return u16(a.mnemonic) < u16(b.mnemonic)
})
op0_idx: [16]Range
for e, i in all {
if op0_idx[e.op0].count == 0 {
op0_idx[e.op0].start = u16(i)
}
op0_idx[e.op0].count += 1
}
sb: strings.Builder
strings.builder_init(&sb)
defer strings.builder_destroy(&sb)
emit_header(&sb)
emit_entries(&sb, all[:])
emit_range_table(&sb, "DECODE_INDEX_OP0", op0_idx[:])
err := os.write_entire_file("decoding_tables.odin", transmute([]u8)strings.to_string(sb))
if err != nil {
fmt.eprintfln("FAILED to write decoding_tables.odin: %v", err)
os.exit(1)
}
max_bucket: u16
for r in op0_idx { if r.count > max_bucket { max_bucket = r.count } }
fmt.printfln("OK -- %d entries across 16 op0 buckets; max bucket = %d",
len(all), max_bucket)
}
emit_header :: proc(sb: ^strings.Builder) {
strings.write_string(sb, `package rexcode_arm64
// =============================================================================
// GENERATED FILE - DO NOT EDIT
// =============================================================================
//
// Generated by tools/gen_decode_tables.odin from ENCODING_TABLE.
// Regenerate with: cd arm64 && odin run tools/gen_decode_tables.odin -file
//
Decode_Entry :: struct #packed {
mnemonic: Mnemonic,
ops: [4]Operand_Type,
enc: [4]Operand_Encoding,
bits: u32,
mask: u32,
feature: Feature,
flags: Encoding_Flags,
}
#assert(size_of(Decode_Entry) == 20)
Decode_Index :: struct #packed {
start: u16,
count: u16,
}
#assert(size_of(Decode_Index) == 4)
`)
}
emit_entries :: proc(sb: ^strings.Builder, entries: []Entry) {
fmt.sbprintfln(sb, "")
fmt.sbprintfln(sb, "@(rodata)")
fmt.sbprintfln(sb, "DECODE_ENTRIES := [%d]Decode_Entry{{", len(entries))
for e in entries {
flags_str := encode_flags_literal(e.flags)
fmt.sbprintfln(sb,
"\t{{.%v, {{.%v, .%v, .%v, .%v}}, {{.%v, .%v, .%v, .%v}}, 0x%08X, 0x%08X, .%v, {{%s}}}},",
e.mnemonic,
e.ops[0], e.ops[1], e.ops[2], e.ops[3],
e.enc[0], e.enc[1], e.enc[2], e.enc[3],
e.bits, e.mask, e.feature, flags_str)
}
strings.write_string(sb, "}\n\n")
}
encode_flags_literal :: proc(f: a.Encoding_Flags) -> string {
sb: strings.Builder
strings.builder_init(&sb)
first := true
write := proc(sb: ^strings.Builder, first: ^bool, s: string) {
if !first^ { strings.write_string(sb, ", ") }
strings.write_string(sb, s)
first^ = false
}
if f.branch { write(&sb, &first, "branch=true") }
if f.cond_branch { write(&sb, &first, "cond_branch=true") }
if f.writes_pc { write(&sb, &first, "writes_pc=true") }
if f.sets_flags { write(&sb, &first, "sets_flags=true") }
if f.is_64 { write(&sb, &first, "is_64=true") }
return strings.to_string(sb)
}
emit_range_table :: proc(sb: ^strings.Builder, name: string, ranges: []Range) {
fmt.sbprintfln(sb, "@(rodata)")
fmt.sbprintfln(sb, "%s := [%d]Decode_Index{{", name, len(ranges))
for r, i in ranges {
if r.count != 0 {
fmt.sbprintfln(sb, "\t0x%02X = {{%d, %d}},", i, r.start, r.count)
}
}
strings.write_string(sb, "}\n\n")
}

View File

@@ -1,4 +1,6 @@
#!/bin/bash
# rexcode · Brendan Punsky (dotbmp@github), original author
# Per-line llvm-mc disassembly wrapper.
#
# llvm-mc reads the entire stdin as a stream and decodes greedily, so a

View File

@@ -1,3 +1,5 @@
// rexcode · Brendan Punsky (dotbmp@github), original author
package main
// =============================================================================

434
core/rexcode/build.lua Executable file
View File

@@ -0,0 +1,434 @@
#!/usr/bin/env luajit
-- rexcode · Brendan Punsky (dotbmp@github), original author
--[[============================================================================
rexcode build driver
Drives the pre-build metaprograms (table generation), validations, and tests
for every `core:rexcode` ISA, with cross-platform (Linux / macOS / Windows)
gating and a clear report.
USAGE
luajit build.lua # no flags -> this help screen
luajit build.lua all # do everything (gen + check + test), all ISAs
luajit build.lua --gen --isa x86 # only (re)generate x86's tables
luajit build.lua --check --test # validate + test all ISAs (using committed blobs)
luajit build.lua --verify --isa mips # external-tool verification where available
luajit build.lua --list # ISA x task availability for THIS platform
TASKS
--gen run the two metaprograms: ENCODING_TABLE -> generated Odin -> tables/*.bin
--check `odin check` (compiles against the #loaded blobs) + structural invariants
--test run each ISA's test suite
--verify round-trip against an external assembler/disassembler (llvm-mc, da65, ...)
--idempotent re-run --gen and confirm the generated files + blobs are byte-stable
all shorthand for `--gen --check --test`
OPTIONS
--isa <list> comma/space-separated ISAs (default: all). e.g. --isa x86,arm64
--odin <path> compiler to use (default: the in-repo ./odin — it has fixes not in
a released/system odin, so prefer it)
--root <path> rexcode root (default: auto-detected from this script's location)
--no-color disable ANSI color
-h, --help this screen
--list availability matrix for the current platform
PLATFORM NOTES
* The in-repo compiler must be built first (./build_odin.sh, or build.bat on Windows).
* `--test` for x86 JIT-executes x86-64 machine code, so it only runs on an x86-64
host; it is skipped (with a message) elsewhere. All other ISAs' tests are portable.
* `--verify` needs the matching external tool in PATH; the retro ISAs use shell
scripts and are skipped on Windows. Missing tools are skipped, never fatal.
============================================================================]]--
-- ----------------------------------------------------------------------------
-- platform
-- ----------------------------------------------------------------------------
local OS = jit.os -- "Linux" | "OSX" | "Windows" | "BSD" | ...
local ARCH = jit.arch -- "x64" | "x86" | "arm64" | "arm" | ...
local WIN = (OS == "Windows")
local HOST_X64 = (ARCH == "x64")
local EXE = WIN and ".exe" or ""
local use_color = not WIN or os.getenv("WT_SESSION") ~= nil or os.getenv("ANSICON") ~= nil
local function paint(code, s) return use_color and ("\27["..code.."m"..s.."\27[0m") or s end
local function bold(s) return paint("1", s) end
local function green(s) return paint("32", s) end
local function red(s) return paint("31", s) end
local function yellow(s)return paint("33", s) end
local function dim(s) return paint("2", s) end
-- ----------------------------------------------------------------------------
-- small utilities
-- ----------------------------------------------------------------------------
local function q(s) return '"' .. s .. '"' end
-- Run a command; capture combined output; success via a shell-portable sentinel
-- (works in both POSIX sh and Windows cmd, regardless of popen close() quirks).
local function run(cmd)
local p = io.popen(cmd .. " 2>&1 && echo __RX_OK__ || echo __RX_FAIL__")
local out = p:read("*a") or ""
p:close()
local ok = out:match("__RX_OK__%s*$") ~= nil
out = out:gsub("__RX_OK__%s*$", ""):gsub("__RX_FAIL__%s*$", "")
return ok, out
end
local function file_exists(path)
local f = io.open(path, "rb"); if f then f:close(); return true end; return false
end
local function read_file(path)
local f = io.open(path, "rb"); if not f then return nil end
local d = f:read("*a"); f:close(); return d
end
local function cwd()
local p = io.popen(WIN and "cd" or "pwd")
local d = p:read("*l"); p:close()
return (d or "."):gsub("\\", "/")
end
-- Is a tool present in PATH?
local function have_tool(name)
local probe = WIN and ("where " .. q(name)) or ("command -v " .. q(name))
local ok = run(probe)
return ok
end
-- ----------------------------------------------------------------------------
-- locate the rexcode root and the in-repo compiler
-- ----------------------------------------------------------------------------
local function script_dir()
local s = (arg and arg[0] or ""):gsub("\\", "/")
return s:match("^(.*)/[^/]*$") or "."
end
local function find_root(override)
local function ok_root(d) return d and file_exists(d .. "/isa/labels.odin") end
if override then return override end
local sd, here = script_dir(), cwd()
local cands = { sd, here, here .. "/" .. sd, here .. "/core/rexcode", "core/rexcode", "." }
for _, d in ipairs(cands) do if ok_root(d) then return (d:gsub("/%.$","")) end end
return sd -- best effort
end
-- ----------------------------------------------------------------------------
-- ISA catalog
-- ----------------------------------------------------------------------------
-- test_x64: test suite JIT-executes target code -> needs an x86-64 host (x86 only).
-- verify: {tool=<PATH binary>, kind="odin"|"sh", harness=<file under tools/>}
local ISAS = {
{ name="x86", test_x64=true, verify={tool="llvm-mc", kind="odin", harness="verify_against_llvm.odin"} },
{ name="arm32", test_x64=false, verify={tool="llvm-mc", kind="odin", harness="verify_against_llvm.odin"} },
{ name="arm64", test_x64=false, verify={tool="llvm-mc", kind="odin", harness="verify_against_llvm.odin"} },
{ name="mips", test_x64=false, verify={tool="llvm-mc", kind="odin", harness="verify_against_llvm.odin"} },
{ name="riscv", test_x64=false, verify={tool="llvm-mc", kind="odin", harness="verify_against_llvm.odin"} },
{ name="ppc", test_x64=false, verify={tool="llvm-mc", kind="odin", harness="verify_against_llvm.odin"} },
{ name="ppc_vle", test_x64=false, verify={tool="powerpc-eabivle-as", kind="sh", harness="verify_against_vle_as.sh"} },
{ name="rsp", test_x64=false, verify={tool="armips", kind="sh", harness="verify_against_armips.sh"} },
{ name="mos6502", test_x64=false, verify={tool="xa", kind="sh", harness="verify_against_xa.sh"} },
{ name="mos65816", test_x64=false, verify={tool="ca65", kind="sh", harness="verify_against_ca65.sh"} },
}
local ISA_BY_NAME = {}; for _, a in ipairs(ISAS) do ISA_BY_NAME[a.name] = a end
-- ----------------------------------------------------------------------------
-- argument parsing
-- ----------------------------------------------------------------------------
local function parse_args(argv)
local o = { tasks={}, isas=nil, odin=nil, root=nil, help=false, list=false }
local i = 1
local function val(flag)
i = i + 1
if not argv[i] then io.stderr:write("error: "..flag.." needs a value\n"); os.exit(2) end
return argv[i]
end
while argv[i] do
local a = argv[i]
if a == "-h" or a == "--help" then o.help = true
elseif a == "--list" then o.list = true
elseif a == "all" or a == "--all" then o.tasks.gen=true; o.tasks.check=true; o.tasks.test=true
elseif a == "--gen" or a == "--generate" then o.tasks.gen = true
elseif a == "--check" or a == "--validate" then o.tasks.check = true
elseif a == "--test" then o.tasks.test = true
elseif a == "--verify" then o.tasks.verify = true
elseif a == "--idempotent" or a == "--idem" then o.tasks.idempotent = true
elseif a == "--no-color" then use_color = false
elseif a == "--isa" then o.isas = val("--isa")
elseif a == "--odin" then o.odin = val("--odin")
elseif a == "--root" then o.root = val("--root")
elseif a:match("^%-%-isa=") then o.isas = a:sub(7)
elseif a:match("^%-%-odin=") then o.odin = a:sub(8)
elseif a:match("^%-%-root=") then o.root = a:sub(8)
else io.stderr:write("error: unknown argument '"..a.."' (try --help)\n"); os.exit(2) end
i = i + 1
end
return o
end
local function selected_isas(spec)
if not spec then local t={}; for _,a in ipairs(ISAS) do t[#t+1]=a end; return t end
local t = {}
for name in spec:gmatch("[%w_]+") do
local a = ISA_BY_NAME[name]
if not a then io.stderr:write("error: unknown ISA '"..name.."'\n"); os.exit(2) end
t[#t+1] = a
end
return t
end
-- ----------------------------------------------------------------------------
-- availability for the current platform
-- ----------------------------------------------------------------------------
-- returns ok(bool), reason(string|nil)
local function avail(isa, task, ctx)
if task == "test" and isa.test_x64 and not HOST_X64 then
return false, "needs x86-64 host (this is "..ARCH..")"
end
if task == "verify" then
local v = isa.verify
if v.kind == "sh" and WIN then return false, v.harness.." (shell script) unsupported on Windows" end
if not ctx.tools[v.tool] then return false, v.tool.." not in PATH" end
end
return true, nil
end
-- ----------------------------------------------------------------------------
-- tasks
-- ----------------------------------------------------------------------------
local ODIN, ROOT, OUT -- set in main
local function pkg(isa, sub) return ROOT .. "/" .. isa.name .. (sub and ("/"..sub) or "") end
local function odin_run(target) return q(ODIN).." run "..q(target).." -out:"..q(OUT) end
local function odin_check(target)return q(ODIN).." check "..q(target).." -no-entry-point" end
-- structural invariants for the migrated layout
local function structural(isa)
local p, bad = pkg(isa), {}
local function must(rel) if not file_exists(p.."/"..rel) then bad[#bad+1]="missing "..rel end end
local function absent(rel) if file_exists(p.."/"..rel) then bad[#bad+1]="stray "..rel end end
must("tables.odin"); must("tablegen/encoding_table.odin"); must("tablegen/gen.odin")
must("tablegen/generated/encode_tables.odin"); must("tablegen/generated/decode_tables.odin")
must("tablegen/generated/writer.odin")
absent("encoding_table.odin"); absent("decoding_tables.odin"); absent("tools/gen_decode_tables.odin")
if #bad == 0 then return true end
return false, table.concat(bad, "; ")
end
-- blob paths an ISA's tables.odin #loads (parsed from the loader)
local function blob_paths(isa)
local txt = read_file(pkg(isa).."/tables.odin") or ""
local t = {}
for name in txt:gmatch('#load%("(tables/[%w%._%-]+)"') do t[#t+1] = pkg(isa).."/"..name end
return t
end
local function gen_files(isa)
local t = { pkg(isa).."/tables.odin",
pkg(isa).."/tablegen/generated/encode_tables.odin",
pkg(isa).."/tablegen/generated/decode_tables.odin" }
for _, b in ipairs(blob_paths(isa)) do t[#t+1] = b end
return t
end
local function do_gen(isa)
local okA, outA = run(odin_run(pkg(isa, "tablegen")))
if not okA then return false, "Stage A failed:\n"..outA end
local okB, outB = run(odin_run(pkg(isa, "tablegen/generated")))
if not okB then return false, "Stage B failed:\n"..outB end
-- counts line from Stage A (e.g. "x86 tablegen: 2355 encode forms, ...")
return true, (outA:match("tablegen:%s*(.-)\n") or ""):gsub("%s+$","")
end
local function do_check(isa)
local s_ok, s_why = structural(isa)
if not s_ok then return false, "structure: "..s_why end
local c_ok, c_out = run(odin_check(pkg(isa)))
if not c_ok then return false, "odin check failed:\n"..(c_out:match("(.-Error:.-)\n") or c_out) end
return true, "structure + compile"
end
local function do_test(isa)
local ok, out = run(odin_run(pkg(isa, "tests")))
local fails = out:match("([1-9]%d* failed)")
if not ok or fails then return false, (fails or "test run failed").."\n"..out:sub(-400) end
local cases = out:match("(%d+ cases? validated)")
if not cases then
local n = 0; for p in out:gmatch("(%d+) passed") do n = n + tonumber(p) end
if n > 0 then cases = n .. " passed" end
end
return true, cases or "passed"
end
local function do_verify(isa)
local v = isa.verify
local cmd
if v.kind == "odin" then
cmd = q(ODIN).." run "..q(pkg(isa, "tools/"..v.harness)).." -file -out:"..q(OUT)
else
cmd = "sh "..q(pkg(isa, "tools/"..v.harness))
end
local ok, out = run(cmd)
if not ok then return false, "verify failed:\n"..out:sub(-400) end
return true, "matched "..v.tool
end
local function do_idempotent(isa)
local files = gen_files(isa)
local before = {}
for _, f in ipairs(files) do before[f] = read_file(f) end
local ok, why = do_gen(isa)
if not ok then return false, "re-gen failed: "..why end
local changed = {}
for _, f in ipairs(files) do
if read_file(f) ~= before[f] then changed[#changed+1] = f:match("[^/]+$") end
end
if #changed == 0 then return true, "byte-stable ("..#files.." artifacts)" end
return false, "changed on re-gen: "..table.concat(changed, ", ")
end
local TASK_FN = { gen=do_gen, check=do_check, test=do_test, verify=do_verify, idempotent=do_idempotent }
local TASK_ORDER = { "gen", "check", "test", "verify", "idempotent" }
local TASK_LABEL = { gen="generate", check="validate", test="test", verify="verify", idempotent="idempotent" }
-- ----------------------------------------------------------------------------
-- help / list
-- ----------------------------------------------------------------------------
local function platform_line()
return ("%s / %s (luajit %s)"):format(OS, ARCH, (jit.version:match("LuaJIT (%S+)") or "?"))
end
local function print_help(ctx)
print(bold("rexcode build driver") .. " — generate tables, validate, and test the core:rexcode ISAs")
print()
print(" Platform : " .. platform_line())
local cstat = ctx.odin_ok and green("[found] "..ODIN) or red("[NOT BUILT] expected "..ODIN.." — run ./build_odin.sh")
print(" Compiler : " .. cstat)
print()
print(bold("USAGE"))
print(" luajit build.lua " .. dim("# no flags -> this help"))
print(" luajit build.lua all " .. dim("# everything (gen + check + test), all ISAs"))
print(" luajit build.lua --gen --isa x86 " .. dim("# only regenerate x86's tables"))
print(" luajit build.lua --check --test " .. dim("# validate + test (using committed blobs)"))
print(" luajit build.lua --list " .. dim("# availability matrix for this platform"))
print()
print(bold("TASKS") .. dim(" (availability on " .. OS .. "/" .. ARCH .. ")"))
print(" --gen metaprograms: ENCODING_TABLE -> generated Odin -> tables/*.bin " .. green("all ISAs"))
print(" --check odin check (compiles vs #loaded blobs) + structural invariants " .. green("all ISAs"))
local tnote = HOST_X64 and green("all ISAs") or yellow("x86 skipped (needs x86-64 host)")
print(" --test run each ISA's test suite " .. tnote)
print(" --verify round-trip vs external assembler/disassembler " .. yellow("per-tool (see --list)"))
print(" --idempotent re-run --gen and confirm byte-stable output " .. green("all ISAs"))
print(" all = --gen --check --test")
print()
print(bold("OPTIONS"))
print(" --isa <list> comma/space ISAs (default: all): " .. dim("x86 arm32 arm64 mips riscv ppc ppc_vle rsp mos6502 mos65816"))
print(" --odin <path> compiler (default: in-repo ./odin) --root <path> rexcode root")
print(" --no-color plain output -h, --help this screen --list availability matrix")
print()
print(dim("The in-repo ./odin is required (it has fixes not in released/system odin)."))
end
local function print_list(ctx)
print(bold("ISA availability on ") .. bold(OS .. "/" .. ARCH))
print(dim((" %-10s %-7s %-7s %-18s %s"):format("ISA","gen","check","test","verify")))
for _, isa in ipairs(ISAS) do
local t_ok, t_why = avail(isa, "test", ctx)
local v_ok, v_why = avail(isa, "verify", ctx)
local tcol = t_ok and green("yes") or yellow("skip")
local vcol = v_ok and green("yes ("..isa.verify.tool..")") or yellow("skip: "..(v_why or "?"))
print((" %-10s %-7s %-7s %-18s %s"):format(
isa.name, green("yes"), green("yes"), tcol .. (t_ok and "" or " "..dim(t_why or "")), vcol))
end
end
-- ----------------------------------------------------------------------------
-- main
-- ----------------------------------------------------------------------------
local function main()
local o = parse_args(arg)
ROOT = (find_root(o.root)):gsub("/+$","")
ODIN = o.odin or (ROOT .. "/../.." .. "/odin" .. EXE)
-- normalize ../.. once for tidy messages
ODIN = ODIN:gsub("/core/rexcode/%.%./%.%./", "/")
local ctx = { odin_ok = file_exists(ODIN) or o.odin ~= nil, tools = {} }
-- probe each distinct verify tool once
local probed = {}
for _, isa in ipairs(ISAS) do
local tname = isa.verify.tool
if probed[tname] == nil then probed[tname] = have_tool(tname) end
ctx.tools[tname] = probed[tname]
end
local temp = (WIN and (os.getenv("TEMP") or os.getenv("TMP")) or (os.getenv("TMPDIR") or "/tmp")) or "."
OUT = (temp:gsub("\\","/"):gsub("/+$","")) .. "/rexcode_build" .. EXE
if o.help then print_help(ctx); return 0 end
if o.list then print_list(ctx); return 0 end
local tasks = {}
for _, t in ipairs(TASK_ORDER) do if o.tasks[t] then tasks[#tasks+1] = t end end
if #tasks == 0 then print_help(ctx); return 0 end
if not ctx.odin_ok then
print(red("error: the in-repo compiler was not found at:\n ") .. ODIN)
print("Build it first: " .. (WIN and "build.bat" or "./build_odin.sh") ..
" (or pass --odin <path>).")
return 2
end
local isas = selected_isas(o.isas)
print(bold("rexcode") .. " " .. dim(platform_line()) .. " odin=" .. dim(ODIN))
print(dim(("tasks: %s isas: %d"):format(table.concat(tasks, " "), #isas)))
local t0 = os.time()
local results, nfail, nskip = {}, 0, 0
for _, task in ipairs(tasks) do
print()
print(bold("== " .. TASK_LABEL[task]:upper() .. " =="))
for _, isa in ipairs(isas) do
results[isa.name] = results[isa.name] or {}
local ok_av, why = avail(isa, task, ctx)
io.write((" %-10s %-11s "):format(isa.name, task))
io.flush()
if not ok_av then
results[isa.name][task] = "skip"; nskip = nskip + 1
print(yellow("skip") .. " " .. dim(why))
else
local ok, detail = TASK_FN[task](isa)
results[isa.name][task] = ok and "ok" or "fail"
if ok then print(green("ok") .. " " .. dim(detail or ""))
else nfail = nfail + 1; print(red("FAIL") .. " " .. (detail or ""):gsub("\n", "\n ")) end
end
end
end
-- summary matrix
print()
print(bold("== REPORT ==") .. dim((" %ds"):format(os.time() - t0)))
io.write(dim((" %-10s"):format("ISA")))
for _, t in ipairs(tasks) do io.write(dim(("%-12s"):format(t))) end
print()
for _, isa in ipairs(isas) do
io.write((" %-10s"):format(isa.name))
for _, t in ipairs(tasks) do
local s = results[isa.name][t] or "--"
local c = (s == "ok" and green("ok")) or (s == "fail" and red("FAIL")) or (s == "skip" and yellow("skip")) or dim("--")
io.write(c .. string.rep(" ", math.max(2, 12 - #s)))
end
print()
end
print()
if nfail == 0 then
print(green(bold("PASS")) .. (" (%d skipped)"):format(nskip))
return 0
else
print(red(bold("FAIL")) .. (" %d failed, %d skipped"):format(nfail, nskip))
return 1
end
end
os.exit(main())

View File

@@ -1,8 +1,10 @@
// rexcode · Brendan Punsky (dotbmp@github), original author
/*
# rexcode
High-performance multi-architecture instruction encoder/decoder/printer
library written in Odin. Developed by dotbmp/Br.
library written in Odin. Original author: Brendan Punsky (dotbmp@github).
## Architectures
@@ -28,8 +30,8 @@ Every package follows the same API contract (see `docs/cross_arch_design.md`).
- **Decoder**: disassembles machine code back to structured instructions.
- **Printer**: emits assembly text output with optional syntax-highlighting
tokens.
- **Table-driven**: O(1) opcode lookup via precomputed encoding/decoding
tables.
- **Table-driven**: O(1) opcode lookup via precomputed encode/decode tables,
serialized to committed binary blobs and `#load`ed into `@(rodata)`.
- **Zero allocations** on the hot path: caller provides all buffers.
The `isa/` package owns the parts that are the same on every ISA labels,
@@ -38,6 +40,20 @@ formatting helpers. Each architecture package owns its registers, memory
model, operand types, mnemonics, encoding tables, and the actual
`encode_one`/`decode_one` bytes.
## Encoding tables
Each arch's `ENCODING_TABLE` (the hand-written single source of truth) lives in
`<arch>/tablegen/`, not in the library. A two-stage metaprogram flattens it and
emits committed binary blobs that the library `#load`s into `@(rodata)` at
compile time no table is built during a normal library build:
```sh
odin run <arch>/tablegen # ENCODING_TABLE -> generated Odin + <arch>/tables.odin
odin run <arch>/tablegen/generated # -> <arch>/tables/<arch>.*.bin
```
Regenerate after editing `ENCODING_TABLE`. See `docs/table_migration.md`.
## Performance (x86)
With `-o:speed -microarch:native -no-bounds-check`:
@@ -133,6 +149,31 @@ for name, id in lm.names { id_to_name[id] = name }
x86.print(decoded_insts[:], decoded_info[:], lm.labels[:], label_names = &id_to_name)
```
## Driver script (`build.lua`)
`build.lua` (LuaJIT) drives the pre-build metaprograms, validations, and tests
across every ISA, with cross-platform gating (Linux / macOS / Windows) and a
clear report. With no flags it prints help, including what's available on the
current platform.
```sh
luajit build.lua # help + platform availability
luajit build.lua all # everything: generate -> validate -> test, all ISAs
luajit build.lua --gen --isa x86 # only regenerate one ISA's tables
luajit build.lua --check --test # validate + test the committed tables
luajit build.lua --verify # external-tool round-trip where the tool is installed
luajit build.lua --list # ISA x task availability matrix for this platform
```
Tasks: `--gen` (table metaprograms), `--check` (compile + structural invariants),
`--test` (run the suites), `--verify` (round-trip vs `llvm-mc`/`da65`/`ca65`/
`armips`/), `--idempotent` (re-gen and confirm byte-stable). Scope with
`--isa <list>`. It uses the in-repo `./odin` build that first.
> Gating: x86's `--test` JIT-executes x86-64 code, so it runs only on an x86-64
> host; `--verify` needs the matching tool in PATH (retro ISAs use shell scripts,
> skipped on Windows). Anything unavailable is skipped with a note, never fatal.
## Running Tests
Each package has its own test suite:
@@ -187,12 +228,13 @@ Per-package layout (canonical, enforced by the cross-arch contract):
operands.odin # Operand, Memory, Operand_Kind, op_* constructors
instructions.odin # Instruction, inst_* builders
encoding_types.odin # Encoding, Encoding_Flags, isa re-exports
encoding_table.odin # ENCODING_TABLE: [Mnemonic][]Encoding
decoding_tables.odin # generated dispatch tables
tables.odin # generated: #load()s the binary tables into @(rodata) + accessors
tables/ # committed binary blobs (<arch>.*.bin) the library #loads
mnemonics.odin # Mnemonic enum (u16, INVALID=0)
reloc.odin # Relocation_Type + Relocation
tablegen/ # ENCODING_TABLE (source of truth) + gen.odin metaprogram
tests/ # smoke, pipeline_smoke, sweep
tools/ # gen_decode_tables, dump_verify_input, verify_against_*
tools/ # dump_verify_input, verify_against_*
```
## Cross-architecture API design

View File

@@ -1,46 +1,40 @@
# rexcode — Cross-Architecture API Design
<!-- rexcode · Brendan Punsky (dotbmp@github), original author -->
> How to grow rexcode from an x86-only encoder/decoder into a multi-target
> library (x86, RISC-V, ARM64, MIPS, …) **without** flattening every
> architecture to a lowest common denominator and **without** adding
# rexcode — Cross-Architecture Design
> Why the rexcode family (`x86`, `arm32`, `arm64`, `mips`, `riscv`, `ppc`,
> `ppc_vle`, `rsp`, `mos6502`, `mos65816`) shares one shape **without**
> flattening every ISA to a lowest common denominator and **without** adding
> runtime overhead to the single-target hot path.
>
> Companion to [x86_api.md](x86_api.md). Written ahead of the RISC-V
> subpackage.
---
## 0. The guiding principle
## The guiding principle
> **Share the bookkeeping, specialize the bytes.**
An encoder/decoder is two things stitched together:
1. **Orchestration & bookkeeping** — labels, relocations, the two-pass
encode/decode loops, error/result reporting, the print framework,
buffer management, the table-gen tooling pattern. This is *the same
problem on every ISA* and should be written once.
encode/decode loops, error/result reporting, the print framework, and the
table-gen tooling pattern. This is *the same problem on every ISA*.
2. **The instruction model & the bytes** — what a register/memory/operand
*is*, what the encoding tables look like, and the actual
bit/byte-twiddling of `encode_one`/`decode_one`. This is *irreducibly
per-architecture* and must stay native and zero-cost.
*is*, what the encoding tables look like, and the actual bit/byte-twiddling
of `encode_one`/`decode_one`. This is *irreducibly per-architecture* and
must stay native and zero-cost.
Every decision below follows from drawing the line in exactly that place.
We do **not** try to invent one `Instruction` type that fits all ISAs —
that path forces x86's `segment`/SIB and ARM's writeback and RISC-V's
split immediates into one bloated struct, and it is precisely the
"compromise performance/effectiveness" outcome to avoid. Instead, each
arch owns its concrete types, and uniformity comes from a **naming
contract** (§6) plus a small **shared core** (§4) plus **opt-in**
generic glue (§5, §7).
We do **not** invent one `Instruction` type that fits all ISAs — that path
forces x86's `segment`/SIB, ARM's writeback, and RISC-V's split immediates into
one bloated struct. Instead each arch owns its concrete types, and uniformity
comes from a **naming contract** (§4) plus a small **shared core** (§3).
---
## 1. The universal shape
Strip away the x86 specifics and every target needs the same nine things:
Strip away the specifics and every target needs the same nine things:
| # | Concept | Example in x86 |
| # | Concept | x86 example |
|---|---|---|
| 1 | A **register** = (class, hw number, size) | `Register` distinct u16 |
| 2 | **Operands** tagged reg / mem / imm / relative | `Operand` + `Operand_Kind` |
@@ -52,282 +46,123 @@ Strip away the x86 specifics and every target needs the same nine things:
| 8 | `decode(bytes) -> []Inst (+info +labels +errors)` | `decode()` |
| 9 | `print([]Inst) -> text (+tokens)` | `print()`/`tprint()`/… |
Plus two cross-cutting concerns: **errors/result** reporting and a
**table-driven core** fed by **codegen tooling**.
The *shape* of items 59 (their signatures and the types they pass around)
is architecture-independent. That is the surface we standardize.
The *shape* of items 59 (their signatures and the types they pass around) is
architecture-independent. That is the surface the naming contract standardizes.
---
## 2. Where architectures actually diverge
This is the heart of the analysis. Ranked from "diverges hardest" to
"barely diverges."
Ranked from "diverges hardest" to "barely diverges":
### 2.1 Encoding mechanics — **maximal divergence**
| ISA | Width | Mechanism |
|---|---|---|
| x86 | 115 B, variable | legacy prefixes → REX/VEX/EVEX → escape → opcode → ModRM → SIB → disp → imm |
| RISC-V | 4 B (2 B for "C") | pack fixed bitfields; ~6 formats (R/I/S/B/U/J) |
| ARM64 | 4 B fixed | pack per-class bitfields; many classes; bitmask-imm encoder |
| MIPS | 4 B fixed | 3 formats (R/I/J), very regular |
`encode()`'s ~500-line body and the whole `Encoding`/`Encoding_Flags`
schema (esc/prefix/vex_*) are **x86-only**. RISC-V's `encode_one` is a
dozen lines of shifts. **Conclusion: the `encode_one`/`decode_one` core
and the `Encoding` struct do not generalize — but the loop that drives
them does (§7).**
### 2.2 Memory addressing — **high divergence**
| ISA | Addressing modes |
|---|---|
| x86 | `[base + index*scale + disp32]`, RIP-relative, segment override, addr-size override |
| RISC-V | `disp12(base)` only — no index, no scale |
| MIPS | `imm16(base)` only |
| ARM64 | `[base]`, `[base,#imm]`, `[base,Xm{,LSL#n}]`, `[base,Wm,SXTW]`, pre/post-index `[base,#imm]!` / `[base],#imm`, PC-rel literal |
The x86 `Memory` bit_field (with `segment`, `addr_size_override`,
index+scale) is deeply x86-flavored. RISC-V's memory is `{base, i32 disp}`.
ARM adds **writeback** (a mode x86 cannot express) and extend/shift on the
index. **Conclusion: `Memory` is per-arch.** What generalizes is only the
*role*: a `MEMORY`-kind operand carrying an arch-defined payload.
### 2.3 Immediates & operand size — **moderate divergence**
- The *value* (an `i64`) generalizes perfectly.
- The *encoding* does not: RISC-V scatters immediate bits across fields
(B-type, J-type) and shifts them; ARM has bitmask-immediate and shifted
forms. All of that lives inside `encode_one`; the `Operand` just holds
the clean value.
- **Size association differs:** x86 carries an explicit `size: u8` and
uses it to select an encoding; RISC-V/ARM bake width into the mnemonic
(`LW` vs `LD`, `W0` vs `X0`). Keep `size` in the shared operand shape as
a *carrier*; let each arch decide how much it matters.
### 2.4 Relocations — **moderate divergence (structurally aligned)**
The `Relocation` *struct* (offset, symbol/label, addend, type, size)
mirrors ELF `rela` and is universal. The *type enum* is per-arch and much
larger on RISC-V (paired `PCREL_HI20`/`PCREL_LO12`, `CALL`, `BRANCH`,
`JAL`, `HI20`, `LO12_I/S`, …) because PC-relative addressing needs
instruction *pairs* (AUIPC+ADDI). **Conclusion: share the struct shape,
make the type enum a per-arch parameter.**
### 2.5 Registers — **low/structural divergence**
The `(class, hw_number)`-packed `distinct u16` scheme generalizes well.
What differs:
- x86: REX/EVEX extension bits, AH↔SPL aliasing, RIP pseudo-reg.
- RISC-V: clean 5-bit fields, `x0`=hardwired zero, ABI names
(`zero/ra/sp/gp/tp/t0../s0../a0..`), separate `f`/`v` files.
- ARM64: reg #31 means **SP or XZR depending on instruction** (a
decode/print-time disambiguation x86 never needs); `w`/`x` and
`b/h/s/d/q` views.
**Conclusion: share the *layout convention* + `reg_hw`/`reg_class`
accessors; per-arch owns classes, enums, names, and extension semantics.**
### 2.6 Mnemonics — **content differs, shape identical**
Per-arch `enum u16`, `INVALID=0`. Nothing to share but the convention.
### 2.7 Labels — **no divergence**
`labels.odin` is pure bookkeeping. The array-index model
(`Label_Definition`, `label`, `label_forward`, `label_set_at`,
`Label_Map`, `label_named`, `label_reserve`, `label_set`) lives in
`isa/labels.odin` and is parametric over the Instruction type. **Fully
shared.** Each arch's `encode()` rewrites label_defs from instruction
indices to byte offsets between pass 1 and pass 2.
### 2.8 Errors / Result — **low divergence**
`Result` is universal. `Error` is universal in shape. `Error_Code` splits
into a **shared core** (`NONE, BUFFER_OVERFLOW, INVALID_MNEMONIC,
NO_MATCHING_ENCODING, BUFFER_TOO_SHORT, INVALID_OPCODE, LABEL_OUT_OF_RANGE,
…`) and **arch-specific** extras (`INVALID_MODRM/SIB/VEX/EVEX,
TOO_MANY_PREFIXES` on x86; RISC-V would add `MISALIGNED_IMMEDIATE`,
`INVALID_ROUNDING_MODE`, …).
### 2.9 Printer — **framework universal, formatting per-arch**
Shareable: `Token`, `Token_Kind` (the kinds are generic), `Print_Options`,
the builder/number-formatting helpers, and the whole family of output
sinks (`sbprint/print/aprint/tprint/bprint/fprint/wprint` + `ln`). Per-arch:
`register_name`, `print_memory` (syntax differs wildly),
`mnemonic_to_string`, and the size-suffix convention (x86's `.b/.w/.d` is
x86-only; RISC-V puts width in the mnemonic).
- **Encoding mechanics — maximal.** x86 is 115 B variable (prefixes → REX/VEX/
EVEX → escape → opcode → ModRM → SIB → disp → imm); RISC-V/ARM64/MIPS/PPC are
fixed 4 B bitfield packs; the 6502/65816 are 1N B opcode + operand bytes.
`encode_one`/`decode_one` and the `Encoding` schema do **not** generalize.
- **Memory addressing — high.** x86 `[base+index*scale+disp32]` + segment +
addr-size; RISC-V `disp12(base)`; MIPS `imm16(base)`; ARM adds writeback and
extend/shift. `Memory` is per-arch; only the *role* (a `MEMORY`-kind operand
carrying an arch-defined payload) generalizes.
- **Immediates / operand size — moderate.** The *value* (`i64`) generalizes; the
*encoding* (split B/J immediates, bitmask-imm) lives inside `encode_one`. x86
carries an explicit `size`; RISC-V/ARM bake width into the mnemonic.
- **Relocations — moderate, structurally aligned.** The `Relocation` struct
(offset, label, addend, type, size) mirrors ELF `rela` and is universal in
*shape*; the **type enum** is per-arch (larger on RISC-V/PPC for paired
PC-relative forms).
- **Registers — low/structural.** The `(class, hw_number)`-packed `distinct u16`
scheme + `reg_hw`/`reg_class` accessors generalize; classes, enums, names, and
extension semantics (REX/EVEX, ARM's SP/XZR #31) are per-arch.
- **Mnemonics — content differs, shape identical.** Per-arch `enum u16`,
`INVALID=0`.
- **Labels — no divergence.** Pure bookkeeping; lives in `isa/`.
- **Errors / Result — low.** `Result`/`Error` shapes universal; `Error_Code`
has a shared core plus arch-specific extras.
- **Printer — framework universal, formatting per-arch.** Tokens, options, and
the output sinks are shared; `register_name`/`print_memory`/mnemonic
formatting are per-arch.
### Divergence summary
| Component | Verdict | What's shared | What's per-arch |
| Component | Verdict | Shared | Per-arch |
|---|---|---|---|
| Labels | ✅ shared | everything | — |
| Result / Error struct | ✅ shared | struct shapes | error-code extras |
| Relocation struct | ✅ shared | struct shape | type enum |
| Printer framework | ◑ split | tokens, options, sinks, num-fmt | reg/mem/mnemonic formatting |
| Register scheme | ◑ split | layout + `reg_hw`/`reg_class` | classes, enums, names, ext bits |
| Operand model | ◑ split | kind tag + union discipline + `size` carrier | `Memory`, `flags` payloads |
| Encode/decode **driver** | ◑ shared via generics | two-pass loops, label/reloc resolution | the per-instruction hook |
| Relocation | ◑ split | struct shape (convention) | type enum (per-arch file) |
| Register scheme | ◑ split | layout + `reg_hw`/`reg_class` convention | classes, enums, names, ext bits |
| Operand model | ◑ split | kind tag + `size` carrier convention | `Memory`, `flags` payloads |
| `Instruction` | ✗ per-arch | shape convention only | concrete struct |
| `Mnemonic` | ✗ per-arch | convention (u16, INVALID=0) | the enum |
| `Encoding` + tables | ✗ per-arch | codegen *pattern* | schema + data |
| `Encoding` + tables | ✗ per-arch | codegen *pattern* (§5) | schema + data |
| `encode`/`decode` driver | ✗ per-arch | — | the whole loop |
| `encode_one`/`decode_one` | ✗ per-arch | nothing | all of it |
| Memory addressing | ✗ per-arch | operand *role* | the model |
---
## 3. Why not the "obvious" unifications
## 3. The shared core (`isa/`) and why nothing else is shared
Three tempting designs that **violate** the no-compromise rule:
`isa/` depends on nothing and owns only the parts that are byte-for-byte the
same problem on every ISA:
1. **One universal `Operand`/`Memory` for all ISAs.** Forces the union of
x86 SIB+segment, ARM writeback+extend, and RISC-V's nothing into a
single struct. Bloats every operand, leaks `segment` into RISC-V, and
still can't represent ARM writeback cleanly. ✗
- `labels.odin``Label`, `Label_Definition`, `Label_Map`, resolution
(parametric over the Instruction type, so it works for any arch unchanged).
- `label_infer.odin` — branch-target → label inference used by `decode`.
- `status.odin``Result`, `Error`, the shared `Error_Code` core.
- `print.odin``Token`/`Token_Kind`, `Print_Options`, the output sinks, and
the number-formatting helpers.
2. **A runtime `interface`/vtable the encoder calls per instruction.**
Adds an indirect call to the hottest loop (x86 does ~17 M inst/s — a
per-instruction `proc` pointer is a measurable tax) and defeats
inlining. ✗ on the default path.
Each arch package **re-exports** these (e.g. `x86.Result`, `x86.Label_Map`) so a
consumer sees one coherent namespace and never imports `isa` directly unless
writing arch-generic tooling.
3. **`any`/tagged-union `Instruction` passed through a generic `encode`.**
Same monomorphization loss + runtime type checks in the hot loop. ✗
Everything else is deliberately **per-arch**, even where it looks shareable:
registers, the memory model, operands, mnemonics, the `Encoding`/table schema,
the `Relocation` type enum, and — notably — the `encode`/`decode` **driver
loops**. The drivers were left native rather than factored behind a generic hook
because they diverge too much to share cleanly (x86's ~500-line prefix/ModRM/SIB
body vs a fixed-width arch's dozen-line bitfield packer), and the hot path must
not pay for indirection.
The design instead gets uniformity from **compile-time** mechanisms
(naming contract + parametric polymorphism), and reserves runtime dispatch
for an **opt-in** facade (§5.3) that only multi-target *tools* pay for.
### Three unifications we deliberately rejected
1. **One universal `Operand`/`Memory`.** Would force x86 SIB+segment, ARM
writeback+extend, and RISC-V's nothing into one struct — bloats every operand
and still can't represent ARM writeback cleanly.
2. **A runtime `interface`/vtable called per instruction.** Adds an indirect
call to the hottest loop (x86 does ~17 M inst/s) and defeats inlining.
3. **An `any`/tagged-union `Instruction` through a generic `encode`.** Same
monomorphization loss + runtime type checks in the hot loop.
---
## 4. Proposed package layout
## 4. The naming contract
```
rexcode/
isa/ # shared, architecture-independent core
labels.odin # Label, Label_Definition, Label_Map, resolution
reloc.odin # Relocation (type field is generic/u8)
status.odin # Result, Error, shared Error_Code core
print.odin # Token, Token_Kind, Print_Options, sinks, num-fmt
register.odin # distinct-u16 layout convention + reg_hw/reg_class
pipeline.odin # parametric encode_stream/decode_stream (§7)
target.odin # optional runtime Target vtable (§5.3)
Every architecture package exposes these names with these signatures. This is
what makes the family feel like one library and what each new ISA is built
against as a checklist.
x86/ # exists today; refactor to import isa
registers.odin operands.odin instructions.odin mnemonics.odin
encoding_types.odin encoder.odin decoder.odin printer.odin
encoding_table.odin decoding_tables.odin mnemonic_builders.odin
tests/ tools/
**Types (concrete per arch, identical names):**
`Register Memory Operand Operand_Kind Instruction Mnemonic Encoding
Instruction_Info`
riscv/ # next: same shape as x86/
registers.odin operands.odin instructions.odin mnemonics.odin
encoding_types.odin encoder.odin decoder.odin printer.odin
encoding_table.odin decoding_tables.odin mnemonic_builders.odin
tests/ tools/
**Re-exported shared types (from `isa`):**
`Label Label_Definition Label_Map LABEL_UNDEFINED Relocation
Relocation_Type Error Error_Code Result Token Token_Kind Print_Options
DEFAULT_PRINT_OPTIONS`
arm64/ mips/ … # future, same template
```
**Operand constructors:** `op_reg(r) op_mem(m, size) op_imm(v, size)
op_label(id, size)`, an arch-specific `mem_*` set (at minimum `mem_base_disp`),
and `op_<class>(typed)` where the arch has typed register classes.
- **`isa` depends on nothing.** Each arch package depends on `isa` and
**re-exports** the shared types (e.g. `x86.Result`, `x86.Label_Map`)
so a consumer of `x86` sees one coherent namespace and never imports
`isa` directly unless writing arch-generic tooling.
- Each arch package is **self-contained** (its own tests/tools), matching
the move already done for x86.
**Instruction builders & emitters** (operand-kind suffixes spelled out):
`inst_none / inst_r / inst_r_r / inst_r_i / inst_r_m / inst_m_r / …` and
`emit_none / emit_r / emit_rr / emit_ri / …` (concatenated suffixes). x86 also
ships generated typed overloads `inst_<mnemonic>` / `emit_<mnemonic>`; other
arches may add them.
---
## 5. Three layers of generality (pick per use case)
### 5.1 Layer A — direct single-arch use (default, zero overhead)
```odin
import "rexcode/x86"
code: [4096]u8
res := x86.encode(insts[:], labels[:], code[:], &relocs, &errors)
```
Fully static, fully inlined, exactly as fast as today. **99% of consumers
live here.**
### 5.2 Layer B — source-portable code via the naming contract
Because every arch package exposes the *same names with the same
signatures* (§6), code that only touches the shared vocabulary
(`Label_Map`, `encode`, `tprint`, `Result`, `Relocation`) can be written
against `import arch "rexcode/x86"` and re-pointed at `rexcode/riscv` by
changing one import — as long as the arch-specific operand construction is
isolated (e.g. behind your own per-arch helper). Still 100% compile-time,
zero overhead.
### 5.3 Layer C — runtime multi-target facade (opt-in, for tools)
For a disassembler or JIT that selects the arch *at runtime*, `isa`
provides a vtable populated by each arch:
```odin
// isa/target.odin
Target :: struct {
name: string,
decode: proc(data: []u8, out: ^Decoded) -> Result, // bytes → generic Decoded
print: proc(d: ^Decoded, opts: ^Print_Options) -> string,
inst_align: u32, // 1 for x86, 4 for riscv/arm64/mips
max_inst: u32, // 15 for x86, 4 for riscv (8 for C-pairs), 4 for arm64
}
// each arch: x86.TARGET: isa.Target = { … }
```
This boundary trades in **bytes and a generic `Decoded` view**, not the
concrete `Instruction`, so it never forces a unified instruction struct.
It carries a proc-pointer indirection — acceptable for a tool that has
already paid a `switch arch` somewhere, and never on Layer A's path.
---
## 6. The naming contract (the most important artifact)
Every architecture package **MUST** expose these names with these
signatures. This is what makes the family feel like one library and what
the RISC-V implementation is built against as a checklist.
### Types (concrete per arch, identical names)
```
Register Memory Operand Operand_Kind
Instruction Mnemonic Encoding Instruction_Info
```
### Re-exported shared types (from `isa`)
```
Label Label_Definition Label_Map LABEL_UNDEFINED
Relocation Relocation_Type Error Error_Code Result
Token Token_Kind Print_Options DEFAULT_PRINT_OPTIONS
```
### Operand constructors
```
op_reg(r) op_mem(m, size) op_imm(v, size) op_label(id, size)
mem_*(…) # arch-specific set; at minimum mem_base_disp
# (mem_base in x86 is an accessor, not a constructor;
# use mem_base_only for the no-displacement case)
op_<class>(typed) # typed safe constructors where the arch has classes
```
### Instruction builders & emitters
Builder names spell out each operand kind separated by underscores
(matches x86's existing convention):
```
inst_none / inst_r / inst_r_r / inst_r_i / inst_r_m / inst_m_r / …
emit_none / emit_r / emit_rr / emit_ri / emit_rm / emit_mr / …
# NB: emit_* uses concatenated suffixes (legacy x86 spelling)
inst_<mnemonic>(…) / emit_<mnemonic>(…) # generated typed overloads
```
### Entry points (identical signatures across arches)
**Entry points (identical signatures across arches):**
```odin
encode(instructions: []Instruction, label_defs: []Label_Definition,
@@ -339,132 +174,36 @@ decode(data: []u8, relocs: []Relocation,
label_defs: ^[dynamic]Label_Definition, errors: ^[dynamic]Error) -> Result
print/println/aprint/tprint/bprint/fprint/wprint(+ln)(
instructions: []Instruction, inst_info: []Instruction_Info,
label_defs: []Label_Definition, tokens=nil, options=nil, label_names=nil)
instructions: []Instruction, inst_info: []Instruction_Info,
label_defs: []Label_Definition, tokens=nil, options=nil, label_names=nil)
```
### Register/label/print helpers
**Register/label/print helpers:** `reg_hw reg_class reg_size register_name
mnemonic_to_string label label_forward label_named label_reserve
label_set`.
```
reg_hw reg_class reg_size register_name mnemonic_to_string
label label_forward label_named label_reserve label_set
```
> Anything an arch genuinely lacks (e.g. RISC-V has no `mem_base_index`)
> is simply **absent**, not stubbed. Portable (Layer B) code stays within
> the intersection; arch-aware code uses the extras.
> Anything an arch genuinely lacks (e.g. RISC-V has no `mem_base_index`) is
> simply **absent**, not stubbed. Source-portable code stays within the
> intersection; arch-aware code uses the extras.
---
## 7. Zero-cost code reuse via parametric polymorphism
## 5. Tables
The encode/decode **drivers** are arch-independent control flow. Factor
them into `isa` as procedures generic over the instruction type `$I`,
parameterized by an arch-provided per-instruction hook. Odin monomorphizes
these at compile time → **no runtime cost, real code sharing.**
```odin
// isa/pipeline.odin (sketch)
encode_stream :: proc(
instructions: []$I,
label_defs: []Label_Definition,
code: []u8,
relocs: ^[dynamic]Relocation,
errors: ^[dynamic]Error,
encode_one: proc(inst: ^I, out: []u8, code_pos: u32,
relocs: ^[dynamic]Relocation, errors: ^[dynamic]Error) -> (n: u32, ok: bool),
resolve := true,
base_address: u64 = 0,
) -> Result {
// PASS 1: for each inst → record offset, call encode_one, advance
// PASS 1.5: rewrite label_defs inst-index → byte-offset (identical on every arch)
// PASS 2: resolve relocations / patch / spill unresolved (identical on every arch)
}
```
x86's current `encode()` becomes a thin wrapper that passes its
`encode_one` (the prefix/ModRM/SIB body); RISC-V's wrapper passes its
12-line bitfield packer. The label/relocation machinery — the part that's
easy to get subtly wrong — is written and tested **once**.
Caveats (arch-specific passes that stay out of the shared driver):
- **RISC-V pseudo-ops** (`li`, `call`, `la`, `j`) expand to 12 real
instructions; needs an arch pre-lowering pass.
- **Branch relaxation** (short↔long form) is arch-specific.
- **ARM literal pools / constant islands** are an extra emission phase.
These plug in *around* the shared driver, not inside it.
The `Encoding` schema and the tables are per-arch, but the table **pipeline** is
a shared pattern. Each arch's hand-written `ENCODING_TABLE` (the single source of
truth) lives in `<arch>/tablegen/`, a two-stage metaprogram flattens it and emits
committed binary blobs, and the library `#load`s them into `@(rodata)` — no table
is compiled into the library. See [table_migration.md](table_migration.md).
---
## 8. Concrete RISC-V mapping (RV64GC as the first target)
## 6. One-paragraph summary
What each contract item becomes, to validate the design before coding:
| Contract item | RISC-V realization |
|---|---|
| `Register` | `distinct u16`, classes `REG_X` (x031), `REG_F` (f031), `REG_V` (v031). No REX/EVEX bits. `x0` semantic = zero. |
| typed enums | `XREG{ZERO,RA,SP,GP,TP,T0,T1,T2,S0,S1,A0..A7,S2..S11,T3..T6}`, `FREG`, `VREG` |
| `Memory` | `struct { base: Register, disp: i32 }` — no index/scale/segment |
| `mem_*` | `mem_base(base)`, `mem_base_disp(base, disp)` only |
| `Operand` | same kind-tagged shape; `size` mostly informational (width is in the mnemonic) |
| `Mnemonic` | `enum u16` — RV32I/64I + M,A,F,D,C,V (`ADDI, LW, LD, BEQ, JAL, AUIPC, FADD_D, …`) |
| `Encoding` | `struct { format: Format, opcode, funct3, funct7: u8, … }`, `Format{R,I,S,B,U,J,R4,…}` |
| `encode_one` | switch on `format`, pack fields, scatter immediate bits |
| `Encoding_Flags` | tiny (e.g. `is_compressible`, `rounding_ok`) vs x86's 11 fields |
| `Relocation_Type` | `R_RISCV_BRANCH, JAL, CALL, PCREL_HI20, PCREL_LO12_I/S, HI20, LO12_I/S, RVC_BRANCH/JUMP, …` |
| `Instruction_Info` | `offset`, `is_compressed: bool`, rounding mode — no prefix/VEX fields |
| printer | `register_name` uses ABI names; `print_memory` emits `disp(base)`; width lives in the mnemonic (no `.b/.w` suffix) |
| tables | `gen_decode_tables` becomes near-trivial: a fixed-field instruction decodes by `(opcode, funct3, funct7)` keys |
| `MAX_INST_SIZE` | `4` (or `8` to cover a compressed pair); `inst_align` = 2 |
Notable RISC-V-only concerns the design already accommodates:
- **Split immediates** → hidden in `encode_one`; operand stays a clean value.
- **Paired PC-relative relocs** (AUIPC+ADDI) → expressed via the shared
`Relocation` struct with RISC-V's type enum; resolution of the *pair* is
a RISC-V detail layered on the shared reloc list.
- **Compressed (C) extension** → variable 2/4-byte width handled by
`decode_one` returning a length, exactly like x86's variable length —
the shared decode driver already threads instruction length.
If RISC-V slots cleanly into the contract (it does above), the contract is
sound for the regular fixed-width ISAs (ARM64, MIPS) too.
---
## 9. Recommended next steps
1. **Stabilize x86 first.** Resolve the constructor-rename drift noted in
[x86_api.md](x86_api.md#known-drift) (tests/README vs `operands.odin`)
so x86 is the clean reference the contract is extracted from.
2. **Extract `isa`** by lifting the *already-arch-independent* files:
`labels.odin`, the `Relocation`/`Error`/`Result` types, and the printer
framework (tokens/options/sinks/number-formatting). Make `x86`
re-export them. This is a low-risk refactor that proves the split.
3. **Add the parametric `encode_stream`/`decode_stream`** to `isa` and
reduce x86's `encode`/`decode` to wrappers. Validate against the
existing test suite (same bytes out).
4. **Write the RISC-V package against the contract** (§6) and the mapping
(§8), reusing `isa` wholesale. Build its `encoding_table.odin` by hand,
then port the two generators.
5. **Only if a runtime-multi-target tool appears**, add the `Target`
vtable (§5.3). Don't build it speculatively.
The deliverable order matters: every step is independently shippable, and
x86 keeps working (and keeps its performance) throughout.
---
## 10. One-paragraph summary
Make `isa` own the parts that are the same on every ISA — labels,
relocations, errors/result, the print framework, and (via Odin
parametric polymorphism) the encode/decode driver loops. Make each arch
package own its registers, memory model, operands, mnemonics, encoding
tables, and the actual `encode_one`/`decode_one` bytes. Bind the family
together with a strict **naming contract** so packages are drop-in
swappable at source level with zero runtime cost, and reserve a single
opt-in runtime `Target` vtable for the rare tool that needs to choose an
architecture dynamically. x86 keeps every cycle of its current
performance; RISC-V (and later ARM/MIPS) gets the boring 60% for free and
writes only the 40% that is genuinely its own.
Make `isa` own the parts that are the same on every ISA — labels, errors/result,
and the print framework. Make each arch package own its registers, memory model,
operands, mnemonics, encoding tables, and the actual `encode_one`/`decode_one`
bytes. Bind the family together with a strict **naming contract** so packages are
drop-in swappable at source level with zero runtime cost. x86 keeps every cycle
of its performance; each new ISA gets the boring shared vocabulary for free and
writes only the part that is genuinely its own.

View File

@@ -1,3 +1,5 @@
<!-- rexcode · Brendan Punsky (dotbmp@github), original author -->
# MIPS targets and extensions — platform catalog
> What's worth supporting in `rexcode/mips/` (or a sibling subpackage) and

View File

@@ -0,0 +1,184 @@
<!-- rexcode · Brendan Punsky (dotbmp@github), original author -->
# rexcode — `#load`ed Table Migration (per-arch checklist)
> Move every arch's encode/decode tables out of the compiled library and into
> committed binary blobs that the library `#load`s into `@(rodata)`. The
> hand-written `ENCODING_TABLE` stays the single source of truth; it just
> moves into a per-arch `tablegen` metaprogram that emits the blobs through a
> human-readable, type-checked intermediate.
>
> **Reference implementations:** `x86/` (CISC, variable-length, 2-D opcode
> index) and `mips/` (fixed-width bits/mask, 1-D bucket index). Read those two
> before doing a new arch — copy whichever paradigm matches.
>
> **Status (2026-06-15): all 10 arches migrated.** This doc now doubles as the
> reference for the table pipeline and for **regenerating** blobs after editing
> an `ENCODING_TABLE` (follow §3, or just the two `odin run` commands in §2).
---
## 0. Prerequisites
- **Use the in-repo compiler**, not the system `odin`. gingerBill's fix that
lets a `bit_field`-bearing struct const-init under `@(rodata)` across a
package boundary is in this branch's `src/` but not in master/system odin.
Build once: `./build_odin.sh release` → use `./odin` for everything.
(With the system odin, both the original tables *and* the moved SoT fail to
compile with `@(rodata) must have constant initialization`.)
## 1. End state per arch
```
<arch>/
encoding_types.odin UNCHANGED (Encoding, enums, flags, Feature/Mode)
mnemonics.odin UNCHANGED
encoder.odin 1-line edit: ENCODING_TABLE[m] -> encoding_forms(m)
decoder.odin x86: 2-D index -> didx(); fixed-width: NO change
tables.odin NEW (generated): subsidiary types + #load globals + accessors
tables/<arch>.*.bin NEW: committed blobs (raw packed struct images)
tablegen/ NEW package rexcode_<arch>_tablegen — exactly two files:
encoding_table.odin the SoT, moved here, byte-identical but the package clause
gen.odin Stage A driver (+ package-scope aliases)
generated/ machine-written subpackage rexcode_<arch>_generated:
encode_tables.odin ENCODE_FORMS + ENCODE_RUNS typed literals
decode_tables.odin decode entries + index tables typed literals
writer.odin Stage B main: serialize the globals to ../../tables/*.bin
tools/ gen_decode_tables.odin REMOVED; others rewritten (see §6)
tests/ ENCODING_TABLE references rewritten (see §6)
```
Deleted: `encoding_table.odin` (moved), `decoding_tables.odin` (now blobs),
`tools/gen_decode_tables.odin` (superseded by `tablegen/`).
## 2. The two-stage pipeline
```
ENCODING_TABLE (SoT, untouched)
--Stage A (odin run <arch>/tablegen)--> generated/*.odin + tables.odin
--Stage B (odin run <arch>/tablegen/generated)--> tables/<arch>.*.bin
--#load (library build)--> @(rodata) globals
```
Stage A emits human-readable, **type-checked** Odin (reusing gingerBill's
`print_enum_buffered`/alignment helpers); Stage B compiles those literals and
dumps their raw bytes. The compiler validates the flattened tables before they
become opaque blobs.
## 3. Step-by-step
1. `mkdir -p <arch>/tablegen/tables` (well, `<arch>/tablegen` and `<arch>/tables`).
2. `git mv <arch>/encoding_table.odin <arch>/tablegen/encoding_table.odin`.
Change **only** its package clause → `package rexcode_<arch>_tablegen`.
Keep `@(rodata)` and every encoding row byte-identical.
3. `git rm <arch>/decoding_tables.odin <arch>/tools/gen_decode_tables.odin`.
4. Write `<arch>/tablegen/gen.odin` (§4). It needs package-scope aliases so the
moved SoT resolves its top-level type names:
```odin
Encoding :: lib.Encoding
Mnemonic :: lib.Mnemonic
```
**Also alias any package-level constant the SoT references** — grep the SoT:
`grep -oE "=[A-Za-z_][A-Za-z0-9_]*" tablegen/encoding_table.odin | grep -vE "=true|=false" | sort -u`
(x86 needed `PREFIX_66/F3/F2`; mips needed none.) Bare `.ENUM` selectors and
`{field=...}` flag literals need no alias — they infer from field types.
5. Bootstrap so the library compiles before Stage A can run (it imports the
library for types): create empty blobs `for n in <names>; do : > tables/<arch>.$n.bin; done`
(a 0-byte file `#load`s as a len-0 slice), and write a first `tables.odin`
(or let Stage A overwrite a stub). Then `mkdir -p tablegen/generated`.
6. `encoder.odin`: `ENCODING_TABLE[inst.mnemonic]` → `encoding_forms(inst.mnemonic)`.
7. `decoder.odin`: **x86 only** — flatten the `[4][256]` index access
`T[prefix][opcode]` → `didx(T, prefix, opcode)`. Fixed-width index tables are
already 1-D, so the decoder is unchanged.
8. `odin run <arch>/tablegen` (Stage A) then `odin run <arch>/tablegen/generated`
(Stage B). Stage A should print form/entry counts matching the old
`decoding_tables.odin` array sizes.
9. Rewrite tool/test consumers of the (formerly public) `ENCODING_TABLE` (§6).
10. `odin run <arch>/tests` → green. Confirm idempotence (re-run Stage A+B; the
committed files must not change).
## 4. `gen.odin` anatomy (copy from x86 or mips)
A single `BLOBS` manifest drives both the loader's `#load` lines and the
writer's dumps, so they can't drift:
```odin
Blob :: struct { global, file, typ: string }
BLOBS := [?]Blob{ {"ENCODE_FORMS","<arch>.encode_forms.bin","Encoding"}, ... }
```
Emitters:
- `emit_encode_tables` — identical on every arch: walk `ENCODING_TABLE` in
`Mnemonic` order → `ENCODE_FORMS: [N]lib.Encoding` + `ENCODE_RUNS:
[lib.Mnemonic]lib.Encode_Run` (run = `{start, count}` into ENCODE_FORMS).
- `emit_decode_tables` — **arch-specific**; port the bucketing from the arch's
old `tools/gen_decode_tables.odin` (Entry struct, sort key, index ranges).
- `emit_writer` — identical: `raw(&G, size_of(G))` → `os.write_entire_file`.
- `emit_loader` — identical shape: emit the subsidiary type defs + a `#load`
line per blob + the accessors.
Use `#directory`-relative output paths so it runs from anywhere.
`Encode_Run :: struct { start: u32, count: u32 }` (8 B; same footprint as a
padded `{u16,u16}`, no caps). The `encoding_forms`/`didx` accessors are
`@(private, require_results)` — **keep them private**; consumers outside the
package use the public `ENCODE_FORMS`/`ENCODE_RUNS` globals instead.
## 5. The three decode paradigms
| Paradigm | Arches | Index tables | decoder.odin | Reference |
|---|---|---|---|---|
| CISC variable | `x86` | 2-D `[4][256]` → load flat `[]Decode_Index`, `didx` | flatten 2-D→`didx` | **x86 (done)** |
| Fixed-width bits/mask | `arm32 arm64 mips riscv ppc ppc_vle rsp` | 1-D bucket arrays | unchanged | **mips (done)** |
| 8-bit opcode/length | `mos6502 mos65816` | 256-entry opcode→entry | unchanged | none yet — by analogy |
For fixed-width, `Decode_Entry` == `Encoding` shape, so one `write_row` helper
serves both ENCODE_FORMS and DECODE_ENTRIES. Each arch's bucket structure
differs (mips: primary + SPECIAL/REGIMM/COP1/SPECIAL2/SPECIAL3; ppc: primary
+ sub(16384) + bucket_list + form_idx; arm/riscv/rsp/ppc_vle: read their gen).
## 6. Consumers of the (formerly public) `ENCODING_TABLE`
`ENCODING_TABLE` was public API. Every `<pkg>.ENCODING_TABLE[m]` outside the
library becomes:
```odin
_run := <pkg>.ENCODE_RUNS[u16(m)]
forms := <pkg>.ENCODE_FORMS[_run.start:][:_run.count]
```
Sweep with `grep -rn "ENCODING_TABLE\[" <arch>` and hit:
- `tools/dump_verify_input.odin` (every arch), `tools/gen_mnemonic_builders.odin`
+ `tools/verify_tables.odin` (x86).
- `tests/smoke.odin`, `tests/sweep.odin`, `tests/full_sweep.odin`,
`tests/decode_sweep.odin` (varies by arch).
Watch for two traps the recompile exposes:
- **2-D index access in tools** (x86 `verify_tables`): `T[prefix][opcode]` →
`T[(int(prefix) << 8) | int(opcode)]` (didx is private).
- **stale field names** (mips `dump_verify_input` used `f.isa`; the field is
`f.feature`) — pre-existing rot, just fix it.
## 7. `.gitignore`
The repo ignores `*.bin`, so blobs need a negation (added once, covers all):
```
!core/rexcode/*/tables/*.bin
```
`x86/` additionally hits the broad VS-style `x86/` rule, so it also needs
`!/core/rexcode/x86/`. No other arch needs the directory negation.
## 8. Tests — pre-existing bugs the suite hits once it runs further
Only **x86** JIT-executes its encoded output, so only x86 needed these (both
fixed in the x86 pass; reuse if another arch adds execution):
- `tests/test.odin alloc_exec` mapped memory R/W only on Linux — add
`virtual.protect(..., {.Read,.Write,.Execute})` so the page is executable.
- `printer.odin` dereferenced a nil `label_names` (`label_names^[k]`); guard
`if label_names != nil`. Check each arch's printer for the same pattern.
## 9. Done-criteria per arch
- `<arch>/` contains `tables.odin` and **no** `encoding_table.odin` /
`decoding_tables.odin`; `tablegen/` has exactly `encoding_table.odin` + `gen.odin`.
- Stage A counts == old `decoding_tables.odin` array sizes; Stage B blob sizes
== count × `size_of(struct)`.
- `odin run <arch>/tests` green; all `tools/*.odin` compile (`odin build T.odin -file`).
- Re-running Stage A+B produces no diff (idempotent).
> `doc.odin`, `cross_arch_design.md`, and `x86_api.md` were updated to this
> layout when the migration completed.

View File

@@ -1,3 +1,5 @@
<!-- rexcode · Brendan Punsky (dotbmp@github), original author -->
# rexcode `x86` — Complete API Extraction
> Snapshot of the entire public surface of the `x86` subpackage
@@ -6,19 +8,19 @@
> is built against.
The package is **table-driven**: a hand-written master encoding table
(`ENCODING_TABLE`) is the single source of truth, from which the decode
tables and the typed builder procedures are *generated*. The runtime is
zero-allocation (caller owns every buffer) and the hot paths are fully
inlined.
(`ENCODING_TABLE`, in `tablegen/`) is the single source of truth, from which the
encode/decode tables (committed binary blobs, `#load`ed into `@(rodata)`) and the
typed builder procedures are *generated*. The runtime is zero-allocation (caller
owns every buffer) and the hot paths are fully inlined.
```
ENCODING_TABLE (hand-written, source of truth)
┌───────────────┼────────────────┐
gen_decode_tables gen_mnemonic_builders
tablegen (2-stage) gen_mnemonic_builders
│ │
decoding_tables.odin mnemonic_builders.odin
(decode() reads these) (typed inst_*/emit_* helpers)
tables/*.bin → tables.odin mnemonic_builders.odin
(#loaded into @(rodata)) (typed inst_*/emit_* helpers)
```
Pipeline at a glance:
@@ -131,10 +133,8 @@ MEM_BASE_RIP :: 30 MEM_BASE_NONE :: 31 MEM_INDEX_NONE :: 31
`mem_base_index(base, index, scale)`,
`mem_base_index_disp(base, index, scale, disp)`, `mem_rip_disp(disp)`.
> ⚠️ The README and `tests/test.odin` still use the *old* names
> (`mem_base`, `mem_base_displacement`, `mem_base_index_displacement`,
> `mem_rip_relative`). `mem_base` is now an **accessor**, not a
> constructor. See the "Known drift" note at the end.
> ⚠️ `mem_base` is an **accessor** (returns the base `Register`), not a
> constructor — use `mem_base_only` for the no-displacement case.
**Accessors:** `mem_scale`, `mem_is_rip_relative`, `mem_has_base`,
`mem_has_index` `(Memory) -> …`; `mem_base`, `mem_index` `(Memory) -> Register`.
@@ -295,7 +295,7 @@ VEX_Type :: enum u8 { NONE, VEX, EVEX, XOP }
VEX_W :: enum u8 { WIG, W0, W1 }
VEX_L :: enum u8 { LIG, L0, L1, L2 }
Encoding_Flags :: bit_field u16 {
Encoding_Flags :: bit_field u32 {
esc: Escape | 2,
prefix: u8 | 2,
vex_type: VEX_Type | 2,
@@ -307,9 +307,10 @@ Encoding_Flags :: bit_field u16 {
lock_ok: bool | 1,
rep_ok: bool | 1,
modrm_reg_ext: bool | 1,
mode_32_only: bool | 1,
}
Encoding :: struct #packed { // 14 bytes — one encoding form
Encoding :: struct #packed { // 16 bytes — one encoding form
mnemonic: Mnemonic,
ops: [4]Operand_Type,
enc: [4]Operand_Encoding,
@@ -455,33 +456,41 @@ label_defs: []Label_Definition, …)`.
---
## 10. Generated tables & builders
## 10. Tables & builders
### `encoding_table.odin` (hand-written master)
### `tablegen/encoding_table.odin` (hand-written master — the source of truth)
```odin
ENCODING_TABLE: [Mnemonic][]Encoding = { .MOV = { forms }, }
```
The single source of truth. `encode()` does `ENCODING_TABLE[mnemonic]`
(O(1)) then linear-scans the forms via `encoding_matches_inline`.
Lives in `x86/tablegen/` (a metaprogram package), **not** in the library. A
two-stage pipeline flattens it and serializes committed binary blobs
(`odin run x86/tablegen` → generated Odin + `tables.odin`; then
`odin run x86/tablegen/generated``tables/x86.*.bin`). See
[table_migration.md](table_migration.md).
### `decoding_tables.odin` (generated from `ENCODING_TABLE`)
### `tables.odin` (generated — `#load`s the blobs into `@(rodata)`)
The library compiles no table body; `tables.odin` `#load`s `tables/x86.*.bin`
and defines the subsidiary types + accessors:
```odin
ModRM_Info :: struct #packed { mod, reg, rm: u8, has_sib: bool, disp_size: u8 }
SIB_Info :: struct #packed { /* scale, index, base */ }
Encode_Run :: struct { start: u32, count: u32 } // run into ENCODE_FORMS
ModRM_Info :: struct #packed { mod, reg, rm: u8, has_sib: bool, disp_size: u8 }
SIB_Info :: struct #packed { scale, index, base: u8 }
Decode_Entry :: struct { esc: Escape, prefix, opcode, ext: u8,
mnemonic: Mnemonic, ops: [4]Operand_Type,
enc: [4]Operand_Encoding, flags: Encoding_Flags }
VEX_Decode_Entry :: struct { Decode_Entry fields + vex_w: VEX_W, vex_l: VEX_L }
Decode_Index :: struct { start: u16, count: u8 } // range into entries
Decode_Index :: struct { start: u16, count: u8 } // range into entries
MODRM_TABLE[256], SIB_TABLE[256]
LEGACY_DECODE_ENTRIES[1266], VEX_DECODE_ENTRIES[667], EVEX_DECODE_ENTRIES[418]
DECODE_INDEX_LEGACY[4][256], DECODE_INDEX_ESC_0F/_0F38/_0F3A[4][256]
VEX_INDEX_0F/_0F38/_0F3A[4][256], EVEX_INDEX_0F/_0F38/_0F3A[4][256]
ENCODE_FORMS: []Encoding, ENCODE_RUNS: []Encode_Run // encode via encoding_forms(m)
MODRM_TABLE, SIB_TABLE, LEGACY/VEX/EVEX_DECODE_ENTRIES (1270/667/418)
DECODE_INDEX_* / VEX_INDEX_* / EVEX_INDEX_* ([]Decode_Index, flat 4×256)
```
`[prefix][opcode] -> Decode_Index` gives O(1) opcode resolution; the
`encode()` does `encoding_forms(mnemonic)` (a run into `ENCODE_FORMS`) then
linear-scans the forms via `encoding_matches_inline`. `decode()` does
`didx(table, prefix, opcode) -> Decode_Index` for O(1) opcode resolution; the
small `count` range is scanned for ModR/M-ext, operand-size, or VEX.W/L
disambiguation.
@@ -505,26 +514,10 @@ with full type checking, no runtime dispatch.
| File | Package | Role |
|---|---|---|
| `gen_decode_tables.odin` | `main` (`-file`) | walk `ENCODING_TABLE`emit `decoding_tables.odin` |
| `gen_mnemonic_builders.odin` | `main` (`-file`) | walk `ENCODING_TABLE` → emit `mnemonic_builders.odin` |
| `verify_tables.odin` | `main`, imports `x86 "../"` | check decode tables consistent with `ENCODING_TABLE` |
| `tablegen/gen.odin` | `main` | flatten `ENCODING_TABLE`generated Odin → `tables/*.bin` (2-stage) |
| `tools/gen_mnemonic_builders.odin` | `main` (`-file`) | walk the encode forms → emit `mnemonic_builders.odin` |
| `tools/verify_tables.odin` | `main`, imports `x86 "../"` | check decode tables consistent with the encode forms |
| `tools/dump_verify_input.odin`, `verify_against_llvm.odin` | `main` | LLVM-mc verification harness |
Tests live in `x86/tests/test.odin` (`package x86_tests`, `import x86 "../"`),
run with `odin run x86/tests`.
---
## Known drift (pre-existing, not from the move)
The working tree had uncommitted edits to `operands.odin`/`printer.odin`
that **renamed the memory constructors** but did not update callers:
- `mem_base_displacement``mem_base_disp`
- `mem_base_index_displacement``mem_base_index_disp`
- `mem_rip_relative``mem_rip_disp`
- `mem_base` repurposed from *constructor* to *accessor*
Result: the library compiles, but `tests/test.odin` (and the README
examples) reference the old names and currently fail to type-check.
Fixing requires either restoring the old constructor names or sweeping
the tests/README to the new ones — a deliberate decision left to you.

View File

@@ -1,3 +1,5 @@
// rexcode · Brendan Punsky (dotbmp@github), original author
package rexcode_isa
// =============================================================================

View File

@@ -1,3 +1,5 @@
// rexcode · Brendan Punsky (dotbmp@github), original author
package rexcode_isa
// =============================================================================

View File

@@ -1,3 +1,5 @@
// rexcode · Brendan Punsky (dotbmp@github), original author
package rexcode_isa
// =============================================================================

View File

@@ -1,3 +1,5 @@
// rexcode · Brendan Punsky (dotbmp@github), original author
package rexcode_isa
// =============================================================================

View File

@@ -1,3 +1,5 @@
// rexcode · Brendan Punsky (dotbmp@github), original author
package rexcode_mips
import "../isa"
@@ -10,7 +12,7 @@ import "../isa"
//
// PASS 1 - read each instruction word in the given endianness, dispatch
// via the generated tables (DECODE_INDEX_PRIMARY plus the five
// sub-tables in decoding_tables.odin), and emit one Instruction
// sub-tables in tables.odin), and emit one Instruction
// + one Instruction_Info. Branch/jump operands are emitted as
// RELATIVE-kind operands carrying the *absolute* target byte
// offset within the decoded region.

View File

@@ -1,3 +1,5 @@
// rexcode · Brendan Punsky (dotbmp@github), original author
package rexcode_mips
// =============================================================================
@@ -142,7 +144,7 @@ encode_one_inline :: #force_inline proc(
return 0, false
}
forms := ENCODING_TABLE[inst.mnemonic]
forms := encoding_forms(inst.mnemonic)
if len(forms) == 0 {
append(errors, Error{inst_idx = u32(inst_idx), code = .INVALID_MNEMONIC})
return 0, false

View File

@@ -1,3 +1,5 @@
// rexcode · Brendan Punsky (dotbmp@github), original author
package rexcode_mips
import "../isa"

View File

@@ -1,3 +1,5 @@
// rexcode · Brendan Punsky (dotbmp@github), original author
package rexcode_mips
// =============================================================================

View File

@@ -1,3 +1,5 @@
// rexcode · Brendan Punsky (dotbmp@github), original author
package rexcode_mips
// =============================================================================

View File

@@ -1,3 +1,5 @@
// rexcode · Brendan Punsky (dotbmp@github), original author
package rexcode_mips
// =============================================================================

View File

@@ -1,3 +1,5 @@
// rexcode · Brendan Punsky (dotbmp@github), original author
package rexcode_mips
import "core:strings"

View File

@@ -1,3 +1,5 @@
// rexcode · Brendan Punsky (dotbmp@github), original author
package rexcode_mips
// =============================================================================

View File

@@ -1,3 +1,5 @@
// rexcode · Brendan Punsky (dotbmp@github), original author
package rexcode_mips
// =============================================================================

View File

@@ -1,4 +1,6 @@
package rexcode_mips
// rexcode · Brendan Punsky (dotbmp@github), original author
package rexcode_mips_tablegen
// =============================================================================
// MIPS ENCODING_TABLE

View File

@@ -0,0 +1,315 @@
// rexcode · Brendan Punsky (dotbmp@github), original author
package rexcode_mips_tablegen
// =============================================================================
// MIPS TABLE GENERATOR (Stage A)
// =============================================================================
//
// Reads the single-source-of-truth ENCODING_TABLE (encoding_table.odin, this
// package) and emits human-readable, type-checked Odin into ./generated/:
//
// generated/encode_tables.odin ENCODE_FORMS + ENCODE_RUNS (flattened encode)
// generated/decode_tables.odin DECODE_ENTRIES + primary/SPECIAL/REGIMM/COP1/
// SPECIAL2/SPECIAL3 index tables
// generated/writer.odin Stage B: serialize those globals to ../tables/*.bin
//
// It also re-emits the library loader ../tables.odin. Run:
// odin run mips/tablegen # Stage A
// odin run mips/tablegen/generated # Stage B
import "core:fmt"
import "core:os"
import "core:strings"
import "core:slice"
import "core:reflect"
import "core:math/bits"
import lib "../"
// Package-scope aliases so the moved SoT resolves Mnemonic/Encoding unqualified.
Encoding :: lib.Encoding
Mnemonic :: lib.Mnemonic
Blob :: struct { global, file, typ: string }
BLOBS := [?]Blob{
{"ENCODE_FORMS", "mips.encode_forms.bin", "Encoding"},
{"ENCODE_RUNS", "mips.encode_runs.bin", "Encode_Run"},
{"DECODE_ENTRIES", "mips.entries.bin", "Decode_Entry"},
{"DECODE_INDEX_PRIMARY", "mips.idx_primary.bin", "Decode_Index"},
{"DECODE_INDEX_SPECIAL", "mips.idx_special.bin", "Decode_Index"},
{"DECODE_INDEX_REGIMM", "mips.idx_regimm.bin", "Decode_Index"},
{"DECODE_INDEX_COP1", "mips.idx_cop1.bin", "Decode_Index"},
{"DECODE_INDEX_SPECIAL2", "mips.idx_special2.bin", "Decode_Index"},
{"DECODE_INDEX_SPECIAL3", "mips.idx_special3.bin", "Decode_Index"},
}
DIR_GEN :: #directory + "/generated/"
PATH_LOADER :: #directory + "/../tables.odin"
Entry :: struct {
mnemonic: lib.Mnemonic,
ops: [4]lib.Operand_Type,
enc: [4]lib.Operand_Encoding,
bits: u32,
mask: u32,
feature: lib.Feature,
flags: lib.Encoding_Flags,
primary_op: u8,
sub_key: u8,
}
Range :: struct { start: u16, count: u16 }
main :: proc() {
n := emit_encode_tables()
ne := emit_decode_tables()
emit_writer()
emit_loader()
fmt.printfln("mips tablegen: %d encode forms, %d decode entries", n, ne)
}
// -----------------------------------------------------------------------------
// Encode side
// -----------------------------------------------------------------------------
emit_encode_tables :: proc() -> (total: int) {
sb := strings.builder_make()
strings.write_string(&sb, "package rexcode_mips_generated\n\n")
strings.write_string(&sb, "// GENERATED by ../gen.odin -- DO NOT EDIT.\n")
strings.write_string(&sb, "// Flattened encode forms + per-mnemonic run index (source: ENCODING_TABLE).\n\n")
strings.write_string(&sb, "import lib \"../..\"\n\n")
for m in Mnemonic { total += len(ENCODING_TABLE[m]) }
fmt.sbprintfln(&sb, "ENCODE_FORMS := [%d]lib.Encoding{{", total)
for m in Mnemonic {
forms := ENCODING_TABLE[m]
if len(forms) == 0 { continue }
fmt.sbprintfln(&sb, "\t// .%v", m)
for f in forms {
write_row(&sb, f.mnemonic, f.ops, f.enc, f.bits, f.mask, f.feature, f.flags)
}
}
strings.write_string(&sb, "}\n\n")
run_w := 0
for m in Mnemonic { run_w = max(run_w, len(reflect.enum_string(m))) }
strings.write_string(&sb, "ENCODE_RUNS := [lib.Mnemonic]lib.Encode_Run{\n")
start := 0
for m in Mnemonic {
c := len(ENCODING_TABLE[m])
name := reflect.enum_string(m)
fmt.sbprintf(&sb, "\t.%s", name)
for _ in 0..<run_w-len(name) { strings.write_byte(&sb, ' ') }
fmt.sbprintfln(&sb, " = {{% 5d, % 3d}},", start, c)
start += c
}
strings.write_string(&sb, "}\n")
emit_file(DIR_GEN + "encode_tables.odin", &sb)
return
}
// -----------------------------------------------------------------------------
// Decode side
// -----------------------------------------------------------------------------
emit_decode_tables :: proc() -> (total: int) {
all: [dynamic]Entry
defer delete(all)
for m in Mnemonic {
for f in ENCODING_TABLE[m] {
primary := u8((f.bits >> 26) & 0x3F)
sub: u8
switch primary {
case 0x00, 0x1C, 0x1F: sub = u8(f.bits & 0x3F)
case 0x01: sub = u8((f.bits >> 16) & 0x1F)
case 0x11: sub = u8((f.bits >> 21) & 0x1F)
}
append(&all, Entry{f.mnemonic, f.ops, f.enc, f.bits, f.mask, f.feature, f.flags, primary, sub})
}
}
slice.sort_by(all[:], proc(a, b: Entry) -> bool {
if a.primary_op != b.primary_op { return a.primary_op < b.primary_op }
if a.sub_key != b.sub_key { return a.sub_key < b.sub_key }
ac := bits.count_ones(a.mask); bc := bits.count_ones(b.mask)
if ac != bc { return ac > bc }
return u16(a.mnemonic) < u16(b.mnemonic)
})
primary_idx: [64]Range
special_idx: [64]Range
regimm_idx: [32]Range
cop1_idx: [32]Range
special2_idx: [64]Range
special3_idx: [64]Range
for e, i in all {
push(&primary_idx[e.primary_op], u16(i))
switch e.primary_op {
case 0x00: push(&special_idx [e.sub_key], u16(i))
case 0x01: push(&regimm_idx [e.sub_key], u16(i))
case 0x11: push(&cop1_idx [e.sub_key], u16(i))
case 0x1C: push(&special2_idx[e.sub_key], u16(i))
case 0x1F: push(&special3_idx[e.sub_key], u16(i))
}
}
sb := strings.builder_make()
strings.write_string(&sb, "package rexcode_mips_generated\n\n")
strings.write_string(&sb, "// GENERATED by ../gen.odin -- DO NOT EDIT.\n")
strings.write_string(&sb, "// Reverse decode tables (source: ENCODING_TABLE), keyed by primary opcode + sub-field.\n\n")
strings.write_string(&sb, "import lib \"../..\"\n\n")
fmt.sbprintfln(&sb, "DECODE_ENTRIES := [%d]lib.Decode_Entry{{", len(all))
for e in all {
write_row(&sb, e.mnemonic, e.ops, e.enc, e.bits, e.mask, e.feature, e.flags)
}
strings.write_string(&sb, "}\n\n")
emit_range(&sb, "DECODE_INDEX_PRIMARY", primary_idx[:])
emit_range(&sb, "DECODE_INDEX_SPECIAL", special_idx[:])
emit_range(&sb, "DECODE_INDEX_REGIMM", regimm_idx[:])
emit_range(&sb, "DECODE_INDEX_COP1", cop1_idx[:])
emit_range(&sb, "DECODE_INDEX_SPECIAL2", special2_idx[:])
emit_range(&sb, "DECODE_INDEX_SPECIAL3", special3_idx[:])
emit_file(DIR_GEN + "decode_tables.odin", &sb)
return len(all)
}
push :: proc(r: ^Range, i: u16) { if r.count == 0 { r.start = i }; r.count += 1 }
emit_range :: proc(sb: ^strings.Builder, name: string, ranges: []Range) {
fmt.sbprintfln(sb, "%s := [%d]lib.Decode_Index{{", name, len(ranges))
for r, i in ranges {
if r.count != 0 {
fmt.sbprintfln(sb, "\t0x%02X = {{% 4d, % 3d}},", i, r.start, r.count)
}
}
strings.write_string(sb, "}\n\n")
}
// -----------------------------------------------------------------------------
// Shared row + flags formatting (compact, matching mips' original generator)
// -----------------------------------------------------------------------------
write_row :: proc(sb: ^strings.Builder, mn: lib.Mnemonic, ops: [4]lib.Operand_Type,
enc: [4]lib.Operand_Encoding, bits, mask: u32, feature: lib.Feature, flags: lib.Encoding_Flags) {
fmt.sbprintf(sb, "\t{{ .%v, {{.%v,.%v,.%v,.%v}}, {{.%v,.%v,.%v,.%v}}, 0x%08X, 0x%08X, .%v, {{%s}} }},\n",
mn, ops[0], ops[1], ops[2], ops[3], enc[0], enc[1], enc[2], enc[3], bits, mask, feature, flags_lit(flags))
}
flags_lit :: proc(f: lib.Encoding_Flags) -> string {
parts: [dynamic]string
defer delete(parts)
if f.delay_slot { append(&parts, "delay_slot=true") }
if f.likely { append(&parts, "likely=true") }
if f.only_64 { append(&parts, "only_64=true") }
if f.writes_hilo { append(&parts, "writes_hilo=true") }
if f.compact { append(&parts, "compact=true") }
return strings.join(parts[:], ", ", context.temp_allocator)
}
// -----------------------------------------------------------------------------
// Stage B writer + the library loader
// -----------------------------------------------------------------------------
emit_writer :: proc() {
sb := strings.builder_make()
strings.write_string(&sb, "package rexcode_mips_generated\n\n")
strings.write_string(&sb, "// GENERATED by ../gen.odin -- DO NOT EDIT.\n")
strings.write_string(&sb, "// Stage B: serialize the typed tables above to raw blobs under ../../tables/.\n\n")
strings.write_string(&sb, "import \"core:os\"\nimport \"core:fmt\"\n\n")
strings.write_string(&sb, "TABLES :: #directory + \"/../../tables/\"\n\n")
strings.write_string(&sb, "raw :: #force_inline proc \"contextless\" (p: rawptr, n: int) -> []u8 {\n\treturn (cast([^]u8)p)[:n]\n}\n\n")
strings.write_string(&sb, "w :: proc(file: string, data: []u8) {\n")
strings.write_string(&sb, "\tif err := os.write_entire_file(file, data); err != nil {\n")
strings.write_string(&sb, "\t\tfmt.eprintfln(\"rexcode tablegen: failed to write %s: %v\", file, err)\n\t\tos.exit(1)\n\t}\n}\n\n")
strings.write_string(&sb, "main :: proc() {\n")
for b in BLOBS {
fmt.sbprintfln(&sb, "\tw(TABLES + \"%s\", raw(&%s, size_of(%s)))", b.file, b.global, b.global)
}
strings.write_string(&sb, "}\n")
emit_file(DIR_GEN + "writer.odin", &sb)
}
LOADER_TYPES :: `// -----------------------------------------------------------------------------
// Subsidiary table types (generated scaffolding)
// -----------------------------------------------------------------------------
// Companion run index: ENCODE_RUNS[mnemonic] -> contiguous run in ENCODE_FORMS.
Encode_Run :: struct {
start: u32,
count: u32,
}
Decode_Entry :: struct #packed {
mnemonic: Mnemonic, // 2
ops: [4]Operand_Type, // 4
enc: [4]Operand_Encoding, // 4
bits: u32, // 4
mask: u32, // 4
feature: Feature, // 1
flags: Encoding_Flags, // 1
}
#assert(size_of(Decode_Entry) == 20)
Decode_Index :: struct #packed {
start: u16,
count: u16,
}
#assert(size_of(Decode_Index) == 4)
`
LOADER_ACCESSORS :: `// -----------------------------------------------------------------------------
// Accessors
// -----------------------------------------------------------------------------
// Per-mnemonic encode forms: the run of ENCODE_FORMS belonging to ` + "`m`" + `.
// Replaces the old ENCODING_TABLE[m] slice; the returned view is into rodata.
@(private, require_results)
encoding_forms :: #force_inline proc "contextless" (m: Mnemonic) -> []Encoding {
r := ENCODE_RUNS[u16(m)]
return ENCODE_FORMS[r.start:][:r.count]
}
`
emit_loader :: proc() {
sb := strings.builder_make()
strings.write_string(&sb, "package rexcode_mips\n\n")
strings.write_string(&sb, "// =============================================================================\n")
strings.write_string(&sb, "// GENERATED FILE - DO NOT EDIT\n")
strings.write_string(&sb, "// =============================================================================\n")
strings.write_string(&sb, "//\n")
strings.write_string(&sb, "// Loads the flat binary encode/decode tables into @(rodata). Produced by tablegen:\n")
strings.write_string(&sb, "//\n")
strings.write_string(&sb, "// odin run tablegen # Stage A: ENCODING_TABLE -> generated/ + this file\n")
strings.write_string(&sb, "// odin run tablegen/generated # Stage B: typed Odin literals -> tables/*.bin\n")
strings.write_string(&sb, "//\n")
strings.write_string(&sb, "// The .bin blobs are raw, host-endian, packed struct images.\n\n")
strings.write_string(&sb, LOADER_TYPES)
strings.write_string(&sb, "\n// -----------------------------------------------------------------------------\n")
strings.write_string(&sb, "// Loaded tables (rodata, embedded from tables/*.bin at compile time)\n")
strings.write_string(&sb, "// -----------------------------------------------------------------------------\n\n")
gmax, fmax := 0, 0
for b in BLOBS { gmax = max(gmax, len(b.global)); fmax = max(fmax, len(b.file)) }
for b in BLOBS {
fmt.sbprintf(&sb, "@(rodata) %s", b.global)
for _ in 0..<gmax-len(b.global) { strings.write_byte(&sb, ' ') }
path := fmt.tprintf("\"tables/%s\",", b.file)
fmt.sbprintf(&sb, " := #load(%s", path)
for _ in 0..<fmax-len(b.file) { strings.write_byte(&sb, ' ') }
fmt.sbprintfln(&sb, " []%s)", b.typ)
}
strings.write_string(&sb, "\n")
strings.write_string(&sb, LOADER_ACCESSORS)
emit_file(PATH_LOADER, &sb)
}
GEN_ATTRIB :: "// rexcode · Brendan Punsky (dotbmp@github), original author\n\n"
emit_file :: proc(path: string, sb: ^strings.Builder) {
if err := os.write_entire_file(path, transmute([]u8)strings.concatenate({GEN_ATTRIB, strings.to_string(sb^)})); err != nil {
fmt.eprintfln("rexcode tablegen: failed to write %s: %v", path, err)
os.exit(1)
}
}

View File

@@ -1,33 +1,13 @@
package rexcode_mips
// rexcode · Brendan Punsky (dotbmp@github), original author
// =============================================================================
// GENERATED FILE - DO NOT EDIT
// =============================================================================
//
// Generated by tools/gen_decode_tables.odin from ENCODING_TABLE.
// Regenerate with: cd mips && odin run tools/gen_decode_tables.odin -file
//
package rexcode_mips_generated
Decode_Entry :: struct #packed {
mnemonic: Mnemonic, // 2
ops: [4]Operand_Type, // 4
enc: [4]Operand_Encoding, // 4
bits: u32, // 4
mask: u32, // 4
feature: Feature, // 1
flags: Encoding_Flags, // 1
}
#assert(size_of(Decode_Entry) == 20)
// GENERATED by ../gen.odin -- DO NOT EDIT.
// Reverse decode tables (source: ENCODING_TABLE), keyed by primary opcode + sub-field.
Decode_Index :: struct #packed {
start: u16,
count: u16,
}
#assert(size_of(Decode_Index) == 4)
import lib "../.."
@(rodata)
DECODE_ENTRIES := [783]Decode_Entry{
DECODE_ENTRIES := [783]lib.Decode_Entry{
{ .NOP, {.NONE,.NONE,.NONE,.NONE}, {.NONE,.NONE,.NONE,.NONE}, 0x00000000, 0xFFFFFFFF, .MIPS_I, {} },
{ .SSNOP, {.NONE,.NONE,.NONE,.NONE}, {.NONE,.NONE,.NONE,.NONE}, 0x00000040, 0xFFFFFFFF, .MIPS32_R1, {} },
{ .EHB, {.NONE,.NONE,.NONE,.NONE}, {.NONE,.NONE,.NONE,.NONE}, 0x000000C0, 0xFFFFFFFF, .MIPS32_R2, {} },
@@ -812,235 +792,230 @@ DECODE_ENTRIES := [783]Decode_Entry{
{ .VNOP, {.NONE,.NONE,.NONE,.NONE}, {.NONE,.NONE,.NONE,.NONE}, 0xFFFF0000, 0xFFFFFFFF, .VFPU_PSP, {} },
{ .SD, {.GPR,.MEM,.NONE,.NONE}, {.RT,.OFFSET_BASE,.NONE,.NONE}, 0xFC000000, 0xFC000000, .MIPS_III, {only_64=true} },
}
@(rodata)
DECODE_INDEX_PRIMARY := [64]Decode_Index{
0x00 = {0, 84},
0x01 = {84, 20},
0x02 = {104, 1},
0x03 = {105, 1},
0x04 = {106, 1},
0x05 = {107, 1},
0x06 = {108, 1},
0x07 = {109, 1},
0x08 = {110, 1},
0x09 = {111, 1},
0x0A = {112, 1},
0x0B = {113, 1},
0x0C = {114, 1},
0x0D = {115, 1},
0x0E = {116, 1},
0x0F = {117, 2},
0x10 = {119, 13},
0x11 = {132, 114},
0x12 = {246, 36},
0x13 = {282, 5},
0x14 = {287, 1},
0x15 = {288, 1},
0x16 = {289, 1},
0x17 = {290, 1},
0x18 = {291, 13},
0x19 = {304, 15},
0x1A = {319, 1},
0x1B = {320, 13},
0x1C = {333, 109},
0x1D = {442, 1},
0x1E = {443, 117},
0x1F = {560, 78},
0x20 = {638, 1},
0x21 = {639, 1},
0x22 = {640, 1},
0x23 = {641, 1},
0x24 = {642, 1},
0x25 = {643, 1},
0x26 = {644, 1},
0x27 = {645, 1},
0x28 = {646, 1},
0x29 = {647, 1},
0x2A = {648, 1},
0x2B = {649, 1},
0x2C = {650, 1},
0x2D = {651, 1},
0x2E = {652, 1},
0x2F = {653, 1},
0x30 = {654, 1},
0x31 = {655, 1},
0x32 = {656, 3},
0x33 = {659, 1},
0x34 = {660, 63},
0x35 = {723, 3},
0x36 = {726, 5},
0x37 = {731, 6},
0x38 = {737, 1},
0x39 = {738, 1},
0x3A = {739, 3},
0x3B = {742, 2},
0x3C = {744, 27},
0x3D = {771, 3},
0x3E = {774, 5},
0x3F = {779, 4},
DECODE_INDEX_PRIMARY := [64]lib.Decode_Index{
0x00 = { 0, 84},
0x01 = { 84, 20},
0x02 = { 104, 1},
0x03 = { 105, 1},
0x04 = { 106, 1},
0x05 = { 107, 1},
0x06 = { 108, 1},
0x07 = { 109, 1},
0x08 = { 110, 1},
0x09 = { 111, 1},
0x0A = { 112, 1},
0x0B = { 113, 1},
0x0C = { 114, 1},
0x0D = { 115, 1},
0x0E = { 116, 1},
0x0F = { 117, 2},
0x10 = { 119, 13},
0x11 = { 132, 114},
0x12 = { 246, 36},
0x13 = { 282, 5},
0x14 = { 287, 1},
0x15 = { 288, 1},
0x16 = { 289, 1},
0x17 = { 290, 1},
0x18 = { 291, 13},
0x19 = { 304, 15},
0x1A = { 319, 1},
0x1B = { 320, 13},
0x1C = { 333, 109},
0x1D = { 442, 1},
0x1E = { 443, 117},
0x1F = { 560, 78},
0x20 = { 638, 1},
0x21 = { 639, 1},
0x22 = { 640, 1},
0x23 = { 641, 1},
0x24 = { 642, 1},
0x25 = { 643, 1},
0x26 = { 644, 1},
0x27 = { 645, 1},
0x28 = { 646, 1},
0x29 = { 647, 1},
0x2A = { 648, 1},
0x2B = { 649, 1},
0x2C = { 650, 1},
0x2D = { 651, 1},
0x2E = { 652, 1},
0x2F = { 653, 1},
0x30 = { 654, 1},
0x31 = { 655, 1},
0x32 = { 656, 3},
0x33 = { 659, 1},
0x34 = { 660, 63},
0x35 = { 723, 3},
0x36 = { 726, 5},
0x37 = { 731, 6},
0x38 = { 737, 1},
0x39 = { 738, 1},
0x3A = { 739, 3},
0x3B = { 742, 2},
0x3C = { 744, 27},
0x3D = { 771, 3},
0x3E = { 774, 5},
0x3F = { 779, 4},
}
@(rodata)
DECODE_INDEX_SPECIAL := [64]Decode_Index{
0x00 = {0, 5},
0x01 = {5, 2},
0x02 = {7, 2},
0x03 = {9, 1},
0x04 = {10, 1},
0x05 = {11, 1},
0x06 = {12, 2},
0x07 = {14, 1},
0x08 = {15, 1},
0x09 = {16, 1},
0x0A = {17, 1},
0x0B = {18, 1},
0x0C = {19, 1},
0x0D = {20, 1},
0x0F = {21, 1},
0x10 = {22, 1},
0x11 = {23, 1},
0x12 = {24, 1},
0x13 = {25, 1},
0x14 = {26, 1},
0x15 = {27, 1},
0x16 = {28, 2},
0x17 = {30, 1},
0x18 = {31, 2},
0x19 = {33, 3},
0x1A = {36, 2},
0x1B = {38, 2},
0x1C = {40, 3},
0x1D = {43, 3},
0x1E = {46, 3},
0x1F = {49, 3},
0x20 = {52, 1},
0x21 = {53, 1},
0x22 = {54, 1},
0x23 = {55, 1},
0x24 = {56, 1},
0x25 = {57, 1},
0x26 = {58, 1},
0x27 = {59, 1},
0x28 = {60, 1},
0x29 = {61, 1},
0x2A = {62, 1},
0x2B = {63, 1},
0x2C = {64, 1},
0x2D = {65, 1},
0x2E = {66, 1},
0x2F = {67, 1},
0x30 = {68, 1},
0x31 = {69, 1},
0x32 = {70, 1},
0x33 = {71, 1},
0x34 = {72, 1},
0x35 = {73, 1},
0x36 = {74, 1},
0x37 = {75, 1},
0x38 = {76, 1},
0x3A = {77, 2},
0x3B = {79, 1},
0x3C = {80, 1},
0x3E = {81, 2},
0x3F = {83, 1},
DECODE_INDEX_SPECIAL := [64]lib.Decode_Index{
0x00 = { 0, 5},
0x01 = { 5, 2},
0x02 = { 7, 2},
0x03 = { 9, 1},
0x04 = { 10, 1},
0x05 = { 11, 1},
0x06 = { 12, 2},
0x07 = { 14, 1},
0x08 = { 15, 1},
0x09 = { 16, 1},
0x0A = { 17, 1},
0x0B = { 18, 1},
0x0C = { 19, 1},
0x0D = { 20, 1},
0x0F = { 21, 1},
0x10 = { 22, 1},
0x11 = { 23, 1},
0x12 = { 24, 1},
0x13 = { 25, 1},
0x14 = { 26, 1},
0x15 = { 27, 1},
0x16 = { 28, 2},
0x17 = { 30, 1},
0x18 = { 31, 2},
0x19 = { 33, 3},
0x1A = { 36, 2},
0x1B = { 38, 2},
0x1C = { 40, 3},
0x1D = { 43, 3},
0x1E = { 46, 3},
0x1F = { 49, 3},
0x20 = { 52, 1},
0x21 = { 53, 1},
0x22 = { 54, 1},
0x23 = { 55, 1},
0x24 = { 56, 1},
0x25 = { 57, 1},
0x26 = { 58, 1},
0x27 = { 59, 1},
0x28 = { 60, 1},
0x29 = { 61, 1},
0x2A = { 62, 1},
0x2B = { 63, 1},
0x2C = { 64, 1},
0x2D = { 65, 1},
0x2E = { 66, 1},
0x2F = { 67, 1},
0x30 = { 68, 1},
0x31 = { 69, 1},
0x32 = { 70, 1},
0x33 = { 71, 1},
0x34 = { 72, 1},
0x35 = { 73, 1},
0x36 = { 74, 1},
0x37 = { 75, 1},
0x38 = { 76, 1},
0x3A = { 77, 2},
0x3B = { 79, 1},
0x3C = { 80, 1},
0x3E = { 81, 2},
0x3F = { 83, 1},
}
@(rodata)
DECODE_INDEX_REGIMM := [32]Decode_Index{
0x00 = {84, 1},
0x01 = {85, 1},
0x02 = {86, 1},
0x03 = {87, 1},
0x06 = {88, 1},
0x08 = {89, 1},
0x09 = {90, 1},
0x0A = {91, 1},
0x0B = {92, 1},
0x0C = {93, 1},
0x0E = {94, 1},
0x10 = {95, 1},
0x11 = {96, 1},
0x12 = {97, 1},
0x13 = {98, 1},
0x17 = {99, 1},
0x18 = {100, 1},
0x19 = {101, 1},
0x1C = {102, 1},
0x1E = {103, 1},
DECODE_INDEX_REGIMM := [32]lib.Decode_Index{
0x00 = { 84, 1},
0x01 = { 85, 1},
0x02 = { 86, 1},
0x03 = { 87, 1},
0x06 = { 88, 1},
0x08 = { 89, 1},
0x09 = { 90, 1},
0x0A = { 91, 1},
0x0B = { 92, 1},
0x0C = { 93, 1},
0x0E = { 94, 1},
0x10 = { 95, 1},
0x11 = { 96, 1},
0x12 = { 97, 1},
0x13 = { 98, 1},
0x17 = { 99, 1},
0x18 = { 100, 1},
0x19 = { 101, 1},
0x1C = { 102, 1},
0x1E = { 103, 1},
}
@(rodata)
DECODE_INDEX_COP1 := [32]Decode_Index{
0x00 = {132, 1},
0x01 = {133, 1},
0x02 = {134, 1},
0x03 = {135, 1},
0x04 = {136, 1},
0x05 = {137, 1},
0x06 = {138, 1},
0x07 = {139, 1},
0x08 = {140, 4},
0x09 = {144, 1},
0x0D = {145, 1},
0x10 = {146, 37},
0x11 = {183, 37},
0x14 = {220, 2},
0x15 = {222, 2},
0x16 = {224, 22},
DECODE_INDEX_COP1 := [32]lib.Decode_Index{
0x00 = { 132, 1},
0x01 = { 133, 1},
0x02 = { 134, 1},
0x03 = { 135, 1},
0x04 = { 136, 1},
0x05 = { 137, 1},
0x06 = { 138, 1},
0x07 = { 139, 1},
0x08 = { 140, 4},
0x09 = { 144, 1},
0x0D = { 145, 1},
0x10 = { 146, 37},
0x11 = { 183, 37},
0x14 = { 220, 2},
0x15 = { 222, 2},
0x16 = { 224, 22},
}
@(rodata)
DECODE_INDEX_SPECIAL2 := [64]Decode_Index{
0x00 = {333, 1},
0x01 = {334, 1},
0x02 = {335, 1},
0x04 = {336, 2},
0x05 = {338, 1},
0x08 = {339, 25},
0x09 = {364, 26},
0x10 = {390, 1},
0x11 = {391, 1},
0x12 = {392, 1},
0x13 = {393, 1},
0x18 = {394, 1},
0x19 = {395, 1},
0x1A = {396, 1},
0x1B = {397, 1},
0x20 = {398, 2},
0x21 = {400, 2},
0x24 = {402, 1},
0x25 = {403, 1},
0x28 = {404, 17},
0x29 = {421, 8},
0x30 = {429, 5},
0x31 = {434, 1},
0x34 = {435, 1},
0x36 = {436, 1},
0x37 = {437, 1},
0x3C = {438, 1},
0x3E = {439, 1},
0x3F = {440, 2},
DECODE_INDEX_SPECIAL2 := [64]lib.Decode_Index{
0x00 = { 333, 1},
0x01 = { 334, 1},
0x02 = { 335, 1},
0x04 = { 336, 2},
0x05 = { 338, 1},
0x08 = { 339, 25},
0x09 = { 364, 26},
0x10 = { 390, 1},
0x11 = { 391, 1},
0x12 = { 392, 1},
0x13 = { 393, 1},
0x18 = { 394, 1},
0x19 = { 395, 1},
0x1A = { 396, 1},
0x1B = { 397, 1},
0x20 = { 398, 2},
0x21 = { 400, 2},
0x24 = { 402, 1},
0x25 = { 403, 1},
0x28 = { 404, 17},
0x29 = { 421, 8},
0x30 = { 429, 5},
0x31 = { 434, 1},
0x34 = { 435, 1},
0x36 = { 436, 1},
0x37 = { 437, 1},
0x3C = { 438, 1},
0x3E = { 439, 1},
0x3F = { 440, 2},
}
@(rodata)
DECODE_INDEX_SPECIAL3 := [64]Decode_Index{
0x00 = {560, 2},
0x01 = {562, 1},
0x02 = {563, 1},
0x03 = {564, 1},
0x04 = {565, 1},
0x05 = {566, 1},
0x06 = {567, 1},
0x07 = {568, 1},
0x0A = {569, 3},
0x0C = {572, 1},
0x0F = {573, 8},
0x10 = {581, 12},
0x12 = {593, 9},
0x13 = {602, 9},
0x20 = {611, 5},
0x24 = {616, 4},
0x30 = {620, 9},
0x38 = {629, 9},
DECODE_INDEX_SPECIAL3 := [64]lib.Decode_Index{
0x00 = { 560, 2},
0x01 = { 562, 1},
0x02 = { 563, 1},
0x03 = { 564, 1},
0x04 = { 565, 1},
0x05 = { 566, 1},
0x06 = { 567, 1},
0x07 = { 568, 1},
0x0A = { 569, 3},
0x0C = { 572, 1},
0x0F = { 573, 8},
0x10 = { 581, 12},
0x12 = { 593, 9},
0x13 = { 602, 9},
0x20 = { 611, 5},
0x24 = { 616, 4},
0x30 = { 620, 9},
0x38 = { 629, 9},
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,34 @@
// rexcode · Brendan Punsky (dotbmp@github), original author
package rexcode_mips_generated
// GENERATED by ../gen.odin -- DO NOT EDIT.
// Stage B: serialize the typed tables above to raw blobs under ../../tables/.
import "core:os"
import "core:fmt"
TABLES :: #directory + "/../../tables/"
raw :: #force_inline proc "contextless" (p: rawptr, n: int) -> []u8 {
return (cast([^]u8)p)[:n]
}
w :: proc(file: string, data: []u8) {
if err := os.write_entire_file(file, data); err != nil {
fmt.eprintfln("rexcode tablegen: failed to write %s: %v", file, err)
os.exit(1)
}
}
main :: proc() {
w(TABLES + "mips.encode_forms.bin", raw(&ENCODE_FORMS, size_of(ENCODE_FORMS)))
w(TABLES + "mips.encode_runs.bin", raw(&ENCODE_RUNS, size_of(ENCODE_RUNS)))
w(TABLES + "mips.entries.bin", raw(&DECODE_ENTRIES, size_of(DECODE_ENTRIES)))
w(TABLES + "mips.idx_primary.bin", raw(&DECODE_INDEX_PRIMARY, size_of(DECODE_INDEX_PRIMARY)))
w(TABLES + "mips.idx_special.bin", raw(&DECODE_INDEX_SPECIAL, size_of(DECODE_INDEX_SPECIAL)))
w(TABLES + "mips.idx_regimm.bin", raw(&DECODE_INDEX_REGIMM, size_of(DECODE_INDEX_REGIMM)))
w(TABLES + "mips.idx_cop1.bin", raw(&DECODE_INDEX_COP1, size_of(DECODE_INDEX_COP1)))
w(TABLES + "mips.idx_special2.bin", raw(&DECODE_INDEX_SPECIAL2, size_of(DECODE_INDEX_SPECIAL2)))
w(TABLES + "mips.idx_special3.bin", raw(&DECODE_INDEX_SPECIAL3, size_of(DECODE_INDEX_SPECIAL3)))
}

View File

@@ -0,0 +1,67 @@
// rexcode · Brendan Punsky (dotbmp@github), original author
package rexcode_mips
// =============================================================================
// GENERATED FILE - DO NOT EDIT
// =============================================================================
//
// Loads the flat binary encode/decode tables into @(rodata). Produced by tablegen:
//
// odin run tablegen # Stage A: ENCODING_TABLE -> generated/ + this file
// odin run tablegen/generated # Stage B: typed Odin literals -> tables/*.bin
//
// The .bin blobs are raw, host-endian, packed struct images.
// -----------------------------------------------------------------------------
// Subsidiary table types (generated scaffolding)
// -----------------------------------------------------------------------------
// Companion run index: ENCODE_RUNS[mnemonic] -> contiguous run in ENCODE_FORMS.
Encode_Run :: struct {
start: u32,
count: u32,
}
Decode_Entry :: struct #packed {
mnemonic: Mnemonic, // 2
ops: [4]Operand_Type, // 4
enc: [4]Operand_Encoding, // 4
bits: u32, // 4
mask: u32, // 4
feature: Feature, // 1
flags: Encoding_Flags, // 1
}
#assert(size_of(Decode_Entry) == 20)
Decode_Index :: struct #packed {
start: u16,
count: u16,
}
#assert(size_of(Decode_Index) == 4)
// -----------------------------------------------------------------------------
// Loaded tables (rodata, embedded from tables/*.bin at compile time)
// -----------------------------------------------------------------------------
@(rodata) ENCODE_FORMS := #load("tables/mips.encode_forms.bin", []Encoding)
@(rodata) ENCODE_RUNS := #load("tables/mips.encode_runs.bin", []Encode_Run)
@(rodata) DECODE_ENTRIES := #load("tables/mips.entries.bin", []Decode_Entry)
@(rodata) DECODE_INDEX_PRIMARY := #load("tables/mips.idx_primary.bin", []Decode_Index)
@(rodata) DECODE_INDEX_SPECIAL := #load("tables/mips.idx_special.bin", []Decode_Index)
@(rodata) DECODE_INDEX_REGIMM := #load("tables/mips.idx_regimm.bin", []Decode_Index)
@(rodata) DECODE_INDEX_COP1 := #load("tables/mips.idx_cop1.bin", []Decode_Index)
@(rodata) DECODE_INDEX_SPECIAL2 := #load("tables/mips.idx_special2.bin", []Decode_Index)
@(rodata) DECODE_INDEX_SPECIAL3 := #load("tables/mips.idx_special3.bin", []Decode_Index)
// -----------------------------------------------------------------------------
// Accessors
// -----------------------------------------------------------------------------
// Per-mnemonic encode forms: the run of ENCODE_FORMS belonging to `m`.
// Replaces the old ENCODING_TABLE[m] slice; the returned view is into rodata.
@(private, require_results)
encoding_forms :: #force_inline proc "contextless" (m: Mnemonic) -> []Encoding {
r := ENCODE_RUNS[u16(m)]
return ENCODE_FORMS[r.start:][:r.count]
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -1,3 +1,5 @@
// rexcode · Brendan Punsky (dotbmp@github), original author
package rexcode_mips_tests
// Decoder smoke tests. Drives encode -> decode round-trips and checks

View File

@@ -1,3 +1,5 @@
// rexcode · Brendan Punsky (dotbmp@github), original author
package rexcode_mips_tests
// Encoder smoke tests. Exercises encode() end-to-end across all the

View File

@@ -1,3 +1,5 @@
// rexcode · Brendan Punsky (dotbmp@github), original author
package rexcode_mips_tests
// Printer smoke tests. Encode a stream, decode it, print it, and check the

View File

@@ -1,3 +1,5 @@
// rexcode · Brendan Punsky (dotbmp@github), original author
package rexcode_mips_tests
// Spot-check that ENCODING_TABLE entries are present and have the
@@ -14,7 +16,8 @@ import mips "../"
@(private="file") failures := 0
check :: proc(name: string, m: mips.Mnemonic, want_bits, want_mask: u32) {
encs := mips.ENCODING_TABLE[m]
r := mips.ENCODE_RUNS[u16(m)]
encs := mips.ENCODE_FORMS[r.start:][:r.count]
if len(encs) == 0 {
fmt.printfln(" [FAIL] %s: no encoding in table", name)
failures += 1

Some files were not shown because too many files have changed in this diff Show More