rexcode/ir/spirv: complete the encoder -- types, constants, globals, function bodies

The second half of encode(): the <id> side tables (Module.type_ids /
global_ids / function_ids -- SPIR-V's flat id space, which ir.Type/Global/
Function don't carry) plus the lowering:

  emit_types       ir.Type -> OpTypeXxx (void/int/float/vector/pointer/struct/
                   function; INT signedness + POINTER storage class ride in aux)
  emit_constants   OpConstant / OpConstantComposite / true/false/null
  emit_globals     OpVariable (storage class from the pointer type)
  emit_operation   generic table-driven op emit: INSTRUCTION_INDEX gives the
                   result-type/result-id prefix, the rest stream from op.operands
  emit_functions   OpFunction / OpLabel / body / OpFunctionEnd

Validated: a complete void compute main module encodes to byte-exact-correct
SPIR-V (29 words, all checked). Known gaps: OpFunctionParameter, ARRAY/bool
types, explicit enum-parameter operands, computed bound. Decoder next.
This commit is contained in:
Brendan Punsky
2026-06-26 09:24:52 -04:00
committed by Flāvius
parent 18f9f141e4
commit 737db89ad0
2 changed files with 144 additions and 6 deletions

View File

@@ -155,15 +155,140 @@ emit_annotations :: proc "contextless" (w: ^Writer, m: ^Module) {
}
}
// -----------------------------------------------------------------------------
// Types / constants / globals (the <id>-defining body, before functions)
// -----------------------------------------------------------------------------
// Type_Ref -> the type's wire <id>, via the side table.
@(private="file")
tid :: #force_inline proc "contextless" (m: ^Module, t: Type_Ref) -> Id {
i := u32(t)
return i < u32(len(m.type_ids)) ? m.type_ids[i] : ID_NONE
}
// Lower ir.Type -> OpTypeXxx. INT signedness and POINTER storage class ride in
// 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 .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 .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 .ARRAY, .OPAQUE, .REF:
w.pos = s // rewind the placeholder; not yet lowered
continue
}
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
}
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)
}
}
// -----------------------------------------------------------------------------
// Function bodies (the generic, table-driven operation emit)
// -----------------------------------------------------------------------------
// Emit one ir.Operand by its kind. Type refs resolve through the type-id table;
// entity refs and literals are emitted as-is.
@(private="file")
emit_operand :: #force_inline proc "contextless" (w: ^Writer, m: ^Module, o: Operand) {
switch o.kind {
case .NONE:
case .LIT_INT, .LIT_FLOAT, .ATTRIBUTE: w_word(w, u32(o.imm))
case .REF: w_id(w, operand_id(o))
case .TYPE: w_id(w, tid(m, operand_type(o)))
}
}
// Emit one Operation. The opcode's layout (INSTRUCTION_INDEX) supplies the
// leading IdResultType/IdResult from `result`; the remaining operands are
// `op.operands` in order (the producer built them correctly, so no per-operand
// spec match is needed -- only whether a result type/id prefix exists).
@(private="file")
emit_operation :: proc "contextless" (w: ^Writer, m: ^Module, op: ^Operation) {
run: Spec_Run
if int(op.opcode) < len(INSTRUCTION_INDEX) { run = INSTRUCTION_INDEX[op.opcode] }
s := inst_begin(w)
si := 0
if si < int(run.count) && INSTRUCTION_SPECS[int(run.start) + si].kind == .IdResultType {
w_id(w, tid(m, op.result.type)); si += 1
}
if si < int(run.count) && INSTRUCTION_SPECS[int(run.start) + si].kind == .IdResult {
w_id(w, op.result.id); si += 1
}
for o in op.operands { emit_operand(w, m, o) }
inst_end(w, s, Opcode(op.opcode))
}
@(private="file")
emit_functions :: proc "contextless" (w: ^Writer, m: ^Module) {
for fn, fi in m.functions {
sig := m.types[u32(fn.signature)] // a FUNCTION type: fields = params ++ [result]
s := inst_begin(w)
w_id(w, tid(m, sig.fields[sig.count])) // result = return type
w_id(w, fi < len(m.function_ids) ? m.function_ids[fi] : ID_NONE)
w_word(w, 0) // FunctionControl (none)
w_id(w, tid(m, fn.signature))
inst_end(w, s, .OpFunction)
// (OpFunctionParameter not yet modelled in ir.Function)
for blk in fn.blocks {
sl := inst_begin(w); w_id(w, blk.id); inst_end(w, sl, .OpLabel)
for &op in blk.ops { emit_operation(w, m, &op) }
}
se := inst_begin(w); inst_end(w, se, .OpFunctionEnd)
}
}
// -----------------------------------------------------------------------------
// Entry point
// -----------------------------------------------------------------------------
// encode: serialize `m` into `code`, returning the byte count written.
//
// (Types / constants / globals / function bodies are not yet emitted -- that
// half needs the <id> assignment + ir.Type -> OpTypeXxx lowering, which lands
// next. The header + preamble / debug / annotation sections are complete.)
// encode: serialize `m` into `code` in spec layout order, returning the byte
// count written. `m.bound` must be the exclusive upper bound on all <id>s.
encode :: proc(m: Module, code: []u8, relocs: ^[dynamic]Relocation, errors: ^[dynamic]Error) -> (byte_count: u32, ok: bool) {
m := m
w := Writer{code = code, ok = true}
@@ -171,6 +296,9 @@ encode :: proc(m: Module, code: []u8, relocs: ^[dynamic]Relocation, errors: ^[dy
emit_preamble(&w, &m)
emit_debug(&w, &m)
emit_annotations(&w, &m)
// TODO(codec): emit_types_constants_globals + emit_functions (the lowered body).
emit_types(&w, &m)
emit_constants(&w, &m)
emit_globals(&w, &m)
emit_functions(&w, &m)
return w.pos, w.ok
}

View File

@@ -44,6 +44,16 @@ Module :: struct {
// --- Debug + annotations ---
debug: Debug,
decorations: []Decoration_Inst,
// --- <id> side tables ---
// SPIR-V has one flat <id> space (types, constants, globals, functions, and
// SSA results all draw from it), but ir.Type/Global/Function carry no id of
// their own. These parallel the ir core arrays and hold each entity's wire
// <id>, so decode->encode preserves them. (Results carry their own id in
// Result.id / Constant.result.id; only these three need a side table.)
type_ids: []Id, // parallel to base.types
global_ids: []Id, // parallel to base.globals
function_ids: []Id, // parallel to base.functions
}
// Member index sentinel: a whole-target decoration / name (OpDecorate / OpName)