diff --git a/docs/architecture.md b/docs/architecture.md new file mode 100644 index 0000000..fe51a66 --- /dev/null +++ b/docs/architecture.md @@ -0,0 +1,130 @@ +# Compiler Architecture + +An overview of the NEScript compiler internals for contributors and maintainers. + +--- + +## Pipeline + +``` +Source (.ne) --> Lexer --> Parser --> Analyzer --> IR Lowering --> Optimizer --> Codegen --> Assembler --> Linker --> ROM (.nes) +``` + +Each phase is a pure function (input to output) with no global state, making every stage independently testable. + +``` +.ne source ---------> Lexer -----> Parser -----> Analyzer ------> + (tokens) (AST) (annotated AST) + +IR Lowering -----> Optimizer -----> Codegen -----> Linker -----> ROM + (IR) (optimized IR) (6502 insns) (.nes file) +``` + +### Phase Summary + +| Phase | Input | Output | Responsibility | +|-----------------|----------------------|------------------------|------------------------------------------------------| +| **Lexer** | Source text | Token stream | Tokenization, number/string literal parsing | +| **Parser** | Token stream | AST | Syntax validation, tree construction | +| **Analyzer** | AST | Annotated AST | Type checking, scope resolution, call graph analysis | +| **IR Lowering** | Annotated AST | NEScript IR | Flatten expressions, expand u16 ops, desugar | +| **Optimizer** | IR | Optimized IR | Constant folding, dead code, ZP promotion, inlining | +| **Codegen** | Optimized IR | 6502 instruction list | Register allocation, instruction selection | +| **Assembler** | 6502 instructions | Byte sequences + fixups| Opcode encoding, address resolution | +| **Linker** | Bytes + assets | .nes file | Bank layout, vectors, iNES header | + +--- + +## Modules + +### `lexer/` +Tokenizes NEScript source text into a stream of typed tokens with source spans. Handles decimal, hex, and binary integer literals, string literals, all keywords, and operators. + +### `parser/` +Recursive descent parser that converts the token stream into an AST. Defines all AST node types (expressions, statements, declarations) in `ast.rs`. + +### `analyzer/` +Performs semantic analysis on the AST: type checking (`types.rs`), scope and symbol table management (`scope.rs`), and call graph construction with depth analysis (`call_graph.rs`). Detects recursion, type mismatches, undefined references, and call depth violations. + +### `ir/` +Defines the intermediate representation and the lowering pass (`lowering.rs`) that translates the annotated AST into IR. Flattens nested expressions, expands 16-bit operations into 8-bit sequences, and resolves syntactic sugar. + +### `optimizer/` +Runs optimization passes over the IR: constant folding (`const_fold.rs`), dead code elimination (`dead_code.rs`), zero-page promotion analysis (`zp_promote.rs`), and function inlining (`inliner.rs`). + +### `codegen/` +Translates optimized IR into 6502 instructions. Includes register allocation for the A/X/Y registers (`regalloc.rs`) and instruction pattern matching for idiomatic 6502 code (`patterns.rs`). + +### `asm/` +The built-in assembler. Encodes 6502 instructions (`encode.rs`) with all addressing modes (`addressing.rs`), using a complete opcode table (`opcodes.rs` -- 56 instructions across all modes). + +### `linker/` +Assigns addresses to code and data segments, resolves fixups/relocations (`fixups.rs`), and handles bank allocation (`banks.rs`) for banked mappers. + +### `rom/` +Builds the final iNES ROM file. Generates the 16-byte iNES header (`header.rs`) and places the NMI/RESET/IRQ vector table (`vectors.rs`). + +### `runtime/` +Contains built-in runtime code that the compiler emits into every ROM: NES hardware initialization (`init.rs`), NMI handler generation (`nmi.rs`), controller read routines (`input.rs`), OAM DMA setup (`oam.rs`), PPU helper routines (`ppu.rs`), and software multiply/divide (`math.rs`). + +### `assets/` +The asset pipeline. Converts PNG images to CHR tile data (`chr.rs`), generates nametables from full-screen images (`nametable.rs`), extracts and maps palettes (`palette.rs`), and handles audio import stubs (`audio.rs`). + +### `debug/` +Debug instrumentation output. Generates source maps relating ROM addresses to source locations (`source_map.rs`), symbol tables compatible with Mesen (`symbols.rs`), and runtime check code for debug builds (`checks.rs`). + +### `errors/` +Error reporting infrastructure. Defines the `Diagnostic` struct with error codes, severity levels, source spans, labels, help text, and notes (`diagnostic.rs`). Renders diagnostics with color and source context for terminal output (`render.rs`). + +--- + +## Testing + +### Test Organization + +Tests are co-located with each module in `tests.rs` files: + +``` +src/lexer/tests.rs -- lexer unit tests +src/parser/tests.rs -- parser unit tests +src/analyzer/tests.rs -- semantic analysis tests +src/ir/tests.rs -- IR lowering tests +src/optimizer/tests.rs -- optimizer tests +src/codegen/tests.rs -- code generation tests +src/asm/tests.rs -- assembler tests +src/linker/tests.rs -- linker tests +src/rom/tests.rs -- ROM builder tests +src/assets/tests.rs -- asset pipeline tests +``` + +Integration tests live in the `tests/` directory: + +``` +tests/integration/ -- full pipeline tests with .ne source files +tests/error_tests/ -- tests that verify specific error codes +tests/asm_tests/ -- 6502 opcode and addressing mode tests +``` + +### Running Tests + +```bash +# Run all tests +cargo test + +# Run tests for a specific module +cargo test --lib lexer +cargo test --lib parser +cargo test --lib analyzer + +# Run integration tests only +cargo test --test '*' + +# Run a specific test by name +cargo test test_name +``` + +### Test Strategy + +Each compiler phase is designed as a pure function, so unit tests provide isolated input and verify output without side effects. Integration tests compile complete `.ne` source files and verify the output ROM matches expected golden files in `tests/integration/expected/`. + +Error tests in `tests/error_tests/` contain intentionally broken programs and verify that the correct error code is produced (e.g., `recursion.ne` should produce `E0402`). diff --git a/docs/language-guide.md b/docs/language-guide.md new file mode 100644 index 0000000..f542e13 --- /dev/null +++ b/docs/language-guide.md @@ -0,0 +1,895 @@ +# NEScript Language Guide + +NEScript is a statically-typed, compiled language designed for NES game development. It compiles directly to 6502 machine code packaged as iNES-format ROMs -- no external assembler or tooling required. + +This guide covers every language feature with practical examples. + +--- + +## Program Structure + +Every NEScript program consists of a game declaration, top-level definitions, and a start declaration. + +``` +game "My Game" { + mapper: NROM + mirroring: vertical +} + +const SPEED: u8 = 2 + +var score: u8 = 0 + +fun helper() -> u8 { + return 42 +} + +state Title { + on frame { + draw Logo at: (100, 100) + if button.start { + transition Playing + } + } +} + +state Playing { + on enter { + score = 0 + } + on frame { + // game logic here + } +} + +start Title +``` + +### Game Declaration + +The `game` block is required and must appear first. It names the game and sets hardware configuration. + +``` +game "Coin Cavern" { + mapper: NROM + mirroring: vertical +} +``` + +Available properties: + +| Property | Values | Default | +|--------------|----------------------------------|--------------| +| `mapper` | `NROM`, `MMC1`, `UxROM`, `MMC3` | required | +| `mirroring` | `horizontal`, `vertical` | `horizontal` | + +### Start Declaration + +Exactly one `start` declaration must exist. It names the initial state entered on power-on. + +``` +start Title +``` + +--- + +## Types + +NEScript has four primitive types and fixed-size arrays. + +### Primitive Types + +| Type | Size | Range | Description | +|--------|---------|-----------------|------------------------------------| +| `u8` | 1 byte | 0 to 255 | Unsigned 8-bit integer | +| `i8` | 1 byte | -128 to 127 | Signed 8-bit integer | +| `u16` | 2 bytes | 0 to 65535 | Unsigned 16-bit integer | +| `bool` | 1 byte | `true` / `false`| Boolean | + +### Arrays + +Arrays are fixed-size, homogeneous, and zero-indexed. The size must be a compile-time constant. Maximum 256 elements. + +``` +var enemies: u8[8] +const TABLE: u8[4] = [10, 20, 30, 40] +``` + +### Type Casting + +NEScript has no implicit coercion. All conversions use `as`: + +``` +var a: u8 = 200 +var b: u16 = a as u16 // zero-extend: 200 +var c: i8 = a as i8 // reinterpret bits +var d: u8 = b as u8 // truncate to low byte +``` + +--- + +## Variables + +### Variable Declarations + +Variables are declared with `var` and must have an explicit type: + +``` +var x: u8 // uninitialized (zeroed on state entry) +var y: u8 = 100 // initialized +var pos: u16 = 0x0400 // 16-bit value +var alive: bool = true +var scores: u8[4] = [0, 0, 0, 0] +``` + +### Constants + +Constants are evaluated at compile time and stored in ROM: + +``` +const MAX_ENEMIES: u8 = 5 +const SPEED: u8 = 3 +const SIN_TABLE: u8[8] = [0, 49, 90, 117, 127, 117, 90, 49] +``` + +### Memory Placement Hints + +The NES has 256 bytes of zero-page RAM with faster access. You can hint where variables should be placed: + +``` +fast var px: u8 // prefer zero-page (faster instructions) +slow var high_score: u16 // prefer upper RAM (saves zero-page space) +var normal: u8 // compiler decides automatically +``` + +If zero-page is exhausted and `fast` variables cannot be placed, the compiler emits error `E0301`. + +### Scope + +| Scope | Declared In | Lifetime | +|----------|----------------|---------------------------------------------| +| Global | Top level | Entire program, permanent RAM allocation | +| State | `state` block | Active while state is active; RAM reusable | +| Function | `fun` block | Duration of function call | +| Block | `if`/`while` | Enclosing block, shares parent allocation | + +--- + +## Functions + +### Declaration + +Functions use `fun`, with optional parameters and return type: + +``` +fun add(a: u8, b: u8) -> u8 { + return a + b +} + +fun reset_score() { + score = 0 +} +``` + +### Inline Functions + +The `inline` keyword hints the compiler to inline the function at call sites: + +``` +inline fun clamp(val: u8, max: u8) -> u8 { + if val > max { + return max + } + return val +} +``` + +`inline` is a hint -- the compiler may decline for large functions. + +### Calling Functions + +``` +var result: u8 = add(10, 20) +reset_score() +``` + +### Restrictions + +- **No recursion.** Both direct and indirect recursion are compile errors (`E0402`). +- **Call depth limit.** The default maximum call depth is 8. Exceeding it produces error `E0401`. + +--- + +## States + +States are the top-level organizational unit. Exactly one state is active at any time. + +### State Declaration + +``` +state Playing { + var timer: u8 = 0 // state-local variable + + on enter { + // runs once when entering this state + timer = 60 + } + + on exit { + // runs once when leaving this state + } + + on frame { + // runs every frame (60 Hz) while this state is active + timer -= 1 + draw Player at: (player_x, player_y) + } +} +``` + +`on frame` is syntactic sugar for a loop with an implicit `wait_frame()` at the end. A state can have any combination of `on enter`, `on exit`, and `on frame`. + +### State Transitions + +``` +transition GameOver +``` + +Transitions are immediate. The current state's `on exit` runs, then the target state's `on enter` runs. The remainder of the current frame handler does not execute. + +--- + +## Expressions + +### Literals + +``` +42 // decimal integer +0xFF // hexadecimal +0b10110001 // binary +1_000 // underscores allowed for readability (if supported) +true // boolean +false // boolean +[1, 2, 3] // array literal +``` + +All integer literals must fit in `u16` (0-65535). The compiler narrows to the required type at usage. + +### Arithmetic Operators + +| Operator | Description | Example | +|----------|----------------|--------------| +| `+` | Addition | `a + b` | +| `-` | Subtraction | `a - b` | +| `*` | Multiplication | `a * b` | +| `/` | Division | `a / b` | +| `%` | Modulo | `a % b` | + +`*`, `/`, and `%` are available but expensive on the 6502 (software routines). The compiler optimizes power-of-two operations to shifts and warns on non-power-of-two multiply/divide. + +### Bitwise Operators + +| Operator | Description | Example | +|----------|----------------|--------------| +| `&` | Bitwise AND | `a & 0x0F` | +| `\|` | Bitwise OR | `a \| 0x80` | +| `^` | Bitwise XOR | `a ^ mask` | +| `~` | Bitwise NOT | `~a` | +| `<<` | Shift left | `a << 2` | +| `>>` | Shift right | `a >> 1` | + +### Comparison Operators + +| Operator | Description | Example | +|----------|-------------------|--------------| +| `==` | Equal | `a == 0` | +| `!=` | Not equal | `a != b` | +| `<` | Less than | `a < 10` | +| `>` | Greater than | `a > max` | +| `<=` | Less or equal | `a <= 255` | +| `>=` | Greater or equal | `a >= min` | + +### Logical Operators + +NEScript uses keyword-based logical operators: + +``` +if alive and (health > 0) { + // ... +} +if not paused or force_update { + // ... +} +``` + +| Operator | Description | +|----------|---------------| +| `and` | Logical AND | +| `or` | Logical OR | +| `not` | Logical NOT | + +### Operator Precedence + +From highest to lowest: + +| Level | Operators | Associativity | +|-------|------------------------------------|---------------| +| 1 | `()` grouping | -- | +| 2 | `-` (unary), `~`, `not` | right | +| 3 | `*`, `/`, `%` | left | +| 4 | `+`, `-` | left | +| 5 | `<<`, `>>` | left | +| 6 | `&` | left | +| 7 | `^` | left | +| 8 | `\|` | left | +| 9 | `==`, `!=`, `<`, `>`, `<=`, `>=` | left | +| 10 | `and` | left | +| 11 | `or` | left | + +### Button Reads + +Read controller input as boolean expressions: + +``` +if button.right { + player_x += SPEED +} +if button.a { + jump() +} +``` + +Available buttons: `up`, `down`, `left`, `right`, `a`, `b`, `start`, `select`. + +For two-player games, prefix with the player: + +``` +if p1.button.a { /* player 1 */ } +if p2.button.right { /* player 2 */ } +``` + +Without a prefix, `button` refers to player 1. + +### Function Calls in Expressions + +``` +var clamped: u8 = clamp_x(player_x + SPEED) +``` + +### Array Indexing + +``` +var val: u8 = table[i] +table[i] = 0 +``` + +### Type Casting + +``` +var wide: u16 = narrow as u16 +``` + +--- + +## Statements + +### Assignment + +``` +x = 10 +x += 5 +x -= 1 +x &= 0x0F +x |= 0x80 +x ^= mask +``` + +All assignment operators: + +| Operator | Description | +|----------|---------------------| +| `=` | Assign | +| `+=` | Add and assign | +| `-=` | Subtract and assign | +| `&=` | AND and assign | +| `\|=` | OR and assign | +| `^=` | XOR and assign | + +Array element assignment: + +``` +enemies[i] = 0 +scores[player] += 10 +``` + +### If / Else If / Else + +Braces are always required. No ternary operator. + +``` +if health == 0 { + transition GameOver +} else if health < 3 { + flash_warning() +} else { + // normal gameplay +} +``` + +### While Loop + +``` +var i: u8 = 0 +while i < 10 { + enemies[i] = 0 + i += 1 +} +``` + +### Loop (Infinite) + +``` +loop { + wait_frame() + if button.start { + break + } +} +``` + +The compiler warns if a `loop` contains neither `break`, `wait_frame`, nor `transition`. + +### Break and Continue + +``` +var i: u8 = 0 +while i < 20 { + i += 1 + if enemies[i] == 0 { + continue // skip inactive enemies + } + if i > 10 { + break // stop processing + } + update_enemy(i) +} +``` + +### Return + +``` +fun abs_diff(a: u8, b: u8) -> u8 { + if a > b { + return a - b + } + return b - a +} +``` + +Functions without a return type use `return` with no value (or simply reach the end of the function body). + +### Draw + +Render a sprite to the screen: + +``` +draw Player at: (player_x, player_y) +draw Coin at: (COIN_X, COIN_Y) frame: anim_frame +``` + +The `draw` statement writes to the OAM shadow buffer. The NES supports up to 64 sprites per frame. + +Syntax: `draw SpriteName at: (x_expr, y_expr) [frame: expr]` + +### Transition + +Switch to another state immediately: + +``` +transition GameOver +``` + +The current state's `on exit` runs, then the target state's `on enter` runs. + +### Wait Frame + +Yield execution until the next vertical blank (NMI). Synchronizes to the 60 Hz display refresh. + +``` +wait_frame() +``` + +This triggers OAM DMA transfer and PPU updates before yielding. Inside `on frame`, a `wait_frame()` is implicit at the end of each frame. + +### Scroll + +Set the PPU scroll position: + +``` +scroll(scroll_x, scroll_y) +``` + +### Load Background + +Load a background nametable: + +``` +load_background TitleBG +``` + +### Set Palette + +Apply a palette: + +``` +set_palette GamePalette +``` + +### Function Calls as Statements + +``` +reset_score() +update_physics(player_x, player_y) +``` + +--- + +## Assets + +### Sprite Declarations + +``` +sprite Player { + chr: @chr("assets/player.png") +} + +sprite Coin { + chr: @binary("assets/coin.bin") +} +``` + +### Palette Declarations + +Define color palettes using NES PPU color indices (`$00`-`$3F`): + +``` +palette GamePalette { + colors: [0x0F, 0x00, 0x10, 0x30, + 0x0F, 0x07, 0x17, 0x27, + 0x0F, 0x09, 0x19, 0x29, + 0x0F, 0x01, 0x11, 0x21] +} +``` + +Each group of 4 values is a sub-palette. The first color is typically `0x0F` (black) as the shared background color. + +### Background Declarations + +``` +background TitleBG { + chr: @chr("assets/title_screen.png") +} +``` + +### Asset Sources + +Three ways to provide asset data: + +| Source | Description | +|----------------------------|---------------------------------------| +| `@chr("file.png")` | Convert PNG to CHR tile data | +| `@binary("file.bin")` | Include raw binary data verbatim | +| Inline `[0x00, 0x7E, ...]`| Hex byte array directly in source | + +--- + +## Mappers + +The mapper determines cartridge hardware and available ROM size. + +| Mapper | PRG ROM | CHR ROM | Features | +|---------|---------------|----------------|----------------------------------| +| `NROM` | 16 or 32 KB | 8 KB | No banking, simplest | +| `MMC1` | Up to 256 KB | Up to 128 KB | Switchable banks | +| `UxROM` | Up to 256 KB | 8 KB CHR RAM | PRG banking only | +| `MMC3` | Up to 512 KB | Up to 256 KB | Scanline counter, banking | + +### Bank Declarations + +For mappers with bank switching: + +``` +bank MainCode { + // Always-resident code (NMI handler, core engine) +} + +bank Level1 { + state Level1 { ... } + background Level1BG { ... } +} +``` + +Banks can hold `prg` (code/data) or `chr` (graphics) content. Transitions between states in different banks automatically emit bank-switch and trampoline code. + +--- + +## Comments + +``` +// Line comment -- extends to end of line + +/* Block comment + spans multiple lines */ +``` + +--- + +## Includes + +Split your game across multiple files: + +``` +include "physics.ne" +include "enemies.ne" +``` + +Includes are resolved relative to the including file. Circular includes are a compile error. Duplicate includes are skipped automatically. + +--- + +## Debug Mode + +Compile with `--debug` to enable runtime instrumentation. All debug features are stripped completely in release builds (zero bytes, zero cycles). + +### Debug Logging + +``` +debug.log("Player position: ", px, ", ", py) +``` + +### Debug Assertions + +``` +debug.assert(lives > 0, "Lives should never be negative") +``` + +### Runtime Checks (Debug Only) + +In debug mode, the compiler inserts: +- Array bounds checking on indexed access +- Arithmetic overflow warnings +- Stack depth monitoring at function entry +- Frame overrun detection (warns if frame handler exceeds vblank period) + +--- + +## Inline Assembly + +For performance-critical code, drop to 6502 assembly: + +### Bound Assembly + +``` +fun fast_shift(input: u8) -> u8 { + asm { + lda {input} + asl a + asl a + sta {return} + } +} +``` + +`{variable_name}` resolves to the variable's memory address. `{return}` is the return value location. + +### Raw Assembly + +``` +raw asm { + .org $C000 + nop + rti +} +``` + +Raw blocks bypass all compiler management. Use with extreme caution. + +--- + +## Error Codes + +### Lexer Errors (E01xx) + +| Code | Description | +|--------|----------------------------| +| E0101 | Unterminated string literal | +| E0102 | Invalid character | +| E0103 | Number literal overflow | + +### Type Errors (E02xx) + +| Code | Description | +|--------|----------------------------| +| E0201 | Type mismatch | +| E0202 | Invalid cast | +| E0203 | Invalid operation for type | + +### Memory Errors (E03xx) + +| Code | Description | +|--------|----------------------------| +| E0301 | Zero-page overflow | + +### Control Flow Errors (E04xx) + +| Code | Description | +|--------|----------------------------| +| E0401 | Call depth exceeded | +| E0402 | Recursion detected | +| E0403 | Unreachable state | +| E0404 | Transition to undefined state | + +### Declaration Errors (E05xx) + +| Code | Description | +|--------|----------------------------| +| E0501 | Duplicate declaration | +| E0502 | Undefined variable | +| E0503 | Undefined function | +| E0504 | Missing start declaration | +| E0505 | Multiple start declarations| + +### Warnings (W01xx) + +| Code | Description | +|--------|------------------------------------------| +| W0101 | Expensive multiply/divide operation | +| W0102 | Loop without break or wait_frame | +| W0103 | Unused variable | +| W0104 | Unreachable code | + +### Example Error Output + +``` +error[E0201]: type mismatch + --> game.ne:42:15 + | +42 | var x: u8 = -5 + | ^^ expected u8, found negative integer + | + = help: use i8 if you need negative values: var x: i8 = -5 +``` + +``` +error[E0402]: recursion is not allowed + --> game.ne:55:5 + | +55 | flood_fill(x + 1, y) + | ^^^^^^^^^^^^^^^^^^^^ + | + = note: flood_fill calls itself (directly recursive) + = help: the NES has only 256 bytes of stack; use an iterative algorithm instead +``` + +--- + +## Compiler Commands + +### Build + +Compile a `.ne` source file into a `.nes` ROM: + +``` +nescript build game.ne +nescript build game.ne --output my_game.nes +nescript build game.ne --debug +nescript build game.ne --asm-dump +``` + +| Flag | Description | +|---------------|------------------------------------------------| +| `--output` | Set output ROM file path (default: input.nes) | +| `--debug` | Enable debug mode with runtime checks | +| `--asm-dump` | Dump generated 6502 assembly to stdout | + +### Check + +Type-check a source file without producing a ROM: + +``` +nescript check game.ne +``` + +--- + +## Complete Example + +A full game demonstrating states, input, functions, constants, and transitions: + +``` +game "Coin Cavern" { + mapper: NROM +} + +const SPEED: u8 = 2 +const SCREEN_RIGHT: u8 = 240 +const COIN_X: u8 = 180 +const COIN_Y: u8 = 100 + +var player_x: u8 = 40 +var player_y: u8 = 200 +var score: u8 = 0 +var coins_left: u8 = 3 + +fun clamp_x(val: u8) -> u8 { + if val > SCREEN_RIGHT { + return 0 + } + return val +} + +state Title { + on frame { + draw Logo at: (100, 100) + if button.start { + transition Playing + } + } +} + +state Playing { + on enter { + player_x = 40 + player_y = 200 + score = 0 + coins_left = 3 + } + + on frame { + if button.right { + player_x += SPEED + if player_x > SCREEN_RIGHT { + player_x = SCREEN_RIGHT + } + } + if button.left { + if player_x >= SPEED { + player_x -= SPEED + } else { + player_x = 0 + } + } + + if player_x >= COIN_X { + if player_y >= COIN_Y { + score += 1 + coins_left -= 1 + if coins_left == 0 { + transition GameOver + } + } + } + + draw Player at: (player_x, player_y) + draw Coin at: (COIN_X, COIN_Y) + } +} + +state GameOver { + on frame { + draw Trophy at: (120, 100) + if button.start { + transition Title + } + } +} + +start Title +``` + +Build and run: + +``` +nescript build coin_cavern.ne +# produces coin_cavern.nes -- open in any NES emulator +``` diff --git a/docs/nes-reference.md b/docs/nes-reference.md new file mode 100644 index 0000000..55df451 --- /dev/null +++ b/docs/nes-reference.md @@ -0,0 +1,190 @@ +# 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, temp registers, NMI flags) +- `$10`-`$FF`: Available for `fast` variables and compiler-promoted variables +- `$0200`-`$02FF`: OAM shadow buffer (DMA'd to PPU each frame) +- `$0300`-`$07FF`: General variables, state-local storage + +--- + +## 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|