From fe27dcdbd49ccd14d65201c78d8bb2053f1cf0df Mon Sep 17 00:00:00 2001 From: Brendan Punsky Date: Fri, 26 Jun 2026 11:45:35 -0400 Subject: [PATCH] rexcode/ir: add Type_Kind.BOOL + array length; SPIR-V bool/array round-trip Extend the shared type model so SPIR-V's OpTypeBool and OpTypeArray (whose length is a constant , not a literal) lower cleanly: - ir.Type_Kind gains BOOL (a distinct boolean; LLVM i1 will use it too). - ir.Type gains len_ref: Id -- an ARRAY length carried as a constant (alongside the existing literal count for dialects with literal lengths). - type_bool / type_array constructors. SPIR-V codec: OpTypeBool <-> Type{.BOOL}; OpTypeArray <-> Type{.ARRAY, elem, len_ref}. Test bool_and_array round-trips byte-exact -> 6 passed. NOTE: a spec-valid module orders an array's length constant before the array type; the codec round-trips the shape byte-exact regardless, but emitting the types/constants section in dependency order is a follow-up. --- core/rexcode/ir/spirv/decoder.odin | 3 ++- core/rexcode/ir/spirv/encoder.odin | 4 ++- .../ir/spirv/tests/roundtrip_test.odin | 18 +++++++++++++ core/rexcode/ir/types.odin | 26 ++++++++++++------- 4 files changed, 40 insertions(+), 11 deletions(-) diff --git a/core/rexcode/ir/spirv/decoder.odin b/core/rexcode/ir/spirv/decoder.odin index 7dcfd27ba..0fc7f4df2 100644 --- a/core/rexcode/ir/spirv/decoder.odin +++ b/core/rexcode/ir/spirv/decoder.odin @@ -213,7 +213,8 @@ lower :: proc(d: ^Decoder, opcode: Opcode, w: []u32) { 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 .OpTypeBool: add_type(d, Id(w[0]), Type{kind = .BOOL}) + case .OpTypeArray: add_type(d, Id(w[0]), Type{kind = .ARRAY, elem = tref(d, w[1]), len_ref = Id(w[2])}) 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]}) diff --git a/core/rexcode/ir/spirv/encoder.odin b/core/rexcode/ir/spirv/encoder.odin index 10ec5c3b6..996d75918 100644 --- a/core/rexcode/ir/spirv/encoder.odin +++ b/core/rexcode/ir/spirv/encoder.odin @@ -177,9 +177,11 @@ emit_types :: proc "contextless" (w: ^Writer, m: ^Module) { op: Opcode switch t.kind { case .VOID: op = .OpTypeVoid + case .BOOL: op = .OpTypeBool case .INT: w_word(w, u32(t.bits)); w_word(w, u32(t.aux & 1)); op = .OpTypeInt case .FLOAT: w_word(w, u32(t.bits)); op = .OpTypeFloat case .VECTOR: w_id(w, tid(m, t.elem)); w_word(w, t.count); op = .OpTypeVector + case .ARRAY: w_id(w, tid(m, t.elem)); w_id(w, t.len_ref); op = .OpTypeArray // length: a constant case .POINTER: w_word(w, u32(t.aux)); w_id(w, tid(m, t.elem)); op = .OpTypePointer case .STRUCT: for f in t.fields { w_id(w, tid(m, f)) } @@ -188,7 +190,7 @@ emit_types :: proc "contextless" (w: ^Writer, m: ^Module) { w_id(w, tid(m, t.fields[t.count])) // return type for pi in 0 ..< int(t.count) { w_id(w, tid(m, t.fields[pi])) } op = .OpTypeFunction - case .ARRAY, .OPAQUE, .REF: + case .OPAQUE, .REF: w.pos = s // rewind the placeholder; not yet lowered continue } diff --git a/core/rexcode/ir/spirv/tests/roundtrip_test.odin b/core/rexcode/ir/spirv/tests/roundtrip_test.odin index 7f27b9258..923d83d50 100644 --- a/core/rexcode/ir/spirv/tests/roundtrip_test.odin +++ b/core/rexcode/ir/spirv/tests/roundtrip_test.odin @@ -203,6 +203,24 @@ main :: proc() { roundtrip("load_aligned", m) } + // (6) the extended ir types: Type_Kind.BOOL (OpTypeBool) and ARRAY with an + // length (OpTypeArray, len_ref -> a constant). NOTE: a spec-valid + // module orders the length constant before the array type; the codec + // round-trips this shape byte-exact regardless (dependency-ordered + // types/constants emit is a separate follow-up). + { + m := spirv.make_module() + m.capabilities = {.Shader} + m.types = { + {kind = .BOOL}, + {kind = .INT, bits = 32, aux = 1}, + {kind = .ARRAY, elem = spirv.Type_Ref(1), len_ref = spirv.Id(4)}, // int32[4] + } + m.type_ids = {spirv.Id(1), spirv.Id(2), spirv.Id(3)} + m.constants = {{result = {spirv.Id(4), spirv.Type_Ref(1)}, opcode = .OpConstant, value = 4}} + roundtrip("bool_and_array", m) + } + fmt.printf("\n%d passed, %d failed\n", ok_count, fail_count) if fail_count > 0 { os.exit(1) } } diff --git a/core/rexcode/ir/types.odin b/core/rexcode/ir/types.odin index 332b77340..323fa9714 100644 --- a/core/rexcode/ir/types.odin +++ b/core/rexcode/ir/types.odin @@ -27,10 +27,11 @@ TYPE_NONE :: Type_Ref(0xFFFFFFFF) Type_Kind :: enum u8 { VOID, + BOOL, // a distinct boolean (SPIR-V OpTypeBool, LLVM i1) INT, // `bits` = width (1/8/16/32/64/...); signedness is op-level in most IRs FLOAT, // `bits` = width (16/32/64/128) VECTOR, // `elem` x `count` (fixed-width SIMD) - ARRAY, // `elem` x `count` + ARRAY, // `elem` x `count` (literal length) or `elem` x `len_ref` ( length) POINTER, // `elem`, address space in `aux` STRUCT, // members in `fields` FUNCTION, // `fields` = params ++ [result]; `count` = param count @@ -41,20 +42,27 @@ Type_Kind :: enum u8 { // One node in a module's type table. `fields` (struct members / function // signature) is caller-owned, like the rest of the decoded module. Type :: struct { - fields: []Type_Ref, // STRUCT members, or FUNCTION params ++ result - name: string, // OPAQUE / named struct - elem: Type_Ref, // VECTOR / ARRAY / POINTER / typed REF element - count: u32, // VECTOR / ARRAY length, or FUNCTION param count - bits: u16, // INT / FLOAT width - aux: u16, // POINTER address space, packed kind flags, ... - kind: Type_Kind, - _: [3]u8, + fields: []Type_Ref, // STRUCT members, or FUNCTION params ++ result + name: string, // OPAQUE / named struct + elem: Type_Ref, // VECTOR / ARRAY / POINTER / typed REF element + count: u32, // VECTOR length, literal ARRAY length, or FUNCTION param count + len_ref: Id, // ARRAY length as a constant (id-typed lengths, e.g. SPIR-V) + bits: u16, // INT / FLOAT width + aux: u16, // POINTER address space, packed kind flags, ... + kind: Type_Kind, + _: [3]u8, } @(require_results) type_void :: #force_inline proc "contextless" () -> Type { return Type{kind = .VOID} } +@(require_results) type_bool :: #force_inline proc "contextless" () -> Type { return Type{kind = .BOOL} } @(require_results) type_int :: #force_inline proc "contextless" (bits: u16) -> Type { return Type{kind = .INT, bits = bits} } @(require_results) type_float :: #force_inline proc "contextless" (bits: u16) -> Type { return Type{kind = .FLOAT, bits = bits} } +@(require_results) +type_array :: #force_inline proc "contextless" (elem: Type_Ref, len_ref: Id) -> Type { + return Type{kind = .ARRAY, elem = elem, len_ref = len_ref} +} + @(require_results) type_vector :: #force_inline proc "contextless" (elem: Type_Ref, count: u32) -> Type { return Type{kind = .VECTOR, elem = elem, count = count}