Files
zig/tools/loongarch/OpcodeDesc.zig
xtex 0fe1894158 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>
2026-08-08 10:53:46 +08:00

456 lines
16 KiB
Zig

//! 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() });
}