1
0
Fork 0
mirror of https://github.com/imjasonh/nescript synced 2026-07-08 17:06:04 +00:00
Commit graph

7 commits

Author SHA1 Message Date
Claude
5e3e68ca11
docs: regenerate platformer.gif and lock it as a CI invariant
The optimizer fix in the previous commit changes the observable
gameplay of `examples/platformer.ne` — pre-fix the player got
spurious enemy-1 stomp bounces every time coin 2 drifted into its
pickup window, so the README demo gif showed the player bouncing
mid-air around emu frames 85-125 instead of walking through the
coin at ground level. Regenerate `docs/platformer.gif` from the
fixed compiler so the README matches reality.

To stop this from drifting again, treat the gif the same way the
repo already treats `examples/*.nes`:

- `gifenc` + `jsnes` + the harness are deterministic, so a fresh
  recording byte-matches a valid commit. Verified across two
  back-to-back runs (identical md5).

- `.github/workflows/ci.yml`'s `emulator` job now renders the gif
  into `/tmp/platformer.gif` and `cmp`s it against `docs/platformer.gif`,
  emitting a `::error` annotation pointing at the exact rerun
  command if the committed copy is stale. This piggybacks on the
  existing puppeteer + node setup, adding ~20s to the job.

- `scripts/pre-commit` runs the same check locally, but only when
  `examples/platformer.{ne,nes}`, `tests/emulator/record_gif.mjs`,
  or `tests/emulator/harness.html` is staged, and only if
  `tests/emulator/node_modules` is already installed. Cold-start
  puppeteer is ~20s — too slow to pay on every commit, but cheap
  enough to pay when something gif-relevant changed.

- The header of `tests/emulator/record_gif.mjs` and the project
  conventions section of `CLAUDE.md` both spell out the rerun
  command and the invariant, so the next agent doesn't have to
  re-derive any of this.

https://claude.ai/code/session_013Bi4H4YQ5or5HtMB4doUFi
2026-04-13 18:04:17 +00:00
Claude
12b54a715e
ci: emulator job must still rebuild ROMs from source
The previous commit wired the emulator job to read the committed
examples/*.nes directly, with the rationale that the `examples`
job's reproducibility diff would catch "stale committed ROMs"
at PR time. That reasoning is wrong and defeats the point of
the emulator harness.

Failure mode: a compiler change lands with correctly-rebuilt
ROMs (passing the examples job) but introduces a rendering
regression that flips a golden. The emulator job would catch
this today because it runs the harness against freshly-compiled
ROMs. With the previous commit's shortcut, the harness would
boot the committed ROMs — which the PR author just rebuilt to
match — so the mismatch wouldn't show up until a second commit
touched anything else. That's exactly the kind of silent-failure
hole the golden harness exists to plug.

Fix: emulator job rebuilds the compiler (toolchain + cache +
`cargo build --release`) and compiles every .ne into the
workspace before running the harness, same as the pre-ROMs-
committed era. The committed ROMs keep their review/demo role
(clone-and-play, diff visibility in PRs) but the test job
always validates the working compiler, not a frozen snapshot.

CLAUDE.md updated to match: the harness runs against whatever
sits in examples/*.nes, so iterating locally still means
rebuilding the ROM(s) you care about first.

https://claude.ai/code/session_01BcCcHi6FUmTh8jC7UgkA3A
2026-04-13 15:21:38 +00:00
Claude
57faf9e36a
commit built ROMs alongside .ne sources
The compiler is deterministic: rebuilding any example produces
a byte-identical ROM, verified across all 22 examples and all
four mappers (NROM, MMC1, UxROM, MMC3). That means the .nes
files are reproducible artefacts and can live next to their
sources without drift.

Benefits:

- Users can clone the repo and open any example in an emulator
  without installing a Rust toolchain or running the compiler.
- The emulator harness can trust examples/*.nes directly, so its
  CI job no longer needs a compiler build or a "compile all
  examples" loop — it just boots jsnes against the committed
  ROMs and diffs each against its golden.
- ROM diffs in PRs are now meaningful: "this compiler change
  flipped 17 bytes in hello_sprite.nes" is visible review
  signal, not hidden behind the emulator golden.

Guard rails so the ROMs don't drift from their sources:

- .gitignore no longer excludes *.nes.
- The `examples` CI job rebuilds every .ne into /tmp and fails
  loudly (with a GitHub error annotation pointing at the exact
  cargo command to rerun) if any committed ROM differs.
- scripts/pre-commit does the same check locally.
- CLAUDE.md now states that editing a .ne file requires
  rebuilding its .nes in the same commit, so future agents
  won't miss the invariant.

Total footprint: 22 ROMs, 624 KB (avg 28 KB each — most are
NROM 24 KB; two banked examples are larger).

https://claude.ai/code/session_01BcCcHi6FUmTh8jC7UgkA3A
2026-04-13 15:12:39 +00:00
Claude
2d66b68c7f
tests/emulator: byte-exact golden-image diffs
The smoke test used to check a per-example `nonBlack` floor — "at
least one sprite rendered," plus a per-example minimum for the
multi-sprite examples. That catches gross regressions (a compiler
bug that makes everything go black) but silently lets through
anything that changes a handful of pixels without dropping below
the sprite floor. The whole point of this harness is to catch
compiler miscompiles before they land; a softer check means bugs
can still sneak in.

This swap makes every run diff the raw canvas framebuffer against
a committed PNG golden. One mismatched byte at pixel (120, 119)
is enough to fail CI — there's nowhere for a regression to hide.

Workflow:

    # normal — fails on any pixel change
    node tests/emulator/run_examples.mjs

    # when the diff is intentional, rewrite the goldens
    UPDATE_GOLDENS=1 node tests/emulator/run_examples.mjs
    # or: node tests/emulator/run_examples.mjs --update-goldens

    git diff tests/emulator/goldens/   # review the change
    git add tests/emulator/goldens/
    git commit                          # explain WHY in the message

When a run fails without `UPDATE_GOLDENS`, the runner writes:

    tests/emulator/actual/<name>.png       the run's raw output
    tests/emulator/actual/<name>.diff.png  red-highlighted pixel diff

so reviewers can eyeball what changed without rerunning locally.
`actual/` is gitignored and re-created on every run. The CI job
now uploads `actual/`, `goldens/`, and `report.json` together as
a single `emulator-diff` artifact on failure — side by side means
the "what changed" story is obvious without cloning.

Implementation:

- `tests/emulator/screenshots/` is renamed to `tests/emulator/goldens/`.
  All 18 existing PNGs are preserved as the initial goldens (git
  detected them as pure renames).

- `harness.html` gets a new `window.nesHarness.rawPixelsBase64()`
  that returns the 245760-byte (256 × 240 × 4 bytes) RGBA canvas
  buffer as base64. The runner compares raw pixels, not PNG
  bytes, so encoder quirks (zlib level, filter heuristics) can't
  cause false positives across Chrome versions or platforms.

- The runner uses `pngjs` (pure-JS, no native deps) to decode
  goldens and to write diff PNGs. `PNG.sync.write` is
  byte-deterministic for identical pixels, so `git diff` on a
  committed golden only ever shows up when the actual rendered
  pixels changed — not because two machines produced slightly
  different compression.

- The committed goldens were re-encoded with pngjs in this commit
  so the baseline is consistent from day one. File sizes are a
  touch larger than Chrome's output (~1KB vs ~800B on average),
  but that's negligible and it eliminates one entire class of
  flaky-looking diffs in the future.

Determinism verification: I ran each of the 18 ROMs twice
through fresh `NES` instances in fresh puppeteer pages, hashed
the 245760-byte framebuffers at frame 180 with SHA-256, and
confirmed `run1 == run2` for every single one. Exact-pixel diffs
are safe for this ROM set.

Negative path verification: I corrupted one golden (flipped one
pixel to pure red via pngjs) and reran the runner. It printed

    DIFF  hello_sprite  1/61440 pixels differ; first at (120,120)
                        expected [255,0,0] got [0,0,0]
            actual: tests/emulator/actual/hello_sprite.png
              diff: tests/emulator/actual/hello_sprite.diff.png

and exited 1 as expected. The diff PNG shows a dim-grayscale
silhouette of the expected frame with a bright-red dot on the
one mismatched pixel — enough visual context to locate the
regression at a glance.

All 18 examples match their goldens in strict mode. `cargo fmt
--check`, `cargo clippy --release --all-targets -- -D warnings`,
and `cargo test --release` (313 unit + 37 integration) are all
still green.

https://claude.ai/code/session_014Z5y3Q9krLcAxYpZQJhZ5V
2026-04-12 21:30:18 +00:00
Claude
24db4e6cda
CI: add emulator smoke test job
Runs the jsnes + puppeteer harness on every push/PR so the same
execution-level bugs that the test suite previously couldn't catch
(missing JMP/JSR fixups, orphaned handler locals, uninitialized
struct fields, silently-swallowed intrinsic calls) will be caught in
CI going forward.

The job installs the handful of shared libs Chrome for Testing needs
on the ubuntu-latest runner, builds the compiler in release, compiles
every example, then drives each ROM through the harness. On failure
it uploads the captured screenshots and the JSON report as a build
artifact so regressions are easy to triage without re-running
locally.

Kept separate from the existing `Build Examples` job so compile-time
and execution-time issues report independently.

https://claude.ai/code/session_014Z5y3Q9krLcAxYpZQJhZ5V
2026-04-12 18:49:36 +00:00
Claude
fd5a940c89
Add example builds to CI, document sprite name behavior
- CI: new "Build Examples" job compiles all .ne files and validates
  the output ROMs have correct iNES headers
- Examples: add notes explaining that sprite names (Smiley, Ball) are
  parsed but not yet resolved — all draws use the built-in CHR tile 0.
  Custom sprite declarations come in M3.
- Codegen: explicit `let _ = &draw.sprite_name` to document the
  intentional skip, with comment about M2/M3 scope

https://claude.ai/code/session_01W6eQFStA66EuMKHUFo2rx3
2026-04-11 22:55:43 +00:00
Claude
39ca246151
Implement NEScript compiler Milestone 1 ("Hello Sprite")
Complete implementation of the NEScript compiler pipeline for M1:
- Lexer: full tokenization with hex/binary/decimal literals, all keywords, operators
- Parser: recursive descent with Pratt expression parsing (M1 subset)
- Analyzer: symbol resolution, type checking, memory allocation
- 6502 Assembler: full opcode encoding table (~150 valid combinations)
- Code Generator: AST → 6502 instructions (direct, no IR for M1)
- Runtime: NES hardware init, NMI handler, controller read, OAM DMA
- Linker: NROM layout, vector table, palette loading, CHR data
- ROM Builder: iNES header generation, PRG/CHR padding
- CLI: `build` and `check` subcommands via clap

143 tests across all modules:
- 22 lexer tests (literals, keywords, operators, error recovery)
- 18 parser tests (expressions, statements, game structure, errors)
- 7 analyzer tests (symbol resolution, memory allocation, transitions)
- 30 assembler tests (every addressing mode, label resolution)
- 7 codegen tests (var init, arithmetic, buttons, draw, comparisons)
- 11 runtime tests (init sequence, NMI handler, controller read)
- 10 ROM builder tests (iNES format, mirroring, banking, validation)
- 5 linker tests (vector table, CHR data, palette loading)
- 7 integration tests (end-to-end compilation, error detection)

CI: GitHub Actions for check, fmt, clippy, test
Pre-commit: script for local fmt + clippy + test validation

https://claude.ai/code/session_01W6eQFStA66EuMKHUFo2rx3
2026-04-11 22:07:56 +00:00