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
9.7 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 logicX(Index X) -- 8-bit, used for indexing and countingY(Index Y) -- 8-bit, used for indexing and countingSP(Stack Pointer) -- 8-bit, points into the $0100-$01FF rangeP(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 forfastvariables (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 bycycle_sprites(only when the program emits acycle_spritesstatement)$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
$0200and 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 framecode runs during the visible frame; the implicitwait_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:
- Write
$01then$00to$4016to strobe (latch button states) - 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 |