rexcode/ir/spirv: generate the typed builders (both layers) from the grammar

Extend tablegen to emit builders_gen.odin -- 723 opcodes get a low-level
inst_<OpName>(buf, ...) constructor and a high-level Builder method, mapping each
grammar operand to a typed param (IdResultType->Type_Ref, IdResult->auto-allocated
Id, IdRef->Id, LiteralInteger->i64, ValueEnum/BitEnum->the typed enum, trailing
IdRef* -> []Id). builder.odin keeps just the hand-written Builder infrastructure.

Skipped (stay hand-writable, like an ISA's can_generate_builder): operands that
aren't simple typed params yet (optional, Pair* composites, LiteralString),
type-declaration opcodes (those are ir.Type), and verbs colliding with Odin
keywords/builtins (return_, switch_, size_of, ...).

Validated: a function body built via i_add / return_ encodes + round-trips
byte-exact (builder_made test) -> 10 passed.
This commit is contained in:
Brendan Punsky
2026-06-26 13:32:07 -04:00
committed by Flāvius
parent dfbb0abed3
commit 6665d5b552
4 changed files with 9059 additions and 95 deletions

View File

@@ -5,75 +5,28 @@ package rexcode_spirv
import "base:runtime"
// =============================================================================
// SECTION: Builders (typed instruction construction)
// SECTION: Builder (typed instruction construction -- infrastructure)
// =============================================================================
//
// Two layers, the SPIR-V analog of an ISA's mnemonic builders:
// The SPIR-V analog of an ISA's mnemonic builders comes in two layers, both
// GENERATED per opcode in builders_gen.odin (by tablegen/gen.odin):
//
// * Low level -- `inst_<OpName>(buf, ...)`: a stateless, allocation-free typed
// constructor returning an Operation. The caller owns `buf`, the operand
// backing store (SPIR-V operands are a slice, so unlike an ISA's inline
// [4]Operand they cannot be owned by the returned value).
//
// * High level -- a `Builder` that owns operand storage and allocates result
// <id>s; `b->iadd(ty, a, c)` appends to the current block and returns the new
// <id>. The ergonomic SSA-construction API.
// * High level -- methods on the `Builder` below that own operand storage and
// allocate result <id>s; `i_add(b, ty, a, c)` appends to the current block
// and returns the new <id>. The ergonomic SSA-construction API.
//
// This file hand-writes a representative slice (covering Id / no-result / variadic
// / enum operands) to fix the pattern; `tablegen/gen.odin` will generate the full
// per-opcode set for both layers from the grammar.
// -----------------------------------------------------------------------------
// Low-level constructors (caller owns `buf`)
// -----------------------------------------------------------------------------
inst_OpIAdd :: #force_inline proc "contextless" (buf: []Operand, result_type: Type_Ref, result: Id, operand_1, operand_2: Id) -> Operation {
buf[0] = op_value(operand_1)
buf[1] = op_value(operand_2)
return Operation{opcode = u16(Opcode.OpIAdd), result = {result, result_type}, operands = buf[:2]}
}
inst_OpLoad :: #force_inline proc "contextless" (buf: []Operand, result_type: Type_Ref, result, pointer: Id) -> Operation {
buf[0] = op_value(pointer)
return Operation{opcode = u16(Opcode.OpLoad), result = {result, result_type}, operands = buf[:1]}
}
inst_OpStore :: #force_inline proc "contextless" (buf: []Operand, pointer, object: Id) -> Operation {
buf[0] = op_value(pointer)
buf[1] = op_value(object)
return Operation{opcode = u16(Opcode.OpStore), result = {id = ID_NONE}, operands = buf[:2]}
}
inst_OpReturn :: #force_inline proc "contextless" () -> Operation {
return Operation{opcode = u16(Opcode.OpReturn), result = {id = ID_NONE}}
}
inst_OpReturnValue :: #force_inline proc "contextless" (buf: []Operand, value: Id) -> Operation {
buf[0] = op_value(value)
return Operation{opcode = u16(Opcode.OpReturnValue), result = {id = ID_NONE}, operands = buf[:1]}
}
// A variadic operand (the call arguments) is a trailing slice.
inst_OpFunctionCall :: #force_inline proc "contextless" (buf: []Operand, result_type: Type_Ref, result, function: Id, arguments: []Id) -> Operation {
buf[0] = op_value(function)
for a, i in arguments { buf[1 + i] = op_value(a) }
return Operation{opcode = u16(Opcode.OpFunctionCall), result = {result, result_type}, operands = buf[:1 + len(arguments)]}
}
// An enum operand (the storage class) becomes a typed parameter.
inst_OpVariable :: #force_inline proc "contextless" (buf: []Operand, result_type: Type_Ref, result: Id, storage_class: Storage_Class) -> Operation {
buf[0] = op_int(i64(storage_class))
return Operation{opcode = u16(Opcode.OpVariable), result = {result, result_type}, operands = buf[:1]}
}
// -----------------------------------------------------------------------------
// High-level builder (owns storage, allocates <id>s)
// -----------------------------------------------------------------------------
// This file holds only the hand-written Builder infrastructure the generated
// high-level methods build on.
// Accumulates operations into the current block. `next_id` hands out fresh result
// <id>s; operand backing for each op is allocated from `alloc` (stable, unlike a
// shared growing pool). Drive a function with begin_block / end into Blocks, and
// set Module.bound from `next_id`.
// shared growing pool). Build a function by emitting ops, then take_block into a
// Block; set Module.bound from `next_id`.
Builder :: struct {
alloc: runtime.Allocator,
next_id: u32,
@@ -106,43 +59,7 @@ take_block :: proc(b: ^Builder, label: Id) -> Block {
return blk
}
@(private="file")
// Stable per-operation operand backing; used by the generated high-level methods.
opbuf :: #force_inline proc(b: ^Builder, n: int) -> []Operand {
return make([]Operand, n, b.alloc)
}
iadd :: proc(b: ^Builder, result_type: Type_Ref, a, c: Id) -> Id {
r := alloc_id(b)
append(&b.ops, inst_OpIAdd(opbuf(b, 2), result_type, r, a, c))
return r
}
load :: proc(b: ^Builder, result_type: Type_Ref, pointer: Id) -> Id {
r := alloc_id(b)
append(&b.ops, inst_OpLoad(opbuf(b, 1), result_type, r, pointer))
return r
}
store :: proc(b: ^Builder, pointer, object: Id) {
append(&b.ops, inst_OpStore(opbuf(b, 2), pointer, object))
}
call :: proc(b: ^Builder, result_type: Type_Ref, function: Id, arguments: []Id) -> Id {
r := alloc_id(b)
append(&b.ops, inst_OpFunctionCall(opbuf(b, 1 + len(arguments)), result_type, r, function, arguments))
return r
}
variable :: proc(b: ^Builder, result_type: Type_Ref, storage_class: Storage_Class) -> Id {
r := alloc_id(b)
append(&b.ops, inst_OpVariable(opbuf(b, 1), result_type, r, storage_class))
return r
}
ret :: proc(b: ^Builder) {
append(&b.ops, inst_OpReturn())
}
ret_value :: proc(b: ^Builder, value: Id) {
append(&b.ops, inst_OpReturnValue(opbuf(b, 1), value))
}

File diff suppressed because it is too large Load Diff

View File

@@ -18,6 +18,7 @@ GRAMMAR_PATH :: #directory + "/spirv.core.grammar.json"
OPCODES_OUT :: #directory + "/../opcodes.odin"
KINDS_OUT :: #directory + "/../operand_kinds.odin"
TABLE_OUT :: #directory + "/../encoding_table.odin"
BUILDERS_OUT :: #directory + "/../builders_gen.odin"
HDR ::
`// rexcode · Brendan Punsky (dotbmp@github), original author
@@ -49,7 +50,8 @@ main :: proc() {
n_op := gen_opcodes(insts)
n_k := gen_operand_kinds(kinds)
n_sp := gen_encoding_table(insts, kinds)
fmt.printfln("spirv tablegen: %d opcodes, %d operand kinds, %d operand specs", n_op, n_k, n_sp)
n_b := gen_builders(insts, kinds)
fmt.printfln("spirv tablegen: %d opcodes, %d operand kinds, %d operand specs, %d builders", n_op, n_k, n_sp, n_b)
}
write_file :: proc(path: string, sb: ^strings.Builder) {
@@ -261,6 +263,168 @@ gen_encoding_table :: proc(insts: json.Array, kinds: json.Array) -> int {
return len(specs)
}
// --- builders_gen.odin : typed per-opcode constructors (low level) + Builder
// methods (high level). Opcodes whose operands aren't yet expressible as
// simple typed params (optional, Pair* composites, LiteralString, ...) are
// skipped -- those stay hand-written, like an ISA's can_generate_builder. ---
@(private="file")
B_Param :: struct { typ: string, emit: string }
gen_builders :: proc(insts: json.Array, kinds: json.Array) -> int {
category: map[string]string; defer delete(category)
for kv in kinds {
k := kv.(json.Object)
category[string(k["kind"].(json.String))] = string(k["category"].(json.String))
}
sb := strings.builder_make(); defer strings.builder_destroy(&sb)
strings.write_string(&sb, HDR)
seen: map[i64]bool; defer delete(seen)
verb_seen: map[string]bool; defer delete(verb_seen)
count := 0
for v in insts {
inst := v.(json.Object)
op := i64(inst["opcode"].(json.Integer))
if op in seen { continue }
seen[op] = true
opname := string(inst["opname"].(json.String))
// Type-declaration opcodes are ir.Type, not Operations -- skip them (their
// verbs would also collide with the re-exported ir.type_* constructors).
if c, hc := inst["class"]; hc && string(c.(json.String)) == "Type-Declaration" { continue }
has_rt, has_r, variadic, ok := false, false, false, true
fixed: [dynamic]B_Param; defer delete(fixed)
if ops, hop := inst["operands"]; hop {
oparr := ops.(json.Array)
for ov, oi in oparr {
o := ov.(json.Object)
kind := string(o["kind"].(json.String))
quant := ""
if q, hq := o["quantifier"]; hq { quant = string(q.(json.String)) }
switch {
case kind == "IdResultType": has_rt = true
case kind == "IdResult": has_r = true
case quant == "*":
if oi != len(oparr) - 1 || kind != "IdRef" { ok = false } else { variadic = true }
case quant == "?":
ok = false // optional operands not expressible yet
case kind == "IdRef" || kind == "IdScope" || kind == "IdMemorySemantics":
append(&fixed, B_Param{"Id", "id"})
case kind == "LiteralInteger":
append(&fixed, B_Param{"i64", "int"})
case category[kind] == "ValueEnum":
append(&fixed, B_Param{snake(kind), "venum"})
case category[kind] == "BitEnum":
append(&fixed, B_Param{snake(kind), "benum"})
case:
ok = false // Composite / LiteralString / context-dependent number
}
if !ok { break }
}
}
if !ok { continue }
verb := verb_name(opname)
if verb in verb_seen { continue } // skip a name collision rather than shadow
verb_seen[verb] = true
gen_one_builder(&sb, opname, verb, has_rt, has_r, fixed[:], variadic)
count += 1
}
write_file(BUILDERS_OUT, &sb)
return count
}
@(private="file")
gen_one_builder :: proc(sb: ^strings.Builder, opname, verb: string, has_rt, has_r: bool, fixed: []B_Param, variadic: bool) {
nfix := len(fixed)
has_buf := nfix > 0 || variadic
// --- low-level constructor ---
ll: [dynamic]string; defer delete(ll)
if has_buf { append(&ll, "buf: []Operand") }
if has_rt { append(&ll, "result_type: Type_Ref") }
if has_r { append(&ll, "result: Id") }
for p, i in fixed { append(&ll, fmt.tprintf("op%d: %s", i + 1, p.typ)) }
if variadic { append(&ll, "args: []Id") }
fmt.sbprintf(sb, "\ninst_%s :: #force_inline proc \"contextless\" (%s) -> Operation {{\n",
opname, join(ll[:]))
for p, i in fixed {
switch p.emit {
case "id": fmt.sbprintf(sb, "\tbuf[%d] = op_value(op%d)\n", i, i + 1)
case "int": fmt.sbprintf(sb, "\tbuf[%d] = op_int(op%d)\n", i, i + 1)
case "venum": fmt.sbprintf(sb, "\tbuf[%d] = op_int(i64(op%d))\n", i, i + 1)
case "benum": fmt.sbprintf(sb, "\tbuf[%d] = op_int(i64(transmute(u32)op%d))\n", i, i + 1)
}
}
if variadic { fmt.sbprintf(sb, "\tfor v, i in args {{ buf[%d + i] = op_value(v) }}\n", nfix) }
result_str := has_r ? (has_rt ? "{result, result_type}" : "{id = result}") : "{id = ID_NONE}"
operands_str := ""
if has_buf { operands_str = variadic ? fmt.tprintf(", operands = buf[:%d + len(args)]", nfix) : fmt.tprintf(", operands = buf[:%d]", nfix) }
fmt.sbprintf(sb, "\treturn Operation{{opcode = u16(Opcode.%s), result = %s%s}}\n}}\n", opname, result_str, operands_str)
// --- high-level Builder method ---
hl: [dynamic]string; defer delete(hl)
append(&hl, "b: ^Builder")
if has_rt { append(&hl, "result_type: Type_Ref") }
for p, i in fixed { append(&hl, fmt.tprintf("op%d: %s", i + 1, p.typ)) }
if variadic { append(&hl, "args: []Id") }
call: [dynamic]string; defer delete(call)
if has_buf { append(&call, variadic ? fmt.tprintf("opbuf(b, %d + len(args))", nfix) : fmt.tprintf("opbuf(b, %d)", nfix)) }
if has_rt { append(&call, "result_type") }
if has_r { append(&call, "r") }
for _, i in fixed { append(&call, fmt.tprintf("op%d", i + 1)) }
if variadic { append(&call, "args") }
fmt.sbprintf(sb, "\n%s :: proc(%s)%s {{\n", verb, join(hl[:]), has_r ? " -> Id" : "")
if has_r { strings.write_string(sb, "\tr := alloc_id(b)\n") }
fmt.sbprintf(sb, "\tappend(&b.ops, inst_%s(%s))\n", opname, join(call[:]))
if has_r { strings.write_string(sb, "\treturn r\n") }
strings.write_string(sb, "}\n")
}
// The high-level verb: the OpName minus "Op", snake-cased + lowercased, with a
// trailing "_" if it collides with an Odin keyword (return, switch, ...).
@(private="file")
verb_name :: proc(opname: string) -> string {
s := opname
if len(s) > 2 && s[0] == 'O' && s[1] == 'p' { s = s[2:] }
out := to_lower(snake(s))
return is_keyword(out) ? strings.concatenate({out, "_"}) : out
}
@(private="file")
to_lower :: proc(s: string) -> string {
b := make([]u8, len(s))
for i in 0 ..< len(s) { b[i] = (s[i] >= 'A' && s[i] <= 'Z') ? s[i] + 32 : s[i] }
return string(b)
}
// Odin keywords AND builtins a generated verb must not shadow (e.g. OpSizeOf ->
// size_of would shadow the size_of builtin and break the package).
@(private="file")
is_keyword :: proc(s: string) -> bool {
switch s {
case "return", "switch", "in", "map", "for", "if", "else", "when", "case",
"struct", "enum", "union", "proc", "import", "package", "using", "defer",
"context", "distinct", "bit_set", "matrix", "or_else", "or_return",
"size_of", "align_of", "offset_of", "type_of", "typeid_of", "type_info_of",
"len", "cap", "min", "max", "abs", "clamp", "swizzle", "make", "new",
"delete", "append", "copy", "assert", "transmute", "cast", "raw_data":
return true
}
return false
}
@(private="file")
join :: proc(parts: []string) -> string {
return strings.join(parts, ", ")
}
// -----------------------------------------------------------------------------
// helpers
// -----------------------------------------------------------------------------

View File

@@ -274,6 +274,27 @@ main :: proc() {
roundtrip("image_type", m)
}
// (10) a function body constructed with the generated high-level builders
// (i_add / return_), then round-tripped.
{
b := spirv.builder_make(8)
spirv.i_add(&b, spirv.Type_Ref(1), spirv.Id(5), spirv.Id(6)) // %8 = OpIAdd %int %5 %6
spirv.return_(&b)
blk := spirv.take_block(&b, spirv.Id(7))
m := spirv.make_module()
m.capabilities = {.Shader}
m.types = {{kind = .VOID}, {kind = .INT, bits = 32, aux = 1}, {kind = .FUNCTION, fields = {spirv.Type_Ref(0)}, count = 0}}
m.type_ids = {spirv.Id(1), spirv.Id(2), spirv.Id(3)}
m.constants = {
{result = {spirv.Id(5), spirv.Type_Ref(1)}, opcode = .OpConstant, value = 10},
{result = {spirv.Id(6), spirv.Type_Ref(1)}, opcode = .OpConstant, value = 20},
}
m.functions = {{signature = spirv.Type_Ref(2), blocks = {blk}}}
m.function_ids = {spirv.Id(4)}
m.bound = b.next_id
roundtrip("builder_made", m)
}
fmt.printf("\n%d passed, %d failed\n", ok_count, fail_count)
if fail_count > 0 { os.exit(1) }
}