m68000-python 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- m68000_python/__init__.py +73 -0
- m68000_python/__main__.py +112 -0
- m68000_python/_alu.py +656 -0
- m68000_python/_bcd.py +137 -0
- m68000_python/_bits.py +100 -0
- m68000_python/_control.py +208 -0
- m68000_python/_core.py +545 -0
- m68000_python/_dispatch.py +243 -0
- m68000_python/_ea.py +177 -0
- m68000_python/_flags.py +152 -0
- m68000_python/_loads.py +405 -0
- m68000_python/_shifts.py +168 -0
- m68000_python/_system.py +314 -0
- m68000_python/conformance.py +796 -0
- m68000_python/console.py +254 -0
- m68000_python/cpu.py +373 -0
- m68000_python/debug.py +361 -0
- m68000_python/disasm.py +354 -0
- m68000_python/py.typed +0 -0
- m68000_python/state.py +76 -0
- m68000_python/trace.py +296 -0
- m68000_python-0.1.0.dist-info/METADATA +379 -0
- m68000_python-0.1.0.dist-info/RECORD +25 -0
- m68000_python-0.1.0.dist-info/WHEEL +4 -0
- m68000_python-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,796 @@
|
|
|
1
|
+
"""Conformance kit: run a manifest on the reference core, or diff a foreign trace against it.
|
|
2
|
+
|
|
3
|
+
A **manifest** is a small JSON document that fully determines a run: what is
|
|
4
|
+
in memory, the initial processor state, how the host answers the interrupt
|
|
5
|
+
acknowledge cycle, where the bus asserts BERR, which reads come from a
|
|
6
|
+
recorded device stream, when the interrupt level changes, and when to stop.
|
|
7
|
+
Two cores given the same manifest see the same machine, so any difference
|
|
8
|
+
between their traces is a difference between the CPUs. The trace format
|
|
9
|
+
itself is docs/trace-schema.md; this module is the part that makes traces
|
|
10
|
+
comparable (docs/conformance.md).
|
|
11
|
+
|
|
12
|
+
Two entry points, also exposed as ``python -m m68000_python.conformance``:
|
|
13
|
+
|
|
14
|
+
* :func:`trace_manifest` runs the manifest on :class:`M68000CPU` and yields
|
|
15
|
+
one :class:`StepRecord` per boundary, bus accesses included: the
|
|
16
|
+
reference trace.
|
|
17
|
+
* :func:`diff_manifest` runs the same manifest in lockstep against an
|
|
18
|
+
external trace and returns the first :class:`TraceDivergence`, or ``None``.
|
|
19
|
+
|
|
20
|
+
The module depends only on the standard library and the rest of this package.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
import argparse
|
|
26
|
+
import json
|
|
27
|
+
import sys
|
|
28
|
+
from collections.abc import Callable, Iterable, Iterator
|
|
29
|
+
from dataclasses import dataclass, field, fields, replace
|
|
30
|
+
from pathlib import Path
|
|
31
|
+
from typing import TextIO
|
|
32
|
+
|
|
33
|
+
from m68000_python._core import AUTOVECTOR, SPURIOUS, BusError
|
|
34
|
+
from m68000_python.cpu import M68000CPU
|
|
35
|
+
from m68000_python.debug import BoundaryKind, DebugSession, StepRecord, next_boundary
|
|
36
|
+
from m68000_python.state import CPUState
|
|
37
|
+
from m68000_python.trace import TraceDivergence, first_trace_divergence, read_trace, write_trace
|
|
38
|
+
|
|
39
|
+
__all__ = [
|
|
40
|
+
"MANIFEST_SCHEMA_VERSION",
|
|
41
|
+
"MEMORY_SIZE",
|
|
42
|
+
"AddressRange",
|
|
43
|
+
"ConformanceHost",
|
|
44
|
+
"Event",
|
|
45
|
+
"Manifest",
|
|
46
|
+
"MemorySegment",
|
|
47
|
+
"Replay",
|
|
48
|
+
"StopRule",
|
|
49
|
+
"TraceRun",
|
|
50
|
+
"diff_manifest",
|
|
51
|
+
"load_manifest",
|
|
52
|
+
"main",
|
|
53
|
+
"manifest_from_dict",
|
|
54
|
+
"manifest_to_dict",
|
|
55
|
+
"trace_manifest",
|
|
56
|
+
]
|
|
57
|
+
|
|
58
|
+
MANIFEST_SCHEMA_VERSION = 1
|
|
59
|
+
|
|
60
|
+
#: The flat host's memory: the whole 24-bit address space the 68000 drives (UM 3.1).
|
|
61
|
+
MEMORY_SIZE = 0x1000000
|
|
62
|
+
|
|
63
|
+
#: Host profiles. ``flat``: 16 MiB of RAM. ``replay``: the same, plus device
|
|
64
|
+
#: windows whose reads come from a recorded stream, see :class:`ConformanceHost`.
|
|
65
|
+
HOST_PROFILES = ("flat", "replay")
|
|
66
|
+
EVENT_KINDS = ("ipl", "reset")
|
|
67
|
+
TAS_WRITES = ("write", "drop")
|
|
68
|
+
#: The acknowledge answers a manifest may name, and the core's value for each.
|
|
69
|
+
ACKNOWLEDGE_NAMES = {"autovector": AUTOVECTOR, "spurious": SPURIOUS}
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@dataclass(frozen=True, slots=True)
|
|
73
|
+
class MemorySegment:
|
|
74
|
+
"""Bytes to place at ``address`` before the run starts."""
|
|
75
|
+
|
|
76
|
+
address: int
|
|
77
|
+
data: bytes
|
|
78
|
+
|
|
79
|
+
def __post_init__(self) -> None:
|
|
80
|
+
if type(self.address) is not int or not 0 <= self.address < MEMORY_SIZE:
|
|
81
|
+
raise ValueError("segment address must be an integer in range 0..0xFFFFFF")
|
|
82
|
+
if type(self.data) is not bytes or not self.data:
|
|
83
|
+
raise ValueError("segment data must be non-empty bytes")
|
|
84
|
+
if self.address + len(self.data) > MEMORY_SIZE:
|
|
85
|
+
raise ValueError("segment does not fit below 0x1000000")
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
@dataclass(frozen=True, slots=True)
|
|
89
|
+
class AddressRange:
|
|
90
|
+
"""``length`` bytes from ``address``: a BERR range or a device window."""
|
|
91
|
+
|
|
92
|
+
address: int
|
|
93
|
+
length: int
|
|
94
|
+
|
|
95
|
+
def __post_init__(self) -> None:
|
|
96
|
+
if type(self.address) is not int or not 0 <= self.address < MEMORY_SIZE:
|
|
97
|
+
raise ValueError("range address must be an integer in range 0..0xFFFFFF")
|
|
98
|
+
if type(self.length) is not int or self.length <= 0:
|
|
99
|
+
raise ValueError("range length must be a positive integer")
|
|
100
|
+
if self.address + self.length > MEMORY_SIZE:
|
|
101
|
+
raise ValueError("range does not fit below 0x1000000")
|
|
102
|
+
|
|
103
|
+
def __contains__(self, address: object) -> bool:
|
|
104
|
+
return type(address) is int and self.address <= address < self.address + self.length
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
@dataclass(frozen=True, slots=True)
|
|
108
|
+
class Replay:
|
|
109
|
+
"""The ``replay`` host's device windows and the recorded values their reads return.
|
|
110
|
+
|
|
111
|
+
``reads`` is a sequence of big-endian 16-bit values, consumed one per
|
|
112
|
+
read inside a window, in the order the CPU makes the reads.
|
|
113
|
+
"""
|
|
114
|
+
|
|
115
|
+
devices: tuple[AddressRange, ...]
|
|
116
|
+
reads: bytes
|
|
117
|
+
|
|
118
|
+
def __post_init__(self) -> None:
|
|
119
|
+
if type(self.devices) is not tuple or not all(
|
|
120
|
+
type(window) is AddressRange for window in self.devices
|
|
121
|
+
):
|
|
122
|
+
raise ValueError("replay devices must be a tuple of AddressRange values")
|
|
123
|
+
if type(self.reads) is not bytes or len(self.reads) % 2:
|
|
124
|
+
raise ValueError("replay reads must be bytes holding whole 16-bit values")
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
@dataclass(frozen=True, slots=True)
|
|
128
|
+
class Event:
|
|
129
|
+
"""A host action applied immediately before boundary ``at_step``.
|
|
130
|
+
|
|
131
|
+
Steps count every record (instruction, trace, interrupt, idle) from 0.
|
|
132
|
+
``ipl`` sets the interrupt level through ``set_ipl(level)``, so the
|
|
133
|
+
boundary at ``at_step`` is the first that can observe it; ``reset`` calls
|
|
134
|
+
``reset()``, which produces no record (its clocks land in ``clock``).
|
|
135
|
+
"""
|
|
136
|
+
|
|
137
|
+
at_step: int
|
|
138
|
+
kind: str
|
|
139
|
+
level: int | None = None
|
|
140
|
+
|
|
141
|
+
def __post_init__(self) -> None:
|
|
142
|
+
if type(self.at_step) is not int or self.at_step < 0:
|
|
143
|
+
raise ValueError("event at_step must be a non-negative integer")
|
|
144
|
+
if self.kind not in EVENT_KINDS:
|
|
145
|
+
raise ValueError(f"event kind must be one of {EVENT_KINDS}, got {self.kind!r}")
|
|
146
|
+
if self.kind == "ipl":
|
|
147
|
+
if type(self.level) is not int or not 0 <= self.level <= 7:
|
|
148
|
+
raise ValueError("an ipl event's level must be an integer in range 0..7")
|
|
149
|
+
elif self.level is not None:
|
|
150
|
+
raise ValueError("a reset event has no level")
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
@dataclass(frozen=True, slots=True)
|
|
154
|
+
class StopRule:
|
|
155
|
+
"""When the run ends. ``max_steps`` is mandatory so every run is finite.
|
|
156
|
+
|
|
157
|
+
The run stops *before* a boundary when the state's ``pc`` is one of
|
|
158
|
+
``at_pc``; when the next boundary would be an idle one (halted, or inside
|
|
159
|
+
STOP with nothing able to end it) and no event remains (``on_idle``); or
|
|
160
|
+
when the step budget is spent. A stopped run's trace ends; the boundary
|
|
161
|
+
that would have followed is not recorded.
|
|
162
|
+
"""
|
|
163
|
+
|
|
164
|
+
max_steps: int
|
|
165
|
+
on_idle: bool = True
|
|
166
|
+
at_pc: tuple[int, ...] = ()
|
|
167
|
+
|
|
168
|
+
def __post_init__(self) -> None:
|
|
169
|
+
if type(self.max_steps) is not int or self.max_steps <= 0:
|
|
170
|
+
raise ValueError("stop.max_steps must be a positive integer")
|
|
171
|
+
if type(self.on_idle) is not bool:
|
|
172
|
+
raise ValueError("stop.on_idle must be a bool")
|
|
173
|
+
if type(self.at_pc) is not tuple or not all(
|
|
174
|
+
type(pc) is int and 0 <= pc <= 0xFFFFFFFF for pc in self.at_pc
|
|
175
|
+
):
|
|
176
|
+
raise ValueError("stop.at_pc must be a tuple of 32-bit addresses")
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
@dataclass(frozen=True, slots=True)
|
|
180
|
+
class Manifest:
|
|
181
|
+
"""A complete, deterministic description of one conformance run.
|
|
182
|
+
|
|
183
|
+
``initial`` is the state restored before the first boundary, its queue
|
|
184
|
+
already filled (see :func:`manifest_from_dict`); with ``reset`` the host
|
|
185
|
+
calls ``reset()`` instead and ``initial`` is unused. ``acknowledge`` is
|
|
186
|
+
``(level, answer)`` pairs, the answer a vector number 0-255 or the core's
|
|
187
|
+
``AUTOVECTOR`` or ``SPURIOUS``; unlisted levels autovector.
|
|
188
|
+
"""
|
|
189
|
+
|
|
190
|
+
name: str
|
|
191
|
+
memory: tuple[MemorySegment, ...]
|
|
192
|
+
initial: CPUState
|
|
193
|
+
stop: StopRule
|
|
194
|
+
host: str = "flat"
|
|
195
|
+
reset: bool = False
|
|
196
|
+
acknowledge: tuple[tuple[int, int], ...] = ()
|
|
197
|
+
bus_error: tuple[AddressRange, ...] = ()
|
|
198
|
+
tas_write: str = "write"
|
|
199
|
+
replay: Replay | None = None
|
|
200
|
+
events: tuple[Event, ...] = field(default_factory=tuple)
|
|
201
|
+
|
|
202
|
+
def __post_init__(self) -> None:
|
|
203
|
+
if type(self.name) is not str or not self.name:
|
|
204
|
+
raise ValueError("manifest name must be a non-empty string")
|
|
205
|
+
if type(self.memory) is not tuple or not all(
|
|
206
|
+
type(segment) is MemorySegment for segment in self.memory
|
|
207
|
+
):
|
|
208
|
+
raise ValueError("manifest memory must be a tuple of MemorySegment values")
|
|
209
|
+
if type(self.initial) is not CPUState:
|
|
210
|
+
raise ValueError("manifest initial must be a CPUState")
|
|
211
|
+
if type(self.stop) is not StopRule:
|
|
212
|
+
raise ValueError("manifest stop must be a StopRule")
|
|
213
|
+
if self.host not in HOST_PROFILES:
|
|
214
|
+
raise ValueError(f"manifest host must be one of {HOST_PROFILES}, got {self.host!r}")
|
|
215
|
+
if type(self.reset) is not bool:
|
|
216
|
+
raise ValueError("manifest reset must be a bool")
|
|
217
|
+
if type(self.acknowledge) is not tuple or not all(
|
|
218
|
+
type(pair) is tuple
|
|
219
|
+
and len(pair) == 2
|
|
220
|
+
and type(pair[0]) is int
|
|
221
|
+
and 1 <= pair[0] <= 7
|
|
222
|
+
and type(pair[1]) is int
|
|
223
|
+
and (0 <= pair[1] <= 0xFF or pair[1] in (AUTOVECTOR, SPURIOUS))
|
|
224
|
+
for pair in self.acknowledge
|
|
225
|
+
):
|
|
226
|
+
raise ValueError("manifest acknowledge must be (level 1-7, answer) pairs")
|
|
227
|
+
levels = [level for level, _ in self.acknowledge]
|
|
228
|
+
if levels != sorted(set(levels)):
|
|
229
|
+
raise ValueError("manifest acknowledge levels must be distinct and ordered")
|
|
230
|
+
if type(self.bus_error) is not tuple or not all(
|
|
231
|
+
type(item) is AddressRange for item in self.bus_error
|
|
232
|
+
):
|
|
233
|
+
raise ValueError("manifest bus_error must be a tuple of AddressRange values")
|
|
234
|
+
if self.tas_write not in TAS_WRITES:
|
|
235
|
+
raise ValueError(f"manifest tas_write must be one of {TAS_WRITES}")
|
|
236
|
+
if (self.host == "replay") != (self.replay is not None):
|
|
237
|
+
raise ValueError("a replay host needs replay, and only a replay host has it")
|
|
238
|
+
if self.replay is not None and type(self.replay) is not Replay:
|
|
239
|
+
raise ValueError("manifest replay must be a Replay")
|
|
240
|
+
if type(self.events) is not tuple or not all(type(event) is Event for event in self.events):
|
|
241
|
+
raise ValueError("manifest events must be a tuple of Event values")
|
|
242
|
+
steps = [event.at_step for event in self.events]
|
|
243
|
+
if steps != sorted(steps):
|
|
244
|
+
raise ValueError("manifest events must be ordered by at_step")
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
class ConformanceHost:
|
|
248
|
+
"""The one host every conformance run uses, so hosts cannot differ between cores.
|
|
249
|
+
|
|
250
|
+
16 MiB of RAM over the 24-bit address space, zero but for the manifest's
|
|
251
|
+
segments; reads return its bytes (words big-endian) and writes store
|
|
252
|
+
them. On every access, in this order:
|
|
253
|
+
|
|
254
|
+
* an address inside a ``bus_error`` range raises :class:`BusError` (a word
|
|
255
|
+
access is checked at its address only, which is even);
|
|
256
|
+
* with the ``replay`` profile, a read inside a device window takes the
|
|
257
|
+
next 16-bit value of the reads stream (a byte read its low 8 bits), or
|
|
258
|
+
0xFFFF (0xFF) once the stream is exhausted, and a write inside one is
|
|
259
|
+
discarded;
|
|
260
|
+
* otherwise memory.
|
|
261
|
+
|
|
262
|
+
The interrupt acknowledge answers with the manifest's ``acknowledge``
|
|
263
|
+
map, autovectoring an unlisted level; ``tas_write: "drop"`` discards
|
|
264
|
+
TAS's write cycle (after the BERR check), as the Genesis bus does.
|
|
265
|
+
A port must implement exactly this to be comparable.
|
|
266
|
+
"""
|
|
267
|
+
|
|
268
|
+
def __init__(self, manifest: Manifest) -> None:
|
|
269
|
+
self.memory = memory = bytearray(MEMORY_SIZE)
|
|
270
|
+
for segment in manifest.memory:
|
|
271
|
+
memory[segment.address : segment.address + len(segment.data)] = segment.data
|
|
272
|
+
self._reads = manifest.replay.reads if manifest.replay is not None else b""
|
|
273
|
+
self.reads_consumed = 0
|
|
274
|
+
read_byte, read_word, write_byte, write_word = self._bus(manifest)
|
|
275
|
+
answers = dict(manifest.acknowledge)
|
|
276
|
+
acknowledge: Callable[[int], int] | None = None
|
|
277
|
+
if answers:
|
|
278
|
+
|
|
279
|
+
def acknowledge(level: int) -> int:
|
|
280
|
+
return answers.get(level, AUTOVECTOR)
|
|
281
|
+
|
|
282
|
+
tas_write = None
|
|
283
|
+
if manifest.tas_write == "drop":
|
|
284
|
+
faults = self._faults(manifest.bus_error)
|
|
285
|
+
|
|
286
|
+
def tas_write(address: int, value: int) -> None:
|
|
287
|
+
if faults(address):
|
|
288
|
+
raise BusError
|
|
289
|
+
# The write cycle never completes: nothing reaches memory.
|
|
290
|
+
|
|
291
|
+
self.cpu = M68000CPU(
|
|
292
|
+
read_byte,
|
|
293
|
+
read_word,
|
|
294
|
+
write_byte,
|
|
295
|
+
write_word,
|
|
296
|
+
acknowledge=acknowledge,
|
|
297
|
+
tas_write=tas_write,
|
|
298
|
+
)
|
|
299
|
+
if manifest.reset:
|
|
300
|
+
self.cpu.reset()
|
|
301
|
+
else:
|
|
302
|
+
self.cpu.restore_state(manifest.initial)
|
|
303
|
+
|
|
304
|
+
def peek_word(self, address: int) -> int:
|
|
305
|
+
"""Side-effect-free word read for disassembly: memory, never the reads stream."""
|
|
306
|
+
address &= 0xFFFFFF
|
|
307
|
+
return (self.memory[address] << 8) | self.memory[(address + 1) & 0xFFFFFF]
|
|
308
|
+
|
|
309
|
+
@staticmethod
|
|
310
|
+
def _faults(ranges: tuple[AddressRange, ...]) -> Callable[[int], bool]:
|
|
311
|
+
spans = tuple((item.address, item.address + item.length) for item in ranges)
|
|
312
|
+
return lambda address: any(start <= address < end for start, end in spans)
|
|
313
|
+
|
|
314
|
+
def _next_read(self) -> int:
|
|
315
|
+
index = self.reads_consumed * 2
|
|
316
|
+
if index >= len(self._reads):
|
|
317
|
+
return 0xFFFF
|
|
318
|
+
self.reads_consumed += 1
|
|
319
|
+
return (self._reads[index] << 8) | self._reads[index + 1]
|
|
320
|
+
|
|
321
|
+
def _bus(self, manifest: Manifest) -> tuple[Callable, Callable, Callable, Callable]:
|
|
322
|
+
memory = self.memory
|
|
323
|
+
if not manifest.bus_error and manifest.replay is None:
|
|
324
|
+
# The common case pays for nothing it does not use.
|
|
325
|
+
def read_word(address: int) -> int:
|
|
326
|
+
return (memory[address] << 8) | memory[address + 1]
|
|
327
|
+
|
|
328
|
+
def write_word(address: int, value: int) -> None:
|
|
329
|
+
memory[address] = value >> 8
|
|
330
|
+
memory[address + 1] = value & 0xFF
|
|
331
|
+
|
|
332
|
+
return memory.__getitem__, read_word, memory.__setitem__, write_word
|
|
333
|
+
|
|
334
|
+
faults = self._faults(manifest.bus_error)
|
|
335
|
+
devices = self._faults(manifest.replay.devices if manifest.replay else ())
|
|
336
|
+
next_read = self._next_read
|
|
337
|
+
|
|
338
|
+
def read_byte(address: int) -> int:
|
|
339
|
+
if faults(address):
|
|
340
|
+
raise BusError
|
|
341
|
+
if devices(address):
|
|
342
|
+
return next_read() & 0xFF
|
|
343
|
+
return memory[address]
|
|
344
|
+
|
|
345
|
+
def read_word(address: int) -> int:
|
|
346
|
+
if faults(address):
|
|
347
|
+
raise BusError
|
|
348
|
+
if devices(address):
|
|
349
|
+
return next_read()
|
|
350
|
+
return (memory[address] << 8) | memory[address + 1]
|
|
351
|
+
|
|
352
|
+
def write_byte(address: int, value: int) -> None:
|
|
353
|
+
if faults(address):
|
|
354
|
+
raise BusError
|
|
355
|
+
if not devices(address):
|
|
356
|
+
memory[address] = value
|
|
357
|
+
|
|
358
|
+
def write_word(address: int, value: int) -> None:
|
|
359
|
+
if faults(address):
|
|
360
|
+
raise BusError
|
|
361
|
+
if not devices(address):
|
|
362
|
+
memory[address] = value >> 8
|
|
363
|
+
memory[address + 1] = value & 0xFF
|
|
364
|
+
|
|
365
|
+
return read_byte, read_word, write_byte, write_word
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
@dataclass(frozen=True, slots=True)
|
|
369
|
+
class TraceRun:
|
|
370
|
+
"""Why a reference run ended: records, the clocks they spent, and the reason.
|
|
371
|
+
|
|
372
|
+
``clocks`` is the sum of the records' ``cycles``; a ``reset()`` (the
|
|
373
|
+
manifest's or an event's) is not a record and is not in it.
|
|
374
|
+
"""
|
|
375
|
+
|
|
376
|
+
steps: int
|
|
377
|
+
clocks: int
|
|
378
|
+
reason: str
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
class _Stop:
|
|
382
|
+
"""Why :func:`_boundaries` stopped; filled in when the generator ends."""
|
|
383
|
+
|
|
384
|
+
reason = "max_steps"
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
_IDLE = (BoundaryKind.HALTED_IDLE, BoundaryKind.STOPPED_IDLE)
|
|
388
|
+
|
|
389
|
+
|
|
390
|
+
def _boundaries(manifest: Manifest, host: ConformanceHost, stopped: _Stop) -> Iterator[int]:
|
|
391
|
+
"""Yield the index of every boundary the run executes, in order.
|
|
392
|
+
|
|
393
|
+
Before each index the events due at it are applied and the stop checks
|
|
394
|
+
run in the reference order: events, ``at_pc``, ``on_idle``, then the
|
|
395
|
+
step budget. The caller performs the boundary itself.
|
|
396
|
+
"""
|
|
397
|
+
events = list(manifest.events)
|
|
398
|
+
stop = manifest.stop
|
|
399
|
+
cpu = host.cpu
|
|
400
|
+
steps = 0
|
|
401
|
+
while steps < stop.max_steps:
|
|
402
|
+
while events and events[0].at_step == steps:
|
|
403
|
+
_apply_event(cpu, events.pop(0))
|
|
404
|
+
state = cpu.capture_state()
|
|
405
|
+
if state.pc in stop.at_pc:
|
|
406
|
+
stopped.reason = "at_pc"
|
|
407
|
+
return
|
|
408
|
+
if stop.on_idle and not events and next_boundary(state) in _IDLE:
|
|
409
|
+
stopped.reason = "idle"
|
|
410
|
+
return
|
|
411
|
+
yield steps
|
|
412
|
+
steps += 1
|
|
413
|
+
stopped.reason = "max_steps"
|
|
414
|
+
|
|
415
|
+
|
|
416
|
+
def _apply_event(cpu: M68000CPU, event: Event) -> None:
|
|
417
|
+
if event.kind == "ipl":
|
|
418
|
+
cpu.set_ipl(event.level)
|
|
419
|
+
else:
|
|
420
|
+
cpu.reset()
|
|
421
|
+
|
|
422
|
+
|
|
423
|
+
def trace_manifest(
|
|
424
|
+
manifest: Manifest, *, result: list[TraceRun] | None = None
|
|
425
|
+
) -> Iterator[StepRecord]:
|
|
426
|
+
"""Run ``manifest`` on the reference core, yielding one record per boundary.
|
|
427
|
+
|
|
428
|
+
Every record carries its bus accesses (a session with
|
|
429
|
+
``track_accesses=True``). Records are produced lazily so a long run can
|
|
430
|
+
be written or compared without buffering. When the iterator is
|
|
431
|
+
exhausted, a :class:`TraceRun` is appended to ``result`` if one is given.
|
|
432
|
+
"""
|
|
433
|
+
host = ConformanceHost(manifest)
|
|
434
|
+
session = DebugSession(host.cpu, peek_word=host.peek_word, history_limit=0, track_accesses=True)
|
|
435
|
+
stopped = _Stop()
|
|
436
|
+
steps = 0
|
|
437
|
+
for index in _boundaries(manifest, host, stopped):
|
|
438
|
+
yield session.step()
|
|
439
|
+
steps = index + 1
|
|
440
|
+
if result is not None:
|
|
441
|
+
result.append(TraceRun(steps, session.total_cycles, stopped.reason))
|
|
442
|
+
|
|
443
|
+
|
|
444
|
+
def diff_manifest(manifest: Manifest, external: Iterable[StepRecord]) -> TraceDivergence | None:
|
|
445
|
+
"""Run the reference in lockstep against ``external`` and return the first divergence."""
|
|
446
|
+
return first_trace_divergence(trace_manifest(manifest), external)
|
|
447
|
+
|
|
448
|
+
|
|
449
|
+
# --- manifest serialization --------------------------------------------------
|
|
450
|
+
|
|
451
|
+
_STATE_FIELD_NAMES = tuple(item.name for item in fields(CPUState))
|
|
452
|
+
_MANIFEST_KEYS = {
|
|
453
|
+
*("version", "name", "host", "memory", "reset", "initial", "acknowledge"),
|
|
454
|
+
*("bus_error", "tas_write", "replay", "events", "stop"),
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
|
|
458
|
+
def manifest_to_dict(manifest: Manifest) -> dict[str, object]:
|
|
459
|
+
"""Return the versioned JSON-compatible form of a manifest (data inline, as hex)."""
|
|
460
|
+
if type(manifest) is not Manifest:
|
|
461
|
+
raise TypeError("manifest must be a Manifest")
|
|
462
|
+
out: dict[str, object] = {
|
|
463
|
+
"version": MANIFEST_SCHEMA_VERSION,
|
|
464
|
+
"name": manifest.name,
|
|
465
|
+
"host": manifest.host,
|
|
466
|
+
"memory": [
|
|
467
|
+
{"address": segment.address, "data": segment.data.hex()} for segment in manifest.memory
|
|
468
|
+
],
|
|
469
|
+
}
|
|
470
|
+
if manifest.reset:
|
|
471
|
+
out["reset"] = True
|
|
472
|
+
else:
|
|
473
|
+
out["initial"] = {
|
|
474
|
+
name: list(value) if isinstance(value, tuple) else value
|
|
475
|
+
for name in _STATE_FIELD_NAMES
|
|
476
|
+
for value in (getattr(manifest.initial, name),)
|
|
477
|
+
}
|
|
478
|
+
names = {value: name for name, value in ACKNOWLEDGE_NAMES.items()}
|
|
479
|
+
out["acknowledge"] = {
|
|
480
|
+
str(level): names.get(answer, answer) for level, answer in manifest.acknowledge
|
|
481
|
+
}
|
|
482
|
+
out["bus_error"] = [_range_to_dict(item) for item in manifest.bus_error]
|
|
483
|
+
out["tas_write"] = manifest.tas_write
|
|
484
|
+
if manifest.replay is not None:
|
|
485
|
+
out["replay"] = {
|
|
486
|
+
"devices": [_range_to_dict(item) for item in manifest.replay.devices],
|
|
487
|
+
"reads": {"data": manifest.replay.reads.hex()},
|
|
488
|
+
}
|
|
489
|
+
out["events"] = [
|
|
490
|
+
{"at_step": event.at_step, "kind": event.kind}
|
|
491
|
+
| ({"level": event.level} if event.kind == "ipl" else {})
|
|
492
|
+
for event in manifest.events
|
|
493
|
+
]
|
|
494
|
+
out["stop"] = {
|
|
495
|
+
"max_steps": manifest.stop.max_steps,
|
|
496
|
+
"on_idle": manifest.stop.on_idle,
|
|
497
|
+
"at_pc": list(manifest.stop.at_pc),
|
|
498
|
+
}
|
|
499
|
+
return out
|
|
500
|
+
|
|
501
|
+
|
|
502
|
+
def manifest_from_dict(value: object, *, base_dir: Path | None = None) -> Manifest:
|
|
503
|
+
"""Build a validated manifest from its JSON form.
|
|
504
|
+
|
|
505
|
+
``initial`` may list any subset of CPUState fields; the rest take
|
|
506
|
+
CPUState's defaults. When it gives neither ``ir`` nor ``irc``, they are
|
|
507
|
+
read from the loaded memory at ``pc`` and ``pc + 2`` (not through the
|
|
508
|
+
bus: no BERR, no device read, no clocks); giving one without the other
|
|
509
|
+
is an error. ``reset: true`` and ``initial`` exclude each other. A
|
|
510
|
+
memory segment, and the replay reads, carry either ``data`` (hex) or
|
|
511
|
+
``file`` (a path relative to ``base_dir``, with optional ``offset`` and
|
|
512
|
+
``length``).
|
|
513
|
+
"""
|
|
514
|
+
root = _object(value, "manifest")
|
|
515
|
+
_allowed(
|
|
516
|
+
root,
|
|
517
|
+
"manifest",
|
|
518
|
+
_MANIFEST_KEYS,
|
|
519
|
+
required={"version", "name", "memory", "stop"},
|
|
520
|
+
)
|
|
521
|
+
if root["version"] != MANIFEST_SCHEMA_VERSION:
|
|
522
|
+
raise ValueError(f"unsupported manifest version: {root['version']!r}")
|
|
523
|
+
memory = tuple(
|
|
524
|
+
_segment(_object(item, f"memory[{index}]"), index, base_dir)
|
|
525
|
+
for index, item in enumerate(_list(root["memory"], "memory"))
|
|
526
|
+
)
|
|
527
|
+
reset = root.get("reset", False)
|
|
528
|
+
if reset is True and "initial" in root:
|
|
529
|
+
raise ValueError("manifest reset and initial exclude each other")
|
|
530
|
+
initial = _initial(_object(root.get("initial", {}), "initial"), memory)
|
|
531
|
+
acknowledge = _acknowledge(_object(root.get("acknowledge", {}), "acknowledge"))
|
|
532
|
+
bus_error = tuple(
|
|
533
|
+
_range(_object(item, f"bus_error[{index}]"), f"bus_error[{index}]")
|
|
534
|
+
for index, item in enumerate(_list(root.get("bus_error", []), "bus_error"))
|
|
535
|
+
)
|
|
536
|
+
replay = None
|
|
537
|
+
if "replay" in root:
|
|
538
|
+
replay = _replay(_object(root["replay"], "replay"), base_dir)
|
|
539
|
+
stop_dict = _object(root["stop"], "stop")
|
|
540
|
+
_allowed(stop_dict, "stop", {"max_steps", "on_idle", "at_pc"}, required={"max_steps"})
|
|
541
|
+
at_pc = _list(stop_dict.get("at_pc", []), "stop.at_pc")
|
|
542
|
+
events = tuple(
|
|
543
|
+
_event(_object(item, f"events[{index}]"), index)
|
|
544
|
+
for index, item in enumerate(_list(root.get("events", []), "events"))
|
|
545
|
+
)
|
|
546
|
+
try:
|
|
547
|
+
return Manifest(
|
|
548
|
+
name=root["name"],
|
|
549
|
+
memory=memory,
|
|
550
|
+
initial=initial,
|
|
551
|
+
stop=StopRule(
|
|
552
|
+
max_steps=stop_dict["max_steps"],
|
|
553
|
+
on_idle=stop_dict.get("on_idle", True),
|
|
554
|
+
at_pc=tuple(at_pc),
|
|
555
|
+
),
|
|
556
|
+
host=root.get("host", "flat"),
|
|
557
|
+
reset=reset,
|
|
558
|
+
acknowledge=acknowledge,
|
|
559
|
+
bus_error=bus_error,
|
|
560
|
+
tas_write=root.get("tas_write", "write"),
|
|
561
|
+
replay=replay,
|
|
562
|
+
events=events,
|
|
563
|
+
)
|
|
564
|
+
except (TypeError, ValueError) as exc:
|
|
565
|
+
raise ValueError(f"invalid manifest: {exc}") from exc
|
|
566
|
+
|
|
567
|
+
|
|
568
|
+
def load_manifest(path: str | Path) -> Manifest:
|
|
569
|
+
"""Read and validate a manifest file; ``file`` entries resolve beside it."""
|
|
570
|
+
path = Path(path)
|
|
571
|
+
with path.open(encoding="utf-8") as handle:
|
|
572
|
+
try:
|
|
573
|
+
value = json.load(handle)
|
|
574
|
+
except json.JSONDecodeError as exc:
|
|
575
|
+
raise ValueError(f"{path}: not valid JSON: {exc}") from exc
|
|
576
|
+
return manifest_from_dict(value, base_dir=path.parent)
|
|
577
|
+
|
|
578
|
+
|
|
579
|
+
def _initial(item: dict[str, object], memory: tuple[MemorySegment, ...]) -> CPUState:
|
|
580
|
+
_allowed(item, "initial", set(_STATE_FIELD_NAMES), required=set())
|
|
581
|
+
if ("ir" in item) != ("irc" in item):
|
|
582
|
+
raise ValueError("initial must give both ir and irc, or neither")
|
|
583
|
+
values = {key: tuple(value) if type(value) is list else value for key, value in item.items()}
|
|
584
|
+
try:
|
|
585
|
+
state = CPUState(**values)
|
|
586
|
+
except (TypeError, ValueError) as exc:
|
|
587
|
+
raise ValueError(f"invalid initial state: {exc}") from exc
|
|
588
|
+
if "ir" in item:
|
|
589
|
+
return state
|
|
590
|
+
|
|
591
|
+
def byte(address: int) -> int:
|
|
592
|
+
value = 0 # a later segment overwrites an earlier one, as loading does
|
|
593
|
+
for segment in memory:
|
|
594
|
+
if segment.address <= address < segment.address + len(segment.data):
|
|
595
|
+
value = segment.data[address - segment.address]
|
|
596
|
+
return value
|
|
597
|
+
|
|
598
|
+
def word(address: int) -> int:
|
|
599
|
+
address &= 0xFFFFFF
|
|
600
|
+
return (byte(address) << 8) | byte((address + 1) & 0xFFFFFF)
|
|
601
|
+
|
|
602
|
+
return replace(state, ir=word(state.pc), irc=word(state.pc + 2))
|
|
603
|
+
|
|
604
|
+
|
|
605
|
+
def _acknowledge(item: dict[str, object]) -> tuple[tuple[int, int], ...]:
|
|
606
|
+
pairs = []
|
|
607
|
+
for key, answer in item.items():
|
|
608
|
+
if key not in tuple("1234567"):
|
|
609
|
+
raise ValueError(f"acknowledge keys are the levels '1'..'7', got {key!r}")
|
|
610
|
+
if type(answer) is str:
|
|
611
|
+
if answer not in ACKNOWLEDGE_NAMES:
|
|
612
|
+
raise ValueError(f"acknowledge answers are 0-255, {sorted(ACKNOWLEDGE_NAMES)}")
|
|
613
|
+
answer = ACKNOWLEDGE_NAMES[answer]
|
|
614
|
+
elif type(answer) is not int or not 0 <= answer <= 0xFF:
|
|
615
|
+
raise ValueError(f"acknowledge answers are 0-255, {sorted(ACKNOWLEDGE_NAMES)}")
|
|
616
|
+
pairs.append((int(key), answer))
|
|
617
|
+
return tuple(sorted(pairs))
|
|
618
|
+
|
|
619
|
+
|
|
620
|
+
def _range(item: dict[str, object], name: str) -> AddressRange:
|
|
621
|
+
_allowed(item, name, {"address", "length"}, required={"address", "length"})
|
|
622
|
+
try:
|
|
623
|
+
return AddressRange(item["address"], item["length"])
|
|
624
|
+
except (TypeError, ValueError) as exc:
|
|
625
|
+
raise ValueError(f"{name}: {exc}") from exc
|
|
626
|
+
|
|
627
|
+
|
|
628
|
+
def _range_to_dict(item: AddressRange) -> dict[str, int]:
|
|
629
|
+
return {"address": item.address, "length": item.length}
|
|
630
|
+
|
|
631
|
+
|
|
632
|
+
def _replay(item: dict[str, object], base_dir: Path | None) -> Replay:
|
|
633
|
+
_allowed(item, "replay", {"devices", "reads"}, required={"devices", "reads"})
|
|
634
|
+
devices = tuple(
|
|
635
|
+
_range(_object(window, f"replay.devices[{index}]"), f"replay.devices[{index}]")
|
|
636
|
+
for index, window in enumerate(_list(item["devices"], "replay.devices"))
|
|
637
|
+
)
|
|
638
|
+
reads_dict = _object(item["reads"], "replay.reads")
|
|
639
|
+
reads = _bytes(reads_dict, "replay.reads", base_dir, allow_empty=True)
|
|
640
|
+
try:
|
|
641
|
+
return Replay(devices, reads)
|
|
642
|
+
except (TypeError, ValueError) as exc:
|
|
643
|
+
raise ValueError(f"replay: {exc}") from exc
|
|
644
|
+
|
|
645
|
+
|
|
646
|
+
def _segment(item: dict[str, object], index: int, base_dir: Path | None) -> MemorySegment:
|
|
647
|
+
name = f"memory[{index}]"
|
|
648
|
+
if "address" not in item:
|
|
649
|
+
raise ValueError(f"{name} fields do not match schema (missing=['address'])")
|
|
650
|
+
rest = {key: value for key, value in item.items() if key != "address"}
|
|
651
|
+
data = _bytes(rest, name, base_dir, allow_empty=False)
|
|
652
|
+
try:
|
|
653
|
+
return MemorySegment(item["address"], data)
|
|
654
|
+
except (TypeError, ValueError) as exc:
|
|
655
|
+
raise ValueError(f"{name}: {exc}") from exc
|
|
656
|
+
|
|
657
|
+
|
|
658
|
+
def _bytes(
|
|
659
|
+
item: dict[str, object], name: str, base_dir: Path | None, *, allow_empty: bool
|
|
660
|
+
) -> bytes:
|
|
661
|
+
"""The bytes of a ``data`` (hex) or ``file`` (+ ``offset``, ``length``) entry."""
|
|
662
|
+
_allowed(item, name, {"data", "file", "offset", "length"}, required=set())
|
|
663
|
+
if ("data" in item) == ("file" in item):
|
|
664
|
+
raise ValueError(f"{name} must have exactly one of 'data' or 'file'")
|
|
665
|
+
if "data" in item:
|
|
666
|
+
if type(item["data"]) is not str:
|
|
667
|
+
raise ValueError(f"{name}.data must be a hexadecimal string")
|
|
668
|
+
try:
|
|
669
|
+
data = bytes.fromhex(item["data"])
|
|
670
|
+
except ValueError as exc:
|
|
671
|
+
raise ValueError(f"{name}.data must be a hexadecimal string") from exc
|
|
672
|
+
if "offset" in item or "length" in item:
|
|
673
|
+
raise ValueError(f"{name}: offset and length go with file")
|
|
674
|
+
else:
|
|
675
|
+
if type(item["file"]) is not str:
|
|
676
|
+
raise ValueError(f"{name}.file must be a path string")
|
|
677
|
+
file_path = Path(item["file"])
|
|
678
|
+
if not file_path.is_absolute():
|
|
679
|
+
file_path = (base_dir or Path.cwd()) / file_path
|
|
680
|
+
data = file_path.read_bytes()
|
|
681
|
+
offset = item.get("offset", 0)
|
|
682
|
+
length = item.get("length", len(data) - offset)
|
|
683
|
+
if type(offset) is not int or type(length) is not int or offset < 0 or length < 0:
|
|
684
|
+
raise ValueError(f"{name}.offset/length must be non-negative integers")
|
|
685
|
+
data = data[offset : offset + length]
|
|
686
|
+
if not data and not allow_empty:
|
|
687
|
+
raise ValueError(f"{name}: segment data must be non-empty bytes")
|
|
688
|
+
return data
|
|
689
|
+
|
|
690
|
+
|
|
691
|
+
def _event(item: dict[str, object], index: int) -> Event:
|
|
692
|
+
name = f"events[{index}]"
|
|
693
|
+
_allowed(item, name, {"at_step", "kind", "level"}, required={"at_step", "kind"})
|
|
694
|
+
try:
|
|
695
|
+
return Event(item["at_step"], item["kind"], item.get("level"))
|
|
696
|
+
except (TypeError, ValueError) as exc:
|
|
697
|
+
raise ValueError(f"{name}: {exc}") from exc
|
|
698
|
+
|
|
699
|
+
|
|
700
|
+
def _object(value: object, name: str) -> dict[str, object]:
|
|
701
|
+
if type(value) is not dict or not all(type(key) is str for key in value):
|
|
702
|
+
raise ValueError(f"{name} must be an object with string keys")
|
|
703
|
+
return value
|
|
704
|
+
|
|
705
|
+
|
|
706
|
+
def _list(value: object, name: str) -> list[object]:
|
|
707
|
+
if type(value) is not list:
|
|
708
|
+
raise ValueError(f"{name} must be a list")
|
|
709
|
+
return value
|
|
710
|
+
|
|
711
|
+
|
|
712
|
+
def _allowed(value: dict[str, object], name: str, keys: set[str], *, required: set[str]) -> None:
|
|
713
|
+
unknown = sorted(set(value) - keys)
|
|
714
|
+
missing = sorted(required - set(value))
|
|
715
|
+
if unknown or missing:
|
|
716
|
+
details = []
|
|
717
|
+
if missing:
|
|
718
|
+
details.append(f"missing={missing}")
|
|
719
|
+
if unknown:
|
|
720
|
+
details.append(f"unknown={unknown}")
|
|
721
|
+
raise ValueError(f"{name} fields do not match schema ({', '.join(details)})")
|
|
722
|
+
|
|
723
|
+
|
|
724
|
+
# --- command line --------------------------------------------------------------
|
|
725
|
+
|
|
726
|
+
|
|
727
|
+
def main(argv: list[str] | None = None, *, stdout: TextIO | None = None) -> int:
|
|
728
|
+
"""``trace`` writes the reference trace; ``diff`` reports the first divergence.
|
|
729
|
+
|
|
730
|
+
Exit status: 0 on success or equal traces, 1 on divergence, 2 on bad input.
|
|
731
|
+
"""
|
|
732
|
+
out = stdout or sys.stdout
|
|
733
|
+
parser = argparse.ArgumentParser(
|
|
734
|
+
prog="python -m m68000_python.conformance",
|
|
735
|
+
description="Run a conformance manifest on the reference core or diff a trace against it.",
|
|
736
|
+
)
|
|
737
|
+
commands = parser.add_subparsers(dest="command", required=True)
|
|
738
|
+
trace_cmd = commands.add_parser("trace", help="write the reference trace for a manifest")
|
|
739
|
+
trace_cmd.add_argument("manifest")
|
|
740
|
+
trace_cmd.add_argument("--out", help="JSON Lines output path (default: stdout)")
|
|
741
|
+
diff_cmd = commands.add_parser("diff", help="compare an external trace against the reference")
|
|
742
|
+
diff_cmd.add_argument("manifest")
|
|
743
|
+
diff_cmd.add_argument("trace", help="JSON Lines trace path, or '-' for stdin")
|
|
744
|
+
args = parser.parse_args(argv)
|
|
745
|
+
|
|
746
|
+
try:
|
|
747
|
+
manifest = load_manifest(args.manifest)
|
|
748
|
+
except (OSError, ValueError) as exc:
|
|
749
|
+
print(f"error: {exc}", file=out)
|
|
750
|
+
return 2
|
|
751
|
+
|
|
752
|
+
if args.command == "trace":
|
|
753
|
+
result: list[TraceRun] = []
|
|
754
|
+
records = trace_manifest(manifest, result=result)
|
|
755
|
+
if args.out:
|
|
756
|
+
with open(args.out, "w", encoding="utf-8") as handle:
|
|
757
|
+
count = write_trace(records, handle)
|
|
758
|
+
else:
|
|
759
|
+
count = write_trace(records, out)
|
|
760
|
+
run = result[0]
|
|
761
|
+
print(
|
|
762
|
+
f"{manifest.name}: {count} records, {run.clocks} clocks, stopped on {run.reason}",
|
|
763
|
+
file=sys.stderr if not args.out else out,
|
|
764
|
+
)
|
|
765
|
+
return 0
|
|
766
|
+
|
|
767
|
+
try:
|
|
768
|
+
if args.trace == "-":
|
|
769
|
+
divergence = diff_manifest(manifest, read_trace(sys.stdin))
|
|
770
|
+
else:
|
|
771
|
+
with open(args.trace, encoding="utf-8") as handle:
|
|
772
|
+
divergence = diff_manifest(manifest, read_trace(handle))
|
|
773
|
+
except (OSError, ValueError) as exc:
|
|
774
|
+
print(f"error: {exc}", file=out)
|
|
775
|
+
return 2
|
|
776
|
+
if divergence is None:
|
|
777
|
+
print(f"{manifest.name}: traces are identical", file=out)
|
|
778
|
+
return 0
|
|
779
|
+
record = divergence.left or divergence.right
|
|
780
|
+
where = "(end of trace)"
|
|
781
|
+
if record is not None:
|
|
782
|
+
if record.instruction is not None:
|
|
783
|
+
where = f"{record.instruction.address:06X}: {record.instruction.text}"
|
|
784
|
+
else:
|
|
785
|
+
where = record.kind.value
|
|
786
|
+
print(f"{manifest.name}: divergence at position {divergence.position}, {where}", file=out)
|
|
787
|
+
for difference in divergence.differences:
|
|
788
|
+
print(
|
|
789
|
+
f" {difference.path}: reference={difference.left!r} external={difference.right!r}",
|
|
790
|
+
file=out,
|
|
791
|
+
)
|
|
792
|
+
return 1
|
|
793
|
+
|
|
794
|
+
|
|
795
|
+
if __name__ == "__main__":
|
|
796
|
+
sys.exit(main())
|