1
0
Fork 0
mirror of https://github.com/imjasonh/nescript synced 2026-07-10 09:42:37 +00:00
nescript/src/linker/mod.rs

230 lines
8.5 KiB
Rust
Raw Normal View History

Implement NEScript compiler Milestone 1 ("Hello Sprite") Complete implementation of the NEScript compiler pipeline for M1: - Lexer: full tokenization with hex/binary/decimal literals, all keywords, operators - Parser: recursive descent with Pratt expression parsing (M1 subset) - Analyzer: symbol resolution, type checking, memory allocation - 6502 Assembler: full opcode encoding table (~150 valid combinations) - Code Generator: AST → 6502 instructions (direct, no IR for M1) - Runtime: NES hardware init, NMI handler, controller read, OAM DMA - Linker: NROM layout, vector table, palette loading, CHR data - ROM Builder: iNES header generation, PRG/CHR padding - CLI: `build` and `check` subcommands via clap 143 tests across all modules: - 22 lexer tests (literals, keywords, operators, error recovery) - 18 parser tests (expressions, statements, game structure, errors) - 7 analyzer tests (symbol resolution, memory allocation, transitions) - 30 assembler tests (every addressing mode, label resolution) - 7 codegen tests (var init, arithmetic, buttons, draw, comparisons) - 11 runtime tests (init sequence, NMI handler, controller read) - 10 ROM builder tests (iNES format, mirroring, banking, validation) - 5 linker tests (vector table, CHR data, palette loading) - 7 integration tests (end-to-end compilation, error detection) CI: GitHub Actions for check, fmt, clippy, test Pre-commit: script for local fmt + clippy + test validation https://claude.ai/code/session_01W6eQFStA66EuMKHUFo2rx3
2026-04-11 22:07:56 +00:00
#[cfg(test)]
mod tests;
use crate::asm;
use crate::asm::{AddressingMode as AM, Instruction, Opcode::*};
use crate::parser::ast::{Mapper, Mirroring};
Implement NEScript compiler Milestone 1 ("Hello Sprite") Complete implementation of the NEScript compiler pipeline for M1: - Lexer: full tokenization with hex/binary/decimal literals, all keywords, operators - Parser: recursive descent with Pratt expression parsing (M1 subset) - Analyzer: symbol resolution, type checking, memory allocation - 6502 Assembler: full opcode encoding table (~150 valid combinations) - Code Generator: AST → 6502 instructions (direct, no IR for M1) - Runtime: NES hardware init, NMI handler, controller read, OAM DMA - Linker: NROM layout, vector table, palette loading, CHR data - ROM Builder: iNES header generation, PRG/CHR padding - CLI: `build` and `check` subcommands via clap 143 tests across all modules: - 22 lexer tests (literals, keywords, operators, error recovery) - 18 parser tests (expressions, statements, game structure, errors) - 7 analyzer tests (symbol resolution, memory allocation, transitions) - 30 assembler tests (every addressing mode, label resolution) - 7 codegen tests (var init, arithmetic, buttons, draw, comparisons) - 11 runtime tests (init sequence, NMI handler, controller read) - 10 ROM builder tests (iNES format, mirroring, banking, validation) - 5 linker tests (vector table, CHR data, palette loading) - 7 integration tests (end-to-end compilation, error detection) CI: GitHub Actions for check, fmt, clippy, test Pre-commit: script for local fmt + clippy + test validation https://claude.ai/code/session_01W6eQFStA66EuMKHUFo2rx3
2026-04-11 22:07:56 +00:00
use crate::rom::RomBuilder;
use crate::runtime;
/// Link compiled code into a complete NES ROM.
pub struct Linker {
mirroring: Mirroring,
mapper: Mapper,
Implement NEScript compiler Milestone 1 ("Hello Sprite") Complete implementation of the NEScript compiler pipeline for M1: - Lexer: full tokenization with hex/binary/decimal literals, all keywords, operators - Parser: recursive descent with Pratt expression parsing (M1 subset) - Analyzer: symbol resolution, type checking, memory allocation - 6502 Assembler: full opcode encoding table (~150 valid combinations) - Code Generator: AST → 6502 instructions (direct, no IR for M1) - Runtime: NES hardware init, NMI handler, controller read, OAM DMA - Linker: NROM layout, vector table, palette loading, CHR data - ROM Builder: iNES header generation, PRG/CHR padding - CLI: `build` and `check` subcommands via clap 143 tests across all modules: - 22 lexer tests (literals, keywords, operators, error recovery) - 18 parser tests (expressions, statements, game structure, errors) - 7 analyzer tests (symbol resolution, memory allocation, transitions) - 30 assembler tests (every addressing mode, label resolution) - 7 codegen tests (var init, arithmetic, buttons, draw, comparisons) - 11 runtime tests (init sequence, NMI handler, controller read) - 10 ROM builder tests (iNES format, mirroring, banking, validation) - 5 linker tests (vector table, CHR data, palette loading) - 7 integration tests (end-to-end compilation, error detection) CI: GitHub Actions for check, fmt, clippy, test Pre-commit: script for local fmt + clippy + test validation https://claude.ai/code/session_01W6eQFStA66EuMKHUFo2rx3
2026-04-11 22:07:56 +00:00
}
/// CHR data for a sprite, placed at a specific tile index in CHR ROM.
#[derive(Debug, Clone)]
pub struct SpriteData {
pub name: String,
pub tile_index: u8,
/// Raw CHR bytes (16 bytes per 8x8 tile).
pub chr_bytes: Vec<u8>,
}
/// True if `instructions` contains a label definition with the given
/// name. Labels are emitted as `NOP` pseudo-instructions whose mode
/// is `AddressingMode::Label(name)`.
fn has_label(instructions: &[Instruction], name: &str) -> bool {
instructions
.iter()
.any(|i| matches!(&i.mode, AM::Label(n) if n == name))
}
Implement NEScript compiler Milestone 1 ("Hello Sprite") Complete implementation of the NEScript compiler pipeline for M1: - Lexer: full tokenization with hex/binary/decimal literals, all keywords, operators - Parser: recursive descent with Pratt expression parsing (M1 subset) - Analyzer: symbol resolution, type checking, memory allocation - 6502 Assembler: full opcode encoding table (~150 valid combinations) - Code Generator: AST → 6502 instructions (direct, no IR for M1) - Runtime: NES hardware init, NMI handler, controller read, OAM DMA - Linker: NROM layout, vector table, palette loading, CHR data - ROM Builder: iNES header generation, PRG/CHR padding - CLI: `build` and `check` subcommands via clap 143 tests across all modules: - 22 lexer tests (literals, keywords, operators, error recovery) - 18 parser tests (expressions, statements, game structure, errors) - 7 analyzer tests (symbol resolution, memory allocation, transitions) - 30 assembler tests (every addressing mode, label resolution) - 7 codegen tests (var init, arithmetic, buttons, draw, comparisons) - 11 runtime tests (init sequence, NMI handler, controller read) - 10 ROM builder tests (iNES format, mirroring, banking, validation) - 5 linker tests (vector table, CHR data, palette loading) - 7 integration tests (end-to-end compilation, error detection) CI: GitHub Actions for check, fmt, clippy, test Pre-commit: script for local fmt + clippy + test validation https://claude.ai/code/session_01W6eQFStA66EuMKHUFo2rx3
2026-04-11 22:07:56 +00:00
/// A smiley face CHR tile for the default sprite (M1).
const DEFAULT_SPRITE_CHR: [u8; 16] = [
// Plane 0 (low bits)
0b0011_1100,
0b0100_0010,
0b1010_0101,
0b1000_0001,
0b1010_0101,
0b1001_1001,
0b0100_0010,
0b0011_1100,
// Plane 1 (high bits) — all zeros means color 1 only
0b0011_1100,
0b0111_1110,
0b1111_1111,
0b1111_1111,
0b1111_1111,
0b1111_1111,
0b0111_1110,
0b0011_1100,
];
/// Default palette data for M1 (writes to PPU $3F00).
const DEFAULT_PALETTE: [u8; 32] = [
// Background palettes
0x0F, 0x00, 0x10, 0x20, // palette 0 (black, dark gray, light gray, white)
0x0F, 0x06, 0x16, 0x26, // palette 1
0x0F, 0x09, 0x19, 0x29, // palette 2
0x0F, 0x01, 0x11, 0x21, // palette 3
// Sprite palettes
0x0F, 0x00, 0x10, 0x20, // sprite palette 0 (same as bg)
0x0F, 0x14, 0x24, 0x34, // sprite palette 1
0x0F, 0x1A, 0x2A, 0x3A, // sprite palette 2
0x0F, 0x12, 0x22, 0x32, // sprite palette 3
];
impl Linker {
pub fn new(mirroring: Mirroring) -> Self {
Self {
mirroring,
mapper: Mapper::NROM,
}
}
pub fn with_mapper(mirroring: Mirroring, mapper: Mapper) -> Self {
Self { mirroring, mapper }
Implement NEScript compiler Milestone 1 ("Hello Sprite") Complete implementation of the NEScript compiler pipeline for M1: - Lexer: full tokenization with hex/binary/decimal literals, all keywords, operators - Parser: recursive descent with Pratt expression parsing (M1 subset) - Analyzer: symbol resolution, type checking, memory allocation - 6502 Assembler: full opcode encoding table (~150 valid combinations) - Code Generator: AST → 6502 instructions (direct, no IR for M1) - Runtime: NES hardware init, NMI handler, controller read, OAM DMA - Linker: NROM layout, vector table, palette loading, CHR data - ROM Builder: iNES header generation, PRG/CHR padding - CLI: `build` and `check` subcommands via clap 143 tests across all modules: - 22 lexer tests (literals, keywords, operators, error recovery) - 18 parser tests (expressions, statements, game structure, errors) - 7 analyzer tests (symbol resolution, memory allocation, transitions) - 30 assembler tests (every addressing mode, label resolution) - 7 codegen tests (var init, arithmetic, buttons, draw, comparisons) - 11 runtime tests (init sequence, NMI handler, controller read) - 10 ROM builder tests (iNES format, mirroring, banking, validation) - 5 linker tests (vector table, CHR data, palette loading) - 7 integration tests (end-to-end compilation, error detection) CI: GitHub Actions for check, fmt, clippy, test Pre-commit: script for local fmt + clippy + test validation https://claude.ai/code/session_01W6eQFStA66EuMKHUFo2rx3
2026-04-11 22:07:56 +00:00
}
/// Link all code sections into a .nes ROM.
///
/// This is a thin wrapper around [`Linker::link_with_assets`] that passes
/// an empty sprite list, so the CHR ROM only contains the default smiley
/// tile at index 0.
Implement NEScript compiler Milestone 1 ("Hello Sprite") Complete implementation of the NEScript compiler pipeline for M1: - Lexer: full tokenization with hex/binary/decimal literals, all keywords, operators - Parser: recursive descent with Pratt expression parsing (M1 subset) - Analyzer: symbol resolution, type checking, memory allocation - 6502 Assembler: full opcode encoding table (~150 valid combinations) - Code Generator: AST → 6502 instructions (direct, no IR for M1) - Runtime: NES hardware init, NMI handler, controller read, OAM DMA - Linker: NROM layout, vector table, palette loading, CHR data - ROM Builder: iNES header generation, PRG/CHR padding - CLI: `build` and `check` subcommands via clap 143 tests across all modules: - 22 lexer tests (literals, keywords, operators, error recovery) - 18 parser tests (expressions, statements, game structure, errors) - 7 analyzer tests (symbol resolution, memory allocation, transitions) - 30 assembler tests (every addressing mode, label resolution) - 7 codegen tests (var init, arithmetic, buttons, draw, comparisons) - 11 runtime tests (init sequence, NMI handler, controller read) - 10 ROM builder tests (iNES format, mirroring, banking, validation) - 5 linker tests (vector table, CHR data, palette loading) - 7 integration tests (end-to-end compilation, error detection) CI: GitHub Actions for check, fmt, clippy, test Pre-commit: script for local fmt + clippy + test validation https://claude.ai/code/session_01W6eQFStA66EuMKHUFo2rx3
2026-04-11 22:07:56 +00:00
pub fn link(&self, user_code: &[Instruction]) -> Vec<u8> {
self.link_with_assets(user_code, &[])
}
/// Link all code sections into a .nes ROM, placing sprite CHR data at
/// specific tile indices.
pub fn link_with_assets(&self, user_code: &[Instruction], sprites: &[SpriteData]) -> Vec<u8> {
Implement NEScript compiler Milestone 1 ("Hello Sprite") Complete implementation of the NEScript compiler pipeline for M1: - Lexer: full tokenization with hex/binary/decimal literals, all keywords, operators - Parser: recursive descent with Pratt expression parsing (M1 subset) - Analyzer: symbol resolution, type checking, memory allocation - 6502 Assembler: full opcode encoding table (~150 valid combinations) - Code Generator: AST → 6502 instructions (direct, no IR for M1) - Runtime: NES hardware init, NMI handler, controller read, OAM DMA - Linker: NROM layout, vector table, palette loading, CHR data - ROM Builder: iNES header generation, PRG/CHR padding - CLI: `build` and `check` subcommands via clap 143 tests across all modules: - 22 lexer tests (literals, keywords, operators, error recovery) - 18 parser tests (expressions, statements, game structure, errors) - 7 analyzer tests (symbol resolution, memory allocation, transitions) - 30 assembler tests (every addressing mode, label resolution) - 7 codegen tests (var init, arithmetic, buttons, draw, comparisons) - 11 runtime tests (init sequence, NMI handler, controller read) - 10 ROM builder tests (iNES format, mirroring, banking, validation) - 5 linker tests (vector table, CHR data, palette loading) - 7 integration tests (end-to-end compilation, error detection) CI: GitHub Actions for check, fmt, clippy, test Pre-commit: script for local fmt + clippy + test validation https://claude.ai/code/session_01W6eQFStA66EuMKHUFo2rx3
2026-04-11 22:07:56 +00:00
// For NROM: everything fits in one 16 KB PRG bank ($C000-$FFFF)
// Layout:
// $C000: RESET handler (init + palette load + user code)
// ... : NMI handler
// ... : IRQ handler
// $FFFA: Vector table (NMI, RESET, IRQ)
let mut all_instructions = Vec::new();
// RESET entry point
all_instructions.push(Instruction::new(NOP, AM::Label("__reset".into())));
// Hardware initialization
all_instructions.extend(runtime::gen_init());
// Load default palette
all_instructions.extend(self.gen_palette_load());
// User code (var init + main loop)
all_instructions.extend(user_code.iter().cloned());
Implement codegen for state dispatch, functions, arrays, math, scroll State machine dispatch: - Assign each state a numeric index, store in ZP $03 - Main loop dispatch table: CMP + BNE + JMP trampoline pattern (avoids branch range limits for large programs) - on_enter/on_exit handlers generated as JSR targets - Transition statement writes state index + JSR enter/exit handlers Function calls: - Function bodies emitted as labeled subroutines with RTS - Statement::Call generates parameter passing via ZP + JSR - Statement::Return generates RTS (with value in A if present) - Parameter slots at ZP $04-$07 Break/continue: - Loop stack tracks continue/break label pairs - Break generates JMP to break_label - Continue generates JMP to continue_label - While and Loop push/pop the stack Array indexing: - LValue::ArrayIndex generates TAX + STA absolute,X - Expr::ArrayIndex generates TAX + LDA absolute,X / ZP,X - Compound array assignments (+=, -=, &=, |=, ^=) load-modify-store Scroll: - scroll(x, y) writes to PPU $2005 twice (X then Y) Math: - Multiply generates JSR __multiply (shift-and-add routine) - Divide generates JSR __divide (restoring division) - Modulo loads remainder from $03 after divide - ShiftLeft generates ASL A, ShiftRight generates LSR A - Math routines wired into linker Error validations: - E0203 for assignment to const variables - Break/continue outside loop detection (in_loop tracking) 233 tests (8 new codegen + 2 analyzer + 2 integration), all passing. https://claude.ai/code/session_01W6eQFStA66EuMKHUFo2rx3
2026-04-12 02:04:49 +00:00
// Math runtime routines (included always for simplicity)
all_instructions.extend(runtime::gen_multiply());
all_instructions.extend(runtime::gen_divide());
compiler: audio driver, u16 arithmetic, multi-scanline, slot recycling Five language features and optimizations from the planned-work backlog: - **Minimal audio driver**: `play`/`start_music`/`stop_music` now generate APU pulse-1/pulse-2 writes from a builtin SFX/music name table, and the NMI handler gains a `JSR __audio_tick` splice (via the linker's `__audio_used` marker lookup) that ages an SFX countdown counter and mutes pulse 1 when the tone expires. Programs that never trigger audio pay zero ROM cost. - **u16 arithmetic and comparisons**: new IR ops `LoadVarHi`, `StoreVarHi`, `Add16`, `Sub16`, and six `Cmp*16` variants. The lowering context tracks variable types via the analyzer's symbol table and routes expressions through the 8-bit or 16-bit path based on operand width. Add16 emits `CLC;ADC;ADC` with carry propagating naturally into the high byte; compares dispatch high-byte-first with a short-circuit low-byte fallback. Fixes a silent miscompile where `big += 1` on a u16 var only incremented the low byte. - **Multi-scanline handlers per state**: `gen_scanline_irq` now dispatches on `(current_state, ZP_SCANLINE_STEP)` and reloads the MMC3 counter with the delta to the next scanline in the same state. `gen_scanline_reload` resets the step counter at the top of each NMI so a state with multiple handlers fires them in ascending line order. Previously only the first handler per state ever fired. - **IR temp slot recycling**: `build_use_counts` pre-scans each function to count per-temp uses; `retire_op_sources` decrements the counts after each op and pushes dead slots back onto `free_slots` for later allocation. `bitwise_ops.ne` used to crash (debug) or miscompile (release) once it hit 128 concurrent temps; with recycling the same function now uses ~4 slots instead of 136. - **INC/DEC peephole fold + improved dead-load elimination**: `fold_inc_dec` collapses `LDA addr; CLC; ADC #1; STA addr` into a single `INC addr` (and the SEC/SBC variant into `DEC addr`), saving 5 bytes and 5 cycles per increment. The fold is suppressed when the next instruction reads carry. `remove_dead_loads` now walks past INC/DEC/STX/STY (which don't touch A) to find the actual next A-use, catching more dead loads. Tests: 331 unit + 39 integration (up from 313 + 37), including new guards for audio, u16, multi-scanline, and slot recycling. https://claude.ai/code/session_01A8qk3gw2jWSzdiXBZPZSFE
2026-04-12 22:21:53 +00:00
// Audio driver body — linked in whenever user code touched
// audio. The driver needs to exist before `__nmi` (the NMI
// JSR target must be defined before the caller), so emit it
// alongside the math routines. No-cost elision when unused:
// the `has_label` check below skips the whole block.
if has_label(user_code, "__audio_used") {
all_instructions.extend(runtime::gen_audio_tick());
}
Implement NEScript compiler Milestone 1 ("Hello Sprite") Complete implementation of the NEScript compiler pipeline for M1: - Lexer: full tokenization with hex/binary/decimal literals, all keywords, operators - Parser: recursive descent with Pratt expression parsing (M1 subset) - Analyzer: symbol resolution, type checking, memory allocation - 6502 Assembler: full opcode encoding table (~150 valid combinations) - Code Generator: AST → 6502 instructions (direct, no IR for M1) - Runtime: NES hardware init, NMI handler, controller read, OAM DMA - Linker: NROM layout, vector table, palette loading, CHR data - ROM Builder: iNES header generation, PRG/CHR padding - CLI: `build` and `check` subcommands via clap 143 tests across all modules: - 22 lexer tests (literals, keywords, operators, error recovery) - 18 parser tests (expressions, statements, game structure, errors) - 7 analyzer tests (symbol resolution, memory allocation, transitions) - 30 assembler tests (every addressing mode, label resolution) - 7 codegen tests (var init, arithmetic, buttons, draw, comparisons) - 11 runtime tests (init sequence, NMI handler, controller read) - 10 ROM builder tests (iNES format, mirroring, banking, validation) - 5 linker tests (vector table, CHR data, palette loading) - 7 integration tests (end-to-end compilation, error detection) CI: GitHub Actions for check, fmt, clippy, test Pre-commit: script for local fmt + clippy + test validation https://claude.ai/code/session_01W6eQFStA66EuMKHUFo2rx3
2026-04-11 22:07:56 +00:00
// NMI handler
all_instructions.push(Instruction::new(NOP, AM::Label("__nmi".into())));
// If user code emits an MMC3 reload hook, splice in a JSR
// before the regular NMI runs. This reloads the scanline IRQ
// counter each frame so the handler fires at the right line.
// The presence of the `__ir_mmc3_reload` label is detected
// during assembly via the labels map; we unconditionally
// emit a conditional JSR whose target is resolved at link
// time. The helper emits an RTS so it's safe to call even
// when there's no work to do.
if has_label(user_code, "__ir_mmc3_reload") {
all_instructions.push(Instruction::new(JSR, AM::Label("__ir_mmc3_reload".into())));
}
compiler: audio driver, u16 arithmetic, multi-scanline, slot recycling Five language features and optimizations from the planned-work backlog: - **Minimal audio driver**: `play`/`start_music`/`stop_music` now generate APU pulse-1/pulse-2 writes from a builtin SFX/music name table, and the NMI handler gains a `JSR __audio_tick` splice (via the linker's `__audio_used` marker lookup) that ages an SFX countdown counter and mutes pulse 1 when the tone expires. Programs that never trigger audio pay zero ROM cost. - **u16 arithmetic and comparisons**: new IR ops `LoadVarHi`, `StoreVarHi`, `Add16`, `Sub16`, and six `Cmp*16` variants. The lowering context tracks variable types via the analyzer's symbol table and routes expressions through the 8-bit or 16-bit path based on operand width. Add16 emits `CLC;ADC;ADC` with carry propagating naturally into the high byte; compares dispatch high-byte-first with a short-circuit low-byte fallback. Fixes a silent miscompile where `big += 1` on a u16 var only incremented the low byte. - **Multi-scanline handlers per state**: `gen_scanline_irq` now dispatches on `(current_state, ZP_SCANLINE_STEP)` and reloads the MMC3 counter with the delta to the next scanline in the same state. `gen_scanline_reload` resets the step counter at the top of each NMI so a state with multiple handlers fires them in ascending line order. Previously only the first handler per state ever fired. - **IR temp slot recycling**: `build_use_counts` pre-scans each function to count per-temp uses; `retire_op_sources` decrements the counts after each op and pushes dead slots back onto `free_slots` for later allocation. `bitwise_ops.ne` used to crash (debug) or miscompile (release) once it hit 128 concurrent temps; with recycling the same function now uses ~4 slots instead of 136. - **INC/DEC peephole fold + improved dead-load elimination**: `fold_inc_dec` collapses `LDA addr; CLC; ADC #1; STA addr` into a single `INC addr` (and the SEC/SBC variant into `DEC addr`), saving 5 bytes and 5 cycles per increment. The fold is suppressed when the next instruction reads carry. `remove_dead_loads` now walks past INC/DEC/STX/STY (which don't touch A) to find the actual next A-use, catching more dead loads. Tests: 331 unit + 39 integration (up from 313 + 37), including new guards for audio, u16, multi-scanline, and slot recycling. https://claude.ai/code/session_01A8qk3gw2jWSzdiXBZPZSFE
2026-04-12 22:21:53 +00:00
// Audio tick: if the user program ever triggered an SFX or
// music (detected by the marker label `__audio_used` emitted
// by `IrCodeGen` when it sees any audio op), JSR into the
// driver's per-frame tick before the normal NMI body. The
// tick decrements the SFX counter and silences pulse 1 when
// it reaches zero. Programs that never play audio skip both
// the splice and the driver body entirely — no ROM cost.
let has_audio = has_label(user_code, "__audio_used");
if has_audio {
all_instructions.push(Instruction::new(JSR, AM::Label("__audio_tick".into())));
}
Implement NEScript compiler Milestone 1 ("Hello Sprite") Complete implementation of the NEScript compiler pipeline for M1: - Lexer: full tokenization with hex/binary/decimal literals, all keywords, operators - Parser: recursive descent with Pratt expression parsing (M1 subset) - Analyzer: symbol resolution, type checking, memory allocation - 6502 Assembler: full opcode encoding table (~150 valid combinations) - Code Generator: AST → 6502 instructions (direct, no IR for M1) - Runtime: NES hardware init, NMI handler, controller read, OAM DMA - Linker: NROM layout, vector table, palette loading, CHR data - ROM Builder: iNES header generation, PRG/CHR padding - CLI: `build` and `check` subcommands via clap 143 tests across all modules: - 22 lexer tests (literals, keywords, operators, error recovery) - 18 parser tests (expressions, statements, game structure, errors) - 7 analyzer tests (symbol resolution, memory allocation, transitions) - 30 assembler tests (every addressing mode, label resolution) - 7 codegen tests (var init, arithmetic, buttons, draw, comparisons) - 11 runtime tests (init sequence, NMI handler, controller read) - 10 ROM builder tests (iNES format, mirroring, banking, validation) - 5 linker tests (vector table, CHR data, palette loading) - 7 integration tests (end-to-end compilation, error detection) CI: GitHub Actions for check, fmt, clippy, test Pre-commit: script for local fmt + clippy + test validation https://claude.ai/code/session_01W6eQFStA66EuMKHUFo2rx3
2026-04-11 22:07:56 +00:00
all_instructions.extend(runtime::gen_nmi());
// IRQ handler
all_instructions.push(Instruction::new(NOP, AM::Label("__irq".into())));
all_instructions.extend(runtime::gen_irq());
// Assemble everything at $C000
let base_addr = 0xC000;
let result = asm::assemble(&all_instructions, base_addr);
// Build PRG ROM with vector table
let mut prg = result.bytes;
// Pad to fill the bank up to vector table location
// Vector table is at $FFFA-$FFFF (relative offset: $3FFA in a 16 KB bank)
let vector_offset = 0x3FFA;
if prg.len() > vector_offset {
panic!("PRG code exceeds 16 KB bank (code is {} bytes)", prg.len());
}
prg.resize(vector_offset, 0xFF);
// Write vector table. IR codegen emits a richer IRQ handler
// under `__irq_user` when the program has scanline handlers;
// prefer that over the generic RTI stub at `__irq`.
Implement NEScript compiler Milestone 1 ("Hello Sprite") Complete implementation of the NEScript compiler pipeline for M1: - Lexer: full tokenization with hex/binary/decimal literals, all keywords, operators - Parser: recursive descent with Pratt expression parsing (M1 subset) - Analyzer: symbol resolution, type checking, memory allocation - 6502 Assembler: full opcode encoding table (~150 valid combinations) - Code Generator: AST → 6502 instructions (direct, no IR for M1) - Runtime: NES hardware init, NMI handler, controller read, OAM DMA - Linker: NROM layout, vector table, palette loading, CHR data - ROM Builder: iNES header generation, PRG/CHR padding - CLI: `build` and `check` subcommands via clap 143 tests across all modules: - 22 lexer tests (literals, keywords, operators, error recovery) - 18 parser tests (expressions, statements, game structure, errors) - 7 analyzer tests (symbol resolution, memory allocation, transitions) - 30 assembler tests (every addressing mode, label resolution) - 7 codegen tests (var init, arithmetic, buttons, draw, comparisons) - 11 runtime tests (init sequence, NMI handler, controller read) - 10 ROM builder tests (iNES format, mirroring, banking, validation) - 5 linker tests (vector table, CHR data, palette loading) - 7 integration tests (end-to-end compilation, error detection) CI: GitHub Actions for check, fmt, clippy, test Pre-commit: script for local fmt + clippy + test validation https://claude.ai/code/session_01W6eQFStA66EuMKHUFo2rx3
2026-04-11 22:07:56 +00:00
let nmi_addr = result.labels.get("__nmi").copied().unwrap_or(0xC000);
let reset_addr = result.labels.get("__reset").copied().unwrap_or(0xC000);
let irq_addr = result
.labels
.get("__irq_user")
.or_else(|| result.labels.get("__irq"))
.copied()
.unwrap_or(0xC000);
Implement NEScript compiler Milestone 1 ("Hello Sprite") Complete implementation of the NEScript compiler pipeline for M1: - Lexer: full tokenization with hex/binary/decimal literals, all keywords, operators - Parser: recursive descent with Pratt expression parsing (M1 subset) - Analyzer: symbol resolution, type checking, memory allocation - 6502 Assembler: full opcode encoding table (~150 valid combinations) - Code Generator: AST → 6502 instructions (direct, no IR for M1) - Runtime: NES hardware init, NMI handler, controller read, OAM DMA - Linker: NROM layout, vector table, palette loading, CHR data - ROM Builder: iNES header generation, PRG/CHR padding - CLI: `build` and `check` subcommands via clap 143 tests across all modules: - 22 lexer tests (literals, keywords, operators, error recovery) - 18 parser tests (expressions, statements, game structure, errors) - 7 analyzer tests (symbol resolution, memory allocation, transitions) - 30 assembler tests (every addressing mode, label resolution) - 7 codegen tests (var init, arithmetic, buttons, draw, comparisons) - 11 runtime tests (init sequence, NMI handler, controller read) - 10 ROM builder tests (iNES format, mirroring, banking, validation) - 5 linker tests (vector table, CHR data, palette loading) - 7 integration tests (end-to-end compilation, error detection) CI: GitHub Actions for check, fmt, clippy, test Pre-commit: script for local fmt + clippy + test validation https://claude.ai/code/session_01W6eQFStA66EuMKHUFo2rx3
2026-04-11 22:07:56 +00:00
prg.extend_from_slice(&nmi_addr.to_le_bytes());
prg.extend_from_slice(&reset_addr.to_le_bytes());
prg.extend_from_slice(&irq_addr.to_le_bytes());
// Build ROM
let mut builder = RomBuilder::new(self.mirroring);
builder.set_mapper(crate::rom::mapper_number(self.mapper));
Implement NEScript compiler Milestone 1 ("Hello Sprite") Complete implementation of the NEScript compiler pipeline for M1: - Lexer: full tokenization with hex/binary/decimal literals, all keywords, operators - Parser: recursive descent with Pratt expression parsing (M1 subset) - Analyzer: symbol resolution, type checking, memory allocation - 6502 Assembler: full opcode encoding table (~150 valid combinations) - Code Generator: AST → 6502 instructions (direct, no IR for M1) - Runtime: NES hardware init, NMI handler, controller read, OAM DMA - Linker: NROM layout, vector table, palette loading, CHR data - ROM Builder: iNES header generation, PRG/CHR padding - CLI: `build` and `check` subcommands via clap 143 tests across all modules: - 22 lexer tests (literals, keywords, operators, error recovery) - 18 parser tests (expressions, statements, game structure, errors) - 7 analyzer tests (symbol resolution, memory allocation, transitions) - 30 assembler tests (every addressing mode, label resolution) - 7 codegen tests (var init, arithmetic, buttons, draw, comparisons) - 11 runtime tests (init sequence, NMI handler, controller read) - 10 ROM builder tests (iNES format, mirroring, banking, validation) - 5 linker tests (vector table, CHR data, palette loading) - 7 integration tests (end-to-end compilation, error detection) CI: GitHub Actions for check, fmt, clippy, test Pre-commit: script for local fmt + clippy + test validation https://claude.ai/code/session_01W6eQFStA66EuMKHUFo2rx3
2026-04-11 22:07:56 +00:00
builder.set_prg(prg);
// CHR ROM: tile 0 is reserved for the default smiley, followed by
// any user-declared sprites placed at their assigned tile indices.
Implement NEScript compiler Milestone 1 ("Hello Sprite") Complete implementation of the NEScript compiler pipeline for M1: - Lexer: full tokenization with hex/binary/decimal literals, all keywords, operators - Parser: recursive descent with Pratt expression parsing (M1 subset) - Analyzer: symbol resolution, type checking, memory allocation - 6502 Assembler: full opcode encoding table (~150 valid combinations) - Code Generator: AST → 6502 instructions (direct, no IR for M1) - Runtime: NES hardware init, NMI handler, controller read, OAM DMA - Linker: NROM layout, vector table, palette loading, CHR data - ROM Builder: iNES header generation, PRG/CHR padding - CLI: `build` and `check` subcommands via clap 143 tests across all modules: - 22 lexer tests (literals, keywords, operators, error recovery) - 18 parser tests (expressions, statements, game structure, errors) - 7 analyzer tests (symbol resolution, memory allocation, transitions) - 30 assembler tests (every addressing mode, label resolution) - 7 codegen tests (var init, arithmetic, buttons, draw, comparisons) - 11 runtime tests (init sequence, NMI handler, controller read) - 10 ROM builder tests (iNES format, mirroring, banking, validation) - 5 linker tests (vector table, CHR data, palette loading) - 7 integration tests (end-to-end compilation, error detection) CI: GitHub Actions for check, fmt, clippy, test Pre-commit: script for local fmt + clippy + test validation https://claude.ai/code/session_01W6eQFStA66EuMKHUFo2rx3
2026-04-11 22:07:56 +00:00
let mut chr = vec![0u8; 8192];
chr[..16].copy_from_slice(&DEFAULT_SPRITE_CHR);
for sprite in sprites {
let offset = sprite.tile_index as usize * 16;
let end = offset + sprite.chr_bytes.len();
if end <= chr.len() {
chr[offset..end].copy_from_slice(&sprite.chr_bytes);
}
}
Implement NEScript compiler Milestone 1 ("Hello Sprite") Complete implementation of the NEScript compiler pipeline for M1: - Lexer: full tokenization with hex/binary/decimal literals, all keywords, operators - Parser: recursive descent with Pratt expression parsing (M1 subset) - Analyzer: symbol resolution, type checking, memory allocation - 6502 Assembler: full opcode encoding table (~150 valid combinations) - Code Generator: AST → 6502 instructions (direct, no IR for M1) - Runtime: NES hardware init, NMI handler, controller read, OAM DMA - Linker: NROM layout, vector table, palette loading, CHR data - ROM Builder: iNES header generation, PRG/CHR padding - CLI: `build` and `check` subcommands via clap 143 tests across all modules: - 22 lexer tests (literals, keywords, operators, error recovery) - 18 parser tests (expressions, statements, game structure, errors) - 7 analyzer tests (symbol resolution, memory allocation, transitions) - 30 assembler tests (every addressing mode, label resolution) - 7 codegen tests (var init, arithmetic, buttons, draw, comparisons) - 11 runtime tests (init sequence, NMI handler, controller read) - 10 ROM builder tests (iNES format, mirroring, banking, validation) - 5 linker tests (vector table, CHR data, palette loading) - 7 integration tests (end-to-end compilation, error detection) CI: GitHub Actions for check, fmt, clippy, test Pre-commit: script for local fmt + clippy + test validation https://claude.ai/code/session_01W6eQFStA66EuMKHUFo2rx3
2026-04-11 22:07:56 +00:00
builder.set_chr(chr);
builder.build()
}
/// Generate instructions to load the default palette into the PPU.
fn gen_palette_load(&self) -> Vec<Instruction> {
let mut out = Vec::new();
// Set PPU address to $3F00 (palette start)
out.push(Instruction::new(LDA, AM::Absolute(0x2002))); // read PPU status to reset latch
out.push(Instruction::new(LDA, AM::Immediate(0x3F)));
out.push(Instruction::new(STA, AM::Absolute(0x2006))); // PPU addr high byte
out.push(Instruction::new(LDA, AM::Immediate(0x00)));
out.push(Instruction::new(STA, AM::Absolute(0x2006))); // PPU addr low byte
// Write all 32 palette bytes
for &color in &DEFAULT_PALETTE {
out.push(Instruction::new(LDA, AM::Immediate(color)));
out.push(Instruction::new(STA, AM::Absolute(0x2007))); // PPU data
}
out
}
}