Files
psxrecomp/recompiler/tests/l2_structural_test.cpp
T
2026-07-19 14:23:36 -07:00

415 lines
16 KiB
C++

/*
* L2 Structural Test — Codegen Pattern Verification
*
* For each MIPS instruction form, decodes the instruction, translates it
* via StrictTranslator, and verifies the generated C code contains the
* expected register references and operation patterns.
*
* This is the "snesrecomp-style" approach: structural pattern matching on
* the generated C text. It catches wrong register indices, missing
* operations, and translation gaps. It does NOT verify semantic
* correctness (that requires L2-semantic with actual execution).
*
* Exit codes:
* 0 all forms pass
* 1 at least one form fails
* 2 usage error
*/
#include <cstdio>
#include <cstdint>
#include <cstring>
#include <string>
#include <vector>
#include <functional>
#include "mips_decoder.h"
#include "strict_translator.h"
#include "gte_register_classification.h"
#include "fmt/format.h"
using PSXRecomp::MipsDecoder;
using PSXRecomp::DecodedInstruction;
using PSXRecompV4::StrictTranslator;
using PSXRecompV4::TranslateResult;
// ── Helpers ─────────────────────────────────────────────────────────────
static bool contains(const std::string& haystack, const std::string& needle) {
return haystack.find(needle) != std::string::npos;
}
// MIPS instruction encoding helpers
static constexpr uint32_t R_TYPE(uint8_t rs, uint8_t rt, uint8_t rd, uint8_t sa, uint8_t fn) {
return (0u << 26) | ((uint32_t)rs << 21) | ((uint32_t)rt << 16)
| ((uint32_t)rd << 11) | ((uint32_t)sa << 6) | fn;
}
static constexpr uint32_t I_TYPE(uint8_t op, uint8_t rs, uint8_t rt, uint16_t imm) {
return ((uint32_t)op << 26) | ((uint32_t)rs << 21) | ((uint32_t)rt << 16) | imm;
}
// Standard test registers: v0(2), v1(3), a0(4)
static constexpr uint8_t V0 = 2, V1 = 3, A0 = 4;
// ── Form definition ─────────────────────────────────────────────────────
struct Form {
uint32_t word;
const char* name;
// Checker: given the TranslateResult, return empty string on pass
// or an error message on failure.
std::function<std::string(const TranslateResult&)> check;
};
// Shorthand: check that translation is supported and c_code contains all
// of the given substrings.
static auto must_contain(std::initializer_list<const char*> needles) {
std::vector<std::string> v(needles.begin(), needles.end());
return [v](const TranslateResult& r) -> std::string {
if (!r.supported)
return fmt::format("unsupported: {}", r.fail_reason);
for (auto& n : v) {
if (!contains(r.c_code, n))
return fmt::format("missing '{}' in: {}", n, r.c_code);
}
return "";
};
}
// Check supported + custom lambda on c_code
static auto check_code(std::function<std::string(const std::string&)> fn) {
return [fn](const TranslateResult& r) -> std::string {
if (!r.supported)
return fmt::format("unsupported: {}", r.fail_reason);
return fn(r.c_code);
};
}
// Check that host-visible operations occur in a specific source order. This
// matters for overlay stores: the DLL-local cycle batch must be published
// before an MMIO write or SWL/SWR's read-modify-write begins.
static auto must_contain_in_order(std::initializer_list<const char*> needles) {
std::vector<std::string> v(needles.begin(), needles.end());
return check_code([v](const std::string& c) -> std::string {
std::string::size_type pos = 0;
for (const auto& n : v) {
const auto found = c.find(n, pos);
if (found == std::string::npos)
return fmt::format("missing/out-of-order '{}' in: {}", n, c);
pos = found + n.size();
}
return "";
});
}
// ── Form table ──────────────────────────────────────────────────────────
static const Form kForms[] = {
// === SPECIAL (R-type) ALU ===
// SLL $v0, $a0, 1
{ R_TYPE(0, A0, V0, 1, 0x00), "sll_v0_a0_1",
must_contain({"gpr[2]", "gpr[4]", "<< 1u"}) },
// SRL $v0, $a0, 1
{ R_TYPE(0, A0, V0, 1, 0x02), "srl_v0_a0_1",
must_contain({"gpr[2]", "gpr[4]", ">> 1u"}) },
// SRA $v0, $a0, 1
{ R_TYPE(0, A0, V0, 1, 0x03), "sra_v0_a0_1",
must_contain({"gpr[2]", "int32_t", "gpr[4]", ">> 1u"}) },
// SLLV $v0, $a0, $v1
{ R_TYPE(V1, A0, V0, 0, 0x04), "sllv_v0_a0_v1",
must_contain({"gpr[2]", "gpr[4]", "gpr[3]", "0x1Fu"}) },
// SRLV $v0, $a0, $v1
{ R_TYPE(V1, A0, V0, 0, 0x06), "srlv_v0_a0_v1",
must_contain({"gpr[2]", "gpr[4]", "gpr[3]", "0x1Fu"}) },
// SRAV $v0, $a0, $v1
{ R_TYPE(V1, A0, V0, 0, 0x07), "srav_v0_a0_v1",
must_contain({"gpr[2]", "int32_t", "gpr[4]", "gpr[3]", "0x1Fu"}) },
// ADDU $v0, $v1, $a0
{ R_TYPE(V1, A0, V0, 0, 0x21), "addu_v0_v1_a0",
must_contain({"gpr[2]", "gpr[3]", "gpr[4]", "+"}) },
// SUBU $v0, $v1, $a0
{ R_TYPE(V1, A0, V0, 0, 0x23), "subu_v0_v1_a0",
must_contain({"gpr[2]", "gpr[3]", "gpr[4]", "-"}) },
// AND $v0, $v1, $a0
{ R_TYPE(V1, A0, V0, 0, 0x24), "and_v0_v1_a0",
must_contain({"gpr[2]", "gpr[3]", "gpr[4]", "&"}) },
// OR $v0, $v1, $a0
{ R_TYPE(V1, A0, V0, 0, 0x25), "or_v0_v1_a0",
must_contain({"gpr[2]", "gpr[3]", "gpr[4]", "|"}) },
// XOR $v0, $v1, $a0
{ R_TYPE(V1, A0, V0, 0, 0x26), "xor_v0_v1_a0",
must_contain({"gpr[2]", "gpr[3]", "gpr[4]", "^"}) },
// NOR $v0, $v1, $a0
{ R_TYPE(V1, A0, V0, 0, 0x27), "nor_v0_v1_a0",
must_contain({"gpr[2]", "gpr[3]", "gpr[4]", "~"}) },
// SLT $v0, $v1, $a0
{ R_TYPE(V1, A0, V0, 0, 0x2A), "slt_v0_v1_a0",
must_contain({"gpr[2]", "int32_t", "gpr[3]", "gpr[4]"}) },
// SLTU $v0, $v1, $a0
{ R_TYPE(V1, A0, V0, 0, 0x2B), "sltu_v0_v1_a0",
must_contain({"gpr[2]", "gpr[3]", "gpr[4]", "<"}) },
// JR $at: target must be snapshotted before its delay slot.
{ R_TYPE(1, 0, 0, 0, 0x08), "jr_at_latches_target",
[](const TranslateResult& r) -> std::string {
if (!r.supported) return fmt::format("unsupported: {}", r.fail_reason);
if (!contains(r.pre_delay_code, "psx_jt_00000000 = cpu->gpr[1]"))
return fmt::format("missing pre-delay target latch: {}", r.pre_delay_code);
if (!contains(r.c_code, "cpu->pc = psx_jt_00000000"))
return fmt::format("terminator does not use latched target: {}", r.c_code);
if (contains(r.c_code, "cpu->pc = cpu->gpr[1]"))
return fmt::format("terminator rereads target after delay: {}", r.c_code);
return "";
}},
// JALR $at,$at: capture must precede link write (rd==rs alias-safe).
{ R_TYPE(1, 0, 1, 0, 0x09), "jalr_at_at_latches_before_link",
[](const TranslateResult& r) -> std::string {
if (!r.supported) return fmt::format("unsupported: {}", r.fail_reason);
const auto latch = r.pre_delay_code.find("psx_jt_00000000 = cpu->gpr[1]");
const auto link = r.pre_delay_code.find("cpu->gpr[1] = 0x00000008u");
if (latch == std::string::npos || link == std::string::npos || latch > link)
return fmt::format("target not latched before aliased link: {}", r.pre_delay_code);
if (!contains(r.c_code, "cpu->pc = psx_jt_00000000"))
return fmt::format("terminator does not use latched target: {}", r.c_code);
return "";
}},
// ADD $v0, $v1, $a0 (with overflow trap)
{ R_TYPE(V1, A0, V0, 0, 0x20), "add_v0_v1_a0",
must_contain({"gpr[2]", "gpr[3]", "gpr[4]", "psx_arith_overflow"}) },
// MULTU $v1, $a0
{ R_TYPE(V1, A0, 0, 0, 0x19), "multu_v1_a0",
must_contain({"cpu->hi", "cpu->lo", "gpr[3]", "gpr[4]", "uint64_t"}) },
// DIV $v1, $a0
{ R_TYPE(V1, A0, 0, 0, 0x1A), "div_v1_a0",
must_contain({"cpu->hi", "cpu->lo", "gpr[3]", "gpr[4]"}) },
// DIVU $v1, $a0
{ R_TYPE(V1, A0, 0, 0, 0x1B), "divu_v1_a0",
must_contain({"cpu->hi", "cpu->lo", "gpr[3]", "gpr[4]"}) },
// MFHI $v0
{ R_TYPE(0, 0, V0, 0, 0x10), "mfhi_v0",
must_contain({"gpr[2]", "cpu->hi"}) },
// MFLO $v0
{ R_TYPE(0, 0, V0, 0, 0x12), "mflo_v0",
must_contain({"gpr[2]", "cpu->lo"}) },
// MTHI $v1
{ R_TYPE(V1, 0, 0, 0, 0x11), "mthi_v1",
must_contain({"cpu->hi", "gpr[3]"}) },
// MTLO $v1
{ R_TYPE(V1, 0, 0, 0, 0x13), "mtlo_v1",
must_contain({"cpu->lo", "gpr[3]"}) },
// === Immediate ALU ===
// ADDIU $v0, $v1, 42
{ I_TYPE(0x09, V1, V0, 42), "addiu_v0_v1_42",
must_contain({"gpr[2]", "gpr[3]", "42"}) },
// ADDI $v0, $v1, 42 (with overflow trap)
{ I_TYPE(0x08, V1, V0, 42), "addi_v0_v1_42",
must_contain({"gpr[2]", "gpr[3]", "42", "psx_arith_overflow"}) },
// ANDI $v0, $v1, 0xFF
{ I_TYPE(0x0C, V1, V0, 0xFF), "andi_v0_v1_ff",
must_contain({"gpr[2]", "gpr[3]", "0xFFu"}) },
// ORI $v0, $v1, 0xFF
{ I_TYPE(0x0D, V1, V0, 0xFF), "ori_v0_v1_ff",
must_contain({"gpr[2]", "gpr[3]", "0xFFu"}) },
// XORI $v0, $v1, 0xFF
{ I_TYPE(0x0E, V1, V0, 0xFF), "xori_v0_v1_ff",
must_contain({"gpr[2]", "gpr[3]", "0xFFu"}) },
// LUI $v0, 0x8000
{ I_TYPE(0x0F, 0, V0, 0x8000), "lui_v0_8000",
must_contain({"gpr[2]", "0x80000000u"}) },
// SLTI $v0, $v1, 42
{ I_TYPE(0x0A, V1, V0, 42), "slti_v0_v1_42",
must_contain({"gpr[2]", "int32_t", "gpr[3]", "42"}) },
// SLTIU $v0, $v1, 42
{ I_TYPE(0x0B, V1, V0, 42), "sltiu_v0_v1_42",
must_contain({"gpr[2]", "gpr[3]", "<"}) },
// === Load/Store ===
// LW $v0, 0($v1)
{ I_TYPE(0x23, V1, V0, 0), "lw_v0_0_v1",
must_contain({"gpr[2]", "gpr[3]", "load_word"}) },
// SW $a0, 0($v1)
{ I_TYPE(0x2B, V1, A0, 0), "sw_a0_0_v1",
must_contain_in_order({"psx_store_cycle_barrier()", "write_word"}) },
// LB $v0, 0($v1)
{ I_TYPE(0x20, V1, V0, 0), "lb_v0_0_v1",
must_contain({"gpr[2]", "gpr[3]", "load_byte", "int8_t"}) },
// LBU $v0, 0($v1)
{ I_TYPE(0x24, V1, V0, 0), "lbu_v0_0_v1",
must_contain({"gpr[2]", "gpr[3]", "load_byte"}) },
// LH $v0, 0($v1)
{ I_TYPE(0x21, V1, V0, 0), "lh_v0_0_v1",
must_contain({"gpr[2]", "gpr[3]", "load_half", "int16_t"}) },
// LHU $v0, 0($v1)
{ I_TYPE(0x25, V1, V0, 0), "lhu_v0_0_v1",
must_contain({"gpr[2]", "gpr[3]", "load_half"}) },
// SB $a0, 0($v1)
{ I_TYPE(0x28, V1, A0, 0), "sb_a0_0_v1",
must_contain_in_order({"psx_store_cycle_barrier()", "write_byte"}) },
// SH $a0, 0($v1)
{ I_TYPE(0x29, V1, A0, 0), "sh_a0_0_v1",
must_contain_in_order({"psx_store_cycle_barrier()", "write_half"}) },
// SWL/SWR are read-modify-write stores. Publishing after the raw read is
// too late because the read itself may be MMIO and host-visible.
{ I_TYPE(0x2A, V1, A0, 1), "swl_a0_1_v1",
must_contain_in_order({"psx_store_cycle_barrier()", "read_word", "write_word"}) },
{ I_TYPE(0x2E, V1, A0, 2), "swr_a0_2_v1",
must_contain_in_order({"psx_store_cycle_barrier()", "read_word", "write_word"}) },
// SWC2 must include the GTE stall in the published timestamp, then expose
// that timestamp before the memory and precision-shadow side effects.
{ I_TYPE(0x3A, V1, A0, 0), "swc2_a0_0_v1",
must_contain_in_order({"psx_gte_stall", "psx_store_cycle_barrier()", "write_word",
"gte_precision_store_word"}) },
// === COP0 ===
// MFC0 $v0, $12 (read SR)
{ I_TYPE(0x10, 0x00, V0, 12 << 11), "mfc0_v0_sr",
check_code([](const std::string& c) -> std::string {
if (!contains(c, "gpr[2]")) return "missing gpr[2]";
if (!contains(c, "cop0[12]")) return "missing cop0[12]";
return "";
})},
// MTC0 $v1, $12 (write SR)
{ I_TYPE(0x10, 0x04, V1, 12 << 11), "mtc0_v1_sr",
check_code([](const std::string& c) -> std::string {
if (!contains(c, "gpr[3]")) return "missing gpr[3]";
if (!contains(c, "cop0[12]")) return "missing cop0[12]";
return "";
})},
// === $zero writes should be discarded ===
// ADDIU $zero, $v1, 1 → should be discarded/commented
{ I_TYPE(0x09, V1, 0, 1), "addiu_zero_v1_1",
check_code([](const std::string& c) -> std::string {
// Must not actually write to gpr[0] — should be a comment or no-op
if (contains(c, "gpr[0] =") && !contains(c, "/*"))
return "writes to $zero without discard";
return "";
})},
// NOP (SLL $zero, $zero, 0 = 0x00000000)
{ 0x00000000, "nop",
check_code([](const std::string& c) -> std::string {
// NOP should produce empty or nop comment
if (c.size() > 40 && !contains(c, "nop"))
return fmt::format("suspicious NOP output: {}", c);
return "";
})},
};
// ── main ────────────────────────────────────────────────────────────────
int main(int argc, char** argv) {
bool verbose = false;
for (int i = 1; i < argc; i++) {
if (std::strcmp(argv[i], "-v") == 0 || std::strcmp(argv[i], "--verbose") == 0)
verbose = true;
}
int total = 0, pass = 0, fail = 0;
for (const auto& form : kForms) {
total++;
DecodedInstruction d = MipsDecoder::decode(form.word, 0);
TranslateResult r = StrictTranslator::translate(d);
std::string err = form.check(r);
if (err.empty()) {
pass++;
if (verbose)
fmt::print(" PASS {:<30s} {}\n", form.name, r.c_code);
} else {
fail++;
fmt::print(" FAIL {:<30s} {}\n", form.name, err);
if (verbose)
fmt::print(" c_code: {}\n", r.c_code);
}
}
// Exhaustively lock the helper/raw classification for every COP2 register
// and transfer form. This prevents native BIOS/overlay code from drifting
// away from the interpreter's canonical register semantics.
const auto check_gte_helper = [&](uint32_t word, const std::string& call,
bool expected, const char *kind,
uint8_t reg) {
++total;
const TranslateResult r = StrictTranslator::translate(
MipsDecoder::decode(word, 0));
const bool actual = r.supported && contains(r.c_code, call);
if (actual == expected) {
++pass;
} else {
++fail;
fmt::print(" FAIL strict GTE {} reg {} helper={} expected={} code={}\n",
kind, reg, actual, expected, r.c_code);
}
};
for (uint8_t reg = 0; reg < 32; ++reg) {
const uint32_t cop2 = 0x12u << 26;
check_gte_helper(cop2 | (0x00u << 21) | (V0 << 16) | (uint32_t(reg) << 11),
fmt::format("gte_read_data(cpu, {})", reg),
PSXRecompGTERegisters::data_read_needs_helper(reg), "MFC2", reg);
check_gte_helper(cop2 | (0x02u << 21) | (V0 << 16) | (uint32_t(reg) << 11),
fmt::format("gte_read_ctrl(cpu, {})", reg),
PSXRecompGTERegisters::ctrl_read_needs_helper(reg), "CFC2", reg);
check_gte_helper(cop2 | (0x04u << 21) | (V0 << 16) | (uint32_t(reg) << 11),
fmt::format("gte_write_data(cpu, {}", reg),
PSXRecompGTERegisters::data_write_needs_helper(reg), "MTC2", reg);
check_gte_helper(cop2 | (0x06u << 21) | (V0 << 16) | (uint32_t(reg) << 11),
fmt::format("gte_write_ctrl(cpu, {}", reg),
PSXRecompGTERegisters::ctrl_write_needs_helper(reg), "CTC2", reg);
check_gte_helper(I_TYPE(0x32, V1, reg, 0),
fmt::format("gte_write_data(cpu, {}", reg),
PSXRecompGTERegisters::data_write_needs_helper(reg), "LWC2", reg);
check_gte_helper(I_TYPE(0x3A, V1, reg, 0),
fmt::format("gte_read_data(cpu, {})", reg),
PSXRecompGTERegisters::data_read_needs_helper(reg), "SWC2", reg);
}
fmt::print("\nL2 structural: {}/{} ok", pass, total);
if (fail > 0) fmt::print(" ({} failures)", fail);
fmt::print("\n");
return fail > 0 ? 1 : 0;
}