mirror of
https://github.com/odin-lang/Odin.git
synced 2026-06-19 16:42:33 +00:00
Merge branch 'bill/rexcode' of https://github.com/odin-lang/Odin into bill/rexcode
This commit is contained in:
@@ -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
@@ -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
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// rexcode · Brendan Punsky (dotbmp@github), original author
|
||||
|
||||
package rexcode_arm32
|
||||
|
||||
import "../isa"
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// rexcode · Brendan Punsky (dotbmp@github), original author
|
||||
|
||||
package rexcode_arm32
|
||||
|
||||
// =============================================================================
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// rexcode · Brendan Punsky (dotbmp@github), original author
|
||||
|
||||
package rexcode_arm32
|
||||
|
||||
// =============================================================================
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// rexcode · Brendan Punsky (dotbmp@github), original author
|
||||
|
||||
package rexcode_arm32
|
||||
|
||||
// =============================================================================
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// rexcode · Brendan Punsky (dotbmp@github), original author
|
||||
|
||||
package rexcode_arm32
|
||||
|
||||
// =============================================================================
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// rexcode · Brendan Punsky (dotbmp@github), original author
|
||||
|
||||
package rexcode_arm32
|
||||
|
||||
import "core:strings"
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// rexcode · Brendan Punsky (dotbmp@github), original author
|
||||
|
||||
package rexcode_arm32
|
||||
|
||||
// =============================================================================
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// rexcode · Brendan Punsky (dotbmp@github), original author
|
||||
|
||||
package rexcode_arm32
|
||||
|
||||
// =============================================================================
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
package rexcode_arm32
|
||||
// rexcode · Brendan Punsky (dotbmp@github), original author
|
||||
|
||||
package rexcode_arm32_tablegen
|
||||
|
||||
// =============================================================================
|
||||
// AArch32 ENCODING TABLE
|
||||
477
core/rexcode/arm32/tablegen/gen.odin
Normal file
477
core/rexcode/arm32/tablegen/gen.odin
Normal 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)
|
||||
}
|
||||
}
|
||||
2628
core/rexcode/arm32/tablegen/generated/decode_tables.odin
Normal file
2628
core/rexcode/arm32/tablegen/generated/decode_tables.odin
Normal file
File diff suppressed because it is too large
Load Diff
2795
core/rexcode/arm32/tablegen/generated/encode_tables.odin
Normal file
2795
core/rexcode/arm32/tablegen/generated/encode_tables.odin
Normal file
File diff suppressed because it is too large
Load Diff
34
core/rexcode/arm32/tablegen/generated/writer.odin
Normal file
34
core/rexcode/arm32/tablegen/generated/writer.odin
Normal 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)))
|
||||
}
|
||||
70
core/rexcode/arm32/tables.odin
Normal file
70
core/rexcode/arm32/tables.odin
Normal 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]
|
||||
}
|
||||
BIN
core/rexcode/arm32/tables/arm32.bucket_list.bin
Normal file
BIN
core/rexcode/arm32/tables/arm32.bucket_list.bin
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 10 KiB |
BIN
core/rexcode/arm32/tables/arm32.encode_forms.bin
Normal file
BIN
core/rexcode/arm32/tables/arm32.encode_forms.bin
Normal file
Binary file not shown.
BIN
core/rexcode/arm32/tables/arm32.encode_runs.bin
Normal file
BIN
core/rexcode/arm32/tables/arm32.encode_runs.bin
Normal file
Binary file not shown.
BIN
core/rexcode/arm32/tables/arm32.entries.bin
Normal file
BIN
core/rexcode/arm32/tables/arm32.entries.bin
Normal file
Binary file not shown.
BIN
core/rexcode/arm32/tables/arm32.form_idx.bin
Normal file
BIN
core/rexcode/arm32/tables/arm32.form_idx.bin
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.0 KiB |
BIN
core/rexcode/arm32/tables/arm32.idx_a32.bin
Normal file
BIN
core/rexcode/arm32/tables/arm32.idx_a32.bin
Normal file
Binary file not shown.
BIN
core/rexcode/arm32/tables/arm32.idx_t16.bin
Normal file
BIN
core/rexcode/arm32/tables/arm32.idx_t16.bin
Normal file
Binary file not shown.
BIN
core/rexcode/arm32/tables/arm32.idx_t32.bin
Normal file
BIN
core/rexcode/arm32/tables/arm32.idx_t32.bin
Normal file
Binary file not shown.
BIN
core/rexcode/arm32/tables/arm32.idx_t32_sub.bin
Normal file
BIN
core/rexcode/arm32/tables/arm32.idx_t32_sub.bin
Normal file
Binary file not shown.
@@ -1,3 +1,5 @@
|
||||
// rexcode · Brendan Punsky (dotbmp@github), original author
|
||||
|
||||
package rexcode_arm32_tests
|
||||
|
||||
import "core:fmt"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// rexcode · Brendan Punsky (dotbmp@github), original author
|
||||
|
||||
package main
|
||||
|
||||
// =============================================================================
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// rexcode · Brendan Punsky (dotbmp@github), original author
|
||||
|
||||
package rexcode_arm64
|
||||
|
||||
// =============================================================================
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// rexcode · Brendan Punsky (dotbmp@github), original author
|
||||
|
||||
package rexcode_arm64
|
||||
|
||||
import "../isa"
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// rexcode · Brendan Punsky (dotbmp@github), original author
|
||||
|
||||
package rexcode_arm64
|
||||
|
||||
// =============================================================================
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// rexcode · Brendan Punsky (dotbmp@github), original author
|
||||
|
||||
package rexcode_arm64
|
||||
|
||||
// =============================================================================
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// rexcode · Brendan Punsky (dotbmp@github), original author
|
||||
|
||||
package rexcode_arm64
|
||||
|
||||
// =============================================================================
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// rexcode · Brendan Punsky (dotbmp@github), original author
|
||||
|
||||
package rexcode_arm64
|
||||
|
||||
import "core:strings"
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// rexcode · Brendan Punsky (dotbmp@github), original author
|
||||
|
||||
package rexcode_arm64
|
||||
|
||||
// =============================================================================
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// rexcode · Brendan Punsky (dotbmp@github), original author
|
||||
|
||||
package rexcode_arm64
|
||||
|
||||
// =============================================================================
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// rexcode · Brendan Punsky (dotbmp@github), original author
|
||||
|
||||
package rexcode_arm64
|
||||
|
||||
// =============================================================================
|
||||
|
||||
@@ -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)
|
||||
293
core/rexcode/arm64/tablegen/gen.odin
Normal file
293
core/rexcode/arm64/tablegen/gen.odin
Normal 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)
|
||||
}
|
||||
}
|
||||
1227
core/rexcode/arm64/tablegen/generated/decode_tables.odin
Normal file
1227
core/rexcode/arm64/tablegen/generated/decode_tables.odin
Normal file
File diff suppressed because it is too large
Load Diff
3107
core/rexcode/arm64/tablegen/generated/encode_tables.odin
Normal file
3107
core/rexcode/arm64/tablegen/generated/encode_tables.odin
Normal file
File diff suppressed because it is too large
Load Diff
29
core/rexcode/arm64/tablegen/generated/writer.odin
Normal file
29
core/rexcode/arm64/tablegen/generated/writer.odin
Normal 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)))
|
||||
}
|
||||
62
core/rexcode/arm64/tables.odin
Normal file
62
core/rexcode/arm64/tables.odin
Normal 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]
|
||||
}
|
||||
BIN
core/rexcode/arm64/tables/arm64.encode_forms.bin
Normal file
BIN
core/rexcode/arm64/tables/arm64.encode_forms.bin
Normal file
Binary file not shown.
BIN
core/rexcode/arm64/tables/arm64.encode_runs.bin
Normal file
BIN
core/rexcode/arm64/tables/arm64.encode_runs.bin
Normal file
Binary file not shown.
BIN
core/rexcode/arm64/tables/arm64.entries.bin
Normal file
BIN
core/rexcode/arm64/tables/arm64.entries.bin
Normal file
Binary file not shown.
BIN
core/rexcode/arm64/tables/arm64.idx_op0.bin
Normal file
BIN
core/rexcode/arm64/tables/arm64.idx_op0.bin
Normal file
Binary file not shown.
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// rexcode · Brendan Punsky (dotbmp@github), original author
|
||||
|
||||
package main
|
||||
|
||||
// =============================================================================
|
||||
|
||||
434
core/rexcode/build.lua
Executable file
434
core/rexcode/build.lua
Executable 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())
|
||||
@@ -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
|
||||
|
||||
@@ -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 5–9 (their signatures and the types they pass around)
|
||||
is architecture-independent. That is the surface we standardize.
|
||||
The *shape* of items 5–9 (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 | 1–15 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 1–15 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 1–N 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 1–2 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` (x0–31), `REG_F` (f0–31), `REG_V` (v0–31). 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.
|
||||
|
||||
@@ -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
|
||||
|
||||
184
core/rexcode/docs/table_migration.md
Normal file
184
core/rexcode/docs/table_migration.md
Normal 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.
|
||||
@@ -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.
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// rexcode · Brendan Punsky (dotbmp@github), original author
|
||||
|
||||
package rexcode_isa
|
||||
|
||||
// =============================================================================
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// rexcode · Brendan Punsky (dotbmp@github), original author
|
||||
|
||||
package rexcode_isa
|
||||
|
||||
// =============================================================================
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// rexcode · Brendan Punsky (dotbmp@github), original author
|
||||
|
||||
package rexcode_isa
|
||||
|
||||
// =============================================================================
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// rexcode · Brendan Punsky (dotbmp@github), original author
|
||||
|
||||
package rexcode_isa
|
||||
|
||||
// =============================================================================
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// rexcode · Brendan Punsky (dotbmp@github), original author
|
||||
|
||||
package rexcode_mips
|
||||
|
||||
import "../isa"
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// rexcode · Brendan Punsky (dotbmp@github), original author
|
||||
|
||||
package rexcode_mips
|
||||
|
||||
// =============================================================================
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// rexcode · Brendan Punsky (dotbmp@github), original author
|
||||
|
||||
package rexcode_mips
|
||||
|
||||
// =============================================================================
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// rexcode · Brendan Punsky (dotbmp@github), original author
|
||||
|
||||
package rexcode_mips
|
||||
|
||||
// =============================================================================
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// rexcode · Brendan Punsky (dotbmp@github), original author
|
||||
|
||||
package rexcode_mips
|
||||
|
||||
import "core:strings"
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// rexcode · Brendan Punsky (dotbmp@github), original author
|
||||
|
||||
package rexcode_mips
|
||||
|
||||
// =============================================================================
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// rexcode · Brendan Punsky (dotbmp@github), original author
|
||||
|
||||
package rexcode_mips
|
||||
|
||||
// =============================================================================
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
package rexcode_mips
|
||||
// rexcode · Brendan Punsky (dotbmp@github), original author
|
||||
|
||||
package rexcode_mips_tablegen
|
||||
|
||||
// =============================================================================
|
||||
// MIPS ENCODING_TABLE
|
||||
315
core/rexcode/mips/tablegen/gen.odin
Normal file
315
core/rexcode/mips/tablegen/gen.odin
Normal 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(®imm_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)
|
||||
}
|
||||
}
|
||||
@@ -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},
|
||||
}
|
||||
|
||||
2608
core/rexcode/mips/tablegen/generated/encode_tables.odin
Normal file
2608
core/rexcode/mips/tablegen/generated/encode_tables.odin
Normal file
File diff suppressed because it is too large
Load Diff
34
core/rexcode/mips/tablegen/generated/writer.odin
Normal file
34
core/rexcode/mips/tablegen/generated/writer.odin
Normal 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)))
|
||||
}
|
||||
67
core/rexcode/mips/tables.odin
Normal file
67
core/rexcode/mips/tables.odin
Normal 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]
|
||||
}
|
||||
BIN
core/rexcode/mips/tables/mips.encode_forms.bin
Normal file
BIN
core/rexcode/mips/tables/mips.encode_forms.bin
Normal file
Binary file not shown.
BIN
core/rexcode/mips/tables/mips.encode_runs.bin
Normal file
BIN
core/rexcode/mips/tables/mips.encode_runs.bin
Normal file
Binary file not shown.
BIN
core/rexcode/mips/tables/mips.entries.bin
Normal file
BIN
core/rexcode/mips/tables/mips.entries.bin
Normal file
Binary file not shown.
BIN
core/rexcode/mips/tables/mips.idx_cop1.bin
Normal file
BIN
core/rexcode/mips/tables/mips.idx_cop1.bin
Normal file
Binary file not shown.
BIN
core/rexcode/mips/tables/mips.idx_primary.bin
Normal file
BIN
core/rexcode/mips/tables/mips.idx_primary.bin
Normal file
Binary file not shown.
BIN
core/rexcode/mips/tables/mips.idx_regimm.bin
Normal file
BIN
core/rexcode/mips/tables/mips.idx_regimm.bin
Normal file
Binary file not shown.
BIN
core/rexcode/mips/tables/mips.idx_special.bin
Normal file
BIN
core/rexcode/mips/tables/mips.idx_special.bin
Normal file
Binary file not shown.
BIN
core/rexcode/mips/tables/mips.idx_special2.bin
Normal file
BIN
core/rexcode/mips/tables/mips.idx_special2.bin
Normal file
Binary file not shown.
BIN
core/rexcode/mips/tables/mips.idx_special3.bin
Normal file
BIN
core/rexcode/mips/tables/mips.idx_special3.bin
Normal file
Binary file not shown.
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// rexcode · Brendan Punsky (dotbmp@github), original author
|
||||
|
||||
package main
|
||||
|
||||
// =============================================================================
|
||||
@@ -31,14 +33,15 @@ main :: proc() {
|
||||
|
||||
count := 0
|
||||
for mn in m.Mnemonic {
|
||||
for f in m.ENCODING_TABLE[mn] {
|
||||
_run := m.ENCODE_RUNS[u16(mn)]
|
||||
for f in m.ENCODE_FORMS[_run.start:][:_run.count] {
|
||||
b3 := u8((f.bits >> 24) & 0xFF)
|
||||
b2 := u8((f.bits >> 16) & 0xFF)
|
||||
b1 := u8((f.bits >> 8) & 0xFF)
|
||||
b0 := u8( f.bits & 0xFF)
|
||||
// MIPS big-endian byte order: most-significant byte first
|
||||
fmt.sbprintf(&hex_buf, "0x%02x,0x%02x,0x%02x,0x%02x\n", b3, b2, b1, b0)
|
||||
fmt.sbprintf(&meta_buf, "%v\t%08x\t%08x\t%v\n", mn, f.bits, f.mask, f.isa)
|
||||
fmt.sbprintf(&meta_buf, "%v\t%08x\t%08x\t%v\n", mn, f.bits, f.mask, f.feature)
|
||||
count += 1
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user