1
0
Fork 0
mirror of https://github.com/imjasonh/nescript synced 2026-07-08 08:55:38 +00:00
nescript/examples/README.md

102 lines
16 KiB
Markdown
Raw Normal View History

# NEScript Examples
## Quick Start
```bash
# Build the compiler
cargo build --release
# Compile all examples
for f in examples/*.ne; do cargo run -- build "$f"; done
# Or compile one
cargo run -- build examples/hello_sprite.ne
```
Open any `.nes` file in an NES emulator ([Mesen](https://www.mesen.ca/), [FCEUX](https://fceux.com/), etc.)
## Examples
| File | Features | Description |
|------|----------|-------------|
| `hello_sprite.ne` | input, draw | Move a sprite with the d-pad |
| `bouncing_ball.ne` | if/else, variables | Auto-bouncing sprite with edge detection |
| `coin_cavern.ne` | states, functions, constants | 3-state game with gravity and coin collection |
| `arrays_and_functions.ne` | arrays, functions, while | Enemy array with collision detection |
| `state_machine.ne` | on enter/exit, transitions | Multi-state flow with timers |
cleanup: fix silent miscompiles and delete dead code exposed by code review Two correctness bugs were silently producing wrong ROMs: - `x << n` / `x >> n` always shifted by 1, regardless of `n`, because the IR lowering for `BinOp::ShiftLeft`/`ShiftRight` hardcoded the count. Now eval_const the RHS into a compile-time count; fall back to a new `IrOp::ShiftLeftVar` / `ShiftRightVar` (runtime loop) when the amount isn't constant. Strength reduction folds the variable form back to a fixed count once the optimizer knows the value. - `x / n` / `x % n` always returned 0, because the lowering emitted `LoadImm(t, 0)` for `BinOp::Div`/`Mod` with a comment saying the runtime call was "TODO for now". Added real `IrOp::Div` and `IrOp::Mod`, wired them through use-counting and DCE, gave codegen `__divide`-based implementations, and taught strength reduction to rewrite power-of-two divisors into shifts and modulo-by-2ⁿ into AND masks. Constant folding now handles `Mul`/`Div`/`Mod`/shifts too, which were previously left for the codegen to emit inefficient software calls. Dead code removed (no backward-compat shims kept): - `src/debug/` entirely. `DebugSymbols`, `SourceMap`, and the Mesen/.sym emitters had no callers outside their own tests; `main.rs` never wrote a symbol file. Documented the intent in `docs/future-work.md` so it comes back intentionally if needed. - `ErrorCode::E0202` (invalid cast) and `E0403` (unreachable state): defined, formatted, and marked `#[allow(dead_code)]` but never emitted. W0104 now carries the unreachable-state semantics too. - `Level::Info`: never constructed. - `load_background` / `set_palette` statements and their `BackgroundDecl` / `PaletteDecl` parser support: parsed and silently dropped by IR lowering (`// TODO: implement in asset pipeline`). Removed keywords, AST variants, parser paths, analyzer arms, and tests. `docs/future-work.md` documents the runtime palette/nametable design for when it comes back. Doc cleanup: - `docs/architecture.md` was describing files that don't exist (`analyzer/types.rs`, `optimizer/const_fold.rs`, `codegen/regalloc.rs`, `rom/header.rs`, `debug/symbols.rs`, …). Rewrote it to match the real flat `mod.rs` + `tests.rs` layout and the real pipeline order. - `docs/future-work.md` was a hybrid of open work and "recently completed" entries that duplicated the active stubs at the top of the file. Collapsed to just the gaps that are actually still open. - `README.md` claimed Mesen symbol export and 210 tests; updated both. - `docs/language-guide.md` and `spec.md` described `palette` decls, `set_palette` / `load_background`, `debug.overlay`, and error codes that were never emitted. Trimmed. - Stale comments on `Statement::Play`/`StartMusic`/`StopMusic` claimed the audio subsystem was "a no-op at codegen time". Tests: - Regression tests for every fix above (`lower_shift_left_with_literal _count_uses_that_count`, `lower_shift_right_with_variable_count _uses_runtime_variant`, `lower_divide_emits_div_op_not_load_imm _zero`, `lower_modulo_emits_mod_op_not_load_imm_zero`, `strength_reduce_div_by_power_of_two`, `strength_reduce_mod_by _power_of_two`, `strength_reduce_shift_var_with_constant_amount`). - Renamed the `program_with_sprites_and_palette` integration test (which was exercising the now-removed `load_background`/`set_palette`) to `program_with_inline_sprite_chr`. `examples/sprites_and_palettes.ne` lost its `palette`/`set_palette` usage. Nothing in the emulator test presses A, so the headless jsnes render shouldn't move, but the golden may need regeneration via `UPDATE_GOLDENS=1` if it does. https://claude.ai/code/session_012fKB251HvEUQwG3tizFyqt
2026-04-13 02:47:37 +00:00
| `sprites_and_palettes.ne` | sprites, scroll, cast | Inline CHR data, PPU scroll writes, type casting |
| `mmc1_banked.ne` | MMC1, banks, multiply | Banked mapper with software multiply |
codegen: user code in switchable banks via cross-bank trampolines Adds a `bank Foo { fun bar() { ... } }` parser form so user functions can opt into living in a switchable PRG bank instead of the fixed bank, plus the IR codegen, runtime, and linker work to make calls across the bank boundary actually run. Programs that don't use the new syntax produce byte-identical ROMs to before — verified by rebuilding every existing example and diffing. Pipeline shape: * Parser accepts both `bank Foo: prg` (legacy reserved slot) and `bank Foo { fun ... }` (functions land in the named bank). Nested functions get tagged `bank: Some("Foo")` on the FunDecl + IrFunction. * Analyzer bumps the user zero-page start past `$10` whenever the program declares any banked function, so `__bank_select`'s STA into ZP_BANK_CURRENT can't clobber a user variable. Programs without banked functions keep the legacy `$10` start. * IrCodeGen emits each banked function into its own per-bank instruction stream (`banked_streams: HashMap<String, Vec<Instruction>>`) while the fixed-bank stream gets the dispatcher loop + state handlers + top-level functions, exactly like before. Cross-bank calls from the fixed bank rewrite `JSR __ir_fn_<name>` to `JSR __tramp_<name>`; in-bank calls stay direct. Banked → fixed calls are direct (the fixed bank is always mapped at $C000-$FFFF). Banked → other-banked calls aren't supported in this pass and panic loudly during codegen. * Runtime's `gen_bank_trampoline` takes the trampoline label and entry label as parameters now (one trampoline per banked function, not one per bank) so the linker can request any number of stubs. * Linker assembles banked banks twice: a discovery pass to learn each bank's labels, then a final pass that seeds the merged label table so banked code can JSR into the fixed bank's runtime helpers (math, audio, etc.). The fixed-bank assembler is also seeded with the cross-bank labels so the trampolines' `JSR __ir_fn_<name>` resolves into the bank's $8000 window. New `asm::assemble_with_labels` / `asm::assemble_discover_labels` helpers wire this up. * PrgBank carries `Vec<Instruction>` + a list of `BankTrampoline` requests now, replacing the old `data: Vec<u8>` + single `entry_label: Option<String>` shape. The compiler populates both from the codegen output; the linker's two-pass assembly handles the rest. New example: `examples/uxrom_user_banked.ne` puts a sprite-stepping helper inside `bank Extras { fun step_animation() { ... } }`. The fixed-bank state handler calls it via the generated trampoline, and the harness golden locks in pixel + audio output at frame 180. UxROM is the only mapper exercised by the new example. MMC1 and MMC3 also work through the same path (the linker emits the right mapper-specific bank-select code), but no example uses them yet — the existing `mmc1_banked.ne` / `mmc3_per_state_split.ne` keep their fixed-bank-only layout. Limitations carried forward: * No banked → banked cross-bank calls (panics in codegen). * No greedy size-packing; placement is explicit-only. * MMC3 state handlers don't get banked (the per-state split path is untouched).
2026-04-14 11:41:20 +00:00
| `uxrom_user_banked.ne` | UxROM, `bank Foo { fun ... }`, cross-bank trampoline | First example to put real user code inside a switchable bank. The animation step lives in `bank Extras` and is invoked from the fixed-bank state handler via a generated `__tramp_step_animation` stub that selects bank 0, JSRs the body, then restores the fixed bank before returning. |
codegen: support banked → banked cross-bank function calls Programs that put functions in switchable banks can now call across bank boundaries — `bank A { fun step() { helper() } }` where `helper` lives in `bank B` used to panic in the IR codegen. Three small pieces unblock it: 1. **Generic trampoline.** `runtime/gen_bank_trampoline` no longer takes a `fixed_bank_index` argument. Instead it reads the caller's current bank from `ZP_BANK_CURRENT`, pushes it on the hardware stack, switches to the target, JSRs the entry, then pulls and restores the saved bank. The same per-callee stub works for fixed→banked and banked→banked direction; nested trampolines compose because each PHA/PLA pair sits inside its own JSR/RTS frame. `gen_mapper_init` seeds `ZP_BANK_CURRENT` with the fixed bank index for any banked mapper so the very first cross-bank call from the fixed bank still restores to the fixed bank (matching pre-banked-banked semantics). 2. **Codegen drops the panic.** The `Some(from), Some(to)` arm in the call-resolution switch now emits `JSR __tramp_<name>` like the fixed→banked case instead of panicking. Banked→fixed calls still go direct (the fixed bank is always mapped at $C000). 3. **Bank-namespaced local labels.** Two banks emitting the same `__ir_cmp_e_8` would trip the linker's discovery-pass duplicate- label check the moment any banked code generated a comparison. The new `local_label_suffix` helper prefixes the suffix with the current bank name when banked code is being emitted, leaving fixed-bank label generation untouched (so existing examples are byte-identical apart from the trampoline / init bytes themselves). The new `examples/uxrom_banked_to_banked.ne` demonstrates the path end-to-end: `bank Logic { fun step() { ... clamp() } }` calls `bank Helpers { fun clamp() { ... } }` once per frame. The harness golden is committed alongside it. The five existing banked example ROMs change byte-for-byte because of the new trampoline shape and the seed-ZP_BANK_CURRENT init, but their emulator goldens still match exactly — observable behaviour is unchanged. https://claude.ai/code/session_01KEczoNUX3WmcFLfq6iAQxB
2026-04-15 02:37:19 +00:00
| `uxrom_banked_to_banked.ne` | UxROM, banked → banked cross-bank call | Two `bank Foo { fun ... }` blocks: `step` lives in bank Logic and calls `clamp` in bank Helpers. The trampoline uses `ZP_BANK_CURRENT + PHA/PLA` to save and restore the caller's bank, so the same per-callee stub works whether the caller is in the fixed bank or another switchable bank. |
palette/background: first-class declarations with reset-time load and runtime swaps Re-adds `palette Name { colors: [...] }` and `background Name { tiles: [...], attributes: [...] }` as first-class declarations, plus `set_palette Name` and `load_background Name` statements for runtime swaps. Unlike the previous iteration that quietly no-op'd, this one is fully wired through the pipeline and its behavior is pinned by both unit tests and an emulator golden. Pipeline: - Lexer: re-adds `palette`, `background`, `set_palette`, `load_background` keywords and tokenizes them. - AST: `PaletteDecl` (name + 1..=32 colour bytes) and `BackgroundDecl` (name + 0..=960 tile bytes + 0..=64 attribute bytes) live in `Program`. `Statement::SetPalette` and `Statement::LoadBackground` name-reference these declarations. - Parser: `palette Name { colors: [...] }` / `background Name { tiles: [...], attributes: [...] }` blocks and their statement forms parse via the existing byte-array helper. - Analyzer: validates colour indices ($00-$3F), palette length (<=32), nametable length (<=960), attribute length (<=64), and duplicate decl names. `set_palette` / `load_background` targets must reference a declared name (E0502 otherwise). When a program declares palette or background, the analyzer bumps the user zero-page allocator's starting address from `$10` to `$18` to reserve `$11-$17` for the runtime update handshake — programs that don't use the feature keep the old layout so their emulator goldens stay byte-exact. - Assets: `PaletteData` and `BackgroundData` resolve declarations into zero-padded fixed-size blobs (32 / 960 / 64 bytes) and expose `label()` / `tiles_label()` / `attrs_label()` for codegen to reference. - IR: new `IrOp::SetPalette(String)` and `IrOp::LoadBackground(String)`; lowering forwards the names verbatim. - Codegen: `gen_set_palette` writes the palette label pointer into ZP `$12/$13` and ORs bit 0 into the update flags at `$11`; `gen_load_background` does the same for tile/attribute pointers at `$14/$15/$16/$17` with bit 1. Both emit a `__ppu_update_used` marker so the linker splices in the NMI apply helper only when the feature is actually used. - Runtime: `gen_initial_palette_load` and `gen_initial_background_load` write the first declared palette/background at reset time (before rendering is enabled, where PPU writes are safe). `gen_nmi(has_ppu_updates)` takes a new flag; when true it splices `gen_ppu_update_apply` at the top of the NMI body, which checks the `$11` flags byte and copies pending palette / nametable data to `$3F00` / `$2000` inside vblank. All helpers use only ZP $02/$03 as scratch at reset time and never clobber ZP slots live across NMI. - Linker: new `link_banked_with_ppu` takes slice of `PaletteData` / `BackgroundData`; splices each blob as a labelled data block in PRG ROM, picks the first-declared as the reset-time load target, enables background rendering automatically when a background is declared, and threads `has_ppu_updates` into `gen_nmi`. Old `link_banked` remains as a thin wrapper for callers without palette/background data so existing tests don't shift. Tests: - Lexer: tokenization of the 4 new keywords (single added test case). - Parser: 5 new tests for `palette` / `background` decls with and without attributes, plus `set_palette` / `load_background` statements. - Analyzer: 9 new tests covering acceptance of declared palettes/backgrounds, E0502 for unknown names, E0201 for out-of-range NES colors and oversized blobs, E0501 for duplicate names, and the zero-page-layout guard (palette/bg decls bump ZP start; no decls keeps it at $10). - Resolver: 3 new tests for zero-padding, truncation of oversized decls, and label derivation. - IR: 2 new lowering tests for `set_palette` and `load_background`. - Integration: 5 new tests — blob contents spliced verbatim into PRG, `STA $12` / `STA $14` emitted by set_palette / load_background codegen, and a regression guard that programs without palette/background still land user vars at $10. - Emulator: new `examples/palette_and_background.ne` driven by a frame counter that toggles between `CoolBlues` / `WarmReds` and `TitleScreen` / `StageOne` every 90 frames. Golden PNG and audio hash checked in under `tests/emulator/goldens/` and verified via `node run_examples.mjs` — rendered image shows the blue `CoolBlues` palette with the nametable populated from `TitleScreen`. Docs: - `README.md` adds the feature to the headline list and the example table. - `docs/language-guide.md` restores the palette/background sections with the full 32-byte layout table and `set_palette` / `load_background` statement references. - `docs/future-work.md` replaces the "removed as dead code" entry with the remaining gaps (PNG-sourced palette and nametable assets, cross-vblank large background updates, memory-map reporting). - `spec.md` restores the grammar productions and usage examples. - `examples/README.md` lists the new demo. All 497 unit + integration tests pass. Clippy clean. All 21 emulator goldens match after the update pass. https://claude.ai/code/session_012fKB251HvEUQwG3tizFyqt
2026-04-13 11:11:33 +00:00
| `palette_and_background.ne` | palette, background, set_palette, load_background | Reset-time initial load plus vblank-safe runtime swaps |
assets: auto-generate CHR data from @nametable() PNG sources `background Foo @nametable("file.png")` previously decoded the PNG into a tile-index table and an attribute table but left CHR generation to the user — they had to supply matching tiles via a separate `sprite Tileset @chr(...)` declaration in the same deduplication order, which was both error-prone and the main thing keeping the shortcut form from being a one-liner. The CHR pipeline now closes the gap. `png_to_nametable_with_chr` returns a `PngNametable` carrying the tile-index table, the attribute table, *and* a per-tile CHR blob encoded with the same brightness-bucketing `png_to_chr` already uses for sprites. The resolver passes `next_sprite_tile` (computed from the resolved sprite list) so each background's CHR allocation slots in immediately after the sprite range, and rewrites the nametable indices to point at the actual physical tile numbers. The linker copies each background's `chr_bytes` into CHR ROM at `chr_base_tile * 16`, so the final image renders without any user-supplied CHR. `BackgroundData` carries `chr_bytes` and `chr_base_tile` so the linker has everything it needs at a glance. Inline `tiles:` / `attributes:` declarations leave them empty and behave exactly like before — that path doesn't auto-generate CHR because the user is implicitly opting into "I'll provide tiles myself" by typing the indices out by hand. The new `examples/auto_chr_background.ne` is a 256×240 grayscale gradient committed alongside its `auto_chr_bg.png` source; the emulator harness verifies the rendered output against a committed golden so a regression in the dedupe/encode/linker plumbing fails CI loudly. Existing example ROMs are byte- identical because their backgrounds either have no PNG source or already provided their own CHR. https://claude.ai/code/session_01KEczoNUX3WmcFLfq6iAQxB
2026-04-15 03:29:58 +00:00
| `auto_chr_background.ne` | `background @nametable(...)` with auto-CHR | First example to use the `@nametable("file.png")` shortcut without supplying any matching CHR data. The resolver dedupes the PNG's 8×8 cells, encodes them via the same brightness-bucketing the sprite CHR encoder uses, and slots them into CHR ROM at the next free tile slot. The committed `auto_chr_bg.png` is a 256×240 grayscale gradient that exercises ~50 unique tiles. |
| `friendly_assets.ne` | named colours, grouped palette, pixel art, tilemap+legend, palette_map, scalar sfx pitch, note-name music | Exercises every "friendlier" asset syntax at once — the `palette` uses `bg0..sp3` + a shared `universal:`, the sprite is authored as ASCII pixel art, the background uses a `legend { ... } + map:` tilemap with a `palette_map:` for attributes, the sfx uses a scalar `pitch:` + `envelope:` alias, and the music uses note names (`C4, E4 40, rest 10`) with a `tempo:` default. |
| `noise_triangle_sfx.ne` | `channel: noise`, `channel: triangle` on `sfx` blocks | Demonstrates the noise and triangle sfx channels. Declares one noise burst and one triangle bass note, plays each on a timer so the emulator harness captures both the pixel output and the APU state. |
audio: per-frame pitch envelopes for pulse SFX Pulse-channel sfx with a multi-byte `pitch:` array used to silently ignore everything past the first byte — the runtime audio tick latched the period at trigger time and never updated it. Programs that wanted a frequency sweep had no way to express it. The compiler now compiles a per-frame pitch envelope blob alongside the existing volume envelope when `decl.pitch` has more than one distinct value. The blob is padded (or truncated) to the volume envelope's length and ends in a zero sentinel so the runtime walker stops both pointers on the same NMI. Sfx with a single scalar pitch (or an array where every byte is the same) keep their historical "no pitch blob, latch once" path and emit byte-identical ROM bytes. The runtime gains two new pieces, both gated on a new `__sfx_pitch_used` codegen marker so programs without varying-pitch sfx pay zero bytes: 1. `gen_audio_tick` emits a per-frame pitch update block inside the SFX tick: read a byte through `(AUDIO_SFX_PITCH_PTR),Y`, write it to `$4002` (pulse-1 period low), advance the pointer. The block bails on a zero high-byte pointer so a single program can mix scalar-pitch and varying-pitch sfx without one clobbering the other. 2. `emit_play_pulse` seeds `AUDIO_SFX_PITCH_PTR_LO/HI` with the pitch-blob label for varying-pitch sfx and zeros it for scalar-pitch sfx. The per-call branch is skipped entirely when the program has no varying-pitch sfx anywhere. The new `examples/sfx_pitch_envelope.ne` exercises the path with a 16-frame siren sweep. Triangle and noise per-frame pitch are deferred — they share the same data shape but the runtime ticks for those channels still write only their volume registers, see docs/future-work.md for the gap. https://claude.ai/code/session_01KEczoNUX3WmcFLfq6iAQxB
2026-04-15 02:54:56 +00:00
| `sfx_pitch_envelope.ne` | varying-pitch pulse SFX | A 16-frame frequency sweep written as a per-frame `pitch:` array on a Pulse-1 sfx. The compiler emits a separate `__sfx_pitch_<name>` blob and gates the audio tick's pitch update path on the `__sfx_pitch_used` marker, so programs that stick to the scalar `pitch:` form still get byte-identical ROM output. |
parser/lowering: declarative metasprites for multi-tile sprite groups Multi-tile sprites used to require one hand-written `draw` per tile, e.g. the four-call sequence in `examples/platformer.ne`'s `draw_player()`. The new `metasprite Name { ... }` declaration collects parallel `dx`/`dy`/`frame` arrays plus a reference to the underlying sprite, and `draw Name at: (x, y)` expands to one OAM slot per tile in the IR lowering — the codegen sees N regular DrawSprite ops, so the runtime OAM cursor allocator picks them up without any metasprite-specific awareness. The metasprite's `frame:` array is interpreted *relative to the underlying sprite's base tile*: index 0 means "the first tile this sprite owns", which is the natural reading for a 16×16 hero whose pixel art the asset resolver split into four consecutive tiles. The lowering walks `program.sprites` to compute base tile indices the same way `assets::resolve_sprites` would, then folds the base into each frame entry before storing the metasprite info. Sprites sourced from external `@chr(...)` / `@binary(...)` files whose bytes aren't available at parse time fall back to a one-tile assumption — those programs are rare and can declare metasprites against pixel-art sprites instead. The new `examples/metasprite_demo.ne` declares a 16×16 hero sprite and arranges its four tiles into a metasprite, then sweeps the hero across the screen so the harness captures it mid-motion. The new keyword is added to the lexer/token list, and the parser accepts `sprite:` (the otherwise-keyword) as a property name in metasprite bodies so the natural spelling parses. https://claude.ai/code/session_01KEczoNUX3WmcFLfq6iAQxB
2026-04-15 03:13:30 +00:00
| `metasprite_demo.ne` | declarative multi-tile sprites | A 16×16 hero sprite split into a `metasprite Hero { sprite: Hero16, dx: [...], dy: [...], frame: [...] }` declaration. `draw Hero at: (px, py)` then expands to one `DrawSprite` op per tile in the IR lowering, each with its dx/dy added to the user's anchor point and the frame offset by the underlying sprite's base tile. The codegen needs no metasprite-specific support — it sees N regular draws and the OAM cursor allocator handles the slots. |
analyzer/lowering: support nested struct fields and array struct fields Struct field types beyond the v1 scalar set (`u8`, `i8`, `u16`, `bool`) used to error out with `E0201: struct fields must be u8/i8/u16/bool`. The size accumulator already handled them correctly — what was missing was: (1) the analyzer side that synthesizes per-leaf symbols and allocations for nested structs plus a single array-typed symbol for array fields, (2) the parser's chained-field-access path, and (3) the IR-lowering recursion through nested struct literal initializers and array literal field values. The synthetic-variable model carries through unchanged: a `var p: Player` where `Player { pos: Vec2, hp: u8, inv: u8[4] }` and `Vec2 { x: u8, y: u8 }` produces flat allocations for `p.pos.x`, `p.pos.y`, `p.hp`, and `p.inv`, plus an intermediate `p.pos` Struct symbol so dotted-name lookups still resolve. Array fields get a single allocation with the array type so the existing `Expr::ArrayIndex` lowering path handles `p.inv[i]` without changes. Array-of-structs is still rejected with E0201 because the synthetic model can't index per-element layouts without further codegen work. The parser change is the only structural move: `parse_primary` and `parse_assign_or_call` now loop the dot chain into a single joined identifier so `p.pos.x` becomes `FieldAccess("p.pos", "x")` and `p.inv[0]` becomes `ArrayIndex("p.inv", 0)`. The downstream analyzer and IR lowering use the same `format!("{name}.{field}")` join they already used for one-level access — no plumbing changes required. Includes a new `examples/nested_structs.ne` that exercises both features end-to-end with two `Hero` instances carrying nested positions and inventory arrays. The reproducibility tripwire ROM is committed alongside it and the emulator harness has a matching pair of golden files. https://claude.ai/code/session_01KEczoNUX3WmcFLfq6iAQxB
2026-04-15 02:19:49 +00:00
| `nested_structs.ne` | nested struct fields, array struct fields, chained literals | Two `Hero` instances each carry a `Vec2` position and a `u8[4]` inventory. Exercises `hero.pos.x` chained access, `hero.inv[i]` array-field access, and chained struct-literal initializers (`Hero { pos: Vec2 { x: ..., y: ... }, inv: [...] }`). |
feat(platformer): add stomp-or-die enemy collisions, live HUD, GameOver state The previous platformer example drew enemies but had almost no interaction with them: only enemy 1 had a stomp check, the stomp window was unreachable under the default +1-px-per-frame-plus-a- jump-every-40-frames autopilot, contact from any other angle was a silent no-op, and the header comment promised a "title → playing → game-over state machine" that didn't actually exist. The README demo gif and the committed golden both froze that state — a level the player could walk through indefinitely with no consequence. Flesh the enemy interaction model out into something real: - `resolve_enemy_hit(e_sx)`: one helper, called symmetrically for both enemies. Computes the player/enemy hitbox overlap (horizontal in `e_sx ∈ (72, 96)`, vertical in `player_y ∈ (152, 176)`) and branches three ways — falling onto the head is a stomp bounce (`rise_count = 6`, `fall_vy = 0`, `stomp_count += 1`, `play Boing`); overlap while `rise_count > 0` is a grace pass-through so the stomp bounce itself can't retrigger contact on the same enemy; anything else (walking into the side, standing on the ground against the enemy) is fatal — `alive = 0` and `play hit`. - New `GameOver` state: draws four enemy tiles across the middle of the screen plus a coin row sized to `stomp_count`, stops the music, lingers 60 frames then auto-retries, and also honours Start for an instant retry. - Proximity-based autopilot: pre-jump when an enemy is exactly 19 px ahead (`e1_sx == 99` or `e2_sx == 99`), capped at two jumps per life by `auto_jumps < AUTOPILOT_JUMPS`. Tuning: a JUMP_RISE=12, GRAVITY_CAP=4 jump lands the player's feet at enemy-head height exactly 21 frames after lift-off, by which point the autopilot camera has scrolled the enemy under the player. The first jump fires on Playing frame 1 and stomps enemy 1 on frame 22; the second fires on Playing frame 101 and stomps enemy 2 on frame 122. After that the autopilot is exhausted and the third enemy encounter (camera wraps back past enemy 1) is fatal — the golden harness now sees the full stomp, stomp, die, retry, stomp loop instead of a frozen walk. - Live HUD: up to four coin sprites in the top-left, one per stomp, rendered both during `Playing` and on the `GameOver` screen so the score is visible in the death frame. `Playing`'s player draw is now guarded by `if alive == 1` so the hero disappears on the fatal-contact frame and the enemy that killed them is visible underneath. Verified with a per-frame ZP trace through the patched puppeteer + jsnes harness: first stomp at emu frame 44 (camera_x=22), second at emu frame 144 (camera_x=122), death at emu frame 283 (camera_x=5 after a 256-px wrap), `Playing` restart at emu frame 343, third stomp at emu frame 365. All 22 emulator goldens still match after the update, and `docs/platformer.gif` regenerated from the new ROM now shows two clean stomps, a clean side-collision death, the GameOver screen, and the retry cycle all inside the 6-second demo window. Golden updates: - `tests/emulator/goldens/platformer.png` — the frame-180 capture now shows the hero walking forward with a two-coin HUD after both autopilot stomps (previously: a frozen bouncing hero). - `tests/emulator/goldens/platformer.audio.hash` — the track now includes two `Boing` stomp bounces, which shifts the hash. - `examples/platformer.nes` — rebuilt from the rewritten source. Also updates the platformer rows in `README.md` and `examples/README.md` to match the new gameplay. https://claude.ai/code/session_013Bi4H4YQ5or5HtMB4doUFi
2026-04-13 20:19:28 +00:00
| `platformer.ne` | **every subsystem** | End-to-end side-scrolling demo: custom CHR tileset, full 32×30 nametable with per-region attribute palettes, 2×2 metasprite hero with gravity/jump physics, wrap-around horizontal scrolling, stomp-or-die enemy collisions with a live stomp-count HUD, coin pickups, user-declared SFX + music, and a Title → Playing → GameOver state machine with a proximity-based autopilot so the headless harness cycles through stomp, stomp, die, and retry inside six seconds. Regenerate the tile art with `cargo run --bin gen_platformer_tiles`. |
sprite-per-scanline: add cycle_sprites runtime flicker + debug telemetry W0109 (shipped last commit) catches the 8-sprites-per-scanline hardware limit at compile time for static layouts, but the dynamic case — enemy formations, projectile clusters, animated NPCs where coordinates come from variables — was still silent. This change adds two layers of defense on top of W0109: Layer 2: `cycle_sprites` runtime flicker intrinsic New keyword statement that rotates the OAM DMA start offset one slot per call. When called once per `on frame`, the PPU's sprite evaluation picks up a different subset of the 12+ overlapping sprites each frame, so the permanent-dropout failure mode becomes visible flicker — the classic NES technique used by Gradius, Battletoads, and every shmup. Implementation: - Lexer keyword `KwCycleSprites` and parser production. - AST `Statement::CycleSprites(Span)`. - `IrOp::CycleSprites` lowered by the IR pass. - Codegen emits `LDA $07EF / CLC / ADC #4 / STA $07EF` with natural u8 wrap, plus a one-shot `__sprite_cycle_used` marker label the first time it fires. - Linker detects the marker and switches `gen_nmi` to the cycling variant, which reads the rotating offset from `$07EF` into OAM_ADDR before the DMA instead of writing a literal 0. Programs that don't call `cycle_sprites` skip the marker and get byte-identical ROM output. Layer 3: debug-mode sprite overflow telemetry Mirrors the frame-overrun pair (`debug.frame_overrun_count` / `debug.frame_overran`). In debug builds the NMI handler reads `$2002` at the top of vblank, masks bit 5 (the PPU's sprite overflow flag), and if set bumps a cumulative counter at `$07FD` plus a sticky bit at `$07FC`. The sticky bit clears on every `wait_frame`. New debug builtins: - `debug.sprite_overflow_count()` → u8 peek of $07FD - `debug.sprite_overflow()` → u8 peek of $07FC (sticky bit) The hardware flag has well-known quirks but is correct for the overwhelming majority of cases and costs ~15 cycles per frame to sample. Release builds emit no overflow-check code at all, so the four bytes at `$07EF` / `$07FC`-`$07FD` stay free for user allocation. Related changes: - `gen_nmi` now takes an `NmiOptions` struct. Four bool parameters tripped clippy's `fn_params_excessive_bools`. - CLI `build` now renders analyzer warnings on a successful build. Previously warnings were silently dropped unless the user also ran `nescript check`, which made W0109 effectively invisible to CI and local dev alike. Existing pre-existing W0103 / W0106 warnings on `coin_cavern`, `mmc3_per_state_split`, `sprites_and_palettes` surface too — not regressions, just now visible. New example: `examples/sprite_flicker_demo.ne` Draws 12 sprites into a 4-pixel band, W0109 fires at compile time with nine labels pointing at the offenders, and a `cycle_sprites` call at the end of `on frame` turns the hardware dropout into flicker. The committed emulator golden captures one frame of the cycling pattern (deterministic). Tests: - `runtime::tests::nmi_debug_mode_samples_sprite_overflow` - `runtime::tests::nmi_sprite_cycle_variant_reads_rotating_offset` - `ir_codegen::*::debug_sprite_overflow_count_loads_07fd` - `ir_codegen::*::debug_sprite_overflow_flag_loads_07fc` - `ir_codegen::*::wait_frame_clears_sprite_overflow_sticky_in_debug_mode` - `ir_codegen::*::wait_frame_release_does_not_touch_sprite_overflow_sticky` - `ir_codegen::*::cycle_sprites_emits_marker_and_add4` - `ir_codegen::*::cycle_sprites_marker_dedup_across_multiple_calls` - `ir_codegen::*::program_without_cycle_sprites_emits_no_marker` - `analyzer::*::accepts_debug_sprite_overflow_builtins` - `analyzer::*::rejects_unknown_debug_method_lists_all_four_known_names` - `analyzer::*::accepts_cycle_sprites_statement` Docs: `examples/war/COMPILER_BUGS.md` §4 now describes all three layers (W0109, `cycle_sprites`, debug telemetry) with reasoning for when each applies. `README.md` and `examples/README.md` add the new example to their tables. All 32 emulator goldens still match — the cycling is opt-in and programs that don't call `cycle_sprites` or enable debug mode are byte-identical to the pre-change output. https://claude.ai/code/session_0143dTgh3UeRrtfHgQwzcv5z
2026-04-15 22:07:19 +00:00
| `sprite_flicker_demo.ne` | `cycle_sprites`, 8-per-scanline hardware limit | Twelve sprites packed onto the same 4-pixel band — two more than the NES's 8-sprites-per-scanline hardware budget. The W0109 analyzer warning fires at compile time, and a `cycle_sprites` call at the end of `on frame` rotates the OAM DMA offset one slot per frame so the PPU drops a *different* sprite each frame. The permanent-dropout failure mode becomes visible flicker, which the eye reconstructs across frames. The classic NES technique used by Gradius, Battletoads, and every shmup that ever existed. |
W0110 inline fallback warning + docs refresh W0110: when a function marked `inline` has a body shape the IR lowerer can't splice (conditional early return, loops, nested control flow, empty void body), the analyzer now emits a warning at the declaration site so the declined hint is visible instead of silently falling back to a regular JSR. Implementation: - New `W0110` error code in `src/errors/diagnostic.rs` (warning level). - New `pub fn can_inline_fun(return_type, body) -> bool` in `src/ir/lowering.rs`, extracted from the existing capture logic so the analyzer and the IR lowerer share the same eligibility rules and can never drift. - New `check_inline_declinability` analyzer pass called from the tail of `analyze_program`, mirroring the existing `check_sprite_scanline_budget` / `check_unreachable_states` passes. Emits W0110 with help + note text pointing at the two accepted body shapes. - `capture_inline_bodies` now defers to `can_inline_fun` instead of duplicating the match pattern, so the two sides stay in lockstep by construction. Four regression tests in `src/analyzer/tests.rs` cover the conditional-return and while-loop declines plus the two accepted shapes (single-return expression, void sequence). Example source cleanups: `wrap52` in `examples/war/deck.ne` and `abs_diff` in both `examples/arrays_and_functions.ne` and `examples/loop_break_continue.ne` drop the `inline` keyword. All three were dead hints — the `inline` was being silently declined before this change, so removing it is source-only; the three ROMs are byte-identical, all 32 emulator goldens still match. Docs refresh - `docs/language-guide.md`: rewrote the Inline Functions section (real behaviour + W0110), added W0105/W0106/W0107/W0108/W0109/ W0110 to the warnings table, added the `debug.sprite_overflow*` builtins + sprite-per-scanline mitigations section to the Debug Mode docs, added a `cycle_sprites` statement entry and cross-referenced it from `draw`. - `docs/nes-reference.md`: fleshed out the "NEScript Memory Usage" block with the full ZP + high-RAM layout, including the new `$07EF` / `$07FC` / `$07FD` slots for sprite cycling and the debug sprite-overflow telemetry. - `docs/future-work.md`: documented all four debug query builtins in the "What ships today" block; updated the open "OAM allocation strategy" question to reference the shipped `cycle_sprites` path and ask about an automatic-flicker game attribute as a follow-up. - `docs/architecture.md`: updated the `ir/` and `optimizer/` module summaries to describe real inline splicing (now in lowering, not the optimizer). - `README.md`: reframed the `inline` bullet from "hint" to "real splicing for single-return / void-body shapes"; expanded the debug-support bullet to mention the four query builtins and their stripping in release builds; added a new bullet for the three-layer sprite-per-scanline mitigations; bumped the test count from 497 → 694; updated the war.ne entry to mention the seven compiler bugs are all fixed and point readers at `git log` (instead of the deleted COMPILER_BUGS.md). - `examples/README.md`: same `git log`-pointing rewrite for the war.ne entry. Deletions - `examples/war/COMPILER_BUGS.md` is removed. All seven catalogued bugs are fixed; the file's historical value lives in `git log` now. Every source-code comment and doc reference to the file has been updated to either point at `git log` or just describe the bug in place. Test count: 616 unit + 75 integration + 3 doctests = 694 total. Clippy / fmt clean. 32/32 emulator goldens match. https://claude.ai/code/session_0143dTgh3UeRrtfHgQwzcv5z
2026-04-15 23:19:07 +00:00
| `war.ne` | **production-quality card game**, multi-file source layout | A complete port of the card game War, split across `examples/war/*.ne` files and pulled in via `include` directives. Title screen with a 0/1/2-player menu (cursor sprite, blinking PRESS A, brisk 4/4 march on pulse 2), a 50-frame deal animation, a deep `Playing` state with an inner phase machine (`P_WAIT_A`/`P_FLY_A`/.../`P_WAR_BANNER`/`P_WAR_BURY`/`P_CHECK`), card-conserving queue-based decks built on a 200-iteration random-swap shuffle, a "WAR!" tie-break that buries 3+1 face-down cards per player and plays a noise-channel thump per bury, and a victory screen with the builtin fanfare. The first NEScript example to use a top-level file as a thin shell that `include`s ~12 component files; building it surfaced seven compiler bugs across the analyzer, IR lowerer, and codegen that were all fixed on the same branch (see `git log` for details). |
examples/pong: production-quality Pong game with powerups and multi-ball A complete, playable Pong game split across examples/pong/*.ne files and pulled in from a top-level examples/pong.ne. Features: - **Title screen** with a 3-option menu (CPU VS CPU / 1 PLAYER / 2 PLAYERS), a cursor sprite, blinking "PRESS A" prompt, brisk title march on pulse 2, and autopilot that auto-confirms CPU VS CPU after 45 frames of no input so the headless jsnes golden harness reaches gameplay by frame 180. - **Ball physics** with signed-magnitude velocity (u8 magnitude + sign bit per axis), wall bounce at top/bottom, paddle AABB collision with push-out, and score-out detection at left/right exits. - **Multi-ball** via parallel ball_* arrays (MAX_BALLS = 3). Each ball scores a point independently; the round continues until the last ball exits the playfield. - **CPU AI** that tracks the nearest active ball heading toward its side with a per-frame step, 4 px dead zone, and CPU_SPEED = 1 so rallies can end naturally. - **Three powerup types** that spawn every ~4 seconds, bounce off all four walls, and are caught by paddle AABB overlap: 1. LONG — extends the catching paddle from 24 → 40 px for 5 hits 2. FAST — doubles ball x-velocity on the catcher's next hit 3. MULTI — spawns two extra balls on the catcher's next hit - **Victory** at first-to-7 with a "PLAYER N WINS" banner and the builtin fanfare, auto-returning to Title. - **Audio**: 5 user-declared sfx (WallBounce, PaddleHit, Score, PowerSpawn, PowerCatch) plus a title march and the builtin fanfare for victory. Source layout mirrors examples/war: examples/pong.ne top-level game shell examples/pong/PLAN.md living design doc examples/pong/constants.ne layout + gameplay constants examples/pong/assets.ne 45-tile Tileset (paddles, ball, alphabet, digits, cursor, center-line, powerup icons) examples/pong/audio.ne sfx + music declarations examples/pong/state.ne all mutable globals examples/pong/rng.ne 8-bit Galois LFSR examples/pong/render.ne draw helpers examples/pong/input.ne paddle step (human + CPU AI) examples/pong/ball.ne multi-ball physics + paddle collision examples/pong/powerup.ne powerup entity (spawn, bounce, catch, apply) examples/pong/title_state.ne state Title + menu examples/pong/play_state.ne state Playing (P_SERVE/P_PLAY/P_POINT) examples/pong/victory_state.ne state Victory Verification: - 616 compiler unit tests pass (cargo test --all-targets) - cargo fmt / cargo clippy --all-targets -- -D warnings clean - 33/33 emulator harness goldens match - examples/pong.nes builds byte-identically from source https://claude.ai/code/session_0134F5uwDEVTes2Ee9S7JeXy
2026-04-16 01:25:29 +00:00
| `pong.ne` | **production-quality Pong**, powerups, multi-ball, multi-file | A complete Pong game split across `examples/pong/*.ne`. CPU VS CPU / 1 PLAYER / 2 PLAYERS title menu with brisk pulse-2 title march and autopilot, smooth ball physics with wall and paddle bouncing, CPU AI that tracks the ball with a reaction lag and dead zone, three powerup types (LONG paddle for 5 hits, FAST ball on next hit, MULTI-ball on next hit spawning 3 balls) that bounce around the field and are caught by paddle AABB overlap, multi-ball scoring (each ball scores a point, round continues until last ball exits), inner phase machine (`P_SERVE`/`P_PLAY`/`P_POINT`), and a "PLAYER N WINS" victory screen with the builtin fanfare. First-to-7 wins. |
examples: add feature_canary that turns red on any memory silent-drop regression Phase 5 of the post-PR-#31 audit, and the structural piece that closes the failure mode the earlier phases couldn't fix alone. The audit's recurring diagnosis: pixel/audio goldens capture *whatever* the program does, not what it *should* do. A silent drop in codegen is still deterministic — the golden locks in the broken behaviour and every future run agrees with it. That's how state-locals, uninitialized struct-field writes, `on exit` handlers, and `slow` placement each sat broken for months-to-a- year in a green CI. The canary inverts the relationship: the committed golden is a solid-green universal backdrop that only appears when every round-trip check passes. Each check writes a distinctive constant through one language construct, reads it back, and clears `all_ok` on mismatch. A final `if all_ok == 0 { set_palette Fail }` flips the entire screen red for the rest of the run. Checks cover the silent-drop shapes caught by this audit: - state-local variable write-read (PR #31) - uninitialized struct-field write-read (caught by phase 1) - u8 / u16 globals (u16 exercises both StoreVar + StoreVarHi) - array-element write at nonzero index - `slow`-placed global still round-trips - function call return value The canary doesn't use `debug.assert` on purpose — debug-only ops get stripped in release and the emulator harness runs release builds. The palette swap works in release and is what the harness pixel-diff sees. ### Why this matters as a long-lived test The harness already had 34 pixel goldens covering full-program behaviour, but none of them exist specifically to fail if a *specific language feature* silently drops. The canary does. Every silent-drop bug the audit found would have flipped it red the moment the check was added, which is the "behaviour assertion that can't be satisfied by silence" the plan called for. ### Harness footprint `tests/emulator/goldens/feature_canary.{png,audio.hash}` + `examples/feature_canary.{ne,nes}`. 35/35 ROMs match their goldens with the canary added. Listed in both README tables. https://claude.ai/code/session_01AoQ678uVeqpyayvWHpfDhC
2026-04-18 00:14:40 +00:00
| `feature_canary.ne` | **regression canary**, state-locals, uninitialized struct-field writes, u16, arrays, `slow` placement, function returns | A minimal program whose sole job is to paint a green universal backdrop at frame 180 when every memory-affecting language construct round-trips a write through the compiler correctly, and to flip to red if any check fails. Each check writes a distinctive byte through one construct (state-local, uninit struct field, u8/u16 global, array element, `slow`-placed u8, function call return), reads it back, and clears `all_ok` on mismatch. Because the emulator harness compares pixels at frame 180, any compiler regression that silently drops one of these writes turns the committed golden red — the structural counter to the "goldens capture whatever happens, not what should happen" failure mode that let PR #31 survive for a year. |
examples/sha256: interactive SHA-256 hasher with on-screen keyboard An end-to-end FIPS 180-4 SHA-256 hasher running entirely on the NES. The player types up to 16 ASCII characters on a 5x8 on-screen keyboard, presses Enter, and the program computes and displays the 64-character hex digest. Layout (`examples/sha256/*.ne`): constants.ne layout + K[64] / H_INIT[8] tables (declared as `var` with init_array because the v0.1 compiler treats `const u8[N] = [...]` as a no-op — noted in the file) assets.ne 44-tile Tileset (A..Z, 0..9, punctuation, special keys, cursor) shared between BG and sprite layers background.ne static nametable (title, labels, keyboard grid) painted at reset state.ne globals sha_core.ne 32-bit byte primitives (copy, xor, and, add, not, rotr, shr) in inline asm + sigma/Sigma mixers + schedule/round steps + fold render.ne OAM helpers for cursor, input buffer, and 64-nibble digest keyboard.ne key dispatch table entering_state.ne cursor navigation + typing + auto-demo computing_state.ne phased driver (48 schedule steps + 64 rounds + fold across ~30 frames at 4 iterations each) showing_state.ne renders the 256-bit digest as 8 rows of 8 sprite glyphs Implementation notes: - All 32-bit words live as 4 little-endian bytes in `wk[64]`, `w[256]`, `h_state[32]` so every primitive walks four bytes with `LDA {arr},X`/`STA {arr},X` chains and, for adds, a carry chain. - Every primitive reads its parameters straight out of the transport slots `$04`/`$05` rather than `{dst}`/`{src}` substitutions: the inline-asm resolver looks parameters up in the analyzer's allocation table but the codegen spills them to a different per-function RAM slot, so `{dst}` would resolve to a ZP slot nothing ever writes to. Bypassing the substitution entirely sidesteps the issue without a compiler change. - Rotate-right by any amount is a byte-rotate loop plus a bit- rotate loop so the 10 SHA amounts (2, 6, 7, 11, 13, 17, 18, 19, 22, 25) all compile to a handful of chained `ROR`s. - The headless jsnes golden auto-types "NES" after 1 s of idle and captures its SHA-256 digest AE9145DB5CABC41FE34B54E34AF8881F462362EA20FD8F861B26532FFBB84E0D — byte-identical to `shasum` / `hashlib.sha256(b"NES")`. Build: `cargo run --release -- build examples/sha256.ne` https://claude.ai/code/session_01FRmSBruVWCufm3LsUVMs8v
2026-04-16 14:02:58 +00:00
| `sha256.ne` | **interactive SHA-256**, inline-asm 32-bit primitives, multi-file | A full FIPS 180-4 SHA-256 hasher split across `examples/sha256/*.ne`. An on-screen 5×8 keyboard grid lets the player type up to 16 ASCII characters (`A`..`Z`, `0`..`9`, space, `.`, backspace, enter), and pressing ↵ runs the 48-entry message-schedule expansion + 64-round compression on the NES itself. Every 32-bit primitive (`copy`, `xor`, `and`, `add`, `not`, rotate-right, shift-right) is hand-tuned inline assembly that walks the four little-endian bytes of a word with `LDA {wk},X` / `ADC {wk},Y` chains, so a whole round costs a few thousand cycles. The phased driver runs four schedule steps or four rounds per frame so the full compression finishes well under a second, and the 64-character hex digest renders as sprites in 8 rows of 8 glyphs at the bottom of the screen. The jsnes golden auto-types `"NES"` after 1 s of keyboard idle and captures its hash `AE9145DB5CABC41FE34B54E34AF8881F462362EA20FD8F861B26532FFBB84E0D`. |
compiler: PRNG / edge input / palette fade / AxROM / CNROM / FCEUX labels Closes seven of the cc65/nesdoug parity gaps catalogued in docs/future-work.md in a single pass. All of the new features are gated on marker labels so programs that don't use them produce byte-identical ROM output (every pre-existing committed .nes file round-trips unchanged). Language / runtime additions: - `rand8()` / `rand16()` / `seed_rand(u16)` intrinsics backed by a 16-bit Galois LFSR (~30 bytes of runtime, ~40 cycles per draw). Reset path seeds state to 0xACE1 so the first draw is useful even without explicit seeding. - `p1.button.a.pressed` / `.released` edge-triggered input via a new ReadInputEdge IR op plus an NMI-side prev-frame snapshot into $07E6/$07E7, gated on the `__edge_input_used` marker. - `set_palette_brightness(level)` builtin mapping levels 0..8 to PPU mask emphasis bytes (`$2001`) for neslib-style screen fades. - `mapper: AxROM` (iNES 7) with automatic 32 KB PRG padding so emulators that enforce mapper-7's 32 KB page size boot cleanly. - `mapper: CNROM` (iNES 3) with a reset-time CHR bank 0 select. - `--fceux-labels <prefix>` CLI flag emitting per-bank `.nl` label files and a `.ram.nl` file for FCEUX's debugger. Tests + examples: - Five new example programs with committed .nes ROMs and pixel+audio goldens: prng_demo, edge_input_demo, palette_brightness_demo, axrom_simple, cnrom_simple. - Seven integration tests covering JSR emission, the omitted-when-unused invariant, the NMI prev-input snapshot, the correct mapper numbers for AxROM/CNROM, and negative tests for unknown button names and bad rand8 arity. - `is_intrinsic()` now runs in expression-position Call paths too, so `var x = rand8(1, 2)` errors at compile time instead of silently dropping the extra arguments.
2026-04-18 18:13:18 +00:00
| `prng_demo.ne` | `rand8()`, `rand16()`, `seed_rand()` | Exercises the runtime xorshift PRNG end-to-end. Four sprite positions are drawn from fresh `rand8()` draws every frame with a `rand16()` sample mixed in. `seed_rand(0x1234)` pins the initial state so the golden is deterministic. The `__rand_used` marker gates linking of `gen_prng` + the reset-time seed — programs that never call any of the three get zero ROM / cycle overhead. |
| `edge_input_demo.ne` | `p1.button.a.pressed`, `p1.button.b.released` | Demonstrates edge-triggered input. The A-sprite advances exactly once per press transition (holding the button does nothing) and the B-sprite advances on release. Lowering emits `IrOp::ReadInputEdge`, which stores the previous-frame input byte into main RAM and XORs it against the current byte at the read site. The NMI handler snapshots both prev bytes before strobing, gated on the `__edge_input_used` marker. |
| `palette_brightness_demo.ne` | `set_palette_brightness(level)` | Cycles through the 9 brightness levels (0 = blank, 4 = normal, 8 = max emphasis) every 20 frames. Exercises the neslib-style `pal_bright` mapping onto `$2001` PPU mask emphasis bits. The runtime routine `__set_palette_brightness` is spliced in only when user code references the builtin. |
| `axrom_simple.ne` | `mapper: AxROM` (mapper 7) | Single-screen AxROM demo. The linker pads PRG to 32 KB (one blank 16 KB bank plus our 16 KB fixed bank) so emulators that enforce mapper-7's 32 KB page size boot cleanly. Register layout: bit 4 of `$8000` selects single-screen lower / upper nametable. |
| `cnrom_simple.ne` | `mapper: CNROM` (mapper 3) | CNROM demo. Fixed 32 KB PRG, switchable 8 KB CHR. Single-bank CNROM is functionally equivalent to NROM at the PRG level, but the iNES header reports mapper 3 and the runtime writes a CHR bank 0 select at reset. |
compiler: GNROM / debug port / sprite flicker / fade / sprite-0 split + docs Another batch from the cc65/nesdoug gap catalogue. All six items gated on marker labels (or default-false attributes) so existing programs produce byte-identical ROMs — every pre-existing .nes file round-trips unchanged. **Language / runtime additions:** - `mapper: GNROM` (iNES 66). Combines AxROM's 32 KB PRG pages with CNROM's 8 KB CHR banks in a single `$8000` register. Linker pads single-page ROMs to 32 KB to match mapper-66 expectations. - `game { debug_port: fceux | mesen | 0xXXXX }`. `debug.log`, `debug.assert`, and the `__debug_halt` sentinel now target a user-selected address. `fceux` (default, $4800) and `mesen` ($4018) are named aliases; custom hex addresses are accepted for unusual debuggers. - `game { sprite_flicker: true }`. IR lowerer injects an `IrOp::CycleSprites` at the top of every `on frame` handler, which flips on the rotating-OAM NMI variant with no per-site boilerplate. Default false so existing ROMs keep their layout. - `fade_out(step_frames)` / `fade_in(step_frames)` builtins. Blocking helpers that walk brightness 4 → 0 or 0 → 4 with `step_frames` frames between each step. Runtime splices `__fade_out`, `__fade_in`, and a callable `__wait_frame_rt` helper when the builtin is used. Zero-guard on step_frames prevents a pathological 256-frame spin when the caller accidentally passes 0. - `sprite_0_split(scroll_x, scroll_y)` intrinsic. Emits a two-phase busy-wait on `$2002` bit 6 (wait-for-clear, wait-for-set) then writes the new scroll values to `$2005`. Works on any mapper — unlike `on_scanline(N)` which requires MMC3. Enables HUD-over-playfield scrolling on NROM/UxROM/MMC1. **Docs:** - New paragraph in the language guide explaining the no-recursion design choice and the explicit-stack workaround pattern. - `future-work.md` updated to mark the shipped items out of the catalogue; remaining items reshuffled in the priority ranking. - README + examples/README updated with the new mapper and builtins. **Tests:** - 12 new integration tests covering: GNROM header emission, debug-port targeting (fceux/mesen/custom), unknown-alias rejection, sprite_flicker on/off/bad-value, fade_out JSR + marker coupling, fade omitted-when-unused, fade-in-expression rejected, sprite_0_split byte-level busy-wait verification, sprite_0_split arity enforcement, sprite_0_split omitted-when-unused, and an extended void-intrinsic-in-expression-position test covering the three new void builtins. - `nes2_mapper_high_nibble_in_byte_8_is_zero_for_small_mappers` extended to include GNROM. - Four new examples with committed .nes ROMs + pixel/audio goldens: `gnrom_simple`, `auto_sprite_flicker`, `fade_demo`, `sprite_0_split_demo`. All 752 tests pass. Clippy clean. 44/44 emulator goldens match.
2026-04-18 19:31:55 +00:00
| `gnrom_simple.ne` | `mapper: GNROM` (mapper 66) | GNROM / MHROM demo. Combines AxROM-style 32 KB PRG pages with CNROM-style 8 KB CHR banks in a single `$8000` register (bits 4-5 select PRG, bits 0-1 select CHR). Like AxROM the linker pads single-page ROMs to 32 KB so emulators that enforce mapper-66's page size boot cleanly. |
| `auto_sprite_flicker.ne` | `game { sprite_flicker: true }` | The `game` attribute equivalent of calling `cycle_sprites` at the top of every `on frame` handler. Same 12-sprite layout as `sprite_flicker_demo.ne`, minus the explicit call — the IR lowerer injects the op automatically when the flag is set, so it's byte-identical to a hand-rolled version without the per-site boilerplate. |
| `fade_demo.ne` | `fade_out(n)`, `fade_in(n)` | Blocking fade helpers that walk brightness 4 → 0 and 0 → 4 with `n` frames per step. The runtime splices `__fade_out` / `__fade_in` plus a callable `__wait_frame_rt` helper when the builtin is used; fade use also forces `__set_palette_brightness` to be linked in since the fade body JSRs into it. |
| `sprite_0_split_demo.ne` | `sprite_0_split(x, y)` | Mid-frame scroll change driven by the PPU's sprite-0 hit flag (`$2002` bit 6), so the effect works on any mapper — NROM, UxROM, MMC1 — not just MMC3 via `on_scanline(N)`. Two-phase busy-wait (wait for clear, then wait for set) guarantees the hit we're responding to came from the current frame. Requires a sprite in OAM slot 0 that overlaps opaque background pixels; this demo uses a full smiley background so every frame's sprite-0 hit fires deterministically. |
| `i16_demo.ne` | `i16` signed 16-bit type | Negative literals fold to wide two's complement (`-10``$FFF6`), so `var vy: i16 = -10` stores the right bytes instead of the zero-extended `$00F6`. The companion `i16_negative_literal_sign_extends_to_wide_store` integration test guards the literal-fold path. |
| `signed_compare.ne` | signed `<` / `<=` / `>` / `>=` on `i8` and `i16` | Bounces a marker between X = 32 and X = 224 driven by signed `i16` compares against negative deltas, plus four pip sprites at the top of the screen that gate on directly-negative compares (`i8_neg < 0`, `i16_minus_one < i16_one`, etc.). The signed lowering uses the canonical `CMP / SBC / BVC / EOR #$80` overflow-correction idiom in `gen_cmp_signed_set_n` so the N flag reflects the true sign of the difference. The fourth pip is intentionally dark — it would only light if the lowering fell back to unsigned semantics. The companion integration tests `signed_i16_lt_emits_overflow_corrected_branch` and `signed_i8_lt_emits_overflow_corrected_branch` enforce the asm shape. |
compiler: i16 / SRAM saves / inline-asm dot labels / docs Another batch from the cc65/nesdoug catalogue. All gated on parser-level opt-in or default-false attributes so existing programs produce byte-identical ROMs (no committed .nes file changed). **§A — `i16` signed 16-bit type:** - New `KwI16` lexer token, `NesType::I16` AST variant, parser case in `parse_type`. Type-size and integer-type tables treat `i16` like `u16` (2 bytes, integer). - IR lowering accepts `i16` everywhere it accepts `u16` for wide-load / wide-store / widen-narrow paths. - New constant fold for `UnaryOp::Negate(IntLiteral(v))` that emits the wide two's-complement form. Without it, `var vy: i16 = -10` would zero-extend to `$00F6` (= 246) instead of sign-extending to `$FFF6` (= -10). Negative literals now store the right bytes. - Comparisons reuse the existing unsigned 16-bit compare ops (matching the existing `i8` behaviour). Documented in the `NesType::I16` doc comment and in `future-work.md` §A. - Example `examples/i16_demo.ne` with committed golden. - Tests cover the literal-fold sign-extension and end-to-end compile of the example. **§S — SRAM / battery-backed saves:** - New `save { var ... }` top-level block. Lexer + parser opt into a dedicated `KwSave` token. Analyzer allocates save vars from a separate `next_sram_addr` bump pointer starting at `$6000`, capped at `$8000` (8 KB cartridge SRAM window). - Linker reads `analysis.has_battery_saves` and flips iNES byte-6 bit-1 via the new `RomBuilder::set_battery` / `Linker::with_battery` chain. - New `W0111` warning for save-var initializers — SRAM is preserved across power cycles, so an init expression would either silently never run or clobber persisted data on every boot. The warning teaches the user about the magic-byte sentinel pattern. - Struct fields in save blocks are explicitly rejected for now (the field-flattening path uses the main-RAM allocator). - Example `examples/sram_demo.ne` with committed golden, plus 4 integration tests. **§D (partial) — inline-asm `.label:` syntax:** - Codegen-side mangler rewrites `.IDENT` → `__ilab_<N>_IDENT` per inline-asm block, where `<N>` is the call site's monotonic suffix. Two `asm { .loop: ... }` blocks in the same function now coexist without colliding in the linker's label table. - Bounds checks on `.` placement: `$2002` and `name.field` are unaffected; only `.IDENT` in label / branch context triggers the rewrite. Two integration tests pin the uniqueness and dollar-vs-dot disambiguation. **§X follow-up — Mesen trace-log docs:** - New "Debugger-assisted workflows" section in `docs/nes-reference.md` walking through the Mesen / FCEUX log workflows alongside the new `debug_port:` attribute. **Misc:** - `future-work.md` updated to mark the shipped items out of the catalogue and reshuffle the priority ranking. Remaining niche follow-ups (signedness on Cmp16, struct save fields, inline-asm format specifiers) documented inline so future passes know the design. All 757 tests pass. Clippy clean. 46/46 emulator goldens match.
2026-04-18 20:49:06 +00:00
| `sram_demo.ne` | `save { var ... }` | Battery-backed save block. The analyzer allocates `high_score` and `coins` at `$6000+` (cartridge SRAM window) instead of main RAM, and the linker flips iNES header byte-6 bit-1 so emulators (FCEUX, Mesen, Nestopia) load and persist the region from a `.sav` file alongside the ROM. SRAM is uninitialized at first power-on; production games should reserve a magic-byte sentinel and validate it before trusting the rest of the data — the compiler doesn't auto-initialize and emits W0111 if you try. |
docs + example: HUD demo and language-guide VRAM buffer section Follow-up to 807c9c7 (the VRAM update buffer core). Adds the realistic-HUD example the core was missing, plus a language-guide section that explains when and how to use the three buffer intrinsics. **examples/hud_demo.ne** A bouncing-ball playfield with a classic status bar across the top: - 5-cell lives indicator that ticks down once per second and resets at zero, drawn via `nt_fill_h` (plus a second `nt_fill_h` to erase the stale tail). - Score counter at the right edge that bumps on every wall bounce, drawn via `nt_set`. - One-shot `nt_attr` call on the first frame flipping the top-left metatile group to sub-palette 1 (the red HUD palette) so the UI chrome reads as distinct from the playfield. The demo's point is the `last_score != score` / `last_lives != lives` shadow-compare pattern: on the ~58-of-60 frames where nothing changed, the buffer stays empty and drain work is zero. That's the whole reason the VRAM buffer exists — per-frame cost scales with what moved, not with HUD complexity. Committed `.nes` + pixel/audio goldens. **docs/language-guide.md** New "VRAM Update Buffer" section between "Hardware Intrinsics" and "Inline Assembly". Covers: - Why user code can't just poke `$2006` / `$2007` directly. - The three intrinsics + their coordinate systems (cell, not pixel). - The HUD pattern with a ready-to-paste code snippet and a pointer at `examples/hud_demo.ne`. - A per-entry budget table + worked 1000-cycle drain example against the ~2273-cycle vblank budget. - Known limits: horizontal-only, no overflow check, no coalescing — all already tracked under `future-work.md` §G. **examples/README.md** `vram_buffer_demo.ne` reframed as the minimal test-case exercise it actually is, with a pointer at `hud_demo.ne` for the realistic pattern. New table row for `hud_demo.ne`. All 758 tests pass. Clippy clean. 48/48 emulator goldens match.
2026-04-18 21:34:44 +00:00
| `vram_buffer_demo.ne` | `nt_set`, `nt_attr`, `nt_fill_h` | Minimal VRAM update buffer exercise — three single-tile writes, a 16-tile horizontal fill, and an attribute write firing every frame. Useful as a test case; see `hud_demo.ne` for a realistic usage pattern. |
| `hud_demo.ne` | VRAM buffer driving a classic status bar | A bouncing ball playfield with a HUD across the top: a 5-cell lives indicator that ticks down once per second via `nt_fill_h`, a score counter at the right edge that bumps on every wall hit via `nt_set`, and a one-shot `nt_attr` call at startup that flips the top-left metatile group to a red "UI chrome" palette. Shadow-comparing `score` / `lives` to their `last_*` copies keeps the buffer empty on the ~58-of-60 frames when nothing changed — per-frame cost scales with what actually moved. This is the pattern every nesdoug scoreboard / dialog box / destroyed-metatile animation is built on. |
## Emulator Controls
| NES Button | Typical Key |
|------------|-------------|
| D-pad | Arrow keys |
| A | Z |
| B | X |
| Start | Enter |
| Select | Right Shift |
## About Sprites
Sprite names in `draw Player at: (x, y)` are parsed and recorded in the AST.
You can define sprites with inline CHR tile data:
```
sprite Player {
chr: [0x3C, 0x42, 0x81, 0x81, 0x81, 0x81, 0x42, 0x3C,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]
}
```
If no matching sprite declaration exists, the draw uses the built-in default
tile (a smiley face). See `sprites_and_palettes.ne` for a full example.
## Compiler Commands
```bash
# Compile to ROM
cargo run -- build game.ne
# Custom output path
cargo run -- build game.ne --output my_game.nes
# Type-check only
cargo run -- check game.ne
# View generated 6502 assembly
cargo run -- build game.ne --asm-dump
# Debug mode
cargo run -- build game.ne --debug
```