Read FAMILY.md first. It is the standard every member of this family carries, byte for byte, and it decides most questions before they are asked. What follows is only what is true of this member. README.md is the document written for a person.
The SPC700: the processor Sony put inside the SNES audio unit. An interpreter for all 256 opcodes, held to a published per-opcode corpus that states every register and every byte of memory before and after, and then held to it again cycle by cycle, with the address each access drove and the cycle it fell on. Sony published no data sheet, so the top rung of the authority ladder is occupied by Nintendo's development manual, which tabulates the instruction set for somebody writing sound drivers and says almost nothing about what happens inside a cycle. This models the processor alone: the RAM and the boot ROM beside it are the audio unit's, not the processor's.
Cpu(model, memory), the shape every clocked part in the family takes.
| Call | What it does |
|---|---|
step() |
One instruction, returning what it cost in cycles |
run_for(cycles) |
Instructions until the budget is spent, returning what was really spent |
run_until(predicate, limit) |
Instructions until the predicate holds, refusing to run forever |
reset() |
The reset line, which is a different event from power on |
held() |
Whether the part has stopped advancing the program on its own |
cycles, steps |
What has been spent, and how many instructions spent it |
There is no irq and no nmi, and that is a fact about the part rather than an
omission: the audio unit brings no interrupt line out. INTERRUPT_LINES in
conformance/family.test.py is empty here, and the
record accounts for why.
Clock(cpu) drives it one cycle at a time. Every cycle passes through one place,
so the clock can stop between two cycles of one instruction rather than only
after it.
- Nintendo's SNES Development Manual, Appendix C, for the instruction tables it prints. All 256 rows are pinned with the page each was read from.
- The single step corpus, for everything the manual does not print, which is most of the behaviour and all of the cycle shape.
- Nothing else.
The rung a Sony data sheet would occupy is empty and the record says so rather than promoting the rung below it.
Not settled: 5 things, each in OPEN-QUESTIONS.md with the measurement that would close it. The sharp one is that the cycle shape rests on a recording alone: no document describes what happens inside a cycle, so 1,182,940 cycles of agreement are agreement between two implementations rather than a measurement of silicon.
Settled: every instruction's effect, every instruction's cycle count and access order, and every figure the manual prints. One row of the manual disagrees with the corpus and the disagreement is recorded rather than averaged away.
Two events, not one. The constructor does not reset: it hands back a part holding
a scrambled pattern derived from a seed, which is reproducible and is not zero.
reset() is the reset line, and it is a separate call.
There is no option to arrive cleared, because no machine hands one over. A model that answers zero to a read of a byte nothing wrote turns a defect into a passing test.
Bus.spend is the only thing that advances the cycle count, and every read,
write and idle goes through it. That is what lets a clock stop mid-instruction,
and it is why the order matters: the access is recorded first and charged second,
so a clock that stops on a cycle sees the access that cycle performed rather than
the one before it.
ruff format --check .
ruff check .
mypy
pnpm run format:check
python3 -m coverage erase
for file in $(find spc700 conformance -name '*.test.py' | sort); do
python3 -m coverage run -a "$file"
done
python3 -m coverage reportThen the throughput floor, which runs outside the coverage step because a tracer costs about ten times what the model does:
python3 -m conformance.speedThe conformance walk needs the corpus, which is fetched rather than vendored:
python3 -m conformance.fetch ~/.cache/conformance-suites
python3 -m conformance.singlestep ~/.cache/conformance-suites/spc700/spc700/v1
python3 -m conformance.cycles ~/.cache/conformance-suites/spc700/spc700/v1And the runs that report what they could not check rather than passing quietly:
python3 spc700/doctor.py
python3 -m conformance.quotesEverything under conformance/ runs as a module. Run as a script, its own
directory goes on the import path and a file there shadows any standard library
module of the same name. doctor.py is the exception and runs as a file on
purpose, so that it still runs when the package itself will not import, which is
the case it exists for.
- Python only, standard library only, no dependencies.
- No comments in source. Reasoning goes in docstrings, and a step that would need a comment is a step that should be a named function.
- Tests sit beside the module they cover as
<module>.test.py. Arrange, blank line, one act, blank line, assert, with no section labels. - 100% statement and branch coverage, enforced.
mypyat strict, with every optional error class on. - Everything a caller can catch is defined once, in
spc700/errors.py, and imported from there. - A check nobody has seen fail is not known to work. Drive every new check against input that should fail it before keeping it.
spc700/
__init__.py the package, and the part chosen at construction
core.py the processor
bus.py cycles, and the one place they are spent
clock.py driving it one cycle at a time
opcodes.py the opcode table and a disassembler
memory.py memory that holds what it held
models.py the catalogue, and the names the part answers to
errors.py everything this package raises, in one place
doctor.py what is actually on this machine, printed for a bug report
version.py rewritten by the release job and by nothing else
conformance/
family.test.py the family standard, held to this repository
suites.json which corpus, at which commit
fetch.py fetching it into a cache
singlestep.py running it, state by state
cycles.py running it, cycle by cycle
hardware.json what Nintendo printed, fact by fact, with the page
divergences.json where the document and the recording part
quotes.py looks for every quoted sentence in the document it cites
speed.py the throughput floor
- The constructor does not reset. A test that expects a cleared status word gets a scrambled one, and a scrambled P flag sends a direct-page read to page one rather than page zero. Set what the test depends on explicitly.
Memorytakesimageandfillseparately.imageis bytes loaded at the bottom with the rest left as it was.fillis one byte across the whole space. One parameter carrying both was how this was written, and it made an image silently zero everything it did not reach.disassemble(data, offset, address)takes three. The offset into the buffer and the address it maps to are separate, which the sibling members do not need and this one does.- The corpus is not here. A run without it compares nothing and says so.
python3 spc700/doctor.pyis the way to find out which state this machine is in.
Every gate above, green, with output shown. A claim without a run behind it is not evidence. If a check was skipped because a file is not on this machine, say which check and why rather than reporting a pass.
A test that fails without the change and passes with it. An entry in OPEN-QUESTIONS.md if it turned a settled thing into an open one, or removed one. A record entry with the sentence and the page if it added a fact from the manual. Nothing fetched into the repository, ever.