diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5128abe..11a14be 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -58,12 +58,26 @@ jobs: - uses: dtolnay/rust-toolchain@stable - uses: Swatinem/rust-cache@v2 - run: cargo build --release - - name: Compile all .ne examples + - name: Verify committed ROMs match their .ne sources + # Every example has a committed .nes file under examples/. The + # compiler is deterministic (verified: same input → byte-identical + # output), so rebuilding each example into a tmp directory and + # diffing against the committed ROM catches "someone edited a .ne + # file but forgot to regenerate the ROM" at PR time. run: | + mkdir -p /tmp/reprotest + fail=0 for f in examples/*.ne; do - echo "==> Compiling $f" - cargo run --release -- build "$f" + name=$(basename "$f" .ne) + ./target/release/nescript build "$f" --output "/tmp/reprotest/$name.nes" + if ! cmp -s "examples/$name.nes" "/tmp/reprotest/$name.nes"; then + echo "::error file=examples/$name.ne::committed examples/$name.nes is stale; rerun \`cargo run --release -- build examples/$name.ne\` and commit the new ROM" + fail=1 + fi done + if [ $fail -ne 0 ]; then + exit 1 + fi - name: Verify ROMs are valid iNES run: | for f in examples/*.nes; do @@ -113,6 +127,14 @@ jobs: - name: Build compiler run: cargo build --release - name: Compile all .ne examples + # Overwrites the committed examples/*.nes in the CI workspace + # (the workspace is ephemeral; the committed ROMs aren't + # touched). This is load-bearing: the goldens must be diffed + # against what *this* compiler produces, not against the ROMs + # an earlier commit happened to ship, otherwise a broken + # compiler change can still pass the emulator job as long as + # the contributor forgot to rebuild the ROMs. The `examples` + # job catches that second case via its reproducibility diff. run: | for f in examples/*.ne; do echo "==> Compiling $f" @@ -121,7 +143,7 @@ jobs: - name: Install emulator harness dependencies working-directory: tests/emulator run: npm ci - - name: Diff each ROM's framebuffer against its committed golden + - name: Diff each ROM's framebuffer against its golden working-directory: tests/emulator run: node run_examples.mjs - name: Upload actual + diff PNGs on failure diff --git a/.gitignore b/.gitignore index 68204c8..3475059 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,3 @@ /target -*.nes *.dbg *.map diff --git a/CLAUDE.md b/CLAUDE.md index aa7bb6a..4df3038 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -15,6 +15,13 @@ re-derive the project conventions from scratch. pipeline. - Examples live in `examples/*.ne`. Every example is expected to compile cleanly and has a pinned emulator golden — see below. +- **`examples/*.nes` is committed.** The compiler is deterministic + (same source → byte-identical ROM), so the ROMs travel with the + repo. If you edit any `.ne` file you **must** rebuild its `.nes` + in the same commit — CI's `examples` job rebuilds each ROM into a + tmp path and fails if the committed version differs, pointing at + the exact `cargo run -- build examples/.ne` to run. The + pre-commit hook under `scripts/pre-commit` catches this locally. - `docs/future-work.md` lists the remaining gaps. If you implement something from that file, update the doc in the same PR. @@ -63,8 +70,9 @@ tests/emulator/ The harness is **separate** from `cargo test`. You have to run it by hand: ```bash -# 1. Build every example first. The harness reads pre-built .nes files -# from examples/ — it will not invoke cargo. +# 1. Rebuild every example with the current compiler. The harness +# reads whatever sits under examples/*.nes — if you want to test +# your working copy you have to rebuild them first. cargo build --release for f in examples/*.ne; do ./target/release/nescript build "$f"; done @@ -72,13 +80,22 @@ for f in examples/*.ne; do ./target/release/nescript build "$f"; done cd tests/emulator npm install # or `npm ci` in CI -# 3. Verify every example still matches its golden. +# 3. Verify every ROM still matches its golden. node run_examples.mjs -# → "21/21 ROMs match their goldens" on success +# → "22/22 ROMs match their goldens" on success # → FAIL / MISS lines + `actual/.png`, `actual/.diff.png`, # `actual/.wav` written for any ROM that mismatched ``` +The harness always runs against whatever sits in `examples/*.nes`, +so iterating on the compiler means rebuilding the example first. +CI's `emulator` job does this too — it builds the compiler, compiles +every `.ne` into the workspace (overwriting the committed ROMs, +which are ephemeral in the CI checkout), and then runs the harness. +The committed ROMs are a PR-review convenience and a "did this +change affect codegen" tripwire via the `examples` job's +reproducibility diff; they are **not** what the emulator job tests. + ### Updating goldens If a change is supposed to flip goldens (you added a new example, changed diff --git a/examples/arrays_and_functions.nes b/examples/arrays_and_functions.nes new file mode 100644 index 0000000..71509e6 Binary files /dev/null and b/examples/arrays_and_functions.nes differ diff --git a/examples/audio_demo.nes b/examples/audio_demo.nes new file mode 100644 index 0000000..b72ed10 Binary files /dev/null and b/examples/audio_demo.nes differ diff --git a/examples/bitwise_ops.nes b/examples/bitwise_ops.nes new file mode 100644 index 0000000..94e82bb Binary files /dev/null and b/examples/bitwise_ops.nes differ diff --git a/examples/bouncing_ball.nes b/examples/bouncing_ball.nes new file mode 100644 index 0000000..0c92f57 Binary files /dev/null and b/examples/bouncing_ball.nes differ diff --git a/examples/coin_cavern.nes b/examples/coin_cavern.nes new file mode 100644 index 0000000..3e72b7f Binary files /dev/null and b/examples/coin_cavern.nes differ diff --git a/examples/comparisons.nes b/examples/comparisons.nes new file mode 100644 index 0000000..c09cb40 Binary files /dev/null and b/examples/comparisons.nes differ diff --git a/examples/function_chain.nes b/examples/function_chain.nes new file mode 100644 index 0000000..6471e48 Binary files /dev/null and b/examples/function_chain.nes differ diff --git a/examples/hello_sprite.nes b/examples/hello_sprite.nes new file mode 100644 index 0000000..97eaccf Binary files /dev/null and b/examples/hello_sprite.nes differ diff --git a/examples/inline_asm_demo.nes b/examples/inline_asm_demo.nes new file mode 100644 index 0000000..caee039 Binary files /dev/null and b/examples/inline_asm_demo.nes differ diff --git a/examples/logic_ops.nes b/examples/logic_ops.nes new file mode 100644 index 0000000..511a9ef Binary files /dev/null and b/examples/logic_ops.nes differ diff --git a/examples/loop_break_continue.nes b/examples/loop_break_continue.nes new file mode 100644 index 0000000..a3792b9 Binary files /dev/null and b/examples/loop_break_continue.nes differ diff --git a/examples/match_demo.nes b/examples/match_demo.nes new file mode 100644 index 0000000..772ecae Binary files /dev/null and b/examples/match_demo.nes differ diff --git a/examples/mmc1_banked.nes b/examples/mmc1_banked.nes new file mode 100644 index 0000000..3d28138 Binary files /dev/null and b/examples/mmc1_banked.nes differ diff --git a/examples/mmc3_per_state_split.nes b/examples/mmc3_per_state_split.nes new file mode 100644 index 0000000..f80e039 Binary files /dev/null and b/examples/mmc3_per_state_split.nes differ diff --git a/examples/palette_and_background.nes b/examples/palette_and_background.nes new file mode 100644 index 0000000..d65391d Binary files /dev/null and b/examples/palette_and_background.nes differ diff --git a/examples/platformer.nes b/examples/platformer.nes new file mode 100644 index 0000000..0ed5185 Binary files /dev/null and b/examples/platformer.nes differ diff --git a/examples/scanline_split.nes b/examples/scanline_split.nes new file mode 100644 index 0000000..5f95708 Binary files /dev/null and b/examples/scanline_split.nes differ diff --git a/examples/sprites_and_palettes.nes b/examples/sprites_and_palettes.nes new file mode 100644 index 0000000..e307e0a Binary files /dev/null and b/examples/sprites_and_palettes.nes differ diff --git a/examples/state_machine.nes b/examples/state_machine.nes new file mode 100644 index 0000000..9b438f4 Binary files /dev/null and b/examples/state_machine.nes differ diff --git a/examples/structs_enums_for.nes b/examples/structs_enums_for.nes new file mode 100644 index 0000000..553f6d0 Binary files /dev/null and b/examples/structs_enums_for.nes differ diff --git a/examples/two_player.nes b/examples/two_player.nes new file mode 100644 index 0000000..2611272 Binary files /dev/null and b/examples/two_player.nes differ diff --git a/examples/uxrom_banked.nes b/examples/uxrom_banked.nes new file mode 100644 index 0000000..dbff6c6 Binary files /dev/null and b/examples/uxrom_banked.nes differ diff --git a/scripts/pre-commit b/scripts/pre-commit index af6ada2..e9539d1 100755 --- a/scripts/pre-commit +++ b/scripts/pre-commit @@ -15,4 +15,28 @@ cargo clippy --all-targets -- -D warnings echo " Running tests..." cargo test --all-targets +# Committed ROMs must match their .ne sources — the compiler is +# deterministic, so any example whose on-disk .nes differs from a +# fresh build means someone edited the .ne file but forgot to +# rebuild. Rebuild each to a tmp path and fail if any differ, then +# point the user at the exact `cargo run` command to fix it. +echo " Checking committed ROMs match .ne sources..." +cargo build --release --quiet +tmp=$(mktemp -d) +trap 'rm -rf "$tmp"' EXIT +stale=0 +for f in examples/*.ne; do + name=$(basename "$f" .ne) + ./target/release/nescript build "$f" --output "$tmp/$name.nes" >/dev/null + if ! cmp -s "examples/$name.nes" "$tmp/$name.nes"; then + echo " STALE: examples/$name.nes" + echo " rerun: cargo run --release -- build $f" + stale=1 + fi +done +if [ $stale -ne 0 ]; then + echo "one or more committed ROMs are out of date" + exit 1 +fi + echo "All pre-commit checks passed."