Commit e566158acf

Robin Voetter <robin@voetter.nl>
2024-03-11 23:39:23
spirv: make IdResult an enum
1 parent 9b05811
Changed files (6)
src/codegen/spirv/Module.zig
@@ -215,12 +215,12 @@ pub fn deinit(self: *Module) void {
 
 pub fn allocId(self: *Module) spec.IdResult {
     defer self.next_result_id += 1;
-    return .{ .id = self.next_result_id };
+    return @enumFromInt(self.next_result_id);
 }
 
 pub fn allocIds(self: *Module, n: u32) spec.IdResult {
     defer self.next_result_id += n;
-    return .{ .id = self.next_result_id };
+    return @enumFromInt(self.next_result_id);
 }
 
 pub fn idBound(self: Module) Word {
src/codegen/spirv/Section.zig
@@ -123,7 +123,7 @@ fn writeOperands(section: *Section, comptime Operands: type, operands: Operands)
 
 pub fn writeOperand(section: *Section, comptime Operand: type, operand: Operand) void {
     switch (Operand) {
-        spec.IdResult => section.writeWord(operand.id),
+        spec.IdResult => section.writeWord(@intFromEnum(operand)),
 
         spec.LiteralInteger => section.writeWord(operand),
 
@@ -138,9 +138,9 @@ pub fn writeOperand(section: *Section, comptime Operand: type, operand: Operand)
         // instruction in which it is used.
         spec.LiteralSpecConstantOpInteger => section.writeWord(@intFromEnum(operand.opcode)),
 
-        spec.PairLiteralIntegerIdRef => section.writeWords(&.{ operand.value, operand.label.id }),
-        spec.PairIdRefLiteralInteger => section.writeWords(&.{ operand.target.id, operand.member }),
-        spec.PairIdRefIdRef => section.writeWords(&.{ operand[0].id, operand[1].id }),
+        spec.PairLiteralIntegerIdRef => section.writeWords(&.{ operand.value, @enumFromInt(operand.label) }),
+        spec.PairIdRefLiteralInteger => section.writeWords(&.{ @intFromEnum(operand.target), operand.member }),
+        spec.PairIdRefIdRef => section.writeWords(&.{ @intFromEnum(operand[0]), @intFromEnum(operand[1]) }),
 
         else => switch (@typeInfo(Operand)) {
             .Enum => section.writeWord(@intFromEnum(operand)),
@@ -338,8 +338,8 @@ test "SPIR-V Section emit() - simple" {
     defer section.deinit(std.testing.allocator);
 
     try section.emit(std.testing.allocator, .OpUndef, .{
-        .id_result_type = .{ .id = 0 },
-        .id_result = .{ .id = 1 },
+        .id_result_type = @enumFromInt(0),
+        .id_result = @enumFromInt(1),
     });
 
     try testing.expectEqualSlices(Word, &.{
@@ -356,7 +356,7 @@ test "SPIR-V Section emit() - string" {
     try section.emit(std.testing.allocator, .OpSource, .{
         .source_language = .Unknown,
         .version = 123,
-        .file = .{ .id = 456 },
+        .file = @enumFromInt(256),
         .source = "pub fn main() void {}",
     });
 
@@ -381,8 +381,8 @@ test "SPIR-V Section emit() - extended mask" {
     defer section.deinit(std.testing.allocator);
 
     try section.emit(std.testing.allocator, .OpLoopMerge, .{
-        .merge_block = .{ .id = 10 },
-        .continue_target = .{ .id = 20 },
+        .merge_block = @enumFromInt(10),
+        .continue_target = @enumFromInt(20),
         .loop_control = .{
             .Unroll = true,
             .DependencyLength = .{
@@ -405,7 +405,7 @@ test "SPIR-V Section emit() - extended union" {
     defer section.deinit(std.testing.allocator);
 
     try section.emit(std.testing.allocator, .OpExecutionMode, .{
-        .entry_point = .{ .id = 888 },
+        .entry_point = @enumFromInt(888),
         .mode = .{
             .LocalSize = .{ .x_size = 4, .y_size = 8, .z_size = 16 },
         },
src/codegen/spirv/spec.zig
@@ -12,8 +12,9 @@ pub const Version = packed struct(Word) {
 };
 
 pub const Word = u32;
-pub const IdResult = struct {
-    id: Word,
+pub const IdResult = enum(Word) {
+    none,
+    _,
 };
 pub const IdResultType = IdResult;
 pub const IdRef = IdResult;
src/codegen/spirv.zig
@@ -5158,7 +5158,7 @@ const DeclGen = struct {
                 const case_body = self.air.extra[case.end + items.len ..][0..case.data.body_len];
                 extra_index = case.end + case.data.items_len + case_body.len;
 
-                const label = IdRef{ .id = @intCast(first_case_label.id + case_i) };
+                const label: IdRef = @enumFromInt(@intFromEnum(first_case_label) + case_i);
 
                 for (items) |item| {
                     const value = (try self.air.value(item, mod)) orelse unreachable;
@@ -5172,7 +5172,7 @@ const DeclGen = struct {
                         else => unreachable,
                     };
                     const int_lit: spec.LiteralContextDependentNumber = switch (cond_words) {
-                        1 => .{ .uint32 = @as(u32, @intCast(int_val)) },
+                        1 => .{ .uint32 = @intCast(int_val) },
                         2 => .{ .uint64 = int_val },
                         else => unreachable,
                     };
@@ -5197,7 +5197,7 @@ const DeclGen = struct {
             const case_body: []const Air.Inst.Index = @ptrCast(self.air.extra[case.end + items.len ..][0..case.data.body_len]);
             extra_index = case.end + case.data.items_len + case_body.len;
 
-            const label = IdResult{ .id = @intCast(first_case_label.id + case_i) };
+            const label: IdResult = @enumFromInt(@intFromEnum(first_case_label) + case_i);
 
             try self.beginSpvBlock(label);
 
src/link/SpirV/BinaryModule.zig
@@ -0,0 +1,597 @@
+const std = @import("std");
+const assert = std.debug.assert;
+const Allocator = std.mem.Allocator;
+const log = std.log.scoped(.spirv_parse);
+
+const spec = @import("../../codegen/spirv/spec.zig");
+const Opcode = spec.Opcode;
+const Word = spec.Word;
+const InstructionSet = spec.InstructionSet;
+const ResultId = spec.IdResult;
+
+const BinaryModule = @This();
+
+pub const header_words = 5;
+
+/// The module SPIR-V version.
+version: spec.Version,
+
+/// The generator magic number.
+generator_magic: u32,
+
+/// The result-id bound of this SPIR-V module.
+id_bound: u32,
+
+/// The instructions of this module. This does not contain the header.
+instructions: []const Word,
+
+/// Maps OpExtInstImport result-ids to their InstructionSet.
+ext_inst_map: std.AutoHashMapUnmanaged(ResultId, InstructionSet),
+
+/// This map contains the width of arithmetic types (OpTypeInt and
+/// OpTypeFloat). We need this information to correctly parse the operands
+/// of Op(Spec)Constant and OpSwitch.
+arith_type_width: std.AutoHashMapUnmanaged(ResultId, u16),
+
+pub fn deinit(self: *BinaryModule, a: Allocator) void {
+    self.ext_inst_map.deinit(a);
+    self.arith_type_width.deinit(a);
+    self.* = undefined;
+}
+
+pub fn iterateInstructions(self: BinaryModule) Instruction.Iterator {
+    return Instruction.Iterator.init(self.instructions);
+}
+
+/// Errors that can be raised when the module is not correct.
+/// Note that the parser doesn't validate SPIR-V modules by a
+/// long shot. It only yields errors that critically prevent
+/// further analysis of the module.
+pub const ParseError = error{
+    /// Raised when the module doesn't start with the SPIR-V magic.
+    /// This usually means that the module isn't actually SPIR-V.
+    InvalidMagic,
+    /// Raised when the module has an invalid "physical" format:
+    /// For example when the header is incomplete, or an instruction
+    /// has an illegal format.
+    InvalidPhysicalFormat,
+    /// OpExtInstImport was used with an unknown extension string.
+    InvalidExtInstImport,
+    /// The module had an instruction with an invalid (unknown) opcode.
+    InvalidOpcode,
+    /// An instruction's operands did not conform to the SPIR-V specification
+    /// for that instruction.
+    InvalidOperands,
+    /// A result-id was declared more than once.
+    DuplicateId,
+    /// Some ID did not resolve.
+    InvalidId,
+    /// Parser ran out of memory.
+    OutOfMemory,
+};
+
+pub const Instruction = struct {
+    pub const Iterator = struct {
+        words: []const Word,
+        index: usize = 0,
+        offset: usize = 0,
+
+        pub fn init(words: []const Word) Iterator {
+            return .{ .words = words };
+        }
+
+        pub fn next(self: *Iterator) ?Instruction {
+            if (self.offset >= self.words.len) return null;
+
+            const instruction_len = self.words[self.offset] >> 16;
+            defer self.offset += instruction_len;
+            defer self.index += 1;
+            assert(instruction_len != 0 and self.offset < self.words.len); // Verified in BinaryModule.parse.
+
+            return Instruction{
+                .opcode = @enumFromInt(self.words[self.offset] & 0xFFFF),
+                .index = self.index,
+                .offset = self.offset,
+                .operands = self.words[self.offset..][1..instruction_len],
+            };
+        }
+    };
+
+    /// The opcode for this instruction.
+    opcode: Opcode,
+    /// The instruction's index.
+    index: usize,
+    /// The instruction's word offset in the module.
+    offset: usize,
+    /// The raw (unparsed) operands for this instruction.
+    operands: []const Word,
+};
+
+/// This struct is used to return information about
+/// a module's functions - entry points, functions,
+/// list of callees.
+pub const FunctionInfo = struct {
+    /// Information that is gathered about a particular function.
+    pub const Fn = struct {
+        /// The word-offset of the first word (of the OpFunction instruction)
+        /// of this instruction.
+        begin_offset: usize,
+        /// The past-end offset of the end (including operands) of the last
+        /// instruction of the function.
+        end_offset: usize,
+        /// The index of the first callee in `callee_store`.
+        first_callee: usize,
+        /// The module offset of the OpTypeFunction instruction corresponding
+        /// to this function.
+        /// We use an offset so that we don't need to keep a separate map.
+        type_offset: usize,
+    };
+
+    /// Maps function result-id -> Function information structure.
+    functions: std.AutoArrayHashMapUnmanaged(ResultId, Fn),
+    /// List of entry points in this module. Contains OpFunction result-ids.
+    entry_points: []const ResultId,
+    /// For each function, a list of function result-ids that it calls.
+    callee_store: []const ResultId,
+
+    pub fn deinit(self: *FunctionInfo, a: Allocator) void {
+        self.functions.deinit(a);
+        a.free(self.entry_points);
+        a.free(self.callee_store);
+        self.* = undefined;
+    }
+
+    /// Fetch the list of callees per function. Guaranteed to contain only unique IDs.
+    pub fn callees(self: FunctionInfo, fn_id: ResultId) []const ResultId {
+        const fn_index = self.functions.getIndex(fn_id).?;
+        const values = self.functions.values();
+        const first_callee = values[fn_index].first_callee;
+        if (fn_index == values.len - 1) {
+            return self.callee_store[first_callee..];
+        } else {
+            const next_first_callee = values[fn_index + 1].first_callee;
+            return self.callee_store[first_callee..next_first_callee];
+        }
+    }
+
+    /// Returns a topological ordering of the functions: For each item
+    /// in the returned list of OpFunction result-ids, it is guaranteed that
+    /// the callees have a lower index. Note that SPIR-V does not support
+    /// any recursion, so this always works.
+    pub fn topologicalSort(self: FunctionInfo, a: Allocator) ![]const ResultId {
+        var sort = std.ArrayList(ResultId).init(a);
+        defer sort.deinit();
+
+        var seen = try std.DynamicBitSetUnmanaged.initEmpty(a, self.functions.count());
+        defer seen.deinit(a);
+
+        var stack = std.ArrayList(ResultId).init(a);
+        defer stack.deinit();
+
+        for (self.functions.keys()) |id| {
+            try self.topologicalSortStep(id, &sort, &seen);
+        }
+
+        return try sort.toOwnedSlice();
+    }
+
+    fn topologicalSortStep(
+        self: FunctionInfo,
+        id: ResultId,
+        sort: *std.ArrayList(ResultId),
+        seen: *std.DynamicBitSetUnmanaged,
+    ) !void {
+        const fn_index = self.functions.getIndex(id) orelse {
+            log.err("function calls invalid callee-id {}", .{@intFromEnum(id)});
+            return error.InvalidId;
+        };
+        if (seen.isSet(fn_index)) {
+            return;
+        }
+
+        seen.set(fn_index);
+        for (self.callees(id)) |callee| {
+            try self.topologicalSortStep(callee, sort, seen);
+        }
+
+        try sort.append(id);
+    }
+};
+
+/// This parser contains information (acceleration tables)
+/// that can be persisted across different modules. This is
+/// used to initialize the module, and is also used when
+/// further analyzing it.
+pub const Parser = struct {
+    /// The allocator used to allocate this parser's structures,
+    /// and also the structures of any parsed module.
+    a: Allocator,
+
+    /// Maps (instruction set, opcode) => instruction index (for instruction set)
+    opcode_table: std.AutoHashMapUnmanaged(u32, u16) = .{},
+
+    pub fn init(a: Allocator) !Parser {
+        var self = Parser{
+            .a = a,
+        };
+        errdefer self.deinit();
+
+        inline for (std.meta.tags(InstructionSet)) |set| {
+            const instructions = set.instructions();
+            try self.opcode_table.ensureUnusedCapacity(a, @intCast(instructions.len));
+            for (instructions, 0..) |inst, i| {
+                // Note: Some instructions may alias another. In this case we don't really care
+                // which one is first: they all (should) have the same operands anyway. Just pick
+                // the first, which is usually the core, KHR or EXT variant.
+                const entry = self.opcode_table.getOrPutAssumeCapacity(mapSetAndOpcode(set, @intCast(inst.opcode)));
+                if (!entry.found_existing) {
+                    entry.value_ptr.* = @intCast(i);
+                }
+            }
+        }
+
+        return self;
+    }
+
+    pub fn deinit(self: *Parser) void {
+        self.opcode_table.deinit(self.a);
+    }
+
+    fn mapSetAndOpcode(set: InstructionSet, opcode: u16) u32 {
+        return (@as(u32, @intFromEnum(set)) << 16) | opcode;
+    }
+
+    pub fn parse(self: *Parser, module: []const u32) ParseError!BinaryModule {
+        if (module[0] != spec.magic_number) {
+            return error.InvalidMagic;
+        } else if (module.len < header_words) {
+            log.err("module only has {}/{} header words", .{ module.len, header_words });
+            return error.InvalidPhysicalFormat;
+        }
+
+        var binary = BinaryModule{
+            .version = @bitCast(module[1]),
+            .generator_magic = module[2],
+            .id_bound = module[3],
+            .instructions = module[header_words..],
+            .ext_inst_map = .{},
+            .arith_type_width = .{},
+        };
+
+        // First pass through the module to verify basic structure and
+        // to gather some initial stuff for more detailed analysis.
+        // We want to check some stuff that Instruction.Iterator is no good for,
+        // so just iterate manually.
+        var offset: usize = 0;
+        while (offset < binary.instructions.len) {
+            const len = binary.instructions[offset] >> 16;
+            if (len == 0 or len + offset > binary.instructions.len) {
+                log.err("invalid instruction format: len={}, end={}, module len={}", .{ len, len + offset, binary.instructions.len });
+                return error.InvalidPhysicalFormat;
+            }
+            defer offset += len;
+
+            // We can't really efficiently use non-exhaustive enums here, because we would
+            // need to manually write out all valid cases. Since we have this map anyway, just
+            // use that.
+            const opcode_num: u16 = @truncate(binary.instructions[offset]);
+            const index = self.opcode_table.get(mapSetAndOpcode(.core, opcode_num)) orelse {
+                log.err("invalid opcode for core set: {}", .{opcode_num});
+                return error.InvalidOpcode;
+            };
+
+            const opcode: Opcode = @enumFromInt(opcode_num);
+            const operands = binary.instructions[offset..][1..len];
+            switch (opcode) {
+                .OpExtInstImport => {
+                    const set_name = std.mem.sliceTo(std.mem.sliceAsBytes(operands[1..]), 0);
+                    const set = std.meta.stringToEnum(InstructionSet, set_name) orelse {
+                        log.err("invalid instruction set '{s}'", .{set_name});
+                        return error.InvalidExtInstImport;
+                    };
+                    if (set == .core) return error.InvalidExtInstImport;
+                    try binary.ext_inst_map.put(self.a, @enumFromInt(operands[0]), set);
+                },
+                .OpTypeInt, .OpTypeFloat => {
+                    const entry = try binary.arith_type_width.getOrPut(self.a, @enumFromInt(operands[0]));
+                    if (entry.found_existing) return error.DuplicateId;
+                    entry.value_ptr.* = std.math.cast(u16, operands[1]) orelse return error.InvalidOperands;
+                },
+                else => {},
+            }
+
+            // OpSwitch takes a value as argument, not an OpType... hence we need to populate arith_type_width
+            // with ALL operations that return an int or float.
+            const proper_operands = InstructionSet.core.instructions()[index].operands;
+
+            if (proper_operands.len >= 2 and
+                proper_operands[0].kind == .IdResultType and
+                proper_operands[1].kind == .IdResult)
+            {
+                if (operands.len < 2) return error.InvalidOperands;
+                if (binary.arith_type_width.get(@enumFromInt(operands[0]))) |width| {
+                    const entry = try binary.arith_type_width.getOrPut(self.a, @enumFromInt(operands[1]));
+                    if (entry.found_existing) return error.DuplicateId;
+                    entry.value_ptr.* = width;
+                }
+            }
+        }
+
+        return binary;
+    }
+
+    pub fn parseFunctionInfo(self: *Parser, binary: BinaryModule) ParseError!FunctionInfo {
+        var entry_points = std.AutoArrayHashMap(ResultId, void).init(self.a);
+        defer entry_points.deinit();
+
+        var functions = std.AutoArrayHashMap(ResultId, FunctionInfo.Fn).init(self.a);
+        errdefer functions.deinit();
+
+        var fn_ty_decls = std.AutoHashMap(ResultId, usize).init(self.a);
+        defer fn_ty_decls.deinit();
+
+        var calls = std.AutoArrayHashMap(ResultId, void).init(self.a);
+        defer calls.deinit();
+
+        var callee_store = std.ArrayList(ResultId).init(self.a);
+        defer callee_store.deinit();
+
+        var maybe_current_function: ?ResultId = null;
+        var begin: usize = undefined;
+        var fn_ty_id: ResultId = undefined;
+
+        var it = binary.iterateInstructions();
+        while (it.next()) |inst| {
+            switch (inst.opcode) {
+                .OpEntryPoint => {
+                    const entry = try entry_points.getOrPut(@enumFromInt(inst.operands[1]));
+                    if (entry.found_existing) return error.DuplicateId;
+                },
+                .OpTypeFunction => {
+                    const entry = try fn_ty_decls.getOrPut(@enumFromInt(inst.operands[0]));
+                    if (entry.found_existing) return error.DuplicateId;
+                    entry.value_ptr.* = inst.offset;
+                },
+                .OpFunction => {
+                    maybe_current_function = @enumFromInt(inst.operands[1]);
+                    begin = inst.offset;
+                    fn_ty_id = @enumFromInt(inst.operands[3]);
+                },
+                .OpFunctionCall => {
+                    const callee: ResultId = @enumFromInt(inst.operands[2]);
+                    try calls.put(callee, {});
+                },
+                .OpFunctionEnd => {
+                    const current_function = maybe_current_function orelse {
+                        log.err("encountered OpFunctionEnd without corresponding OpFunction", .{});
+                        return error.InvalidPhysicalFormat;
+                    };
+                    const entry = try functions.getOrPut(current_function);
+                    if (entry.found_existing) return error.DuplicateId;
+
+                    const first_callee = callee_store.items.len;
+                    try callee_store.appendSlice(calls.keys());
+
+                    const type_offset = fn_ty_decls.get(fn_ty_id) orelse {
+                        log.err("Invalid OpFunction type", .{});
+                        return error.InvalidId;
+                    };
+
+                    entry.value_ptr.* = .{
+                        .begin_offset = begin,
+                        .end_offset = it.offset, // Use past-end offset
+                        .first_callee = first_callee,
+                        .type_offset = type_offset,
+                    };
+                    maybe_current_function = null;
+                    calls.clearRetainingCapacity();
+                },
+                else => {},
+            }
+        }
+
+        if (maybe_current_function != null) {
+            log.err("final OpFunction does not have an OpFunctionEnd", .{});
+            return error.InvalidPhysicalFormat;
+        }
+
+        return FunctionInfo{
+            .functions = functions.unmanaged,
+            .entry_points = try self.a.dupe(ResultId, entry_points.keys()),
+            .callee_store = try callee_store.toOwnedSlice(),
+        };
+    }
+
+    /// Parse offsets in the instruction that contain result-ids.
+    /// Returned offsets are relative to inst.operands.
+    /// Returns in an arraylist to armortize allocations.
+    pub fn parseInstructionResultIds(
+        self: *Parser,
+        binary: BinaryModule,
+        inst: Instruction,
+        offsets: *std.ArrayList(u16),
+    ) !void {
+        const index = self.opcode_table.get(mapSetAndOpcode(.core, @intFromEnum(inst.opcode))).?;
+        const operands = InstructionSet.core.instructions()[index].operands;
+
+        var offset: usize = 0;
+        switch (inst.opcode) {
+            .OpSpecConstantOp => {
+                assert(operands[0].kind == .IdResultType);
+                assert(operands[1].kind == .IdResult);
+                offset = try self.parseOperandsResultIds(binary, inst, operands[0..2], offset, offsets);
+
+                if (offset >= inst.operands.len) return error.InvalidPhysicalFormat;
+                const spec_opcode = std.math.cast(u16, inst.operands[offset]) orelse return error.InvalidPhysicalFormat;
+                const spec_index = self.opcode_table.get(mapSetAndOpcode(.core, spec_opcode)) orelse
+                    return error.InvalidPhysicalFormat;
+                const spec_operands = InstructionSet.core.instructions()[spec_index].operands;
+                assert(spec_operands[0].kind == .IdResultType);
+                assert(spec_operands[1].kind == .IdResult);
+                offset = try self.parseOperandsResultIds(binary, inst, spec_operands[2..], offset + 1, offsets);
+            },
+            .OpExtInst => {
+                assert(operands[0].kind == .IdResultType);
+                assert(operands[1].kind == .IdResult);
+                offset = try self.parseOperandsResultIds(binary, inst, operands[0..2], offset, offsets);
+
+                if (offset + 1 >= inst.operands.len) return error.InvalidPhysicalFormat;
+                const set_id: ResultId = @enumFromInt(inst.operands[offset]);
+                const set = binary.ext_inst_map.get(set_id) orelse {
+                    log.err("Invalid instruction set {}", .{@intFromEnum(set_id)});
+                    return error.InvalidId;
+                };
+                const ext_opcode = std.math.cast(u16, inst.operands[offset + 1]) orelse return error.InvalidPhysicalFormat;
+                const ext_index = self.opcode_table.get(mapSetAndOpcode(set, ext_opcode)) orelse
+                    return error.InvalidPhysicalFormat;
+                const ext_operands = set.instructions()[ext_index].operands;
+                offset = try self.parseOperandsResultIds(binary, inst, ext_operands, offset + 2, offsets);
+            },
+            else => {
+                offset = try self.parseOperandsResultIds(binary, inst, operands, offset, offsets);
+            },
+        }
+
+        if (offset != inst.operands.len) return error.InvalidPhysicalFormat;
+    }
+
+    fn parseOperandsResultIds(
+        self: *Parser,
+        binary: BinaryModule,
+        inst: Instruction,
+        operands: []const spec.Operand,
+        start_offset: usize,
+        offsets: *std.ArrayList(u16),
+    ) !usize {
+        var offset = start_offset;
+        for (operands) |operand| {
+            offset = try self.parseOperandResultIds(binary, inst, operand, offset, offsets);
+        }
+        return offset;
+    }
+
+    fn parseOperandResultIds(
+        self: *Parser,
+        binary: BinaryModule,
+        inst: Instruction,
+        operand: spec.Operand,
+        start_offset: usize,
+        offsets: *std.ArrayList(u16),
+    ) !usize {
+        var offset = start_offset;
+        switch (operand.quantifier) {
+            .variadic => while (offset < inst.operands.len) {
+                offset = try self.parseOperandKindResultIds(binary, inst, operand.kind, offset, offsets);
+            },
+            .optional => if (offset < inst.operands.len) {
+                offset = try self.parseOperandKindResultIds(binary, inst, operand.kind, offset, offsets);
+            },
+            .required => {
+                offset = try self.parseOperandKindResultIds(binary, inst, operand.kind, offset, offsets);
+            },
+        }
+        return offset;
+    }
+
+    fn parseOperandKindResultIds(
+        self: *Parser,
+        binary: BinaryModule,
+        inst: Instruction,
+        kind: spec.OperandKind,
+        start_offset: usize,
+        offsets: *std.ArrayList(u16),
+    ) !usize {
+        var offset = start_offset;
+        if (offset >= inst.operands.len) return error.InvalidPhysicalFormat;
+
+        switch (kind.category()) {
+            .bit_enum => {
+                const mask = inst.operands[offset];
+                offset += 1;
+                for (kind.enumerants()) |enumerant| {
+                    if ((mask & enumerant.value) != 0) {
+                        for (enumerant.parameters) |param_kind| {
+                            offset = try self.parseOperandKindResultIds(binary, inst, param_kind, offset, offsets);
+                        }
+                    }
+                }
+            },
+            .value_enum => {
+                const value = inst.operands[offset];
+                offset += 1;
+                for (kind.enumerants()) |enumerant| {
+                    if (value == enumerant.value) {
+                        for (enumerant.parameters) |param_kind| {
+                            offset = try self.parseOperandKindResultIds(binary, inst, param_kind, offset, offsets);
+                        }
+                        break;
+                    }
+                }
+            },
+            .id => {
+                const this_offset = std.math.cast(u16, offset) orelse return error.InvalidPhysicalFormat;
+                try offsets.append(this_offset);
+                offset += 1;
+            },
+            else => switch (kind) {
+                .LiteralInteger, .LiteralFloat => offset += 1,
+                .LiteralString => while (true) {
+                    if (offset >= inst.operands.len) return error.InvalidPhysicalFormat;
+                    const word = inst.operands[offset];
+                    offset += 1;
+
+                    if (word & 0xFF000000 == 0 or
+                        word & 0x00FF0000 == 0 or
+                        word & 0x0000FF00 == 0 or
+                        word & 0x000000FF == 0)
+                    {
+                        break;
+                    }
+                },
+                .LiteralContextDependentNumber => {
+                    assert(inst.opcode == .OpConstant or inst.opcode == .OpSpecConstantOp);
+                    const bit_width = binary.arith_type_width.get(@enumFromInt(inst.operands[0])) orelse {
+                        log.err("invalid LiteralContextDependentNumber type {}", .{inst.operands[0]});
+                        return error.InvalidId;
+                    };
+                    offset += switch (bit_width) {
+                        1...32 => 1,
+                        33...64 => 2,
+                        else => unreachable,
+                    };
+                },
+                .LiteralExtInstInteger => unreachable,
+                .LiteralSpecConstantOpInteger => unreachable,
+                .PairLiteralIntegerIdRef => { // Switch case
+                    assert(inst.opcode == .OpSwitch);
+                    const bit_width = binary.arith_type_width.get(@enumFromInt(inst.operands[0])) orelse {
+                        log.err("invalid OpSwitch type {}", .{inst.operands[0]});
+                        return error.InvalidId;
+                    };
+                    offset += switch (bit_width) {
+                        1...32 => 1,
+                        33...64 => 2,
+                        else => unreachable,
+                    };
+                    const this_offset = std.math.cast(u16, offset) orelse return error.InvalidPhysicalFormat;
+                    try offsets.append(this_offset);
+                    offset += 1;
+                },
+                .PairIdRefLiteralInteger => {
+                    const this_offset = std.math.cast(u16, offset) orelse return error.InvalidPhysicalFormat;
+                    try offsets.append(this_offset);
+                    offset += 2;
+                },
+                .PairIdRefIdRef => {
+                    const a = std.math.cast(u16, offset) orelse return error.InvalidPhysicalFormat;
+                    const b = std.math.cast(u16, offset + 1) orelse return error.InvalidPhysicalFormat;
+                    try offsets.append(a);
+                    try offsets.append(b);
+                    offset += 2;
+                },
+                else => unreachable,
+            },
+        }
+        return offset;
+    }
+};
tools/gen_spirv_spec.zig
@@ -154,8 +154,9 @@ fn render(writer: anytype, a: Allocator, registry: CoreRegistry, extensions: []c
         \\};
         \\
         \\pub const Word = u32;
-        \\pub const IdResult = struct{
-        \\    id: Word,
+        \\pub const IdResult = enum(Word) {
+        \\   none,
+        \\   _,
         \\};
         \\pub const IdResultType = IdResult;
         \\pub const IdRef = IdResult;