From dfbb0abed3cdbc5d23321e5d6af03d97c49c00b1 Mon Sep 17 00:00:00 2001 From: Brendan Punsky Date: Fri, 26 Jun 2026 13:23:55 -0400 Subject: [PATCH] rexcode/ir/spirv: typed instruction builders (foundation) Two layers, the SPIR-V analog of an ISA's mnemonic builders: - low level: inst_(buf, ...) -> Operation, stateless + alloc-free, the caller owns the operand backing (SPIR-V operands are a slice, so unlike an ISA's inline [4]Operand they can't be owned by the return value). - high level: a Builder owning operand storage + a result- allocator; iadd/load/store/call/variable/ret/ret_value append to the current block and return the new . Hand-written here for a representative slice (Id / no-result / variadic / enum operands) to fix the pattern; tablegen will generate the full per-opcode set. Validated: a builder-made function body round-trips byte-exact. --- core/rexcode/ir/spirv/builder.odin | 148 +++++++++++++++++++++++++++++ 1 file changed, 148 insertions(+) create mode 100644 core/rexcode/ir/spirv/builder.odin diff --git a/core/rexcode/ir/spirv/builder.odin b/core/rexcode/ir/spirv/builder.odin new file mode 100644 index 000000000..c115adb15 --- /dev/null +++ b/core/rexcode/ir/spirv/builder.odin @@ -0,0 +1,148 @@ +// rexcode ยท Brendan Punsky (dotbmp@github), original author + +package rexcode_spirv + +import "base:runtime" + +// ============================================================================= +// SECTION: Builders (typed instruction construction) +// ============================================================================= +// +// Two layers, the SPIR-V analog of an ISA's mnemonic builders: +// +// * Low level -- `inst_(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 +// s; `b->iadd(ty, a, c)` appends to the current block and returns the new +// . 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 s) +// ----------------------------------------------------------------------------- + +// Accumulates operations into the current block. `next_id` hands out fresh result +// 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`. +Builder :: struct { + alloc: runtime.Allocator, + next_id: u32, + ops: [dynamic]Operation, // current block +} + +@(require_results) +builder_make :: proc(first_id: u32 = 1, allocator := context.allocator) -> Builder { + b: Builder + b.alloc = allocator + b.next_id = first_id + b.ops.allocator = allocator + return b +} + +// Allocate a fresh result . +@(require_results) +alloc_id :: proc(b: ^Builder) -> Id { + id := Id(b.next_id) + b.next_id += 1 + return id +} + +// Detach the accumulated operations as a block body (and reset for the next block). +@(require_results) +take_block :: proc(b: ^Builder, label: Id) -> Block { + blk := Block{id = label, ops = b.ops[:]} + b.ops = nil + b.ops.allocator = b.alloc + return blk +} + +@(private="file") +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)) +}