OXYGENHUB
◆ main enhanced-x16-emulator / tests / check_opcode_spec.py
4.1 KiB · Python · last changed chore(): initial commit 25 days ago source blame history raw
9120e874 Andy McCall 1
#!/usr/bin/env python3
2
"""Check the generated opcode tables against the published 65C02 spec.
3
4
ProcessorTests measures what the hardware does, across tens of thousands of
5
randomised cases. This checks something different and much smaller: that the
6
sizes and cycle counts in src/cpu/tables.h still agree with the documentation.
7
8
The two have disagreed once already, over $5C, where both WDC datasheets and
9
the 6502.org list say eight cycles and the test suite says four. Divergences
10
like that are listed below rather than hidden, so changing one is a deliberate
11
act.
12
"""
13
14
import pathlib
15
import re
16
import sys
17
18
ROOT = pathlib.Path(__file__).resolve().parent.parent
19
20
# Bytes consumed by each addressing mode, including the opcode.
21
MODE_BYTES = {
22
    "imp": 1, "acc": 1, "imp8": 1,
23
    "imm8": 2, "immm": 2, "immx": 2,
24
    "zp": 2, "zpx": 2, "zpy": 2, "rel": 2,
25
    "ind0": 2, "indx": 2, "indy": 2,
26
    "abso": 3, "absx": 3, "absy": 3, "ind": 3, "ainx": 3, "zprel": 3,
27
}
28
29
# Where this emulator knowingly differs from the published 65C02 spec. The
30
# value is what the emulator is expected to be, so a drift away from the
31
# recorded difference fails rather than being waved through.
32
KNOWN = {
33
    0xDB: ((1, 1), "the X16 maps $DB to a debugger trap rather than STP"),
34
}
35
36
# The spec file is data, and data can be quietly emptied. A truncated file would
37
# otherwise report zero failures out of zero opcodes and pass.
38
MIN_ROWS = 120
39
40
41
def read_table(path, decl):
42
    text = (ROOT / path).read_text(encoding="utf-8")
43
    start = text.index(decl) + len(decl)
44
    body = text[start:text.index("};", start)]
45
    body = re.sub(r"/\*.*?\*/", "", body, flags=re.S)
46
    return [x.strip() for x in body.replace("\n", " ").split(",") if x.strip()]
47
48
49
def read_spec():
50
    rows = {}
51
    path = ROOT / "tests" / "spec_65c02.txt"
52
    for line in path.read_text(encoding="utf-8").splitlines():
53
        if not line.strip() or line.startswith("#"):
54
            continue
55
        op, size, cycles, note = line.split()
56
        rows[int(op, 16)] = (int(size), int(cycles), note)
57
    return rows
58
59
60
def main() -> int:
61
    modes = read_table("src/cpu/tables.h", "addrtable_c02[256])() = {")
62
    ticks = [int(x) for x in read_table("src/cpu/tables.h", "ticktable_c02[256] = {")]
63
    spec = read_spec()
64
65
    if len(modes) != 256 or len(ticks) != 256:
66
        print("FAIL: parsed %d modes and %d cycle counts, expected 256 of each"
67
              % (len(modes), len(ticks)))
68
        return 1
69
70
    failures = 0
71
    if len(spec) < MIN_ROWS:
72
        print("FAIL: spec_65c02.txt holds %d opcodes, expected at least %d"
73
              % (len(spec), MIN_ROWS))
74
        failures += 1
75
76
    for op in sorted(spec):
77
        want_size, want_cycles, _note = spec[op]
78
        mode = modes[op]
79
        if mode not in MODE_BYTES:
80
            print("FAIL: $%02X uses addressing mode %s, which has no size here"
81
                  % (op, mode))
82
            failures += 1
83
            continue
84
        got = (MODE_BYTES[mode], ticks[op])
85
        if got == (want_size, want_cycles):
86
            if op in KNOWN:
87
                print("FAIL: $%02X now matches the spec; drop it from KNOWN (%s)"
88
                      % (op, KNOWN[op][1]))
89
                failures += 1
90
            continue
91
        if op in KNOWN:
92
            expected, why = KNOWN[op]
93
            if got == expected:
94
                print("ok   : $%02X is %d bytes and %d cycles rather than %d and "
95
                      "%d -- %s" % (op, got[0], got[1], want_size, want_cycles, why))
96
            else:
97
                print("FAIL: $%02X is %d bytes and %d cycles; the spec says %d and "
98
                      "%d and the recorded difference is %d and %d"
99
                      % (op, got[0], got[1], want_size, want_cycles,
100
                         expected[0], expected[1]))
101
                failures += 1
102
            continue
103
        print("FAIL: $%02X is %d bytes and %d cycles, spec says %d and %d"
104
              % (op, got[0], got[1], want_size, want_cycles))
105
        failures += 1
106
107
    print("%s: %d opcodes checked against the published spec, %d failed"
108
          % ("opcode_spec", len(spec), failures))
109
    return 1 if failures else 0
110
111
112
if __name__ == "__main__":
113
    sys.exit(main())