1
0
Fork 0
mirror of https://github.com/imjasonh/nescript synced 2026-07-09 01:16:12 +00:00

compiler: GNROM / debug port / sprite flicker / fade / sprite-0 split + docs

Another batch from the cc65/nesdoug gap catalogue. All six items
gated on marker labels (or default-false attributes) so existing
programs produce byte-identical ROMs — every pre-existing .nes
file round-trips unchanged.

**Language / runtime additions:**

- `mapper: GNROM` (iNES 66). Combines AxROM's 32 KB PRG pages with
  CNROM's 8 KB CHR banks in a single `$8000` register. Linker
  pads single-page ROMs to 32 KB to match mapper-66 expectations.
- `game { debug_port: fceux | mesen | 0xXXXX }`. `debug.log`,
  `debug.assert`, and the `__debug_halt` sentinel now target a
  user-selected address. `fceux` (default, $4800) and `mesen`
  ($4018) are named aliases; custom hex addresses are accepted
  for unusual debuggers.
- `game { sprite_flicker: true }`. IR lowerer injects an
  `IrOp::CycleSprites` at the top of every `on frame` handler,
  which flips on the rotating-OAM NMI variant with no per-site
  boilerplate. Default false so existing ROMs keep their layout.
- `fade_out(step_frames)` / `fade_in(step_frames)` builtins.
  Blocking helpers that walk brightness 4 → 0 or 0 → 4 with
  `step_frames` frames between each step. Runtime splices
  `__fade_out`, `__fade_in`, and a callable `__wait_frame_rt`
  helper when the builtin is used. Zero-guard on step_frames
  prevents a pathological 256-frame spin when the caller
  accidentally passes 0.
- `sprite_0_split(scroll_x, scroll_y)` intrinsic. Emits a
  two-phase busy-wait on `$2002` bit 6 (wait-for-clear,
  wait-for-set) then writes the new scroll values to `$2005`.
  Works on any mapper — unlike `on_scanline(N)` which requires
  MMC3. Enables HUD-over-playfield scrolling on NROM/UxROM/MMC1.

**Docs:**

- New paragraph in the language guide explaining the no-recursion
  design choice and the explicit-stack workaround pattern.
- `future-work.md` updated to mark the shipped items out of the
  catalogue; remaining items reshuffled in the priority ranking.
- README + examples/README updated with the new mapper and
  builtins.

**Tests:**

- 12 new integration tests covering: GNROM header emission,
  debug-port targeting (fceux/mesen/custom), unknown-alias
  rejection, sprite_flicker on/off/bad-value, fade_out JSR + marker
  coupling, fade omitted-when-unused, fade-in-expression rejected,
  sprite_0_split byte-level busy-wait verification, sprite_0_split
  arity enforcement, sprite_0_split omitted-when-unused, and an
  extended void-intrinsic-in-expression-position test covering the
  three new void builtins.
- `nes2_mapper_high_nibble_in_byte_8_is_zero_for_small_mappers`
  extended to include GNROM.
- Four new examples with committed .nes ROMs + pixel/audio
  goldens: `gnrom_simple`, `auto_sprite_flicker`, `fade_demo`,
  `sprite_0_split_demo`.

All 752 tests pass. Clippy clean. 44/44 emulator goldens match.
This commit is contained in:
Claude 2026-04-18 19:31:55 +00:00
parent f4968256f4
commit e0b268eea9
No known key found for this signature in database
35 changed files with 1269 additions and 89 deletions

View file

@ -306,6 +306,58 @@ reset_score()
- **Call depth limit.** The default maximum call depth is 8. Exceeding it produces error `E0401`.
- **Maximum 8 parameters per function.** The calling convention is hybrid: **leaf** functions (no nested `JSR` in their body) receive up to four parameters through fixed zero-page transport slots `$04`-`$07`, while **non-leaf** functions receive up to eight parameters via direct caller writes into per-function RAM spill slots (no transport, no prologue copy). Declaring a function with 9+ parameters produces error `E0506`. Declaring a leaf with 5+ parameters silently promotes it to the non-leaf convention — you pay the direct-write cost rather than the prologue-copy cost, which is still cheaper than the old transport-plus-spill path.
#### Why no recursion?
This is a deliberate design choice, not a bug. NEScript uses a hybrid
direct-write calling convention that lands each function's
parameters and locals at a fixed RAM address the analyzer reserves
at compile time. Recursion would require each activation to have
its own stack frame, which means either:
1. A software stack pointer managed by a prologue/epilogue at every
call site (costs cycles on a platform that only has 2 KB of RAM
and a 256-byte hardware stack), or
2. The hardware stack carrying frames directly (the 6502's 256-byte
`$0100-$01FF` stack overflows fast — a single recursive call
with any meaningful locals blows it within a handful of levels).
Neither is a good fit for the NES's constraints, and NEScript
already surfaces the tradeoff at compile time via the call-depth
limit (`E0401`) and the parameter cap (`E0506`). The direct-write
convention is what makes those limits enforceable.
If you actually need recursion-shaped logic — flood fill, tree
walking, tile-spread simulations — the idiomatic pattern is an
explicit stack held in a small `u8` array:
```
const MAX_STACK: u8 = 32
var stack: u8[MAX_STACK] = [0; 32]
var top: u8 = 0
fun flood_push(x: u8) {
stack[top] = x
top += 1
}
fun flood_pop() -> u8 {
top -= 1
return stack[top]
}
fun flood_fill(start: u8) {
flood_push(start)
while top > 0 {
var here: u8 = flood_pop()
// ...process `here`, push neighbours that need visiting...
}
}
```
This gives the compiler full visibility into the worst-case stack
depth (`MAX_STACK`), uses flat RAM instead of the hardware stack,
and composes cleanly with the call-graph validator.
---
## States