rexcode/ir/spirv: dependency-ordered definitions + runtime arrays

- Definition order: Module.defs records the exact type/constant/global
  interleaving (SPIR-V's single 'types, constants, global variables' section must
  be dependency-ordered -- a length constant before the array type that uses it,
  etc.). decode records it; encode replays it for byte-exact, spec-valid output
  (empty defs falls back to all-types, then -constants, then -globals).
  emit_one_type/constant/global factored out for the replay; no encoder alloc.
- OpTypeRuntimeArray: ARRAY with len_ref == ID_NONE (vs OpTypeArray's <id> length).

Tests: bool_and_array sets defs so OpConstant precedes OpTypeArray (verified in
disasm); runtime_array added -> 7 passed.
This commit is contained in:
Brendan Punsky
2026-06-26 12:04:27 -04:00
committed by Flāvius
parent fe27dcdbd4
commit 0cd5ed84b2
4 changed files with 117 additions and 57 deletions

View File

@@ -49,6 +49,7 @@ Decoder :: struct {
global_ids: [dynamic]Id,
functions: [dynamic]Function,
function_ids: [dynamic]Id,
defs: [dynamic]Def, // type/constant/global definition order
// in-flight function / block
in_fn: bool,
@@ -107,10 +108,17 @@ tref :: proc(d: ^Decoder, id: u32) -> Type_Ref {
@(private="file")
add_type :: proc(d: ^Decoder, id: Id, t: Type) {
d.id_to_type[id] = Type_Ref(len(d.types))
append(&d.defs, Def{.TYPE, u32(len(d.types))})
append(&d.types, t)
append(&d.type_ids, id)
}
@(private="file")
add_const :: proc(d: ^Decoder, c: Constant) {
append(&d.defs, Def{.CONSTANT, u32(len(d.constants))})
append(&d.constants, c)
}
// 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).
@@ -218,6 +226,7 @@ lower :: proc(d: ^Decoder, opcode: Opcode, w: []u32) {
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 .OpTypeRuntimeArray: add_type(d, Id(w[0]), Type{kind = .ARRAY, elem = tref(d, w[1]), len_ref = ID_NONE})
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)
@@ -233,18 +242,19 @@ lower :: proc(d: ^Decoder, opcode: Opcode, w: []u32) {
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)
add_const(d, c)
case .OpConstantTrue, .OpConstantFalse, .OpConstantNull:
append(&d.constants, Constant{result = {Id(w[1]), tref(d, w[0])}, opcode = opcode})
add_const(d, 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})
add_const(d, 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.defs, Def{.GLOBAL, u32(len(d.globals))})
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]))
}
@@ -337,6 +347,7 @@ decode :: proc(data: []u8, m: ^Module, errors: ^[dynamic]Error, allocator := con
m.global_ids = d.global_ids[:]
m.functions = d.functions[:]
m.function_ids = d.function_ids[:]
m.defs = d.defs[:]
return u32(nwords * 4), true
}

View File

@@ -170,64 +170,85 @@ tid :: #force_inline proc "contextless" (m: ^Module, t: Type_Ref) -> Id {
// Type.aux. (ARRAY/OPAQUE/REF need a length constant / extra modelling and are
// skipped for now.)
@(private="file")
emit_types :: proc "contextless" (w: ^Writer, m: ^Module) {
for t, i in m.types {
s := inst_begin(w)
w_id(w, i < len(m.type_ids) ? m.type_ids[i] : ID_NONE)
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)) }
op = .OpTypeStruct
case .FUNCTION:
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 .OPAQUE, .REF:
w.pos = s // rewind the placeholder; not yet lowered
continue
}
inst_end(w, s, op)
emit_one_type :: proc "contextless" (w: ^Writer, m: ^Module, i: int) {
t := m.types[i]
s := inst_begin(w)
w_id(w, i < len(m.type_ids) ? m.type_ids[i] : ID_NONE)
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:
// length: an <id> (OpTypeArray) or absent (OpTypeRuntimeArray).
w_id(w, tid(m, t.elem))
if t.len_ref != ID_NONE { w_id(w, t.len_ref); op = .OpTypeArray }
else { op = .OpTypeRuntimeArray }
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)) }
op = .OpTypeStruct
case .FUNCTION:
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 .OPAQUE, .REF:
w.pos = s // rewind the placeholder; not yet lowered
return
}
inst_end(w, s, op)
}
@(private="file")
emit_constants :: proc "contextless" (w: ^Writer, m: ^Module) {
for c in m.constants {
s := inst_begin(w)
w_id(w, tid(m, c.result.type))
w_id(w, c.result.id)
#partial switch c.opcode {
case .OpConstant:
t := m.types[u32(c.result.type)]
w_word(w, u32(c.value))
if (t.kind == .INT || t.kind == .FLOAT) && t.bits > 32 {
w_word(w, u32(c.value >> 32)) // context-dependent number, second word
emit_one_constant :: proc "contextless" (w: ^Writer, m: ^Module, i: int) {
c := m.constants[i]
s := inst_begin(w)
w_id(w, tid(m, c.result.type))
w_id(w, c.result.id)
#partial switch c.opcode {
case .OpConstant:
t := m.types[u32(c.result.type)]
w_word(w, u32(c.value))
if (t.kind == .INT || t.kind == .FLOAT) && t.bits > 32 {
w_word(w, u32(c.value >> 32)) // context-dependent number, second word
}
case .OpConstantComposite:
for e in c.elements { w_id(w, e) }
}
inst_end(w, s, c.opcode)
}
@(private="file")
emit_one_global :: proc "contextless" (w: ^Writer, m: ^Module, i: int) {
g := m.globals[i]
s := inst_begin(w)
w_id(w, tid(m, g.type)) // a pointer type
w_id(w, i < len(m.global_ids) ? m.global_ids[i] : ID_NONE)
w_word(w, u32(m.types[u32(g.type)].aux)) // storage class = the pointer's address space
if g.init != ID_NONE { w_id(w, g.init) }
inst_end(w, s, .OpVariable)
}
// The "types, constants, global variables" section. When Module.defs records the
// stream order (from decode), replay it -- byte-exact and dependency-correct.
// Otherwise fall back to all-types, then all-constants, then all-globals.
@(private="file")
emit_definitions :: proc "contextless" (w: ^Writer, m: ^Module) {
if len(m.defs) > 0 {
for d in m.defs {
switch d.kind {
case .TYPE: emit_one_type(w, m, int(d.index))
case .CONSTANT: emit_one_constant(w, m, int(d.index))
case .GLOBAL: emit_one_global(w, m, int(d.index))
}
case .OpConstantComposite:
for e in c.elements { w_id(w, e) }
}
inst_end(w, s, c.opcode)
}
}
@(private="file")
emit_globals :: proc "contextless" (w: ^Writer, m: ^Module) {
for g, gi in m.globals {
s := inst_begin(w)
w_id(w, tid(m, g.type)) // a pointer type
w_id(w, gi < len(m.global_ids) ? m.global_ids[gi] : ID_NONE)
w_word(w, u32(m.types[u32(g.type)].aux)) // storage class = the pointer's address space
if g.init != ID_NONE { w_id(w, g.init) }
inst_end(w, s, .OpVariable)
return
}
for i in 0 ..< len(m.types) { emit_one_type(w, m, i) }
for i in 0 ..< len(m.constants) { emit_one_constant(w, m, i) }
for i in 0 ..< len(m.globals) { emit_one_global(w, m, i) }
}
// -----------------------------------------------------------------------------
@@ -331,9 +352,7 @@ encode :: proc(m: Module, code: []u8, relocs: ^[dynamic]Relocation, errors: ^[dy
emit_preamble(&w, &m)
emit_debug(&w, &m)
emit_annotations(&w, &m)
emit_types(&w, &m)
emit_constants(&w, &m)
emit_globals(&w, &m)
emit_definitions(&w, &m)
emit_functions(&w, &m)
return w.pos, w.ok
}

View File

@@ -54,6 +54,22 @@ Module :: struct {
type_ids: []Id, // parallel to base.types
global_ids: []Id, // parallel to base.globals
function_ids: []Id, // parallel to base.functions
// --- Definition order ---
// The interleaving of types / constants / globals as they appear in the stream.
// SPIR-V has one "types, constants, global variables" section that must be in
// dependency order (an array's length constant before the array type, a
// constant's result type before the constant, ...). decode records the order;
// encode replays it for byte-exact, spec-valid output. Empty => encode falls
// back to all-types, then all-constants, then all-globals.
defs: []Def,
}
// A node in Module.defs: which of the three definition arrays, and its index.
Def_Kind :: enum u8 { TYPE, CONSTANT, GLOBAL }
Def :: struct {
kind: Def_Kind,
index: u32,
}
// Member index sentinel: a whole-target decoration / name (OpDecorate / OpName)

View File

@@ -218,9 +218,23 @@ main :: proc() {
}
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}}
// definition order: the length constant before the array type (spec-valid).
m.defs = {{.TYPE, 0}, {.TYPE, 1}, {.CONSTANT, 0}, {.TYPE, 2}}
roundtrip("bool_and_array", m)
}
// (7) a runtime array (OpTypeRuntimeArray): ARRAY with len_ref == ID_NONE.
{
m := spirv.make_module()
m.capabilities = {.Shader}
m.types = {
{kind = .INT, bits = 32, aux = 1},
{kind = .ARRAY, elem = spirv.Type_Ref(0), len_ref = spirv.ID_NONE}, // int32[]
}
m.type_ids = {spirv.Id(1), spirv.Id(2)}
roundtrip("runtime_array", m)
}
fmt.printf("\n%d passed, %d failed\n", ok_count, fail_count)
if fail_count > 0 { os.exit(1) }
}