From f2fef240a1a1d7caa06daa8e1bdf58206b42a73c Mon Sep 17 00:00:00 2001 From: Noam Preil Date: Wed, 19 Aug 2020 12:45:34 -0400 Subject: [PATCH] SPU-II: Test harness skeleton --- lib/std/spu/interpreter.zig | 159 ++++++++++++++++++++++++++++++++++++ src-self-hosted/test.zig | 51 +++++++++++- test/stage2/spu-ii.zig | 23 ++++++ test/stage2/test.zig | 1 + 4 files changed, 233 insertions(+), 1 deletion(-) create mode 100644 lib/std/spu/interpreter.zig create mode 100644 test/stage2/spu-ii.zig diff --git a/lib/std/spu/interpreter.zig b/lib/std/spu/interpreter.zig new file mode 100644 index 0000000000..ed22b13099 --- /dev/null +++ b/lib/std/spu/interpreter.zig @@ -0,0 +1,159 @@ +const std = @import("std"); +usingnamespace @import("defines.zig"); + +pub fn Interpreter(comptime Bus: type) type { + return struct { + ip: u16 = 0, + sp: u16 = undefined, + bp: u16 = undefined, + fr: FlagRegister = @bitCast(FlagRegister, @as(u16, 0)), + /// This is set to true when we hit an undefined0 instruction, allowing it to + /// be used as a trap for testing purposes + undefined0: bool = false, + bus: Bus, + + pub fn ExecuteBlock(self: *@This(), comptime size: ?u32) !void { + var count: usize = 0; + while (size == null or count < size.?) { + count += 1; + var instruction = @bitCast(Instruction, self.bus.read16(self.ip)); + + std.log.debug(.SPU_2_Interpreter, "Executing {}\n", .{instruction}); + + self.ip +%= 2; + + const execute = switch (instruction.condition) { + .always => true, + .not_zero => !self.fr.zero, + .when_zero => self.fr.zero, + .overflow => self.fr.carry, + ExecutionCondition.greater_or_equal_zero => !self.fr.negative, + else => return error.unimplemented, + }; + + if (execute) { + const val0 = switch (instruction.input0) { + .zero => @as(u16, 0), + .immediate => i: { + const val = self.bus.read16(@intCast(u16, self.ip)); + self.ip +%= 2; + break :i val; + }, + else => |e| e: { + // peek or pop; show value at current SP, and if pop, increment sp + const val = self.bus.read16(self.sp); + if (e == .pop) { + self.sp +%= 2; + } + break :e val; + }, + }; + const val1 = switch (instruction.input1) { + .zero => @as(u16, 0), + .immediate => i: { + const val = self.bus.read16(@intCast(u16, self.ip)); + self.ip +%= 2; + break :i val; + }, + else => |e| e: { + // peek or pop; show value at current SP, and if pop, increment sp + const val = self.bus.read16(self.sp); + if (e == .pop) { + self.sp +%= 2; + } + break :e val; + }, + }; + + const output: u16 = switch (instruction.command) { + .get => self.bus.read16(self.bp +% (2 *% val0)), + .set => a: { + self.bus.write16(self.bp +% 2 *% val0, val1); + break :a val1; + }, + .load8 => self.bus.read8(val0), + .load16 => self.bus.read16(val0), + .store8 => a: { + const val = @truncate(u8, val1); + self.bus.write8(val0, val); + break :a val; + }, + .store16 => a: { + self.bus.write16(val0, val1); + break :a val1; + }, + .copy => val0, + .add => a: { + var val: u16 = undefined; + self.fr.carry = @addWithOverflow(u16, val0, val1, &val); + break :a val; + }, + .sub => a: { + var val: u16 = undefined; + self.fr.carry = @subWithOverflow(u16, val0, val1, &val); + break :a val; + }, + .spset => a: { + self.sp = val0; + break :a val0; + }, + .bpset => a: { + self.bp = val0; + break :a val0; + }, + .frset => a: { + const val = (@bitCast(u16, self.fr) & val1) | (val0 & ~val1); + self.fr = @bitCast(FlagRegister, val); + break :a val; + }, + .bswap => (val0 >> 8) | (val0 << 8), + .bpget => self.bp, + .spget => self.sp, + .ipget => self.ip +% (2 *% val0), + .lsl => val0 << 1, + .lsr => val0 >> 1, + .@"and" => val0 & val1, + .@"or" => val0 | val1, + .xor => val0 ^ val1, + .not => ~val0, + .undefined0 => { + self.undefined0 = true; + // Break out of the loop, and let the caller decide what to do + return; + }, + .undefined1 => return error.BadInstruction, + .signext => if ((val0 & 0x80) != 0) + (val0 & 0xFF) | 0xFF00 + else + (val0 & 0xFF), + else => return error.unimplemented, + }; + + switch (instruction.output) { + .discard => {}, + .push => { + self.sp -%= 2; + self.bus.write16(self.sp, output); + }, + .jump => { + self.ip = output; + if (!(instruction.command == .copy and instruction.input0 == .immediate)) { + // Not absolute. Break, for compatibility with JIT. + break; + } + }, + else => return error.unimplemented, + } + if (instruction.modify_flags) { + self.fr.negative = (output & 0x8000) != 0; + self.fr.zero = (output == 0x0000); + } + } else { + if (instruction.input0 == .immediate) self.ip +%= 2; + if (instruction.input1 == .immediate) self.ip +%= 2; + break; + } + } + } + }; +} diff --git a/src-self-hosted/test.zig b/src-self-hosted/test.zig index 9e88466cf7..db2c4a8838 100644 --- a/src-self-hosted/test.zig +++ b/src-self-hosted/test.zig @@ -571,6 +571,56 @@ pub const TestContext = struct { std.debug.assert(!case.cbe); update_node.estimated_total_items = 4; + if (case.target.cpu_arch) |arch| { + if (arch == .spu_2) { + if (case.target.os_tag) |os| { + if (os != .freestanding) { + std.debug.panic("Only freestanding makes sense for SPU-II tests!", .{}); + } + } else { + std.debug.panic("SPU_2 has no native OS, check the test!", .{}); + } + var output = std.ArrayList(u8).init(allocator); + // defer output.deinit(); + const uart = struct { + fn in(_out: usize) !u8 { + return error.not_implemented; + } + fn out(_output: usize, val: u8) !void { + try @intToPtr(*std.ArrayList(u8), _output).append(val); + } + }; + var interpreter = std.spu.interpreter(struct { + RAM: [0x10000]u8 = undefined, + + pub fn read8(bus: @This(), addr: u16) u8 { + return bus.RAM[addr]; + } + pub fn read16(bus: @This(), addr: u16) u16 { + return std.mem.readIntLittle(u16, bus.RAM[addr..][0..2]); + } + + pub fn write8(bus: *@This(), addr: u16, val: u8) void { + bus.RAM[addr] = val; + } + + pub fn write16(bus: *@This(), addr: u16, val: u16) void { + std.mem.writeIntLittle(u16, bus.RAM[addr..][0..2], val); + } + }){ + .bus = .{}, + }; + + //defer interpreter.deinit(allocator); + std.debug.print("TODO implement SPU-II test loader\n", .{}); + std.process.exit(1); + // TODO: loop detection? Solve the halting problem? fork() and limit by wall clock? + // Limit by emulated cycles? + // while (!interpreter.undefined0) { + // try interpreter.ExecuteBlock(100); + // } + } + } var exec_result = x: { var exec_node = update_node.start("execute", null); exec_node.activate(); @@ -635,7 +685,6 @@ pub const TestContext = struct { var test_node = update_node.start("test", null); test_node.activate(); defer test_node.end(); - defer allocator.free(exec_result.stdout); defer allocator.free(exec_result.stderr); switch (exec_result.term) { diff --git a/test/stage2/spu-ii.zig b/test/stage2/spu-ii.zig new file mode 100644 index 0000000000..1316f19d0d --- /dev/null +++ b/test/stage2/spu-ii.zig @@ -0,0 +1,23 @@ +const std = @import("std"); +const TestContext = @import("../../src-self-hosted/test.zig").TestContext; + +const spu = std.zig.CrossTarget{ + .cpu_arch = .spu_2, + .os_tag = .freestanding, +}; + +pub fn addCases(ctx: *TestContext) !void { + { + var case = ctx.exe("SPU-II Basic Test", spu); + case.addCompareOutput( + \\fn killEmulator() noreturn { + \\ asm volatile ("undefined0"); + \\ unreachable; + \\} + \\ + \\export fn _start() noreturn { + \\ killEmulator(); + \\} + , ""); + } +} diff --git a/test/stage2/test.zig b/test/stage2/test.zig index 791a073393..832d9a44db 100644 --- a/test/stage2/test.zig +++ b/test/stage2/test.zig @@ -785,4 +785,5 @@ pub fn addCases(ctx: *TestContext) !void { \\} \\extern var foo; , &[_][]const u8{":4:1: error: unable to infer variable type"}); + try @import("spu-ii.zig").addCases(ctx); }