authorgravatar for xtex@astrafall.orgxtex <xtex@astrafall.org> 2026-08-07 14:52:44+08:00
committergravatar for xtex@astrafall.orgxtex <xtex@astrafall.org> 2026-08-08 10:53:46+08:00
log0fe189415864329769ae5967d504c0574fe41060
tree0b0f425e459e7bfb47e10cbcabf0cef491955fe7
parent2597da02544c05f7de6c209d9d4d509b74e63f73
signaturebadge-check Signed by SSH key SHA256:IEYEjkZlkUTr5U9GiDAmZU/4eZus2t2RsxusyhQqwao

tools: add LoongArch opcode encoding generator

This tool reads data from loongarch-opcodes.git and generates three data files. encoding.zig contains an enum of mnemonics, a packed union of packed structs to represent bit-fields in instructions and encoder functions for each instruction. inst_formats.zon contains required target feature set for each instruction, which can be used by the backend to, under debug mode, verify if it emits any instruction that is not supported by the compilation target. It also contains information about slots in each instruction format, their type, location and postprocessing, which can be used by disassemblers. decode_tree.zon is the auto-generated decode tree, which will be used by disassemblers. Signed-off-by: xtex <xtex@astrafall.org>

4 files changed, 1139 insertions(+), 0 deletions(-)

tools/gen_loongarch_encoding.zig created+526
......@@ -0,0 +1,526 @@
1//! Example usage:
2//! git clone https://github.com/loongson-community/loongarch-opcodes.git ../loongarch-opcodes
3//! zig run tools/gen_loongarch_encoding.zig -- ../loongarch-opcodes .
4
5const std = @import("std");
6const fs = std.fs;
7const Allocator = std.mem.Allocator;
8const print = std.debug.print;
9const Writer = std.Io.Writer;
10const ZonSerializer = std.zon.Serializer;
11
12const OpcodeDesc = @import("loongarch/OpcodeDesc.zig");
13const decode_tree = @import("loongarch/decode_tree.zig");
14
15pub fn main(init: std.process.Init) !void {
16 const arena = init.arena.allocator();
17 const io = init.io;
18
19 var args = try init.minimal.args.iterateAllocator(arena);
20 const arg0 = args.next().?;
21 const opcodes_path = args.next() orelse usageAndExit(arg0, 0);
22 const zig_path = args.next() orelse usageAndExit(arg0, 1);
23 args.deinit();
24
25 var desc: OpcodeDesc = .{};
26 defer desc.deinit(arena);
27
28 var zig_dir = try std.Io.Dir.cwd().openDir(io, zig_path, .{});
29 defer zig_dir.close(io);
30
31 // load opcode data
32 {
33 print("Loading description files ..\n", .{});
34 var opcodes_dir = try std.Io.Dir.cwd().openDir(io, opcodes_path, .{ .iterate = true });
35 defer opcodes_dir.close(io);
36 var opcodes_iter = opcodes_dir.iterateAssumeFirstIteration();
37 while (try opcodes_iter.next(io)) |opcodes_file| {
38 if (opcodes_file.kind != .file) continue;
39 if (!std.mem.endsWith(u8, opcodes_file.name, ".txt")) continue;
40
41 print("Loading {s} ...\n", .{opcodes_file.name});
42 const data = try opcodes_dir.readFileAlloc(io, opcodes_file.name, arena, .unlimited);
43 try desc.parse(arena, data);
44 // `data` is intentionally leaked here because it must live longer than `desc`
45 // ArenaAllocator should clean them up.
46 }
47
48 print("Loading extra.txt ...\n", .{});
49 const data = try zig_dir.readFileAlloc(io, "tools/loongarch/extra.txt", arena, .unlimited);
50 try desc.parse(arena, data);
51
52 print("Loaded {} instructions, {} formats\n", .{ desc.opcode.items.len, desc.format.count() });
53 desc.sort();
54 print("Sorted data\n", .{});
55 }
56
57 // generate encoding.zig
58 {
59 print("Writing encoding.zig ...\n", .{});
60 var buffer: Writer.Allocating = .init(arena);
61 defer buffer.deinit();
62 const writer = &buffer.writer;
63
64 try writer.print(
65 \\// DO NOT MODIFY. This file is generated by tools/gen_loongarch_encoding.zig.
66 \\const Register = @import("bits.zig").Register;
67 \\
68 , .{});
69
70 // mnemonic enum
71 {
72 try writer.print("\npub const Mnemonic = enum {{\n", .{});
73 for (desc.opcode.items) |*opcode| {
74 try writer.print(" {f},\n", .{std.zig.fmtIdPU(opcode.name)});
75 }
76 try writer.print("}};\n", .{});
77 }
78
79 // instruction struct
80 {
81 try writer.print(
82 \\
83 \\pub const Instruction = packed union {{
84 \\ word: u32,
85 \\
86 , .{});
87
88 // format-based variants
89 {
90 var format_iter = desc.format.iterator();
91 while (format_iter.next()) |entry|
92 try printFormatStruct(writer, entry.key_ptr.*, entry.value_ptr.*);
93 }
94
95 // format-based encoders
96 {
97 var format_iter = desc.format.iterator();
98 while (format_iter.next()) |entry|
99 try printFormatEncoder(writer, entry.key_ptr.*, entry.value_ptr.*);
100 }
101
102 // opcode-based encoders
103 for (desc.opcode.items) |*opcode| {
104 const encoder_format = if (std.mem.eql(u8, opcode.name, opcode.orig_name)) opcode.orig_format else opcode.format;
105 try printInstructionEncoder(writer, opcode, encoder_format);
106 }
107
108 try writer.print("}};\n", .{});
109 }
110
111 try zig_dir.writeFile(io, .{
112 .sub_path = "src/codegen/loongarch/encoding.zig",
113 .data = buffer.written(),
114 });
115 }
116
117 // generate decode_tree.zon
118 {
119 print("Writing decode_tree.zon ...\n", .{});
120 var buffer: Writer.Allocating = .init(arena);
121 defer buffer.deinit();
122 const writer = &buffer.writer;
123
124 try writer.print(
125 \\// DO NOT MODIFY. This file is generated by tools/gen_loongarch_encoding.zig.
126 \\
127 , .{});
128
129 try printDecodeTree(writer, arena, &desc);
130
131 try writer.writeAll("\n");
132
133 try zig_dir.writeFile(io, .{
134 .sub_path = "src/codegen/loongarch/decode_tree.zon",
135 .data = buffer.written(),
136 });
137 }
138
139 // generate inst_formats.zon
140 {
141 print("Writing inst_formats.zon ...\n", .{});
142 var buffer: Writer.Allocating = .init(arena);
143 defer buffer.deinit();
144 const writer = &buffer.writer;
145
146 try writer.print(
147 \\// DO NOT MODIFY. This file is generated by tools/gen_loongarch_encoding.zig.
148 \\
149 , .{});
150
151 var s: ZonSerializer = .{
152 .writer = writer,
153 .options = .{},
154 };
155 try serializeInstFormats(&s, &desc);
156
157 try writer.writeAll("\n");
158
159 try zig_dir.writeFile(io, .{
160 .sub_path = "src/codegen/loongarch/inst_formats.zon",
161 .data = buffer.written(),
162 });
163 }
164
165 print("Done.\n", .{});
166}
167
168fn usageAndExit(arg0: []const u8, code: u8) noreturn {
169 print(
170 \\Usage: {s} /path/loongarch-opcodes /path/zig
171 \\
172 \\Updates LoongArch encoding data from loongarch-opcodes.git.
173 \\
174 , .{arg0});
175 std.process.exit(code);
176}
177
178fn printFormatStruct(writer: *Writer, name: []const u8, format: *const OpcodeDesc.Format) !void {
179 if (format.slots[0].tag == .none) return; // skips EMPTY format
180
181 try writer.print(" /// Fields of a `{s}` instruction.", .{name});
182 try writer.print("\n {s}: packed struct {{ ", .{name});
183 const Field = union(enum) {
184 funct: struct { width: u5 },
185 immediate: struct {
186 signedness: std.builtin.Signedness,
187 width: u5,
188 },
189 register: struct {
190 index: OpcodeDesc.Slot.Index,
191 width: u5,
192 },
193 };
194 var fields_buf: [4 * 2 + 1]Field = undefined;
195 var fields: std.ArrayList(Field) = .initBuffer(&fields_buf);
196
197 // collect fields
198 var bit_offset: u6 = 0;
199 var slots = format.slots;
200 std.mem.sort(OpcodeDesc.Slot, &slots, false, struct {
201 fn cmp(_: bool, lhs: OpcodeDesc.Slot, rhs: OpcodeDesc.Slot) bool {
202 if (lhs.tag == .none) return false;
203 if (rhs.tag == .none) return true;
204 return lhs.offset() < rhs.offset();
205 }
206 }.cmp);
207 for (slots) |slot| {
208 if (slot.tag == .none) break;
209 const slot_offset = slot.offset();
210 const slot_width = slot.width();
211 if (bit_offset != slot_offset)
212 fields.appendAssumeCapacity(.{ .funct = .{ .width = @truncate(slot_offset - bit_offset) } });
213
214 switch (slot.tag) {
215 .none => unreachable,
216 .imm => {
217 fields.appendAssumeCapacity(.{ .immediate = .{
218 .signedness = slot.payload.imm.signedness,
219 .width = slot_width,
220 } });
221 },
222 .reg => fields.appendAssumeCapacity(.{ .register = .{
223 .index = slot.payload.reg.index,
224 .width = slot_width,
225 } }),
226 }
227 bit_offset = slot_offset + slot_width;
228 }
229 if (bit_offset != 32)
230 fields.appendAssumeCapacity(.{ .funct = .{ .width = @intCast(@as(u6, 32) - bit_offset) } });
231
232 // detect shadowed immediate field names
233 const imm_shadowed = imm_shadowed: {
234 var imm_names: [std.math.maxInt(u6) + 1]bool = @splat(false);
235 for (fields.items) |field| {
236 switch (field) {
237 else => {},
238 .immediate => |imm_field| {
239 var imm_name_key: u6 = imm_field.width;
240 if (imm_field.signedness == .signed) imm_name_key |= std.math.maxInt(u5) + 1;
241 if (imm_names[imm_name_key]) break :imm_shadowed true;
242 imm_names[imm_name_key] = true;
243 },
244 }
245 }
246 break :imm_shadowed false;
247 };
248
249 // print fields
250 bit_offset = 0;
251 for (fields.items, 0..) |field, field_i| {
252 if (field_i != 0) try writer.writeAll(", ");
253 switch (field) {
254 .funct => |pl| {
255 try writer.print("funct{}: u{}", .{ bit_offset, pl.width });
256 bit_offset += pl.width;
257 },
258 .immediate => |pl| {
259 if (imm_shadowed) {
260 try writer.print("imm{}: {c}{}", .{
261 bit_offset,
262 @as(u8, switch (pl.signedness) {
263 .signed => 'i',
264 .unsigned => 'u',
265 }),
266 pl.width,
267 });
268 } else {
269 try writer.print("{c}i{}: {c}{}", .{
270 @as(u8, switch (pl.signedness) {
271 .signed => 's',
272 .unsigned => 'u',
273 }),
274 pl.width,
275 @as(u8, switch (pl.signedness) {
276 .signed => 'i',
277 .unsigned => 'u',
278 }),
279 pl.width,
280 });
281 }
282 bit_offset += pl.width;
283 },
284 .register => |pl| {
285 try writer.print("r{s}: u{}", .{ @tagName(pl.index), pl.width });
286 bit_offset += pl.width;
287 },
288 }
289 }
290 try writer.print(" }},\n", .{});
291}
292
293fn printFormatEncoder(writer: *Writer, name: []const u8, format: *const OpcodeDesc.Format) !void {
294 try writer.print("\n /// Encodes a `{s}` instruction.", .{name});
295 try writer.print("\n pub inline fn encode{s}(word: u32", .{name});
296 for (format.slots, 0..) |slot, slot_i| {
297 switch (slot.tag) {
298 .none => break,
299 .imm => {
300 const pl = slot.payload.imm;
301
302 try writer.print(", p{}: {c}{}", .{
303 slot_i,
304 @as(u8, switch (pl.signedness) {
305 .signed => 'i',
306 .unsigned => 'u',
307 }),
308 pl.length,
309 });
310 },
311 .reg => try writer.print(", p{}: Register", .{slot_i}),
312 }
313 }
314 try writer.print(") Instruction {{\n", .{});
315 if (format.slots[0].tag == .none) {
316 try writer.print(" return .{{ .word = word }};\n", .{});
317 } else {
318 try writer.print(" return .{{ .word = word", .{});
319 for (format.slots, 0..) |slot, slot_i| {
320 switch (slot.tag) {
321 .none => break,
322 .imm => {
323 const pl = slot.payload.imm;
324 try writer.print(" |\n (", .{});
325 try writer.print("(@as(u32, @as(u{}, @bitCast(p{})))", .{ pl.length, slot_i });
326
327 switch (pl.post_proc.tag) {
328 .none => {},
329 .add => try writer.print(" - {}", .{pl.post_proc.payload.add}),
330 .shl => try writer.print(" >> {}", .{pl.post_proc.payload.shl}),
331 }
332
333 try writer.print(") << {})", .{pl.index.offset()});
334 },
335 .reg => {
336 const pl = slot.payload.reg;
337 try writer.print(" |\n (@as(u32, p{}.encode()) << {})", .{ slot_i, pl.index.offset() });
338 },
339 }
340 }
341 try writer.print(" }};\n", .{});
342 }
343 try writer.print(" }}\n", .{});
344}
345
346fn printInstructionEncoder(writer: *Writer, opcode: *OpcodeDesc.Opcode, format: *const OpcodeDesc.Format) !void {
347 try writer.print("\n /// Encodes a `{s}` instruction", .{opcode.name});
348 if (opcode.required_features != OpcodeDesc.Opcode.RequiredFeatures{}) {
349 try writer.writeAll(" (requires ");
350 var first = true;
351 const feature_fields = comptime std.meta.fieldNames(OpcodeDesc.Opcode.RequiredFeatures);
352 inline for (feature_fields) |field| {
353 if (@field(opcode.required_features, field)) {
354 if (first) first = false else try writer.writeAll(" & ");
355 try writer.writeAll(field);
356 }
357 }
358 try writer.writeByte(')');
359 }
360 try writer.writeByte('.');
361 try writer.print("\n pub inline fn {f}(", .{std.zig.fmtIdPU(opcode.name)});
362 for (format.slots, 0..) |slot, slot_i| {
363 if (slot_i != 0 and slot.tag != .none)
364 try writer.print(", ", .{});
365 switch (slot.tag) {
366 .none => break,
367 .imm => {
368 const pl = slot.payload.imm;
369
370 try writer.print("p{}: {c}{}", .{
371 slot_i,
372 @as(u8, switch (pl.signedness) {
373 .signed => 'i',
374 .unsigned => 'u',
375 }),
376 pl.length,
377 });
378 },
379 .reg => try writer.print("p{}: Register", .{slot_i}),
380 }
381 }
382 try writer.print(") Instruction {{\n", .{});
383 try writer.print(" return encode{s}(0x{x:0>8}", .{ format.name, opcode.word });
384 for (format.slots, 0..) |slot, slot_i| {
385 switch (slot.tag) {
386 .none => break,
387 else => try writer.print(", p{}", .{slot_i}),
388 }
389 }
390 try writer.print(");\n", .{});
391 try writer.print(" }}\n", .{});
392}
393
394fn printDecodeTree(writer: *Writer, gpa: Allocator, desc: *const OpcodeDesc) !void {
395 var arena: std.heap.ArenaAllocator = .init(gpa);
396 defer arena.deinit();
397 const root_node = try decode_tree.populate(arena.allocator(), desc);
398 try printDecodeTreeNode(writer, root_node, 0);
399}
400
401fn printDecodeTreeNode(writer: *Writer, node: *const decode_tree.Node, indent: usize) !void {
402 if (node.mask == 0) {
403 try writer.print(".{{ .instruction = .{f} }}", .{std.zig.fmtId(node.next.instruction.name)});
404 } else {
405 try writer.print(".{{ .mask = 0x{x:0>8}, .cases = .{{\n", .{node.mask});
406 for (node.next.cases) |*case| {
407 try printIndentation(writer, indent + 1);
408 if (case.catch_all) {
409 try writer.print(".{{ .then = ", .{});
410 try printDecodeTreeNode(writer, case.child, indent + 1);
411 try writer.print(" }},\n", .{});
412 } else {
413 try writer.print(".{{ .value = 0x{x:0>8}, .then = ", .{case.variant});
414 try printDecodeTreeNode(writer, case.child, indent + 1);
415 try writer.print(" }},\n", .{});
416 }
417 }
418 try printIndentation(writer, indent);
419 try writer.print("}} }}", .{});
420 }
421}
422
423fn printIndentation(writer: *Writer, indent: usize) !void {
424 try writer.splatByteAll(' ', 4 * indent);
425}
426
427fn serializeInstFormats(s: *ZonSerializer, desc: *const OpcodeDesc) !void {
428 var root_struct = try s.beginStruct(.{});
429
430 {
431 var instructions_s = try root_struct.beginStructField("instructions", .{});
432 for (desc.opcode.items) |*opcode| {
433 var opcode_s = try instructions_s.beginStructField(opcode.name, .{});
434
435 try opcode_s.fieldPrefix("word");
436 try s.writer.writeAll("0x");
437 try s.writer.printInt(opcode.word, 16, .lower, .{
438 .alignment = .right,
439 .fill = '0',
440 .width = 8,
441 });
442
443 try opcode_s.fieldPrefix("format");
444 try s.ident(opcode.format.name);
445 if (opcode.orig_format != opcode.format) {
446 try opcode_s.fieldPrefix("orig_format");
447 try s.ident(opcode.orig_format.name);
448 }
449
450 if (opcode.orig_name.ptr != opcode.name.ptr)
451 try opcode_s.field("orig_name", opcode.orig_name, .{});
452
453 const field_names = comptime std.meta.fieldNames(OpcodeDesc.Opcode.RequiredFeatures);
454 var num_features: u32 = 0;
455 inline for (field_names) |field| {
456 if (@field(opcode.required_features, field))
457 num_features += 1;
458 }
459 var features_s = try opcode_s.beginTupleField("features", .{ .whitespace_style = .{ .fields = num_features } });
460 inline for (field_names) |field| {
461 if (@field(opcode.required_features, field)) {
462 try features_s.fieldPrefix();
463 try s.ident(field);
464 }
465 }
466 try features_s.end();
467
468 try opcode_s.end();
469 }
470 try instructions_s.end();
471 }
472
473 {
474 var formats_s = try root_struct.beginStructField("formats", .{});
475 for (desc.format.values()) |format| {
476 var format_s = try formats_s.beginStructField(format.name, .{ .whitespace_style = .{ .wrap = false } });
477
478 var slots_s = try format_s.beginTupleField("slots", .{});
479 for (format.slots) |slot| {
480 if (slot.tag == .none) break;
481 var slot_s = try slots_s.beginStructField(.{ .whitespace_style = .{ .wrap = false } });
482 switch (slot.tag) {
483 .none => unreachable,
484 .reg => {
485 const pl = slot.payload.reg;
486 var reg_s = try slot_s.beginStructField("reg", .{ .whitespace_style = .{ .fields = 2 } });
487 try reg_s.field("location", pl.index.offset(), .{});
488
489 try reg_s.fieldPrefix("class");
490 try s.ident(@tagName(pl.class));
491
492 try reg_s.end();
493 },
494 .imm => {
495 const pl = slot.payload.imm;
496 var imm_s = try slot_s.beginStructField("imm", .{ .whitespace_style = .{ .wrap = false } });
497 try imm_s.field("location", pl.index.offset(), .{});
498 try imm_s.field("length", pl.length, .{});
499
500 try imm_s.fieldPrefix("signedness");
501 try s.ident(@tagName(pl.signedness));
502
503 if (pl.post_proc.tag != .none) {
504 var pp_s = try imm_s.beginStructField("post_proc", .{ .whitespace_style = .{ .fields = 1 } });
505 switch (pl.post_proc.tag) {
506 .none => unreachable,
507 .add => try pp_s.field("add", pl.post_proc.payload.add, .{}),
508 .shl => try pp_s.field("shl", pl.post_proc.payload.shl, .{}),
509 }
510 try pp_s.end();
511 }
512
513 try imm_s.end();
514 },
515 }
516 try slot_s.end();
517 }
518 try slots_s.end();
519
520 try format_s.end();
521 }
522 try formats_s.end();
523 }
524
525 try root_struct.end();
526}
tools/loongarch/OpcodeDesc.zig created+455
......@@ -0,0 +1,455 @@
1//! Parser for format description files in
2//! https://github.com/loongson-community/loongarch-opcodes.
3
4const std = @import("std");
5const mem = std.mem;
6const Allocator = mem.Allocator;
7const Reader = std.Io.Reader;
8
9const OpcodeDesc = @This();
10
11/// Maximum number of slots in one instruction format.
12const max_slots = 4;
13
14opcode: std.ArrayList(Opcode) = .empty,
15format_pool: std.heap.MemoryPool(Format) = .empty,
16format: std.StringArrayHashMapUnmanaged(*Format) = .empty,
17
18pub fn deinit(desc: *OpcodeDesc, gpa: Allocator) void {
19 desc.opcode.deinit(gpa);
20 desc.format.deinit(gpa);
21 desc.format_pool.deinit(gpa);
22}
23
24/// Instruction format. Slots are filled one by one, ending with reaching max_slots or a .none slot.
25pub const Format = struct {
26 name: []const u8,
27 slots: [max_slots]Slot,
28
29 pub fn parse(name: []const u8) !Format {
30 var format: Format = .{
31 .name = name,
32 .slots = .{ .none, .none, .none, .none },
33 };
34 var reader: Reader = .fixed(name);
35 var slot_index: std.math.IntFittingRange(0, max_slots) = 0;
36 parse_empty: {
37 const str = reader.peekArray(5) catch |err| switch (err) {
38 error.EndOfStream => break :parse_empty,
39 else => return err,
40 };
41 if (mem.eql(u8, &str.*, "EMPTY"))
42 return format;
43 }
44
45 parse_slots: while (slot_index < max_slots) : (slot_index += 1) {
46 switch (reader.takeByte() catch |err| switch (err) {
47 error.EndOfStream => break :parse_slots,
48 else => return err,
49 }) {
50 'D' => format.slots[slot_index] = .{ .tag = .reg, .payload = .{ .reg = .{
51 .class = .int,
52 .index = .d,
53 } } },
54 'J' => format.slots[slot_index] = .{ .tag = .reg, .payload = .{ .reg = .{
55 .class = .int,
56 .index = .j,
57 } } },
58 'K' => format.slots[slot_index] = .{ .tag = .reg, .payload = .{ .reg = .{
59 .class = .int,
60 .index = .k,
61 } } },
62 'A' => format.slots[slot_index] = .{ .tag = .reg, .payload = .{ .reg = .{
63 .class = .int,
64 .index = .a,
65 } } },
66 'F' => format.slots[slot_index] = .{ .tag = .reg, .payload = .{ .reg = .{
67 .class = .fp,
68 .index = try .parse(&reader),
69 } } },
70 'C' => format.slots[slot_index] = .{ .tag = .reg, .payload = .{ .reg = .{
71 .class = .fcc,
72 .index = try .parse(&reader),
73 } } },
74 'T' => format.slots[slot_index] = .{ .tag = .reg, .payload = .{ .reg = .{
75 .class = .lbt_scratch,
76 .index = try .parse(&reader),
77 } } },
78 'V' => format.slots[slot_index] = .{ .tag = .reg, .payload = .{ .reg = .{
79 .class = .lsx,
80 .index = try .parse(&reader),
81 } } },
82 'X' => format.slots[slot_index] = .{ .tag = .reg, .payload = .{ .reg = .{
83 .class = .lasx,
84 .index = try .parse(&reader),
85 } } },
86 'S', 'U' => |signedness_ch| {
87 const signedness: std.builtin.Signedness = if (signedness_ch == 'S') .signed else .unsigned;
88 while (slot_index < max_slots and continue_imm_slot: {
89 _ = Slot.Index.fromChar(reader.peekByte() catch |err| switch (err) {
90 error.EndOfStream => break :continue_imm_slot false,
91 else => return err,
92 }) catch break :continue_imm_slot false;
93 break :continue_imm_slot true;
94 }) : (slot_index += 1) {
95 const index: Slot.Index = try .parse(&reader);
96 const length = try takeInteger(u5, &reader);
97 const post_proc = post_proc: {
98 if ('p' == reader.peekByte() catch |err| switch (err) {
99 error.EndOfStream => ' ',
100 else => return err,
101 }) {
102 reader.toss(1);
103 break :post_proc try Slot.PostProcess.parse(&reader);
104 } else break :post_proc Slot.PostProcess.none;
105 };
106 format.slots[slot_index] = .{ .tag = .imm, .payload = .{ .imm = .{
107 .index = index,
108 .length = length,
109 .signedness = signedness,
110 .post_proc = post_proc,
111 } } };
112 }
113 slot_index -= 1;
114 },
115 else => return error.InvalidCharacter,
116 }
117 }
118
119 return format;
120 }
121};
122
123test "parse format" {
124 _ = try Format.parse("DJFmSk12m13ps3");
125 _ = try Format.parse("DJSk12m13ps3U16pp1");
126 _ = try Format.parse("DJK");
127}
128
129pub const Slot = packed struct {
130 tag: Slot.Tag,
131 payload: Slot.Payload,
132
133 comptime {
134 std.debug.assert(@sizeOf(Slot) == 4);
135 }
136
137 const Payload = packed union {
138 none: u16, // unused number, just for padding
139 imm: packed struct {
140 index: Index,
141 length: u5,
142 signedness: std.builtin.Signedness,
143 post_proc: PostProcess = .none,
144 },
145 reg: packed struct {
146 class: enum(u13) { int, fp, fcc, lbt_scratch, lsx, lasx },
147 index: Index,
148 },
149 };
150
151 const Tag = enum(u16) { reg, imm, none };
152
153 pub const none: Slot = .{ .tag = .none, .payload = .{ .none = 0 } };
154
155 pub const Index = enum(u3) {
156 // zig fmt: off
157 d, j, k, a, m, n,
158 // zig fmt: on
159
160 pub fn offset(index: Index) u5 {
161 return switch (index) {
162 .d => 0,
163 .j => 5,
164 .k => 10,
165 .a => 15,
166 .m => 16,
167 .n => 18,
168 };
169 }
170
171 pub fn fromChar(ch: u8) error{UnknownIndexChar}!Index {
172 return switch (ch) {
173 'd' => .d,
174 'j' => .j,
175 'k' => .k,
176 'a' => .a,
177 'm' => .m,
178 'n' => .n,
179 else => return error.UnknownIndexChar,
180 };
181 }
182
183 pub const ParseError = Reader.Error || error{UnknownIndexChar};
184 pub fn parse(reader: *Reader) Index.ParseError!Index {
185 return fromChar(try reader.takeByte());
186 }
187 };
188
189 /// Post-process operations for disassemblying.
190 pub const PostProcess = packed struct {
191 tag: PostProcess.Tag,
192 payload: PostProcess.Payload,
193
194 const Payload = packed union {
195 /// assembly value = encoded value + N
196 add: u5,
197 /// assembly value = encoded value << N
198 shl: u5,
199 none: u5, // unused number, for padding
200 };
201
202 const Tag = std.meta.FieldEnum(PostProcess.Payload);
203
204 pub const none: PostProcess = .{ .tag = .none, .payload = .{ .none = 0 } };
205
206 pub const ParseError = Reader.Error || std.fmt.ParseIntError;
207 pub fn parse(reader: *Reader) PostProcess.ParseError!PostProcess {
208 switch (try reader.takeByte()) {
209 'p' => return .{
210 .tag = .add,
211 .payload = .{ .add = try takeInteger(u4, reader) },
212 },
213 's' => return .{
214 .tag = .shl,
215 .payload = .{ .shl = try takeInteger(u4, reader) },
216 },
217 else => return error.InvalidCharacter,
218 }
219 }
220 };
221
222 pub fn offset(slot: Slot) u5 {
223 return switch (slot.tag) {
224 .none => unreachable,
225 .imm => slot.payload.imm.index.offset(),
226 .reg => slot.payload.reg.index.offset(),
227 };
228 }
229
230 pub fn width(slot: Slot) u5 {
231 return switch (slot.tag) {
232 .none => unreachable,
233 .imm => slot.payload.imm.length,
234 .reg => switch (slot.payload.reg.class) {
235 .fcc => 3,
236 else => 5,
237 },
238 };
239 }
240
241 pub fn mask(slot: Slot) u32 {
242 const off = slot.offset();
243 const size = slot.width();
244 const msb, const overflow = @addWithOverflow(off, size);
245 if (overflow == 1) {
246 @branchHint(.unlikely);
247 return ~((@as(u32, 1) << off) - 1);
248 }
249 return ((@as(u32, 1) << msb) - 1) ^ ((@as(u32, 1) << off) - 1);
250 }
251};
252
253test "mask" {
254 try std.testing.expectEqual(0b111100000, (Slot{ .tag = .imm, .payload = .{ .imm = .{
255 .index = .j,
256 .length = 4,
257 .signedness = .unsigned,
258 } } }).mask());
259 try std.testing.expectEqual(0x7fffffff, (Slot{ .tag = .imm, .payload = .{ .imm = .{
260 .index = .d,
261 .length = 31,
262 .signedness = .unsigned,
263 } } }).mask());
264 try std.testing.expectEqual(0x7fffffff, (Slot{ .tag = .imm, .payload = .{ .imm = .{
265 .index = .d,
266 .length = 31,
267 .signedness = .unsigned,
268 } } }).mask());
269 try std.testing.expectEqual(0xffffffe0, (Slot{ .tag = .imm, .payload = .{ .imm = .{
270 .index = .j,
271 .length = 27,
272 .signedness = .unsigned,
273 } } }).mask());
274 try std.testing.expectEqual(0b111110000000000, (Slot{ .tag = .reg, .payload = .{ .reg = .{
275 .class = .int,
276 .index = .k,
277 } } }).mask());
278}
279
280fn takeInteger(comptime T: type, reader: *Reader) (Reader.Error || std.fmt.ParseIntError)!T {
281 if (std.math.maxInt(T) < 10) {
282 const ch = try reader.takeByte();
283 return std.math.cast(T, ch ^ '0') orelse return error.Overflow;
284 }
285 var v: T = 0;
286
287 var ch: u8 = try reader.peekByte();
288 if (!std.ascii.isDigit(ch)) return error.InvalidCharacter;
289
290 while (std.ascii.isDigit(ch)) : (ch = reader.peekByte() catch |err| switch (err) {
291 error.EndOfStream => break,
292 else => return err,
293 }) {
294 v = try std.math.add(
295 T,
296 try std.math.add(
297 T,
298 try std.math.shlExact(T, v, 3),
299 try std.math.shlExact(T, v, 1),
300 ),
301 std.math.cast(T, ch ^ '0') orelse return error.Overflow,
302 );
303 reader.toss(1);
304 }
305 return v;
306}
307
308test takeInteger {
309 var reader: std.Io.Reader = undefined;
310
311 reader = .fixed("123");
312 try std.testing.expectEqual(123, try takeInteger(u8, &reader));
313 reader = .fixed("123ignored");
314 try std.testing.expectEqual(123, try takeInteger(u8, &reader));
315 reader = .fixed("bad");
316 try std.testing.expectError(error.InvalidCharacter, takeInteger(u8, &reader));
317 reader = .fixed("1");
318 try std.testing.expectEqual(1, try takeInteger(u1, &reader));
319}
320
321pub const Opcode = struct {
322 word: u32,
323 name: []const u8,
324 format: *Format,
325 orig_name: []const u8,
326 orig_format: *Format,
327 required_features: RequiredFeatures,
328
329 pub const RequiredFeatures = packed struct {
330 @"32bit": bool = false,
331 @"32s": bool = false,
332 @"64bit": bool = false,
333 f: bool = false,
334 d: bool = false,
335 lsx: bool = false,
336 lasx: bool = false,
337 lbt: bool = false,
338 lvz: bool = false,
339 };
340};
341
342/// Parses a opcode data file.
343/// Caller owns the data string and the data string must live longer
344/// than the OpcodeDesc.
345pub fn parse(desc: *OpcodeDesc, gpa: Allocator, data: []const u8) !void {
346 var lines = mem.tokenizeScalar(u8, data, '\n');
347 while (lines.next()) |line| {
348 if (line[0] == '#') continue; // skip comments, not used by upstream but used in tools/loongarch/extra.txt
349 var tokens = mem.tokenizeScalar(u8, line, ' ');
350
351 const word_buf = tokens.next() orelse return error.UnexpectedEol;
352 const word = try std.fmt.parseInt(u32, word_buf, 16);
353 const name = tokens.next() orelse return error.UnexpectedEol;
354 const format_str = tokens.next() orelse return error.UnexpectedEol;
355 const format = try desc.getOrParseFormat(gpa, format_str);
356
357 const opcode = try desc.opcode.addOne(gpa);
358 opcode.* = .{
359 .word = word,
360 .name = name,
361 .format = format,
362 .orig_name = name,
363 .orig_format = format,
364 .required_features = .{},
365 };
366
367 // parse attributes
368 while (tokens.next()) |attr| {
369 if (attr[0] != '@') return error.MalformedAttribute;
370 if (mem.indexOfScalar(u8, attr, '=')) |eql_pos| {
371 const attr_name = attr[1..][0 .. eql_pos - 1];
372 const attr_val = attr[eql_pos + 1 ..];
373
374 if (mem.eql(u8, attr_name, "orig_name")) { // manual name
375 opcode.orig_name = attr_val;
376 } else if (mem.eql(u8, attr_name, "orig_fmt")) { // manual format
377 opcode.orig_format = try desc.getOrParseFormat(gpa, attr_val);
378 }
379 } else {
380 const attr_name = attr[1..];
381
382 if (mem.eql(u8, attr_name, "la32")) { // available in LA32S
383 if (!opcode.required_features.@"32bit")
384 opcode.required_features.@"32s" = true;
385 } else if (mem.eql(u8, attr_name, "primary")) { // available in LA32R
386 opcode.required_features.@"32bit" = true;
387 opcode.required_features.@"32s" = false;
388 } else if (mem.eql(u8, attr_name, "lvz")) { // requires LVZ
389 opcode.required_features.lvz = false;
390 } else if (mem.eql(u8, attr_name, "lbt")) { // requires LBT
391 opcode.required_features.lbt = false;
392 }
393 }
394 }
395
396 // determine based on register usages
397 for (opcode.format.slots) |slot| {
398 switch (slot.tag) {
399 .none => break,
400 .imm => {},
401 .reg => {
402 switch (slot.payload.reg.class) {
403 .fp => {
404 opcode.required_features.f = true;
405 if (mem.eql(u8, opcode.name, ".d")) { // requires double-precision FP
406 opcode.required_features.d = true;
407 }
408 },
409 .fcc => opcode.required_features.f = true,
410 .lsx => opcode.required_features.lsx = true,
411 .lasx => opcode.required_features.lasx = true,
412 .lbt_scratch => opcode.required_features.lbt = true,
413 else => {},
414 }
415 },
416 }
417 }
418
419 // if there are not any attributes indicating that the instruction requires
420 // any other features or supports LA32, we assume that it requires LA64.
421 if (opcode.required_features == Opcode.RequiredFeatures{}) {
422 opcode.required_features.@"64bit" = true;
423 }
424 }
425}
426
427/// Gets or parses a format string.
428/// The string must live longer than the OpcodeDesc.
429pub fn getOrParseFormat(desc: *OpcodeDesc, gpa: Allocator, format: []const u8) !*Format {
430 const gop = try desc.format.getOrPut(gpa, format);
431 if (!gop.found_existing) {
432 errdefer _ = desc.format.swapRemove(format);
433 const format_ptr = try desc.format_pool.create(gpa);
434 errdefer desc.format_pool.destroy(format_ptr);
435
436 format_ptr.* = try Format.parse(format);
437 gop.value_ptr.* = format_ptr;
438 }
439 return gop.value_ptr.*;
440}
441
442pub fn sort(desc: *OpcodeDesc) void {
443 mem.sort(Opcode, desc.opcode.items, false, struct {
444 fn cmp(_: bool, lhs: Opcode, rhs: Opcode) bool {
445 return mem.order(u8, lhs.name, rhs.name) == .lt;
446 }
447 }.cmp);
448 desc.format.sort(struct {
449 keys: [][]const u8,
450
451 pub fn lessThan(ctx: @This(), a_index: usize, b_index: usize) bool {
452 return mem.order(u8, ctx.keys[a_index], ctx.keys[b_index]) == .lt;
453 }
454 }{ .keys = desc.format.keys() });
455}
tools/loongarch/decode_tree.zig created+149
......@@ -0,0 +1,149 @@
1const std = @import("std");
2const Allocator = std.mem.Allocator;
3const log = std.log.scoped(.loongarch_decode_tree);
4
5const OpcodeDesc = @import("OpcodeDesc.zig");
6const Opcode = OpcodeDesc.Opcode;
7
8pub const Node = struct {
9 mask: u32,
10 /// `cases` when mask is non-zero. `instruction` when mask is zero.
11 next: union {
12 /// catch-all case
13 cases: []const Case,
14 instruction: *const Opcode,
15 },
16};
17
18pub const Case = struct {
19 catch_all: bool,
20 variant: u32, // valid only when catch-all is not set
21 child: *const Node,
22};
23
24pub fn populate(arena: Allocator, desc: *const OpcodeDesc) !*Node {
25 var ops: std.ArrayList(*const Opcode) = .empty;
26 defer ops.deinit(arena);
27 try ops.ensureUnusedCapacity(arena, desc.opcode.items.len);
28 for (desc.opcode.items) |*op| ops.appendAssumeCapacity(op);
29
30 return try populateAdvanced(arena, ops.items, 0);
31}
32
33pub fn populateAdvanced(arena: Allocator, ops: []const *const Opcode, checked_mask: u32) !*Node {
34 if (ops.len == 1) {
35 const node = try arena.create(Node);
36 node.* = .{ .mask = 0, .next = .{ .instruction = ops[0] } };
37 return node;
38 }
39
40 // look for unchecked common static bits
41 common_static_bits: {
42 var common_mask: u32 = ~checked_mask;
43 for (ops) |op| {
44 for (op.format.slots) |slot| {
45 if (slot.tag == .none) break;
46 common_mask &= ~slot.mask();
47 }
48 }
49 if (common_mask == 0) break :common_static_bits;
50
51 // there are some common bits to check
52 var cases: std.ArrayList(Case) = .empty;
53 defer cases.deinit(arena);
54 var known_variants: std.ArrayList(u32) = .empty;
55 defer known_variants.deinit(arena);
56
57 for (ops) |op| {
58 const variant = op.word & common_mask;
59 if (std.mem.indexOfScalar(u32, known_variants.items, variant) == null) {
60 // new variant
61 try known_variants.append(arena, variant);
62
63 var variant_ops: std.ArrayList(*const Opcode) = .empty;
64 defer variant_ops.deinit(arena);
65 variant_ops.ensureTotalCapacity(arena, ops.len / 2) catch {};
66 for (ops) |op1|
67 if ((op1.word & common_mask) == variant) try variant_ops.append(arena, op1);
68
69 const child = try populateAdvanced(arena, variant_ops.items, checked_mask | common_mask);
70 try cases.append(arena, .{
71 .catch_all = false,
72 .variant = variant,
73 .child = child,
74 });
75 }
76 }
77
78 const node = try arena.create(Node);
79 node.* = .{
80 .mask = common_mask,
81 .next = .{ .cases = try cases.toOwnedSlice(arena) },
82 };
83 return node;
84 }
85
86 // look for bits that are static for some opcodes but dynamic for one opcode, e.g. csrxchg
87 half_static_bits: {
88 // these bits are static in at least one opcode
89 var half_static_mask: u32 = 0;
90 for (ops) |op| {
91 var op_static_mask: u32 = 0xffffffff;
92 for (op.format.slots) |slot| {
93 if (slot.tag == .none) break;
94 op_static_mask &= ~slot.mask();
95 }
96 half_static_mask |= op_static_mask;
97 }
98 half_static_mask &= ~checked_mask;
99 if (half_static_mask == 0) break :half_static_bits;
100
101 var cases: std.ArrayList(Case) = .empty;
102 defer cases.deinit(arena);
103 var maybe_dynamic_op: ?*const Opcode = null;
104
105 for (ops) |op| {
106 const variant = op.word & half_static_mask;
107
108 var op_static_mask = ~checked_mask;
109 for (op.format.slots) |slot| {
110 if (slot.tag == .none) break;
111 op_static_mask &= ~slot.mask();
112 }
113 if (op_static_mask != half_static_mask) {
114 if (maybe_dynamic_op) |dynamic_op| {
115 log.err("unsupported: {s}, {s}", .{ dynamic_op.name, op.name });
116 return error.Unsupported;
117 } else {
118 maybe_dynamic_op = op;
119 continue;
120 }
121 }
122
123 const child = try populateAdvanced(arena, &.{op}, checked_mask | half_static_mask);
124 try cases.append(arena, .{
125 .catch_all = false,
126 .variant = variant,
127 .child = child,
128 });
129 }
130
131 if (maybe_dynamic_op) |dynamic_op| {
132 const child = try populateAdvanced(arena, &.{dynamic_op}, checked_mask | half_static_mask);
133 try cases.append(arena, .{
134 .catch_all = true,
135 .variant = 0,
136 .child = child,
137 });
138 } else unreachable;
139
140 const node = try arena.create(Node);
141 node.* = .{
142 .mask = half_static_mask,
143 .next = .{ .cases = try cases.toOwnedSlice(arena) },
144 };
145 return node;
146 }
147
148 return error.Unsupported;
149}
tools/loongarch/extra.txt created+9
......@@ -0,0 +1,9 @@
1# loongarch-opcodes removed these instructions as their encodings
2# collide with csrxchg/gcsrxchg.
3# They are added back here to support assembler and disassembler
4# as decode_tree.zig has supported generating decode-trees for these
5# colliding encodings.
604000000 csrrd DUk14 @primary
704000032 csrwr DUk14 @primary
805000000 gcsrrd DUk14 @lvz
905000032 gcsrwr DUk14 @lvz