1
0
Fork 0
mirror of https://github.com/imjasonh/nescript synced 2026-07-08 17:06:04 +00:00

compiler: PRNG / edge input / palette fade / AxROM / CNROM / FCEUX labels

Closes seven of the cc65/nesdoug parity gaps catalogued in
docs/future-work.md in a single pass. All of the new features are
gated on marker labels so programs that don't use them produce
byte-identical ROM output (every pre-existing committed .nes file
round-trips unchanged).

Language / runtime additions:
- `rand8()` / `rand16()` / `seed_rand(u16)` intrinsics backed by a
  16-bit Galois LFSR (~30 bytes of runtime, ~40 cycles per draw).
  Reset path seeds state to 0xACE1 so the first draw is useful even
  without explicit seeding.
- `p1.button.a.pressed` / `.released` edge-triggered input via a
  new ReadInputEdge IR op plus an NMI-side prev-frame snapshot into
  $07E6/$07E7, gated on the `__edge_input_used` marker.
- `set_palette_brightness(level)` builtin mapping levels 0..8 to
  PPU mask emphasis bytes (`$2001`) for neslib-style screen fades.
- `mapper: AxROM` (iNES 7) with automatic 32 KB PRG padding so
  emulators that enforce mapper-7's 32 KB page size boot cleanly.
- `mapper: CNROM` (iNES 3) with a reset-time CHR bank 0 select.
- `--fceux-labels <prefix>` CLI flag emitting per-bank `.nl` label
  files and a `.ram.nl` file for FCEUX's debugger.

Tests + examples:
- Five new example programs with committed .nes ROMs and
  pixel+audio goldens: prng_demo, edge_input_demo,
  palette_brightness_demo, axrom_simple, cnrom_simple.
- Seven integration tests covering JSR emission, the
  omitted-when-unused invariant, the NMI prev-input snapshot, the
  correct mapper numbers for AxROM/CNROM, and negative tests for
  unknown button names and bad rand8 arity.
- `is_intrinsic()` now runs in expression-position Call paths too,
  so `var x = rand8(1, 2)` errors at compile time instead of
  silently dropping the extra arguments.
This commit is contained in:
Claude 2026-04-18 18:13:18 +00:00
parent c96135fd86
commit 7507459787
No known key found for this signature in database
35 changed files with 1272 additions and 22 deletions

View file

@ -41,6 +41,11 @@ Open any `.nes` file in an NES emulator ([Mesen](https://www.mesen.ca/), [FCEUX]
| `pong.ne` | **production-quality Pong**, powerups, multi-ball, multi-file | A complete Pong game split across `examples/pong/*.ne`. CPU VS CPU / 1 PLAYER / 2 PLAYERS title menu with brisk pulse-2 title march and autopilot, smooth ball physics with wall and paddle bouncing, CPU AI that tracks the ball with a reaction lag and dead zone, three powerup types (LONG paddle for 5 hits, FAST ball on next hit, MULTI-ball on next hit spawning 3 balls) that bounce around the field and are caught by paddle AABB overlap, multi-ball scoring (each ball scores a point, round continues until last ball exits), inner phase machine (`P_SERVE`/`P_PLAY`/`P_POINT`), and a "PLAYER N WINS" victory screen with the builtin fanfare. First-to-7 wins. |
| `feature_canary.ne` | **regression canary**, state-locals, uninitialized struct-field writes, u16, arrays, `slow` placement, function returns | A minimal program whose sole job is to paint a green universal backdrop at frame 180 when every memory-affecting language construct round-trips a write through the compiler correctly, and to flip to red if any check fails. Each check writes a distinctive byte through one construct (state-local, uninit struct field, u8/u16 global, array element, `slow`-placed u8, function call return), reads it back, and clears `all_ok` on mismatch. Because the emulator harness compares pixels at frame 180, any compiler regression that silently drops one of these writes turns the committed golden red — the structural counter to the "goldens capture whatever happens, not what should happen" failure mode that let PR #31 survive for a year. |
| `sha256.ne` | **interactive SHA-256**, inline-asm 32-bit primitives, multi-file | A full FIPS 180-4 SHA-256 hasher split across `examples/sha256/*.ne`. An on-screen 5×8 keyboard grid lets the player type up to 16 ASCII characters (`A`..`Z`, `0`..`9`, space, `.`, backspace, enter), and pressing ↵ runs the 48-entry message-schedule expansion + 64-round compression on the NES itself. Every 32-bit primitive (`copy`, `xor`, `and`, `add`, `not`, rotate-right, shift-right) is hand-tuned inline assembly that walks the four little-endian bytes of a word with `LDA {wk},X` / `ADC {wk},Y` chains, so a whole round costs a few thousand cycles. The phased driver runs four schedule steps or four rounds per frame so the full compression finishes well under a second, and the 64-character hex digest renders as sprites in 8 rows of 8 glyphs at the bottom of the screen. The jsnes golden auto-types `"NES"` after 1 s of keyboard idle and captures its hash `AE9145DB5CABC41FE34B54E34AF8881F462362EA20FD8F861B26532FFBB84E0D`. |
| `prng_demo.ne` | `rand8()`, `rand16()`, `seed_rand()` | Exercises the runtime xorshift PRNG end-to-end. Four sprite positions are drawn from fresh `rand8()` draws every frame with a `rand16()` sample mixed in. `seed_rand(0x1234)` pins the initial state so the golden is deterministic. The `__rand_used` marker gates linking of `gen_prng` + the reset-time seed — programs that never call any of the three get zero ROM / cycle overhead. |
| `edge_input_demo.ne` | `p1.button.a.pressed`, `p1.button.b.released` | Demonstrates edge-triggered input. The A-sprite advances exactly once per press transition (holding the button does nothing) and the B-sprite advances on release. Lowering emits `IrOp::ReadInputEdge`, which stores the previous-frame input byte into main RAM and XORs it against the current byte at the read site. The NMI handler snapshots both prev bytes before strobing, gated on the `__edge_input_used` marker. |
| `palette_brightness_demo.ne` | `set_palette_brightness(level)` | Cycles through the 9 brightness levels (0 = blank, 4 = normal, 8 = max emphasis) every 20 frames. Exercises the neslib-style `pal_bright` mapping onto `$2001` PPU mask emphasis bits. The runtime routine `__set_palette_brightness` is spliced in only when user code references the builtin. |
| `axrom_simple.ne` | `mapper: AxROM` (mapper 7) | Single-screen AxROM demo. The linker pads PRG to 32 KB (one blank 16 KB bank plus our 16 KB fixed bank) so emulators that enforce mapper-7's 32 KB page size boot cleanly. Register layout: bit 4 of `$8000` selects single-screen lower / upper nametable. |
| `cnrom_simple.ne` | `mapper: CNROM` (mapper 3) | CNROM demo. Fixed 32 KB PRG, switchable 8 KB CHR. Single-bank CNROM is functionally equivalent to NROM at the PRG level, but the iNES header reports mapper 3 and the runtime writes a CHR bank 0 select at reset. |
## Emulator Controls

25
examples/axrom_simple.ne Normal file
View file

@ -0,0 +1,25 @@
// AxROM (mapper 7) demo — single 32 KB PRG with single-screen
// mirroring. Most homebrew AxROM games have multiple 32 KB pages;
// this minimal demo runs in bank 0 of a 32 KB-padded ROM to
// exercise the mapper's reset, bank-select register, and iNES
// header emission.
game "AxROM Demo" {
mapper: AxROM
}
var px: u8 = 120
var dx: u8 = 1
on frame {
if dx == 1 {
px += 1
if px >= 240 { dx = 0 }
} else {
px -= 1
if px == 0 { dx = 1 }
}
draw Ball at: (px, 100)
}
start Main

BIN
examples/axrom_simple.nes Normal file

Binary file not shown.

24
examples/cnrom_simple.ne Normal file
View file

@ -0,0 +1,24 @@
// CNROM (mapper 3) demo — fixed 32 KB PRG, switchable 8 KB CHR.
// Single-bank CNROM exercises the mapper reset, header emission,
// and compatible runtime — functionally equivalent to NROM but
// with a different iNES mapper number.
game "CNROM Demo" {
mapper: CNROM
}
var px: u8 = 120
var dx: u8 = 1
on frame {
if dx == 1 {
px += 1
if px >= 240 { dx = 0 }
} else {
px -= 1
if px == 0 { dx = 1 }
}
draw Ball at: (px, 100)
}
start Main

BIN
examples/cnrom_simple.nes Normal file

Binary file not shown.

View file

@ -0,0 +1,42 @@
// Edge-triggered input demo — the A / B buttons toggle a pair
// of sprites using `.pressed` / `.released` rather than the
// level-state `p1.button.a`. Held buttons only fire once per
// press, which is the canonical menu / one-shot action pattern.
game "Edge Input Demo" {
mapper: NROM
}
var ax: u8 = 64
var bx: u8 = 120
var toggle_a: u8 = 0
var toggle_b: u8 = 0
on frame {
// Each press moves the A-sprite 8 pixels right. Because
// `.pressed` fires once per press transition, holding the
// button down does not accelerate movement.
if p1.button.a.pressed {
ax += 8
toggle_a = toggle_a ^ 1
}
// Release moves the B-sprite 4 pixels right on letting go.
if p1.button.b.released {
bx += 4
toggle_b = toggle_b ^ 1
}
// Drive the frame colours off the toggles so the output is
// observable in the emulator golden.
draw Ball at: (ax, 80)
draw Ball at: (bx, 120)
// A tiny visual beacon for "toggle_a" / "toggle_b" so the
// golden captures the state even before the user presses
// anything.
if toggle_a == 1 { draw Ball at: (ax, 90) }
if toggle_b == 1 { draw Ball at: (bx, 130) }
}
start Main

Binary file not shown.

View file

@ -0,0 +1,28 @@
// Palette brightness demo — cycle through the 9 brightness levels
// every ~20 frames. Exercises the `set_palette_brightness` builtin,
// which writes PPU mask emphasis bits for cheap neslib-style fades.
game "Palette Brightness Demo" {
mapper: NROM
}
var frame: u8 = 0
var level: u8 = 4 // normal
on frame {
frame += 1
// Every 20 frames, bump the brightness level. Roll back to 0
// at 9 so we cycle through the full 0..8 range.
if frame >= 20 {
frame = 0
level += 1
if level >= 9 {
level = 0
}
set_palette_brightness(level)
}
draw Ball at: (120, 100)
}
start Main

Binary file not shown.

45
examples/prng_demo.ne Normal file
View file

@ -0,0 +1,45 @@
// PRNG demo — sprite positions driven by the runtime PRNG.
//
// Each frame draws four sprites at addresses pulled from the
// xorshift-style `rand8()` intrinsic, with an extra `rand16()`
// sample binned into a horizontal velocity. Exercises `rand8`,
// `rand16`, and `seed_rand` end-to-end.
game "PRNG Demo" {
mapper: NROM
}
var seeded: u8 = 0
on frame {
if seeded == 0 {
// Pin the seed so the recording is deterministic. The
// runtime forces bit 0 of the low byte high so a zero
// seed doesn't stick the LFSR.
seed_rand(0x1234)
seeded = 1
}
// Four random sprites per frame. Each call pulls fresh
// entropy from the shared PRNG state, so the positions
// drift over time instead of staying fixed.
var x1: u8 = rand8()
var y1: u8 = rand8()
draw Ball at: (x1, y1)
var x2: u8 = rand8()
var y2: u8 = rand8()
draw Ball at: (x2, y2)
// rand16() returns u16; truncate to u8 for the draw.
var r: u16 = rand16()
var x3: u8 = r as u8
var y3: u8 = (r >> 8) as u8
draw Ball at: (x3, y3)
var x4: u8 = rand8()
var y4: u8 = rand8()
draw Ball at: (x4, y4)
}
start Main

BIN
examples/prng_demo.nes Normal file

Binary file not shown.