From be65df38c179c4400694692de80c238888efb5f4 Mon Sep 17 00:00:00 2001 From: Brendan Punsky Date: Fri, 26 Jun 2026 09:41:27 -0400 Subject: [PATCH] rexcode/ir/spirv: decoder + byte-exact round-trip The inverse of the encoder: read the header (detecting endianness from the magic word), walk the instruction stream, and lower each instruction by opcode back into the structured Module -- the ir core (types/constants/globals/ functions, the generic table-driven operation decode), the SPIR-V sections (preamble / debug / annotations), and the flat space into the side id tables (with an id->Type_Ref map so type-naming operands recover TYPE operands). Single allocator pass (context.allocator), one reused per-instruction word scratch buffer. Validated: encode -> decode -> re-encode is byte-exact on the void compute main module (116 bytes; caps/types/functions/blocks/ops/ids/bound all recovered). Same gaps as the encoder (OpFunctionParameter, ARRAY/bool, enum-parameter operands); big-endian sources a later refinement. --- core/rexcode/ir/spirv/decoder.odin | 327 +++++++++++++++++++++++++++++ 1 file changed, 327 insertions(+) create mode 100644 core/rexcode/ir/spirv/decoder.odin diff --git a/core/rexcode/ir/spirv/decoder.odin b/core/rexcode/ir/spirv/decoder.odin new file mode 100644 index 000000000..c75aa5252 --- /dev/null +++ b/core/rexcode/ir/spirv/decoder.odin @@ -0,0 +1,327 @@ +// rexcode ยท Brendan Punsky (dotbmp@github), original author + +package rexcode_spirv + +import "base:intrinsics" +import "core:strings" + +// ============================================================================= +// SECTION: Decoder (SPIR-V word stream -> Module) +// ============================================================================= +// +// The inverse of the encoder: read the 5-word header (detecting endianness from +// the magic), then walk the instruction stream, lowering each instruction by its +// opcode into the structured Module + side sections. Types/constants/globals/ +// functions are recovered into the ir core; the preamble / debug / annotations +// into the SPIR-V sections; the flat space into the side id tables, with an +// id->Type_Ref map so operands that name a type lower to a TYPE operand. +// +// Every slice/string in the returned Module is allocated with `allocator` (set +// as context.allocator for the whole pass). String operands assume little-endian +// byte packing -- our encoder's output; big-endian sources are a later refinement. + +@(private="file") +Decoder :: struct { + data: []u8, + swap: bool, + + scratch: [dynamic]u32, // reused per-instruction operand-word buffer + + // section accumulators + caps: [dynamic]Capability, + exts: [dynamic]string, + ext_imports: [dynamic]Ext_Import, + addressing: Addressing_Model, + memory: Memory_Model, + entry_points: [dynamic]Entry_Point, + exec_modes: [dynamic]Exec_Mode, + names: [dynamic]Name, + strs: [dynamic]Str, + src_lang: u32, + src_ver: u32, + src_file: Id, + decorations: [dynamic]Decoration_Inst, + types: [dynamic]Type, + type_ids: [dynamic]Id, + id_to_type: map[Id]Type_Ref, + constants: [dynamic]Constant, + globals: [dynamic]Global, + global_ids: [dynamic]Id, + functions: [dynamic]Function, + function_ids: [dynamic]Id, + + // in-flight function / block + in_fn: bool, + fn_sig: Type_Ref, + fn_id: Id, + fn_blocks: [dynamic]Block, + have_blk: bool, + blk_id: Id, + blk_ops: [dynamic]Operation, +} + +@(private="file") +rd :: #force_inline proc "contextless" (d: ^Decoder, wi: int) -> u32 { + o := wi * 4 + w := u32(d.data[o]) | (u32(d.data[o + 1]) << 8) | (u32(d.data[o + 2]) << 16) | (u32(d.data[o + 3]) << 24) + return d.swap ? intrinsics.byte_swap(w) : w +} + +// A LiteralString carried in `w`: NUL-terminated UTF-8 packed little-endian into +// words. Returns the (cloned) string and the words it occupied. +@(private="file") +rd_string :: proc(d: ^Decoder, w: []u32) -> (s: string, nwords: int) { + buf: [dynamic]u8 + defer delete(buf) + outer: for word in w { + for b in 0 ..< 4 { + c := u8(word >> uint(b * 8)) + if c == 0 { break outer } + append(&buf, c) + } + } + return strings.clone(string(buf[:])), (len(buf) + 4) / 4 +} + +// An IdRef operand: a known type id lowers to a TYPE operand, anything else to a VALUE. +@(private="file") +id_operand :: proc(d: ^Decoder, w: u32) -> Operand { + if t, is_type := d.id_to_type[Id(w)]; is_type { return op_type(t) } + return op_value(Id(w)) +} + +@(private="file") +is_id_spec :: proc "contextless" (k: Spec_Kind) -> bool { + return k == .IdRef || k == .IdScope || k == .IdMemorySemantics +} + +@(private="file") +tref :: proc(d: ^Decoder, id: u32) -> Type_Ref { + if r, ok := d.id_to_type[Id(id)]; ok { return r } + return TYPE_NONE +} + +@(private="file") +add_type :: proc(d: ^Decoder, id: Id, t: Type) { + d.id_to_type[id] = Type_Ref(len(d.types)) + append(&d.types, t) + append(&d.type_ids, id) +} + +// Decode a function-body operation generically, by its operand layout: the +// result-type/result-id prefix from the leading specs, then one operand per +// remaining spec (Id specs -> entity/type refs, the rest -> integer literals). +@(private="file") +decode_operation :: proc(d: ^Decoder, opcode: Opcode, w: []u32) -> Operation { + op: Operation + op.opcode = u16(opcode) + op.result.id = ID_NONE + run: Spec_Run + if int(opcode) < len(INSTRUCTION_INDEX) { run = INSTRUCTION_INDEX[u16(opcode)] } + + wi, si := 0, 0 + if si < int(run.count) && INSTRUCTION_SPECS[int(run.start) + si].kind == .IdResultType { + op.result.type = tref(d, w[wi]); wi += 1; si += 1 + } + if si < int(run.count) && INSTRUCTION_SPECS[int(run.start) + si].kind == .IdResult { + op.result.id = Id(w[wi]); wi += 1; si += 1 + } + + ops: [dynamic]Operand + for ; si < int(run.count) && wi < len(w); si += 1 { + spec := INSTRUCTION_SPECS[int(run.start) + si] + if spec.quant == .VARIADIC { + for wi < len(w) { + append(&ops, is_id_spec(spec.kind) ? id_operand(d, w[wi]) : op_int(i64(w[wi]))) + wi += 1 + } + } else { + append(&ops, is_id_spec(spec.kind) ? id_operand(d, w[wi]) : op_int(i64(w[wi]))) + wi += 1 + } + } + op.operands = ops[:] + return op +} + +@(private="file") +finish_block :: proc(d: ^Decoder) { + if d.have_blk { + append(&d.fn_blocks, Block{id = d.blk_id, ops = d.blk_ops[:]}) + d.blk_ops = nil + d.have_blk = false + } +} + +@(private="file") +finish_function :: proc(d: ^Decoder) { + finish_block(d) + append(&d.functions, Function{signature = d.fn_sig, blocks = d.fn_blocks[:]}) + append(&d.function_ids, d.fn_id) + d.fn_blocks = nil + d.in_fn = false +} + +@(private="file") +lower :: proc(d: ^Decoder, opcode: Opcode, w: []u32) { + #partial switch opcode { + case .OpCapability: + append(&d.caps, Capability(w[0])) + case .OpExtension: + s, _ := rd_string(d, w); append(&d.exts, s) + case .OpExtInstImport: + s, _ := rd_string(d, w[1:]); append(&d.ext_imports, Ext_Import{Id(w[0]), s}) + case .OpMemoryModel: + d.addressing = Addressing_Model(w[0]); d.memory = Memory_Model(w[1]) + case .OpEntryPoint: + name, nw := rd_string(d, w[2:]) + iface: [dynamic]Id + for j in 2 + nw ..< len(w) { append(&iface, Id(w[j])) } + append(&d.entry_points, Entry_Point{Execution_Model(w[0]), Id(w[1]), name, iface[:]}) + case .OpExecutionMode, .OpExecutionModeId: + operands := make([]u32, len(w) - 2) + for j in 2 ..< len(w) { operands[j - 2] = w[j] } + append(&d.exec_modes, Exec_Mode{Id(w[0]), Execution_Mode(w[1]), operands, opcode == .OpExecutionModeId}) + + case .OpString: + s, _ := rd_string(d, w[1:]); append(&d.strs, Str{Id(w[0]), s}) + case .OpSource: + d.src_lang = w[0]; d.src_ver = len(w) > 1 ? w[1] : 0 + d.src_file = len(w) > 2 ? Id(w[2]) : ID_NONE + case .OpName: + name, _ := rd_string(d, w[1:]); append(&d.names, Name{Id(w[0]), MEMBER_NONE, name}) + case .OpMemberName: + name, _ := rd_string(d, w[2:]); append(&d.names, Name{Id(w[0]), w[1], name}) + case .OpDecorate: + ops := make([]u32, len(w) - 2) + for j in 2 ..< len(w) { ops[j - 2] = w[j] } + append(&d.decorations, Decoration_Inst{Id(w[0]), Decoration(w[1]), MEMBER_NONE, ops}) + case .OpMemberDecorate: + ops := make([]u32, len(w) - 3) + for j in 3 ..< len(w) { ops[j - 3] = w[j] } + append(&d.decorations, Decoration_Inst{Id(w[0]), Decoration(w[2]), w[1], ops}) + + case .OpTypeVoid: add_type(d, Id(w[0]), Type{kind = .VOID}) + case .OpTypeBool: add_type(d, Id(w[0]), Type{kind = .INT, bits = 1}) // ir has no BOOL kind + case .OpTypeInt: add_type(d, Id(w[0]), Type{kind = .INT, bits = u16(w[1]), aux = u16(w[2] & 1)}) + case .OpTypeFloat: add_type(d, Id(w[0]), Type{kind = .FLOAT, bits = u16(w[1])}) + case .OpTypeVector: add_type(d, Id(w[0]), Type{kind = .VECTOR, elem = tref(d, w[1]), count = w[2]}) + case .OpTypePointer: add_type(d, Id(w[0]), Type{kind = .POINTER, aux = u16(w[1]), elem = tref(d, w[2])}) + case .OpTypeStruct: + fields := make([]Type_Ref, len(w) - 1) + for j in 1 ..< len(w) { fields[j - 1] = tref(d, w[j]) } + add_type(d, Id(w[0]), Type{kind = .STRUCT, fields = fields}) + case .OpTypeFunction: + nparam := len(w) - 2 + fields := make([]Type_Ref, nparam + 1) + for j in 0 ..< nparam { fields[j] = tref(d, w[2 + j]) } + fields[nparam] = tref(d, w[1]) // return type last: fields = params ++ [result] + add_type(d, Id(w[0]), Type{kind = .FUNCTION, fields = fields, count = u32(nparam)}) + + case .OpConstant: + c := Constant{result = {Id(w[1]), tref(d, w[0])}, opcode = opcode, value = u64(w[2])} + if len(w) > 3 { c.value |= u64(w[3]) << 32 } + append(&d.constants, c) + case .OpConstantTrue, .OpConstantFalse, .OpConstantNull: + append(&d.constants, Constant{result = {Id(w[1]), tref(d, w[0])}, opcode = opcode}) + case .OpConstantComposite: + elems := make([]Id, len(w) - 2) + for j in 2 ..< len(w) { elems[j - 2] = Id(w[j]) } + append(&d.constants, Constant{result = {Id(w[1]), tref(d, w[0])}, opcode = opcode, elements = elems}) + + case .OpVariable: + if d.in_fn { + if d.have_blk { append(&d.blk_ops, decode_operation(d, opcode, w)) } + } else { + append(&d.globals, Global{type = tref(d, w[0]), init = len(w) > 3 ? Id(w[3]) : ID_NONE}) + append(&d.global_ids, Id(w[1])) + } + + case .OpFunction: + d.in_fn = true + d.fn_id = Id(w[1]); d.fn_sig = tref(d, w[3]) + d.fn_blocks = nil + case .OpLabel: + finish_block(d) + d.have_blk = true; d.blk_id = Id(w[0]); d.blk_ops = nil + case .OpFunctionEnd: + finish_function(d) + + case: + if d.in_fn && d.have_blk { + append(&d.blk_ops, decode_operation(d, opcode, w)) + } + } +} + +// ----------------------------------------------------------------------------- +// Entry point +// ----------------------------------------------------------------------------- + +decode :: proc(data: []u8, m: ^Module, errors: ^[dynamic]Error, allocator := context.allocator) -> (byte_count: u32, ok: bool) { + context.allocator = allocator + + if len(data) < HEADER_WORDS * 4 { + if errors != nil { append(errors, Error{location = 0, code = .BUFFER_TOO_SHORT}) } + return 0, false + } + d := Decoder{data = data} + defer delete(d.scratch) + + // endianness from the magic word + raw := u32(data[0]) | (u32(data[1]) << 8) | (u32(data[2]) << 16) | (u32(data[3]) << 24) + if raw == MAGIC { + d.swap = false + } else if raw == intrinsics.byte_swap(MAGIC) { + d.swap = true + } else { + if errors != nil { append(errors, Error{location = 0, code = .INVALID_OPCODE}) } + return 0, false + } + + m.dataflow = .SSA + m.version = rd(&d, 1) + m.generator = rd(&d, 2) + m.bound = rd(&d, 3) + + wi := HEADER_WORDS + nwords := len(data) / 4 + for wi < nwords { + head := rd(&d, wi) + count := int(head >> 16) + opcode := Opcode(head & 0xFFFF) + if count == 0 || wi + count > nwords { + if errors != nil { append(errors, Error{location = u32(wi * 4), code = .BUFFER_TOO_SHORT}) } + return 0, false + } + clear(&d.scratch) + for j in 0 ..< count - 1 { append(&d.scratch, rd(&d, wi + 1 + j)) } + lower(&d, opcode, d.scratch[:]) + wi += count + } + + m.capabilities = d.caps[:] + m.extensions = d.exts[:] + m.ext_imports = d.ext_imports[:] + m.addressing = d.addressing + m.memory = d.memory + m.entry_points = d.entry_points[:] + m.exec_modes = d.exec_modes[:] + m.decorations = d.decorations[:] + m.debug = Debug{ + source_language = d.src_lang, + source_version = d.src_ver, + source_file = d.src_file, + names = d.names[:], + strings = d.strs[:], + } + m.types = d.types[:] + m.type_ids = d.type_ids[:] + m.constants = d.constants[:] + m.globals = d.globals[:] + m.global_ids = d.global_ids[:] + m.functions = d.functions[:] + m.function_ids = d.function_ids[:] + + return u32(nwords * 4), true +}