#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(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 }