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/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
+ [![CI](https://github.com/alewman/m68000-python/actions/workflows/ci.yml/badge.svg)](https://github.com/alewman/m68000-python/actions/workflows/ci.yml)
54
+ [![Oracles](https://github.com/alewman/m68000-python/actions/workflows/oracles.yml/badge.svg)](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.