hblang/src/hbvm/Vm.zig
Jakub Doka b8893d70b2
fizing around 500 fuzz tests
Signed-off-by: Jakub Doka <jakub.doka2@gmail.com>
2025-03-18 18:27:30 +01:00

474 lines
19 KiB
Zig

regs: std.EnumArray(isa.Reg, u64) = .{ .values = [_]u64{0} ++ [_]u64{undefined} ** 255 },
ip: usize = undefined,
fuel: usize = 0,
const std = @import("std");
const isa = @import("isa.zig");
const root = @import("../utils.zig");
const Vm = @This();
const debug = @import("builtin").mode == .Debug;
const one: u64 = 1;
pub const SafeContext = struct {
color_cfg: std.io.tty.Config = .no_color,
writer: std.io.AnyWriter = std.io.null_writer.any(),
symbols: std.AutoHashMapUnmanaged(u32, []const u8) = .{},
memory: []u8,
code_start: usize,
code_end: usize,
const check_ops = true;
const assume_no_div_by_zero = false;
const Self = @This();
fn read(self: *Self, src: usize, dst: []u8) !void {
try self.assertOutsideCode(src);
@memcpy(dst, self.memory[src..][0..dst.len]);
}
fn write(self: *Self, src: []u8, dst: usize) !void {
try self.assertOutsideCode(dst);
@memcpy(self.memory[dst..][0..src.len], src);
}
fn setColor(self: *Self, color: std.io.tty.Color) !void {
try self.color_cfg.setColor(self.writer, color);
}
fn memmove(self: *Self, dst: usize, src: usize, len: usize) !void {
try self.assertOutsideCode(src);
try self.assertOutsideCode(dst);
const srcp = self.memory[src..];
const dstp = self.memory[dst..];
if (dst + len <= src or src + len <= dst) {
@memcpy(dstp[0..len], srcp[0..len]);
} else if (dst <= src) {
std.mem.copyForwards(u8, dstp[0..len], srcp[0..len]);
} else {
std.mem.copyBackwards(u8, dstp[0..len], srcp[0..len]);
}
}
fn assertOutsideCode(self: *const Self, pos: usize) !void {
if (self.code_start <= pos and pos < self.code_end) return error.MemOob;
if (self.memory.len <= pos) return error.MemOob;
}
fn progRead(self: *Self, comptime T: type, src: usize) !*align(1) const T {
const mem = self.memory[src..][0..@sizeOf(T)];
return @ptrCast(mem.ptr);
}
};
pub fn UnsafeCtx(comptime Writer: type) type {
return struct {
color_cfg: if (debug) std.io.tty.Config else void = undefined,
writer: if (debug) Writer else void = undefined,
const check_ops = debug;
const assume_no_div_by_zero = true;
const Self = @This();
fn read(_: *Self, src: usize, dst: []u8) !void {
const ptr: [*]u8 = @ptrFromInt(src);
@memcpy(dst, ptr);
}
fn write(_: *Self, src: []u8, dst: usize) !void {
const ptr: [*]u8 = @ptrFromInt(dst);
@memcpy(ptr, src);
}
fn setColor(self: *Self, color: std.io.tty.Color) !void {
try self.color_cfg.setColor(self.writer, color);
}
fn memmove(_: *Self, dst: usize, src: usize, len: usize) !void {
const srcp: [*]u8 = @ptrFromInt(src);
const dstp: [*]u8 = @ptrFromInt(dst);
if (dst + len <= src or src + len <= dst) {
@memcpy(dstp[0..len], srcp[0..len]);
} else if (dst <= src) {
std.mem.copyForwards(u8, dstp[0..len], srcp[0..len]);
} else {
std.mem.copyBackwards(u8, dstp[0..len], srcp[0..len]);
}
}
fn progRead(_: *Self, comptime T: type, src: usize) !*align(1) T {
return @ptrFromInt(src);
}
};
}
pub fn run(self: *Vm, ctx: anytype) !isa.Op {
@setEvalBranchQuota(3000);
while (self.fuel > 0) : (self.fuel -= 1) switch (try self.readOp(ctx)) {
.un => return error.Unreachable,
.nop => {},
.tx, .eca, .ebp => |op| return op,
.cp => {
const args = try self.readArgs(.cp, ctx);
self.writeReg(args.arg0, self.regs.get(args.arg1));
},
.swa => {
const args = try self.readArgs(.swa, ctx);
std.mem.swap(u64, self.regs.getPtr(args.arg0), self.regs.getPtr(args.arg1));
},
inline .@"and", .@"or", .xor, .cmpu, .cmps, .andi, .ori, .xori, .cmpui, .cmpsi => |op| {
const args = try self.readArgs(op, ctx);
const lhs = self.regs.get(args.arg1);
const rhs = if (@TypeOf(args.arg2) != isa.Reg) args.arg2 else self.regs.get(args.arg2);
const res = switch (op) {
.@"and", .andi => lhs & rhs,
.@"or", .ori => lhs | rhs,
.xor, .xori => lhs ^ rhs,
.cmpu, .cmpui => b: {
if (lhs < rhs) break :b toUnsigned(64, -1);
if (lhs == rhs) break :b 0;
break :b 1;
},
.cmps, .cmpsi => b: {
const slhs = toSigned(64, lhs);
const srhs = toSigned(64, rhs);
if (slhs < srhs) break :b toUnsigned(64, -1);
if (slhs == srhs) break :b 0;
break :b 1;
},
else => @compileError("what"),
};
self.writeReg(args.arg0, res);
},
inline .add8, .add16, .add32, .add64 => |op| try self.ibinOp(.add8, op, ctx),
inline .addi8, .addi16, .addi32, .addi64 => |op| try self.ibinOp(.addi8, op, ctx),
inline .sub8, .sub16, .sub32, .sub64 => |op| try self.ibinOp(.sub8, op, ctx),
inline .mul8, .mul16, .mul32, .mul64 => |op| try self.ibinOp(.mul8, op, ctx),
inline .muli8, .muli16, .muli32, .muli64 => |op| try self.ibinOp(.muli8, op, ctx),
inline .slu8, .slu16, .slu32, .slu64 => |op| try self.ibinOp(.slu8, op, ctx),
inline .slui8, .slui16, .slui32, .slui64 => |op| try self.ibinOp(.slui8, op, ctx),
inline .sru8, .sru16, .sru32, .sru64 => |op| try self.ibinOp(.sru8, op, ctx),
inline .srui8, .srui16, .srui32, .srui64 => |op| try self.ibinOp(.srui8, op, ctx),
inline .srs8, .srs16, .srs32, .srs64 => |op| try self.ibinOp(.srs8, op, ctx),
inline .srsi8, .srsi16, .srsi32, .srsi64 => |op| try self.ibinOp(.srsi8, op, ctx),
inline .diru8, .diru16, .diru32, .diru64 => |op| try self.idivOp(.diru8, op, ctx),
inline .dirs8, .dirs16, .dirs32, .dirs64 => |op| try self.idivOp(.dirs8, op, ctx),
inline .sxt8, .sxt16, .sxt32 => |op| {
const mask = comptime OpInteger(.sxt8, op);
const width = @bitSizeOf(mask);
const args = try self.readArgs(op, ctx);
const opera: mask = @truncate(self.regs.get(args.arg1));
self.writeReg(args.arg0, toUnsigned(64, toSigned(width, opera)));
},
inline .not, .neg, .itf32, .itf64 => |op| {
const args = try self.readArgs(op, ctx);
const opera = self.regs.get(args.arg1);
const res = switch (op) {
.not => @intFromBool(opera == 0),
.neg => -%opera,
.itf32 => @as(u32, @bitCast(@as(f32, @floatFromInt(toSigned(64, @truncate(opera)))))),
.itf64 => @as(u64, @bitCast(@as(f64, @floatFromInt(toSigned(64, opera))))),
else => @compileError("baka"),
};
self.writeReg(args.arg0, res);
},
inline .st, .ld, .str, .ldr, .str16, .ldr16 => |op| {
const addPc = op != .ld and op != .st;
const args = try self.readArgs(op, ctx);
const ptr = (if (addPc) self.ip -% isa.instrSize(op) else 0) +%
self.regs.get(args.arg1) +% toUnsigned(64, args.arg2);
const regp: [*]u8 = @ptrCast(@alignCast(self.regs.getPtr(args.arg0)));
switch (op) {
.st, .str, .str16 => try ctx.write(regp[0..args.arg3], @truncate(ptr)),
.ld, .ldr, .ldr16 => try ctx.read(@truncate(ptr), regp[0..args.arg3]),
else => @compileError("co"),
}
},
inline .li8, .li16, .li32, .li64 => |op| {
const args = try self.readArgs(op, ctx);
self.writeReg(args.arg0, args.arg1);
},
inline .lra, .lra16 => |op| {
const args = try self.readArgs(op, ctx);
const addr = self.ip -% isa.instrSize(op) +% self.regs.get(args.arg1) +% @as(usize, @bitCast(@as(isize, args.arg2)));
self.writeReg(args.arg0, addr);
},
.bmc => {
const args = try self.readArgs(.bmc, ctx);
// yep, retarded
try ctx.memmove(@truncate(self.regs.get(args.arg1)), @truncate(self.regs.get(args.arg0)), args.arg2);
},
.brc => {
const args = try self.readArgs(.brc, ctx);
const dst = self.regs.values[@intFromEnum(args.arg0)..][0..args.arg2];
const src = self.regs.values[@intFromEnum(args.arg1)..][0..args.arg2];
if (@intFromEnum(args.arg0) <= @intFromEnum(args.arg1)) {
std.mem.copyForwards(u64, dst, src);
} else {
std.mem.copyBackwards(u64, dst, src);
}
},
inline .jmp, .jmp16 => |op| {
const args = try self.readArgs(op, ctx);
self.ip +%= @as(usize, @truncate(toUnsigned(64, args.arg0))) -% isa.instrSize(op);
},
inline .jal, .jala => |op| {
const args = try self.readArgs(op, ctx);
self.writeReg(args.arg0, self.ip);
self.ip = (if (op == .jal) self.ip -% isa.instrSize(op) else 0);
self.ip +%= @as(usize, @truncate(self.regs.get(args.arg1))) +% @as(usize, @truncate(toUnsigned(64, args.arg2)));
},
inline .jltu, .jgtu, .jlts, .jgts, .jeq, .jne => |op| {
const args = try self.readArgs(op, ctx);
const lhs = self.regs.get(args.arg0);
const rhs = self.regs.get(args.arg1);
if (switch (op) {
.jltu => lhs < rhs,
.jgtu => lhs > rhs,
.jlts => toSigned(64, lhs) < toSigned(64, rhs),
.jgts => toSigned(64, lhs) > toSigned(64, rhs),
.jeq => lhs == rhs,
.jne => lhs != rhs,
else => @compileError("ke"),
}) {
self.ip +%= @as(usize, @truncate(toUnsigned(64, args.arg2))) -% isa.instrSize(op);
}
},
inline .fadd32, .fadd64 => |op| try self.fbinOp(.fadd32, op, ctx),
inline .fsub32, .fsub64 => |op| try self.fbinOp(.fsub32, op, ctx),
inline .fmul32, .fmul64 => |op| try self.fbinOp(.fmul32, op, ctx),
inline .fdiv32, .fdiv64 => |op| try self.fbinOp(.fdiv32, op, ctx),
inline .fma32, .fma64 => |op| try self.fbinOp(.fma32, op, ctx),
inline .finv32, .finv64 => |op| try self.fbinOp(.finv32, op, ctx),
inline .fcmplt32, .fcmplt64 => |op| try self.fbinOp(.fcmplt32, op, ctx),
inline .fcmpgt32, .fcmpgt64 => |op| try self.fbinOp(.fcmpgt32, op, ctx),
inline .fti32, .fti64 => |op| try self.fbinOp(.fti32, op, ctx),
inline .fc32t64, .fc64t32 => |op| try self.fbinOp(.fc32t64, op, ctx),
};
return error.Timeout;
}
inline fn fbinOp(self: *Vm, comptime base: isa.Op, comptime op: isa.Op, ctx: anytype) !void {
const Repr = OpFloat(base, op);
const args = try self.readArgs(op, ctx);
const Args = @TypeOf(args);
const lhs = self.readFloatReg(Repr, args.arg1);
const rhs = if (@hasField(Args, "arg2") and @TypeOf(args.arg2) == isa.Reg) self.readFloatReg(Repr, args.arg2);
const rhs2 = if (@hasField(Args, "arg3")) self.readFloatReg(Repr, args.arg3);
const res = switch (base) {
.fadd32 => lhs + rhs,
.fsub32 => lhs - rhs,
.fmul32 => lhs * rhs,
.fdiv32 => lhs / rhs,
.fma32 => lhs * rhs + rhs2,
.finv32 => 1.0 / lhs,
.fcmplt32 => lhs < rhs,
.fcmpgt32 => lhs > rhs,
.fc32t64 => @as(f64, @floatCast(lhs)),
.fc64t32 => @as(f32, @floatCast(lhs)),
.fti32 => b: {
const ty = if (Repr == f32) i32 else i64;
if (lhs > std.math.maxInt(ty)) return error.FloatToIntOverflow;
if (lhs < std.math.minInt(ty)) return error.FloatToIntOverflow;
break :b @as(ty, @intFromFloat(lhs));
},
else => |t| @compileError(std.fmt.comptimePrint("unspupported op {any}", .{t})),
};
self.writeReg(args.arg0, switch (@TypeOf(res)) {
bool => @intFromBool(res),
f32 => @as(u32, @bitCast(res)),
f64 => @as(u64, @bitCast(res)),
i32, i64 => toUnsigned(64, res),
else => @compileError("wat"),
});
}
inline fn readFloatReg(self: *Vm, comptime Repr: type, src: isa.Reg) Repr {
if (src == .null) return 0;
const IntRepr = if (Repr == f32) u32 else u64;
return @bitCast(@as(IntRepr, @truncate(self.regs.get(src))));
}
inline fn ibinOp(self: *Vm, comptime base: isa.Op, comptime op: isa.Op, ctx: anytype) !void {
const Repr = OpInteger(base, op);
const width = @bitSizeOf(Repr);
const args = try self.readArgs(op, ctx);
const lhs: Repr = @truncate(self.regs.get(args.arg1));
const rhs: Repr = if (@TypeOf(args.arg2) != isa.Reg) args.arg2 else @truncate(self.regs.get(args.arg2));
const res = switch (base) {
.add8, .addi8 => lhs +% rhs,
.sub8 => lhs -% rhs,
.mul8, .muli8 => lhs *% rhs,
.slu8, .slui8 => @shlWithOverflow(lhs, @as(std.math.Log2Int(@TypeOf(rhs)), @truncate(rhs)))[1],
.sru8, .srui8 => lhs >> @truncate(rhs),
.srs8, .srsi8 => toUnsigned(width, toSigned(width, lhs) >> @truncate(rhs)),
else => |t| @compileError(std.fmt.comptimePrint("unspupported op {any}", .{t})),
};
self.writeReg(args.arg0, res);
}
inline fn idivOp(self: *Vm, comptime base: isa.Op, comptime op: isa.Op, ctx: anytype) !void {
const Ctx = std.meta.Child(@TypeOf(ctx));
const Repr = OpInteger(base, op);
const width = @bitSizeOf(Repr);
const args = try self.readArgs(op, ctx);
const lhs: Repr = @truncate(self.regs.get(args.arg2));
const rhs: Repr = @truncate(self.regs.get(args.arg3));
if (!Ctx.assume_no_div_by_zero and rhs == 0) return error.DivideByZero;
switch (base) {
.diru8 => {
self.writeReg(args.arg0, lhs / rhs);
self.writeReg(args.arg1, lhs % rhs);
},
.dirs8 => {
const slhs = toSigned(width, lhs);
const srhs = toSigned(width, rhs);
self.writeReg(args.arg0, toUnsigned(width, @divTrunc(slhs, srhs)));
self.writeReg(args.arg1, toUnsigned(width, @mod(slhs, srhs)));
},
else => |t| @compileError(std.fmt.comptimePrint("unspupported op {any}", .{t})),
}
}
pub inline fn toSigned(comptime width: u16, value: std.meta.Int(.unsigned, width)) std.meta.Int(.signed, width) {
return @bitCast(value);
}
pub inline fn toUnsigned(comptime width: u16, value: std.meta.Int(.signed, width)) std.meta.Int(.unsigned, width) {
return @bitCast(value);
}
fn OpInteger(base: isa.Op, offset: isa.Op) type {
return .{ u8, u16, u32, u64 }[@as(u8, @intFromEnum(offset)) - @as(u8, @intFromEnum(base))];
}
fn OpFloat(base: isa.Op, offset: isa.Op) type {
return .{ f32, f64 }[@as(u8, @intFromEnum(offset)) - @as(u8, @intFromEnum(base))];
}
inline fn writeReg(self: *Vm, dst: isa.Reg, value: u64) void {
if (dst == .null) return;
self.regs.set(dst, value);
}
fn readOp(self: *Vm, ctx: anytype) !isa.Op {
const byte = try self.progRead(u8, ctx);
self.ip += 1;
if (std.meta.Child(@TypeOf(ctx)).check_ops and byte > isa.instr_count) {
return error.InvalidOp;
}
if (@TypeOf(ctx.writer) != void) {
const prev_ip = self.ip;
const instr_name = @tagName(@as(isa.Op, @enumFromInt(byte)));
for (instr_name.len..isa.max_instr_len) |_| try ctx.writer.writeAll(" ");
try ctx.writer.writeAll(instr_name);
const argTys = isa.spec[byte][1];
try self.readOpArgs(argTys, ctx);
try ctx.writer.writeAll("\n");
self.ip = prev_ip;
}
return @enumFromInt(byte);
}
fn readOpArgs(self: *Vm, argTys: []const u8, ctx: anytype) !void {
const prev_ip = self.ip - 1;
var seen_regs = std.EnumSet(isa.Reg){};
for (argTys, 0..) |argTy, i| {
const argt = isa.Arg.fromChar(argTy);
if (i > 0) try ctx.writer.writeAll(", ") else try ctx.writer.writeAll(" ");
try self.displayArg(prev_ip, argt, ctx, &seen_regs);
}
}
fn displayArg(
self: *Vm,
prev_ip: usize,
arg: isa.Arg,
ctx: anytype,
seen_regs: *std.EnumSet(isa.Reg),
) !void {
switch (arg) {
inline .reg => |t| {
const value = try self.progRead(isa.ArgType(t), ctx);
self.ip += @sizeOf(isa.ArgType(t));
const col: std.io.tty.Color = @enumFromInt(3 + @intFromEnum(value) % 12);
try ctx.setColor(col);
try ctx.writer.print("${d}", .{@intFromEnum(value)});
try ctx.setColor(.reset);
if (!seen_regs.contains(value)) {
seen_regs.insert(value);
try ctx.setColor(.dim);
try ctx.writer.writeAll("=");
try ctx.writer.print("{d}", .{@as(i64, @bitCast(self.regs.get(value)))});
try ctx.setColor(.reset);
}
},
inline else => |t| {
const value = try self.progRead(isa.ArgType(t), ctx);
self.ip += @sizeOf(isa.ArgType(t));
try ctx.setColor(arg.color());
const pos = if (t == .rel32) @as(i32, @intCast(prev_ip)) + value - @as(i32, @intCast(ctx.code_start)) else 0;
if (t == .rel32 and ctx.symbols.get(@intCast(pos)) != null) {
try ctx.writer.print("{s}", .{ctx.symbols.get(@intCast(pos)).?});
} else if (@typeInfo(@TypeOf(value)).int.signedness == .unsigned) {
try ctx.writer.print("{any}", .{@as(std.meta.Int(.signed, @bitSizeOf(@TypeOf(value))), @bitCast(value))});
} else {
try ctx.writer.print("{any}", .{value});
}
try ctx.setColor(.reset);
},
}
}
fn readArgs(self: *Vm, comptime op: isa.Op, ctx: anytype) !isa.ArgsOf(op) {
defer self.ip += @sizeOf(isa.ArgsOf(op));
return try self.progRead(isa.ArgsOf(op), ctx);
}
fn progRead(self: *Vm, comptime T: type, ctx: anytype) !T {
return (try ctx.progRead(T, self.ip)).*;
}
pub fn testRun(vm: *Vm, res: anyerror!isa.Op, regRes: anytype, comptime code: anytype) !void {
const fode = isa.packMany(code);
vm.ip = @intFromPtr(fode.ptr);
var ctx = UnsafeCtx(void){};
try std.testing.expectEqual(res, vm.run(&ctx));
try std.testing.expectEqual(regRes, vm.regs.get(.ret(0)));
}
test "sanity" {
var vm = Vm{ .fuel = 1000 };
try testRun(&vm, error.Unreachable, 1, .{
.{ .li8, 1, 1 },
.{.un},
});
try testRun(&vm, .tx, 2, .{
.{ .li8, 1, 1 },
.{ .li8, 2, 1 },
.{ .add8, 1, 1, 2 },
.{.tx},
});
try testRun(&vm, .tx, toUnsigned(64, -1), .{
.{ .li64, 1, 1 },
.{ .li64, 2, toUnsigned(64, -1) },
.{ .cmpu, 1, 1, 2 },
.{.tx},
});
try testRun(&vm, .tx, toUnsigned(64, -1), .{
.{ .li8, 1, toUnsigned(8, -1) },
.{ .sxt8, 1, 1 },
.{.tx},
});
}