mirror of
https://github.com/ApfelTeeSaft/WebMetal.git
synced 2026-08-27 03:53:24 +00:00
208 lines
7.0 KiB
C++
208 lines
7.0 KiB
C++
#define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN
|
|
#include "doctest.h"
|
|
|
|
#include "webmetal/engine.h"
|
|
|
|
using webmetal::Engine;
|
|
|
|
namespace {
|
|
|
|
const char* kModel = R"({
|
|
"modelVersion": 1,
|
|
"banks": [{ "name": "R", "width": 8, "count": 4 }],
|
|
"memories": [{ "name": "MAIN", "size": 256, "width": 8 }],
|
|
"flags": ["Z", "N", "C"],
|
|
"pc": { "width": 16 },
|
|
"programMemory": "MAIN"
|
|
})";
|
|
|
|
const char* kIsa = R"({
|
|
"opcodeField": { "offset": 0, "width": 8 },
|
|
"instructions": [
|
|
{
|
|
"mnemonic": "LDI", "opcode": 1, "words": 2,
|
|
"operands": [
|
|
{ "name": "rd", "field": { "word": 1, "offset": 6, "width": 2 } },
|
|
{ "name": "imm", "field": { "word": 1, "offset": 0, "width": 6 } }
|
|
],
|
|
"microOps": [
|
|
{ "op": "move",
|
|
"dst": { "kind": "reg", "bank": "R", "index": { "kind": "operand", "field": "rd" } },
|
|
"src": { "kind": "operand", "field": "imm" } }
|
|
]
|
|
},
|
|
{
|
|
"mnemonic": "ADD", "opcode": 2, "words": 2,
|
|
"operands": [
|
|
{ "name": "rd", "field": { "word": 1, "offset": 6, "width": 2 } },
|
|
{ "name": "rs", "field": { "word": 1, "offset": 4, "width": 2 } }
|
|
],
|
|
"microOps": [
|
|
{ "op": "alu", "fn": "add", "width": 8, "setFlags": true,
|
|
"dst": { "kind": "reg", "bank": "R", "index": { "kind": "operand", "field": "rd" } },
|
|
"a": { "kind": "reg", "bank": "R", "index": { "kind": "operand", "field": "rd" } },
|
|
"b": { "kind": "reg", "bank": "R", "index": { "kind": "operand", "field": "rs" } } }
|
|
]
|
|
},
|
|
{
|
|
"mnemonic": "JMP", "opcode": 3, "words": 2,
|
|
"operands": [
|
|
{ "name": "addr", "field": { "word": 1, "offset": 0, "width": 8 } }
|
|
],
|
|
"microOps": [
|
|
{ "op": "jump", "target": { "kind": "operand", "field": "addr" } }
|
|
]
|
|
},
|
|
{ "mnemonic": "HLT", "opcode": 4, "words": 1, "operands": [],
|
|
"microOps": [ { "op": "halt" } ] }
|
|
]
|
|
})";
|
|
|
|
// LDI R0,#5; LDI R1,#7; ADD R0,R1; HLT
|
|
const char* kProgram =
|
|
R"([{ "address": 0, "values": [1, 5, 1, 71, 2, 16, 4] }])";
|
|
// 71 = (1<<6)|7 -> rd=1 imm=7; 16 = (0<<6)|(1<<4) -> rd=0 rs=1
|
|
|
|
struct Layout {
|
|
std::uint32_t flagsOffset = 4;
|
|
std::uint32_t bankROffset = 7; // 4 header + 3 flags
|
|
std::uint32_t mainOffset = 11; // + 4 registers
|
|
};
|
|
|
|
Engine loadedEngine() {
|
|
Engine engine;
|
|
REQUIRE(engine.loadModel(kModel));
|
|
REQUIRE(engine.loadIsa(kIsa));
|
|
REQUIRE(engine.loadProgram(kProgram));
|
|
return engine;
|
|
}
|
|
|
|
const std::uint32_t* stateOf(const Engine& engine) {
|
|
return reinterpret_cast<const std::uint32_t*>(engine.stateBufferPtr());
|
|
}
|
|
|
|
} // namespace
|
|
|
|
TEST_CASE("version identifies the real engine") {
|
|
Engine engine;
|
|
CHECK(engine.version() == std::string("webmetal-engine 0.2.0"));
|
|
}
|
|
|
|
TEST_CASE("loadModel builds the state layout") {
|
|
Engine engine;
|
|
REQUIRE(engine.loadModel(kModel));
|
|
// 4 header + 3 flags + 4 registers + 256 memory words
|
|
CHECK(engine.stateBufferLen() == 4 + 3 + 4 + 256);
|
|
const auto layout = engine.stateLayout();
|
|
CHECK(layout.find("\"banks\"") != std::string::npos);
|
|
CHECK(layout.find("\"MAIN\"") != std::string::npos);
|
|
}
|
|
|
|
TEST_CASE("loadModel/loadIsa reject malformed input with messages") {
|
|
Engine engine;
|
|
CHECK_FALSE(engine.loadModel("{nope"));
|
|
CHECK(engine.lastError() == "not valid JSON");
|
|
CHECK_FALSE(engine.loadModel("{\"x\": 1}"));
|
|
CHECK(engine.lastError() == "model is missing \"modelVersion\"");
|
|
CHECK_FALSE(engine.loadIsa("{\"instructions\": []}"));
|
|
CHECK(engine.lastError() == "ISA is missing \"opcodeField\"");
|
|
}
|
|
|
|
TEST_CASE("a full program runs to halt with correct state") {
|
|
Engine engine = loadedEngine();
|
|
const std::uint32_t executed = engine.run(100);
|
|
CHECK(executed == 4);
|
|
CHECK(engine.halted());
|
|
CHECK_FALSE(engine.errored());
|
|
|
|
const Layout layout;
|
|
const auto* state = stateOf(engine);
|
|
CHECK(state[layout.bankROffset + 0] == 12); // R0 = 5 + 7
|
|
CHECK(state[layout.bankROffset + 1] == 7);
|
|
CHECK(state[layout.flagsOffset + 0] == 0); // Z
|
|
CHECK(state[layout.flagsOffset + 1] == 0); // N
|
|
CHECK(state[layout.flagsOffset + 2] == 0); // C
|
|
CHECK(engine.pc() == 7); // advanced past HLT
|
|
CHECK(state[webmetal::kStateInstructionCount] == 4);
|
|
CHECK(state[webmetal::kStateCycleCount] == 4); // one micro-op each
|
|
// program image loaded into memory
|
|
CHECK(state[layout.mainOffset + 0] == 1);
|
|
CHECK(state[layout.mainOffset + 6] == 4);
|
|
}
|
|
|
|
TEST_CASE("unknown opcode stops with an error") {
|
|
Engine engine = loadedEngine();
|
|
REQUIRE(engine.loadProgram(R"([{ "address": 0, "values": [99] }])"));
|
|
CHECK(engine.step(1) == 0);
|
|
CHECK(engine.errored());
|
|
CHECK(engine.lastError().find("unknown opcode 99 at address 0") !=
|
|
std::string::npos);
|
|
// further stepping is a no-op until reset
|
|
CHECK(engine.step(5) == 0);
|
|
engine.reset();
|
|
CHECK_FALSE(engine.errored());
|
|
}
|
|
|
|
TEST_CASE("run honors breakpoints; step ignores them") {
|
|
Engine engine = loadedEngine();
|
|
engine.setBreakpoint(4); // address of ADD
|
|
|
|
CHECK(engine.run(100) == 2); // LDI, LDI, stop AT ADD
|
|
CHECK(engine.pc() == 4);
|
|
CHECK_FALSE(engine.halted());
|
|
|
|
// continue from the breakpoint: does not immediately re-trigger
|
|
CHECK(engine.run(100) == 2); // ADD, HLT
|
|
CHECK(engine.halted());
|
|
|
|
engine.reset();
|
|
CHECK(engine.step(4) == 4); // step ignores the breakpoint entirely
|
|
CHECK(engine.halted());
|
|
|
|
engine.clearBreakpoint(4);
|
|
CHECK_FALSE(engine.hasBreakpoint(4));
|
|
}
|
|
|
|
TEST_CASE("reset restores the program and clears state") {
|
|
Engine engine = loadedEngine();
|
|
engine.run(100);
|
|
engine.reset();
|
|
const Layout layout;
|
|
const auto* state = stateOf(engine);
|
|
CHECK(engine.pc() == 0);
|
|
CHECK_FALSE(engine.halted());
|
|
CHECK(state[layout.bankROffset + 0] == 0);
|
|
CHECK(state[webmetal::kStateInstructionCount] == 0);
|
|
CHECK(state[layout.mainOffset + 0] == 1); // program re-applied
|
|
CHECK(engine.run(100) == 4); // and runs again
|
|
}
|
|
|
|
TEST_CASE("jump loops execute (pc pre-advance semantics)") {
|
|
Engine engine = loadedEngine();
|
|
// JMP 0 at address 0 -> infinite loop
|
|
REQUIRE(engine.loadProgram(R"([{ "address": 0, "values": [3, 0] }])"));
|
|
CHECK(engine.step(10) == 10);
|
|
CHECK(engine.pc() == 0);
|
|
CHECK_FALSE(engine.halted());
|
|
}
|
|
|
|
TEST_CASE("execSequence mirrors the reference executor error surface") {
|
|
Engine engine;
|
|
REQUIRE(engine.loadModel(kModel));
|
|
const auto err = engine.execSequence(
|
|
R"([{ "op": "move",
|
|
"dst": { "kind": "reg", "bank": "GHOST", "index": { "kind": "literal", "value": 0 } },
|
|
"src": { "kind": "const", "value": 1 } }])",
|
|
"{}");
|
|
CHECK(err.find("unknown register bank") != std::string::npos);
|
|
|
|
const auto ok = engine.execSequence(
|
|
R"([{ "op": "move",
|
|
"dst": { "kind": "reg", "bank": "R", "index": { "kind": "literal", "value": 2 } },
|
|
"src": { "kind": "const", "value": 511 } }])",
|
|
"{}");
|
|
CHECK(ok.empty());
|
|
const Layout layout;
|
|
CHECK(stateOf(engine)[layout.bankROffset + 2] == 255); // masked to 8 bits
|
|
}
|