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
20 KiB
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
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.
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
spritedeclaration 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_slotcounter 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
drawgets 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_statezero-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 StateNamewrites the new state index tocurrent_state- Generate
on_entercall on transition,on_exitcall 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
ProgramAST - Span tracking: included files need their own
file_idfor 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 failuredebug.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/DebugAssertin theStatementenum 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"), callpng_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()insrc/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 castE0203— invalid operation for typeE0301— zero-page overflowE0403— unreachable stateE0505— multiple start declarationsW0101— expensive multiply/divide operationW0102— loop without break or wait_frameW0103— unused variableW0104— unreachable code after return/break/transition
Missing validations
- No error for assigning to a
const - No error for
break/continueoutside a loop - No warning for variables declared but never read
- No error for
returnwith 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
Status: play, start_music, stop_music keywords are lexed but produce
no output. No audio driver exists.
What's needed:
@sfx("file.nsf")/@music("file.ftm")asset directives- Audio driver running in the NMI handler (after OAM DMA)
play SfxName→ trigger one-shot sound effectstart_music TrackName/stop_music→ start/stop background music- FamiTracker export format parsing (complex — consider using existing tools)
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 benchbenchmarks 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
&strsource →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:
- Inline asm label syntax:
.label:(ca65 style) vslabel:(generic)? - Debug port address: $4800 is conventional but not universal. Support multiple debug output methods?
- OAM allocation strategy: Sequential allocation vs priority-based with automatic sprite cycling for the 8-per-scanline limit?
- 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::ShiftLeftAssignandAssignOp::ShiftRightAssignvariants - 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.Xandp2.button.Xsyntax producingPlayer::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
Recently completed (removed from backlog)
These items were documented as future work but have since been implemented:
- State machine dispatch — CMP + BNE + JMP trampoline dispatch table,
current_statein 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
- 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.Xsyntax, 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--debugis set, stripped in release mode - --debug CLI flag wired — threads through
CodeGen::with_debug - IR-based codegen —
src/codegen/ir_codegen.rswalksIrProgramand emits 6502 for every IR op: load/store, arithmetic, comparisons, arrays, calls, draws, input (P1 and P2), scroll, debug.log/assert, state dispatch, runtime OAM cursor for looped draws, transitions + on_enter handlers. It's the only codegen — the legacy AST-based path and the--use-astflag were removed once the IR pipeline was proven correct by the jsnes emulator smoke test. - IR lowering bug fixes —
ReadInputnow has a destination temp,ButtonReaduses the proper input temp, logical AND/OR use a newemit_movehelper instead of the buggy raw VarId temp storage - IR Player 2 controller —
ReadInput(temp, player)selects $01 or $08 based on player index - IR scroll support —
scroll(x, y)lowers toIrOp::Scroll(x, y)which emits two PPU $2005 writes in IR codegen - IR debug.log / debug.assert — new
IrOp::DebugLog(temps)andIrOp::DebugAssert(cond)variants, emitted as $4800 writes in debug mode and stripped in release (same behavior as AST codegen) - Asset pipeline @binary / @chr loading —
resolve_sprites()reads raw binary files and converts PNGs viapng_to_chr(). Missing files are silently skipped (documentation-friendly) - Call arity validation — E0203 when
Statement::CallorExpr::Callhas the wrong number of arguments or a mismatched argument type (uses afunction_signaturesmap) - Return type validation —
return valueis 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 nobreak,return,transition, orwait_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, ortransitionin 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-$FFfor 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 Xtoken on scanline(N)parsing —state { on scanline(240) { ... } }parses and populatesStateDecl::on_scanline; analyzer emits E0203 if the game isn't using MMC3. Codegen (MMC3 IRQ vector wiring) is still TODO- Inline assembly —
asm { ... }blocks. The lexer captures the body as a rawAsmBodytoken;src/asm/inline_parser.rsprovides a minimal 6502 mnemonic parser that handles every addressing mode the codegen emits. Both IR and AST codegen splice parsed instructions directly into the output stream - Enum types —
enum Name { V1, V2, ... }declares u8 constants with values equal to declaration order. Variant names are flattened into the global symbol table - 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 sobreak/continuework.- Audio statement parsing —
play SfxName,start_music,stop_musicparse and analyze as no-ops (no driver yet; users can wire audio via inlineasmblocks). - Constant expression folding —
const B: u8 = A + 3evaluates at compile time and feeds through to variable initializers too. on scanlinecodegen (minimal) — MMC3 IRQ setup at startup plus a__irq_userdispatcher that saves registers, ACKs via$E000, dispatches oncurrent_stateto the right scanline handler, and restores.__ir_mmc3_reloadhelper re-arms the counter each frame from NMI.- Peephole optimizer —
src/codegen/peephole.rsruns to fixed 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
--dump-irCLI flag — prints the lowered IR program after the optimizer pass for debugging- Function-local variables — IR codegen allocates backing
storage for
vars 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_namebut functions were defined as__ir_fn_name, and param VarIds weren't invar_addrsso callees read temp slots instead of parameters. Both bugs are now fixed with an integration test guard. - 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 anasm { ... }block,{name}is replaced with the hex address of the variablename. 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-mapCLI flag — prints a human-readable variable allocation table showing what's in ZP vs main RAM.--call-graphCLI flag — prints a call-tree view with max depth reached from each entry point handler.
Remaining priority order
For someone picking up this codebase, the recommended order of work:
- u16 / array / nested struct fields — current structs only allow u8/i8/bool fields. The layout machinery is ready for more types but the codegen only handles 1-byte loads/stores.
- Audio driver — the
play/start_music/stop_musicstatements parse but don't generate any code. A minimal NSF-style driver running in NMI would unblock game projects. - Multi-scanline on_scanline reload — the current codegen supports one scanline per state but not a chain of handlers at different scanlines in the same frame.
- Register allocator — proper A/X/Y allocation to replace zero-page spills used by the current IR codegen. Partially mitigated by peephole passes but still wasteful in some cases.
- Match statement —
match x { 0 => ... , 1 => ... }would be useful for state dispatch and enum-driven logic. - Text / HUD layer — font sheet + layout system for scores.