# NEScript Compiler — Engineering Design & Implementation Plan **Version 0.1 — Draft** --- ## 1. Executive Summary This document describes the architecture, implementation strategy, and milestone plan for the NEScript compiler: a Rust-based toolchain that compiles NEScript source files into iNES-format NES ROMs. The compiler owns the entire pipeline from source text to playable ROM, with no external assembler or linker dependencies. The plan is structured around five milestones, each producing a working compiler that generates a playable ROM demonstrating progressively more language features. The first milestone targets a "sprite on screen, moving with the d-pad" demo in approximately 4–6 weeks of focused development. --- ## 2. Architecture Overview ### 2.1 Pipeline ``` ┌─────────────────────────────────────────┐ │ NEScript Compiler │ │ (nescript) │ │ │ .ne source ────────────►│ Lexer ──► Parser ──► Analyzer ──► │ .png assets │ │ .ftm/.nsf audio │ IR Lowering ──► Optimizer ──► │ │ │ │ Codegen ──► Assembler ──► ROM Builder │ │ │ └──────────┬──────────┬──────────┬────────┘ │ │ │ game.nes game.dbg game.map (ROM) (symbols) (memory map) ``` ### 2.2 Compilation Phases | Phase | Input | Output | Key Responsibility | |-----------------|--------------------|------------------------|----------------------------------------------------------| | **Lexer** | Source text | Token stream | Tokenization, string/number literal parsing | | **Parser** | Token stream | AST | Syntax validation, tree construction | | **Analyzer** | AST | Annotated AST | Type checking, call graph, depth limits, scope resolution| | **IR Lowering** | Annotated AST | NEScript IR | Flatten expressions, expand u16 ops, resolve sugar | | **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 | | **ROM Builder** | Bytes + assets | .nes file | Bank layout, vectors, iNES header | ### 2.3 Design Principles for the Compiler Itself 1. **Single binary, zero dependencies.** The compiler is one Rust binary. No need to install ca65, Python, or Node. Asset conversion (PNG → CHR) is built in. 2. **Fast compilation.** NES games are small. Compilation should be under 1 second for any reasonable project. No incremental compilation needed for v1. 3. **Excellent errors.** Every error message includes the source location, a clear explanation, and a `help:` suggestion where possible. Errors are the primary teaching tool. 4. **Testable at every layer.** Each compiler phase is a pure function (input → output) with no global state, enabling unit testing in isolation. --- ## 3. Rust Project Structure ``` nescript/ ├── Cargo.toml ├── Cargo.lock ├── src/ │ ├── main.rs // CLI entry point (clap-based) │ ├── lib.rs // Library root — exposes all phases │ │ │ ├── lexer/ │ │ ├── mod.rs // Lexer public API │ │ ├── token.rs // Token enum and Span type │ │ └── tests.rs │ │ │ ├── parser/ │ │ ├── mod.rs // Recursive descent parser │ │ ├── ast.rs // AST node definitions │ │ └── tests.rs │ │ │ ├── analyzer/ │ │ ├── mod.rs // Orchestrates analysis passes │ │ ├── types.rs // Type checking │ │ ├── scope.rs // Scope/symbol table management │ │ ├── call_graph.rs // Call graph + depth analysis │ │ └── tests.rs │ │ │ ├── ir/ │ │ ├── mod.rs // IR type definitions │ │ ├── lowering.rs // AST → IR translation │ │ └── tests.rs │ │ │ ├── optimizer/ │ │ ├── mod.rs // Optimization pass runner │ │ ├── const_fold.rs // Constant folding │ │ ├── dead_code.rs // Dead code elimination │ │ ├── zp_promote.rs // Zero-page promotion analysis │ │ ├── inliner.rs // Function inlining │ │ └── tests.rs │ │ │ ├── codegen/ │ │ ├── mod.rs // IR → 6502 instruction selection │ │ ├── regalloc.rs // A/X/Y register allocation │ │ ├── patterns.rs // Instruction patterns (idiom matching) │ │ └── tests.rs │ │ │ ├── asm/ │ │ ├── mod.rs // Assembler public API │ │ ├── opcodes.rs // 6502 opcode table (56 instructions × addressing modes) │ │ ├── encode.rs // Instruction → bytes │ │ ├── addressing.rs // Addressing mode types and resolution │ │ └── tests.rs │ │ │ ├── linker/ │ │ ├── mod.rs // Bank layout and address assignment │ │ ├── banks.rs // Bank allocation logic │ │ ├── fixups.rs // Address fixup/relocation │ │ └── tests.rs │ │ │ ├── rom/ │ │ ├── mod.rs // iNES ROM builder │ │ ├── header.rs // iNES header generation │ │ ├── vectors.rs // NMI/RESET/IRQ vector table │ │ └── tests.rs │ │ │ ├── assets/ │ │ ├── mod.rs // Asset pipeline orchestration │ │ ├── chr.rs // PNG → CHR tile conversion │ │ ├── nametable.rs // PNG → nametable + tile set │ │ ├── palette.rs // Color extraction and NES palette mapping │ │ ├── audio.rs // FamiTracker/NSF import (stub for v1) │ │ └── tests.rs │ │ │ ├── runtime/ │ │ ├── mod.rs // Built-in runtime code │ │ ├── init.rs // NES hardware initialization sequence │ │ ├── nmi.rs // NMI handler generation │ │ ├── input.rs // Controller read routine │ │ ├── oam.rs // OAM DMA setup │ │ ├── ppu.rs // PPU helper routines │ │ └── math.rs // Software multiply/divide routines │ │ │ ├── debug/ │ │ ├── mod.rs // Debug instrumentation │ │ ├── source_map.rs // ROM address → source location mapping │ │ ├── symbols.rs // Symbol table output (Mesen-compatible) │ │ └── checks.rs // Runtime check code generation │ │ │ ├── reports/ │ │ ├── mod.rs // Human-readable reports │ │ ├── memory_map.rs // Memory map report generator │ │ └── call_graph.rs // Call graph report generator │ │ │ └── errors/ │ ├── mod.rs // Error types and formatting │ ├── diagnostic.rs // Diagnostic struct with spans │ └── render.rs // Terminal rendering with color and context │ ├── runtime_asm/ │ ├── init.s // Reference 6502 init sequence │ ├── nmi.s // Reference NMI handler │ └── input.s // Reference controller read │ ├── tests/ │ ├── integration/ │ │ ├── hello_sprite.ne // Minimal test: sprite on screen │ │ ├── input_test.ne // Controller input test │ │ ├── state_machine.ne // State transition test │ │ ├── coin_cavern.ne // Full sample game │ │ └── expected/ // Expected ROM outputs (golden files) │ │ │ ├── error_tests/ │ │ ├── recursion.ne // Should produce E0402 │ │ ├── type_mismatch.ne // Should produce E0201 │ │ ├── depth_exceeded.ne // Should produce E0401 │ │ └── zp_overflow.ne // Should produce E0301 │ │ │ └── asm_tests/ │ ├── opcode_tests.rs // Verify every 6502 opcode encodes correctly │ └── addressing_tests.rs // Verify addressing mode resolution │ ├── examples/ │ ├── hello_world.ne // Minimal "hello" program │ ├── coin_cavern.ne // Full sample game from spec │ └── assets/ // Sample PNGs, audio files │ └── docs/ ├── language_spec.md // NEScript language specification ├── architecture.md // This document └── nes_reference.md // NES hardware quick reference for contributors ``` --- ## 4. Key Data Structures ### 4.1 Tokens ```rust #[derive(Debug, Clone, PartialEq)] pub struct Token { pub kind: TokenKind, pub span: Span, } #[derive(Debug, Clone, Copy, PartialEq)] pub struct Span { pub file_id: u16, // index into file table pub start: u32, // byte offset in source pub end: u32, } #[derive(Debug, Clone, PartialEq)] pub enum TokenKind { // Literals IntLiteral(u16), StringLiteral(String), BoolLiteral(bool), // Identifiers and keywords Ident(String), KwGame, KwState, KwOn, KwFun, KwVar, KwConst, KwIf, KwElse, KwWhile, KwBreak, KwContinue, KwReturn, KwTrue, KwFalse, KwNot, KwAnd, KwOr, KwFast, KwSlow, KwInline, KwInclude, KwStart, KwTransition, KwSprite, KwBackground, KwPalette, KwSfx, KwMusic, KwDraw, KwPlay, KwStopMusic, KwStartMusic, KwLoadBackground, KwSetPalette, KwScroll, KwAsm, KwRaw, KwBank, KwLoop, KwWaitFrame, KwU8, KwI8, KwU16, KwBool, KwDebug, KwAs, // Symbols LParen, RParen, LBrace, RBrace, LBracket, RBracket, Comma, Colon, Semicolon, Arrow, // -> Dot, At, // @ // Operators Plus, Minus, Star, Slash, Percent, Amp, Pipe, Caret, Tilde, ShiftLeft, ShiftRight, Eq, NotEq, Lt, Gt, LtEq, GtEq, Assign, PlusAssign, MinusAssign, AmpAssign, PipeAssign, CaretAssign, ShiftLeftAssign, ShiftRightAssign, // Special Eof, AsmBody(String), // raw asm content between { } } ``` ### 4.2 AST Nodes ```rust pub struct Program { pub game: GameDecl, pub includes: Vec, pub globals: Vec, pub constants: Vec, pub functions: Vec, pub states: Vec, pub sprites: Vec, pub backgrounds: Vec, pub palettes: Vec, pub sound_effects: Vec, pub music_tracks: Vec, pub banks: Vec, pub start_state: String, pub span: Span, } pub struct GameDecl { pub name: String, pub mapper: Mapper, pub mirroring: Mirroring, pub stack_depth: u8, pub span: Span, } pub struct StateDecl { pub name: String, pub locals: Vec, pub on_enter: Option, pub on_exit: Option, pub on_frame: Option, pub on_scanline: Vec<(u8, Block)>, pub span: Span, } pub struct FunDecl { pub name: String, pub params: Vec, pub return_type: Option, pub body: Block, pub is_inline: bool, pub span: Span, } pub struct VarDecl { pub name: String, pub var_type: NesType, pub init: Option, pub placement: Placement, // Fast, Slow, Auto pub span: Span, } #[derive(Debug, Clone, PartialEq)] pub enum NesType { U8, I8, U16, Bool, Array(Box, u16), // element type, fixed size } #[derive(Debug, Clone)] pub enum Expr { IntLiteral(u16, Span), BoolLiteral(bool, Span), Ident(String, Span), ArrayIndex(String, Box, Span), ArrayLiteral(Vec, Span), BinaryOp(Box, BinOp, Box, Span), UnaryOp(UnaryOp, Box, Span), Call(String, Vec, Span), Cast(Box, NesType, Span), ButtonRead(Option, String, Span), // p1/p2, button name } #[derive(Debug, Clone)] pub enum Statement { VarDecl(VarDecl), Assign(LValue, AssignOp, Expr, Span), If(Expr, Block, Vec<(Expr, Block)>, Option, Span), While(Expr, Block, Span), Loop(Block, Span), Break(Span), Continue(Span), Return(Option, Span), Draw(DrawStmt, Span), Play(String, Span), StartMusic(String, Span), StopMusic(Span), Transition(String, Span), LoadBackground(String, Span), SetPalette(String, Span), Scroll(Expr, Expr, Span), WaitFrame(Span), Call(String, Vec, Span), Asm(String, Vec, Span), // body, variable bindings RawAsm(String, Span), DebugLog(Vec, Span), DebugOverlay(Vec, Span), DebugAssert(Expr, Option, Span), } ``` ### 4.3 Intermediate Representation The IR is a flat, register-agnostic representation that maps closely to 6502 operations without committing to specific registers or addresses. ```rust pub struct IrProgram { pub functions: Vec, pub globals: Vec, pub rom_data: Vec, // constant data, asset data } pub struct IrFunction { pub name: String, pub blocks: Vec, pub locals: Vec, pub source_span: Span, } pub struct IrBasicBlock { pub label: String, pub ops: Vec, pub terminator: IrTerminator, } #[derive(Debug, Clone)] pub enum IrOp { // Load/Store LoadImm(IrTemp, u8), // temp = immediate LoadVar(IrTemp, VarId), // temp = variable StoreVar(VarId, IrTemp), // variable = temp // Arithmetic (8-bit) Add(IrTemp, IrTemp, IrTemp), // dest = a + b Sub(IrTemp, IrTemp, IrTemp), And(IrTemp, IrTemp, IrTemp), Or(IrTemp, IrTemp, IrTemp), Xor(IrTemp, IrTemp, IrTemp), ShiftLeft(IrTemp, IrTemp, u8), ShiftRight(IrTemp, IrTemp, u8), Negate(IrTemp, IrTemp), // dest = -src (two's complement) Complement(IrTemp, IrTemp), // dest = ~src // 16-bit operations (expanded from u16 expressions) Add16(IrTemp, IrTemp, IrTemp), // dest_lo/hi = a_lo/hi + b_lo/hi Sub16(IrTemp, IrTemp, IrTemp), LoadImm16(IrTemp, u16), LoadVar16(IrTemp, VarId), StoreVar16(VarId, IrTemp), // Comparison (sets a boolean temp) CmpEq(IrTemp, IrTemp, IrTemp), CmpNe(IrTemp, IrTemp, IrTemp), CmpLt(IrTemp, IrTemp, IrTemp), // signed or unsigned variant CmpGt(IrTemp, IrTemp, IrTemp), CmpLtU(IrTemp, IrTemp, IrTemp), // explicitly unsigned CmpGtU(IrTemp, IrTemp, IrTemp), // Array access ArrayLoad(IrTemp, VarId, IrTemp), // dest = array[index] ArrayStore(VarId, IrTemp, IrTemp), // array[index] = value // Function call Call(Option, String, Vec), // dest = func(args) // Hardware operations DrawSprite(SpriteId, IrTemp, IrTemp, IrTemp, IrTemp, IrTemp), PlaySfx(SfxId), StartMusic(MusicId), StopMusic, ReadInput, WaitFrame, SetScroll(IrTemp, IrTemp), LoadBackground(BackgroundId), SetPalette(PaletteId), // Debug (stripped in release) DebugLog(Vec), DebugAssert(IrTemp, String), // Source mapping SourceLoc(Span), } #[derive(Debug, Clone)] pub enum IrTerminator { Jump(String), // unconditional jump to label Branch(IrTemp, String, String), // if temp then label_t else label_f Return(Option), Transition(String), // state transition Unreachable, } // IrTemp is a virtual register — unlimited supply, resolved during codegen #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub struct IrTemp(pub u32); // VarId uniquely identifies a variable across the program #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub struct VarId(pub u32); ``` ### 4.4 6502 Instruction Representation ```rust #[derive(Debug, Clone)] pub struct Instruction { pub opcode: Opcode, pub mode: AddressingMode, pub source: Option, // link back to NEScript source } #[derive(Debug, Clone, Copy, PartialEq)] pub enum Opcode { LDA, LDX, LDY, STA, STX, STY, ADC, SBC, AND, ORA, EOR, ASL, LSR, ROL, ROR, INC, DEC, INX, INY, DEX, DEY, CMP, CPX, CPY, BIT, JMP, JSR, RTS, RTI, BEQ, BNE, BCC, BCS, BMI, BPL, BVC, BVS, CLC, SEC, CLI, SEI, CLV, CLD, SED, PHA, PLA, PHP, PLP, TAX, TAY, TXA, TYA, TSX, TXS, NOP, BRK, } #[derive(Debug, Clone)] pub enum AddressingMode { Implied, // CLC, RTS, etc. Accumulator, // ASL A Immediate(u8), // LDA #$FF ZeroPage(u8), // LDA $10 ZeroPageX(u8), // LDA $10,X ZeroPageY(u8), // LDX $10,Y Absolute(u16), // LDA $8000 AbsoluteX(u16), // LDA $8000,X AbsoluteY(u16), // LDA $8000,Y Indirect(u16), // JMP ($FFFC) IndirectX(u8), // LDA ($10,X) IndirectY(u8), // LDA ($10),Y Relative(i8), // BEQ +5 (branch offset) // Pre-resolution symbolic forms (resolved by linker) Label(String), // JMP some_label LabelRelative(String), // BEQ some_label SymbolLo(String), // LDA #some_address } ``` --- ## 5. Phase Details ### 5.1 Lexer **Implementation:** Hand-written scanner (no parser generator). NEScript's lexical grammar is simple enough that a hand-written lexer is clearer and produces better error messages than a generated one. **Key behavior:** - Tracks byte offset, line, and column for every token. - `asm { ... }` blocks: when the lexer encounters `asm` followed by `{`, it switches to a raw-capture mode that collects everything until the matching `}`, respecting nested braces. The content is emitted as a single `AsmBody` token. - Numeric literal parsing handles decimal, hex (`0x`), and binary (`0b`) prefixes. - Unterminated strings and unknown characters produce error tokens with descriptive messages rather than panicking. **Estimated size:** ~400 lines of Rust. ### 5.2 Parser **Implementation:** Recursive descent, single-token lookahead. The grammar is LL(1)-friendly by design. No ambiguities requiring backtracking. **Key decisions:** - `draw` statements use a keyword-argument syntax that looks like `draw SpriteName frame: expr at: (expr, expr)`. The parser recognizes `draw` as a statement keyword, then consumes named property pairs until it hits a statement-ending token (newline at same indentation, `}`, or another statement keyword). Properties can appear in any order. - Operator precedence is handled via Pratt parsing (precedence climbing) for expressions. - Error recovery: on a parse error, the parser skips to the next statement boundary (`;`, `}`, or a keyword that starts a new declaration) and continues. This allows reporting multiple errors per compilation. **Estimated size:** ~800 lines of Rust. ### 5.3 Semantic Analyzer This is the most complex phase. It performs multiple passes over the AST: **Pass 1 — Symbol Resolution:** Build a symbol table. Resolve all identifier references to their declarations. Detect undefined variables, duplicate declarations, and scope violations. **Pass 2 — Type Checking:** Verify that all operations have compatible types. Check assignments, function arguments, return types, and array index types. Insert explicit cast nodes where the programmer wrote `as`. Flag implicit coercion attempts as errors. **Pass 3 — Call Graph Analysis:** Build a directed graph of function calls. Detect recursion (cycles in the graph). Compute the maximum call depth for every entry point (each `on frame`, `on enter`, etc.). Compare against `stack_depth` and emit errors if exceeded. **Pass 4 — State Machine Validation:** Verify that all `transition` targets name real states. Verify that `start` names a real state. Warn if any state is unreachable from the start state. **Pass 5 — Memory Planning:** Compute variable lifetimes. Determine which state-local variables can share RAM addresses. Count zero-page demand vs. supply. Assign preliminary memory locations (finalized during codegen/linking). **Estimated size:** ~1,200 lines of Rust. ### 5.4 IR Lowering Translates the annotated AST into the flat IR representation: - Expressions become sequences of IrOps operating on virtual temps. - `u16` operations expand into paired `u8` operations with carry propagation. - `if`/`while`/`loop` become basic blocks with branch terminators. - `on frame` desugars into a loop + wait_frame. - `draw` statements become `DrawSprite` ops with evaluated sub-expressions. - `debug.*` statements are either lowered to debug ops (debug mode) or discarded entirely (release mode). - `button.X_pressed` expressions expand to `(current & mask) AND NOT (previous & mask)`. **Estimated size:** ~600 lines of Rust. ### 5.5 Optimizer Runs a sequence of optimization passes over the IR. Each pass is a function `fn optimize(ir: &mut IrProgram)`. **Constant Folding:** Evaluate operations on known constants at compile time. `3 + 5` becomes `8`. `score > 255` on a `u8` becomes `false`. **Dead Code Elimination:** Remove ops whose results are never used. Remove basic blocks that are never jumped to. **Zero-Page Promotion:** Analyze access frequency across hot paths (frame handlers). Rank variables by access count. Assign the hottest variables to zero-page, respecting the `fast`/`slow` hints as hard constraints. Variables marked `fast` get ZP unconditionally (or error if no room). Variables marked `slow` are excluded. Unmarked variables are auto-promoted by rank. **Function Inlining:** Inline functions marked `inline` or functions under a size threshold (~8 IR ops) that are called from only 1–2 sites. **Strength Reduction:** Replace multiply/divide by powers of 2 with shifts. Replace `x * 3` with `x + x + x` (three adds are cheaper than a software multiply on the 6502). **Estimated size:** ~800 lines of Rust. ### 5.6 Code Generation Translates IR ops into 6502 instructions. This is the heart of the compiler. **Register Allocation Strategy:** The 6502 has three registers: A (accumulator, used for all arithmetic), X and Y (index registers, used for array indexing and loops). There is no general register file. The codegen uses a simple strategy: 1. A is the primary working register. Most IR temps map to A. 2. X is used for array indexing and loop counters. 3. Y is used as a secondary index register. 4. When all three are in use, spill to a set of compiler-reserved zero-page temporaries ($00–$0F). This is not a full graph-coloring allocator — the 6502's constraints make that overkill. Instead, the codegen walks each basic block linearly, tracking what's currently in A/X/Y, and emitting loads/stores as needed. **Pattern Matching:** Common IR sequences map to idiomatic 6502 code: ``` IR: LoadVar(t0, px), LoadImm(t1, 2), Add(t2, t0, t1), StoreVar(px, t2) 6502: LDA px ; load px into A CLC ADC #2 ; add immediate 2 STA px ; store back ``` The codegen has a pattern table for these common cases, falling back to a generic temp-based approach for complex expressions. **Comparison and Branching:** The 6502 has no "compare and branch" — it sets flags with CMP, then branches on flags. The codegen maps: - `==` → CMP + BEQ - `!=` → CMP + BNE - `<` (unsigned) → CMP + BCC - `>=` (unsigned) → CMP + BCS - Signed comparisons require additional flag checks (N xor V). **Estimated size:** ~1,000 lines of Rust. ### 5.7 Assembler Encodes 6502 instructions into bytes. The 6502 is well-documented and regular. **Implementation:** - A lookup table maps (Opcode, AddressingMode) → u8 opcode byte. - Instructions are 1, 2, or 3 bytes depending on addressing mode. - Labels are resolved in two passes: first pass collects label addresses, second pass fills in references. - Branch instructions (BEQ, BNE, etc.) use relative addressing (signed 8-bit offset). If a branch target is out of range (>127 bytes away), the assembler automatically rewrites it as the inverse branch over a JMP (e.g., BEQ +3 / JMP far_target). **Opcode table dimensions:** 56 unique instructions × ~12 addressing modes = ~150 valid combinations. The full table fits in a static array. **Estimated size:** ~500 lines of Rust (including the opcode table). ### 5.8 Linker / ROM Builder Arranges compiled code and data into the final ROM image. **For NROM (no banking):** 1. Lay out PRG ROM starting at $8000 (32 KB) or $C000 (16 KB). 2. Place the runtime init code at the RESET vector entry. 3. Place the NMI handler. 4. Place all compiled functions and state handlers. 5. Place constant data (lookup tables, palette data, nametable data). 6. Write the interrupt vector table at $FFFA: NMI, RESET, IRQ addresses. 7. Place CHR data (tiles) into the CHR ROM section. 8. Prepend the 16-byte iNES header. 9. Write the .nes file. **For banked mappers:** The linker manages multiple PRG banks. The fixed bank contains the runtime, NMI handler, and trampoline stubs. Switchable banks contain state code and associated data. The linker verifies that no single bank exceeds its size limit and that cross-bank references go through trampolines. **Estimated size:** ~400 lines of Rust. ### 5.9 Runtime Library The compiler emits a small runtime library embedded in every ROM. This is not a separate file — the codegen emits these routines directly. **Components:** | Routine | Size (est.) | Purpose | |----------------------|-------------|--------------------------------------------------| | Hardware init | ~80 bytes | Disable IRQ, reset PPU, clear RAM, set stack | | NMI handler | ~60 bytes | OAM DMA, PPU updates, set vblank flag | | Controller read | ~40 bytes | Read joypad register, compute pressed/released | | OAM buffer clear | ~20 bytes | Zero the OAM shadow buffer each frame | | State dispatcher | ~30 bytes | Jump table for active state's frame handler | | Software multiply | ~50 bytes | 8×8→16 multiply (included only if used) | | Software divide | ~60 bytes | 8÷8→8 divide (included only if used) | | PPU write helpers | ~40 bytes | Palette write, nametable write routines | | Debug output (debug) | ~30 bytes | Write to debug port $4800 (stripped in release) | **Total runtime overhead:** ~300–400 bytes in release mode (out of 32,768). This is well within budget. --- ## 6. Asset Pipeline ### 6.1 PNG → CHR Conversion **Dependencies:** The `image` crate (pure Rust, no system dependencies) for PNG decoding. **Process:** 1. Load PNG image. 2. Divide into 8×8 pixel grid. 3. For each tile, extract the 4 most-used colors and map to NES palette indices (2-bit depth). 4. Encode each tile as 16 bytes of CHR data (two bitplanes, 8 bytes each). 5. Optionally deduplicate tiles (for nametable conversion). **Color mapping:** The compiler includes a reference table of all 64 NES colors in RGB. Source image colors are mapped to the nearest NES color by Euclidean distance in RGB space. The compiler warns if a source color is far from any NES color. ### 6.2 PNG → Nametable Conversion 1. Convert the full 256×240 image into 8×8 tiles as above. 2. Deduplicate tiles (NES nametable is 960 tile indices, but CHR ROM holds at most 256 unique tiles per bank). 3. Build the 960-byte nametable (tile indices) and 64-byte attribute table (palette assignments per 16×16 pixel region). 4. Emit tile data to CHR ROM and nametable data to PRG ROM. 5. Error if more than 256 unique tiles are needed (NES hardware limit). ### 6.3 Audio (Deferred) For v1, audio asset handling is stubbed. The `@sfx()` and `@music()` directives will accept pre-formatted binary data via `@binary()` as a workaround. Full FamiTracker import is a v2 feature. --- ## 7. Error System ### 7.1 Error Architecture ```rust pub struct Diagnostic { pub level: Level, // Error, Warning, Info pub code: ErrorCode, // E0201, W0101, etc. pub message: String, // primary message pub span: Span, // primary source location pub labels: Vec