1
0
Fork 0
mirror of https://github.com/imjasonh/nescript synced 2026-07-08 17:06:04 +00:00
No description
Find a file
Claude 688d9afcec
platformer: end-to-end side-scroller demo + three runtime bug fixes
Adds `examples/platformer.ne`, a full side-scrolling game that
exercises nearly every subsystem of the compiler in one program:
custom CHR tileset, 32×30 background nametable with per-region
attribute palettes, 2×2 metasprite hero with gravity/jump physics,
wrap-around horizontal scrolling, moving enemies, coin pickups,
user-declared SFX + music, and a Title → Playing state machine
with autopilot so the headless jsnes harness captures real
gameplay at frame 180. Tile art + nametable are generated by
`scripts/gen_platformer_tiles.rs` (`cargo run --bin gen_platformer_tiles`).

Building this out uncovered three independent runtime bugs that
together made the example render as black-on-black smileys. All
three are fixed in this commit:

1. **`gen_init` enabled sprite rendering before the linker's
   initial palette/background load runs.** The PPU's v-register
   auto-increments on every `$2007` write *during active
   rendering*, so the palette load (32 B) and nametable load
   (1024 B) were scrambled past the first ~72 bytes — every
   existing program with a `background Level { ... }` block was
   silently rendering zero-filled VRAM. Fix: leave `PPU_MASK = 0`
   at the end of `gen_init` and emit a new `gen_enable_rendering`
   call *after* all initial VRAM writes complete.

2. **Audio tick corrupted `ZP_CURRENT_STATE`.** The audio
   driver's period-table lookup reused `$02/$03` as a temporary
   indirect pointer with a comment claiming the slots were free
   because the tick doesn't call mul/div. But `$03` is also
   `ZP_CURRENT_STATE` used by the state dispatch loop, so every
   music note silently overwrote the state index with the high
   byte of `__period_table` (`0xC5` in the platformer ROM),
   wedging the state machine forever. Fix: `gen_nmi` now PHAs
   `$02/$03` on entry and PLA-restores them on exit, and the
   audio tick JSR moves inside that save/restore window (it used
   to be spliced by the linker *before* the register saves, so
   even A/X/Y were technically being trashed pre-save). Only
   `audio_demo`'s audio hash shifts (its note timings move a few
   cycles); every other golden is unchanged.

3. **Sub-palette mirroring footgun.** Writing a 32-byte palette
   blob sequentially causes the sprite sub-palettes' "index 0"
   slots at `$3F10/$3F14/$3F18/$3F1C` to clobber the background
   universal colour at `$3F00/$3F04/$3F08/$3F0C` via NES hardware
   mirroring. The example's palette sets all eight first bytes
   to `$22` (sky blue) for this reason; `docs/future-work.md`
   picks up a TODO to warn on inconsistent first-byte values in
   the analyzer.

Also:

- `docs/platformer.gif` — 6-second recording of the example
  running in jsnes, generated by the new
  `tests/emulator/record_gif.mjs` puppeteer helper (encodes via
  `gifenc`, committed as a dev-dependency under
  `tests/emulator/package.json`).
- README / examples/README tables and the 497-test count are
  updated to cover the new example.

https://claude.ai/code/session_01BcCcHi6FUmTh8jC7UgkA3A
2026-04-13 13:04:26 +00:00
.github/workflows tests/emulator: byte-exact golden-image diffs 2026-04-12 21:30:18 +00:00
docs platformer: end-to-end side-scroller demo + three runtime bug fixes 2026-04-13 13:04:26 +00:00
examples platformer: end-to-end side-scroller demo + three runtime bug fixes 2026-04-13 13:04:26 +00:00
fuzz Add fuzz testing and parser edge case regression tests 2026-04-12 00:52:35 +00:00
scripts platformer: end-to-end side-scroller demo + three runtime bug fixes 2026-04-13 13:04:26 +00:00
src platformer: end-to-end side-scroller demo + three runtime bug fixes 2026-04-13 13:04:26 +00:00
tests platformer: end-to-end side-scroller demo + three runtime bug fixes 2026-04-13 13:04:26 +00:00
.gitignore Implement NEScript compiler Milestone 1 ("Hello Sprite") 2026-04-11 22:07:56 +00:00
Cargo.lock M3: Asset pipeline, sprite/palette/background declarations, debug symbols 2026-04-12 00:09:47 +00:00
Cargo.toml platformer: end-to-end side-scroller demo + three runtime bug fixes 2026-04-13 13:04:26 +00:00
CLAUDE.md docs: add CLAUDE.md documenting the jsnes harness and repo conventions 2026-04-13 11:38:54 +00:00
LICENSE Add README, LICENSE, examples, fix draw parser lookahead 2026-04-12 00:38:19 +00:00
plan.md docs: scrub remaining stale AST-codegen references 2026-04-12 20:53:36 +00:00
README.md platformer: end-to-end side-scroller demo + three runtime bug fixes 2026-04-13 13:04:26 +00:00
spec.md palette/background: first-class declarations with reset-time load and runtime swaps 2026-04-13 11:11:33 +00:00

NEScript

A statically-typed, compiled programming language for NES game development.

NEScript compiles .ne source files directly into playable iNES ROM files, with no external assembler or linker dependencies. The compiler handles everything from source text to a ROM you can run in any NES emulator.

Platformer demo

An end-to-end side-scrolling platformer compiled from a single .ne source file — custom CHR art, full background nametable with per-region palettes, physics-driven hero, scrolling camera, enemies, coins, user-declared SFX/music, and a Title → Playing state machine, all in 24 KB of 6502 code.

Quick Start

# Build the compiler
cargo build --release

# Compile an example
cargo run -- build examples/hello_sprite.ne

# Run the output ROM in an emulator
# (produces examples/hello_sprite.nes)

Hello World

game "Hello" {
    mapper: NROM
}

var px: u8 = 128
var py: u8 = 120

on frame {
    if button.right { px += 2 }
    if button.left  { px -= 2 }
    if button.down  { py += 2 }
    if button.up    { py -= 2 }

    draw Smiley at: (px, py)
}

start Main

Features

  • Game-aware syntax -- states, sprites, palettes, backgrounds, and input are first-class constructs
  • Full type system -- u8, i8, u16, bool, fixed-size arrays (u8[N]), enum, struct
  • Rich control flow -- if/else, while, for i in 0..N, loop, match
  • Functions -- with parameters, return types, inline hint, recursion detection
  • State machines -- state with on enter, on exit, on frame, on scanline(N) handlers
  • Compile-time safety -- call depth limits, recursion detection, type checking, unused-var warnings
  • IR-based optimizer -- constant folding, dead code elimination, strength reduction (incl. div/mod by power-of-two), copy propagation, peephole passes including INC/DEC fold and live-range slot recycling
  • Full 16-bit arithmetic -- u16 add/sub/compare lower to carry-propagating paired operations
  • Multiple mappers -- NROM, MMC1, UxROM, MMC3 (including multi-scanline IRQ dispatch per state)
  • Audio subsystem -- frame-walking pulse driver with user-declared sfx/music blocks, builtin effects and tracks, period table, and zero-cost elision when unused
  • Palette & background pipeline -- palette and background blocks, initial values loaded at reset, vblank-safe set_palette / load_background runtime swaps
  • Asset pipeline -- PNG-to-CHR conversion, inline tile data, sfx envelopes, music note streams
  • Inline assembly -- asm { ... } with {var} substitution, plus raw asm { ... } for verbatim blocks
  • Hardware intrinsics -- poke(addr, value) / peek(addr) for direct register access
  • Debug support -- --debug flag enables debug.log / debug.assert writes to the emulator debug port
  • Compile-time diagnostics -- --dump-ir, --memory-map, --call-graph flags
  • Single binary -- no dependencies on ca65, Python, or any external tools

Documentation

Examples

Example Features demonstrated
hello_sprite.ne D-pad input, sprite drawing
bouncing_ball.ne Automatic movement, edge detection
coin_cavern.ne Multi-state game, functions, constants, gravity
arrays_and_functions.ne Arrays, functions, while loops, inline functions
state_machine.ne State transitions, on enter/exit, timers
sprites_and_palettes.ne Inline CHR data, scroll, type casting
mmc1_banked.ne MMC1 mapper, bank declarations, multiply
palette_and_background.ne Palette and background declarations, reset-time load, vblank-safe set_palette / load_background swaps
structs_enums_for.ne Structs, enums, for loops, struct literals
inline_asm_demo.ne Inline asm with {var} substitution, poke/peek
audio_demo.ne Audio subsystem: user sfx/music blocks, builtin effects, play/start_music/stop_music
platformer.ne End-to-end side-scroller — custom CHR tileset, full background nametable, metasprite player with gravity/jump physics, wrap-around scrolling, enemies, coins, user-declared SFX + music, and a Title → Playing state machine with auto-play for the headless harness

Compiler Commands

# Compile to ROM
nescript build game.ne

# Compile with custom output path
nescript build game.ne --output my_game.nes

# Type-check only (no ROM output)
nescript check game.ne

# View generated 6502 assembly
nescript build game.ne --asm-dump

# Enable debug mode
nescript build game.ne --debug

Emulator Compatibility

Output ROMs are standard iNES format and work with any NES emulator:

Project Status

NEScript implements all five planned milestones:

Milestone Status Key Features
M1: Hello Sprite Done Full compiler pipeline, assembler, ROM builder
M2: Game Loop Done Functions, arrays, IR, optimizer, call graph analysis
M3: Asset Pipeline Done PNG-to-CHR, sprites, debug.log / debug.assert
M4: Optimization Done Strength reduction, ZP promotion, type casting, asm-dump
M5: Bank Switching Done MMC1/UxROM/MMC3, bank declarations, software mul/div

497 tests across the lexer, parser, analyzer, IR, optimizer, codegen, assembler, linker, runtime, ROM, and asset modules, with CI running fmt, clippy, test, and example compilation on every push.

License

MIT