1
0
Fork 0
mirror of https://github.com/imjasonh/nescript synced 2026-07-08 08:55:38 +00:00
nescript/examples
Claude 48bae97c51
analyzer+codegen: turn silently-dropped feature paths into hard failures (or fix them)
Phase 2 of the post-PR-#31 audit. The codebase had four documented
"silently skip" paths that parsed user intent but produced no code.
Each one was the same shape as the state-local bug: the analyzer
accepted the program, the IR lowered the construct, but somewhere
downstream the emitted code was dropped on the floor — and a pixel
golden that captured the broken behaviour locked it in as correct.

Fix each per the plan, either by implementing the feature or
rejecting the program at the analyzer.

### on_exit handlers now actually run

`IrOp::Transition` used to comment "on_exit of the current state
isn't called here because we don't know from an IR op alone which
state we're leaving." The codegen emitted the exit handler's body
as an IR function but never JSR'd it. Three example programs
(pong, war, state_machine) relied on `stop_music` or mode-flag
translation inside `on exit` that had been silently never running.

Emit a small CMP-chain against `ZP_CURRENT_STATE` before each
transition: for every state that declares an on_exit, compare the
current index, branch past on miss, JSR the exit handler on match,
then JMP to the shared done-label so only the leaving state's
handler fires. The chain is inlined at each transition site
(bounded by the number of states declaring on_exit) rather than
factored into a single trampoline — simpler to reason about, and
transitions are rare enough that the extra bytes don't matter.

Pong / war / state_machine ROMs change because the dispatch code
is now emitted. Video goldens stay byte-identical (no transitions
happen within the 180-frame harness window under no-input). Pong
and war audio hashes shifted from pure code-layout timing and are
regenerated. `docs/pong.gif` and `docs/war.gif` are byte-identical.

### State-local array initializers now refuse to compile (E0601)

`src/ir/lowering.rs:887` had the comment "Array initializers for
state-locals aren't supported yet... Programs that try this should
get a diagnostic from the analyzer; for now, silently skip." The
analyzer never actually emitted that diagnostic. Verified by
compiling `state Main { var buf: u8[4] = [10,20,30,40] ... }`:
the program built a valid ROM with no trace of 10/20/30/40 in PRG.

Add E0601 to the analyzer's state-local pass. The IR lowerer's
defensive `continue` stays in place as a belt-and-braces guard.

### `on scanline` without MMC3 is now E0603

Previously E0203 ("invalid operation for type") which is a
miscategorisation — the feature is unsupported on the current
mapper, not a type error. Dedicated E0603 makes the future-work
shape explicit.

### `slow` variables now actually live outside zero page

`Placement::Slow` was parsed into the AST but `allocate_ram`
ignored it, so `slow var cold: u8` still landed in ZP like any
other u8. Wire `var.placement` through `allocate_ram_with_placement`
and skip the ZP branch when `Slow` is set. `Fast` remains
advisory (the existing default already prefers ZP for u8 vars),
validated by W0107.

### Other address-map silent drops hardened

Alongside the var_addrs hardening from phase 1, three `state_indices`
lookup sites that did `.copied().unwrap_or(0)` or silent `if let`
are now explicit panics: scanline IRQ dispatch, MMC3 reload, and
`IrOp::Transition`. A miss in any of them is a compiler bug, not
valid input — the analyzer catches unknown state names upstream.

### Regression guards

Four new tests would have failed against the old silently-dropping
code paths:

- `analyze_state_local_array_initializer_rejected` — expects E0601.
- `analyze_on_exit_declaration_accepted` — expects no errors.
- `analyze_slow_var_forced_out_of_zero_page` — expects alloc
  address >= $0100.
- `transition_dispatches_leaving_states_on_exit_handler` — counts
  distinct JSR targets in the PRG before/after adding `on exit` to
  a state; the exit-bearing build must have more.

All 720 tests pass. All 34 emulator goldens pass after the pong/war
audio hash refresh.

https://claude.ai/code/session_01AoQ678uVeqpyayvWHpfDhC
2026-04-18 00:06:39 +00:00
..
pong examples/pong: production-quality Pong game with powerups and multi-ball 2026-04-16 01:25:29 +00:00
sha256 sha256/computing: track byte offsets directly to skip per-iter shift 2026-04-16 17:17:10 +00:00
war W0110 inline fallback warning + docs refresh 2026-04-15 23:19:07 +00:00
arrays_and_functions.ne W0110 inline fallback warning + docs refresh 2026-04-15 23:19:07 +00:00
arrays_and_functions.nes codegen: emit gate markers at end of generate() to protect peephole 2026-04-16 21:31:47 +00:00
audio_demo.ne
audio_demo.nes codegen: emit gate markers at end of generate() to protect peephole 2026-04-16 21:31:47 +00:00
auto_chr_background.ne assets: auto-generate CHR data from @nametable() PNG sources 2026-04-15 03:29:58 +00:00
auto_chr_background.nes runtime: gate controller-1 reads, skip whole input block when unused 2026-04-16 21:15:09 +00:00
auto_chr_bg.png assets: auto-generate CHR data from @nametable() PNG sources 2026-04-15 03:29:58 +00:00
bitwise_ops.ne
bitwise_ops.nes codegen: emit gate markers at end of generate() to protect peephole 2026-04-16 21:31:47 +00:00
bouncing_ball.ne
bouncing_ball.nes codegen: emit gate markers at end of generate() to protect peephole 2026-04-16 21:31:47 +00:00
coin_cavern.ne examples: move state-scoped globals to state-local in coin_cavern + platformer 2026-04-17 11:58:02 +00:00
coin_cavern.nes examples: move state-scoped globals to state-local in coin_cavern + platformer 2026-04-17 11:58:02 +00:00
comparisons.ne
comparisons.nes codegen: emit gate markers at end of generate() to protect peephole 2026-04-16 21:31:47 +00:00
friendly_assets.ne
friendly_assets.nes codegen: emit gate markers at end of generate() to protect peephole 2026-04-16 21:31:47 +00:00
function_chain.ne
function_chain.nes codegen: emit gate markers at end of generate() to protect peephole 2026-04-16 21:31:47 +00:00
hello_sprite.ne
hello_sprite.nes codegen: emit gate markers at end of generate() to protect peephole 2026-04-16 21:31:47 +00:00
inline_asm_demo.ne
inline_asm_demo.nes codegen: emit gate markers at end of generate() to protect peephole 2026-04-16 21:31:47 +00:00
logic_ops.ne
logic_ops.nes codegen: emit gate markers at end of generate() to protect peephole 2026-04-16 21:31:47 +00:00
loop_break_continue.ne W0110 inline fallback warning + docs refresh 2026-04-15 23:19:07 +00:00
loop_break_continue.nes codegen: emit gate markers at end of generate() to protect peephole 2026-04-16 21:31:47 +00:00
match_demo.ne
match_demo.nes codegen: emit gate markers at end of generate() to protect peephole 2026-04-16 21:31:47 +00:00
metasprite_demo.ne parser/lowering: declarative metasprites for multi-tile sprite groups 2026-04-15 03:13:30 +00:00
metasprite_demo.nes codegen: emit gate markers at end of generate() to protect peephole 2026-04-16 21:31:47 +00:00
mmc1_banked.ne
mmc1_banked.nes codegen: emit gate markers at end of generate() to protect peephole 2026-04-16 21:31:47 +00:00
mmc3_per_state_split.ne
mmc3_per_state_split.nes codegen: emit gate markers at end of generate() to protect peephole 2026-04-16 21:31:47 +00:00
nested_structs.ne codereview: address six findings from a fresh review pass 2026-04-15 03:56:42 +00:00
nested_structs.nes codegen: emit gate markers at end of generate() to protect peephole 2026-04-16 21:31:47 +00:00
noise_triangle_sfx.ne
noise_triangle_sfx.nes codegen: emit gate markers at end of generate() to protect peephole 2026-04-16 21:31:47 +00:00
palette_and_background.ne
palette_and_background.nes codegen: emit gate markers at end of generate() to protect peephole 2026-04-16 21:31:47 +00:00
platformer.ne examples: move state-scoped globals to state-local in coin_cavern + platformer 2026-04-17 11:58:02 +00:00
platformer.nes examples: move state-scoped globals to state-local in coin_cavern + platformer 2026-04-17 11:58:02 +00:00
pong.ne examples/pong: production-quality Pong game with powerups and multi-ball 2026-04-16 01:25:29 +00:00
pong.nes analyzer+codegen: turn silently-dropped feature paths into hard failures (or fix them) 2026-04-18 00:06:39 +00:00
README.md examples/sha256: interactive SHA-256 hasher with on-screen keyboard 2026-04-16 14:02:58 +00:00
scanline_split.ne
scanline_split.nes codegen: emit gate markers at end of generate() to protect peephole 2026-04-16 21:31:47 +00:00
sfx_pitch_envelope.ne audio: per-frame pitch envelopes for pulse SFX 2026-04-15 02:54:56 +00:00
sfx_pitch_envelope.nes codegen: emit gate markers at end of generate() to protect peephole 2026-04-16 21:31:47 +00:00
sha256.ne examples/sha256: interactive SHA-256 hasher with on-screen keyboard 2026-04-16 14:02:58 +00:00
sha256.nes codegen: emit gate markers at end of generate() to protect peephole 2026-04-16 21:31:47 +00:00
sprite_flicker_demo.ne sprite-per-scanline: add cycle_sprites runtime flicker + debug telemetry 2026-04-15 22:07:19 +00:00
sprite_flicker_demo.nes codegen: emit gate markers at end of generate() to protect peephole 2026-04-16 21:31:47 +00:00
sprites_and_palettes.ne
sprites_and_palettes.nes codegen: emit gate markers at end of generate() to protect peephole 2026-04-16 21:31:47 +00:00
state_machine.ne
state_machine.nes analyzer+codegen: turn silently-dropped feature paths into hard failures (or fix them) 2026-04-18 00:06:39 +00:00
structs_enums_for.ne
structs_enums_for.nes codegen: emit gate markers at end of generate() to protect peephole 2026-04-16 21:31:47 +00:00
two_player.ne
two_player.nes codegen: emit gate markers at end of generate() to protect peephole 2026-04-16 21:31:47 +00:00
uxrom_banked.ne
uxrom_banked.nes codegen: emit gate markers at end of generate() to protect peephole 2026-04-16 21:31:47 +00:00
uxrom_banked_to_banked.ne codegen: support banked → banked cross-bank function calls 2026-04-15 02:37:19 +00:00
uxrom_banked_to_banked.nes codegen: emit gate markers at end of generate() to protect peephole 2026-04-16 21:31:47 +00:00
uxrom_user_banked.ne
uxrom_user_banked.nes codegen: emit gate markers at end of generate() to protect peephole 2026-04-16 21:31:47 +00:00
war.ne examples/war: working end-to-end War card game 2026-04-15 15:22:20 +00:00
war.nes analyzer+codegen: turn silently-dropped feature paths into hard failures (or fix them) 2026-04-18 00:06:39 +00:00

NEScript Examples

Quick Start

# 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, FCEUX, 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
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
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.
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_and_background.ne palette, background, set_palette, load_background Reset-time initial load plus vblank-safe runtime swaps
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.
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.
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.
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: [...] }).
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_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.
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 includes ~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).
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.
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.

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

# 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