1
0
Fork 0
mirror of https://github.com/imjasonh/nescript synced 2026-07-08 00:45:38 +00:00
nescript/docs/nes-reference.md
Claude 7b4570eee5
compiler: i16 / SRAM saves / inline-asm dot labels / docs
Another batch from the cc65/nesdoug catalogue. All gated on
parser-level opt-in or default-false attributes so existing
programs produce byte-identical ROMs (no committed .nes file
changed).

**§A — `i16` signed 16-bit type:**
- New `KwI16` lexer token, `NesType::I16` AST variant, parser
  case in `parse_type`. Type-size and integer-type tables
  treat `i16` like `u16` (2 bytes, integer).
- IR lowering accepts `i16` everywhere it accepts `u16` for
  wide-load / wide-store / widen-narrow paths.
- New constant fold for `UnaryOp::Negate(IntLiteral(v))` that
  emits the wide two's-complement form. Without it, `var vy:
  i16 = -10` would zero-extend to `$00F6` (= 246) instead of
  sign-extending to `$FFF6` (= -10). Negative literals now
  store the right bytes.
- Comparisons reuse the existing unsigned 16-bit compare ops
  (matching the existing `i8` behaviour). Documented in the
  `NesType::I16` doc comment and in `future-work.md` §A.
- Example `examples/i16_demo.ne` with committed golden.
- Tests cover the literal-fold sign-extension and end-to-end
  compile of the example.

**§S — SRAM / battery-backed saves:**
- New `save { var ... }` top-level block. Lexer + parser opt
  into a dedicated `KwSave` token. Analyzer allocates save
  vars from a separate `next_sram_addr` bump pointer starting
  at `$6000`, capped at `$8000` (8 KB cartridge SRAM window).
- Linker reads `analysis.has_battery_saves` and flips iNES
  byte-6 bit-1 via the new `RomBuilder::set_battery` /
  `Linker::with_battery` chain.
- New `W0111` warning for save-var initializers — SRAM is
  preserved across power cycles, so an init expression would
  either silently never run or clobber persisted data on
  every boot. The warning teaches the user about the
  magic-byte sentinel pattern.
- Struct fields in save blocks are explicitly rejected for now
  (the field-flattening path uses the main-RAM allocator).
- Example `examples/sram_demo.ne` with committed golden, plus
  4 integration tests.

**§D (partial) — inline-asm `.label:` syntax:**
- Codegen-side mangler rewrites `.IDENT` → `__ilab_<N>_IDENT`
  per inline-asm block, where `<N>` is the call site's
  monotonic suffix. Two `asm { .loop: ... }` blocks in the
  same function now coexist without colliding in the linker's
  label table.
- Bounds checks on `.` placement: `$2002` and `name.field`
  are unaffected; only `.IDENT` in label / branch context
  triggers the rewrite. Two integration tests pin the
  uniqueness and dollar-vs-dot disambiguation.

**§X follow-up — Mesen trace-log docs:**
- New "Debugger-assisted workflows" section in
  `docs/nes-reference.md` walking through the Mesen / FCEUX
  log workflows alongside the new `debug_port:` attribute.

**Misc:**
- `future-work.md` updated to mark the shipped items out of
  the catalogue and reshuffle the priority ranking. Remaining
  niche follow-ups (signedness on Cmp16, struct save fields,
  inline-asm format specifiers) documented inline so future
  passes know the design.

All 757 tests pass. Clippy clean. 46/46 emulator goldens match.
2026-04-18 20:49:06 +00:00

12 KiB

NES Hardware Quick Reference

A concise reference to the NES hardware for NEScript contributors. Understanding these constraints explains many of the compiler's design decisions.


CPU: Ricoh 2A03 (MOS 6502 variant)

  • Clock speed: 1.79 MHz (NTSC), 1.66 MHz (PAL)
  • Architecture: 8-bit data bus, 16-bit address bus
  • Registers:
    • A (Accumulator) -- 8-bit, used for arithmetic and logic
    • X (Index X) -- 8-bit, used for indexing and counting
    • Y (Index Y) -- 8-bit, used for indexing and counting
    • SP (Stack Pointer) -- 8-bit, points into the $0100-$01FF range
    • P (Status) -- 8-bit flags: N(egative), V(overflow), B(reak), D(ecimal), I(nterrupt), Z(ero), C(arry)
    • PC (Program Counter) -- 16-bit
  • No multiply, divide, or floating point instructions
  • Instruction set: 56 official opcodes with multiple addressing modes (implied, immediate, zero page, zero page X/Y, absolute, absolute X/Y, indirect, indexed indirect, indirect indexed)

Why This Matters for NEScript

The 6502's 3-register architecture drives the compiler's register allocator. The lack of multiply/divide means the compiler emits software routines for *, /, and %. Zero-page addressing is 1 byte shorter and 1 cycle faster than absolute addressing, which is why fast variable placement matters.


Memory Map

Address Range Size Description
$0000-$00FF 256 bytes Zero page -- fast access
$0100-$01FF 256 bytes Stack (grows downward)
$0200-$02FF 256 bytes OAM shadow buffer (convention)
$0300-$07FF 1280 bytes General purpose RAM
$0800-$1FFF -- Mirrors of $0000-$07FF
$2000-$2007 8 bytes PPU registers
$2008-$3FFF -- Mirrors of PPU registers
$4000-$4017 24 bytes APU and I/O registers
$4018-$401F 8 bytes CPU test mode (normally disabled)
$4020-$5FFF -- Expansion ROM (mapper-dependent)
$6000-$7FFF 8 KB SRAM / PRG RAM (if present)
$8000-$BFFF 16 KB PRG ROM lower bank
$C000-$FFFF 16 KB PRG ROM upper bank (or fixed bank)

Interrupt Vectors

Address Vector Description
$FFFA NMI Non-Maskable Interrupt (vertical blank)
$FFFC RESET Power-on / reset entry point
$FFFE IRQ Interrupt Request (mapper-dependent)

NEScript Memory Usage

The compiler reserves the following:

  • $00-$0F: System use (frame counter, input, OAM cursor, SFX/music pointers, mul/div scratch)
  • $10: ZP_BANK_CURRENT (current switchable PRG bank index, only in banked programs)
  • $11-$17: PPU update slots (palette/nametable flags and pending pointers, only when the program declares palette or background blocks)
  • $18-$7F: Available for fast variables (user zero-page)
  • $80-$FF: IR codegen temp slots (scratch for expression evaluation)
  • $0200-$02FF: OAM shadow buffer (DMA'd to PPU each frame)
  • $0300-$07EE: General variables, per-function RAM (parameter spill + locals), state-local storage
  • $07EF: SPRITE_CYCLE_ADDR — rotating offset byte used by cycle_sprites (only when the program emits a cycle_sprites statement)
  • $07F0-$07F7: Audio channel state (noise/triangle/sfx-pitch pointers; only when the program declares a matching sfx)
  • $07FC: DEBUG_SPRITE_OVERFLOW_FLAG_ADDR — per-frame sticky bit (debug builds only)
  • $07FD: DEBUG_SPRITE_OVERFLOW_COUNT_ADDR — cumulative PPU sprite overflow counter (debug builds only)
  • $07FE: DEBUG_FRAME_OVERRUN_FLAG_ADDR — per-frame sticky bit (debug builds only)
  • $07FF: DEBUG_FRAME_OVERRUN_ADDR — cumulative frame overrun counter (debug builds only)

Release-mode programs that don't opt into audio, banking, debug, or sprite cycling leave the corresponding slots untouched and the analyzer is free to allocate user globals over them.


PPU (Picture Processing Unit)

  • Resolution: 256 x 240 pixels
  • Refresh rate: 60 Hz (NTSC), 50 Hz (PAL)
  • VRAM: 2 KB internal (2 nametables), expandable by mapper
  • Pattern tables: 2 tables of 256 tiles each (one for backgrounds, one for sprites), stored in CHR ROM/RAM
  • Tile size: 8x8 pixels (or 8x16 for tall sprites)
  • Color depth: 2 bits per pixel (4 colors per tile, selected from a sub-palette)

Nametables

  • 4 logical nametables, each 960 bytes of tile indices + 64 bytes of attribute data
  • With 2 KB VRAM, 2 physical nametables exist; the other 2 are mirrors
  • Mirroring arrangement (horizontal or vertical) is set by the cartridge/mapper

Sprites (OAM)

  • 64 sprites total, each defined by 4 bytes:
    • Byte 0: Y position
    • Byte 1: Tile index
    • Byte 2: Attributes (palette, priority, flip H/V)
    • Byte 3: X position
  • 8 sprites per scanline maximum (excess sprites are dropped)
  • OAM is 256 bytes, typically shadow-buffered at CPU $0200 and DMA'd via $4014

Palettes

  • Background: 4 sub-palettes, each with 4 colors (first color shared across all)
  • Sprite: 4 sub-palettes, each with 4 colors (first color is transparent)
  • Colors are indices into the NES master palette (64 entries, $00-$3F)
  • Palette RAM is 32 bytes total at PPU $3F00-$3F1F

PPU Registers

Address Name Description
$2000 PPUCTRL NMI enable, sprite size, pattern table select, nametable select
$2001 PPUMASK Color emphasis, sprite/background enable, clipping
$2002 PPUSTATUS Vblank flag, sprite 0 hit, sprite overflow
$2003 OAMADDR OAM address for writes
$2004 OAMDATA OAM data read/write
$2005 PPUSCROLL Scroll position (write twice: X, Y)
$2006 PPUADDR VRAM address (write twice: high, low)
$2007 PPUDATA VRAM data read/write

Rendering Timing

  • Vblank starts at scanline 241 and lasts ~20 scanlines (~2,273 CPU cycles)
  • PPU updates (palette, nametable, scroll) must happen during vblank
  • on frame code runs during the visible frame; the implicit wait_frame() yields until the next vblank
  • Approximately 29,780 CPU cycles per frame (NTSC)

APU (Audio Processing Unit)

Channel Type Description
Pulse 1 Square wave Variable duty cycle (12.5/25/50/75%)
Pulse 2 Square wave Same as Pulse 1
Triangle Triangle Fixed volume, good for bass
Noise Noise Pseudo-random, for percussion
DMC Sample 1-bit delta-modulated samples

APU registers span $4000-$4017. The audio driver (included automatically when sfx or music declarations exist) runs during NMI.


iNES ROM Format

The standard ROM file format for NES emulators:

Offset  Size     Description
------  -------  -----------
0       4 bytes  Magic number: "NES" followed by $1A
4       1 byte   PRG ROM size in 16 KB units
5       1 byte   CHR ROM size in 8 KB units
6       1 byte   Flags 6: mapper (low nibble), mirroring, battery, trainer
7       1 byte   Flags 7: mapper (high nibble), VS/Playchoice, NES 2.0
8-15    8 bytes  Padding (zeros)
16+     varies   PRG ROM data (N x 16384 bytes)
after   varies   CHR ROM data (N x 8192 bytes)

Common Mapper Numbers

Number Name PRG ROM CHR Notes
0 NROM 16/32 KB 8 KB ROM No bank switching
1 MMC1 Up to 256 KB Up to 128 KB Switchable 16 KB PRG banks
2 UxROM Up to 256 KB 8 KB RAM Switchable 16 KB PRG banks
4 MMC3 Up to 512 KB Up to 256 KB Scanline counter, 8 KB banks

Mirroring

  • Horizontal: nametables A-A-B-B (vertical scrolling games)
  • Vertical: nametables A-B-A-B (horizontal scrolling games)
  • Set in byte 6, bit 0 of the iNES header (0 = horizontal, 1 = vertical)

Controller

Each controller has 8 buttons read as a serial shift register via $4016 (port 1) and $4017 (port 2).

Read sequence:

  1. Write $01 then $00 to $4016 to strobe (latch button states)
  2. Read $4016 (or $4017) 8 times; bit 0 of each read is one button

Button order: A, B, Select, Start, Up, Down, Left, Right.

The NEScript runtime handles this automatically. The programmer reads button state via button.a, button.up, etc.


Key Constraints for NEScript

Constraint Limit NEScript Response
Total RAM 2 KB Static allocation, no heap
Zero page 256 bytes fast/slow hints, compiler promotion
Stack 256 bytes Call depth limit, no recursion
Sprites per frame 64 Compiler manages OAM buffer
Sprites per scanline 8 Hardware limit, no workaround
Vblank time ~2,273 cycles PPU updates must be fast
Frame budget ~29,780 cycles Frame overrun detection in debug mode
No multiply/divide HW -- Software routines, power-of-2 optimization

Debugger-assisted workflows

NEScript compiles three debugger-friendly sidecar files that Mesen, Mesen2, and FCEUX can load alongside the .nes. All three are off by default (release ROMs should be as small as possible) and enabled per-run via CLI flags:

Flag Format Consumers
--symbols <path.mlb> Mesen labels (P: / R: lines) Mesen, Mesen2
--dbg <path.dbg> ca65 debug-info format Mesen, Mesen2, FCEUX
--fceux-labels <prefix> <prefix>.<bank>.nl + .ram.nl FCEUX

Mesen trace-log

Mesen supports address-based execution tracing via its log / save-log scripting APIs. Combined with NEScript's debug.log builtin, the standard workflow is:

  1. Build with --debug and --symbols out.mlb. debug.log(value) writes value to the emulator debug port every time it executes.

  2. In the game { } block, set debug_port: mesen so writes land at $4018 (Mesen's documented tracing port):

    game "MyGame" {
        mapper: NROM
        debug_port: mesen
    }
    
  3. In Mesen, enable the trace log (Debug → Trace Logger), load the .mlb, and filter by memory operation on $4018. Each debug.log(x) call appears in the trace with the source function + line resolved from the label table.

For FCEUX on Linux, keep the default debug_port: fceux (writes to $4800) and pass --fceux-labels out — FCEUX reads out.<bank>.nl automatically when it opens the ROM from the same directory. FCEUX's conditional-breakpoint syntax can match writes to $4800 directly (A(4800)!=0) so you can break when any log fires.