rexcode/ir/spirv: codec completeness -- function params, computed bound, enum-param operands

- OpFunctionParameter: the entry block's Block.params round-trip as
  OpFunctionParameter (emitted between OpFunction and the entry OpLabel, decoded
  back onto the entry block).
- encode computes bound (max <id> + 1) when the caller leaves it 0; a non-zero
  bound (e.g. from decode) is preserved, so re-encode is stable.
- Trailing operands an enum value/bit pulls in (MemoryAccess Aligned's alignment,
  ...) are captured on decode as literals, so enum-parameter instructions
  re-encode byte-exact.

Tests: param_function + load_aligned added -> 5 passed.
This commit is contained in:
Brendan Punsky
2026-06-26 11:14:12 -04:00
committed by Flāvius
parent 47c23d6d35
commit 35fac68c7c
3 changed files with 116 additions and 11 deletions

View File

@@ -51,13 +51,16 @@ Decoder :: struct {
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,
in_fn: bool,
fn_sig: Type_Ref,
fn_id: Id,
fn_blocks: [dynamic]Block,
fn_params: [dynamic]Result, // OpFunctionParameters, attached to the entry block
first_block: bool,
have_blk: bool,
blk_id: Id,
blk_params: []Result,
blk_ops: [dynamic]Operation,
}
@(private="file")
@@ -140,6 +143,13 @@ decode_operation :: proc(d: ^Decoder, opcode: Opcode, w: []u32) -> Operation {
wi += 1
}
}
// Trailing words beyond the fixed layout: the parameter operands an enum
// value/bit pulls in (MemoryAccess Aligned's alignment, etc.). Captured as
// literals -- enough to re-encode byte-exact (semantic typing is a refinement).
for wi < len(w) {
append(&ops, op_int(i64(w[wi])))
wi += 1
}
op.operands = ops[:]
return op
}
@@ -147,8 +157,9 @@ decode_operation :: proc(d: ^Decoder, opcode: Opcode, w: []u32) -> Operation {
@(private="file")
finish_block :: proc(d: ^Decoder) {
if d.have_blk {
append(&d.fn_blocks, Block{id = d.blk_id, ops = d.blk_ops[:]})
append(&d.fn_blocks, Block{id = d.blk_id, ops = d.blk_ops[:], params = d.blk_params})
d.blk_ops = nil
d.blk_params = nil
d.have_blk = false
}
}
@@ -240,10 +251,13 @@ lower :: proc(d: ^Decoder, opcode: Opcode, w: []u32) {
case .OpFunction:
d.in_fn = true
d.fn_id = Id(w[1]); d.fn_sig = tref(d, w[3])
d.fn_blocks = nil
d.fn_blocks = nil; d.fn_params = nil; d.first_block = true
case .OpFunctionParameter:
append(&d.fn_params, Result{id = Id(w[1]), type = tref(d, w[0])})
case .OpLabel:
finish_block(d)
d.have_blk = true; d.blk_id = Id(w[0]); d.blk_ops = nil
if d.first_block { d.blk_params = d.fn_params[:]; d.first_block = false }
case .OpFunctionEnd:
finish_function(d)

View File

@@ -274,7 +274,13 @@ emit_functions :: proc "contextless" (w: ^Writer, m: ^Module) {
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)
// Function parameters: the entry block's params are the OpFunctionParameters,
// emitted between OpFunction and the entry OpLabel.
if len(fn.blocks) > 0 {
for p in fn.blocks[0].params {
sp := inst_begin(w); w_id(w, tid(m, p.type)); w_id(w, p.id); inst_end(w, sp, .OpFunctionParameter)
}
}
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) }
@@ -287,10 +293,37 @@ emit_functions :: proc "contextless" (w: ^Writer, m: ^Module) {
// Entry point
// -----------------------------------------------------------------------------
@(private="file")
max_id :: #force_inline proc "contextless" (cur: u32, id: Id) -> u32 {
return id != ID_NONE ? max(cur, u32(id)) : cur
}
// The exclusive upper bound on every <id> in the module (the header's `bound`).
@(private="file")
compute_bound :: proc "contextless" (m: ^Module) -> u32 {
hi := u32(0)
for id in m.type_ids { hi = max_id(hi, id) }
for id in m.global_ids { hi = max_id(hi, id) }
for id in m.function_ids { hi = max_id(hi, id) }
for ei in m.ext_imports { hi = max_id(hi, ei.result) }
for s in m.debug.strings { hi = max_id(hi, s.result) }
for c in m.constants { hi = max_id(hi, c.result.id) }
for fn in m.functions {
for blk in fn.blocks {
hi = max_id(hi, blk.id)
for p in blk.params { hi = max_id(hi, p.id) }
for op in blk.ops { hi = max_id(hi, op.result.id) }
}
}
return hi + 1
}
// 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.
// count written. `bound` is computed when left 0 (else taken as-is, so a decoded
// module re-encodes with its original bound).
encode :: proc(m: Module, code: []u8, relocs: ^[dynamic]Relocation, errors: ^[dynamic]Error) -> (byte_count: u32, ok: bool) {
m := m
if m.bound == 0 { m.bound = compute_bound(&m) }
w := Writer{code = code, ok = true}
emit_header(&w, &m)
emit_preamble(&w, &m)

View File

@@ -125,6 +125,64 @@ main :: proc() {
roundtrip("iadd_function", m)
}
// (4) a function with a parameter, and bound left 0 so encode computes it:
// i32 @f(i32 %4) { %5: OpReturnValue %4 }
{
m := spirv.make_module()
m.capabilities = {.Shader}
m.types = {
{kind = .INT, bits = 32, aux = 1},
{kind = .FUNCTION, fields = {spirv.Type_Ref(0), spirv.Type_Ref(0)}, count = 1},
}
m.type_ids = {spirv.Id(1), spirv.Id(2)}
ret := spirv.Operation{
opcode = u16(spirv.Opcode.OpReturnValue),
result = {id = spirv.ID_NONE},
operands = {spirv.op_value(spirv.Id(4))},
}
m.functions = {
{signature = spirv.Type_Ref(1), blocks = {
{
id = spirv.Id(5),
params = {{id = spirv.Id(4), type = spirv.Type_Ref(0)}}, // OpFunctionParameter
ops = {ret},
},
}},
}
m.function_ids = {spirv.Id(3)}
// m.bound left 0 -> computed
roundtrip("param_function", m)
}
// (5) an enum-parameter operand: OpLoad ... MemoryAccess Aligned 16, where the
// alignment (16) is a trailing operand pulled in by the Aligned bit.
{
m := spirv.make_module()
m.capabilities = {.Shader}
m.types = {
{kind = .VOID},
{kind = .FLOAT, bits = 32},
{kind = .POINTER, aux = 7, elem = spirv.Type_Ref(1)}, // Function* float
{kind = .FUNCTION, fields = {spirv.Type_Ref(0)}, count = 0},
}
m.type_ids = {spirv.Id(1), spirv.Id(2), spirv.Id(3), spirv.Id(4)}
var := spirv.Operation{ // %7 = OpVariable %ptr Function
opcode = u16(spirv.Opcode.OpVariable), result = {spirv.Id(7), spirv.Type_Ref(2)},
operands = {spirv.op_int(7)},
}
load := spirv.Operation{ // %8 = OpLoad %float %7 Aligned 16
opcode = u16(spirv.Opcode.OpLoad), result = {spirv.Id(8), spirv.Type_Ref(1)},
operands = {spirv.op_value(spirv.Id(7)), spirv.op_int(0x2), spirv.op_int(16)},
}
m.functions = {
{signature = spirv.Type_Ref(3), blocks = {
{id = spirv.Id(6), ops = {var, load, {opcode = u16(spirv.Opcode.OpReturn)}}},
}},
}
m.function_ids = {spirv.Id(5)}
roundtrip("load_aligned", m)
}
fmt.printf("\n%d passed, %d failed\n", ok_count, fail_count)
if fail_count > 0 { os.exit(1) }
}