1const std = @import("std");
2const Io = std.Io;
3const Allocator = std.mem.Allocator;
4const assert = std.debug.assert;
5
6const g = @import("spirv/grammar.zig");
7const CoreRegistry = g.CoreRegistry;
8const ExtensionRegistry = g.ExtensionRegistry;
9const Instruction = g.Instruction;
10const OperandKind = g.OperandKind;
11const Enumerant = g.Enumerant;
12const Operand = g.Operand;
13
14const ExtendedStructSet = std.StringHashMap(void);
15
16const allowed_vendors = [_][]const u8{
17 "KHR",
18 "EXT",
19};
20
21fn isAllowedCapability(name: []const u8) bool {
22 // core capabilities (no vendor suffix) end in a lowercase letter or digit.
23 const last = name[name.len - 1];
24 if (std.ascii.isLower(last) or std.ascii.isDigit(last)) return true;
25 for (allowed_vendors) |vendor| {
26 if (std.mem.endsWith(u8, name, vendor)) return true;
27 }
28 return false;
29}
30
31fn isAllowedExtension(name: []const u8) bool {
32 const spv_prefix = "SPV_";
33 if (!std.mem.startsWith(u8, name, spv_prefix)) return false;
34 const tail = name[spv_prefix.len..];
35 for (allowed_vendors) |vendor| {
36 if (std.mem.startsWith(u8, tail, vendor) and
37 tail.len > vendor.len and tail[vendor.len] == '_')
38 return true;
39 }
40 return false;
41}
42
43const Extension = struct {
44 name: []const u8,
45 opcode_name: []const u8,
46 spec: ExtensionRegistry,
47};
48
49const CmpInst = struct {
50 fn lt(_: CmpInst, a: Instruction, b: Instruction) bool {
51 return a.opcode < b.opcode;
52 }
53};
54
55const StringPair = struct { []const u8, []const u8 };
56
57const StringPairContext = struct {
58 pub fn hash(_: @This(), a: StringPair) u32 {
59 var hasher = std.hash.Wyhash.init(0);
60 const x, const y = a;
61 hasher.update(x);
62 hasher.update(y);
63 return @truncate(hasher.final());
64 }
65
66 pub fn eql(_: @This(), a: StringPair, b: StringPair, b_index: usize) bool {
67 _ = b_index;
68 const a_x, const a_y = a;
69 const b_x, const b_y = b;
70 return std.mem.eql(u8, a_x, b_x) and std.mem.eql(u8, a_y, b_y);
71 }
72};
73
74const OperandKindMap = std.array_hash_map.Custom(StringPair, OperandKind, StringPairContext, true);
75
76/// Khronos made it so that these names are not defined explicitly, so
77/// we need to hardcode it (like they did).
78/// See https://github.com/KhronosGroup/SPIRV-Registry
79const set_names = std.StaticStringMap(struct { []const u8, []const u8 }).initComptime(.{
80 .{ "opencl.std.100", .{ "OpenCL.std", "OpenClOpcode" } },
81 .{ "glsl.std.450", .{ "GLSL.std.450", "GlslOpcode" } },
82 .{ "zig", .{ "zig", "Zig" } },
83});
84
85pub fn main(init: std.process.Init) !void {
86 const arena = init.arena.allocator();
87 const args = try init.minimal.args.toSlice(arena);
88 if (args.len != 3) {
89 usageAndExit(args[0], 1);
90 }
91
92 const io = init.io;
93
94 const json_path = try Io.Dir.path.join(arena, &.{ args[1], "include/spirv/unified1/" });
95 const dir = try Io.Dir.cwd().openDir(io, json_path, .{ .iterate = true });
96
97 const core_spec = try readRegistry(io, arena, CoreRegistry, dir, "spirv.core.grammar.json");
98 std.mem.sortUnstable(Instruction, core_spec.instructions, CmpInst{}, CmpInst.lt);
99
100 var exts = std.array_list.Managed(Extension).init(arena);
101
102 var it = dir.iterate();
103 while (try it.next(io)) |entry| {
104 if (entry.kind != .file) {
105 continue;
106 }
107
108 try readExtRegistry(io, arena, &exts, dir, entry.name);
109 }
110
111 try readExtRegistry(io, arena, &exts, Io.Dir.cwd(), args[2]);
112
113 var allocating: std.Io.Writer.Allocating = .init(arena);
114 defer allocating.deinit();
115 try render(arena, &allocating.writer, core_spec, exts.items);
116 try allocating.writer.writeByte(0);
117 const output = allocating.written()[0 .. allocating.written().len - 1 :0];
118
119 var tree = try std.zig.Ast.parse(arena, output, .{});
120
121 if (tree.errors.len != 0) {
122 try std.zig.printAstErrorsToStderr(arena, io, tree, "", .auto);
123 return;
124 }
125
126 var zir = try std.zig.AstGen.generate(arena, tree);
127 if (zir.hasCompileErrors()) {
128 var wip_errors: std.zig.ErrorBundle.Wip = undefined;
129 try wip_errors.init(arena);
130 defer wip_errors.deinit();
131 try wip_errors.addZirErrorMessages(zir, tree, output, "");
132 var error_bundle = try wip_errors.toOwnedBundle("");
133 defer error_bundle.deinit(arena);
134 try error_bundle.renderToStderr(io, .{}, .auto);
135 }
136
137 const formatted_output = try tree.renderAlloc(arena);
138 try Io.File.stdout().writeStreamingAll(io, formatted_output);
139}
140
141fn readExtRegistry(io: Io, arena: Allocator, exts: *std.array_list.Managed(Extension), dir: Io.Dir, sub_path: []const u8) !void {
142 const filename = Io.Dir.path.basename(sub_path);
143 if (!std.mem.startsWith(u8, filename, "extinst.")) {
144 return;
145 }
146
147 assert(std.mem.endsWith(u8, filename, ".grammar.json"));
148 const name = filename["extinst.".len .. filename.len - ".grammar.json".len];
149 const spec = try readRegistry(io, arena, ExtensionRegistry, dir, sub_path);
150
151 const set_name = set_names.get(name) orelse {
152 std.log.info("ignored instruction set '{s}'", .{name});
153 return;
154 };
155
156 std.sort.block(Instruction, spec.instructions, CmpInst{}, CmpInst.lt);
157
158 try exts.append(.{
159 .name = set_name.@"0",
160 .opcode_name = set_name.@"1",
161 .spec = spec,
162 });
163}
164
165fn readRegistry(io: Io, arena: Allocator, comptime RegistryType: type, dir: Io.Dir, path: []const u8) !RegistryType {
166 const spec = try dir.readFileAlloc(io, path, arena, .unlimited);
167 // Required for json parsing.
168 // TODO: ALI
169 @setEvalBranchQuota(10000);
170
171 var scanner = std.json.Scanner.initCompleteInput(arena, spec);
172 var diagnostics = std.json.Diagnostics{};
173 scanner.enableDiagnostics(&diagnostics);
174 const parsed = std.json.parseFromTokenSource(RegistryType, arena, &scanner, .{}) catch |err| {
175 std.debug.print("{s}:{}:{}:\n", .{ path, diagnostics.getLine(), diagnostics.getColumn() });
176 return err;
177 };
178 return parsed.value;
179}
180
181/// Returns a set with types that require an extra struct for the `Instruction` interface
182/// to the spir-v spec, or whether the original type can be used.
183fn extendedStructs(arena: Allocator, kinds: []const OperandKind) !ExtendedStructSet {
184 var map = ExtendedStructSet.init(arena);
185 try map.ensureTotalCapacity(@as(u32, @intCast(kinds.len)));
186
187 for (kinds) |kind| {
188 const enumerants = kind.enumerants orelse continue;
189
190 for (enumerants) |enumerant| {
191 if (enumerant.parameters.len > 0) {
192 break;
193 }
194 } else continue;
195
196 map.putAssumeCapacity(kind.kind, {});
197 }
198
199 return map;
200}
201
202// Return a score for a particular priority. Duplicate instruction/operand enum values are
203// removed by picking the tag with the lowest score to keep, and by making an alias for the
204// other. Note that the tag does not need to be just a tag at this point, in which case it
205// gets the lowest score automatically anyway.
206fn tagPriorityScore(tag: []const u8) usize {
207 if (tag.len == 0) {
208 return 1;
209 } else if (std.mem.eql(u8, tag, "EXT")) {
210 return 2;
211 } else if (std.mem.eql(u8, tag, "KHR")) {
212 return 3;
213 } else {
214 return 4;
215 }
216}
217
218fn render(
219 arena: Allocator,
220 writer: *std.Io.Writer,
221 registry: CoreRegistry,
222 extensions: []const Extension,
223) !void {
224 try writer.writeAll(
225 \\//! This file is auto-generated by tools/gen_spirv_spec.zig.
226 \\
227 \\const std = @import("std");
228 \\
229 \\pub const Version = packed struct(Word) {
230 \\ padding: u8 = 0,
231 \\ minor: u8,
232 \\ major: u8,
233 \\ padding0: u8 = 0,
234 \\
235 \\ pub fn toWord(self: @This()) Word {
236 \\ return @bitCast(self);
237 \\ }
238 \\};
239 \\
240 \\pub const Word = u32;
241 \\pub const Id = enum(Word) {
242 \\ none,
243 \\ _,
244 \\
245 \\ pub fn format(self: Id, writer: *std.Io.Writer) std.Io.Writer.Error!void {
246 \\ switch (self) {
247 \\ .none => try writer.writeAll("(none)"),
248 \\ else => try writer.print("%{d}", .{@intFromEnum(self)}),
249 \\ }
250 \\ }
251 \\};
252 \\
253 \\pub const IdRange = struct {
254 \\ base: u32,
255 \\ len: u32,
256 \\
257 \\ pub fn at(range: IdRange, i: usize) Id {
258 \\ std.debug.assert(i < range.len);
259 \\ return @enumFromInt(range.base + i);
260 \\ }
261 \\};
262 \\
263 \\pub const LiteralInteger = Word;
264 \\pub const LiteralFloat = Word;
265 \\pub const LiteralString = []const u8;
266 \\pub const LiteralContextDependentNumber = union(enum) {
267 \\ int32: i32,
268 \\ uint32: u32,
269 \\ int64: i64,
270 \\ uint64: u64,
271 \\ float32: f32,
272 \\ float64: f64,
273 \\};
274 \\pub const LiteralExtInstInteger = struct{ inst: Word };
275 \\pub const LiteralSpecConstantOpInteger = struct { opcode: Opcode };
276 \\pub const PairLiteralIntegerIdRef = struct { value: LiteralInteger, label: Id };
277 \\pub const PairIdRefLiteralInteger = struct { target: Id, member: LiteralInteger };
278 \\pub const PairIdRefIdRef = [2]Id;
279 \\
280 \\pub const Quantifier = enum {
281 \\ required,
282 \\ optional,
283 \\ variadic,
284 \\};
285 \\
286 \\pub const Operand = struct {
287 \\ kind: OperandKind,
288 \\ quantifier: Quantifier,
289 \\};
290 \\
291 \\pub const OperandCategory = enum {
292 \\ bit_enum,
293 \\ value_enum,
294 \\ id,
295 \\ literal,
296 \\ composite,
297 \\};
298 \\
299 \\pub const Enumerant = struct {
300 \\ name: []const u8,
301 \\ value: Word,
302 \\ parameters: []const OperandKind,
303 \\};
304 \\
305 \\pub const Instruction = struct {
306 \\ name: []const u8,
307 \\ opcode: Word,
308 \\ operands: []const Operand,
309 \\};
310 \\
311 \\pub const zig_generator_id: Word = 41;
312 \\
313 );
314
315 try writer.print(
316 \\pub const version: Version = .{{ .major = {}, .minor = {}, .patch = {} }};
317 \\pub const magic_number: Word = {s};
318 \\
319 \\
320 ,
321 .{ registry.major_version, registry.minor_version, registry.revision, registry.magic_number },
322 );
323
324 // Merge the operand kinds from all extensions together.
325 var all_operand_kinds: OperandKindMap = .empty;
326 for (registry.operand_kinds) |kind| {
327 try all_operand_kinds.putNoClobber(arena, .{ "core", kind.kind }, kind);
328 }
329 for (extensions) |ext| {
330 // Note: extensions may define the same operand kind, with different
331 // parameters. Instead of trying to merge them, just discriminate them
332 // using the name of the extension. This is similar to what
333 // the official headers do.
334
335 try all_operand_kinds.ensureUnusedCapacity(arena, ext.spec.operand_kinds.len);
336 for (ext.spec.operand_kinds) |kind| {
337 var new_kind = kind;
338 new_kind.kind = try std.mem.join(arena, ".", &.{ ext.name, kind.kind });
339 try all_operand_kinds.putNoClobber(arena, .{ ext.name, kind.kind }, new_kind);
340 }
341 }
342
343 const extended_structs = try extendedStructs(arena, all_operand_kinds.values());
344 // Note: extensions don't seem to have class.
345 try renderClass(arena, writer, registry.instructions);
346 try renderOperandKind(writer, all_operand_kinds.values());
347
348 try renderOpcodes(arena, writer, "Opcode", true, registry.instructions, extended_structs);
349 for (extensions) |ext| {
350 try renderOpcodes(arena, writer, ext.opcode_name, false, ext.spec.instructions, extended_structs);
351 }
352
353 try renderOperandKinds(arena, writer, all_operand_kinds.values(), extended_structs);
354 try renderInstructionSet(writer, registry, extensions, all_operand_kinds);
355 try renderExtension(arena, writer, all_operand_kinds.values());
356}
357
358fn renderExtension(
359 arena: Allocator,
360 writer: *std.Io.Writer,
361 kinds: []const OperandKind,
362) !void {
363 try writer.writeAll(
364 \\pub const Extension = enum {
365 \\v1_0,
366 \\v1_1,
367 \\v1_2,
368 \\v1_3,
369 \\v1_4,
370 \\v1_5,
371 \\v1_6,
372 \\
373 );
374
375 var seen_extensions: std.StringHashMapUnmanaged(void) = .empty;
376 defer seen_extensions.deinit(arena);
377
378 for (kinds) |kind| {
379 if (std.mem.eql(u8, "Capability", kind.kind)) {
380 for (kind.enumerants.?) |enumerant| {
381 if (!isAllowedCapability(enumerant.enumerant)) continue;
382 for (enumerant.extensions) |ext| {
383 if (!isAllowedExtension(ext)) continue;
384 if (seen_extensions.contains(ext)) continue;
385 try seen_extensions.put(arena, ext, {});
386 try writer.print("{s},\n", .{ext});
387 }
388 }
389 }
390 }
391 try writer.writeAll("};\n");
392}
393
394fn renderInstructionSet(
395 writer: *std.Io.Writer,
396 core: CoreRegistry,
397 extensions: []const Extension,
398 all_operand_kinds: OperandKindMap,
399) !void {
400 try writer.writeAll(
401 \\pub const InstructionSet = enum {
402 \\ core,
403 );
404
405 for (extensions) |ext| {
406 try writer.print("{f},\n", .{std.zig.fmtId(ext.name)});
407 }
408
409 try writer.writeAll(
410 \\
411 \\ pub fn instructions(self: InstructionSet) []const Instruction {
412 \\ return switch (self) {
413 \\
414 );
415
416 try renderInstructionsCase(writer, "core", core.instructions, all_operand_kinds);
417 for (extensions) |ext| {
418 try renderInstructionsCase(writer, ext.name, ext.spec.instructions, all_operand_kinds);
419 }
420
421 try writer.writeAll(
422 \\ };
423 \\ }
424 \\};
425 \\
426 );
427}
428
429fn renderInstructionsCase(
430 writer: *std.Io.Writer,
431 set_name: []const u8,
432 instructions: []const Instruction,
433 all_operand_kinds: OperandKindMap,
434) !void {
435 // Note: theoretically we could dedup from tags and give every instruction a list of aliases,
436 // but there aren't so many total aliases and that would add more overhead in total. We will
437 // just filter those out when needed.
438
439 try writer.print(".{f} => &.{{\n", .{std.zig.fmtId(set_name)});
440
441 for (instructions) |inst| {
442 try writer.print(
443 \\.{{
444 \\ .name = "{s}",
445 \\ .opcode = {},
446 \\ .operands = &.{{
447 \\
448 , .{ inst.opname, inst.opcode });
449
450 for (inst.operands) |operand| {
451 const quantifier = if (operand.quantifier) |q|
452 switch (q) {
453 .@"?" => "optional",
454 .@"*" => "variadic",
455 }
456 else
457 "required";
458
459 const kind = all_operand_kinds.get(.{ set_name, operand.kind }) orelse
460 all_operand_kinds.get(.{ "core", operand.kind }).?;
461 try writer.print(".{{.kind = .{f}, .quantifier = .{s}}},\n", .{ formatId(kind.kind), quantifier });
462 }
463
464 try writer.writeAll(
465 \\ },
466 \\},
467 \\
468 );
469 }
470
471 try writer.writeAll(
472 \\},
473 \\
474 );
475}
476
477fn renderClass(arena: Allocator, writer: *std.Io.Writer, instructions: []const Instruction) !void {
478 var class_map: std.array_hash_map.String(void) = .empty;
479
480 for (instructions) |inst| {
481 if (std.mem.eql(u8, inst.class.?, "@exclude")) continue;
482 try class_map.put(arena, inst.class.?, {});
483 }
484
485 try writer.writeAll("pub const Class = enum {\n");
486 for (class_map.keys()) |class| {
487 try writer.print("{f},\n", .{formatId(class)});
488 }
489 try writer.writeAll("};\n\n");
490}
491
492const Formatter = struct {
493 data: []const u8,
494
495 fn format(f: Formatter, writer: *std.Io.Writer) std.Io.Writer.Error!void {
496 var id_buf: [128]u8 = undefined;
497 var fw: std.Io.Writer = .fixed(&id_buf);
498 for (f.data, 0..) |c, i| {
499 switch (c) {
500 '-', '_', '.', '~', ' ' => fw.writeByte('_') catch return error.WriteFailed,
501 'a'...'z', '0'...'9' => fw.writeByte(c) catch return error.WriteFailed,
502 'A'...'Z' => {
503 if ((i > 0 and std.ascii.isLower(f.data[i - 1])) or
504 (i > 0 and std.ascii.isUpper(f.data[i - 1]) and
505 i + 1 < f.data.len and std.ascii.isLower(f.data[i + 1])))
506 {
507 _ = fw.write(&.{ '_', std.ascii.toLower(c) }) catch return error.WriteFailed;
508 } else {
509 fw.writeByte(std.ascii.toLower(c)) catch return error.WriteFailed;
510 }
511 },
512 else => unreachable,
513 }
514 }
515
516 // make sure that this won't clobber with zig keywords
517 try writer.print("{f}", .{std.zig.fmtId(fw.buffered())});
518 }
519};
520
521fn formatId(identifier: []const u8) std.fmt.Alt(Formatter, Formatter.format) {
522 return .{ .data = .{ .data = identifier } };
523}
524
525fn renderOperandKind(writer: *std.Io.Writer, operands: []const OperandKind) !void {
526 try writer.writeAll(
527 \\pub const OperandKind = enum {
528 \\ opcode,
529 \\
530 );
531 for (operands) |operand| {
532 try writer.print("{f},\n", .{formatId(operand.kind)});
533 }
534 try writer.writeAll(
535 \\
536 \\pub fn category(self: OperandKind) OperandCategory {
537 \\ return switch (self) {
538 \\ .opcode => .literal,
539 \\
540 );
541 for (operands) |operand| {
542 const cat = switch (operand.category) {
543 .BitEnum => "bit_enum",
544 .ValueEnum => "value_enum",
545 .Id => "id",
546 .Literal => "literal",
547 .Composite => "composite",
548 };
549 try writer.print(".{f} => .{s},\n", .{ formatId(operand.kind), cat });
550 }
551 try writer.writeAll(
552 \\ };
553 \\}
554 \\pub fn enumerants(self: OperandKind) []const Enumerant {
555 \\ return switch (self) {
556 \\ .opcode => unreachable,
557 \\
558 );
559 for (operands) |operand| {
560 switch (operand.category) {
561 .BitEnum, .ValueEnum => {},
562 else => {
563 try writer.print(".{f} => unreachable,\n", .{formatId(operand.kind)});
564 continue;
565 },
566 }
567
568 try writer.print(".{f} => &.{{", .{formatId(operand.kind)});
569 for (operand.enumerants.?) |enumerant| {
570 if (enumerant.value == .bitflag and std.mem.eql(u8, enumerant.enumerant, "None")) {
571 continue;
572 }
573 try renderEnumerant(writer, enumerant);
574 try writer.writeAll(",");
575 }
576 try writer.writeAll("},\n");
577 }
578 try writer.writeAll("};\n}\n};\n");
579}
580
581fn renderEnumerant(writer: *std.Io.Writer, enumerant: Enumerant) !void {
582 try writer.print(".{{.name = \"{s}\", .value = ", .{enumerant.enumerant});
583 switch (enumerant.value) {
584 .bitflag => |flag| try writer.writeAll(flag),
585 .int => |int| try writer.print("{}", .{int}),
586 }
587 try writer.writeAll(", .parameters = &.{");
588 for (enumerant.parameters, 0..) |param, i| {
589 if (i != 0)
590 try writer.writeAll(", ");
591 // Note, param.quantifier will always be one.
592 try writer.print(".{f}", .{formatId(param.kind)});
593 }
594 try writer.writeAll("}}");
595}
596
597fn renderOpcodes(
598 arena: Allocator,
599 writer: *std.Io.Writer,
600 opcode_type_name: []const u8,
601 want_operands: bool,
602 instructions: []const Instruction,
603 extended_structs: ExtendedStructSet,
604) !void {
605 var inst_map: std.array_hash_map.Auto(u32, usize) = .empty;
606 try inst_map.ensureTotalCapacity(arena, instructions.len);
607
608 var aliases = std.array_list.Managed(struct { inst: usize, alias: usize }).init(arena);
609 try aliases.ensureTotalCapacity(instructions.len);
610
611 for (instructions, 0..) |inst, i| {
612 if (inst.class) |class| {
613 if (std.mem.eql(u8, class, "@exclude")) continue;
614 }
615
616 const result = inst_map.getOrPutAssumeCapacity(inst.opcode);
617 if (!result.found_existing) {
618 result.value_ptr.* = i;
619 continue;
620 }
621
622 const existing = instructions[result.value_ptr.*];
623
624 const tag_index = std.mem.indexOfDiff(u8, inst.opname, existing.opname).?;
625 const inst_priority = tagPriorityScore(inst.opname[tag_index..]);
626 const existing_priority = tagPriorityScore(existing.opname[tag_index..]);
627
628 if (inst_priority < existing_priority) {
629 aliases.appendAssumeCapacity(.{ .inst = result.value_ptr.*, .alias = i });
630 result.value_ptr.* = i;
631 } else {
632 aliases.appendAssumeCapacity(.{ .inst = i, .alias = result.value_ptr.* });
633 }
634 }
635
636 const instructions_indices = inst_map.values();
637
638 try writer.print("\npub const {f} = enum(u16) {{\n", .{std.zig.fmtId(opcode_type_name)});
639 for (instructions_indices) |i| {
640 const inst = instructions[i];
641 try writer.print("{f} = {},\n", .{ std.zig.fmtId(inst.opname), inst.opcode });
642 }
643
644 try writer.writeAll("\n");
645
646 for (aliases.items) |alias| {
647 try writer.print("pub const {f} = {f}.{f};\n", .{
648 formatId(instructions[alias.inst].opname),
649 std.zig.fmtId(opcode_type_name),
650 formatId(instructions[alias.alias].opname),
651 });
652 }
653
654 if (want_operands) {
655 try writer.print(
656 \\
657 \\pub fn Operands(comptime self: {f}) type {{
658 \\ return switch (self) {{
659 \\
660 , .{std.zig.fmtId(opcode_type_name)});
661
662 for (instructions_indices) |i| {
663 const inst = instructions[i];
664 try renderOperand(writer, .instruction, inst.opname, inst.operands, extended_structs, false);
665 }
666
667 try writer.writeAll(
668 \\ };
669 \\}
670 \\
671 );
672
673 try writer.print(
674 \\pub fn class(self: {f}) Class {{
675 \\ return switch (self) {{
676 \\
677 , .{std.zig.fmtId(opcode_type_name)});
678
679 for (instructions_indices) |i| {
680 const inst = instructions[i];
681 try writer.print(".{f} => .{f},\n", .{ std.zig.fmtId(inst.opname), formatId(inst.class.?) });
682 }
683
684 try writer.writeAll(
685 \\ };
686 \\}
687 \\
688 );
689 }
690
691 try writer.writeAll(
692 \\};
693 \\
694 );
695}
696
697fn renderOperandKinds(
698 arena: Allocator,
699 writer: *std.Io.Writer,
700 kinds: []const OperandKind,
701 extended_structs: ExtendedStructSet,
702) !void {
703 for (kinds) |kind| {
704 switch (kind.category) {
705 .ValueEnum => try renderValueEnum(arena, writer, kind, extended_structs),
706 .BitEnum => try renderBitEnum(arena, writer, kind, extended_structs),
707 else => {},
708 }
709 }
710}
711
712fn renderValueEnum(
713 arena: Allocator,
714 writer: *std.Io.Writer,
715 enumeration: OperandKind,
716 extended_structs: ExtendedStructSet,
717) !void {
718 const enumerants = enumeration.enumerants orelse return error.InvalidRegistry;
719
720 var enum_map: std.array_hash_map.Auto(u32, usize) = .empty;
721 try enum_map.ensureTotalCapacity(arena, enumerants.len);
722
723 var aliases = std.array_list.Managed(struct { enumerant: usize, alias: usize }).init(arena);
724 try aliases.ensureTotalCapacity(enumerants.len);
725
726 for (enumerants, 0..) |enumerant, i| {
727 const value: u31 = switch (enumerant.value) {
728 .int => |value| value,
729 // Some extensions declare ints as string
730 .bitflag => |value| try std.fmt.parseInt(u31, value, 10),
731 };
732 const result = enum_map.getOrPutAssumeCapacity(value);
733 if (!result.found_existing) {
734 result.value_ptr.* = i;
735 continue;
736 }
737
738 const existing = enumerants[result.value_ptr.*];
739
740 const tag_index = std.mem.indexOfDiff(u8, enumerant.enumerant, existing.enumerant).?;
741 const enum_priority = tagPriorityScore(enumerant.enumerant[tag_index..]);
742 const existing_priority = tagPriorityScore(existing.enumerant[tag_index..]);
743
744 if (enum_priority < existing_priority) {
745 aliases.appendAssumeCapacity(.{ .enumerant = result.value_ptr.*, .alias = i });
746 result.value_ptr.* = i;
747 } else {
748 aliases.appendAssumeCapacity(.{ .enumerant = i, .alias = result.value_ptr.* });
749 }
750 }
751
752 const enum_indices = enum_map.values();
753
754 const is_capability = std.mem.eql(u8, "Capability", enumeration.kind);
755
756 try writer.print("pub const {f} = enum(u32) {{\n", .{std.zig.fmtId(enumeration.kind)});
757
758 for (enum_indices) |i| {
759 const enumerant = enumerants[i];
760 if (is_capability and !isAllowedCapability(enumerant.enumerant)) continue;
761 // if (enumerant.value != .int) return error.InvalidRegistry;
762
763 switch (enumerant.value) {
764 .int => |value| try writer.print("{f} = {},\n", .{ formatId(enumerant.enumerant), value }),
765 .bitflag => |value| try writer.print("{f} = {s},\n", .{ formatId(enumerant.enumerant), value }),
766 }
767 }
768
769 try writer.writeByte('\n');
770
771 for (aliases.items) |alias| {
772 if (is_capability and (!isAllowedCapability(enumerants[alias.enumerant].enumerant) or
773 !isAllowedCapability(enumerants[alias.alias].enumerant))) continue;
774 try writer.print("pub const {f} = {f}.{f};\n", .{
775 formatId(enumerants[alias.enumerant].enumerant),
776 std.zig.fmtId(enumeration.kind),
777 formatId(enumerants[alias.alias].enumerant),
778 });
779 }
780
781 if (is_capability) {
782 try writer.writeAll(
783 \\
784 \\pub fn dependencies(self: Capability) []const Extension {
785 \\ return switch (self) {
786 );
787
788 for (enum_indices) |i| {
789 const enumerant = enumerants[i];
790 if (!isAllowedCapability(enumerant.enumerant)) continue;
791
792 // Convert version to enum.
793 // None is for reserved
794 // Example: "None" -> .v1_0
795 // Example: "1.5" -> .v1_5
796 const enum_version = enumerant.version.?;
797 const version: [4]u8 = .{ 'v', '1', '_', if (enum_version[0] == 'N') '0' else enum_version[2] };
798
799 try writer.print("\n.{f} => &.{{.{s},", .{ formatId(enumerant.enumerant), version });
800 for (enumerant.extensions) |extension| {
801 if (!isAllowedExtension(extension)) continue;
802 try writer.print(".{s},", .{extension});
803 }
804 try writer.writeAll("},");
805 }
806
807 try writer.writeAll(
808 \\};
809 \\}
810 \\};
811 \\
812 );
813 return;
814 }
815
816 if (!extended_structs.contains(enumeration.kind)) {
817 try writer.writeAll("};\n");
818 return;
819 }
820
821 try writer.print("\npub const Extended = union({f}) {{\n", .{std.zig.fmtId(enumeration.kind)});
822
823 for (enum_indices) |i| {
824 const enumerant = enumerants[i];
825 try renderOperand(writer, .@"union", enumerant.enumerant, enumerant.parameters, extended_structs, true);
826 }
827
828 try writer.writeAll("};\n};\n");
829}
830
831fn renderBitEnum(
832 arena: Allocator,
833 writer: *std.Io.Writer,
834 enumeration: OperandKind,
835 extended_structs: ExtendedStructSet,
836) !void {
837 try writer.print("pub const {f} = packed struct {{\n", .{std.zig.fmtId(enumeration.kind)});
838
839 var flags_by_bitpos: [32]?usize = @splat(null);
840 const enumerants = enumeration.enumerants orelse return error.InvalidRegistry;
841
842 var aliases = std.array_list.Managed(struct { flag: usize, alias: u5 }).init(arena);
843 try aliases.ensureTotalCapacity(enumerants.len);
844
845 for (enumerants, 0..) |enumerant, i| {
846 if (enumerant.value != .bitflag) return error.InvalidRegistry;
847 const value = try parseHexInt(enumerant.value.bitflag);
848 if (value == 0) {
849 continue; // Skip 'none' items
850 } else if (std.mem.eql(u8, enumerant.enumerant, "FlagIsPublic")) {
851 // This flag is special and poorly defined in the json files.
852 // Just skip it for now
853 continue;
854 }
855
856 assert(@popCount(value) == 1);
857
858 const bitpos = std.math.log2_int(u32, value);
859 if (flags_by_bitpos[bitpos]) |*existing| {
860 const tag_index = std.mem.indexOfDiff(u8, enumerant.enumerant, enumerants[existing.*].enumerant).?;
861 const enum_priority = tagPriorityScore(enumerant.enumerant[tag_index..]);
862 const existing_priority = tagPriorityScore(enumerants[existing.*].enumerant[tag_index..]);
863
864 if (enum_priority < existing_priority) {
865 aliases.appendAssumeCapacity(.{ .flag = existing.*, .alias = bitpos });
866 existing.* = i;
867 } else {
868 aliases.appendAssumeCapacity(.{ .flag = i, .alias = bitpos });
869 }
870 } else {
871 flags_by_bitpos[bitpos] = i;
872 }
873 }
874
875 for (flags_by_bitpos, 0..) |maybe_flag_index, bitpos| {
876 if (maybe_flag_index) |flag_index| {
877 try writer.print("{f}", .{formatId(enumerants[flag_index].enumerant)});
878 } else {
879 try writer.print("_reserved_bit_{}", .{bitpos});
880 }
881
882 try writer.writeAll(": bool = false,\n");
883 }
884
885 try writer.writeByte('\n');
886
887 for (aliases.items) |alias| {
888 try writer.print("pub const {f}: {f} = .{{.{f} = true}};\n", .{
889 formatId(enumerants[alias.flag].enumerant),
890 std.zig.fmtId(enumeration.kind),
891 formatId(enumerants[flags_by_bitpos[alias.alias].?].enumerant),
892 });
893 }
894
895 if (!extended_structs.contains(enumeration.kind)) {
896 try writer.writeAll("};\n");
897 return;
898 }
899
900 try writer.print("\npub const Extended = struct {{\n", .{});
901
902 for (flags_by_bitpos, 0..) |maybe_flag_index, bitpos| {
903 const flag_index = maybe_flag_index orelse {
904 try writer.print("_reserved_bit_{}: bool = false,\n", .{bitpos});
905 continue;
906 };
907 const enumerant = enumerants[flag_index];
908
909 try renderOperand(writer, .mask, enumerant.enumerant, enumerant.parameters, extended_structs, true);
910 }
911
912 try writer.writeAll("};\n};\n");
913}
914
915fn renderOperand(
916 writer: *std.Io.Writer,
917 kind: enum {
918 @"union",
919 instruction,
920 mask,
921 },
922 field_name: []const u8,
923 parameters: []const Operand,
924 extended_structs: ExtendedStructSet,
925 snake_case: bool,
926) !void {
927 if (kind == .instruction) {
928 try writer.writeByte('.');
929 }
930
931 if (snake_case) {
932 try writer.print("{f}", .{formatId(field_name)});
933 } else {
934 try writer.print("{f}", .{std.zig.fmtId(field_name)});
935 }
936
937 if (parameters.len == 0) {
938 switch (kind) {
939 .@"union" => try writer.writeAll(",\n"),
940 .instruction => try writer.writeAll(" => void,\n"),
941 .mask => try writer.writeAll(": bool = false,\n"),
942 }
943 return;
944 }
945
946 if (kind == .instruction) {
947 try writer.writeAll(" => ");
948 } else {
949 try writer.writeAll(": ");
950 }
951
952 if (kind == .mask) {
953 try writer.writeByte('?');
954 }
955
956 try writer.writeAll("struct {");
957
958 for (parameters, 0..) |param, j| {
959 if (j != 0) {
960 try writer.writeAll(", ");
961 }
962
963 try renderFieldName(writer, parameters, j);
964 try writer.writeAll(": ");
965
966 if (param.quantifier) |q| {
967 switch (q) {
968 .@"?" => try writer.writeByte('?'),
969 .@"*" => try writer.writeAll("[]const "),
970 }
971 }
972
973 if (std.mem.startsWith(u8, param.kind, "Id")) {
974 _ = try writer.write("Id");
975 } else {
976 try writer.print("{f}", .{std.zig.fmtId(param.kind)});
977 }
978
979 if (extended_structs.contains(param.kind)) {
980 try writer.writeAll(".Extended");
981 }
982
983 if (param.quantifier) |q| {
984 switch (q) {
985 .@"?" => try writer.writeAll(" = null"),
986 .@"*" => try writer.writeAll(" = &.{}"),
987 }
988 }
989 }
990
991 try writer.writeAll("}");
992
993 if (kind == .mask) {
994 try writer.writeAll(" = null");
995 }
996
997 try writer.writeAll(",\n");
998}
999
1000fn renderFieldName(writer: *std.Io.Writer, operands: []const Operand, field_index: usize) !void {
1001 const operand = operands[field_index];
1002
1003 derive_from_kind: {
1004 // Operand names are often in the json encoded as "'Name'" (with two sets of quotes).
1005 // Additionally, some operands have ~ in them at the end (D~ref~).
1006 const name = std.mem.trim(u8, operand.name, "'~");
1007 if (name.len == 0) break :derive_from_kind;
1008
1009 for (name) |c| {
1010 switch (c) {
1011 'a'...'z', '0'...'9', 'A'...'Z', ' ', '~' => continue,
1012 else => break :derive_from_kind,
1013 }
1014 }
1015
1016 try writer.print("{f}", .{formatId(name)});
1017 return;
1018 }
1019
1020 try writer.print("{f}", .{formatId(operand.kind)});
1021
1022 // For fields derived from type name, there could be any amount.
1023 // Simply check against all other fields, and if another similar one exists, add a number.
1024 const need_extra_index = for (operands, 0..) |other_operand, i| {
1025 if (i != field_index and std.mem.eql(u8, operand.kind, other_operand.kind)) {
1026 break true;
1027 }
1028 } else false;
1029
1030 if (need_extra_index) {
1031 try writer.print("_{}", .{field_index});
1032 }
1033}
1034
1035fn parseHexInt(text: []const u8) !u31 {
1036 const prefix = "0x";
1037 if (!std.mem.startsWith(u8, text, prefix))
1038 return error.InvalidHexInt;
1039 return try std.fmt.parseInt(u31, text[prefix.len..], 16);
1040}
1041
1042fn usageAndExit(arg0: []const u8, code: u8) noreturn {
1043 const stderr = std.debug.lockStderr(&.{});
1044 const w = &stderr.file_writer.interface;
1045 w.print(
1046 \\Usage: {s} <SPIRV-Headers repository path> <path/to/zig/src/codegen/spirv/extinst.zig.grammar.json>
1047 \\
1048 \\Generates Zig bindings for SPIR-V specifications found in the SPIRV-Headers
1049 \\repository. The result, printed to stdout, should be used to update
1050 \\files in src/codegen/spirv. Don't forget to format the output.
1051 \\
1052 \\<SPIRV-Headers repository path> should point to a clone of
1053 \\https://github.com/KhronosGroup/SPIRV-Headers/
1054 \\
1055 , .{arg0}) catch std.process.exit(1);
1056 std.process.exit(code);
1057}