Files
WebMetal/wasm/tests/conformance_test.cpp
2026-07-19 18:05:04 +02:00

212 lines
7.5 KiB
C++

#define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN
#include "doctest.h"
#include <fstream>
#include <sstream>
#include <nlohmann/json.hpp>
#include "webmetal/engine.h"
using nlohmann::json;
using webmetal::Engine;
#ifndef WEBMETAL_CONFORMANCE_PATH
#error "WEBMETAL_CONFORMANCE_PATH must be defined by the build"
#endif
namespace {
json loadVectorFile() {
std::ifstream in(WEBMETAL_CONFORMANCE_PATH);
REQUIRE_MESSAGE(in.good(), "cannot open " WEBMETAL_CONFORMANCE_PATH);
std::stringstream buffer;
buffer << in.rdbuf();
json j = json::parse(buffer.str(), nullptr, false);
REQUIRE_FALSE(j.is_discarded());
return j;
}
struct Layout {
std::uint32_t pc = 0;
std::uint32_t status = 1;
std::uint32_t flagsOffset = 4;
std::vector<std::string> flagNames;
struct Entry {
std::uint32_t offset = 0;
std::uint32_t count = 0;
};
std::map<std::string, Entry> banks;
std::map<std::string, Entry> memories;
};
Layout parseLayout(const std::string& text) {
const json j = json::parse(text, nullptr, false);
REQUIRE_FALSE(j.is_discarded());
Layout layout;
layout.pc = j["pc"].get<std::uint32_t>();
layout.status = j["status"].get<std::uint32_t>();
layout.flagsOffset = j["flags"]["offset"].get<std::uint32_t>();
for (const auto& name : j["flags"]["names"]) {
layout.flagNames.push_back(name.get<std::string>());
}
for (const auto& b : j["banks"]) {
layout.banks[b["name"].get<std::string>()] = {
b["offset"].get<std::uint32_t>(), b["count"].get<std::uint32_t>()};
}
for (const auto& m : j["memories"]) {
layout.memories[m["name"].get<std::string>()] = {
m["offset"].get<std::uint32_t>(), m["size"].get<std::uint32_t>()};
}
return layout;
}
int flagIndexOf(const Layout& layout, const std::string& name) {
for (std::size_t i = 0; i < layout.flagNames.size(); i++) {
if (layout.flagNames[i] == name) return static_cast<int>(i);
}
return -1;
}
void applySetup(Engine& engine, const Layout& layout, const json& setup) {
auto* state = reinterpret_cast<std::uint32_t*>(engine.stateBufferPtr());
if (setup.contains("banks")) {
for (const auto& [name, values] : setup["banks"].items()) {
REQUIRE(layout.banks.count(name) == 1);
const auto entry = layout.banks.at(name);
std::uint32_t i = 0;
for (const auto& v : values) {
REQUIRE(i < entry.count);
state[entry.offset + i++] = v.get<std::uint32_t>();
}
}
}
if (setup.contains("memories")) {
for (const auto& [name, cells] : setup["memories"].items()) {
REQUIRE(layout.memories.count(name) == 1);
const auto entry = layout.memories.at(name);
for (const auto& [addr, v] : cells.items()) {
state[entry.offset + std::stoul(addr)] = v.get<std::uint32_t>();
}
}
}
if (setup.contains("flags")) {
for (const auto& [name, v] : setup["flags"].items()) {
const int index = flagIndexOf(layout, name);
REQUIRE(index >= 0);
state[layout.flagsOffset + index] = v.get<bool>() ? 1 : 0;
}
}
if (setup.contains("pc")) {
state[layout.pc] = setup["pc"].get<std::uint32_t>();
}
}
std::vector<std::string> compareExpect(const Engine& engine,
const Layout& layout,
const json& expect) {
std::vector<std::string> problems;
const auto* state =
reinterpret_cast<const std::uint32_t*>(engine.stateBufferPtr());
const auto mismatch = [&](const std::string& what, std::uint32_t got,
std::uint32_t want) {
problems.push_back(what + " = " + std::to_string(got) + ", want " +
std::to_string(want));
};
if (expect.contains("banks")) {
for (const auto& [name, values] : expect["banks"].items()) {
const auto entry = layout.banks.at(name);
std::uint32_t i = 0;
for (const auto& v : values) {
const auto want = v.get<std::uint32_t>();
const auto got = state[entry.offset + i];
if (got != want) {
mismatch(name + "[" + std::to_string(i) + "]", got, want);
}
i++;
}
}
}
if (expect.contains("memories")) {
for (const auto& [name, cells] : expect["memories"].items()) {
const auto entry = layout.memories.at(name);
for (const auto& [addr, v] : cells.items()) {
const auto want = v.get<std::uint32_t>();
const auto got = state[entry.offset + std::stoul(addr)];
if (got != want) mismatch(name + "[" + addr + "]", got, want);
}
}
}
if (expect.contains("flags")) {
for (const auto& [name, v] : expect["flags"].items()) {
const int index = flagIndexOf(layout, name);
const bool want = v.get<bool>();
const bool got = state[layout.flagsOffset + index] != 0;
if (got != want) {
problems.push_back("flag " + name + " = " +
(got ? "true" : "false"));
}
}
}
if (expect.contains("pc")) {
const auto want = expect["pc"].get<std::uint32_t>();
if (state[layout.pc] != want) mismatch("pc", state[layout.pc], want);
}
if (expect.contains("halted")) {
const bool want = expect["halted"].get<bool>();
const bool got = (state[layout.status] & 1u) != 0;
if (got != want) {
problems.push_back(std::string("halted = ") +
(got ? "true" : "false"));
}
}
return problems;
}
} // namespace
TEST_CASE("C++ engine passes every shared conformance vector") {
const json file = loadVectorFile();
const json defaultModel = file["defaultModel"];
for (const auto& vector : file["vectors"]) {
const std::string name = vector["name"].get<std::string>();
CAPTURE(name);
Engine engine;
const json model =
vector.contains("model") ? vector["model"] : defaultModel;
REQUIRE_MESSAGE(engine.loadModel(model.dump()), name);
const Layout layout = parseLayout(engine.stateLayout());
if (vector.contains("setup")) {
applySetup(engine, layout, vector["setup"]);
}
std::string error;
for (const auto& step : vector["run"]) {
const std::string operands =
step.contains("operands") ? step["operands"].dump() : "{}";
error = engine.execSequence(step["microOps"].dump(), operands);
if (!error.empty()) break;
}
if (vector.contains("expectError")) {
const auto substring = vector["expectError"].get<std::string>();
CHECK_MESSAGE(!error.empty(), name, ": expected an error");
CHECK_MESSAGE(error.find(substring) != std::string::npos, name,
": error \"", error, "\" lacks \"", substring, "\"");
continue;
}
CHECK_MESSAGE(error.empty(), name, ": unexpected error: ", error);
if (vector.contains("expect")) {
const auto problems =
compareExpect(engine, layout, vector["expect"]);
for (const auto& problem : problems) {
FAIL_CHECK(name << ": " << problem);
}
}
}
}