rexcode/ir: add Type_Kind.BOOL + array <id> length; SPIR-V bool/array round-trip

Extend the shared type model so SPIR-V's OpTypeBool and OpTypeArray (whose length
is a constant <id>, 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 <id>
    (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.
This commit is contained in:
Brendan Punsky
2026-06-26 11:45:35 -04:00
committed by Flāvius
parent 7a8dc34d5f
commit fe27dcdbd4
4 changed files with 40 additions and 11 deletions

View File

@@ -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]})

View File

@@ -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 <id>
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
}

View File

@@ -203,6 +203,24 @@ main :: proc() {
roundtrip("load_aligned", m)
}
// (6) the extended ir types: Type_Kind.BOOL (OpTypeBool) and ARRAY with an
// <id> 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) }
}

View File

@@ -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` (<id> 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> (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}