mirror of
https://github.com/imjasonh/nescript
synced 2026-07-08 08:55:38 +00:00
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
203 lines
9.7 KiB
Markdown
203 lines
9.7 KiB
Markdown
# 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|
|