Files
2026-07-19 18:05:04 +02:00

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
}