diff --git a/tools/gen_loongarch_encoding.zig b/tools/gen_loongarch_encoding.zig new file mode 100644 index 0000000000000000000000000000000000000000..767988d51ac1d2b7c2aa33e3b90f040e871fa1c7 --- /dev/null +++ b/tools/gen_loongarch_encoding.zig @@ -0,0 +1,526 @@ +//! Example usage: +//! git clone https://github.com/loongson-community/loongarch-opcodes.git ../loongarch-opcodes +//! zig run tools/gen_loongarch_encoding.zig -- ../loongarch-opcodes . + +const std = @import("std"); +const fs = std.fs; +const Allocator = std.mem.Allocator; +const print = std.debug.print; +const Writer = std.Io.Writer; +const ZonSerializer = std.zon.Serializer; + +const OpcodeDesc = @import("loongarch/OpcodeDesc.zig"); +const decode_tree = @import("loongarch/decode_tree.zig"); + +pub fn main(init: std.process.Init) !void { + const arena = init.arena.allocator(); + const io = init.io; + + var args = try init.minimal.args.iterateAllocator(arena); + const arg0 = args.next().?; + const opcodes_path = args.next() orelse usageAndExit(arg0, 0); + const zig_path = args.next() orelse usageAndExit(arg0, 1); + args.deinit(); + + var desc: OpcodeDesc = .{}; + defer desc.deinit(arena); + + var zig_dir = try std.Io.Dir.cwd().openDir(io, zig_path, .{}); + defer zig_dir.close(io); + + // load opcode data + { + print("Loading description files ..\n", .{}); + var opcodes_dir = try std.Io.Dir.cwd().openDir(io, opcodes_path, .{ .iterate = true }); + defer opcodes_dir.close(io); + var opcodes_iter = opcodes_dir.iterateAssumeFirstIteration(); + while (try opcodes_iter.next(io)) |opcodes_file| { + if (opcodes_file.kind != .file) continue; + if (!std.mem.endsWith(u8, opcodes_file.name, ".txt")) continue; + + print("Loading {s} ...\n", .{opcodes_file.name}); + const data = try opcodes_dir.readFileAlloc(io, opcodes_file.name, arena, .unlimited); + try desc.parse(arena, data); + // `data` is intentionally leaked here because it must live longer than `desc` + // ArenaAllocator should clean them up. + } + + print("Loading extra.txt ...\n", .{}); + const data = try zig_dir.readFileAlloc(io, "tools/loongarch/extra.txt", arena, .unlimited); + try desc.parse(arena, data); + + print("Loaded {} instructions, {} formats\n", .{ desc.opcode.items.len, desc.format.count() }); + desc.sort(); + print("Sorted data\n", .{}); + } + + // generate encoding.zig + { + print("Writing encoding.zig ...\n", .{}); + var buffer: Writer.Allocating = .init(arena); + defer buffer.deinit(); + const writer = &buffer.writer; + + try writer.print( + \\// DO NOT MODIFY. This file is generated by tools/gen_loongarch_encoding.zig. + \\const Register = @import("bits.zig").Register; + \\ + , .{}); + + // mnemonic enum + { + try writer.print("\npub const Mnemonic = enum {{\n", .{}); + for (desc.opcode.items) |*opcode| { + try writer.print(" {f},\n", .{std.zig.fmtIdPU(opcode.name)}); + } + try writer.print("}};\n", .{}); + } + + // instruction struct + { + try writer.print( + \\ + \\pub const Instruction = packed union {{ + \\ word: u32, + \\ + , .{}); + + // format-based variants + { + var format_iter = desc.format.iterator(); + while (format_iter.next()) |entry| + try printFormatStruct(writer, entry.key_ptr.*, entry.value_ptr.*); + } + + // format-based encoders + { + var format_iter = desc.format.iterator(); + while (format_iter.next()) |entry| + try printFormatEncoder(writer, entry.key_ptr.*, entry.value_ptr.*); + } + + // opcode-based encoders + for (desc.opcode.items) |*opcode| { + const encoder_format = if (std.mem.eql(u8, opcode.name, opcode.orig_name)) opcode.orig_format else opcode.format; + try printInstructionEncoder(writer, opcode, encoder_format); + } + + try writer.print("}};\n", .{}); + } + + try zig_dir.writeFile(io, .{ + .sub_path = "src/codegen/loongarch/encoding.zig", + .data = buffer.written(), + }); + } + + // generate decode_tree.zon + { + print("Writing decode_tree.zon ...\n", .{}); + var buffer: Writer.Allocating = .init(arena); + defer buffer.deinit(); + const writer = &buffer.writer; + + try writer.print( + \\// DO NOT MODIFY. This file is generated by tools/gen_loongarch_encoding.zig. + \\ + , .{}); + + try printDecodeTree(writer, arena, &desc); + + try writer.writeAll("\n"); + + try zig_dir.writeFile(io, .{ + .sub_path = "src/codegen/loongarch/decode_tree.zon", + .data = buffer.written(), + }); + } + + // generate inst_formats.zon + { + print("Writing inst_formats.zon ...\n", .{}); + var buffer: Writer.Allocating = .init(arena); + defer buffer.deinit(); + const writer = &buffer.writer; + + try writer.print( + \\// DO NOT MODIFY. This file is generated by tools/gen_loongarch_encoding.zig. + \\ + , .{}); + + var s: ZonSerializer = .{ + .writer = writer, + .options = .{}, + }; + try serializeInstFormats(&s, &desc); + + try writer.writeAll("\n"); + + try zig_dir.writeFile(io, .{ + .sub_path = "src/codegen/loongarch/inst_formats.zon", + .data = buffer.written(), + }); + } + + print("Done.\n", .{}); +} + +fn usageAndExit(arg0: []const u8, code: u8) noreturn { + print( + \\Usage: {s} /path/loongarch-opcodes /path/zig + \\ + \\Updates LoongArch encoding data from loongarch-opcodes.git. + \\ + , .{arg0}); + std.process.exit(code); +} + +fn printFormatStruct(writer: *Writer, name: []const u8, format: *const OpcodeDesc.Format) !void { + if (format.slots[0].tag == .none) return; // skips EMPTY format + + try writer.print(" /// Fields of a `{s}` instruction.", .{name}); + try writer.print("\n {s}: packed struct {{ ", .{name}); + const Field = union(enum) { + funct: struct { width: u5 }, + immediate: struct { + signedness: std.builtin.Signedness, + width: u5, + }, + register: struct { + index: OpcodeDesc.Slot.Index, + width: u5, + }, + }; + var fields_buf: [4 * 2 + 1]Field = undefined; + var fields: std.ArrayList(Field) = .initBuffer(&fields_buf); + + // collect fields + var bit_offset: u6 = 0; + var slots = format.slots; + std.mem.sort(OpcodeDesc.Slot, &slots, false, struct { + fn cmp(_: bool, lhs: OpcodeDesc.Slot, rhs: OpcodeDesc.Slot) bool { + if (lhs.tag == .none) return false; + if (rhs.tag == .none) return true; + return lhs.offset() < rhs.offset(); + } + }.cmp); + for (slots) |slot| { + if (slot.tag == .none) break; + const slot_offset = slot.offset(); + const slot_width = slot.width(); + if (bit_offset != slot_offset) + fields.appendAssumeCapacity(.{ .funct = .{ .width = @truncate(slot_offset - bit_offset) } }); + + switch (slot.tag) { + .none => unreachable, + .imm => { + fields.appendAssumeCapacity(.{ .immediate = .{ + .signedness = slot.payload.imm.signedness, + .width = slot_width, + } }); + }, + .reg => fields.appendAssumeCapacity(.{ .register = .{ + .index = slot.payload.reg.index, + .width = slot_width, + } }), + } + bit_offset = slot_offset + slot_width; + } + if (bit_offset != 32) + fields.appendAssumeCapacity(.{ .funct = .{ .width = @intCast(@as(u6, 32) - bit_offset) } }); + + // detect shadowed immediate field names + const imm_shadowed = imm_shadowed: { + var imm_names: [std.math.maxInt(u6) + 1]bool = @splat(false); + for (fields.items) |field| { + switch (field) { + else => {}, + .immediate => |imm_field| { + var imm_name_key: u6 = imm_field.width; + if (imm_field.signedness == .signed) imm_name_key |= std.math.maxInt(u5) + 1; + if (imm_names[imm_name_key]) break :imm_shadowed true; + imm_names[imm_name_key] = true; + }, + } + } + break :imm_shadowed false; + }; + + // print fields + bit_offset = 0; + for (fields.items, 0..) |field, field_i| { + if (field_i != 0) try writer.writeAll(", "); + switch (field) { + .funct => |pl| { + try writer.print("funct{}: u{}", .{ bit_offset, pl.width }); + bit_offset += pl.width; + }, + .immediate => |pl| { + if (imm_shadowed) { + try writer.print("imm{}: {c}{}", .{ + bit_offset, + @as(u8, switch (pl.signedness) { + .signed => 'i', + .unsigned => 'u', + }), + pl.width, + }); + } else { + try writer.print("{c}i{}: {c}{}", .{ + @as(u8, switch (pl.signedness) { + .signed => 's', + .unsigned => 'u', + }), + pl.width, + @as(u8, switch (pl.signedness) { + .signed => 'i', + .unsigned => 'u', + }), + pl.width, + }); + } + bit_offset += pl.width; + }, + .register => |pl| { + try writer.print("r{s}: u{}", .{ @tagName(pl.index), pl.width }); + bit_offset += pl.width; + }, + } + } + try writer.print(" }},\n", .{}); +} + +fn printFormatEncoder(writer: *Writer, name: []const u8, format: *const OpcodeDesc.Format) !void { + try writer.print("\n /// Encodes a `{s}` instruction.", .{name}); + try writer.print("\n pub inline fn encode{s}(word: u32", .{name}); + for (format.slots, 0..) |slot, slot_i| { + switch (slot.tag) { + .none => break, + .imm => { + const pl = slot.payload.imm; + + try writer.print(", p{}: {c}{}", .{ + slot_i, + @as(u8, switch (pl.signedness) { + .signed => 'i', + .unsigned => 'u', + }), + pl.length, + }); + }, + .reg => try writer.print(", p{}: Register", .{slot_i}), + } + } + try writer.print(") Instruction {{\n", .{}); + if (format.slots[0].tag == .none) { + try writer.print(" return .{{ .word = word }};\n", .{}); + } else { + try writer.print(" return .{{ .word = word", .{}); + for (format.slots, 0..) |slot, slot_i| { + switch (slot.tag) { + .none => break, + .imm => { + const pl = slot.payload.imm; + try writer.print(" |\n (", .{}); + try writer.print("(@as(u32, @as(u{}, @bitCast(p{})))", .{ pl.length, slot_i }); + + switch (pl.post_proc.tag) { + .none => {}, + .add => try writer.print(" - {}", .{pl.post_proc.payload.add}), + .shl => try writer.print(" >> {}", .{pl.post_proc.payload.shl}), + } + + try writer.print(") << {})", .{pl.index.offset()}); + }, + .reg => { + const pl = slot.payload.reg; + try writer.print(" |\n (@as(u32, p{}.encode()) << {})", .{ slot_i, pl.index.offset() }); + }, + } + } + try writer.print(" }};\n", .{}); + } + try writer.print(" }}\n", .{}); +} + +fn printInstructionEncoder(writer: *Writer, opcode: *OpcodeDesc.Opcode, format: *const OpcodeDesc.Format) !void { + try writer.print("\n /// Encodes a `{s}` instruction", .{opcode.name}); + if (opcode.required_features != OpcodeDesc.Opcode.RequiredFeatures{}) { + try writer.writeAll(" (requires "); + var first = true; + const feature_fields = comptime std.meta.fieldNames(OpcodeDesc.Opcode.RequiredFeatures); + inline for (feature_fields) |field| { + if (@field(opcode.required_features, field)) { + if (first) first = false else try writer.writeAll(" & "); + try writer.writeAll(field); + } + } + try writer.writeByte(')'); + } + try writer.writeByte('.'); + try writer.print("\n pub inline fn {f}(", .{std.zig.fmtIdPU(opcode.name)}); + for (format.slots, 0..) |slot, slot_i| { + if (slot_i != 0 and slot.tag != .none) + try writer.print(", ", .{}); + switch (slot.tag) { + .none => break, + .imm => { + const pl = slot.payload.imm; + + try writer.print("p{}: {c}{}", .{ + slot_i, + @as(u8, switch (pl.signedness) { + .signed => 'i', + .unsigned => 'u', + }), + pl.length, + }); + }, + .reg => try writer.print("p{}: Register", .{slot_i}), + } + } + try writer.print(") Instruction {{\n", .{}); + try writer.print(" return encode{s}(0x{x:0>8}", .{ format.name, opcode.word }); + for (format.slots, 0..) |slot, slot_i| { + switch (slot.tag) { + .none => break, + else => try writer.print(", p{}", .{slot_i}), + } + } + try writer.print(");\n", .{}); + try writer.print(" }}\n", .{}); +} + +fn printDecodeTree(writer: *Writer, gpa: Allocator, desc: *const OpcodeDesc) !void { + var arena: std.heap.ArenaAllocator = .init(gpa); + defer arena.deinit(); + const root_node = try decode_tree.populate(arena.allocator(), desc); + try printDecodeTreeNode(writer, root_node, 0); +} + +fn printDecodeTreeNode(writer: *Writer, node: *const decode_tree.Node, indent: usize) !void { + if (node.mask == 0) { + try writer.print(".{{ .instruction = .{f} }}", .{std.zig.fmtId(node.next.instruction.name)}); + } else { + try writer.print(".{{ .mask = 0x{x:0>8}, .cases = .{{\n", .{node.mask}); + for (node.next.cases) |*case| { + try printIndentation(writer, indent + 1); + if (case.catch_all) { + try writer.print(".{{ .then = ", .{}); + try printDecodeTreeNode(writer, case.child, indent + 1); + try writer.print(" }},\n", .{}); + } else { + try writer.print(".{{ .value = 0x{x:0>8}, .then = ", .{case.variant}); + try printDecodeTreeNode(writer, case.child, indent + 1); + try writer.print(" }},\n", .{}); + } + } + try printIndentation(writer, indent); + try writer.print("}} }}", .{}); + } +} + +fn printIndentation(writer: *Writer, indent: usize) !void { + try writer.splatByteAll(' ', 4 * indent); +} + +fn serializeInstFormats(s: *ZonSerializer, desc: *const OpcodeDesc) !void { + var root_struct = try s.beginStruct(.{}); + + { + var instructions_s = try root_struct.beginStructField("instructions", .{}); + for (desc.opcode.items) |*opcode| { + var opcode_s = try instructions_s.beginStructField(opcode.name, .{}); + + try opcode_s.fieldPrefix("word"); + try s.writer.writeAll("0x"); + try s.writer.printInt(opcode.word, 16, .lower, .{ + .alignment = .right, + .fill = '0', + .width = 8, + }); + + try opcode_s.fieldPrefix("format"); + try s.ident(opcode.format.name); + if (opcode.orig_format != opcode.format) { + try opcode_s.fieldPrefix("orig_format"); + try s.ident(opcode.orig_format.name); + } + + if (opcode.orig_name.ptr != opcode.name.ptr) + try opcode_s.field("orig_name", opcode.orig_name, .{}); + + const field_names = comptime std.meta.fieldNames(OpcodeDesc.Opcode.RequiredFeatures); + var num_features: u32 = 0; + inline for (field_names) |field| { + if (@field(opcode.required_features, field)) + num_features += 1; + } + var features_s = try opcode_s.beginTupleField("features", .{ .whitespace_style = .{ .fields = num_features } }); + inline for (field_names) |field| { + if (@field(opcode.required_features, field)) { + try features_s.fieldPrefix(); + try s.ident(field); + } + } + try features_s.end(); + + try opcode_s.end(); + } + try instructions_s.end(); + } + + { + var formats_s = try root_struct.beginStructField("formats", .{}); + for (desc.format.values()) |format| { + var format_s = try formats_s.beginStructField(format.name, .{ .whitespace_style = .{ .wrap = false } }); + + var slots_s = try format_s.beginTupleField("slots", .{}); + for (format.slots) |slot| { + if (slot.tag == .none) break; + var slot_s = try slots_s.beginStructField(.{ .whitespace_style = .{ .wrap = false } }); + switch (slot.tag) { + .none => unreachable, + .reg => { + const pl = slot.payload.reg; + var reg_s = try slot_s.beginStructField("reg", .{ .whitespace_style = .{ .fields = 2 } }); + try reg_s.field("location", pl.index.offset(), .{}); + + try reg_s.fieldPrefix("class"); + try s.ident(@tagName(pl.class)); + + try reg_s.end(); + }, + .imm => { + const pl = slot.payload.imm; + var imm_s = try slot_s.beginStructField("imm", .{ .whitespace_style = .{ .wrap = false } }); + try imm_s.field("location", pl.index.offset(), .{}); + try imm_s.field("length", pl.length, .{}); + + try imm_s.fieldPrefix("signedness"); + try s.ident(@tagName(pl.signedness)); + + if (pl.post_proc.tag != .none) { + var pp_s = try imm_s.beginStructField("post_proc", .{ .whitespace_style = .{ .fields = 1 } }); + switch (pl.post_proc.tag) { + .none => unreachable, + .add => try pp_s.field("add", pl.post_proc.payload.add, .{}), + .shl => try pp_s.field("shl", pl.post_proc.payload.shl, .{}), + } + try pp_s.end(); + } + + try imm_s.end(); + }, + } + try slot_s.end(); + } + try slots_s.end(); + + try format_s.end(); + } + try formats_s.end(); + } + + try root_struct.end(); +} diff --git a/tools/loongarch/OpcodeDesc.zig b/tools/loongarch/OpcodeDesc.zig new file mode 100644 index 0000000000000000000000000000000000000000..e88a65c2718e8cc9d24219d8a7dc4c19b91d8f3d --- /dev/null +++ b/tools/loongarch/OpcodeDesc.zig @@ -0,0 +1,455 @@ +//! Parser for format description files in +//! https://github.com/loongson-community/loongarch-opcodes. + +const std = @import("std"); +const mem = std.mem; +const Allocator = mem.Allocator; +const Reader = std.Io.Reader; + +const OpcodeDesc = @This(); + +/// Maximum number of slots in one instruction format. +const max_slots = 4; + +opcode: std.ArrayList(Opcode) = .empty, +format_pool: std.heap.MemoryPool(Format) = .empty, +format: std.StringArrayHashMapUnmanaged(*Format) = .empty, + +pub fn deinit(desc: *OpcodeDesc, gpa: Allocator) void { + desc.opcode.deinit(gpa); + desc.format.deinit(gpa); + desc.format_pool.deinit(gpa); +} + +/// Instruction format. Slots are filled one by one, ending with reaching max_slots or a .none slot. +pub const Format = struct { + name: []const u8, + slots: [max_slots]Slot, + + pub fn parse(name: []const u8) !Format { + var format: Format = .{ + .name = name, + .slots = .{ .none, .none, .none, .none }, + }; + var reader: Reader = .fixed(name); + var slot_index: std.math.IntFittingRange(0, max_slots) = 0; + parse_empty: { + const str = reader.peekArray(5) catch |err| switch (err) { + error.EndOfStream => break :parse_empty, + else => return err, + }; + if (mem.eql(u8, &str.*, "EMPTY")) + return format; + } + + parse_slots: while (slot_index < max_slots) : (slot_index += 1) { + switch (reader.takeByte() catch |err| switch (err) { + error.EndOfStream => break :parse_slots, + else => return err, + }) { + 'D' => format.slots[slot_index] = .{ .tag = .reg, .payload = .{ .reg = .{ + .class = .int, + .index = .d, + } } }, + 'J' => format.slots[slot_index] = .{ .tag = .reg, .payload = .{ .reg = .{ + .class = .int, + .index = .j, + } } }, + 'K' => format.slots[slot_index] = .{ .tag = .reg, .payload = .{ .reg = .{ + .class = .int, + .index = .k, + } } }, + 'A' => format.slots[slot_index] = .{ .tag = .reg, .payload = .{ .reg = .{ + .class = .int, + .index = .a, + } } }, + 'F' => format.slots[slot_index] = .{ .tag = .reg, .payload = .{ .reg = .{ + .class = .fp, + .index = try .parse(&reader), + } } }, + 'C' => format.slots[slot_index] = .{ .tag = .reg, .payload = .{ .reg = .{ + .class = .fcc, + .index = try .parse(&reader), + } } }, + 'T' => format.slots[slot_index] = .{ .tag = .reg, .payload = .{ .reg = .{ + .class = .lbt_scratch, + .index = try .parse(&reader), + } } }, + 'V' => format.slots[slot_index] = .{ .tag = .reg, .payload = .{ .reg = .{ + .class = .lsx, + .index = try .parse(&reader), + } } }, + 'X' => format.slots[slot_index] = .{ .tag = .reg, .payload = .{ .reg = .{ + .class = .lasx, + .index = try .parse(&reader), + } } }, + 'S', 'U' => |signedness_ch| { + const signedness: std.builtin.Signedness = if (signedness_ch == 'S') .signed else .unsigned; + while (slot_index < max_slots and continue_imm_slot: { + _ = Slot.Index.fromChar(reader.peekByte() catch |err| switch (err) { + error.EndOfStream => break :continue_imm_slot false, + else => return err, + }) catch break :continue_imm_slot false; + break :continue_imm_slot true; + }) : (slot_index += 1) { + const index: Slot.Index = try .parse(&reader); + const length = try takeInteger(u5, &reader); + const post_proc = post_proc: { + if ('p' == reader.peekByte() catch |err| switch (err) { + error.EndOfStream => ' ', + else => return err, + }) { + reader.toss(1); + break :post_proc try Slot.PostProcess.parse(&reader); + } else break :post_proc Slot.PostProcess.none; + }; + format.slots[slot_index] = .{ .tag = .imm, .payload = .{ .imm = .{ + .index = index, + .length = length, + .signedness = signedness, + .post_proc = post_proc, + } } }; + } + slot_index -= 1; + }, + else => return error.InvalidCharacter, + } + } + + return format; + } +}; + +test "parse format" { + _ = try Format.parse("DJFmSk12m13ps3"); + _ = try Format.parse("DJSk12m13ps3U16pp1"); + _ = try Format.parse("DJK"); +} + +pub const Slot = packed struct { + tag: Slot.Tag, + payload: Slot.Payload, + + comptime { + std.debug.assert(@sizeOf(Slot) == 4); + } + + const Payload = packed union { + none: u16, // unused number, just for padding + imm: packed struct { + index: Index, + length: u5, + signedness: std.builtin.Signedness, + post_proc: PostProcess = .none, + }, + reg: packed struct { + class: enum(u13) { int, fp, fcc, lbt_scratch, lsx, lasx }, + index: Index, + }, + }; + + const Tag = enum(u16) { reg, imm, none }; + + pub const none: Slot = .{ .tag = .none, .payload = .{ .none = 0 } }; + + pub const Index = enum(u3) { + // zig fmt: off + d, j, k, a, m, n, + // zig fmt: on + + pub fn offset(index: Index) u5 { + return switch (index) { + .d => 0, + .j => 5, + .k => 10, + .a => 15, + .m => 16, + .n => 18, + }; + } + + pub fn fromChar(ch: u8) error{UnknownIndexChar}!Index { + return switch (ch) { + 'd' => .d, + 'j' => .j, + 'k' => .k, + 'a' => .a, + 'm' => .m, + 'n' => .n, + else => return error.UnknownIndexChar, + }; + } + + pub const ParseError = Reader.Error || error{UnknownIndexChar}; + pub fn parse(reader: *Reader) Index.ParseError!Index { + return fromChar(try reader.takeByte()); + } + }; + + /// Post-process operations for disassemblying. + pub const PostProcess = packed struct { + tag: PostProcess.Tag, + payload: PostProcess.Payload, + + const Payload = packed union { + /// assembly value = encoded value + N + add: u5, + /// assembly value = encoded value << N + shl: u5, + none: u5, // unused number, for padding + }; + + const Tag = std.meta.FieldEnum(PostProcess.Payload); + + pub const none: PostProcess = .{ .tag = .none, .payload = .{ .none = 0 } }; + + pub const ParseError = Reader.Error || std.fmt.ParseIntError; + pub fn parse(reader: *Reader) PostProcess.ParseError!PostProcess { + switch (try reader.takeByte()) { + 'p' => return .{ + .tag = .add, + .payload = .{ .add = try takeInteger(u4, reader) }, + }, + 's' => return .{ + .tag = .shl, + .payload = .{ .shl = try takeInteger(u4, reader) }, + }, + else => return error.InvalidCharacter, + } + } + }; + + pub fn offset(slot: Slot) u5 { + return switch (slot.tag) { + .none => unreachable, + .imm => slot.payload.imm.index.offset(), + .reg => slot.payload.reg.index.offset(), + }; + } + + pub fn width(slot: Slot) u5 { + return switch (slot.tag) { + .none => unreachable, + .imm => slot.payload.imm.length, + .reg => switch (slot.payload.reg.class) { + .fcc => 3, + else => 5, + }, + }; + } + + pub fn mask(slot: Slot) u32 { + const off = slot.offset(); + const size = slot.width(); + const msb, const overflow = @addWithOverflow(off, size); + if (overflow == 1) { + @branchHint(.unlikely); + return ~((@as(u32, 1) << off) - 1); + } + return ((@as(u32, 1) << msb) - 1) ^ ((@as(u32, 1) << off) - 1); + } +}; + +test "mask" { + try std.testing.expectEqual(0b111100000, (Slot{ .tag = .imm, .payload = .{ .imm = .{ + .index = .j, + .length = 4, + .signedness = .unsigned, + } } }).mask()); + try std.testing.expectEqual(0x7fffffff, (Slot{ .tag = .imm, .payload = .{ .imm = .{ + .index = .d, + .length = 31, + .signedness = .unsigned, + } } }).mask()); + try std.testing.expectEqual(0x7fffffff, (Slot{ .tag = .imm, .payload = .{ .imm = .{ + .index = .d, + .length = 31, + .signedness = .unsigned, + } } }).mask()); + try std.testing.expectEqual(0xffffffe0, (Slot{ .tag = .imm, .payload = .{ .imm = .{ + .index = .j, + .length = 27, + .signedness = .unsigned, + } } }).mask()); + try std.testing.expectEqual(0b111110000000000, (Slot{ .tag = .reg, .payload = .{ .reg = .{ + .class = .int, + .index = .k, + } } }).mask()); +} + +fn takeInteger(comptime T: type, reader: *Reader) (Reader.Error || std.fmt.ParseIntError)!T { + if (std.math.maxInt(T) < 10) { + const ch = try reader.takeByte(); + return std.math.cast(T, ch ^ '0') orelse return error.Overflow; + } + var v: T = 0; + + var ch: u8 = try reader.peekByte(); + if (!std.ascii.isDigit(ch)) return error.InvalidCharacter; + + while (std.ascii.isDigit(ch)) : (ch = reader.peekByte() catch |err| switch (err) { + error.EndOfStream => break, + else => return err, + }) { + v = try std.math.add( + T, + try std.math.add( + T, + try std.math.shlExact(T, v, 3), + try std.math.shlExact(T, v, 1), + ), + std.math.cast(T, ch ^ '0') orelse return error.Overflow, + ); + reader.toss(1); + } + return v; +} + +test takeInteger { + var reader: std.Io.Reader = undefined; + + reader = .fixed("123"); + try std.testing.expectEqual(123, try takeInteger(u8, &reader)); + reader = .fixed("123ignored"); + try std.testing.expectEqual(123, try takeInteger(u8, &reader)); + reader = .fixed("bad"); + try std.testing.expectError(error.InvalidCharacter, takeInteger(u8, &reader)); + reader = .fixed("1"); + try std.testing.expectEqual(1, try takeInteger(u1, &reader)); +} + +pub const Opcode = struct { + word: u32, + name: []const u8, + format: *Format, + orig_name: []const u8, + orig_format: *Format, + required_features: RequiredFeatures, + + pub const RequiredFeatures = packed struct { + @"32bit": bool = false, + @"32s": bool = false, + @"64bit": bool = false, + f: bool = false, + d: bool = false, + lsx: bool = false, + lasx: bool = false, + lbt: bool = false, + lvz: bool = false, + }; +}; + +/// Parses a opcode data file. +/// Caller owns the data string and the data string must live longer +/// than the OpcodeDesc. +pub fn parse(desc: *OpcodeDesc, gpa: Allocator, data: []const u8) !void { + var lines = mem.tokenizeScalar(u8, data, '\n'); + while (lines.next()) |line| { + if (line[0] == '#') continue; // skip comments, not used by upstream but used in tools/loongarch/extra.txt + var tokens = mem.tokenizeScalar(u8, line, ' '); + + const word_buf = tokens.next() orelse return error.UnexpectedEol; + const word = try std.fmt.parseInt(u32, word_buf, 16); + const name = tokens.next() orelse return error.UnexpectedEol; + const format_str = tokens.next() orelse return error.UnexpectedEol; + const format = try desc.getOrParseFormat(gpa, format_str); + + const opcode = try desc.opcode.addOne(gpa); + opcode.* = .{ + .word = word, + .name = name, + .format = format, + .orig_name = name, + .orig_format = format, + .required_features = .{}, + }; + + // parse attributes + while (tokens.next()) |attr| { + if (attr[0] != '@') return error.MalformedAttribute; + if (mem.indexOfScalar(u8, attr, '=')) |eql_pos| { + const attr_name = attr[1..][0 .. eql_pos - 1]; + const attr_val = attr[eql_pos + 1 ..]; + + if (mem.eql(u8, attr_name, "orig_name")) { // manual name + opcode.orig_name = attr_val; + } else if (mem.eql(u8, attr_name, "orig_fmt")) { // manual format + opcode.orig_format = try desc.getOrParseFormat(gpa, attr_val); + } + } else { + const attr_name = attr[1..]; + + if (mem.eql(u8, attr_name, "la32")) { // available in LA32S + if (!opcode.required_features.@"32bit") + opcode.required_features.@"32s" = true; + } else if (mem.eql(u8, attr_name, "primary")) { // available in LA32R + opcode.required_features.@"32bit" = true; + opcode.required_features.@"32s" = false; + } else if (mem.eql(u8, attr_name, "lvz")) { // requires LVZ + opcode.required_features.lvz = false; + } else if (mem.eql(u8, attr_name, "lbt")) { // requires LBT + opcode.required_features.lbt = false; + } + } + } + + // determine based on register usages + for (opcode.format.slots) |slot| { + switch (slot.tag) { + .none => break, + .imm => {}, + .reg => { + switch (slot.payload.reg.class) { + .fp => { + opcode.required_features.f = true; + if (mem.eql(u8, opcode.name, ".d")) { // requires double-precision FP + opcode.required_features.d = true; + } + }, + .fcc => opcode.required_features.f = true, + .lsx => opcode.required_features.lsx = true, + .lasx => opcode.required_features.lasx = true, + .lbt_scratch => opcode.required_features.lbt = true, + else => {}, + } + }, + } + } + + // if there are not any attributes indicating that the instruction requires + // any other features or supports LA32, we assume that it requires LA64. + if (opcode.required_features == Opcode.RequiredFeatures{}) { + opcode.required_features.@"64bit" = true; + } + } +} + +/// Gets or parses a format string. +/// The string must live longer than the OpcodeDesc. +pub fn getOrParseFormat(desc: *OpcodeDesc, gpa: Allocator, format: []const u8) !*Format { + const gop = try desc.format.getOrPut(gpa, format); + if (!gop.found_existing) { + errdefer _ = desc.format.swapRemove(format); + const format_ptr = try desc.format_pool.create(gpa); + errdefer desc.format_pool.destroy(format_ptr); + + format_ptr.* = try Format.parse(format); + gop.value_ptr.* = format_ptr; + } + return gop.value_ptr.*; +} + +pub fn sort(desc: *OpcodeDesc) void { + mem.sort(Opcode, desc.opcode.items, false, struct { + fn cmp(_: bool, lhs: Opcode, rhs: Opcode) bool { + return mem.order(u8, lhs.name, rhs.name) == .lt; + } + }.cmp); + desc.format.sort(struct { + keys: [][]const u8, + + pub fn lessThan(ctx: @This(), a_index: usize, b_index: usize) bool { + return mem.order(u8, ctx.keys[a_index], ctx.keys[b_index]) == .lt; + } + }{ .keys = desc.format.keys() }); +} diff --git a/tools/loongarch/decode_tree.zig b/tools/loongarch/decode_tree.zig new file mode 100644 index 0000000000000000000000000000000000000000..186a3f00efdee9797436257de0a974f5c1056c56 --- /dev/null +++ b/tools/loongarch/decode_tree.zig @@ -0,0 +1,149 @@ +const std = @import("std"); +const Allocator = std.mem.Allocator; +const log = std.log.scoped(.loongarch_decode_tree); + +const OpcodeDesc = @import("OpcodeDesc.zig"); +const Opcode = OpcodeDesc.Opcode; + +pub const Node = struct { + mask: u32, + /// `cases` when mask is non-zero. `instruction` when mask is zero. + next: union { + /// catch-all case + cases: []const Case, + instruction: *const Opcode, + }, +}; + +pub const Case = struct { + catch_all: bool, + variant: u32, // valid only when catch-all is not set + child: *const Node, +}; + +pub fn populate(arena: Allocator, desc: *const OpcodeDesc) !*Node { + var ops: std.ArrayList(*const Opcode) = .empty; + defer ops.deinit(arena); + try ops.ensureUnusedCapacity(arena, desc.opcode.items.len); + for (desc.opcode.items) |*op| ops.appendAssumeCapacity(op); + + return try populateAdvanced(arena, ops.items, 0); +} + +pub fn populateAdvanced(arena: Allocator, ops: []const *const Opcode, checked_mask: u32) !*Node { + if (ops.len == 1) { + const node = try arena.create(Node); + node.* = .{ .mask = 0, .next = .{ .instruction = ops[0] } }; + return node; + } + + // look for unchecked common static bits + common_static_bits: { + var common_mask: u32 = ~checked_mask; + for (ops) |op| { + for (op.format.slots) |slot| { + if (slot.tag == .none) break; + common_mask &= ~slot.mask(); + } + } + if (common_mask == 0) break :common_static_bits; + + // there are some common bits to check + var cases: std.ArrayList(Case) = .empty; + defer cases.deinit(arena); + var known_variants: std.ArrayList(u32) = .empty; + defer known_variants.deinit(arena); + + for (ops) |op| { + const variant = op.word & common_mask; + if (std.mem.indexOfScalar(u32, known_variants.items, variant) == null) { + // new variant + try known_variants.append(arena, variant); + + var variant_ops: std.ArrayList(*const Opcode) = .empty; + defer variant_ops.deinit(arena); + variant_ops.ensureTotalCapacity(arena, ops.len / 2) catch {}; + for (ops) |op1| + if ((op1.word & common_mask) == variant) try variant_ops.append(arena, op1); + + const child = try populateAdvanced(arena, variant_ops.items, checked_mask | common_mask); + try cases.append(arena, .{ + .catch_all = false, + .variant = variant, + .child = child, + }); + } + } + + const node = try arena.create(Node); + node.* = .{ + .mask = common_mask, + .next = .{ .cases = try cases.toOwnedSlice(arena) }, + }; + return node; + } + + // look for bits that are static for some opcodes but dynamic for one opcode, e.g. csrxchg + half_static_bits: { + // these bits are static in at least one opcode + var half_static_mask: u32 = 0; + for (ops) |op| { + var op_static_mask: u32 = 0xffffffff; + for (op.format.slots) |slot| { + if (slot.tag == .none) break; + op_static_mask &= ~slot.mask(); + } + half_static_mask |= op_static_mask; + } + half_static_mask &= ~checked_mask; + if (half_static_mask == 0) break :half_static_bits; + + var cases: std.ArrayList(Case) = .empty; + defer cases.deinit(arena); + var maybe_dynamic_op: ?*const Opcode = null; + + for (ops) |op| { + const variant = op.word & half_static_mask; + + var op_static_mask = ~checked_mask; + for (op.format.slots) |slot| { + if (slot.tag == .none) break; + op_static_mask &= ~slot.mask(); + } + if (op_static_mask != half_static_mask) { + if (maybe_dynamic_op) |dynamic_op| { + log.err("unsupported: {s}, {s}", .{ dynamic_op.name, op.name }); + return error.Unsupported; + } else { + maybe_dynamic_op = op; + continue; + } + } + + const child = try populateAdvanced(arena, &.{op}, checked_mask | half_static_mask); + try cases.append(arena, .{ + .catch_all = false, + .variant = variant, + .child = child, + }); + } + + if (maybe_dynamic_op) |dynamic_op| { + const child = try populateAdvanced(arena, &.{dynamic_op}, checked_mask | half_static_mask); + try cases.append(arena, .{ + .catch_all = true, + .variant = 0, + .child = child, + }); + } else unreachable; + + const node = try arena.create(Node); + node.* = .{ + .mask = half_static_mask, + .next = .{ .cases = try cases.toOwnedSlice(arena) }, + }; + return node; + } + + return error.Unsupported; +} diff --git a/tools/loongarch/extra.txt b/tools/loongarch/extra.txt new file mode 100644 index 0000000000000000000000000000000000000000..116a5a068ad2d3ae5b9f0ea4f600135656f80027 --- /dev/null +++ b/tools/loongarch/extra.txt @@ -0,0 +1,9 @@ +# loongarch-opcodes removed these instructions as their encodings +# collide with csrxchg/gcsrxchg. +# They are added back here to support assembler and disassembler +# as decode_tree.zig has supported generating decode-trees for these +# colliding encodings. +04000000 csrrd DUk14 @primary +04000032 csrwr DUk14 @primary +05000000 gcsrrd DUk14 @lvz +05000032 gcsrwr DUk14 @lvz