Lists structs, for loops, audio parsing, const folding, on_scanline codegen, all new peephole passes, and the fixed function call ABI / local variable allocation. Re-prioritizes remaining work. https://claude.ai/code/session_01W6eQFStA66EuMKHUFo2rx3
21 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: The IR pipeline (lowering + optimization) runs during compilation but
its output is discarded. Code generation still works from the AST directly
(src/codegen/mod.rs). This means IR-level optimizations have no effect on the
final ROM.
What exists:
src/ir/mod.rs: Complete IR type definitions (IrProgram,IrFunction,IrBasicBlock,IrOp,IrTerminator)src/ir/lowering.rs: AST → IR translation for all statement and expression typessrc/optimizer/mod.rs: Constant folding, dead code elimination, strength reduction, ZP promotion analysis, function inlining — all operating on IR
What's needed:
- A new
src/codegen/ir_codegen.rsthat walksIrProgramand emits 6502Instructionsequences fromIrOp/IrTerminatorinstead of from AST nodes - Register allocation strategy for IR temps → A/X/Y/zero-page spill slots
- Replace the
CodeGen::generate(&program)call inmain.rswithir_codegen::generate(&ir_program, &analysis) - Once working, delete the AST-based codegen entirely
IR lowering issues to fix first (found during code review):
ButtonReademitsReadInputwith no destination temp, then uses uninitialized temp in theAndmask operation (src/ir/lowering.rs:534). The fix:ReadInputshould store the input byte into a temp, or the lowering should emitLoadVar(t, input_var_id)afterReadInput.- Logical AND/OR use raw
VarId(self.next_var_id)for temp storage without registering it (src/ir/lowering.rs:603,637). Should useIrTempinstead. - Break/continue create unreachable blocks that contain subsequent dead statements
(
src/ir/lowering.rs:259). Should either skip lowering after a terminator, or the dead code elimination pass should handle it.
Impact: Enables all optimizer passes to actually affect output quality. Currently the optimizer is validated by tests but its results are thrown away.
2. Codegen Gaps (AST-Based)
These features are parsed and analyzed but produce no 6502 output:
| Feature | Location | Status |
|---|---|---|
| Function calls | codegen:148 |
Statement::Call is a no-op |
| Return values | codegen:148 |
Statement::Return is a no-op |
| State transitions | codegen:148 |
Statement::Transition is a no-op |
| Array indexing | codegen:199-200 |
LValue::ArrayIndex assignment is a no-op |
| Array expressions | codegen:417 |
Expr::ArrayIndex, Expr::ArrayLiteral are no-ops |
| Function call expressions | codegen:417 |
Expr::Call returns nothing |
| Scroll | codegen:151-152 |
Statement::Scroll is a no-op |
| Load background | codegen:154-155 |
Statement::LoadBackground is a no-op |
| Set palette | codegen:154-155 |
Statement::SetPalette is a no-op |
| Multiply/divide/modulo | codegen:450-452 |
BinOp::Mul/Div/Mod only emit left operand |
| Dynamic shifts | ir/lowering:575 |
Shift amount is hardcoded to 1 |
Priority fixes for a working multi-state game:
- Function calls: JSR to function label, pass args via zero-page, return via A
- State transitions: Write state ID to a zero-page variable, jump to dispatcher
- Array indexing: Use X register for index, LDA absolute,X for loads
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, multi-OAM slot allocation, transitions + on_enter handlers. Now the default;--use-astfalls back to the legacy AST-based codegen. - 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.
Remaining priority order
For someone picking up this codebase, the recommended order of work:
- Delete AST codegen — IR codegen is now the default and beats
the AST codegen in 5/7 examples. Once confidence is high, remove
--use-astandsrc/codegen/mod.rs's AST-specific code. - Struct literals —
pos = Vec2 { x: 100, y: 50 }as an expression. Currently fields must be assigned individually. - 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.