1
0
Fork 0
mirror of https://github.com/imjasonh/nescript synced 2026-07-08 08:55:38 +00:00
No description
Find a file
Claude 9878b7d87d
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
.github/workflows docs: regenerate platformer.gif and lock it as a CI invariant 2026-04-13 18:04:17 +00:00
benches pipeline: share a single compile function across CLI, bench, and tests 2026-04-14 13:02:58 +00:00
docs docs: update future-work to reflect shipped items 2026-04-14 11:43:59 +00:00
examples audio: per-frame pitch envelopes for pulse SFX 2026-04-15 02:54:56 +00:00
fuzz Add fuzz testing and parser edge case regression tests 2026-04-12 00:52:35 +00:00
scripts Merge branch 'main' into claude/test-platformer-game-6S3TX 2026-04-13 15:55:50 -04:00
src audio: per-frame pitch envelopes for pulse SFX 2026-04-15 02:54:56 +00:00
tests audio: per-frame pitch envelopes for pulse SFX 2026-04-15 02:54:56 +00:00
.gitignore commit built ROMs alongside .ne sources 2026-04-13 15:12:39 +00:00
Cargo.lock tooling: add --no-opt CLI flag and criterion compile benchmarks 2026-04-14 01:43:51 +00:00
Cargo.toml tooling: add --no-opt CLI flag and criterion compile benchmarks 2026-04-14 01:43:51 +00:00
CLAUDE.md pipeline: share a single compile function across CLI, bench, and tests 2026-04-14 13:02:58 +00:00
LICENSE
plan.md docs: scrub remaining stale AST-codegen references 2026-04-12 20:53:36 +00:00
README.md audio: per-frame pitch envelopes for pulse SFX 2026-04-15 02:54:56 +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

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, 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
uxrom_user_banked.ne UxROM mapper with a bank Foo { fun ... } block — first example to put real user code in a switchable bank, called via a generated cross-bank trampoline
uxrom_banked_to_banked.ne UxROM with two bank Foo { fun ... } blocks — exercises a banked→banked call (step in Logic calls clamp in Helpers) routed through the same trampoline that handles fixed→banked
palette_and_background.ne Palette and background declarations, reset-time load, vblank-safe set_palette / load_background swaps
friendly_assets.ne Pleasant asset syntax — named NES colours, grouped bg0..sp3 palettes with universal:, ASCII pixel-art sprites, legend { } + map: tilemaps, palette_map: attribute grids, scalar sfx pitch:, note-name music with tempo:
structs_enums_for.ne Structs, enums, for loops, struct literals
nested_structs.ne Nested-struct fields (hero.pos.x) and array struct fields (hero.inv[0]) with chained literal initializers
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
noise_triangle_sfx.ne Noise and triangle channel sfx via channel: noise / channel: triangle on sfx blocks
sfx_pitch_envelope.ne Per-frame pulse pitch: arrays — the audio tick walks the pitch envelope in lockstep with the volume envelope and writes $4002 on every NMI for a frequency-sweeping siren tone
platformer.ne End-to-end side-scroller — custom CHR tileset, full background nametable, metasprite player with gravity/jump physics, wrap-around scrolling, stomp-or-die enemy collisions, live stomp-count HUD, pickup coins, user-declared SFX + music, and a Title → Playing → GameOver state machine with a proximity-based autopilot so the headless harness demonstrates the full gameplay loop (stomp, stomp, die, retry) inside six seconds

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