JNEXT
Skip to content

1.3 The rules that shape the code

If you read one page of this guide before touching the code, make it this one.

JNEXT holds a handful of project-wide invariants that are not visible from any single file. You cannot discover them by reading the function you are about to change, and each of them exists for the same reason: the failure it prevents already happened once, and nobody noticed at the time. The practical consequence is that breaking one of these rules rarely produces an error message about the rule. It produces a red test run somewhere else entirely, about something that looks unrelated to your change. Five minutes here saves that afternoon.

The VHDL is the specification

The authoritative description of the hardware is not a manual or a wiki page. It is the VHDL source of the official ZX Spectrum Next FPGA core — the cores/zxnext/src/ tree of the FPGA repository, which is expected to be checked out beside this one. When JNEXT and the documentation disagree about what the machine does, the VHDL decides.

Calling it "authoritative" is not a slogan. In practice it means three specific things.

Expected values come from the VHDL, never from the current code. This is the rule most easily broken with good intentions. A test written by reading Layer2::render_scanline and asserting what it currently produces proves only that the function still does what it did yesterday — it will happily pin a bug in place and then defend it. The expected value has to come from the hardware description instead. doc/testing/UNIT-TEST-PLAN-EXECUTION.md is the process document for all of this, and it is mandatory reading before you write or rewrite a subsystem's test plan.

Fixes are faithful, not convenient. When a defect could be closed either by a targeted hack or by transcribing what the VHDL actually does, the transcription wins, even when it is the larger job. The comment blocks scattered through the source that quote signal names and line numbers — zxnext.vhd:7103, zxula_timing.vhd:577 — are the record of those decisions, and they exist so the next reader can check the claim rather than take it on faith.

Citations are machine-checked. Those references are not decorative. doc/testing/TRACEABILITY-MATRIX.md is a generated table mapping plan row → test ID → VHDL citation → test location, and its generator validates every one of those citations against the real FPGA source tree, so a reference that has been typo'd or points at a file that no longer exists is caught rather than published. See 4.4 Traceability.

Some documents are generated, committed, and gated

Four outputs in the repository are produced by a tool and then checked into git anyway, so that a fresh clone can read them with no toolchain installed at all:

Output Source Regenerate with
doc/man/jnext.1 and USAGE.md doc/man/jnext.1.md make docs-man
doc/user-guide/ src/doc/user-guide/ make docs-userguide
doc/developer-guide/ and src/doc/developer-guide/img/*.svg src/doc/developer-guide/ and diagrams/*.dot make docs-devguide
doc/testing/TRACEABILITY-MATRIX.md the test sources and plan docs regenerated by make unit-test

Committing a generated file has an obvious hazard: the copy in git can stop matching the source it came from, and a stale copy is a silent lie that no reviewer can see by reading the diff. So the project closes that hole mechanically — make docs-check is a prerequisite of both make unit-test and make regression, which means a stale generated document fails the test run itself rather than waiting to be noticed. What that asks of you is simple: edit the source, re-render, and commit both halves in the same change.

On a machine without pandoc, mkdocs or graphviz the relevant part of the check skips rather than failing, so you can still work. In CI it hard-fails, because a check that quietly skips is indistinguishable from a check that passed.

It is worth being precise about what this gate does and does not establish. docs-check proves the outputs match jnext.1.md. It does not prove that jnext.1.md describes the command line the program actually parses — that is make cli-check, and even then only for the flag set. The prose in the man page's narrative sections is checked by nobody at all. See 4.5 The documentation and CLI gates.

The command line is data, not control flow

src/core/cli_options.h holds every accepted flag as a constexpr table of { name, arity, doc-status, id }, and src/main.cpp dispatches from it with a switch over cli::OptId compiled with -Werror=switch. Adding a flag to the table and forgetting to write its parser arm is therefore a compile error, not a flag that exists and silently does nothing. Because the flag set is now a value the program can enumerate, cli_options_test can diff it against the man page's OPTIONS section in both directions at once — undocumented flags and documented-but-absent flags are both failures.

The shape of that is a direct response to what came before it. The flag set used to exist only as a 47-arm if chain, which nothing could enumerate; because nothing could enumerate it, nothing could compare it to the documentation, and the documentation drifted twice while every gate stayed green. Deliberate exceptions live in the table — there is exactly one, the undocumented --sd-card back-compat alias — and never in a checker's exclusion list, because an exclusion list is invisible from the code it excuses.

Test suites are declared, and a missing test is loud

test/unit-tests.conf names every unit suite together with the exact number of rows it must report. test/run-unit-tests.sh refuses to run at all if that manifest and the suites CMake registered disagree in either direction, and fails the run if a suite reports any other count — including a lower one, which is the case that matters. The regression suite carries the same contract in test/00regression/regression_tests.conf and functional_tests.conf, with one extra witness on top: every checked-in reference PNG must have an entry in the conf, so deleting lines from the conf cannot quietly shrink the suite.

The practical effect is that adding or removing a test row means editing a number in a manifest, by hand. That is not friction to be engineered away — it is the mechanism. The number is the project's claim about how much it tests, so it should only ever change because someone decided to change it. Before this existed, three suites vanished from the counts in a single day and each was found by accident; a green result is only as trustworthy as its denominator. 4.2 Declared suites and pinned counts has the detail.

The core does not know about the frontend

Emulator can be constructed and run with no window, no audio device and no host input at all. That is what makes headless testing and the unit suites possible in the first place, and it is maintained by a simple rule: nothing under src/core, src/cpu, src/memory, src/video, src/audio, src/port or src/peripheral includes SDL or Qt. When the core genuinely does need to tell a host something — a joystick source changed, a rewind restored the input state — it goes through a std::function whose signature mentions no SDL or Qt type.

This is the one rule in the list that no gate will catch. Adding such an include compiles perfectly well today; it just makes the core unbuildable in some configuration, on somebody else's machine, long after you wrote it.

Build configurations and the ENABLE_QT_UI / ENABLE_DEBUGGER arms are covered in 5.2 Build configurations; branch, worktree and review discipline is in 6.2 Branches, worktrees and review.