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
m68000_python/trace.py
ADDED
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
"""Deterministic boundary traces: write, read, and compare them incrementally.
|
|
2
|
+
|
|
3
|
+
A trace is a JSON Lines file of :class:`~m68000_python.debug.StepRecord`
|
|
4
|
+
values, one processor boundary per line (docs/trace-schema.md). Two cores
|
|
5
|
+
that produce equal traces for the same program and host behave identically
|
|
6
|
+
as far as software can tell; ``first_trace_divergence`` finds where they do
|
|
7
|
+
not without reading either trace to the end.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
from collections.abc import Iterable, Iterator
|
|
12
|
+
from dataclasses import dataclass, fields
|
|
13
|
+
from itertools import zip_longest
|
|
14
|
+
from typing import TextIO
|
|
15
|
+
|
|
16
|
+
from m68000_python.debug import BoundaryKind, DebugSession, StepRecord
|
|
17
|
+
from m68000_python.disasm import Instruction, disassemble_bytes
|
|
18
|
+
from m68000_python.state import CPUState
|
|
19
|
+
|
|
20
|
+
TRACE_SCHEMA_VERSION = 1
|
|
21
|
+
TraceValue = int | bool | str | tuple | None
|
|
22
|
+
_STATE_KEYS = tuple(field.name for field in fields(CPUState))
|
|
23
|
+
_RECORD_KEYS = {"version", "sequence", "kind", "cycles", "instruction", "before", "after"}
|
|
24
|
+
_MISSING = object()
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass(frozen=True, slots=True)
|
|
28
|
+
class TraceDifference:
|
|
29
|
+
"""One unequal observation at an aligned trace position."""
|
|
30
|
+
|
|
31
|
+
path: str
|
|
32
|
+
left: TraceValue
|
|
33
|
+
right: TraceValue
|
|
34
|
+
|
|
35
|
+
def __post_init__(self) -> None:
|
|
36
|
+
if type(self.path) is not str or not self.path:
|
|
37
|
+
raise ValueError("path must be a non-empty string")
|
|
38
|
+
if self.left == self.right:
|
|
39
|
+
raise ValueError("a TraceDifference records unequal values")
|
|
40
|
+
|
|
41
|
+
def as_dict(self) -> dict[str, TraceValue]:
|
|
42
|
+
return {"path": self.path, "left": self.left, "right": self.right}
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@dataclass(frozen=True, slots=True)
|
|
46
|
+
class TraceDivergence:
|
|
47
|
+
"""All differences at one aligned position."""
|
|
48
|
+
|
|
49
|
+
position: int
|
|
50
|
+
left: StepRecord | None
|
|
51
|
+
right: StepRecord | None
|
|
52
|
+
differences: tuple[TraceDifference, ...]
|
|
53
|
+
|
|
54
|
+
def as_dict(self) -> dict[str, object]:
|
|
55
|
+
return {
|
|
56
|
+
"position": self.position,
|
|
57
|
+
"left_sequence": None if self.left is None else self.left.sequence,
|
|
58
|
+
"right_sequence": None if self.right is None else self.right.sequence,
|
|
59
|
+
"differences": [difference.as_dict() for difference in self.differences],
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
# -- records <-> JSON ---------------------------------------------------------
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def step_record_to_dict(record: StepRecord) -> dict[str, object]:
|
|
67
|
+
"""The JSON-compatible form of one record (docs/trace-schema.md)."""
|
|
68
|
+
instruction = None
|
|
69
|
+
if record.instruction is not None:
|
|
70
|
+
instruction = {
|
|
71
|
+
"address": record.instruction.address,
|
|
72
|
+
"data": record.instruction.data.hex(),
|
|
73
|
+
"mnemonic": record.instruction.mnemonic,
|
|
74
|
+
"operands": list(record.instruction.operands),
|
|
75
|
+
}
|
|
76
|
+
out: dict[str, object] = {
|
|
77
|
+
"version": TRACE_SCHEMA_VERSION,
|
|
78
|
+
"sequence": record.sequence,
|
|
79
|
+
"kind": record.kind.value,
|
|
80
|
+
"cycles": record.cycles,
|
|
81
|
+
"instruction": instruction,
|
|
82
|
+
"before": _state_to_dict(record.before),
|
|
83
|
+
"after": _state_to_dict(record.after),
|
|
84
|
+
}
|
|
85
|
+
if record.accesses is not None:
|
|
86
|
+
out["accesses"] = [list(access) for access in record.accesses]
|
|
87
|
+
return out
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def step_record_from_dict(data: dict) -> StepRecord:
|
|
91
|
+
"""Validate one decoded JSON record and rebuild it."""
|
|
92
|
+
if type(data) is not dict:
|
|
93
|
+
raise ValueError("a trace record must be a JSON object")
|
|
94
|
+
keys = set(data)
|
|
95
|
+
if not _RECORD_KEYS <= keys or keys - _RECORD_KEYS - {"accesses"}:
|
|
96
|
+
raise ValueError(f"trace record keys must be {sorted(_RECORD_KEYS)} (+ accesses)")
|
|
97
|
+
if data["version"] != TRACE_SCHEMA_VERSION:
|
|
98
|
+
raise ValueError(f"unsupported trace version {data['version']!r}")
|
|
99
|
+
for key in ("sequence", "cycles"):
|
|
100
|
+
if type(data[key]) is not int:
|
|
101
|
+
raise ValueError(f"{key} must be an integer")
|
|
102
|
+
if type(data["kind"]) is not str:
|
|
103
|
+
raise ValueError("kind must be a string")
|
|
104
|
+
instruction = None
|
|
105
|
+
if data["instruction"] is not None:
|
|
106
|
+
instruction = _instruction_from_dict(data["instruction"])
|
|
107
|
+
accesses = None
|
|
108
|
+
if "accesses" in data:
|
|
109
|
+
if type(data["accesses"]) is not list:
|
|
110
|
+
raise ValueError("accesses must be a list")
|
|
111
|
+
accesses = tuple(
|
|
112
|
+
tuple(access) if type(access) is list else access for access in data["accesses"]
|
|
113
|
+
)
|
|
114
|
+
return StepRecord(
|
|
115
|
+
sequence=data["sequence"],
|
|
116
|
+
kind=BoundaryKind(data["kind"]),
|
|
117
|
+
before=_state_from_dict(data["before"]),
|
|
118
|
+
after=_state_from_dict(data["after"]),
|
|
119
|
+
cycles=data["cycles"],
|
|
120
|
+
instruction=instruction,
|
|
121
|
+
accesses=accesses,
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _instruction_from_dict(spec: object) -> Instruction:
|
|
126
|
+
"""Rebuild an instruction from its bytes; text, when present, must be complete and agree.
|
|
127
|
+
|
|
128
|
+
A producer in another language writes only ``address`` and ``data``; this
|
|
129
|
+
package's disassembler supplies the text, so two traces of the same bytes
|
|
130
|
+
compare equal whoever wrote them.
|
|
131
|
+
"""
|
|
132
|
+
if type(spec) is not dict:
|
|
133
|
+
raise ValueError("instruction must be an object or null")
|
|
134
|
+
keys = set(spec)
|
|
135
|
+
if not {"address", "data"} <= keys or keys - {"address", "data", "mnemonic", "operands"}:
|
|
136
|
+
raise ValueError("instruction fields are address, data and optionally mnemonic + operands")
|
|
137
|
+
if ("mnemonic" in keys) != ("operands" in keys):
|
|
138
|
+
raise ValueError("instruction fields mnemonic and operands come together")
|
|
139
|
+
if type(spec["address"]) is not int or type(spec["data"]) is not str:
|
|
140
|
+
raise ValueError("instruction address must be an integer and data a hex string")
|
|
141
|
+
try:
|
|
142
|
+
data = bytes.fromhex(spec["data"])
|
|
143
|
+
except ValueError:
|
|
144
|
+
raise ValueError("instruction data must be hexadecimal") from None
|
|
145
|
+
try:
|
|
146
|
+
instruction = disassemble_bytes(data, spec["address"])
|
|
147
|
+
except ValueError as exc:
|
|
148
|
+
raise ValueError(f"instruction data must hold exactly one instruction: {exc}") from None
|
|
149
|
+
if "mnemonic" in keys and (
|
|
150
|
+
spec["mnemonic"] != instruction.mnemonic or tuple(spec["operands"]) != instruction.operands
|
|
151
|
+
):
|
|
152
|
+
raise ValueError("instruction text does not match its bytes")
|
|
153
|
+
return instruction
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def write_trace(records: Iterable[StepRecord], stream: TextIO) -> int:
|
|
157
|
+
"""Write records as JSON Lines with sorted keys; return how many."""
|
|
158
|
+
count = 0
|
|
159
|
+
for record in records:
|
|
160
|
+
stream.write(json.dumps(step_record_to_dict(record), sort_keys=True, separators=(",", ":")))
|
|
161
|
+
stream.write("\n")
|
|
162
|
+
count += 1
|
|
163
|
+
return count
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def read_trace(stream: TextIO) -> Iterator[StepRecord]:
|
|
167
|
+
"""Read JSON Lines records lazily; blank lines are ignored."""
|
|
168
|
+
for number, line in enumerate(stream, 1):
|
|
169
|
+
if not line.strip():
|
|
170
|
+
continue
|
|
171
|
+
try:
|
|
172
|
+
yield step_record_from_dict(json.loads(line))
|
|
173
|
+
except (ValueError, KeyError, TypeError) as exc:
|
|
174
|
+
raise ValueError(f"trace line {number}: {exc}") from exc
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def iter_session_steps(session: DebugSession, *, max_steps: int) -> Iterator[StepRecord]:
|
|
178
|
+
"""Yield ``max_steps`` live boundaries from a session without buffering them."""
|
|
179
|
+
if type(max_steps) is not int or max_steps <= 0:
|
|
180
|
+
raise ValueError("max_steps must be a positive integer")
|
|
181
|
+
for _ in range(max_steps):
|
|
182
|
+
yield session.step()
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
# -- comparison ---------------------------------------------------------------
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def compare_step_records(left: StepRecord, right: StepRecord) -> tuple[TraceDifference, ...]:
|
|
189
|
+
"""Every differing field of two aligned records (sequence numbers are ignored).
|
|
190
|
+
|
|
191
|
+
Bus accesses are compared only when both records carry them.
|
|
192
|
+
"""
|
|
193
|
+
_require_record(left, "compared records")
|
|
194
|
+
_require_record(right, "compared records")
|
|
195
|
+
out: list[TraceDifference] = []
|
|
196
|
+
_append(out, "kind", left.kind.value, right.kind.value)
|
|
197
|
+
_append(out, "cycles", left.cycles, right.cycles)
|
|
198
|
+
li, ri = left.instruction, right.instruction
|
|
199
|
+
if (li is None) != (ri is None):
|
|
200
|
+
_append(
|
|
201
|
+
out, "instruction", None if li is None else li.text, None if ri is None else ri.text
|
|
202
|
+
)
|
|
203
|
+
elif li is not None and ri is not None:
|
|
204
|
+
_append(out, "instruction.address", li.address, ri.address)
|
|
205
|
+
_append(out, "instruction.data", li.data.hex(), ri.data.hex())
|
|
206
|
+
for side in ("before", "after"):
|
|
207
|
+
a, b = getattr(left, side), getattr(right, side)
|
|
208
|
+
for key in _STATE_KEYS:
|
|
209
|
+
_append(out, f"{side}.{key}", getattr(a, key), getattr(b, key))
|
|
210
|
+
if left.accesses is not None and right.accesses is not None:
|
|
211
|
+
_append(out, "accesses", tuple(left.accesses), tuple(right.accesses))
|
|
212
|
+
return tuple(out)
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def iter_trace_divergences(
|
|
216
|
+
left: Iterable[StepRecord], right: Iterable[StepRecord]
|
|
217
|
+
) -> Iterator[TraceDivergence]:
|
|
218
|
+
"""Yield every unequal aligned position, lazily, until both traces end."""
|
|
219
|
+
for position, (a, b) in enumerate(zip_longest(left, right, fillvalue=_MISSING)):
|
|
220
|
+
if a is _MISSING:
|
|
221
|
+
_require_record(b, "traces")
|
|
222
|
+
yield TraceDivergence(position, None, b, (TraceDifference("record", None, "present"),))
|
|
223
|
+
elif b is _MISSING:
|
|
224
|
+
_require_record(a, "traces")
|
|
225
|
+
yield TraceDivergence(position, a, None, (TraceDifference("record", "present", None),))
|
|
226
|
+
else:
|
|
227
|
+
_require_record(a, "traces")
|
|
228
|
+
_require_record(b, "traces")
|
|
229
|
+
differences = compare_step_records(a, b)
|
|
230
|
+
if differences:
|
|
231
|
+
yield TraceDivergence(position, a, b, differences)
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def first_trace_divergence(
|
|
235
|
+
left: Iterable[StepRecord], right: Iterable[StepRecord]
|
|
236
|
+
) -> TraceDivergence | None:
|
|
237
|
+
"""The first unequal aligned position, or ``None`` when the traces are equal."""
|
|
238
|
+
return next(iter_trace_divergences(left, right), None)
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def first_session_divergence(
|
|
242
|
+
left: DebugSession, right: DebugSession, *, max_steps: int
|
|
243
|
+
) -> TraceDivergence | None:
|
|
244
|
+
"""Advance two live sessions in lockstep and stop at the first differing boundary.
|
|
245
|
+
|
|
246
|
+
Each session keeps its own bounded history, which is the context before
|
|
247
|
+
the divergence; nothing else is buffered. ``max_steps`` is mandatory.
|
|
248
|
+
"""
|
|
249
|
+
return first_trace_divergence(
|
|
250
|
+
iter_session_steps(left, max_steps=max_steps),
|
|
251
|
+
iter_session_steps(right, max_steps=max_steps),
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def _append(out: list, path: str, left: TraceValue, right: TraceValue) -> None:
|
|
256
|
+
if left != right:
|
|
257
|
+
out.append(TraceDifference(path, left, right))
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def _require_record(record: object, what: str) -> None:
|
|
261
|
+
if type(record) is not StepRecord:
|
|
262
|
+
raise TypeError(f"{what} must be StepRecord values")
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def _state_to_dict(state: CPUState) -> dict[str, object]:
|
|
266
|
+
out: dict[str, object] = {}
|
|
267
|
+
for key in _STATE_KEYS:
|
|
268
|
+
value = getattr(state, key)
|
|
269
|
+
out[key] = list(value) if isinstance(value, tuple) else value
|
|
270
|
+
return out
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def _state_from_dict(data: dict) -> CPUState:
|
|
274
|
+
if type(data) is not dict or set(data) != set(_STATE_KEYS):
|
|
275
|
+
raise ValueError(f"a state object must have exactly the keys {sorted(_STATE_KEYS)}")
|
|
276
|
+
values = {
|
|
277
|
+
key: tuple(value) if isinstance(value, list) else value for key, value in data.items()
|
|
278
|
+
}
|
|
279
|
+
return CPUState(**values)
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
__all__ = [
|
|
283
|
+
"TRACE_SCHEMA_VERSION",
|
|
284
|
+
"TraceDifference",
|
|
285
|
+
"TraceDivergence",
|
|
286
|
+
"TraceValue",
|
|
287
|
+
"compare_step_records",
|
|
288
|
+
"first_session_divergence",
|
|
289
|
+
"first_trace_divergence",
|
|
290
|
+
"iter_session_steps",
|
|
291
|
+
"iter_trace_divergences",
|
|
292
|
+
"read_trace",
|
|
293
|
+
"step_record_from_dict",
|
|
294
|
+
"step_record_to_dict",
|
|
295
|
+
"write_trace",
|
|
296
|
+
]
|
|
@@ -0,0 +1,379 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: m68000-python
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Readable, pure-Python Motorola MC68000 instruction-core reference implementation
|
|
5
|
+
Project-URL: Homepage, https://github.com/alewman/m68000-python
|
|
6
|
+
Project-URL: Repository, https://github.com/alewman/m68000-python
|
|
7
|
+
Project-URL: Issues, https://github.com/alewman/m68000-python/issues
|
|
8
|
+
Project-URL: Changelog, https://github.com/alewman/m68000-python/blob/main/CHANGELOG.md
|
|
9
|
+
Author: alewman
|
|
10
|
+
License: MIT License
|
|
11
|
+
|
|
12
|
+
Copyright (c) 2026 alewman
|
|
13
|
+
|
|
14
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
15
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
16
|
+
in the Software without restriction, including without limitation the rights
|
|
17
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
18
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
19
|
+
furnished to do so, subject to the following conditions:
|
|
20
|
+
|
|
21
|
+
The above copyright notice and this permission notice shall be included in all
|
|
22
|
+
copies or substantial portions of the Software.
|
|
23
|
+
|
|
24
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
25
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
26
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
27
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
28
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
29
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
30
|
+
SOFTWARE.
|
|
31
|
+
License-File: LICENSE
|
|
32
|
+
Classifier: Development Status :: 3 - Alpha
|
|
33
|
+
Classifier: Intended Audience :: Developers
|
|
34
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
35
|
+
Classifier: Operating System :: OS Independent
|
|
36
|
+
Classifier: Programming Language :: Python :: 3
|
|
37
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
38
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
39
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
40
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
41
|
+
Classifier: Programming Language :: Python :: Implementation :: CPython
|
|
42
|
+
Classifier: Programming Language :: Python :: Implementation :: PyPy
|
|
43
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
44
|
+
Classifier: Topic :: System :: Emulators
|
|
45
|
+
Requires-Python: >=3.11
|
|
46
|
+
Provides-Extra: dev
|
|
47
|
+
Requires-Dist: pytest>=8; extra == 'dev'
|
|
48
|
+
Requires-Dist: ruff==0.16.8; extra == 'dev'
|
|
49
|
+
Description-Content-Type: text/markdown
|
|
50
|
+
|
|
51
|
+
# m68000-python
|
|
52
|
+
|
|
53
|
+
[](https://github.com/alewman/m68000-python/actions/workflows/ci.yml)
|
|
54
|
+
[](https://github.com/alewman/m68000-python/actions/workflows/oracles.yml)
|
|
55
|
+
|
|
56
|
+
A readable, pure-Python Motorola MC68000 **instruction-core reference
|
|
57
|
+
implementation**.
|
|
58
|
+
|
|
59
|
+
`m68000-python` is a processor core built to be read, learned from, embedded
|
|
60
|
+
in real machines, and inspected by humans and AI tools. It implements the
|
|
61
|
+
68000 instruction set, the prefetch queue, and the exception model at
|
|
62
|
+
instruction boundaries, leaving memory maps, devices and machine scheduling
|
|
63
|
+
to the host, in the same shape as [z80-python](https://github.com/alewman/z80-python),
|
|
64
|
+
the family's reference core. Where z80-python could be checked exhaustively
|
|
65
|
+
against hardware-captured values, no such oracle exists for the 68000, so
|
|
66
|
+
this core carries a **claim boundary** instead: every behaviour is labelled
|
|
67
|
+
by how many independent lines of evidence support it and how close to
|
|
68
|
+
silicon the best of them is ([docs/claims.md](docs/claims.md)).
|
|
69
|
+
|
|
70
|
+
The project is deliberately:
|
|
71
|
+
|
|
72
|
+
- **readable** — every instruction is an ordinary Python method; a table
|
|
73
|
+
only routes each of the 65,536 first words to its method, which cites the
|
|
74
|
+
manual page its rule comes from and the corpus file that pins what the
|
|
75
|
+
manual leaves open;
|
|
76
|
+
- **pure Python** — no runtime dependencies; CPython and PyPy;
|
|
77
|
+
- **independently validated** — correctness claims come from external
|
|
78
|
+
oracles ranked by tier, and from referees built and run from pinned
|
|
79
|
+
sources, never from code-generation confidence;
|
|
80
|
+
- **embeddable** — a host passes in its bus as callables and controls when
|
|
81
|
+
the processor advances; and
|
|
82
|
+
- **inspectable** — processor state, disassembly in MAME's spelling, bounded
|
|
83
|
+
debugging with breakpoints, watchpoints and bus-access tracking,
|
|
84
|
+
structured traces, and `python -m m68000_python` for stepping a binary.
|
|
85
|
+
|
|
86
|
+
## Validation
|
|
87
|
+
|
|
88
|
+
Each oracle is named with its tier: where its expected values came from. A
|
|
89
|
+
lower tier detects; the highest tier that checks a claim decides it
|
|
90
|
+
([oracle tiers](docs/validation.md#the-tier-rule)). The core passes:
|
|
91
|
+
|
|
92
|
+
- **hardware-captured:** every input of flamewing's 68k BCD verifier tables,
|
|
93
|
+
**525,312 cases** of `ABCD`, `SBCD` and `NBCD` recorded on two Sega Genesis
|
|
94
|
+
models, result and all five flags (`tests/test_bcd.py`);
|
|
95
|
+
- **hardware-corrected, run:** WinUAE's CPU-tester core, whose 68000 its
|
|
96
|
+
author corrects against real Amigas with `cputest`, built here from a
|
|
97
|
+
pinned commit and run over the whole gate: **308,416 of 314,988** judged
|
|
98
|
+
cases agree, and every residual is an address-error or 2-clock difference
|
|
99
|
+
listed by name ([docs/referees.md](docs/referees.md));
|
|
100
|
+
- **emulator-derived, the gate:** all **127 files, 317,500 cases** of the
|
|
101
|
+
pinned SingleStepTests/m68000 corpus, generated from MAME's
|
|
102
|
+
microcode-transcribed core, compared on registers, SR, both stack
|
|
103
|
+
pointers, the prefetch queue, RAM, the clock total and every bus access in
|
|
104
|
+
order with its function code, **no case excluded**; and the clock at which
|
|
105
|
+
each access ends within its step, over the 261,894 cases without an
|
|
106
|
+
address error (`tests/test_step_clocks.py`);
|
|
107
|
+
- **emulator-derived, real code:** MAME 0.285 in lockstep, every register
|
|
108
|
+
before every instruction of **24,595,631 instructions** of System 16B
|
|
109
|
+
Altered Beast (every write and every instruction's clocks checked) and
|
|
110
|
+
**28,249,660** of Genesis Altered Beast;
|
|
111
|
+
- **emulator-derived, detector:** SingleStepTests/680x0, **787,660 of
|
|
112
|
+
1,000,060** cases agree and every disagreement has a named cause, most
|
|
113
|
+
decided in the core's favour by the WinUAE run; and Musashi, an
|
|
114
|
+
independent hand-written core, run over the gate: 257,300 of 261,894
|
|
115
|
+
judged cases, every difference decided by a higher tier;
|
|
116
|
+
- **documentation:** the interrupt and STOP scenarios, consistent with the
|
|
117
|
+
manual and with MAME's 5,579 lockstep interrupts; and every handler's
|
|
118
|
+
rule, cited to a page of Motorola's manuals in its docstring, with the
|
|
119
|
+
SingleStepTests file named wherever the manual is silent.
|
|
120
|
+
|
|
121
|
+
Where the sources disagree the core follows the gate and the claim is
|
|
122
|
+
**contested**: six address-error behaviours, three 2-clock questions and
|
|
123
|
+
the double bus fault. Each is listed with what
|
|
124
|
+
would settle it ([docs/claims.md](docs/claims.md)). The order of bus cycles
|
|
125
|
+
and the exact clock totals rest on MAME's lineage alone, and the pages say so.
|
|
126
|
+
|
|
127
|
+
What the evidence reaches is mapped: the suite executes all 45,815 defined
|
|
128
|
+
first words ([docs/coverage.md](docs/coverage.md)), and of 176 seeded
|
|
129
|
+
mutants it kills 174, the two survivors provably equivalent
|
|
130
|
+
([docs/mutation.md](docs/mutation.md)). Exact revisions, hashes, commands
|
|
131
|
+
and timings are in [the validation record](docs/validation.md).
|
|
132
|
+
|
|
133
|
+
This is an instruction-level semantic and lifecycle claim. It is **not** a
|
|
134
|
+
claim of cycle-accurate bus-pin behaviour, of wait states, or of a complete
|
|
135
|
+
computer.
|
|
136
|
+
|
|
137
|
+
### CI coverage
|
|
138
|
+
|
|
139
|
+
The two badges cover different things, and neither covers everything:
|
|
140
|
+
|
|
141
|
+
| Badge | Runs | When |
|
|
142
|
+
| --- | --- | --- |
|
|
143
|
+
| **CI** | the fast suite (decoder, readability, BCD tables, manual-derived and referee-pinned tests, the tooling), Ruff check and format, both examples, on CPython 3.11-3.14 and PyPy 3.11; the SingleStepTests gate and the `step_clocks` claim on CPython 3.14 and PyPy 3.11; a wheel build and installed-API smoke test | every push and pull request |
|
|
144
|
+
| **Oracles** | the decoder against MAME's `m68000.lst`, the 680x0 detector, the WinUAE and Musashi referees built and calibrated, the coverage map and the mutation score, each failing if its number moves from the one the documents record | weekly, and on demand |
|
|
145
|
+
|
|
146
|
+
**The MAME lockstep is certified locally, not in CI.** It needs MAME 0.285
|
|
147
|
+
and the ROMs; its command lines, counts and timings are in
|
|
148
|
+
[the validation record](docs/validation.md).
|
|
149
|
+
|
|
150
|
+
## Vibe coded, oracle validated
|
|
151
|
+
|
|
152
|
+
This core was written by an AI agent in one night from a brief, then taken
|
|
153
|
+
through three verification rounds: coverage and mutation testing, referees
|
|
154
|
+
built and run, and a claim boundary. That history is stated plainly because
|
|
155
|
+
the correctness claim does not rest on it. Generated emulator code can be
|
|
156
|
+
plausible and wrong, above all around the prefetch queue, address-error
|
|
157
|
+
frames and undefined flags; the feedback loop was made stronger than the
|
|
158
|
+
model's confidence, and four core bugs the gate could not see were found by
|
|
159
|
+
the coverage work and the referees and fixed as failing tests first. See
|
|
160
|
+
[AI-assisted development](docs/ai-assisted-development.md).
|
|
161
|
+
|
|
162
|
+
## Version status
|
|
163
|
+
|
|
164
|
+
The current release is **`0.1.0`** (see its [release note](docs/releases/0.1.0.md)
|
|
165
|
+
and the [changelog](CHANGELOG.md)).
|
|
166
|
+
|
|
167
|
+
### Install from PyPI
|
|
168
|
+
|
|
169
|
+
```text
|
|
170
|
+
python -m pip install m68000-python
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
### Install the source tree
|
|
174
|
+
|
|
175
|
+
```text
|
|
176
|
+
git clone https://github.com/alewman/m68000-python.git
|
|
177
|
+
cd m68000-python
|
|
178
|
+
python -m venv .venv && . .venv/bin/activate # Windows: .venv\Scripts\activate
|
|
179
|
+
python -m pip install -e ".[dev]"
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
Recent Debian, Ubuntu, Fedora and Homebrew Pythons refuse `pip install` into
|
|
183
|
+
the system interpreter (PEP 668); the virtual environment line above is the
|
|
184
|
+
supported way around that. The distribution is named `m68000-python`; its
|
|
185
|
+
import is `m68000_python`.
|
|
186
|
+
|
|
187
|
+
## Minimal host
|
|
188
|
+
|
|
189
|
+
The host owns memory and devices and passes the CPU its bus as four
|
|
190
|
+
callables over a 24-bit address space. A bytearray serves the byte accesses
|
|
191
|
+
as it is; two small functions assemble and split the words, big-endian as
|
|
192
|
+
the chip does. `reset()` fetches SSP and PC from addresses 0 and 4.
|
|
193
|
+
|
|
194
|
+
```python
|
|
195
|
+
from m68000_python import M68000CPU
|
|
196
|
+
|
|
197
|
+
memory = bytearray(1 << 16)
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def read_word(address):
|
|
201
|
+
return (memory[address] << 8) | memory[address + 1]
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def write_word(address, value):
|
|
205
|
+
memory[address] = value >> 8
|
|
206
|
+
memory[address + 1] = value & 0xFF
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
memory[0:8] = (0x8000).to_bytes(4, "big") + (0x1000).to_bytes(4, "big") # SSP, PC
|
|
210
|
+
memory[0x1000:0x1004] = bytes((0x70, 0x05, 0x52, 0x80)) # moveq #5,D0; addq.l #1,D0
|
|
211
|
+
cpu = M68000CPU(memory.__getitem__, read_word, memory.__setitem__, write_word)
|
|
212
|
+
cpu.reset()
|
|
213
|
+
assert cpu.step() == 4
|
|
214
|
+
assert cpu.step() == 8
|
|
215
|
+
assert cpu.R[0] == 6
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
`examples/minimal_m68000_host.py` is this program, run in CI;
|
|
219
|
+
`examples/interrupt_host.py` adds a device that raises level 4. This is the
|
|
220
|
+
embedding contract of the whole family: z80-python and m6800-python take
|
|
221
|
+
their buses the same way.
|
|
222
|
+
|
|
223
|
+
`step()` runs one instruction or one exception entry and returns its clock
|
|
224
|
+
total. Registers (`R[0:8]` D0-D7, `R[8:16]` A0-A7, `SR`) are directly
|
|
225
|
+
readable and writable; `PC` reads the address of the next instruction and
|
|
226
|
+
`set_pc()` starts execution somewhere else. Optional constructor keywords,
|
|
227
|
+
each free unless used: `acknowledge(level)` answers the interrupt-acknowledge
|
|
228
|
+
cycle with a vector number, `AUTOVECTOR` or `SPURIOUS`; `function_codes=True`
|
|
229
|
+
passes `fc=` on every access; `tas_write(address, value)` receives TAS's
|
|
230
|
+
write half, which the Genesis bus drops; `address_error(address, write, fc)`
|
|
231
|
+
is told about the access an address error aborted; `reset_devices()` sees
|
|
232
|
+
the RESET instruction's pulse. A host raises `BusError` from a callable to
|
|
233
|
+
assert BERR. Inside a callable, `cpu.step_clocks` says at which clock of the
|
|
234
|
+
step the access ends, which a board needs to stall the CPU at the right
|
|
235
|
+
point; the core models no wait states, so the host adds its own stall
|
|
236
|
+
clocks to the total. [The interrupt lifecycle](docs/interrupt-lifecycle.md)
|
|
237
|
+
has the whole host protocol.
|
|
238
|
+
|
|
239
|
+
Speed, on the `base` workload of `benchmarks/m68000_core_benchmark.py`
|
|
240
|
+
(2026-09-25, a shared machine at load 12): about 1.4 million instructions
|
|
241
|
+
per second on CPython 3.14.4 and about 33 million on PyPy 7.3.20; a Mega
|
|
242
|
+
Drive's 68000 executes about 1 million a second. The four workloads and the
|
|
243
|
+
polish round's speed ladder are in [the validation record](docs/validation.md#speed).
|
|
244
|
+
|
|
245
|
+
## Reference-core boundary
|
|
246
|
+
|
|
247
|
+
`m68000-python` owns:
|
|
248
|
+
|
|
249
|
+
- the 68000 instruction semantics, the two-word prefetch queue and the bus
|
|
250
|
+
sequence of every instruction, and the clock total of every step;
|
|
251
|
+
- the registers, SR, both stack pointers, and the internal state that
|
|
252
|
+
decides what the next step does;
|
|
253
|
+
- the exception model: group 0 (address error, and bus error when the host
|
|
254
|
+
asserts it), group 1 (trace, interrupts, illegal, privilege) and group 2
|
|
255
|
+
(TRAP, TRAPV, CHK, divide by zero), with their frames; and
|
|
256
|
+
- processor-level observation and debugging values.
|
|
257
|
+
|
|
258
|
+
A host machine owns:
|
|
259
|
+
|
|
260
|
+
- ROM, RAM, memory maps, mappers, devices and open-bus values;
|
|
261
|
+
- video, audio, input, DMA and a second processor's share of the bus;
|
|
262
|
+
- frame, scanline, clock and interrupt scheduling, and wait states;
|
|
263
|
+
- device resets and the RESET and HALT pins beyond the RESET instruction;
|
|
264
|
+
- side-effect-free memory peeking; and
|
|
265
|
+
- complete machine save states, deterministic replay and rewind.
|
|
266
|
+
|
|
267
|
+
Accordingly, the project does not claim:
|
|
268
|
+
|
|
269
|
+
- the contents of a bus-error frame (the vector is taken; the frame's
|
|
270
|
+
stacked PC, IR and I/N bit are not checked by any oracle here);
|
|
271
|
+
- cycle placement inside an instruction beyond `step_clocks`, TAS's
|
|
272
|
+
read-modify-write shape, wait states, or pin timing;
|
|
273
|
+
- the 68010 and later (VBR, loop mode, the 68010 frame, `MOVEC`, `MOVES`,
|
|
274
|
+
the 68020's bus and addressing), or the 68008's timing; or
|
|
275
|
+
- a complete arcade board, console or computer.
|
|
276
|
+
|
|
277
|
+
These are scope boundaries, not unfinished promises; the claim boundary in
|
|
278
|
+
[docs/claims.md](docs/claims.md) is exact about each.
|
|
279
|
+
|
|
280
|
+
## Learning and inspection
|
|
281
|
+
|
|
282
|
+
New to the 68000? Read [Start here](docs/start-here.md) first: the register
|
|
283
|
+
file, the status register, how an opcode word splits into fields, the twelve
|
|
284
|
+
effective-address modes, the exception model and the prefetch queue, each
|
|
285
|
+
section naming the module that implements it.
|
|
286
|
+
|
|
287
|
+
The implementation is organised by instruction family behind a small public
|
|
288
|
+
`M68000CPU` facade. Every opcode handler's docstring starts with its
|
|
289
|
+
Motorola name, so `grep MOVEM src/` lands on the implementation, and ends
|
|
290
|
+
with where its rule comes from: a page of the *M68000 Programmer's Reference
|
|
291
|
+
Manual* or a table of the *User's Manual*, plus the SingleStepTests file,
|
|
292
|
+
the WinUAE run or the hardware tables that pin any rule the manuals do not
|
|
293
|
+
give (the PC an address error stacks, the order of bus cycles, an undefined
|
|
294
|
+
flag). A test (`tests/test_readability.py`) enforces the name, the citation
|
|
295
|
+
and the evidence line the same way the corpus gate enforces correctness.
|
|
296
|
+
|
|
297
|
+
The tree also provides immutable `CPUState` capture and restoration for
|
|
298
|
+
processor-owned state and a disassembler for every first word, in MAME's
|
|
299
|
+
spelling, checked against MAME's own disassembly of 1,300 instructions of
|
|
300
|
+
real game code. Disassembly requires an explicit side-effect-free word
|
|
301
|
+
reader: debugging must not accidentally acknowledge a device.
|
|
302
|
+
|
|
303
|
+
See [CPU state](docs/cpu-state.md), [disassembly](docs/disassembly.md),
|
|
304
|
+
[undocumented behaviour](docs/undocumented-behavior.md) and
|
|
305
|
+
[timing](docs/timing.md).
|
|
306
|
+
|
|
307
|
+
## Diagnostics and tooling
|
|
308
|
+
|
|
309
|
+
- `DebugSession` wraps an existing host with bounded execution, execute
|
|
310
|
+
breakpoints, watchpoints and bus-access tracking, boundary-kind records
|
|
311
|
+
and bounded history. It adds nothing to the hot path when unused.
|
|
312
|
+
- `CommandDebugger` is a dependency-free text-stream frontend; `python -m
|
|
313
|
+
m68000_python --load FILE@ADDR --pc ADDR` or `--zip ROMS.zip:even,odd@0
|
|
314
|
+
--reset` steps a binary without writing a host.
|
|
315
|
+
- Traces are versioned JSON Lines; `first_trace_divergence` and
|
|
316
|
+
`first_session_divergence` stop at the first differing boundary of two
|
|
317
|
+
files or two live machines, with every field named.
|
|
318
|
+
- `python -m m68000_python.conformance trace|diff` runs a manifest (memory,
|
|
319
|
+
initial state, interrupt events, BERR ranges, replayed devices) on the
|
|
320
|
+
reference and diffs another core's trace against it, bus accesses
|
|
321
|
+
included: the kit and the certification ladder for a 68000 core in
|
|
322
|
+
another language.
|
|
323
|
+
|
|
324
|
+
See [debug sessions](docs/debug-session.md), [trace comparison](docs/trace-comparison.md),
|
|
325
|
+
[the trace schema](docs/trace-schema.md) and [conformance](docs/conformance.md).
|
|
326
|
+
|
|
327
|
+
## Development
|
|
328
|
+
|
|
329
|
+
Run the ordinary quality gate:
|
|
330
|
+
|
|
331
|
+
```text
|
|
332
|
+
python -m pytest -q
|
|
333
|
+
python -m ruff check .
|
|
334
|
+
python -m ruff format --check .
|
|
335
|
+
python examples/minimal_m68000_host.py
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
The corpora are external artifacts and are not bundled. Fetch the pinned
|
|
339
|
+
gate corpus (138 MB) with:
|
|
340
|
+
|
|
341
|
+
```text
|
|
342
|
+
python scripts/fetch_test_vectors.py
|
|
343
|
+
python -m pytest -q tests/test_corpus.py tests/test_step_clocks.py
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
`--with-680x0` adds the second corpus (203 MB, unlicensed, detector only);
|
|
347
|
+
`--files NOP,ABCD` fetches a few files for a quick start. The referees,
|
|
348
|
+
the detector, the coverage map and the mutation run each have their command
|
|
349
|
+
in the document that records their result.
|
|
350
|
+
|
|
351
|
+
## Project records
|
|
352
|
+
|
|
353
|
+
- [The claim boundary](docs/claims.md): every behaviour with its status
|
|
354
|
+
- [0.1.0 release notes](docs/releases/0.1.0.md)
|
|
355
|
+
- [Validation evidence and scope](docs/validation.md)
|
|
356
|
+
- [Referees: WinUAE and Musashi, built and run](docs/referees.md)
|
|
357
|
+
- [Coverage: what the evidence reaches](docs/coverage.md)
|
|
358
|
+
- [Mutation: what the suite would notice](docs/mutation.md)
|
|
359
|
+
- [Public API stability](docs/api-stability.md)
|
|
360
|
+
- [Interrupt lifecycle](docs/interrupt-lifecycle.md)
|
|
361
|
+
- [CPU state](docs/cpu-state.md)
|
|
362
|
+
- [Disassembly](docs/disassembly.md)
|
|
363
|
+
- [Debug sessions](docs/debug-session.md)
|
|
364
|
+
- [Trace comparison](docs/trace-comparison.md)
|
|
365
|
+
- [Trace schema](docs/trace-schema.md)
|
|
366
|
+
- [Conformance: proving another core is the same CPU](docs/conformance.md)
|
|
367
|
+
- [Start here: 68000 primer](docs/start-here.md)
|
|
368
|
+
- [Timing](docs/timing.md)
|
|
369
|
+
- [Undocumented behaviour](docs/undocumented-behavior.md)
|
|
370
|
+
- [MAME as a trace oracle](docs/mame-oracle.md)
|
|
371
|
+
- [AI-assisted development](docs/ai-assisted-development.md)
|
|
372
|
+
- [Contribution guidance](CONTRIBUTING.md)
|
|
373
|
+
- [History: the handoff brief and the worklog](docs/README.md)
|
|
374
|
+
|
|
375
|
+
## License
|
|
376
|
+
|
|
377
|
+
MIT. The Motorola manuals, MAME, WinUAE, Musashi, the SingleStepTests
|
|
378
|
+
corpora and the BCD verifier retain their own licenses and are cited, not
|
|
379
|
+
bundled.
|