Add comprehensive future work document
docs/future-work.md (347 lines) catalogs every known gap, incomplete
feature, and planned improvement with file:line references:
- IR-based codegen (IR exists but output uses AST codegen)
- 11 codegen gaps (calls, returns, transitions, arrays, mul/div, scroll)
- Sprite name resolution and multi-OAM support
- State machine dispatch (only start state generates code)
- Include directive, debug mode, scroll hardware
- Asset pipeline wiring (PNG conversion exists but unused)
- 10 unused error codes, missing validations
- Scanline IRQ, audio, inline assembly
- Language features: structs, enums, fixed-point, metasprites
- Missing shift-assign operators, P2 controller, register allocator
- WASM build target, compiler benchmarks
- Prioritized recommended order of work for contributors
https://claude.ai/code/session_01W6eQFStA66EuMKHUFo2rx3
2026-04-12 01:10:11 +00:00
|
|
|
|
# Future Work
|
|
|
|
|
|
|
|
|
|
|
|
This document catalogs known gaps, incomplete features, and planned improvements
|
|
|
|
|
|
in the NEScript compiler. Items are organized by priority and area.
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 1. IR-Based Code Generation
|
|
|
|
|
|
|
Remove the legacy AST codegen — IR path is canonical now
The `--use-ast` path through `src/codegen/mod.rs` was a strictly
inferior subset of the IR codegen. Building every example with
`--use-ast` through the jsnes harness:
- `arrays_and_functions` — fully black (array init + function
return values + OAM-in-loop all broken)
- `structs_enums_for` — fully black (struct literal is a no-op,
all fields stay at 0)
- `inline_asm_demo` — fully black
- `bitwise_ops`, `loop_break_continue` — below sprite floors
(static `next_oam_slot` bug B)
- `match_demo` — panics at compile time with
`branch offset 153 out of range` (AST's if/else-chain
desugaring of `match` emits short branches that can't reach
the far arms in a multi-arm match)
Six of fourteen examples are non-functional under `--use-ast`.
The other eight happen to fall inside the subset AST handles
(no arrays, no structs, no function return values, no
multi-sprite loops, no long match chains).
`docs/future-work.md` already listed "Once working, delete the
AST-based codegen entirely" as the intended direction. It's
working, so this commit does the deletion.
What's removed:
- The `CodeGen` struct, its impl block, and every helper in
`src/codegen/mod.rs` (the AST codegen body) — ~1150 lines.
The file is now a module header that re-exports `IrCodeGen`.
- `src/codegen/tests.rs` — 15 AST-specific instruction-pattern
tests. Every feature they covered has an equivalent test in
`src/codegen/ir_codegen.rs::{tests,more_tests}` already.
- The `--use-ast` CLI flag and its branch in `src/main.rs`.
- `compile_with_ir_codegen` in `tests/integration_test.rs` —
`compile()` now does what it did, so they merged. All 40
integration tests go through the IR path.
- Outdated sections in `docs/future-work.md` that described the
IR codegen as "not yet implemented" and listed AST codegen
gaps as priority work.
What's kept:
- `src/codegen/ir_codegen.rs` — the real codegen.
- `src/codegen/peephole.rs` — post-codegen cleanup pass, now
run unconditionally from `main.rs`.
Test plan:
- `cargo test --release` — 313 unit + 37 integration tests pass
(was 328 + 37; the 15 dropped are the deleted AST-specific
tests).
- `cargo fmt --check` clean.
- `cargo clippy --release --all-targets -- -D warnings` clean.
- `node tests/emulator/run_examples.mjs` — 14/14 ROMs render
above their per-example nonBlack floors.
- The one tightening: `sprite_resolution_uses_tile_index` was
asserting on the old static-slot encoding
(`A9 01 8D 01 02`). Updated to the cursor-based form
(`A9 01 99 01 02`, i.e. STA AbsoluteY).
Net diff: 1581 deletions, 62 insertions.
https://claude.ai/code/session_014Z5y3Q9krLcAxYpZQJhZ5V
2026-04-12 20:37:59 +00:00
|
|
|
|
**Status**: Complete. The AST → IR lowering, optimizer, and
|
|
|
|
|
|
`src/codegen/ir_codegen.rs` all work end-to-end; the legacy AST
|
|
|
|
|
|
codegen has been removed. See "Recently completed" below.
|
Add comprehensive future work document
docs/future-work.md (347 lines) catalogs every known gap, incomplete
feature, and planned improvement with file:line references:
- IR-based codegen (IR exists but output uses AST codegen)
- 11 codegen gaps (calls, returns, transitions, arrays, mul/div, scroll)
- Sprite name resolution and multi-OAM support
- State machine dispatch (only start state generates code)
- Include directive, debug mode, scroll hardware
- Asset pipeline wiring (PNG conversion exists but unused)
- 10 unused error codes, missing validations
- Scanline IRQ, audio, inline assembly
- Language features: structs, enums, fixed-point, metasprites
- Missing shift-assign operators, P2 controller, register allocator
- WASM build target, compiler benchmarks
- Prioritized recommended order of work for contributors
https://claude.ai/code/session_01W6eQFStA66EuMKHUFo2rx3
2026-04-12 01:10:11 +00:00
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 3. Sprite Name Resolution
|
|
|
|
|
|
|
|
|
|
|
|
**Status**: `draw SpriteName at: (x, y)` parses the sprite name but codegen
|
|
|
|
|
|
ignores it. All draws use OAM slot 0 with CHR tile index 0 (the built-in smiley).
|
|
|
|
|
|
|
|
|
|
|
|
**What's needed**:
|
|
|
|
|
|
- Track a mapping from sprite name → CHR tile index in the linker
|
|
|
|
|
|
- When a `sprite` declaration provides inline CHR data or `@chr("file.png")`,
|
|
|
|
|
|
write that data to the CHR ROM at a known tile index
|
|
|
|
|
|
- In codegen, look up the sprite name to get the tile index and write it to
|
|
|
|
|
|
OAM byte 1 (tile number)
|
|
|
|
|
|
- Support multiple OAM slots: track a `next_oam_slot` counter per frame,
|
|
|
|
|
|
allocate slots 0..63 as draws are emitted, warn if >64
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 4. Multi-OAM Sprite Support
|
|
|
|
|
|
|
|
|
|
|
|
**Status**: Every `draw` writes to the same OAM bytes ($0200-$0203). Only one
|
|
|
|
|
|
sprite is visible at a time.
|
|
|
|
|
|
|
|
|
|
|
|
**What's needed**:
|
|
|
|
|
|
- Frame-level OAM slot allocator: each `draw` gets the next available 4-byte slot
|
|
|
|
|
|
- Counter reset at the start of each frame handler
|
|
|
|
|
|
- Warn at compile time if a frame handler has >64 static draws
|
|
|
|
|
|
- Runtime: clear unused OAM slots to Y=$FE (off-screen) at frame start
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 5. State Machine Dispatch
|
|
|
|
|
|
|
|
|
|
|
|
**Status**: The codegen only generates code for the `start` state. Other states
|
|
|
|
|
|
are parsed and analyzed but their frame handlers are never called.
|
|
|
|
|
|
|
|
|
|
|
|
**What's needed**:
|
|
|
|
|
|
- A `current_state` zero-page variable holding the active state index
|
|
|
|
|
|
- A dispatch table at the start of the main loop: load `current_state`, branch to
|
|
|
|
|
|
the corresponding state's frame handler
|
|
|
|
|
|
- `transition StateName` writes the new state index to `current_state`
|
|
|
|
|
|
- Generate `on_enter` call on transition, `on_exit` call before leaving
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 6. Include Directive
|
|
|
|
|
|
|
|
|
|
|
|
**Status**: The `include` keyword is lexed (`KwInclude`) but not parsed or
|
|
|
|
|
|
implemented.
|
|
|
|
|
|
|
|
|
|
|
|
**What's needed**:
|
|
|
|
|
|
- Parser: `include "path.ne"` at top level
|
|
|
|
|
|
- File resolution: relative to the including file's directory
|
|
|
|
|
|
- Circular include detection (track include stack)
|
|
|
|
|
|
- Merge included declarations into the main `Program` AST
|
|
|
|
|
|
- Span tracking: included files need their own `file_id` for error messages
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 7. Debug Mode
|
|
|
|
|
|
|
|
|
|
|
|
**Status**: `--debug` flag is accepted by the CLI but has no effect.
|
|
|
|
|
|
|
|
|
|
|
|
**What's needed**:
|
|
|
|
|
|
- `debug.log(expr, ...)`: write values to emulator debug port ($4800)
|
|
|
|
|
|
- `debug.assert(expr)`: emit runtime check, halt on failure
|
|
|
|
|
|
- `debug.overlay(x, y, text)`: render text to a reserved nametable region
|
|
|
|
|
|
- Array bounds checking in debug mode (compare index against array size)
|
|
|
|
|
|
- Frame overrun detection: count cycles per frame, warn if approaching vblank
|
|
|
|
|
|
- All debug code stripped in release mode (already designed: `DebugLog`/`DebugAssert`
|
|
|
|
|
|
in the `Statement` enum are defined in the spec but not yet in the AST)
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 8. Scroll Hardware Writes
|
|
|
|
|
|
|
|
|
|
|
|
**Status**: `scroll(x, y)` is parsed but produces no output.
|
|
|
|
|
|
|
|
|
|
|
|
**What's needed**:
|
|
|
|
|
|
- Write X scroll value to PPU register $2005
|
|
|
|
|
|
- Write Y scroll value to PPU register $2005 (second write)
|
|
|
|
|
|
- Must happen during vblank (inside NMI handler or after `wait_frame`)
|
|
|
|
|
|
- Split-screen scroll requires MMC3 scanline IRQ (`on scanline`)
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 9. Asset Pipeline Completion
|
|
|
|
|
|
|
|
|
|
|
|
**Status**: PNG → CHR conversion exists (`src/assets/chr.rs`) but is never called
|
|
|
|
|
|
from the compilation pipeline.
|
|
|
|
|
|
|
|
|
|
|
|
### 9a. Wire `@chr("file.png")` to actual PNG loading
|
|
|
|
|
|
- When a sprite/background declares `chr: @chr("path.png")`, call `png_to_chr()`
|
|
|
|
|
|
during compilation
|
|
|
|
|
|
- Resolve the path relative to the source file
|
|
|
|
|
|
- Store resulting CHR data in the ROM's CHR section at a known tile index
|
|
|
|
|
|
|
|
|
|
|
|
### 9b. Wire `@binary("file.bin")` to raw file inclusion
|
|
|
|
|
|
- Read the file as raw bytes and include in CHR or PRG ROM
|
|
|
|
|
|
|
|
|
|
|
|
### 9c. Palette extraction from PNG
|
|
|
|
|
|
- `@palette("file.png")`: analyze image colors, map to nearest NES palette entries
|
|
|
|
|
|
- Already have `nearest_nes_color()` in `src/assets/palette.rs`
|
|
|
|
|
|
|
|
|
|
|
|
### 9d. Nametable conversion
|
|
|
|
|
|
- Full 256×240 PNG → 960-byte nametable + 64-byte attribute table
|
|
|
|
|
|
- Tile deduplication (max 256 unique tiles per pattern table)
|
|
|
|
|
|
- Error if >256 unique tiles
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 10. Error Message Polish
|
|
|
|
|
|
|
|
|
|
|
|
**Status**: Errors work and render with ariadne, but many error paths use generic
|
|
|
|
|
|
messages.
|
|
|
|
|
|
|
|
|
|
|
|
### Unused error codes
|
|
|
|
|
|
These are defined in `ErrorCode` but never emitted:
|
|
|
|
|
|
- `E0202` — invalid cast
|
|
|
|
|
|
- `E0203` — invalid operation for type
|
|
|
|
|
|
- `E0301` — zero-page overflow
|
|
|
|
|
|
- `E0403` — unreachable state
|
|
|
|
|
|
- `E0505` — multiple start declarations
|
|
|
|
|
|
- `W0101` — expensive multiply/divide operation
|
|
|
|
|
|
- `W0102` — loop without break or wait_frame
|
|
|
|
|
|
- `W0103` — unused variable
|
|
|
|
|
|
- `W0104` — unreachable code after return/break/transition
|
|
|
|
|
|
|
|
|
|
|
|
### Missing validations
|
|
|
|
|
|
- No error for assigning to a `const`
|
|
|
|
|
|
- No error for `break`/`continue` outside a loop
|
|
|
|
|
|
- No warning for variables declared but never read
|
|
|
|
|
|
- No error for `return` with wrong type vs function signature
|
|
|
|
|
|
- No error for calling a function with wrong argument count/types
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 11. Scanline IRQ (MMC3)
|
|
|
|
|
|
|
|
|
|
|
|
**Status**: `on scanline(N)` is in the spec and `on_scanline` field exists in
|
|
|
|
|
|
`StateDecl`, but parsing and codegen are not implemented.
|
|
|
|
|
|
|
|
|
|
|
|
**What's needed**:
|
|
|
|
|
|
- Parser: `on scanline(N) { ... }` event handler in state bodies
|
|
|
|
|
|
- MMC3 IRQ setup: write scanline counter to $C000/$C001/$E000/$E001
|
|
|
|
|
|
- IRQ handler generation: branch to the scanline handler code
|
|
|
|
|
|
- Only valid with `mapper: MMC3`
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 12. Audio
|
|
|
|
|
|
|
audio: complete the subsystem — asset pipeline, user decls, tracker-style driver
The audio subsystem was a sketch: `play name` / `start_music name` /
`stop_music` parsed, lowered, and emitted a few hardcoded register
writes from a builtin name table. No user-declared effects, no
per-frame envelope, no note streams, no real engine.
This flesh-out brings audio up to the quality bar of the rest of
the compiler (sprites, palettes, bank switching, scanline IRQ,
etc.) with a full data-driven pipeline:
## Asset pipeline (new `src/assets/audio.rs`)
- `sfx Name { duty, pitch, volume }` blocks compile into per-frame
pulse-1 envelopes. Pitch/volume arrays must match in length; each
entry is one NMI's worth of `$4000` data.
- `music Name { duty, volume, repeat, notes }` blocks compile into
flat `(pitch, duration)` streams for pulse 2. Pitch 0 is a rest,
1-60 indexes a builtin period table covering C1-B5.
- `resolve_sfx` / `resolve_music` walk the program for `play` /
`start_music` references and append builtin fallbacks for any
name that isn't user-declared — so `play coin` still works
without a `sfx Coin { ... }` block.
- Builtin effects (coin, jump, hit, click, cancel, shoot, step)
and tracks (theme, battle, victory, gameover) synthesize through
the same compile path as user decls — one data model, one driver.
## Runtime engine (`src/runtime/mod.rs`)
- `gen_audio_tick()` walks both channels every NMI: reads one
envelope byte through `(ZP_SFX_PTR),Y` -> writes `$4000`,
advances ptr, mutes on zero sentinel. Music decrements the note
counter, advances to the next `(pitch, dur)` pair on zero, looks
up the period through `(__period_table),Y`, loops on `0xFF 0xFF`.
- `gen_period_table()` emits a 60-entry equal-tempered table
(A4 = 440 Hz, NTSC 1.789773 MHz CPU clock) with length-counter
load bits pre-baked into each high byte.
- `gen_data_block()` emits a label + raw-bytes pseudo pair so
user sfx/music data can be spliced into PRG with regular labels
that the two-pass assembler resolves.
- New ZP layout: `$05/$06` music loop base, `$07` music state
(duty/volume/loop/active), `$0C-$0F` sfx and music pointers.
## IR codegen (`src/codegen/ir_codegen.rs`)
- `with_audio(sfx, music)` registers compile-time trigger constants
per blob name.
- `gen_play_sfx` emits: write period to `$4002`/`$4003`, load
envelope pointer into `ZP_SFX_PTR` via SymbolLo/SymbolHi of
`__sfx_<name>`, mark the sfx counter active.
- `gen_start_music` stamps the header byte into `ZP_MUSIC_STATE`
with the active bit OR'd in, seeds both ptr and loop base from
`__music_<name>`, primes the duration counter.
- `gen_stop_music` mutes pulse 2 and clears state.
## Linker (`src/linker/mod.rs`)
- New `link_with_all_assets(user_code, sprites, sfx, music)` path
that splices driver body, period table, and each sfx/music data
blob into PRG — all guarded on the `__audio_used` marker so
silent programs pay zero ROM cost.
## Assembler (`src/asm/opcodes.rs`, `src/asm/mod.rs`)
- New `AddressingMode::Bytes(Vec<u8>)` variant for raw-data
pseudo-instructions. `NOP+Bytes(v)` emits the payload verbatim,
letting the linker splice ROM data tables into a code section
and still have `Label` / `SymbolLo` / `SymbolHi` fixups resolve
correctly in the same assembly pass.
## Analyzer
- `play` / `start_music` now validate the name against user decls
and builtin tables. Unknown names emit E0505 with a helpful list
of builtins — previously a typo would silently compile to no-op.
## Parser
- New `sfx_decl` / `music_decl` grammar with property-style
configuration. Strict validation: duty 0-3, volume 0-15, pitch
arrays must match volume length, music notes must come in pairs,
pitch 0-60, duration ≥ 1.
## Tests
+170 new tests across every layer:
- `src/assets/audio.rs`: 17 tests (compile, resolve, builtins,
shadowing, label sanitation, nested reference walks)
- `src/parser/tests.rs`: 13 tests (valid/invalid sfx + music
declarations, property validation, play/start_music/stop_music)
- `src/analyzer/tests.rs`: 7 tests (builtin acceptance, user decl
acceptance, unknown-name rejection)
- `src/runtime/tests.rs`: 10 tests (audio tick labels, RTS end,
$4000 write, $4004 mute, period table assembly, A4 = 440 Hz,
length counter bits, data block verbatim emit)
- `src/linker/tests.rs`: 4 tests (sfx/music blob placement,
pointer resolution, elision when unused)
- `src/codegen/ir_codegen.rs`: rewrote the 4 existing audio tests
to match the new data-driven contract
- `tests/integration_test.rs`: 4 end-to-end tests including a
user-declared `sfx` + `music` program that verifies bytes land
in PRG ROM at the right addresses
## Docs
- New Audio section in `docs/language-guide.md` with syntax
reference, builtin tables, and an explanation of how the
driver works at compile and run time.
- `docs/architecture.md` updated to reflect the real audio
pipeline instead of the old "audio import stubs" stub.
- `docs/future-work.md` moves audio from "status: minimal" to
"status: full subsystem" with a narrower list of follow-up work
(triangle/noise/DMC channels, NSF/FTM imports, richer envelopes).
- `examples/audio_demo.ne` rewritten to showcase user-declared
`sfx LongCoin`, `sfx Zap`, `music Theme`, still demonstrating
builtin fallback via `play coin`.
Total: 424 tests passing (381 unit + 43 integration), clippy clean,
fmt clean, all 19 examples compile.
https://claude.ai/code/session_015WfaDttE3DpWn9rpyfpQd8
2026-04-13 01:10:21 +00:00
|
|
|
|
**Status**: A full data-driven audio subsystem is in place. User programs
|
|
|
|
|
|
can declare `sfx Name { duty, pitch, volume }` blocks (frame-accurate
|
|
|
|
|
|
pulse-1 envelopes) and `music Name { duty, volume, repeat, notes }`
|
|
|
|
|
|
blocks (pulse-2 note streams with rests and looping). The resolver
|
|
|
|
|
|
compiles these into ROM-ready byte tables; the IR codegen emits
|
|
|
|
|
|
trigger sequences that load pointers into ZP; the runtime NMI tick
|
|
|
|
|
|
walks the envelope and note stream each frame, indexing into a
|
|
|
|
|
|
builtin 60-note period table. Builtin effects (`coin`, `jump`, `hit`,
|
|
|
|
|
|
`click`, `cancel`, `shoot`, `step`) and tracks (`theme`, `battle`,
|
|
|
|
|
|
`victory`, `gameover`) are synthesized from the same data path so
|
|
|
|
|
|
programs that don't declare their own audio still make sound.
|
|
|
|
|
|
Programs that touch no audio pay zero ROM or cycle cost — the whole
|
|
|
|
|
|
subsystem elides when the `__audio_used` marker is absent.
|
|
|
|
|
|
|
|
|
|
|
|
**Still TODO for richer audio**:
|
|
|
|
|
|
- Triangle/noise/DMC channels (currently only pulse 1 and 2 are used)
|
|
|
|
|
|
- Multi-channel tracker playback (one `notes` list per channel)
|
Add comprehensive future work document
docs/future-work.md (347 lines) catalogs every known gap, incomplete
feature, and planned improvement with file:line references:
- IR-based codegen (IR exists but output uses AST codegen)
- 11 codegen gaps (calls, returns, transitions, arrays, mul/div, scroll)
- Sprite name resolution and multi-OAM support
- State machine dispatch (only start state generates code)
- Include directive, debug mode, scroll hardware
- Asset pipeline wiring (PNG conversion exists but unused)
- 10 unused error codes, missing validations
- Scanline IRQ, audio, inline assembly
- Language features: structs, enums, fixed-point, metasprites
- Missing shift-assign operators, P2 controller, register allocator
- WASM build target, compiler benchmarks
- Prioritized recommended order of work for contributors
https://claude.ai/code/session_01W6eQFStA66EuMKHUFo2rx3
2026-04-12 01:10:11 +00:00
|
|
|
|
- `@sfx("file.nsf")` / `@music("file.ftm")` asset directives
|
compiler: audio driver, u16 arithmetic, multi-scanline, slot recycling
Five language features and optimizations from the planned-work backlog:
- **Minimal audio driver**: `play`/`start_music`/`stop_music` now generate
APU pulse-1/pulse-2 writes from a builtin SFX/music name table, and
the NMI handler gains a `JSR __audio_tick` splice (via the linker's
`__audio_used` marker lookup) that ages an SFX countdown counter and
mutes pulse 1 when the tone expires. Programs that never trigger
audio pay zero ROM cost.
- **u16 arithmetic and comparisons**: new IR ops `LoadVarHi`, `StoreVarHi`,
`Add16`, `Sub16`, and six `Cmp*16` variants. The lowering context
tracks variable types via the analyzer's symbol table and routes
expressions through the 8-bit or 16-bit path based on operand width.
Add16 emits `CLC;ADC;ADC` with carry propagating naturally into the
high byte; compares dispatch high-byte-first with a short-circuit
low-byte fallback. Fixes a silent miscompile where `big += 1` on a
u16 var only incremented the low byte.
- **Multi-scanline handlers per state**: `gen_scanline_irq` now
dispatches on `(current_state, ZP_SCANLINE_STEP)` and reloads the
MMC3 counter with the delta to the next scanline in the same state.
`gen_scanline_reload` resets the step counter at the top of each
NMI so a state with multiple handlers fires them in ascending line
order. Previously only the first handler per state ever fired.
- **IR temp slot recycling**: `build_use_counts` pre-scans each
function to count per-temp uses; `retire_op_sources` decrements
the counts after each op and pushes dead slots back onto
`free_slots` for later allocation. `bitwise_ops.ne` used to crash
(debug) or miscompile (release) once it hit 128 concurrent temps;
with recycling the same function now uses ~4 slots instead of 136.
- **INC/DEC peephole fold + improved dead-load elimination**:
`fold_inc_dec` collapses `LDA addr; CLC; ADC #1; STA addr` into
a single `INC addr` (and the SEC/SBC variant into `DEC addr`),
saving 5 bytes and 5 cycles per increment. The fold is suppressed
when the next instruction reads carry. `remove_dead_loads` now
walks past INC/DEC/STX/STY (which don't touch A) to find the
actual next A-use, catching more dead loads.
Tests: 331 unit + 39 integration (up from 313 + 37), including new
guards for audio, u16, multi-scanline, and slot recycling.
https://claude.ai/code/session_01A8qk3gw2jWSzdiXBZPZSFE
2026-04-12 22:21:53 +00:00
|
|
|
|
- FamiTracker export format parsing
|
audio: complete the subsystem — asset pipeline, user decls, tracker-style driver
The audio subsystem was a sketch: `play name` / `start_music name` /
`stop_music` parsed, lowered, and emitted a few hardcoded register
writes from a builtin name table. No user-declared effects, no
per-frame envelope, no note streams, no real engine.
This flesh-out brings audio up to the quality bar of the rest of
the compiler (sprites, palettes, bank switching, scanline IRQ,
etc.) with a full data-driven pipeline:
## Asset pipeline (new `src/assets/audio.rs`)
- `sfx Name { duty, pitch, volume }` blocks compile into per-frame
pulse-1 envelopes. Pitch/volume arrays must match in length; each
entry is one NMI's worth of `$4000` data.
- `music Name { duty, volume, repeat, notes }` blocks compile into
flat `(pitch, duration)` streams for pulse 2. Pitch 0 is a rest,
1-60 indexes a builtin period table covering C1-B5.
- `resolve_sfx` / `resolve_music` walk the program for `play` /
`start_music` references and append builtin fallbacks for any
name that isn't user-declared — so `play coin` still works
without a `sfx Coin { ... }` block.
- Builtin effects (coin, jump, hit, click, cancel, shoot, step)
and tracks (theme, battle, victory, gameover) synthesize through
the same compile path as user decls — one data model, one driver.
## Runtime engine (`src/runtime/mod.rs`)
- `gen_audio_tick()` walks both channels every NMI: reads one
envelope byte through `(ZP_SFX_PTR),Y` -> writes `$4000`,
advances ptr, mutes on zero sentinel. Music decrements the note
counter, advances to the next `(pitch, dur)` pair on zero, looks
up the period through `(__period_table),Y`, loops on `0xFF 0xFF`.
- `gen_period_table()` emits a 60-entry equal-tempered table
(A4 = 440 Hz, NTSC 1.789773 MHz CPU clock) with length-counter
load bits pre-baked into each high byte.
- `gen_data_block()` emits a label + raw-bytes pseudo pair so
user sfx/music data can be spliced into PRG with regular labels
that the two-pass assembler resolves.
- New ZP layout: `$05/$06` music loop base, `$07` music state
(duty/volume/loop/active), `$0C-$0F` sfx and music pointers.
## IR codegen (`src/codegen/ir_codegen.rs`)
- `with_audio(sfx, music)` registers compile-time trigger constants
per blob name.
- `gen_play_sfx` emits: write period to `$4002`/`$4003`, load
envelope pointer into `ZP_SFX_PTR` via SymbolLo/SymbolHi of
`__sfx_<name>`, mark the sfx counter active.
- `gen_start_music` stamps the header byte into `ZP_MUSIC_STATE`
with the active bit OR'd in, seeds both ptr and loop base from
`__music_<name>`, primes the duration counter.
- `gen_stop_music` mutes pulse 2 and clears state.
## Linker (`src/linker/mod.rs`)
- New `link_with_all_assets(user_code, sprites, sfx, music)` path
that splices driver body, period table, and each sfx/music data
blob into PRG — all guarded on the `__audio_used` marker so
silent programs pay zero ROM cost.
## Assembler (`src/asm/opcodes.rs`, `src/asm/mod.rs`)
- New `AddressingMode::Bytes(Vec<u8>)` variant for raw-data
pseudo-instructions. `NOP+Bytes(v)` emits the payload verbatim,
letting the linker splice ROM data tables into a code section
and still have `Label` / `SymbolLo` / `SymbolHi` fixups resolve
correctly in the same assembly pass.
## Analyzer
- `play` / `start_music` now validate the name against user decls
and builtin tables. Unknown names emit E0505 with a helpful list
of builtins — previously a typo would silently compile to no-op.
## Parser
- New `sfx_decl` / `music_decl` grammar with property-style
configuration. Strict validation: duty 0-3, volume 0-15, pitch
arrays must match volume length, music notes must come in pairs,
pitch 0-60, duration ≥ 1.
## Tests
+170 new tests across every layer:
- `src/assets/audio.rs`: 17 tests (compile, resolve, builtins,
shadowing, label sanitation, nested reference walks)
- `src/parser/tests.rs`: 13 tests (valid/invalid sfx + music
declarations, property validation, play/start_music/stop_music)
- `src/analyzer/tests.rs`: 7 tests (builtin acceptance, user decl
acceptance, unknown-name rejection)
- `src/runtime/tests.rs`: 10 tests (audio tick labels, RTS end,
$4000 write, $4004 mute, period table assembly, A4 = 440 Hz,
length counter bits, data block verbatim emit)
- `src/linker/tests.rs`: 4 tests (sfx/music blob placement,
pointer resolution, elision when unused)
- `src/codegen/ir_codegen.rs`: rewrote the 4 existing audio tests
to match the new data-driven contract
- `tests/integration_test.rs`: 4 end-to-end tests including a
user-declared `sfx` + `music` program that verifies bytes land
in PRG ROM at the right addresses
## Docs
- New Audio section in `docs/language-guide.md` with syntax
reference, builtin tables, and an explanation of how the
driver works at compile and run time.
- `docs/architecture.md` updated to reflect the real audio
pipeline instead of the old "audio import stubs" stub.
- `docs/future-work.md` moves audio from "status: minimal" to
"status: full subsystem" with a narrower list of follow-up work
(triangle/noise/DMC channels, NSF/FTM imports, richer envelopes).
- `examples/audio_demo.ne` rewritten to showcase user-declared
`sfx LongCoin`, `sfx Zap`, `music Theme`, still demonstrating
builtin fallback via `play coin`.
Total: 424 tests passing (381 unit + 43 integration), clippy clean,
fmt clean, all 19 examples compile.
https://claude.ai/code/session_015WfaDttE3DpWn9rpyfpQd8
2026-04-13 01:10:21 +00:00
|
|
|
|
- Per-note pitch changes within a sfx (currently pitch is latched once)
|
Add comprehensive future work document
docs/future-work.md (347 lines) catalogs every known gap, incomplete
feature, and planned improvement with file:line references:
- IR-based codegen (IR exists but output uses AST codegen)
- 11 codegen gaps (calls, returns, transitions, arrays, mul/div, scroll)
- Sprite name resolution and multi-OAM support
- State machine dispatch (only start state generates code)
- Include directive, debug mode, scroll hardware
- Asset pipeline wiring (PNG conversion exists but unused)
- 10 unused error codes, missing validations
- Scanline IRQ, audio, inline assembly
- Language features: structs, enums, fixed-point, metasprites
- Missing shift-assign operators, P2 controller, register allocator
- WASM build target, compiler benchmarks
- Prioritized recommended order of work for contributors
https://claude.ai/code/session_01W6eQFStA66EuMKHUFo2rx3
2026-04-12 01:10:11 +00:00
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 13. Language Features (Post-v0.1)
|
|
|
|
|
|
|
|
|
|
|
|
From the spec's "Reserved for Future Versions" section:
|
|
|
|
|
|
|
|
|
|
|
|
| Feature | Description |
|
|
|
|
|
|
|---------|-------------|
|
|
|
|
|
|
| **Structs** | `struct Vec2 { x: u8, y: u8 }` — composite types with known layout |
|
|
|
|
|
|
| **Enums** | `enum Direction { Up, Down, Left, Right }` — mapped to u8 values |
|
|
|
|
|
|
| **Fixed-point** | `fixed8.8` type for sub-pixel movement |
|
|
|
|
|
|
| **Text/HUD** | Font sheet declarations, layout system for scores/health/menus |
|
|
|
|
|
|
| **Metasprites** | Multi-tile sprite groups with relative positioning |
|
|
|
|
|
|
| **Tilemaps** | Declarative level data with collision queries |
|
|
|
|
|
|
| **SRAM/saves** | Persistent storage declarations for battery-backed save data |
|
|
|
|
|
|
| **NES 2.0** | Extended iNES header format |
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 14. Inline Assembly
|
|
|
|
|
|
|
|
|
|
|
|
**Status**: `asm { }` blocks are lexed (`KwAsm`, `AsmBody`) but not parsed or
|
|
|
|
|
|
compiled. The lexer has raw-capture mode for asm content.
|
|
|
|
|
|
|
|
|
|
|
|
**What's needed**:
|
|
|
|
|
|
- Parser: capture asm body text, parse `{variable_name}` substitutions
|
|
|
|
|
|
- Codegen: emit raw 6502 instructions with variable address substitution
|
|
|
|
|
|
- Labels: local to the asm block scope
|
|
|
|
|
|
- `raw asm { }` variant with no substitution or safety checks
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 15. Compiler Performance
|
|
|
|
|
|
|
|
|
|
|
|
**Status**: Compilation is fast (<100ms for all examples) but has no benchmarks.
|
|
|
|
|
|
|
|
|
|
|
|
**What's needed**:
|
|
|
|
|
|
- `cargo bench` benchmarks for each pipeline phase
|
|
|
|
|
|
- Regression test: compilation must stay under 500ms for any reasonable project
|
|
|
|
|
|
- Profile-guided optimization of hot paths (lexer, parser)
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 16. WASM Build Target
|
|
|
|
|
|
|
|
|
|
|
|
**What's needed**:
|
|
|
|
|
|
- Factor out all file I/O behind a trait (`FileSystem` / `VFS`)
|
|
|
|
|
|
- Core pipeline takes `&str` source → `Vec<u8>` ROM with no filesystem access
|
|
|
|
|
|
- Compile the compiler to WASM for a browser-based IDE
|
|
|
|
|
|
- In-browser NES emulator integration for instant preview
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 17. Open Design Questions
|
|
|
|
|
|
|
|
|
|
|
|
From the engineering plan:
|
|
|
|
|
|
|
|
|
|
|
|
1. **Inline asm label syntax**: `.label:` (ca65 style) vs `label:` (generic)?
|
|
|
|
|
|
2. **Debug port address**: $4800 is conventional but not universal. Support
|
|
|
|
|
|
multiple debug output methods?
|
|
|
|
|
|
3. **OAM allocation strategy**: Sequential allocation vs priority-based with
|
|
|
|
|
|
automatic sprite cycling for the 8-per-scanline limit?
|
|
|
|
|
|
4. **Error recovery granularity**: How aggressively should the parser recover?
|
|
|
|
|
|
More recovery = more errors per compile, but risk of cascading false errors.
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 18. Missing Assignment Operators
|
|
|
|
|
|
|
|
|
|
|
|
`<<=` and `>>=` (shift-assign) are lexed as `ShiftLeftAssign`/`ShiftRightAssign`
|
|
|
|
|
|
tokens but have no corresponding variants in the `AssignOp` AST enum. They can
|
|
|
|
|
|
never appear in parsed code. Adding them requires:
|
|
|
|
|
|
- New `AssignOp::ShiftLeftAssign` and `AssignOp::ShiftRightAssign` variants
|
|
|
|
|
|
- Parser handling in `parse_assign_or_call` (alongside the other compound ops)
|
|
|
|
|
|
- Codegen: load value, shift, store back
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 19. Player 2 Controller
|
|
|
|
|
|
|
|
|
|
|
|
`Player::P1` and `Player::P2` are defined in the AST but marked `#[allow(dead_code)]`.
|
|
|
|
|
|
The parser always produces `ButtonRead(None, ...)` — it never parses `p1.button.X`
|
|
|
|
|
|
or `p2.button.X` syntax. The runtime only reads controller 1 ($4016).
|
|
|
|
|
|
|
|
|
|
|
|
**What's needed**:
|
|
|
|
|
|
- Parser: `p1.button.X` and `p2.button.X` syntax producing `Player::P1`/`P2`
|
|
|
|
|
|
- Runtime: read controller 2 from $4017 into a second zero-page byte
|
|
|
|
|
|
- Codegen: select the correct input byte based on player
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 20. Register Allocator
|
|
|
|
|
|
|
|
|
|
|
|
The plan describes `src/codegen/regalloc.rs` for managing A/X/Y allocation, but
|
|
|
|
|
|
no register allocator exists. The current codegen uses A for everything and
|
|
|
|
|
|
spills to zero-page $02 for comparisons. A proper allocator would:
|
|
|
|
|
|
- Track A/X/Y liveness across basic blocks
|
|
|
|
|
|
- Use X for array indexing and loop counters
|
|
|
|
|
|
- Use Y as secondary index
|
|
|
|
|
|
- Spill to zero-page temps only when all three are live
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## Priority Order
|
|
|
|
|
|
|
Update future-work.md: mark completed items
13 items moved from backlog to "recently completed":
state dispatch, function calls, break/continue, return, transition,
array indexing, scroll, multiply/divide/modulo, shifts, multi-OAM,
const assignment error, break-outside-loop error, math in linker.
Remaining priority: IR codegen, sprite resolution, include, asset
pipeline wiring, debug mode, error polish, audio, language features.
https://claude.ai/code/session_01W6eQFStA66EuMKHUFo2rx3
2026-04-12 02:07:50 +00:00
|
|
|
|
### Recently completed (removed from backlog)
|
|
|
|
|
|
|
|
|
|
|
|
These items were documented as future work but have since been implemented:
|
|
|
|
|
|
|
audio: complete the subsystem — asset pipeline, user decls, tracker-style driver
The audio subsystem was a sketch: `play name` / `start_music name` /
`stop_music` parsed, lowered, and emitted a few hardcoded register
writes from a builtin name table. No user-declared effects, no
per-frame envelope, no note streams, no real engine.
This flesh-out brings audio up to the quality bar of the rest of
the compiler (sprites, palettes, bank switching, scanline IRQ,
etc.) with a full data-driven pipeline:
## Asset pipeline (new `src/assets/audio.rs`)
- `sfx Name { duty, pitch, volume }` blocks compile into per-frame
pulse-1 envelopes. Pitch/volume arrays must match in length; each
entry is one NMI's worth of `$4000` data.
- `music Name { duty, volume, repeat, notes }` blocks compile into
flat `(pitch, duration)` streams for pulse 2. Pitch 0 is a rest,
1-60 indexes a builtin period table covering C1-B5.
- `resolve_sfx` / `resolve_music` walk the program for `play` /
`start_music` references and append builtin fallbacks for any
name that isn't user-declared — so `play coin` still works
without a `sfx Coin { ... }` block.
- Builtin effects (coin, jump, hit, click, cancel, shoot, step)
and tracks (theme, battle, victory, gameover) synthesize through
the same compile path as user decls — one data model, one driver.
## Runtime engine (`src/runtime/mod.rs`)
- `gen_audio_tick()` walks both channels every NMI: reads one
envelope byte through `(ZP_SFX_PTR),Y` -> writes `$4000`,
advances ptr, mutes on zero sentinel. Music decrements the note
counter, advances to the next `(pitch, dur)` pair on zero, looks
up the period through `(__period_table),Y`, loops on `0xFF 0xFF`.
- `gen_period_table()` emits a 60-entry equal-tempered table
(A4 = 440 Hz, NTSC 1.789773 MHz CPU clock) with length-counter
load bits pre-baked into each high byte.
- `gen_data_block()` emits a label + raw-bytes pseudo pair so
user sfx/music data can be spliced into PRG with regular labels
that the two-pass assembler resolves.
- New ZP layout: `$05/$06` music loop base, `$07` music state
(duty/volume/loop/active), `$0C-$0F` sfx and music pointers.
## IR codegen (`src/codegen/ir_codegen.rs`)
- `with_audio(sfx, music)` registers compile-time trigger constants
per blob name.
- `gen_play_sfx` emits: write period to `$4002`/`$4003`, load
envelope pointer into `ZP_SFX_PTR` via SymbolLo/SymbolHi of
`__sfx_<name>`, mark the sfx counter active.
- `gen_start_music` stamps the header byte into `ZP_MUSIC_STATE`
with the active bit OR'd in, seeds both ptr and loop base from
`__music_<name>`, primes the duration counter.
- `gen_stop_music` mutes pulse 2 and clears state.
## Linker (`src/linker/mod.rs`)
- New `link_with_all_assets(user_code, sprites, sfx, music)` path
that splices driver body, period table, and each sfx/music data
blob into PRG — all guarded on the `__audio_used` marker so
silent programs pay zero ROM cost.
## Assembler (`src/asm/opcodes.rs`, `src/asm/mod.rs`)
- New `AddressingMode::Bytes(Vec<u8>)` variant for raw-data
pseudo-instructions. `NOP+Bytes(v)` emits the payload verbatim,
letting the linker splice ROM data tables into a code section
and still have `Label` / `SymbolLo` / `SymbolHi` fixups resolve
correctly in the same assembly pass.
## Analyzer
- `play` / `start_music` now validate the name against user decls
and builtin tables. Unknown names emit E0505 with a helpful list
of builtins — previously a typo would silently compile to no-op.
## Parser
- New `sfx_decl` / `music_decl` grammar with property-style
configuration. Strict validation: duty 0-3, volume 0-15, pitch
arrays must match volume length, music notes must come in pairs,
pitch 0-60, duration ≥ 1.
## Tests
+170 new tests across every layer:
- `src/assets/audio.rs`: 17 tests (compile, resolve, builtins,
shadowing, label sanitation, nested reference walks)
- `src/parser/tests.rs`: 13 tests (valid/invalid sfx + music
declarations, property validation, play/start_music/stop_music)
- `src/analyzer/tests.rs`: 7 tests (builtin acceptance, user decl
acceptance, unknown-name rejection)
- `src/runtime/tests.rs`: 10 tests (audio tick labels, RTS end,
$4000 write, $4004 mute, period table assembly, A4 = 440 Hz,
length counter bits, data block verbatim emit)
- `src/linker/tests.rs`: 4 tests (sfx/music blob placement,
pointer resolution, elision when unused)
- `src/codegen/ir_codegen.rs`: rewrote the 4 existing audio tests
to match the new data-driven contract
- `tests/integration_test.rs`: 4 end-to-end tests including a
user-declared `sfx` + `music` program that verifies bytes land
in PRG ROM at the right addresses
## Docs
- New Audio section in `docs/language-guide.md` with syntax
reference, builtin tables, and an explanation of how the
driver works at compile and run time.
- `docs/architecture.md` updated to reflect the real audio
pipeline instead of the old "audio import stubs" stub.
- `docs/future-work.md` moves audio from "status: minimal" to
"status: full subsystem" with a narrower list of follow-up work
(triangle/noise/DMC channels, NSF/FTM imports, richer envelopes).
- `examples/audio_demo.ne` rewritten to showcase user-declared
`sfx LongCoin`, `sfx Zap`, `music Theme`, still demonstrating
builtin fallback via `play coin`.
Total: 424 tests passing (381 unit + 43 integration), clippy clean,
fmt clean, all 19 examples compile.
https://claude.ai/code/session_015WfaDttE3DpWn9rpyfpQd8
2026-04-13 01:10:21 +00:00
|
|
|
|
- **Full audio subsystem** — `src/runtime/mod.rs::gen_audio_tick`,
|
|
|
|
|
|
`gen_period_table`, and `gen_data_block` implement a frame-walking
|
|
|
|
|
|
pulse driver. `src/assets/audio.rs` compiles user `sfx`/`music`
|
|
|
|
|
|
declarations (and builtins referenced via `play coin` etc.) into
|
|
|
|
|
|
ROM-ready envelope and note-stream byte tables. `IrCodeGen::with_audio`
|
|
|
|
|
|
threads the compile-time trigger constants into `play`/`start_music`,
|
|
|
|
|
|
which emit pointer loads against per-blob labels. The linker splices
|
|
|
|
|
|
driver body, period table, and every blob into PRG gated on the
|
|
|
|
|
|
`__audio_used` marker so silent programs pay no cost. Full
|
|
|
|
|
|
parser/analyzer/codegen/linker/runtime test coverage.
|
compiler: audio driver, u16 arithmetic, multi-scanline, slot recycling
Five language features and optimizations from the planned-work backlog:
- **Minimal audio driver**: `play`/`start_music`/`stop_music` now generate
APU pulse-1/pulse-2 writes from a builtin SFX/music name table, and
the NMI handler gains a `JSR __audio_tick` splice (via the linker's
`__audio_used` marker lookup) that ages an SFX countdown counter and
mutes pulse 1 when the tone expires. Programs that never trigger
audio pay zero ROM cost.
- **u16 arithmetic and comparisons**: new IR ops `LoadVarHi`, `StoreVarHi`,
`Add16`, `Sub16`, and six `Cmp*16` variants. The lowering context
tracks variable types via the analyzer's symbol table and routes
expressions through the 8-bit or 16-bit path based on operand width.
Add16 emits `CLC;ADC;ADC` with carry propagating naturally into the
high byte; compares dispatch high-byte-first with a short-circuit
low-byte fallback. Fixes a silent miscompile where `big += 1` on a
u16 var only incremented the low byte.
- **Multi-scanline handlers per state**: `gen_scanline_irq` now
dispatches on `(current_state, ZP_SCANLINE_STEP)` and reloads the
MMC3 counter with the delta to the next scanline in the same state.
`gen_scanline_reload` resets the step counter at the top of each
NMI so a state with multiple handlers fires them in ascending line
order. Previously only the first handler per state ever fired.
- **IR temp slot recycling**: `build_use_counts` pre-scans each
function to count per-temp uses; `retire_op_sources` decrements
the counts after each op and pushes dead slots back onto
`free_slots` for later allocation. `bitwise_ops.ne` used to crash
(debug) or miscompile (release) once it hit 128 concurrent temps;
with recycling the same function now uses ~4 slots instead of 136.
- **INC/DEC peephole fold + improved dead-load elimination**:
`fold_inc_dec` collapses `LDA addr; CLC; ADC #1; STA addr` into
a single `INC addr` (and the SEC/SBC variant into `DEC addr`),
saving 5 bytes and 5 cycles per increment. The fold is suppressed
when the next instruction reads carry. `remove_dead_loads` now
walks past INC/DEC/STX/STY (which don't touch A) to find the
actual next A-use, catching more dead loads.
Tests: 331 unit + 39 integration (up from 313 + 37), including new
guards for audio, u16, multi-scanline, and slot recycling.
https://claude.ai/code/session_01A8qk3gw2jWSzdiXBZPZSFE
2026-04-12 22:21:53 +00:00
|
|
|
|
- **u16 arithmetic and comparisons** — new IR ops `LoadVarHi`,
|
|
|
|
|
|
`StoreVarHi`, `Add16`, `Sub16`, `CmpEq16` through `CmpGtEq16`. The
|
|
|
|
|
|
lowering context tracks variable types via the analyzer's symbol
|
|
|
|
|
|
table and routes each expression through the 8-bit or 16-bit path
|
|
|
|
|
|
based on operand width. Initializers, compound assignments, and
|
|
|
|
|
|
comparisons all preserve both bytes. The codegen emits
|
|
|
|
|
|
`CLC;ADC;ADC` for Add16 with carry propagating naturally, and
|
|
|
|
|
|
compare-high-then-compare-low dispatch for the six comparison
|
|
|
|
|
|
variants.
|
|
|
|
|
|
- **Multi-scanline on_scanline per state** — `gen_scanline_irq` now
|
|
|
|
|
|
dispatches on `(current_state, ZP_SCANLINE_STEP)` and reloads the
|
|
|
|
|
|
MMC3 counter with the delta to the next scanline in the same
|
|
|
|
|
|
state. `gen_scanline_reload` resets the step counter at the top
|
|
|
|
|
|
of each NMI so a state with multiple handlers fires them in
|
|
|
|
|
|
ascending line order.
|
|
|
|
|
|
- **IR temp slot recycling** — `build_use_counts` pre-scans each
|
|
|
|
|
|
function to count per-temp uses; `retire_op_sources` decrements
|
|
|
|
|
|
the counts after each op runs and pushes dead slots back onto
|
|
|
|
|
|
`free_slots` for later allocation. Previously, `bitwise_ops.ne`
|
|
|
|
|
|
crashed (debug) or silently miscompiled (release) once it
|
|
|
|
|
|
allocated more than 128 concurrent temps. With recycling the
|
|
|
|
|
|
same function now uses ~4 slots instead of 136.
|
|
|
|
|
|
- **INC/DEC peephole fold** — `fold_inc_dec` collapses
|
|
|
|
|
|
`LDA addr; CLC; ADC #1; STA addr` into a single `INC addr`
|
|
|
|
|
|
(and the SEC/SBC/#1 variant into `DEC addr`). Saves 5 bytes and
|
|
|
|
|
|
5 cycles per increment. The fold is suppressed when the next
|
|
|
|
|
|
instruction is a carry-dependent branch (`BCC`/`BCS`) since
|
|
|
|
|
|
INC/DEC don't update the carry flag.
|
|
|
|
|
|
- **Peephole dead-load elimination across passive ops** — the
|
|
|
|
|
|
old `remove_dead_loads` only dropped an LDA if the very next
|
|
|
|
|
|
instruction was another A-writer. Now it walks past
|
|
|
|
|
|
INC/DEC/STX/STY (which don't touch A) to find the actual
|
|
|
|
|
|
next A-use, catching more dead loads produced by copy
|
|
|
|
|
|
propagation.
|
|
|
|
|
|
|
Update future-work.md: mark completed items
13 items moved from backlog to "recently completed":
state dispatch, function calls, break/continue, return, transition,
array indexing, scroll, multiply/divide/modulo, shifts, multi-OAM,
const assignment error, break-outside-loop error, math in linker.
Remaining priority: IR codegen, sprite resolution, include, asset
pipeline wiring, debug mode, error polish, audio, language features.
https://claude.ai/code/session_01W6eQFStA66EuMKHUFo2rx3
2026-04-12 02:07:50 +00:00
|
|
|
|
- **State machine dispatch** — CMP + BNE + JMP trampoline dispatch table,
|
|
|
|
|
|
`current_state` in ZP $03, on_enter/on_exit handlers as JSR targets
|
|
|
|
|
|
- **Function call codegen** — JSR to function labels, ZP $04-$07 param passing,
|
|
|
|
|
|
RTS for returns
|
|
|
|
|
|
- **Break/continue** — loop_stack with JMP to continue/break labels
|
|
|
|
|
|
- **Return** — evaluate expr to A + RTS
|
|
|
|
|
|
- **Transition** — write state index + JMP to main loop
|
|
|
|
|
|
- **Array indexing** — TAX + LDA/STA with ZeroPageX or AbsoluteX
|
|
|
|
|
|
- **Scroll** — PPU $2005 writes (X then Y)
|
|
|
|
|
|
- **Multiply/divide/modulo** — JSR __multiply/__divide with shift-and-add/restoring division
|
|
|
|
|
|
- **Shift left/right** — ASL A / LSR A
|
|
|
|
|
|
- **Multi-OAM sprites** — sequential slot allocation (0-63), reset per frame
|
|
|
|
|
|
- **Const assignment error** — E0203 for assigning to constants
|
|
|
|
|
|
- **Break outside loop error** — E0203 for break/continue without enclosing loop
|
|
|
|
|
|
- **Math routines wired into linker** — gen_multiply/gen_divide included in ROM
|
Asset pipeline: resolve @binary and @chr file paths
resolve_sprites() now handles all three AssetSource variants:
- Inline(bytes): use directly (existing behavior)
- Binary(path): read raw bytes from file relative to source_dir
- Chr(path): convert PNG to CHR via png_to_chr()
Missing files are silently skipped rather than erroring, so
declarations can reference assets that haven't been added yet.
This keeps existing tests that use placeholder file paths working.
Updated future-work.md: moved include directive, P2 controller,
sprite resolution, shift-assign, debug statements, warnings, and
asset wiring to the "completed" section. Remaining work is IR codegen,
audio, on_scanline, and language features (structs/enums).
Tests: 257 total (3 new resolve_sprites tests)
https://claude.ai/code/session_01W6eQFStA66EuMKHUFo2rx3
2026-04-12 10:13:26 +00:00
|
|
|
|
- **Sprite name resolution** — sprite declarations map to CHR tile indices,
|
|
|
|
|
|
draw statements use the correct tile number
|
|
|
|
|
|
- **Inline sprite CHR data** — sprite decls with `chr: [0x..., ...]` work
|
|
|
|
|
|
- **Include directive** — `include "path"` inlines files at parse time,
|
|
|
|
|
|
with circular include detection
|
|
|
|
|
|
- **Shift-assign operators** — `<<=` and `>>=` work in all contexts
|
|
|
|
|
|
- **Player 2 controller** — `p1.button.X` / `p2.button.X` syntax, P2 input
|
|
|
|
|
|
read from $4017 into ZP $08
|
|
|
|
|
|
- **Unused variable warning** — W0103 emits for declared-but-never-read
|
|
|
|
|
|
globals (underscore-prefix silences)
|
|
|
|
|
|
- **Unreachable state warning** — W0104 emits for states not reachable from
|
|
|
|
|
|
the start state via transitions
|
|
|
|
|
|
- **E0502 "did you mean" suggestions** — undefined variable errors include
|
|
|
|
|
|
a suggestion for nearby-named symbols
|
|
|
|
|
|
- **debug.log / debug.assert** — parses into `Statement::DebugLog` /
|
|
|
|
|
|
`Statement::DebugAssert`, codegen emits runtime writes to $4800 when
|
|
|
|
|
|
`--debug` is set, stripped in release mode
|
2026-04-12 20:53:36 +00:00
|
|
|
|
- **--debug CLI flag wired** — threads through `IrCodeGen::with_debug`
|
2026-04-12 10:24:12 +00:00
|
|
|
|
- **IR-based codegen** — `src/codegen/ir_codegen.rs` walks `IrProgram` and
|
2026-04-12 10:46:01 +00:00
|
|
|
|
emits 6502 for every IR op: load/store, arithmetic, comparisons, arrays,
|
|
|
|
|
|
calls, draws, input (P1 and P2), scroll, debug.log/assert, state
|
Remove the legacy AST codegen — IR path is canonical now
The `--use-ast` path through `src/codegen/mod.rs` was a strictly
inferior subset of the IR codegen. Building every example with
`--use-ast` through the jsnes harness:
- `arrays_and_functions` — fully black (array init + function
return values + OAM-in-loop all broken)
- `structs_enums_for` — fully black (struct literal is a no-op,
all fields stay at 0)
- `inline_asm_demo` — fully black
- `bitwise_ops`, `loop_break_continue` — below sprite floors
(static `next_oam_slot` bug B)
- `match_demo` — panics at compile time with
`branch offset 153 out of range` (AST's if/else-chain
desugaring of `match` emits short branches that can't reach
the far arms in a multi-arm match)
Six of fourteen examples are non-functional under `--use-ast`.
The other eight happen to fall inside the subset AST handles
(no arrays, no structs, no function return values, no
multi-sprite loops, no long match chains).
`docs/future-work.md` already listed "Once working, delete the
AST-based codegen entirely" as the intended direction. It's
working, so this commit does the deletion.
What's removed:
- The `CodeGen` struct, its impl block, and every helper in
`src/codegen/mod.rs` (the AST codegen body) — ~1150 lines.
The file is now a module header that re-exports `IrCodeGen`.
- `src/codegen/tests.rs` — 15 AST-specific instruction-pattern
tests. Every feature they covered has an equivalent test in
`src/codegen/ir_codegen.rs::{tests,more_tests}` already.
- The `--use-ast` CLI flag and its branch in `src/main.rs`.
- `compile_with_ir_codegen` in `tests/integration_test.rs` —
`compile()` now does what it did, so they merged. All 40
integration tests go through the IR path.
- Outdated sections in `docs/future-work.md` that described the
IR codegen as "not yet implemented" and listed AST codegen
gaps as priority work.
What's kept:
- `src/codegen/ir_codegen.rs` — the real codegen.
- `src/codegen/peephole.rs` — post-codegen cleanup pass, now
run unconditionally from `main.rs`.
Test plan:
- `cargo test --release` — 313 unit + 37 integration tests pass
(was 328 + 37; the 15 dropped are the deleted AST-specific
tests).
- `cargo fmt --check` clean.
- `cargo clippy --release --all-targets -- -D warnings` clean.
- `node tests/emulator/run_examples.mjs` — 14/14 ROMs render
above their per-example nonBlack floors.
- The one tightening: `sprite_resolution_uses_tile_index` was
asserting on the old static-slot encoding
(`A9 01 8D 01 02`). Updated to the cursor-based form
(`A9 01 99 01 02`, i.e. STA AbsoluteY).
Net diff: 1581 deletions, 62 insertions.
https://claude.ai/code/session_014Z5y3Q9krLcAxYpZQJhZ5V
2026-04-12 20:37:59 +00:00
|
|
|
|
dispatch, runtime OAM cursor for looped draws, transitions + on_enter
|
|
|
|
|
|
handlers. It's the only codegen — the legacy AST-based path and the
|
|
|
|
|
|
`--use-ast` flag were removed once the IR pipeline was proven correct
|
|
|
|
|
|
by the jsnes emulator smoke test.
|
2026-04-12 10:24:12 +00:00
|
|
|
|
- **IR lowering bug fixes** — `ReadInput` now has a destination temp,
|
|
|
|
|
|
`ButtonRead` uses the proper input temp, logical AND/OR use a new
|
|
|
|
|
|
`emit_move` helper instead of the buggy raw VarId temp storage
|
2026-04-12 10:46:01 +00:00
|
|
|
|
- **IR Player 2 controller** — `ReadInput(temp, player)` selects $01
|
|
|
|
|
|
or $08 based on player index
|
|
|
|
|
|
- **IR scroll support** — `scroll(x, y)` lowers to `IrOp::Scroll(x, y)`
|
|
|
|
|
|
which emits two PPU $2005 writes in IR codegen
|
|
|
|
|
|
- **IR debug.log / debug.assert** — new `IrOp::DebugLog(temps)` and
|
2026-04-12 20:53:36 +00:00
|
|
|
|
`IrOp::DebugAssert(cond)` variants, emitted as $4800 writes in
|
|
|
|
|
|
debug mode and stripped in release
|
2026-04-12 10:24:12 +00:00
|
|
|
|
- **Asset pipeline @binary / @chr loading** — `resolve_sprites()` reads
|
|
|
|
|
|
raw binary files and converts PNGs via `png_to_chr()`. Missing files
|
|
|
|
|
|
are silently skipped (documentation-friendly)
|
2026-04-12 11:17:19 +00:00
|
|
|
|
- **Call arity validation** — E0203 when `Statement::Call` or
|
|
|
|
|
|
`Expr::Call` has the wrong number of arguments or a mismatched
|
|
|
|
|
|
argument type (uses a `function_signatures` map)
|
|
|
|
|
|
- **Return type validation** — `return value` is type-checked against
|
|
|
|
|
|
the function's declared return type (E0201); returning a value from a
|
|
|
|
|
|
void function emits E0203
|
|
|
|
|
|
- **W0102 loop-without-yield warning** — emitted when a `loop { ... }`
|
|
|
|
|
|
body contains no `break`, `return`, `transition`, or `wait_frame`
|
|
|
|
|
|
- **W0101 expensive mul/div/mod warning** — flags multiply/divide/modulo
|
|
|
|
|
|
with two non-constant operands; literal operands are silent because
|
|
|
|
|
|
the optimizer strength-reduces them
|
|
|
|
|
|
- **W0104 dead-code-after-terminator warning** — statements after
|
|
|
|
|
|
`return`, `break`, `continue`, or `transition` in the same block
|
|
|
|
|
|
emit W0104 with a label pointing at the terminator
|
|
|
|
|
|
- **E0301 RAM overflow** — the zero-page user region is now bounded
|
|
|
|
|
|
above by `$80` (leaving `$80-$FF` for IR temps) and the main RAM
|
|
|
|
|
|
allocator stops at `$0800`; overflow emits E0301 at the declaration
|
|
|
|
|
|
- **E0505 multiple start declarations** — parser rejects a second
|
|
|
|
|
|
`start X` token
|
2026-04-12 20:53:36 +00:00
|
|
|
|
- **`on scanline(N)` handlers** — `state { on scanline(240) { ... } }`
|
2026-04-12 11:17:19 +00:00
|
|
|
|
parses and populates `StateDecl::on_scanline`; analyzer emits E0203
|
2026-04-12 20:53:36 +00:00
|
|
|
|
if the game isn't using MMC3. The IR codegen now emits the full
|
|
|
|
|
|
MMC3 IRQ vector glue: per-state dispatch in `__irq_user` and a
|
|
|
|
|
|
`__ir_mmc3_reload` helper that picks the right `$C000` latch value
|
|
|
|
|
|
based on `current_state`. See `examples/scanline_split.ne` and
|
|
|
|
|
|
`examples/mmc3_per_state_split.ne`.
|
2026-04-12 11:17:19 +00:00
|
|
|
|
- **Inline assembly** — `asm { ... }` blocks. The lexer captures the
|
|
|
|
|
|
body as a raw `AsmBody` token; `src/asm/inline_parser.rs` provides a
|
|
|
|
|
|
minimal 6502 mnemonic parser that handles every addressing mode the
|
2026-04-12 20:53:36 +00:00
|
|
|
|
codegen emits. The IR codegen splices parsed instructions directly
|
|
|
|
|
|
into the output stream
|
2026-04-12 11:43:01 +00:00
|
|
|
|
- **Enum types** — `enum Name { V1, V2, ... }` declares u8 constants
|
|
|
|
|
|
with values equal to declaration order. Variant names are flattened
|
|
|
|
|
|
into the global symbol table
|
2026-04-12 17:07:54 +00:00
|
|
|
|
- **Struct types** — `struct Vec2 { x: u8, y: u8 }` with field
|
|
|
|
|
|
access (`pos.x = 5`) and contiguous u8-only field layout. The
|
|
|
|
|
|
analyzer synthesizes per-field VarAllocations so the rest of the
|
|
|
|
|
|
compiler treats field access as ordinary variable access.
|
|
|
|
|
|
- **`for i in start..end { ... }` loops** — half-open range with a
|
|
|
|
|
|
u8 index variable. Desugared in IR lowering to a while loop with
|
|
|
|
|
|
a proper continue-edge block so `break`/`continue` work.
|
audio: complete the subsystem — asset pipeline, user decls, tracker-style driver
The audio subsystem was a sketch: `play name` / `start_music name` /
`stop_music` parsed, lowered, and emitted a few hardcoded register
writes from a builtin name table. No user-declared effects, no
per-frame envelope, no note streams, no real engine.
This flesh-out brings audio up to the quality bar of the rest of
the compiler (sprites, palettes, bank switching, scanline IRQ,
etc.) with a full data-driven pipeline:
## Asset pipeline (new `src/assets/audio.rs`)
- `sfx Name { duty, pitch, volume }` blocks compile into per-frame
pulse-1 envelopes. Pitch/volume arrays must match in length; each
entry is one NMI's worth of `$4000` data.
- `music Name { duty, volume, repeat, notes }` blocks compile into
flat `(pitch, duration)` streams for pulse 2. Pitch 0 is a rest,
1-60 indexes a builtin period table covering C1-B5.
- `resolve_sfx` / `resolve_music` walk the program for `play` /
`start_music` references and append builtin fallbacks for any
name that isn't user-declared — so `play coin` still works
without a `sfx Coin { ... }` block.
- Builtin effects (coin, jump, hit, click, cancel, shoot, step)
and tracks (theme, battle, victory, gameover) synthesize through
the same compile path as user decls — one data model, one driver.
## Runtime engine (`src/runtime/mod.rs`)
- `gen_audio_tick()` walks both channels every NMI: reads one
envelope byte through `(ZP_SFX_PTR),Y` -> writes `$4000`,
advances ptr, mutes on zero sentinel. Music decrements the note
counter, advances to the next `(pitch, dur)` pair on zero, looks
up the period through `(__period_table),Y`, loops on `0xFF 0xFF`.
- `gen_period_table()` emits a 60-entry equal-tempered table
(A4 = 440 Hz, NTSC 1.789773 MHz CPU clock) with length-counter
load bits pre-baked into each high byte.
- `gen_data_block()` emits a label + raw-bytes pseudo pair so
user sfx/music data can be spliced into PRG with regular labels
that the two-pass assembler resolves.
- New ZP layout: `$05/$06` music loop base, `$07` music state
(duty/volume/loop/active), `$0C-$0F` sfx and music pointers.
## IR codegen (`src/codegen/ir_codegen.rs`)
- `with_audio(sfx, music)` registers compile-time trigger constants
per blob name.
- `gen_play_sfx` emits: write period to `$4002`/`$4003`, load
envelope pointer into `ZP_SFX_PTR` via SymbolLo/SymbolHi of
`__sfx_<name>`, mark the sfx counter active.
- `gen_start_music` stamps the header byte into `ZP_MUSIC_STATE`
with the active bit OR'd in, seeds both ptr and loop base from
`__music_<name>`, primes the duration counter.
- `gen_stop_music` mutes pulse 2 and clears state.
## Linker (`src/linker/mod.rs`)
- New `link_with_all_assets(user_code, sprites, sfx, music)` path
that splices driver body, period table, and each sfx/music data
blob into PRG — all guarded on the `__audio_used` marker so
silent programs pay zero ROM cost.
## Assembler (`src/asm/opcodes.rs`, `src/asm/mod.rs`)
- New `AddressingMode::Bytes(Vec<u8>)` variant for raw-data
pseudo-instructions. `NOP+Bytes(v)` emits the payload verbatim,
letting the linker splice ROM data tables into a code section
and still have `Label` / `SymbolLo` / `SymbolHi` fixups resolve
correctly in the same assembly pass.
## Analyzer
- `play` / `start_music` now validate the name against user decls
and builtin tables. Unknown names emit E0505 with a helpful list
of builtins — previously a typo would silently compile to no-op.
## Parser
- New `sfx_decl` / `music_decl` grammar with property-style
configuration. Strict validation: duty 0-3, volume 0-15, pitch
arrays must match volume length, music notes must come in pairs,
pitch 0-60, duration ≥ 1.
## Tests
+170 new tests across every layer:
- `src/assets/audio.rs`: 17 tests (compile, resolve, builtins,
shadowing, label sanitation, nested reference walks)
- `src/parser/tests.rs`: 13 tests (valid/invalid sfx + music
declarations, property validation, play/start_music/stop_music)
- `src/analyzer/tests.rs`: 7 tests (builtin acceptance, user decl
acceptance, unknown-name rejection)
- `src/runtime/tests.rs`: 10 tests (audio tick labels, RTS end,
$4000 write, $4004 mute, period table assembly, A4 = 440 Hz,
length counter bits, data block verbatim emit)
- `src/linker/tests.rs`: 4 tests (sfx/music blob placement,
pointer resolution, elision when unused)
- `src/codegen/ir_codegen.rs`: rewrote the 4 existing audio tests
to match the new data-driven contract
- `tests/integration_test.rs`: 4 end-to-end tests including a
user-declared `sfx` + `music` program that verifies bytes land
in PRG ROM at the right addresses
## Docs
- New Audio section in `docs/language-guide.md` with syntax
reference, builtin tables, and an explanation of how the
driver works at compile and run time.
- `docs/architecture.md` updated to reflect the real audio
pipeline instead of the old "audio import stubs" stub.
- `docs/future-work.md` moves audio from "status: minimal" to
"status: full subsystem" with a narrower list of follow-up work
(triangle/noise/DMC channels, NSF/FTM imports, richer envelopes).
- `examples/audio_demo.ne` rewritten to showcase user-declared
`sfx LongCoin`, `sfx Zap`, `music Theme`, still demonstrating
builtin fallback via `play coin`.
Total: 424 tests passing (381 unit + 43 integration), clippy clean,
fmt clean, all 19 examples compile.
https://claude.ai/code/session_015WfaDttE3DpWn9rpyfpQd8
2026-04-13 01:10:21 +00:00
|
|
|
|
- **Audio subsystem** — full data-driven pulse driver with
|
|
|
|
|
|
`sfx`/`music` block declarations, builtin effects, and an
|
|
|
|
|
|
NMI-time tick that walks envelope and note-stream tables.
|
|
|
|
|
|
See section 12 above for the full writeup.
|
2026-04-12 17:07:54 +00:00
|
|
|
|
- **Constant expression folding** — `const B: u8 = A + 3` evaluates
|
|
|
|
|
|
at compile time and feeds through to variable initializers too.
|
|
|
|
|
|
- **`on scanline` codegen (minimal)** — MMC3 IRQ setup at startup
|
|
|
|
|
|
plus a `__irq_user` dispatcher that saves registers, ACKs via
|
|
|
|
|
|
`$E000`, dispatches on `current_state` to the right scanline
|
|
|
|
|
|
handler, and restores. `__ir_mmc3_reload` helper re-arms the
|
|
|
|
|
|
counter each frame from NMI.
|
2026-04-12 11:43:01 +00:00
|
|
|
|
- **Peephole optimizer** — `src/codegen/peephole.rs` runs to fixed
|
2026-04-12 17:07:54 +00:00
|
|
|
|
point after codegen. Current passes:
|
|
|
|
|
|
- copy propagation for IR temps (rewrites loads to their source)
|
|
|
|
|
|
- dead-LDA elimination (drops overwritten LDAs)
|
|
|
|
|
|
- redundant STA/LDA pair removal
|
|
|
|
|
|
- LDA-then-STA-same-address removal
|
|
|
|
|
|
- dead-store elimination for IR temp slots (function-wide scan)
|
|
|
|
|
|
- A-value tracking eliminates redundant LDAs (ZP and absolute)
|
|
|
|
|
|
- branch folding: `Bxx L; JMP M; L:` → `Byy M`
|
|
|
|
|
|
- dead JMP to next label removal
|
2026-04-12 11:43:01 +00:00
|
|
|
|
- **`--dump-ir` CLI flag** — prints the lowered IR program after the
|
|
|
|
|
|
optimizer pass for debugging
|
2026-04-12 17:07:54 +00:00
|
|
|
|
- **Function-local variables** — IR codegen allocates backing
|
|
|
|
|
|
storage for `var`s declared inside function bodies, using a
|
|
|
|
|
|
per-function RAM range at `$0300+` so nested calls don't clobber
|
|
|
|
|
|
each other.
|
|
|
|
|
|
- **E0502 on assignment to undefined variable** — previously was
|
|
|
|
|
|
silently creating a new variable.
|
|
|
|
|
|
- **Function call ABI fix** — IR codegen was JSRing to `__fn_name`
|
|
|
|
|
|
but functions were defined as `__ir_fn_name`, and param VarIds
|
|
|
|
|
|
weren't in `var_addrs` so callees read temp slots instead of
|
|
|
|
|
|
parameters. Both bugs are now fixed with an integration test
|
|
|
|
|
|
guard.
|
2026-04-12 17:47:09 +00:00
|
|
|
|
- **Struct literal syntax** — `Vec2 { x: 100, y: 50 }` in both
|
|
|
|
|
|
variable initializers and assignments. Desugars in lowering to
|
|
|
|
|
|
per-field stores. Restricted to non-condition expression contexts
|
|
|
|
|
|
(if/while/for conditions) to avoid ambiguity with block `{`.
|
|
|
|
|
|
- **Match statement** — `match x { pat => body, _ => default }`
|
|
|
|
|
|
parses to an if/else-if chain at parse time, so no new AST
|
|
|
|
|
|
variant is needed. Supports any expression patterns and an
|
|
|
|
|
|
underscore catch-all.
|
|
|
|
|
|
- **For loops** — `for i in start..end { body }` (half-open range).
|
|
|
|
|
|
Desugars in IR lowering to a while loop with a proper
|
|
|
|
|
|
continue-edge block.
|
|
|
|
|
|
- **Semicolon statement separators** — short statements can share
|
|
|
|
|
|
a line: `a += 1; b += 2`.
|
|
|
|
|
|
- **Inline asm `{var}` substitution** — inside an `asm { ... }`
|
|
|
|
|
|
block, `{name}` is replaced with the hex address of the variable
|
|
|
|
|
|
`name`. The lexer balances nested braces so `{counter}` inside
|
|
|
|
|
|
an asm body is captured correctly.
|
|
|
|
|
|
- **`raw asm { ... }` blocks** — variant of inline asm that skips
|
|
|
|
|
|
`{var}` substitution, passing the body through verbatim.
|
|
|
|
|
|
- **`poke(addr, value)` / `peek(addr)` intrinsics** — hardware
|
|
|
|
|
|
register access without needing an asm block. Compile to a
|
|
|
|
|
|
single LDA/STA against a compile-time-constant address.
|
|
|
|
|
|
- **`--memory-map` CLI flag** — prints a human-readable variable
|
|
|
|
|
|
allocation table showing what's in ZP vs main RAM.
|
|
|
|
|
|
- **`--call-graph` CLI flag** — prints a call-tree view with max
|
|
|
|
|
|
depth reached from each entry point handler.
|
Update future-work.md: mark completed items
13 items moved from backlog to "recently completed":
state dispatch, function calls, break/continue, return, transition,
array indexing, scroll, multiply/divide/modulo, shifts, multi-OAM,
const assignment error, break-outside-loop error, math in linker.
Remaining priority: IR codegen, sprite resolution, include, asset
pipeline wiring, debug mode, error polish, audio, language features.
https://claude.ai/code/session_01W6eQFStA66EuMKHUFo2rx3
2026-04-12 02:07:50 +00:00
|
|
|
|
|
|
|
|
|
|
### Remaining priority order
|
|
|
|
|
|
|
Add comprehensive future work document
docs/future-work.md (347 lines) catalogs every known gap, incomplete
feature, and planned improvement with file:line references:
- IR-based codegen (IR exists but output uses AST codegen)
- 11 codegen gaps (calls, returns, transitions, arrays, mul/div, scroll)
- Sprite name resolution and multi-OAM support
- State machine dispatch (only start state generates code)
- Include directive, debug mode, scroll hardware
- Asset pipeline wiring (PNG conversion exists but unused)
- 10 unused error codes, missing validations
- Scanline IRQ, audio, inline assembly
- Language features: structs, enums, fixed-point, metasprites
- Missing shift-assign operators, P2 controller, register allocator
- WASM build target, compiler benchmarks
- Prioritized recommended order of work for contributors
https://claude.ai/code/session_01W6eQFStA66EuMKHUFo2rx3
2026-04-12 01:10:11 +00:00
|
|
|
|
For someone picking up this codebase, the recommended order of work:
|
|
|
|
|
|
|
compiler: audio driver, u16 arithmetic, multi-scanline, slot recycling
Five language features and optimizations from the planned-work backlog:
- **Minimal audio driver**: `play`/`start_music`/`stop_music` now generate
APU pulse-1/pulse-2 writes from a builtin SFX/music name table, and
the NMI handler gains a `JSR __audio_tick` splice (via the linker's
`__audio_used` marker lookup) that ages an SFX countdown counter and
mutes pulse 1 when the tone expires. Programs that never trigger
audio pay zero ROM cost.
- **u16 arithmetic and comparisons**: new IR ops `LoadVarHi`, `StoreVarHi`,
`Add16`, `Sub16`, and six `Cmp*16` variants. The lowering context
tracks variable types via the analyzer's symbol table and routes
expressions through the 8-bit or 16-bit path based on operand width.
Add16 emits `CLC;ADC;ADC` with carry propagating naturally into the
high byte; compares dispatch high-byte-first with a short-circuit
low-byte fallback. Fixes a silent miscompile where `big += 1` on a
u16 var only incremented the low byte.
- **Multi-scanline handlers per state**: `gen_scanline_irq` now
dispatches on `(current_state, ZP_SCANLINE_STEP)` and reloads the
MMC3 counter with the delta to the next scanline in the same state.
`gen_scanline_reload` resets the step counter at the top of each
NMI so a state with multiple handlers fires them in ascending line
order. Previously only the first handler per state ever fired.
- **IR temp slot recycling**: `build_use_counts` pre-scans each
function to count per-temp uses; `retire_op_sources` decrements
the counts after each op and pushes dead slots back onto
`free_slots` for later allocation. `bitwise_ops.ne` used to crash
(debug) or miscompile (release) once it hit 128 concurrent temps;
with recycling the same function now uses ~4 slots instead of 136.
- **INC/DEC peephole fold + improved dead-load elimination**:
`fold_inc_dec` collapses `LDA addr; CLC; ADC #1; STA addr` into
a single `INC addr` (and the SEC/SBC variant into `DEC addr`),
saving 5 bytes and 5 cycles per increment. The fold is suppressed
when the next instruction reads carry. `remove_dead_loads` now
walks past INC/DEC/STX/STY (which don't touch A) to find the
actual next A-use, catching more dead loads.
Tests: 331 unit + 39 integration (up from 313 + 37), including new
guards for audio, u16, multi-scanline, and slot recycling.
https://claude.ai/code/session_01A8qk3gw2jWSzdiXBZPZSFE
2026-04-12 22:21:53 +00:00
|
|
|
|
1. **u16 / array / nested struct fields** — u16 *globals* now work
|
|
|
|
|
|
end-to-end (load/store, +/-, comparisons all propagate through
|
|
|
|
|
|
16-bit IR ops). Struct fields and array elements are still u8-only
|
|
|
|
|
|
— the layout machinery needs to grow multi-byte field offsets.
|
audio: complete the subsystem — asset pipeline, user decls, tracker-style driver
The audio subsystem was a sketch: `play name` / `start_music name` /
`stop_music` parsed, lowered, and emitted a few hardcoded register
writes from a builtin name table. No user-declared effects, no
per-frame envelope, no note streams, no real engine.
This flesh-out brings audio up to the quality bar of the rest of
the compiler (sprites, palettes, bank switching, scanline IRQ,
etc.) with a full data-driven pipeline:
## Asset pipeline (new `src/assets/audio.rs`)
- `sfx Name { duty, pitch, volume }` blocks compile into per-frame
pulse-1 envelopes. Pitch/volume arrays must match in length; each
entry is one NMI's worth of `$4000` data.
- `music Name { duty, volume, repeat, notes }` blocks compile into
flat `(pitch, duration)` streams for pulse 2. Pitch 0 is a rest,
1-60 indexes a builtin period table covering C1-B5.
- `resolve_sfx` / `resolve_music` walk the program for `play` /
`start_music` references and append builtin fallbacks for any
name that isn't user-declared — so `play coin` still works
without a `sfx Coin { ... }` block.
- Builtin effects (coin, jump, hit, click, cancel, shoot, step)
and tracks (theme, battle, victory, gameover) synthesize through
the same compile path as user decls — one data model, one driver.
## Runtime engine (`src/runtime/mod.rs`)
- `gen_audio_tick()` walks both channels every NMI: reads one
envelope byte through `(ZP_SFX_PTR),Y` -> writes `$4000`,
advances ptr, mutes on zero sentinel. Music decrements the note
counter, advances to the next `(pitch, dur)` pair on zero, looks
up the period through `(__period_table),Y`, loops on `0xFF 0xFF`.
- `gen_period_table()` emits a 60-entry equal-tempered table
(A4 = 440 Hz, NTSC 1.789773 MHz CPU clock) with length-counter
load bits pre-baked into each high byte.
- `gen_data_block()` emits a label + raw-bytes pseudo pair so
user sfx/music data can be spliced into PRG with regular labels
that the two-pass assembler resolves.
- New ZP layout: `$05/$06` music loop base, `$07` music state
(duty/volume/loop/active), `$0C-$0F` sfx and music pointers.
## IR codegen (`src/codegen/ir_codegen.rs`)
- `with_audio(sfx, music)` registers compile-time trigger constants
per blob name.
- `gen_play_sfx` emits: write period to `$4002`/`$4003`, load
envelope pointer into `ZP_SFX_PTR` via SymbolLo/SymbolHi of
`__sfx_<name>`, mark the sfx counter active.
- `gen_start_music` stamps the header byte into `ZP_MUSIC_STATE`
with the active bit OR'd in, seeds both ptr and loop base from
`__music_<name>`, primes the duration counter.
- `gen_stop_music` mutes pulse 2 and clears state.
## Linker (`src/linker/mod.rs`)
- New `link_with_all_assets(user_code, sprites, sfx, music)` path
that splices driver body, period table, and each sfx/music data
blob into PRG — all guarded on the `__audio_used` marker so
silent programs pay zero ROM cost.
## Assembler (`src/asm/opcodes.rs`, `src/asm/mod.rs`)
- New `AddressingMode::Bytes(Vec<u8>)` variant for raw-data
pseudo-instructions. `NOP+Bytes(v)` emits the payload verbatim,
letting the linker splice ROM data tables into a code section
and still have `Label` / `SymbolLo` / `SymbolHi` fixups resolve
correctly in the same assembly pass.
## Analyzer
- `play` / `start_music` now validate the name against user decls
and builtin tables. Unknown names emit E0505 with a helpful list
of builtins — previously a typo would silently compile to no-op.
## Parser
- New `sfx_decl` / `music_decl` grammar with property-style
configuration. Strict validation: duty 0-3, volume 0-15, pitch
arrays must match volume length, music notes must come in pairs,
pitch 0-60, duration ≥ 1.
## Tests
+170 new tests across every layer:
- `src/assets/audio.rs`: 17 tests (compile, resolve, builtins,
shadowing, label sanitation, nested reference walks)
- `src/parser/tests.rs`: 13 tests (valid/invalid sfx + music
declarations, property validation, play/start_music/stop_music)
- `src/analyzer/tests.rs`: 7 tests (builtin acceptance, user decl
acceptance, unknown-name rejection)
- `src/runtime/tests.rs`: 10 tests (audio tick labels, RTS end,
$4000 write, $4004 mute, period table assembly, A4 = 440 Hz,
length counter bits, data block verbatim emit)
- `src/linker/tests.rs`: 4 tests (sfx/music blob placement,
pointer resolution, elision when unused)
- `src/codegen/ir_codegen.rs`: rewrote the 4 existing audio tests
to match the new data-driven contract
- `tests/integration_test.rs`: 4 end-to-end tests including a
user-declared `sfx` + `music` program that verifies bytes land
in PRG ROM at the right addresses
## Docs
- New Audio section in `docs/language-guide.md` with syntax
reference, builtin tables, and an explanation of how the
driver works at compile and run time.
- `docs/architecture.md` updated to reflect the real audio
pipeline instead of the old "audio import stubs" stub.
- `docs/future-work.md` moves audio from "status: minimal" to
"status: full subsystem" with a narrower list of follow-up work
(triangle/noise/DMC channels, NSF/FTM imports, richer envelopes).
- `examples/audio_demo.ne` rewritten to showcase user-declared
`sfx LongCoin`, `sfx Zap`, `music Theme`, still demonstrating
builtin fallback via `play coin`.
Total: 424 tests passing (381 unit + 43 integration), clippy clean,
fmt clean, all 19 examples compile.
https://claude.ai/code/session_015WfaDttE3DpWn9rpyfpQd8
2026-04-13 01:10:21 +00:00
|
|
|
|
2. **Triangle / noise / DMC channels** — the current audio engine
|
|
|
|
|
|
plays sfx on pulse 1 and music on pulse 2 with a full
|
|
|
|
|
|
data-driven tracker model (envelope walk, period table,
|
|
|
|
|
|
`(pitch, duration)` note streams, loop-back). Wiring triangle
|
|
|
|
|
|
and noise channels into the same model would unblock richer
|
|
|
|
|
|
multi-part compositions.
|
compiler: audio driver, u16 arithmetic, multi-scanline, slot recycling
Five language features and optimizations from the planned-work backlog:
- **Minimal audio driver**: `play`/`start_music`/`stop_music` now generate
APU pulse-1/pulse-2 writes from a builtin SFX/music name table, and
the NMI handler gains a `JSR __audio_tick` splice (via the linker's
`__audio_used` marker lookup) that ages an SFX countdown counter and
mutes pulse 1 when the tone expires. Programs that never trigger
audio pay zero ROM cost.
- **u16 arithmetic and comparisons**: new IR ops `LoadVarHi`, `StoreVarHi`,
`Add16`, `Sub16`, and six `Cmp*16` variants. The lowering context
tracks variable types via the analyzer's symbol table and routes
expressions through the 8-bit or 16-bit path based on operand width.
Add16 emits `CLC;ADC;ADC` with carry propagating naturally into the
high byte; compares dispatch high-byte-first with a short-circuit
low-byte fallback. Fixes a silent miscompile where `big += 1` on a
u16 var only incremented the low byte.
- **Multi-scanline handlers per state**: `gen_scanline_irq` now
dispatches on `(current_state, ZP_SCANLINE_STEP)` and reloads the
MMC3 counter with the delta to the next scanline in the same state.
`gen_scanline_reload` resets the step counter at the top of each
NMI so a state with multiple handlers fires them in ascending line
order. Previously only the first handler per state ever fired.
- **IR temp slot recycling**: `build_use_counts` pre-scans each
function to count per-temp uses; `retire_op_sources` decrements
the counts after each op and pushes dead slots back onto
`free_slots` for later allocation. `bitwise_ops.ne` used to crash
(debug) or miscompile (release) once it hit 128 concurrent temps;
with recycling the same function now uses ~4 slots instead of 136.
- **INC/DEC peephole fold + improved dead-load elimination**:
`fold_inc_dec` collapses `LDA addr; CLC; ADC #1; STA addr` into
a single `INC addr` (and the SEC/SBC variant into `DEC addr`),
saving 5 bytes and 5 cycles per increment. The fold is suppressed
when the next instruction reads carry. `remove_dead_loads` now
walks past INC/DEC/STX/STY (which don't touch A) to find the
actual next A-use, catching more dead loads.
Tests: 331 unit + 39 integration (up from 313 + 37), including new
guards for audio, u16, multi-scanline, and slot recycling.
https://claude.ai/code/session_01A8qk3gw2jWSzdiXBZPZSFE
2026-04-12 22:21:53 +00:00
|
|
|
|
3. **Register allocator** — proper A/X/Y allocation to replace
|
2026-04-12 17:07:54 +00:00
|
|
|
|
zero-page spills used by the current IR codegen. Partially
|
compiler: audio driver, u16 arithmetic, multi-scanline, slot recycling
Five language features and optimizations from the planned-work backlog:
- **Minimal audio driver**: `play`/`start_music`/`stop_music` now generate
APU pulse-1/pulse-2 writes from a builtin SFX/music name table, and
the NMI handler gains a `JSR __audio_tick` splice (via the linker's
`__audio_used` marker lookup) that ages an SFX countdown counter and
mutes pulse 1 when the tone expires. Programs that never trigger
audio pay zero ROM cost.
- **u16 arithmetic and comparisons**: new IR ops `LoadVarHi`, `StoreVarHi`,
`Add16`, `Sub16`, and six `Cmp*16` variants. The lowering context
tracks variable types via the analyzer's symbol table and routes
expressions through the 8-bit or 16-bit path based on operand width.
Add16 emits `CLC;ADC;ADC` with carry propagating naturally into the
high byte; compares dispatch high-byte-first with a short-circuit
low-byte fallback. Fixes a silent miscompile where `big += 1` on a
u16 var only incremented the low byte.
- **Multi-scanline handlers per state**: `gen_scanline_irq` now
dispatches on `(current_state, ZP_SCANLINE_STEP)` and reloads the
MMC3 counter with the delta to the next scanline in the same state.
`gen_scanline_reload` resets the step counter at the top of each
NMI so a state with multiple handlers fires them in ascending line
order. Previously only the first handler per state ever fired.
- **IR temp slot recycling**: `build_use_counts` pre-scans each
function to count per-temp uses; `retire_op_sources` decrements
the counts after each op and pushes dead slots back onto
`free_slots` for later allocation. `bitwise_ops.ne` used to crash
(debug) or miscompile (release) once it hit 128 concurrent temps;
with recycling the same function now uses ~4 slots instead of 136.
- **INC/DEC peephole fold + improved dead-load elimination**:
`fold_inc_dec` collapses `LDA addr; CLC; ADC #1; STA addr` into
a single `INC addr` (and the SEC/SBC variant into `DEC addr`),
saving 5 bytes and 5 cycles per increment. The fold is suppressed
when the next instruction reads carry. `remove_dead_loads` now
walks past INC/DEC/STX/STY (which don't touch A) to find the
actual next A-use, catching more dead loads.
Tests: 331 unit + 39 integration (up from 313 + 37), including new
guards for audio, u16, multi-scanline, and slot recycling.
https://claude.ai/code/session_01A8qk3gw2jWSzdiXBZPZSFE
2026-04-12 22:21:53 +00:00
|
|
|
|
mitigated by peephole passes + the new slot recycler, but still
|
|
|
|
|
|
wasteful in some cases (every temp spills to a ZP slot even if
|
|
|
|
|
|
its live range is one op wide).
|
|
|
|
|
|
4. **Text / HUD layer** — font sheet + layout system for scores.
|
|
|
|
|
|
5. **Cross-block temp live-range analysis** — the current slot
|
|
|
|
|
|
recycler is function-local; temps that flow across block
|
|
|
|
|
|
boundaries always get a dedicated slot for the full function.
|
|
|
|
|
|
A proper CFG-aware live range interference graph would let more
|
|
|
|
|
|
temps share slots.
|
|
|
|
|
|
6. **Peephole: drop LDA dead across unconditional JMPs** — after
|
|
|
|
|
|
the INC/DEC fold we sometimes leave an `LDA #1` whose value is
|
|
|
|
|
|
consumed by nothing before the next `JMP __ir_main_loop`. Local
|
|
|
|
|
|
analysis can't prove it's dead; a cross-block pass could.
|