mirror of
https://github.com/imjasonh/nescript
synced 2026-07-09 09:18:01 +00:00
Re-adds `palette Name { colors: [...] }` and
`background Name { tiles: [...], attributes: [...] }` as first-class
declarations, plus `set_palette Name` and `load_background Name`
statements for runtime swaps. Unlike the previous iteration that
quietly no-op'd, this one is fully wired through the pipeline and
its behavior is pinned by both unit tests and an emulator golden.
Pipeline:
- Lexer: re-adds `palette`, `background`, `set_palette`,
`load_background` keywords and tokenizes them.
- AST: `PaletteDecl` (name + 1..=32 colour bytes) and `BackgroundDecl`
(name + 0..=960 tile bytes + 0..=64 attribute bytes) live in
`Program`. `Statement::SetPalette` and `Statement::LoadBackground`
name-reference these declarations.
- Parser: `palette Name { colors: [...] }` / `background Name
{ tiles: [...], attributes: [...] }` blocks and their statement
forms parse via the existing byte-array helper.
- Analyzer: validates colour indices ($00-$3F), palette length
(<=32), nametable length (<=960), attribute length (<=64), and
duplicate decl names. `set_palette` / `load_background` targets
must reference a declared name (E0502 otherwise). When a program
declares palette or background, the analyzer bumps the user
zero-page allocator's starting address from `$10` to `$18` to
reserve `$11-$17` for the runtime update handshake — programs
that don't use the feature keep the old layout so their emulator
goldens stay byte-exact.
- Assets: `PaletteData` and `BackgroundData` resolve declarations
into zero-padded fixed-size blobs (32 / 960 / 64 bytes) and
expose `label()` / `tiles_label()` / `attrs_label()` for codegen
to reference.
- IR: new `IrOp::SetPalette(String)` and
`IrOp::LoadBackground(String)`; lowering forwards the names
verbatim.
- Codegen: `gen_set_palette` writes the palette label pointer into
ZP `$12/$13` and ORs bit 0 into the update flags at `$11`;
`gen_load_background` does the same for tile/attribute pointers
at `$14/$15/$16/$17` with bit 1. Both emit a `__ppu_update_used`
marker so the linker splices in the NMI apply helper only when
the feature is actually used.
- Runtime: `gen_initial_palette_load` and
`gen_initial_background_load` write the first declared
palette/background at reset time (before rendering is enabled,
where PPU writes are safe). `gen_nmi(has_ppu_updates)` takes a
new flag; when true it splices `gen_ppu_update_apply` at the top
of the NMI body, which checks the `$11` flags byte and copies
pending palette / nametable data to `$3F00` / `$2000` inside
vblank. All helpers use only ZP $02/$03 as scratch at reset time
and never clobber ZP slots live across NMI.
- Linker: new `link_banked_with_ppu` takes slice of `PaletteData` /
`BackgroundData`; splices each blob as a labelled data block in
PRG ROM, picks the first-declared as the reset-time load target,
enables background rendering automatically when a background is
declared, and threads `has_ppu_updates` into `gen_nmi`. Old
`link_banked` remains as a thin wrapper for callers without
palette/background data so existing tests don't shift.
Tests:
- Lexer: tokenization of the 4 new keywords (single added test case).
- Parser: 5 new tests for `palette` / `background` decls with and
without attributes, plus `set_palette` / `load_background`
statements.
- Analyzer: 9 new tests covering acceptance of declared
palettes/backgrounds, E0502 for unknown names, E0201 for
out-of-range NES colors and oversized blobs, E0501 for duplicate
names, and the zero-page-layout guard (palette/bg decls bump ZP
start; no decls keeps it at $10).
- Resolver: 3 new tests for zero-padding, truncation of oversized
decls, and label derivation.
- IR: 2 new lowering tests for `set_palette` and `load_background`.
- Integration: 5 new tests — blob contents spliced verbatim into
PRG, `STA $12` / `STA $14` emitted by set_palette /
load_background codegen, and a regression guard that programs
without palette/background still land user vars at $10.
- Emulator: new `examples/palette_and_background.ne` driven by a
frame counter that toggles between `CoolBlues` / `WarmReds` and
`TitleScreen` / `StageOne` every 90 frames. Golden PNG and audio
hash checked in under `tests/emulator/goldens/` and verified via
`node run_examples.mjs` — rendered image shows the blue
`CoolBlues` palette with the nametable populated from
`TitleScreen`.
Docs:
- `README.md` adds the feature to the headline list and the example
table.
- `docs/language-guide.md` restores the palette/background sections
with the full 32-byte layout table and `set_palette` /
`load_background` statement references.
- `docs/future-work.md` replaces the "removed as dead code" entry
with the remaining gaps (PNG-sourced palette and nametable
assets, cross-vblank large background updates, memory-map
reporting).
- `spec.md` restores the grammar productions and usage examples.
- `examples/README.md` lists the new demo.
All 497 unit + integration tests pass. Clippy clean. All 21
emulator goldens match after the update pass.
https://claude.ai/code/session_012fKB251HvEUQwG3tizFyqt
178 lines
7.5 KiB
Markdown
178 lines
7.5 KiB
Markdown
# Future Work
|
||
|
||
This document tracks the gaps between what NEScript currently compiles and
|
||
what the spec describes. Items are grouped by area. Anything implemented and
|
||
tested is omitted — `git log` is the authoritative record of what shipped.
|
||
|
||
---
|
||
|
||
## PNG-sourced palette and nametable assets
|
||
|
||
**What ships today.** `palette Name { colors: [...] }` and
|
||
`background Name { tiles: [...], attributes: [...] }` declarations with
|
||
inline byte arrays. The first declared palette / background is loaded
|
||
at reset time (before rendering enables), and both blocks get named
|
||
data blobs in PRG ROM so `set_palette Name` / `load_background Name`
|
||
can queue a vblank-safe swap via the NMI handler. See
|
||
`examples/palette_and_background.ne`.
|
||
|
||
**Still TODO.**
|
||
- `@palette("file.png")` — analyze image colours and map to nearest
|
||
NES master-palette indices. `nearest_nes_color()` already lives in
|
||
`src/assets/palette.rs` but is not wired through the resolver.
|
||
- `@nametable("file.png")` — convert a 256×240 image into a 960-byte
|
||
nametable plus 64-byte attribute table with automatic tile
|
||
deduplication (max 256 unique tiles per pattern table).
|
||
- Per-state background rendering control — programs currently get a
|
||
single fixed nametable at reset; per-state swaps work but are
|
||
limited by the NMI-time write budget (~2273 cycles, enough for a
|
||
palette but not a full 1024-byte nametable).
|
||
- `--memory-map` should report palette and background PRG ROM usage
|
||
alongside the variable layout.
|
||
|
||
---
|
||
|
||
## User code distribution across switchable banks
|
||
|
||
**Status.** `mapper: MMC1 / UxROM / MMC3` plus top-level `bank Name { prg }`
|
||
declarations are honored by the iNES header and by the linker, which reserves
|
||
each declared bank as a 16 KB switchable slot. However, the IR codegen puts
|
||
every user function and state handler into the fixed bank at `$C000-$FFFF` —
|
||
the declared banks exist only as empty space. Programs outgrowing the fixed
|
||
16 KB have nowhere to put their code.
|
||
|
||
**What's needed.**
|
||
- A bank-assignment step (analyzer or a new pass) that maps each user function
|
||
/ state handler to a target bank, either via explicit `bank Foo { fun bar()
|
||
... }` nesting or by greedy size-packing.
|
||
- Codegen support for emitting into non-fixed banks and for generating
|
||
cross-bank trampolines (the runtime helper scaffold already exists in
|
||
`runtime/mod.rs::gen_bank_trampoline`; it just isn't invoked).
|
||
- Linker changes so that functions in a switchable bank are found by the
|
||
JSR fix-up logic when the call crosses bank boundaries.
|
||
|
||
---
|
||
|
||
## Language feature gaps (post-v0.1)
|
||
|
||
From the spec's "Reserved for Future Versions" section:
|
||
|
||
| Feature | Description |
|
||
|--------------------|-----------------------------------------------------------------------|
|
||
| **Fixed-point** | `fixed8.8` type for sub-pixel movement with operator support. |
|
||
| **Text / HUD** | Font sheet declarations + layout system for scores, health, menus. |
|
||
| **Metasprites** | Multi-tile sprite groups with relative positioning. |
|
||
| **Tilemaps** | Declarative level data with built-in collision queries. |
|
||
| **SRAM / saves** | Persistent storage declarations for battery-backed save data. |
|
||
| **NES 2.0 header** | Extended iNES header format for additional metadata. |
|
||
|
||
### Struct / array field widths
|
||
|
||
Struct and array element types are currently restricted to the single-byte
|
||
primitives (`u8`, `i8`, `bool`). `u16`, nested struct fields, and array fields
|
||
are rejected with `E0201`. The analyzer's field-layout machinery needs to
|
||
grow multi-byte offsets, and IR lowering needs to treat wide fields as the
|
||
existing wide-var path already does for `u16` globals.
|
||
|
||
---
|
||
|
||
## Audio pipeline
|
||
|
||
**What ships today.** Frame-walking pulse driver with `sfx Name { duty, pitch,
|
||
volume }` and `music Name { duty, volume, repeat, notes }` blocks; builtin
|
||
effects and tracks; a 60-entry period table; `__audio_used` marker that
|
||
elides the whole subsystem when no program statement references it.
|
||
|
||
**Still TODO for richer audio.**
|
||
- Triangle / noise / DMC channels (today the driver only uses pulse 1 and
|
||
pulse 2).
|
||
- Multi-channel tracker playback (one `notes` list per channel).
|
||
- `@sfx("file.nsf")` / `@music("file.ftm")` asset directives — neither the
|
||
NSF nor the FamiTracker format is parsed yet.
|
||
- Per-note pitch changes within a sfx (currently `pitch` latches once at
|
||
trigger time).
|
||
|
||
---
|
||
|
||
## Debug instrumentation
|
||
|
||
**What ships today.** `debug.log(...)` and `debug.assert(...)` lower to $4800
|
||
writes when `--debug` is passed, and are stripped entirely in release builds.
|
||
|
||
**Not yet implemented.**
|
||
- Mesen-compatible symbol export (`.mlb` / `.sym` files) — the CLI does not
|
||
emit them, and the previous `DebugSymbols` helper was removed as dead code
|
||
during cleanup.
|
||
- Source maps relating ROM addresses to source lines — the `SourceLoc`
|
||
IR op exists but is not consumed by the linker or CLI.
|
||
- Array bounds checking in debug mode.
|
||
- Frame overrun detection (cycles-per-frame counting).
|
||
- `debug.overlay(x, y, text)` — needs the text/HUD subsystem above.
|
||
|
||
---
|
||
|
||
## Code quality / tooling
|
||
|
||
### Register allocator
|
||
|
||
All IR temps currently spill to a recycled zero-page slot (`$80-$FF`). The
|
||
peephole pass mops up the most obvious waste, but a real CFG-aware allocator
|
||
that holds short-lived temps in `A`/`X`/`Y` would cut a noticeable number of
|
||
LDA/STA pairs.
|
||
|
||
### Cross-block temp live-range analysis
|
||
|
||
The slot recycler is function-local per-block. Temps that flow across block
|
||
boundaries get a dedicated slot for the entire function, even if a later
|
||
block could reuse the slot.
|
||
|
||
### `--no-opt` flag
|
||
|
||
There is no way to disable the optimizer from the CLI. Adding one would make
|
||
optimizer-introduced bugs easier to bisect.
|
||
|
||
### Compilation benchmarks
|
||
|
||
Compilation is fast (<100 ms for every example today) but has no `cargo
|
||
bench` harness, so regressions would slip through.
|
||
|
||
### WASM build target
|
||
|
||
To build a browser IDE we would need to route file I/O through a trait so the
|
||
core pipeline works on `&str → Vec<u8>` without touching `std::fs`. Today the
|
||
parser's preprocess pass and the asset resolver read files directly.
|
||
|
||
---
|
||
|
||
## Error message polish
|
||
|
||
### Unused error codes
|
||
|
||
`ErrorCode` only defines codes that are actually emitted. Previously there
|
||
were placeholder variants (`E0202` invalid cast, `E0403` unreachable state)
|
||
marked `#[allow(dead_code)]`; those were removed during cleanup. If those
|
||
semantics come back, add the codes at that point.
|
||
|
||
### Missing diagnostics
|
||
|
||
- No warning for implicit-drop of a function return value (`my_fun()` at
|
||
statement position when `my_fun` returns non-void).
|
||
- `W0102` ("loop without yield") is only emitted for bare `loop`, not for
|
||
`while true` or `loop { if cond { continue } }`.
|
||
- No warning for `fast` variables that never justify the zero-page slot
|
||
(could cross-reference access counts).
|
||
|
||
---
|
||
|
||
## Open design questions
|
||
|
||
1. **Inline asm label syntax.** `.label:` (ca65 style) vs `label:` (generic)?
|
||
Today the inline-asm parser accepts `label:` but not `.label`; migrating
|
||
would be cheap but would invalidate any copy-pasted ca65 fragments.
|
||
2. **Debug port address.** $4800 is conventional but not universal. Should
|
||
we 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 means more errors per compile but also risks
|
||
cascading false errors.
|