rexcode/x86: generate the encode-recipe table (drop the @init interim)

Move ENCODE_RECIPES from a startup-built (@init) static table in the library to
a generated, #loaded table like every other one. The tablegen runs
form_to_recipe over every form, serializes to x86.encode_recipes.bin, and the
loader #loads it -- no @init, no allocation; the forms remain the single source
of truth and the recipe is derived from them.

- tablegen/gen.odin: ENCODE_RECIPES added to the BLOBS manifest (which drives
  both the Stage B serializer and the loader's #load); emit the generated-package
  global plus an @init that fills it from ENCODE_FORMS via lib.form_to_recipe.
- encoder_recipe.odin: drop the @init, the static storage, and the ENCODE_RECIPES
  declaration. form_to_recipe stays -- now solely the tablegen-time derivation.
- tables.odin, generated/{encode_tables,writer}.odin: regenerated.
- tables/x86.encode_recipes.bin: the generated blob (28260 bytes = 2355 x 12).

No behavior change: the recipes are identical to the @init ones. gen / builders /
check / test / idempotent green (2282 cases, 22 byte-stable artifacts); encode
perf unchanged.
This commit is contained in:
Brendan Punsky
2026-06-26 04:22:03 -04:00
committed by Flāvius
parent 19bb9070e1
commit 3d852158bc
6 changed files with 49 additions and 40 deletions

View File

@@ -18,10 +18,9 @@ package rexcode_x86
// marked `eligible = false` and falls back to the existing interpreter, which
// stays the source of truth for correctness.
//
// NOTE(interim): ENCODE_RECIPES is currently built at startup from the #loaded
// ENCODE_FORMS (static storage, no heap). Once the fast path is validated this
// moves into the table generator -- serialized and #loaded like every other
// table, with no @init.
// ENCODE_RECIPES is produced by the tablegen (form_to_recipe over every form)
// and #loaded from x86.encode_recipes.bin like every other table -- see the note
// by form_to_recipe below.
Form_Recipe :: struct {
prefix: u8, // mandatory legacy prefix emitted before REX (0 = none)
@@ -121,23 +120,10 @@ form_to_recipe :: proc "contextless" (enc: ^Encoding) -> (r: Form_Recipe) {
return
}
// -----------------------------------------------------------------------------
// Interim recipe table: built once at startup from the #loaded forms into static
// storage (no heap). ENCODE_RECIPES is parallel to ENCODE_FORMS.
// -----------------------------------------------------------------------------
@(private) ENCODE_RECIPE_CAP :: 4096
@(private) encode_recipes_storage: [ENCODE_RECIPE_CAP]Form_Recipe
ENCODE_RECIPES: []Form_Recipe
@(init)
build_encode_recipes :: proc "contextless" () {
n := min(len(ENCODE_FORMS), ENCODE_RECIPE_CAP)
for i in 0..<n {
encode_recipes_storage[i] = form_to_recipe(&ENCODE_FORMS[i])
}
ENCODE_RECIPES = encode_recipes_storage[:n]
}
// ENCODE_RECIPES (parallel to ENCODE_FORMS) is generated, not built here: the
// tablegen runs form_to_recipe over every form, serializes the result to
// x86.encode_recipes.bin, and tables.odin #loads it. So form_to_recipe above is
// a tablegen-time helper -- it is not called on the encode hot path.
@(private)
op_is_spl :: #force_inline proc "contextless" (op: ^Operand) -> bool {

View File

@@ -44,8 +44,9 @@ Blob :: struct { global, file, typ: string }
@(rodata)
BLOBS := [?]Blob{
{"ENCODE_FORMS", "x86.encode_forms.bin", "Encoding"},
{"ENCODE_RUNS", "x86.encode_runs.bin", "Encode_Run"},
{"ENCODE_FORMS", "x86.encode_forms.bin", "Encoding"},
{"ENCODE_RUNS", "x86.encode_runs.bin", "Encode_Run"},
{"ENCODE_RECIPES", "x86.encode_recipes.bin", "Form_Recipe"},
{"MODRM_TABLE", "x86.modrm.bin", "ModRM_Info"},
{"SIB_TABLE", "x86.sib.bin", "SIB_Info"},
{"LEGACY_DECODE_ENTRIES", "x86.legacy.bin", "Decode_Entry"},
@@ -123,6 +124,18 @@ emit_encode_tables :: proc() {
start += n
}
strings.write_string(&sb, "}\n")
// Emit descriptor per form: the flattened recipe the encoder replays, derived
// from the forms via the library's form_to_recipe (single source of truth).
// Filled here at startup and serialized to x86.encode_recipes.bin; the loader
// #loads it, so the library itself needs no @init.
strings.write_string(&sb, "\n// Emit descriptor per form (derived from ENCODE_FORMS via lib.form_to_recipe).\n")
fmt.sbprintfln(&sb, "ENCODE_RECIPES: [%d]lib.Form_Recipe", total_forms())
strings.write_string(&sb, "@(init) _fill_recipes :: proc \"contextless\" () {\n")
strings.write_string(&sb, "\tfor i in 0 ..< len(ENCODE_FORMS) {\n")
strings.write_string(&sb, "\t\tENCODE_RECIPES[i] = lib.form_to_recipe(&ENCODE_FORMS[i])\n")
strings.write_string(&sb, "\t}\n}\n")
emit_file(DIR_GEN + "encode_tables.odin", &sb)
}

View File

@@ -4720,3 +4720,11 @@ ENCODE_RUNS := [lib.Mnemonic]lib.Encode_Run{
.RDRAND = { 2349, 3},
.RDSEED = { 2352, 3},
}
// Emit descriptor per form (derived from ENCODE_FORMS via lib.form_to_recipe).
ENCODE_RECIPES: [2355]lib.Form_Recipe
@(init) _fill_recipes :: proc "contextless" () {
for i in 0 ..< len(ENCODE_FORMS) {
ENCODE_RECIPES[i] = lib.form_to_recipe(&ENCODE_FORMS[i])
}
}

View File

@@ -24,6 +24,7 @@ w :: proc(file: string, data: []u8) {
main :: proc() {
w(TABLES + "x86.encode_forms.bin", raw(&ENCODE_FORMS, size_of(ENCODE_FORMS)))
w(TABLES + "x86.encode_runs.bin", raw(&ENCODE_RUNS, size_of(ENCODE_RUNS)))
w(TABLES + "x86.encode_recipes.bin", raw(&ENCODE_RECIPES, size_of(ENCODE_RECIPES)))
w(TABLES + "x86.modrm.bin", raw(&MODRM_TABLE, size_of(MODRM_TABLE)))
w(TABLES + "x86.sib.bin", raw(&SIB_TABLE, size_of(SIB_TABLE)))
w(TABLES + "x86.legacy.bin", raw(&LEGACY_DECODE_ENTRIES, size_of(LEGACY_DECODE_ENTRIES)))

View File

@@ -76,23 +76,24 @@ Decode_Index :: struct {
// Loaded tables (rodata, embedded from tables/*.bin at compile time)
// -----------------------------------------------------------------------------
@(rodata) ENCODE_FORMS := #load("tables/x86.encode_forms.bin", []Encoding)
@(rodata) ENCODE_RUNS := #load("tables/x86.encode_runs.bin", []Encode_Run)
@(rodata) MODRM_TABLE := #load("tables/x86.modrm.bin", []ModRM_Info)
@(rodata) SIB_TABLE := #load("tables/x86.sib.bin", []SIB_Info)
@(rodata) LEGACY_DECODE_ENTRIES := #load("tables/x86.legacy.bin", []Decode_Entry)
@(rodata) VEX_DECODE_ENTRIES := #load("tables/x86.vex.bin", []VEX_Decode_Entry)
@(rodata) EVEX_DECODE_ENTRIES := #load("tables/x86.evex.bin", []VEX_Decode_Entry)
@(rodata) DECODE_INDEX_LEGACY := #load("tables/x86.idx_legacy.bin", []Decode_Index)
@(rodata) DECODE_INDEX_ESC_0F := #load("tables/x86.idx_0f.bin", []Decode_Index)
@(rodata) DECODE_INDEX_ESC_0F38 := #load("tables/x86.idx_0f38.bin", []Decode_Index)
@(rodata) DECODE_INDEX_ESC_0F3A := #load("tables/x86.idx_0f3a.bin", []Decode_Index)
@(rodata) VEX_INDEX_0F := #load("tables/x86.vex_idx_0f.bin", []Decode_Index)
@(rodata) VEX_INDEX_0F38 := #load("tables/x86.vex_idx_0f38.bin", []Decode_Index)
@(rodata) VEX_INDEX_0F3A := #load("tables/x86.vex_idx_0f3a.bin", []Decode_Index)
@(rodata) EVEX_INDEX_0F := #load("tables/x86.evex_idx_0f.bin", []Decode_Index)
@(rodata) EVEX_INDEX_0F38 := #load("tables/x86.evex_idx_0f38.bin", []Decode_Index)
@(rodata) EVEX_INDEX_0F3A := #load("tables/x86.evex_idx_0f3a.bin", []Decode_Index)
@(rodata) ENCODE_FORMS := #load("tables/x86.encode_forms.bin", []Encoding)
@(rodata) ENCODE_RUNS := #load("tables/x86.encode_runs.bin", []Encode_Run)
@(rodata) ENCODE_RECIPES := #load("tables/x86.encode_recipes.bin", []Form_Recipe)
@(rodata) MODRM_TABLE := #load("tables/x86.modrm.bin", []ModRM_Info)
@(rodata) SIB_TABLE := #load("tables/x86.sib.bin", []SIB_Info)
@(rodata) LEGACY_DECODE_ENTRIES := #load("tables/x86.legacy.bin", []Decode_Entry)
@(rodata) VEX_DECODE_ENTRIES := #load("tables/x86.vex.bin", []VEX_Decode_Entry)
@(rodata) EVEX_DECODE_ENTRIES := #load("tables/x86.evex.bin", []VEX_Decode_Entry)
@(rodata) DECODE_INDEX_LEGACY := #load("tables/x86.idx_legacy.bin", []Decode_Index)
@(rodata) DECODE_INDEX_ESC_0F := #load("tables/x86.idx_0f.bin", []Decode_Index)
@(rodata) DECODE_INDEX_ESC_0F38 := #load("tables/x86.idx_0f38.bin", []Decode_Index)
@(rodata) DECODE_INDEX_ESC_0F3A := #load("tables/x86.idx_0f3a.bin", []Decode_Index)
@(rodata) VEX_INDEX_0F := #load("tables/x86.vex_idx_0f.bin", []Decode_Index)
@(rodata) VEX_INDEX_0F38 := #load("tables/x86.vex_idx_0f38.bin", []Decode_Index)
@(rodata) VEX_INDEX_0F3A := #load("tables/x86.vex_idx_0f3a.bin", []Decode_Index)
@(rodata) EVEX_INDEX_0F := #load("tables/x86.evex_idx_0f.bin", []Decode_Index)
@(rodata) EVEX_INDEX_0F38 := #load("tables/x86.evex_idx_0f38.bin", []Decode_Index)
@(rodata) EVEX_INDEX_0F3A := #load("tables/x86.evex_idx_0f3a.bin", []Decode_Index)
// -----------------------------------------------------------------------------
// Accessors

Binary file not shown.