1const std = @import("std");
2const assert = std.debug.assert;
3const Allocator = std.mem.Allocator;
4const log = std.log.scoped(.spirv_parse);
5
6const spec = @import("../../codegen/spirv/spec.zig");
7const Opcode = spec.Opcode;
8const Word = spec.Word;
9const InstructionSet = spec.InstructionSet;
10const ResultId = spec.Id;
11
12const BinaryModule = @This();
13
14/// The result-id bound of this SPIR-V module.
15id_bound: u32,
16
17/// The instructions of this module (no header).
18instructions: []const Word,
19
20/// Maps OpExtInstImport result-ids to their InstructionSet.
21ext_inst_map: std.AutoHashMapUnmanaged(ResultId, InstructionSet),
22
23/// Width of arithmetic types (OpTypeInt/OpTypeFloat). Needed to correctly
24/// parse operands of Op(Spec)Constant and OpSwitch.
25arith_type_width: std.AutoHashMapUnmanaged(ResultId, u16),
26
27functions_start: usize,
28
29pub fn deinit(bm: *BinaryModule, gpa: Allocator) void {
30 bm.ext_inst_map.deinit(gpa);
31 bm.arith_type_width.deinit(gpa);
32 bm.* = undefined;
33}
34
35pub fn iterateInstructions(bm: BinaryModule) Instruction.Iterator {
36 return Instruction.Iterator.init(bm.instructions, 0);
37}
38
39pub fn iterateInstructionsFrom(bm: BinaryModule, offset: usize) Instruction.Iterator {
40 return Instruction.Iterator.init(bm.instructions, offset);
41}
42
43pub const Instruction = struct {
44 pub const Iterator = struct {
45 words: []const Word,
46 offset: usize = 0,
47
48 pub fn init(words: []const Word, start_offset: usize) Iterator {
49 return .{ .words = words, .offset = start_offset };
50 }
51
52 pub fn next(it: *Iterator) ?Instruction {
53 if (it.offset >= it.words.len) return null;
54
55 const instruction_len = it.words[it.offset] >> 16;
56 defer it.offset += instruction_len;
57 assert(instruction_len != 0);
58 assert(it.offset < it.words.len);
59
60 return Instruction{
61 .opcode = @fromBackingInt(@intCast(it.words[it.offset] & 0xFFFF)),
62 .offset = it.offset,
63 .operands = it.words[it.offset..][1..instruction_len],
64 };
65 }
66 };
67
68 opcode: Opcode,
69 offset: usize,
70 operands: []const Word,
71};
72
73pub const Parser = struct {
74 gpa: Allocator,
75 opcode_table: std.AutoHashMapUnmanaged(u32, u16) = .empty,
76
77 pub fn init(gpa: Allocator) !Parser {
78 var parser = Parser{ .gpa = gpa };
79 errdefer parser.deinit();
80
81 inline for (std.meta.tags(InstructionSet)) |set| {
82 const instructions = set.instructions();
83 try parser.opcode_table.ensureUnusedCapacity(gpa, @intCast(instructions.len));
84 for (instructions, 0..) |inst, i| {
85 const entry = parser.opcode_table.getOrPutAssumeCapacity(mapSetAndOpcode(set, @intCast(inst.opcode)));
86 if (!entry.found_existing) {
87 entry.value_ptr.* = @intCast(i);
88 }
89 }
90 }
91
92 return parser;
93 }
94
95 pub fn deinit(parser: *Parser) void {
96 parser.opcode_table.deinit(parser.gpa);
97 }
98
99 fn mapSetAndOpcode(set: InstructionSet, opcode: u16) u32 {
100 return (@as(u32, @backingInt(set)) << 16) | opcode;
101 }
102
103 pub fn getInstSpec(parser: Parser, opcode: Opcode) ?spec.Instruction {
104 const index = parser.opcode_table.get(mapSetAndOpcode(.core, @backingInt(opcode))) orelse return null;
105 return InstructionSet.core.instructions()[index];
106 }
107
108 /// Build a BinaryModule from raw instruction words (no header).
109 /// Scans for ext_inst_map, arith_type_width, and the functions section offset.
110 pub fn initFromWords(parser: *Parser, words: []const Word, id_bound: u32) !BinaryModule {
111 var binary = BinaryModule{
112 .id_bound = id_bound,
113 .instructions = words,
114 .ext_inst_map = .{},
115 .arith_type_width = .{},
116 .functions_start = undefined,
117 };
118
119 var maybe_function_section: ?usize = null;
120 var it = binary.iterateInstructions();
121 while (it.next()) |inst| {
122 const inst_spec = parser.getInstSpec(inst.opcode) orelse continue;
123 const operands = inst.operands;
124
125 switch (inst.opcode) {
126 .OpExtInstImport => {
127 const set_name = std.mem.sliceTo(std.mem.sliceAsBytes(operands[1..]), 0);
128 const set = std.meta.stringToEnum(InstructionSet, set_name) orelse continue;
129 if (set == .core) continue;
130 try binary.ext_inst_map.put(parser.gpa, @fromBackingInt(@intCast(operands[0])), set);
131 },
132 .OpTypeInt, .OpTypeFloat => {
133 try binary.arith_type_width.put(parser.gpa, @fromBackingInt(@intCast(operands[0])), @intCast(operands[1]));
134 },
135 .OpFunction => if (maybe_function_section == null) {
136 maybe_function_section = inst.offset;
137 },
138 else => {},
139 }
140
141 // propagate arith type widths through instructions that return int/float
142 const spec_operands = inst_spec.operands;
143 if (spec_operands.len >= 2 and
144 spec_operands[0].kind == .id_result_type and
145 spec_operands[1].kind == .id_result)
146 {
147 if (operands.len >= 2) {
148 if (binary.arith_type_width.get(@fromBackingInt(@intCast(operands[0])))) |width| {
149 try binary.arith_type_width.put(parser.gpa, @fromBackingInt(@intCast(operands[1])), width);
150 }
151 }
152 }
153 }
154
155 binary.functions_start = maybe_function_section orelse binary.instructions.len;
156
157 return binary;
158 }
159
160 /// Parse offsets in the instruction that contain result-ids.
161 /// Returned offsets are relative to inst.operands.
162 pub fn parseInstructionResultIds(
163 parser: *Parser,
164 binary: BinaryModule,
165 inst: Instruction,
166 offsets: *std.ArrayList(u16),
167 ) !void {
168 const index = parser.opcode_table.get(mapSetAndOpcode(.core, @backingInt(inst.opcode))).?;
169 const operands = InstructionSet.core.instructions()[index].operands;
170
171 var offset: usize = 0;
172 switch (inst.opcode) {
173 .OpSpecConstantOp => {
174 assert(operands[0].kind == .id_result_type);
175 assert(operands[1].kind == .id_result);
176 offset = try parser.parseOperandsResultIds(binary, inst, operands[0..2], offset, offsets);
177
178 if (offset >= inst.operands.len) return error.InvalidPhysicalFormat;
179 const spec_opcode = std.math.cast(u16, inst.operands[offset]) orelse return error.InvalidPhysicalFormat;
180 const spec_index = parser.opcode_table.get(mapSetAndOpcode(.core, spec_opcode)) orelse
181 return error.InvalidPhysicalFormat;
182 const spec_operands = InstructionSet.core.instructions()[spec_index].operands;
183 assert(spec_operands[0].kind == .id_result_type);
184 assert(spec_operands[1].kind == .id_result);
185 offset = try parser.parseOperandsResultIds(binary, inst, spec_operands[2..], offset + 1, offsets);
186 },
187 .OpExtInst => {
188 assert(operands[0].kind == .id_result_type);
189 assert(operands[1].kind == .id_result);
190 offset = try parser.parseOperandsResultIds(binary, inst, operands[0..2], offset, offsets);
191
192 if (offset + 1 >= inst.operands.len) return error.InvalidPhysicalFormat;
193 const set_id: ResultId = @fromBackingInt(@intCast(inst.operands[offset]));
194 try offsets.append(parser.gpa, @intCast(offset));
195 const set = binary.ext_inst_map.get(set_id) orelse {
196 log.err("invalid instruction set {}", .{@backingInt(set_id)});
197 return error.InvalidId;
198 };
199 const ext_opcode = std.math.cast(u16, inst.operands[offset + 1]) orelse return error.InvalidPhysicalFormat;
200 const ext_index = parser.opcode_table.get(mapSetAndOpcode(set, ext_opcode)) orelse
201 return error.InvalidPhysicalFormat;
202 const ext_operands = set.instructions()[ext_index].operands;
203 offset = try parser.parseOperandsResultIds(binary, inst, ext_operands, offset + 2, offsets);
204 },
205 else => {
206 offset = try parser.parseOperandsResultIds(binary, inst, operands, offset, offsets);
207 },
208 }
209
210 if (offset != inst.operands.len) return error.InvalidPhysicalFormat;
211 }
212
213 fn parseOperandsResultIds(
214 parser: *Parser,
215 binary: BinaryModule,
216 inst: Instruction,
217 operands: []const spec.Operand,
218 start_offset: usize,
219 offsets: *std.ArrayList(u16),
220 ) !usize {
221 var offset = start_offset;
222 for (operands) |operand| {
223 offset = try parser.parseOperandResultIds(binary, inst, operand, offset, offsets);
224 }
225 return offset;
226 }
227
228 fn parseOperandResultIds(
229 parser: *Parser,
230 binary: BinaryModule,
231 inst: Instruction,
232 operand: spec.Operand,
233 start_offset: usize,
234 offsets: *std.ArrayList(u16),
235 ) !usize {
236 var offset = start_offset;
237 switch (operand.quantifier) {
238 .variadic => while (offset < inst.operands.len) {
239 offset = try parser.parseOperandKindResultIds(binary, inst, operand.kind, offset, offsets);
240 },
241 .optional => if (offset < inst.operands.len) {
242 offset = try parser.parseOperandKindResultIds(binary, inst, operand.kind, offset, offsets);
243 },
244 .required => {
245 offset = try parser.parseOperandKindResultIds(binary, inst, operand.kind, offset, offsets);
246 },
247 }
248 return offset;
249 }
250
251 fn parseOperandKindResultIds(
252 parser: *Parser,
253 binary: BinaryModule,
254 inst: Instruction,
255 kind: spec.OperandKind,
256 start_offset: usize,
257 offsets: *std.ArrayList(u16),
258 ) !usize {
259 var offset = start_offset;
260 if (offset >= inst.operands.len) return error.InvalidPhysicalFormat;
261
262 switch (kind.category()) {
263 .bit_enum => {
264 const mask = inst.operands[offset];
265 offset += 1;
266 for (kind.enumerants()) |enumerant| {
267 if ((mask & enumerant.value) != 0) {
268 for (enumerant.parameters) |param_kind| {
269 offset = try parser.parseOperandKindResultIds(binary, inst, param_kind, offset, offsets);
270 }
271 }
272 }
273 },
274 .value_enum => {
275 const value = inst.operands[offset];
276 offset += 1;
277 for (kind.enumerants()) |enumerant| {
278 if (value == enumerant.value) {
279 for (enumerant.parameters) |param_kind| {
280 offset = try parser.parseOperandKindResultIds(binary, inst, param_kind, offset, offsets);
281 }
282 break;
283 }
284 }
285 },
286 .id => {
287 try offsets.append(parser.gpa, @intCast(offset));
288 offset += 1;
289 },
290 else => switch (kind) {
291 .literal_integer, .literal_float => offset += 1,
292 .literal_string => while (true) {
293 if (offset >= inst.operands.len) return error.InvalidPhysicalFormat;
294 const word = inst.operands[offset];
295 offset += 1;
296
297 if (word & 0xFF000000 == 0 or
298 word & 0x00FF0000 == 0 or
299 word & 0x0000FF00 == 0 or
300 word & 0x000000FF == 0)
301 {
302 break;
303 }
304 },
305 .literal_context_dependent_number => {
306 assert(inst.opcode == .OpConstant or inst.opcode == .OpSpecConstant);
307 const bit_width = binary.arith_type_width.get(@fromBackingInt(@intCast(inst.operands[0]))) orelse {
308 log.err("invalid LiteralContextDependentNumber type {}", .{inst.operands[0]});
309 return error.InvalidId;
310 };
311 offset += switch (bit_width) {
312 1...32 => 1,
313 33...64 => 2,
314 else => unreachable,
315 };
316 },
317 .literal_ext_inst_integer => unreachable,
318 .literal_spec_constant_op_integer => unreachable,
319 .pair_literal_integer_id_ref => {
320 assert(inst.opcode == .OpSwitch);
321 const bit_width = binary.arith_type_width.get(@fromBackingInt(@intCast(inst.operands[0]))) orelse {
322 log.err("invalid OpSwitch type {}", .{inst.operands[0]});
323 return error.InvalidId;
324 };
325 offset += switch (bit_width) {
326 1...32 => 1,
327 33...64 => 2,
328 else => unreachable,
329 };
330 try offsets.append(parser.gpa, @intCast(offset));
331 offset += 1;
332 },
333 .pair_id_ref_literal_integer => {
334 try offsets.append(parser.gpa, @intCast(offset));
335 offset += 2;
336 },
337 .pair_id_ref_id_ref => {
338 try offsets.append(parser.gpa, @intCast(offset));
339 try offsets.append(parser.gpa, @intCast(offset + 1));
340 offset += 2;
341 },
342 else => unreachable,
343 },
344 }
345 return offset;
346 }
347};