1
0
Fork 0
mirror of https://github.com/imjasonh/nescript synced 2026-07-08 00:45:38 +00:00
nescript/docs/future-work.md
Claude a757336681
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

20 KiB
Raw Blame History

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 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

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 effect
  • start_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 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

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_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
  • 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 directiveinclude "path" inlines files at parse time, with circular include detection
  • Shift-assign operators<<= and >>= work in all contexts
  • Player 2 controllerp1.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
  • --debug CLI flag wired — threads through CodeGen::with_debug
  • IR-based codegensrc/codegen/ir_codegen.rs walks IrProgram and 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-ast flag were removed once the IR pipeline was proven correct by the jsnes emulator smoke test.
  • IR lowering bug fixesReadInput 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
  • IR Player 2 controllerReadInput(temp, player) selects $01 or $08 based on player index
  • IR scroll supportscroll(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 IrOp::DebugAssert(cond) variants, emitted as $4800 writes in debug mode and stripped in release (same behavior as AST codegen)
  • Asset pipeline @binary / @chr loadingresolve_sprites() reads raw binary files and converts PNGs via png_to_chr(). Missing files are silently skipped (documentation-friendly)
  • 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 validationreturn 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
  • on scanline(N) parsingstate { on scanline(240) { ... } } parses and populates StateDecl::on_scanline; analyzer emits E0203 if the game isn't using MMC3. Codegen (MMC3 IRQ vector wiring) is still TODO
  • Inline assemblyasm { ... } 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 codegen emits. Both IR and AST codegen splice parsed instructions directly into the output stream
  • Enum typesenum Name { V1, V2, ... } declares u8 constants with values equal to declaration order. Variant names are flattened into the global symbol table
  • Struct typesstruct 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 statement parsingplay SfxName, start_music, stop_music parse and analyze as no-ops (no driver yet; users can wire audio via inline asm blocks).
  • Constant expression foldingconst 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.
  • Peephole optimizersrc/codegen/peephole.rs runs 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-ir CLI 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_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.
  • Struct literal syntaxVec2 { 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 statementmatch 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 loopsfor 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.

Remaining priority order

For someone picking up this codebase, the recommended order of work:

  1. 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.
  2. Audio driver — the play/start_music/stop_music statements parse but don't generate any code. A minimal NSF-style driver running in NMI would unblock game projects.
  3. 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.
  4. 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.
  5. Match statementmatch x { 0 => ... , 1 => ... } would be useful for state dispatch and enum-driven logic.
  6. Text / HUD layer — font sheet + layout system for scores.