mirror of
https://github.com/odin-lang/Odin.git
synced 2026-07-17 13:11:07 +00:00
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:
@@ -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))
|
||||
}
|
||||
|
||||
8862
core/rexcode/ir/spirv/builders_gen.odin
Normal file
8862
core/rexcode/ir/spirv/builders_gen.odin
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
@@ -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) }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user