Add comprehensive documentation
docs/language-guide.md (895 lines):
Complete reference for every language feature with code examples.
Covers: program structure, types, expressions, statements, assets,
mappers, error codes, and compiler flags.
docs/architecture.md (130 lines):
Compiler pipeline overview, module descriptions, testing guide.
docs/nes-reference.md (190 lines):
NES hardware quick reference: CPU, memory map, PPU, iNES format.
https://claude.ai/code/session_01W6eQFStA66EuMKHUFo2rx3
2026-04-12 00:41:34 +00:00
# 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:
W0110 inline fallback warning + docs refresh
W0110: when a function marked `inline` has a body shape the IR
lowerer can't splice (conditional early return, loops, nested
control flow, empty void body), the analyzer now emits a
warning at the declaration site so the declined hint is
visible instead of silently falling back to a regular JSR.
Implementation:
- New `W0110` error code in `src/errors/diagnostic.rs` (warning level).
- New `pub fn can_inline_fun(return_type, body) -> bool` in
`src/ir/lowering.rs`, extracted from the existing capture
logic so the analyzer and the IR lowerer share the same
eligibility rules and can never drift.
- New `check_inline_declinability` analyzer pass called from
the tail of `analyze_program`, mirroring the existing
`check_sprite_scanline_budget` / `check_unreachable_states`
passes. Emits W0110 with help + note text pointing at the
two accepted body shapes.
- `capture_inline_bodies` now defers to `can_inline_fun`
instead of duplicating the match pattern, so the two sides
stay in lockstep by construction.
Four regression tests in `src/analyzer/tests.rs` cover the
conditional-return and while-loop declines plus the two
accepted shapes (single-return expression, void sequence).
Example source cleanups: `wrap52` in `examples/war/deck.ne`
and `abs_diff` in both `examples/arrays_and_functions.ne` and
`examples/loop_break_continue.ne` drop the `inline` keyword.
All three were dead hints — the `inline` was being silently
declined before this change, so removing it is source-only;
the three ROMs are byte-identical, all 32 emulator goldens
still match.
Docs refresh
- `docs/language-guide.md`: rewrote the Inline Functions section
(real behaviour + W0110), added W0105/W0106/W0107/W0108/W0109/
W0110 to the warnings table, added the `debug.sprite_overflow*`
builtins + sprite-per-scanline mitigations section to the
Debug Mode docs, added a `cycle_sprites` statement entry and
cross-referenced it from `draw`.
- `docs/nes-reference.md`: fleshed out the "NEScript Memory
Usage" block with the full ZP + high-RAM layout, including
the new `$07EF` / `$07FC` / `$07FD` slots for sprite cycling
and the debug sprite-overflow telemetry.
- `docs/future-work.md`: documented all four debug query
builtins in the "What ships today" block; updated the open
"OAM allocation strategy" question to reference the shipped
`cycle_sprites` path and ask about an automatic-flicker
game attribute as a follow-up.
- `docs/architecture.md`: updated the `ir/` and `optimizer/`
module summaries to describe real inline splicing (now
in lowering, not the optimizer).
- `README.md`: reframed the `inline` bullet from "hint" to
"real splicing for single-return / void-body shapes";
expanded the debug-support bullet to mention the four
query builtins and their stripping in release builds; added
a new bullet for the three-layer sprite-per-scanline
mitigations; bumped the test count from 497 → 694; updated
the war.ne entry to mention the seven compiler bugs are all
fixed and point readers at `git log` (instead of the
deleted COMPILER_BUGS.md).
- `examples/README.md`: same `git log`-pointing rewrite for
the war.ne entry.
Deletions
- `examples/war/COMPILER_BUGS.md` is removed. All seven
catalogued bugs are fixed; the file's historical value
lives in `git log` now. Every source-code comment and doc
reference to the file has been updated to either point at
`git log` or just describe the bug in place.
Test count: 616 unit + 75 integration + 3 doctests = 694 total.
Clippy / fmt clean. 32/32 emulator goldens match.
https://claude.ai/code/session_0143dTgh3UeRrtfHgQwzcv5z
2026-04-15 23:19:07 +00:00
- `$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)
Add comprehensive documentation
docs/language-guide.md (895 lines):
Complete reference for every language feature with code examples.
Covers: program structure, types, expressions, statements, assets,
mappers, error codes, and compiler flags.
docs/architecture.md (130 lines):
Compiler pipeline overview, module descriptions, testing guide.
docs/nes-reference.md (190 lines):
NES hardware quick reference: CPU, memory map, PPU, iNES format.
https://claude.ai/code/session_01W6eQFStA66EuMKHUFo2rx3
2026-04-12 00:41:34 +00:00
- `$0200` -`$02FF` : OAM shadow buffer (DMA'd to PPU each frame)
W0110 inline fallback warning + docs refresh
W0110: when a function marked `inline` has a body shape the IR
lowerer can't splice (conditional early return, loops, nested
control flow, empty void body), the analyzer now emits a
warning at the declaration site so the declined hint is
visible instead of silently falling back to a regular JSR.
Implementation:
- New `W0110` error code in `src/errors/diagnostic.rs` (warning level).
- New `pub fn can_inline_fun(return_type, body) -> bool` in
`src/ir/lowering.rs`, extracted from the existing capture
logic so the analyzer and the IR lowerer share the same
eligibility rules and can never drift.
- New `check_inline_declinability` analyzer pass called from
the tail of `analyze_program`, mirroring the existing
`check_sprite_scanline_budget` / `check_unreachable_states`
passes. Emits W0110 with help + note text pointing at the
two accepted body shapes.
- `capture_inline_bodies` now defers to `can_inline_fun`
instead of duplicating the match pattern, so the two sides
stay in lockstep by construction.
Four regression tests in `src/analyzer/tests.rs` cover the
conditional-return and while-loop declines plus the two
accepted shapes (single-return expression, void sequence).
Example source cleanups: `wrap52` in `examples/war/deck.ne`
and `abs_diff` in both `examples/arrays_and_functions.ne` and
`examples/loop_break_continue.ne` drop the `inline` keyword.
All three were dead hints — the `inline` was being silently
declined before this change, so removing it is source-only;
the three ROMs are byte-identical, all 32 emulator goldens
still match.
Docs refresh
- `docs/language-guide.md`: rewrote the Inline Functions section
(real behaviour + W0110), added W0105/W0106/W0107/W0108/W0109/
W0110 to the warnings table, added the `debug.sprite_overflow*`
builtins + sprite-per-scanline mitigations section to the
Debug Mode docs, added a `cycle_sprites` statement entry and
cross-referenced it from `draw`.
- `docs/nes-reference.md`: fleshed out the "NEScript Memory
Usage" block with the full ZP + high-RAM layout, including
the new `$07EF` / `$07FC` / `$07FD` slots for sprite cycling
and the debug sprite-overflow telemetry.
- `docs/future-work.md`: documented all four debug query
builtins in the "What ships today" block; updated the open
"OAM allocation strategy" question to reference the shipped
`cycle_sprites` path and ask about an automatic-flicker
game attribute as a follow-up.
- `docs/architecture.md`: updated the `ir/` and `optimizer/`
module summaries to describe real inline splicing (now
in lowering, not the optimizer).
- `README.md`: reframed the `inline` bullet from "hint" to
"real splicing for single-return / void-body shapes";
expanded the debug-support bullet to mention the four
query builtins and their stripping in release builds; added
a new bullet for the three-layer sprite-per-scanline
mitigations; bumped the test count from 497 → 694; updated
the war.ne entry to mention the seven compiler bugs are all
fixed and point readers at `git log` (instead of the
deleted COMPILER_BUGS.md).
- `examples/README.md`: same `git log`-pointing rewrite for
the war.ne entry.
Deletions
- `examples/war/COMPILER_BUGS.md` is removed. All seven
catalogued bugs are fixed; the file's historical value
lives in `git log` now. Every source-code comment and doc
reference to the file has been updated to either point at
`git log` or just describe the bug in place.
Test count: 616 unit + 75 integration + 3 doctests = 694 total.
Clippy / fmt clean. 32/32 emulator goldens match.
https://claude.ai/code/session_0143dTgh3UeRrtfHgQwzcv5z
2026-04-15 23:19:07 +00:00
- `$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.
Add comprehensive documentation
docs/language-guide.md (895 lines):
Complete reference for every language feature with code examples.
Covers: program structure, types, expressions, statements, assets,
mappers, error codes, and compiler flags.
docs/architecture.md (130 lines):
Compiler pipeline overview, module descriptions, testing guide.
docs/nes-reference.md (190 lines):
NES hardware quick reference: CPU, memory map, PPU, iNES format.
https://claude.ai/code/session_01W6eQFStA66EuMKHUFo2rx3
2026-04-12 00:41:34 +00:00
---
## 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|
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
---
## 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.