mirror of
https://github.com/imjasonh/nescript
synced 2026-07-09 01:16:12 +00:00
No description
`const_fold_block`'s per-block dead-code pass was collecting temp usage from only the block it was folding, so a `LoadImm` whose destination is consumed by a *sibling* block (for example via the merge block's branch terminator) was incorrectly treated as dead and dropped. The `and` / `or` short-circuit lowering emits exactly that shape: the false path writes `LoadImm(result, 0)` and joins with the right path at an `and_end` / `or_end` block whose branch terminator reads `result`. After the DCE the false path's store was gone, leaving the zero-page result slot to carry whatever value the *previous* `and` / `or` evaluation had written there — stale data that bled into subsequent conditional branches. I found this while instrumenting `examples/platformer.ne` through a puppeteer-driven jsnes harness, stepping one frame at a time and snapshotting the full zero-page trace of each scenario (title-skip, hold-right, hold-left, jump-spam, coin-drift, enemy-stomp, long-run). In a clean idle run the enemy-1 stomp bounce (`rise_count = 6`, `fall_vy = 0`) fired at emulator frames 83 and 96 with `camera_x` = 61 and 74, i.e. with `e1_sx` = 39 and 26, nowhere near the intended `[72, 96)` pickup window. The trigger turned out to be the slot alias: every time `c2_sx` landed in its pickup window (so the coin-2 `and` stored 1 into ZP(130)) and the player was mid-fall at or past `player_y = 152`, the enemy-1 stomp `and` short-circuited to its false path, left ZP(130) at 1, and the stomp `if` fired on stale data. The fix is to compute function-wide source-operand usage once before folding each function's blocks and OR it into the per-block liveness check, so a LoadImm is only dropped if nobody — neither its own block nor any other block in the function — reads the temp. Added a regression test (`const_fold_preserves_loadimm_used_by_sibling_branch`) that builds the exact CFG shape the `and` lowering emits and verifies the false-path `LoadImm(result, 0)` survives optimization. Impact on the example ROMs: - `examples/platformer.nes`: enemy-1 stomp now fires only when `e1_sx ∈ [72, 96)`, as the source intends. The pixel golden is unchanged (`player_y` converges back to the ground line before frame 180), but the audio hash flips because the spurious `play hit` sfx calls during coin-2 passage are gone. Committing the new `tests/emulator/goldens/platformer.audio.hash`. - `examples/logic_ops.nes`, `examples/bitwise_ops.nes`, `examples/match_demo.nes`, `examples/mmc3_per_state_split.nes`, `examples/two_player.nes`: byte-different but observably unchanged — their pixel + audio goldens still match to the byte. They exercise `and` / `or` in the source and now compile through the corrected DCE. All other example ROMs are byte-identical to pre-fix. `cargo fmt`, `cargo clippy --all-targets`, `cargo test --release` (498 tests), and `tests/emulator/run_examples.mjs` (22/22 goldens) are clean. https://claude.ai/code/session_013Bi4H4YQ5or5HtMB4doUFi |
||
|---|---|---|
| .github/workflows | ||
| docs | ||
| examples | ||
| fuzz | ||
| scripts | ||
| src | ||
| tests | ||
| .gitignore | ||
| Cargo.lock | ||
| Cargo.toml | ||
| CLAUDE.md | ||
| LICENSE | ||
| plan.md | ||
| README.md | ||
| spec.md | ||
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.
Source: examples/platformer.ne
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,
inlinehint, recursion detection - State machines --
statewithon 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/musicblocks, builtin effects and tracks, period table, and zero-cost elision when unused - Palette & background pipeline --
paletteandbackgroundblocks, initial values loaded at reset, vblank-safeset_palette/load_backgroundruntime swaps - Asset pipeline -- PNG-to-CHR conversion, inline tile data, sfx envelopes, music note streams
- Inline assembly --
asm { ... }with{var}substitution, plusraw asm { ... }for verbatim blocks - Hardware intrinsics --
poke(addr, value)/peek(addr)for direct register access - Debug support --
--debugflag enablesdebug.log/debug.assertwrites to the emulator debug port - Compile-time diagnostics --
--dump-ir,--memory-map,--call-graphflags - Single binary -- no dependencies on ca65, Python, or any external tools
Documentation
- Language Guide -- complete reference for every language feature
- Architecture -- compiler internals and module overview
- NES Reference -- hardware quick reference for contributors
- Examples README -- how to build and run examples
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.
