1
0
Fork 0
mirror of https://github.com/imjasonh/nescript synced 2026-07-14 19:46:46 +00:00
nescript/src/rom/mod.rs

198 lines
6.1 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::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
/// iNES header magic bytes
const INES_MAGIC: [u8; 4] = [0x4E, 0x45, 0x53, 0x1A]; // "NES\x1A"
/// PRG ROM bank size (16 KB)
const PRG_BANK_SIZE: usize = 16384;
/// CHR ROM bank size (8 KB)
const CHR_BANK_SIZE: usize = 8192;
/// Build a complete iNES ROM file.
banks: implement multi-bank PRG layout and bank-switching runtime Prior to this commit the linker always shipped a single 16 KB PRG bank regardless of the declared mapper, so the README's MMC1/UxROM/ MMC3 support was aspirational. This commit gives the three banked mappers a real multi-bank ROM layout: * RomBuilder.set_prg_banks() writes any number of 16 KB banks back-to-back so the iNES header reflects the true PRG size. * Linker.link_banked() places switchable banks first, fixed bank last, so the fixed bank maps to $C000-$FFFF (the address window where vectors and the runtime live). * runtime::gen_mapper_init() emits reset-time mapper config: MMC1 serial-writes a control-register value that pins the last bank at $C000 with the correct mirroring, UxROM relies on the power-on default, MMC3 writes the $8000/$8001/$A000/$E000 registers to get a known PRG and mirroring state. * runtime::gen_bank_select() is a mapper-specific subroutine (callable with the target bank in A) that maps any physical bank to $8000-$BFFF. * runtime::gen_bank_trampoline() generates a cross-bank call stub in the fixed bank that saves the caller's bank, switches, JSRs the target, and restores the fixed bank. * The CLI and integration helper thread declared `bank X: prg` declarations through to the linker so MMC1/UxROM/MMC3 programs actually produce multi-bank ROMs. Coverage: * Runtime unit tests (18 new): mapper init patterns for every supported mapper, bank-select signatures, trampoline dispatch order, UxROM bus-conflict table contents. * RomBuilder tests (6 new): multi-bank layout, padding, byte-level fidelity, per-bank size validation, legacy single-bank fallback. * Linker tests (13 new): multi-bank ROM sizes across MMC1/ UxROM/MMC3, fixed-bank placement, switchable-bank payload fidelity, bank-select subroutine detection, NROM rejection of switchable banks. * Integration e2e tests (16 new): compile real .ne sources through the full pipeline and assert on iNES headers, mapper init signatures in the fixed bank, vector locations, and a regression check against `examples/mmc1_banked.ne`. Total: 474 tests pass under `cargo test` with `RUSTFLAGS="-D warnings"`. https://claude.ai/code/session_01UCressA5e8k1XsuoJYLav2
2026-04-13 01:50:51 +00:00
///
/// Supports both single-bank (NROM) and multi-bank (MMC1, `UxROM`,
/// MMC3) layouts. When you call [`RomBuilder::set_prg`] the builder
/// treats the bytes as a single 16 KB bank and pads out. When you
/// call [`RomBuilder::set_prg_banks`] the builder writes each bank
/// back-to-back in the order you provided. The iNES header's PRG
/// bank count always reflects the actual number of 16 KB slots.
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 struct RomBuilder {
banks: implement multi-bank PRG layout and bank-switching runtime Prior to this commit the linker always shipped a single 16 KB PRG bank regardless of the declared mapper, so the README's MMC1/UxROM/ MMC3 support was aspirational. This commit gives the three banked mappers a real multi-bank ROM layout: * RomBuilder.set_prg_banks() writes any number of 16 KB banks back-to-back so the iNES header reflects the true PRG size. * Linker.link_banked() places switchable banks first, fixed bank last, so the fixed bank maps to $C000-$FFFF (the address window where vectors and the runtime live). * runtime::gen_mapper_init() emits reset-time mapper config: MMC1 serial-writes a control-register value that pins the last bank at $C000 with the correct mirroring, UxROM relies on the power-on default, MMC3 writes the $8000/$8001/$A000/$E000 registers to get a known PRG and mirroring state. * runtime::gen_bank_select() is a mapper-specific subroutine (callable with the target bank in A) that maps any physical bank to $8000-$BFFF. * runtime::gen_bank_trampoline() generates a cross-bank call stub in the fixed bank that saves the caller's bank, switches, JSRs the target, and restores the fixed bank. * The CLI and integration helper thread declared `bank X: prg` declarations through to the linker so MMC1/UxROM/MMC3 programs actually produce multi-bank ROMs. Coverage: * Runtime unit tests (18 new): mapper init patterns for every supported mapper, bank-select signatures, trampoline dispatch order, UxROM bus-conflict table contents. * RomBuilder tests (6 new): multi-bank layout, padding, byte-level fidelity, per-bank size validation, legacy single-bank fallback. * Linker tests (13 new): multi-bank ROM sizes across MMC1/ UxROM/MMC3, fixed-bank placement, switchable-bank payload fidelity, bank-select subroutine detection, NROM rejection of switchable banks. * Integration e2e tests (16 new): compile real .ne sources through the full pipeline and assert on iNES headers, mapper init signatures in the fixed bank, vector locations, and a regression check against `examples/mmc1_banked.ne`. Total: 474 tests pass under `cargo test` with `RUSTFLAGS="-D warnings"`. https://claude.ai/code/session_01UCressA5e8k1XsuoJYLav2
2026-04-13 01:50:51 +00:00
/// One Vec per 16 KB PRG bank, in physical order. An empty
/// outer Vec means no PRG has been set yet; a single inner Vec
/// means a classic NROM layout.
prg_banks: Vec<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
chr_data: Vec<u8>,
mapper: u8,
mirroring: Mirroring,
}
impl RomBuilder {
pub fn new(mirroring: Mirroring) -> Self {
Self {
banks: implement multi-bank PRG layout and bank-switching runtime Prior to this commit the linker always shipped a single 16 KB PRG bank regardless of the declared mapper, so the README's MMC1/UxROM/ MMC3 support was aspirational. This commit gives the three banked mappers a real multi-bank ROM layout: * RomBuilder.set_prg_banks() writes any number of 16 KB banks back-to-back so the iNES header reflects the true PRG size. * Linker.link_banked() places switchable banks first, fixed bank last, so the fixed bank maps to $C000-$FFFF (the address window where vectors and the runtime live). * runtime::gen_mapper_init() emits reset-time mapper config: MMC1 serial-writes a control-register value that pins the last bank at $C000 with the correct mirroring, UxROM relies on the power-on default, MMC3 writes the $8000/$8001/$A000/$E000 registers to get a known PRG and mirroring state. * runtime::gen_bank_select() is a mapper-specific subroutine (callable with the target bank in A) that maps any physical bank to $8000-$BFFF. * runtime::gen_bank_trampoline() generates a cross-bank call stub in the fixed bank that saves the caller's bank, switches, JSRs the target, and restores the fixed bank. * The CLI and integration helper thread declared `bank X: prg` declarations through to the linker so MMC1/UxROM/MMC3 programs actually produce multi-bank ROMs. Coverage: * Runtime unit tests (18 new): mapper init patterns for every supported mapper, bank-select signatures, trampoline dispatch order, UxROM bus-conflict table contents. * RomBuilder tests (6 new): multi-bank layout, padding, byte-level fidelity, per-bank size validation, legacy single-bank fallback. * Linker tests (13 new): multi-bank ROM sizes across MMC1/ UxROM/MMC3, fixed-bank placement, switchable-bank payload fidelity, bank-select subroutine detection, NROM rejection of switchable banks. * Integration e2e tests (16 new): compile real .ne sources through the full pipeline and assert on iNES headers, mapper init signatures in the fixed bank, vector locations, and a regression check against `examples/mmc1_banked.ne`. Total: 474 tests pass under `cargo test` with `RUSTFLAGS="-D warnings"`. https://claude.ai/code/session_01UCressA5e8k1XsuoJYLav2
2026-04-13 01:50:51 +00:00
prg_banks: Vec::new(),
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: Vec::new(),
mapper: 0, // NROM
mirroring,
}
}
#[allow(dead_code)]
pub fn set_mapper(&mut self, mapper: u8) {
self.mapper = mapper;
}
banks: implement multi-bank PRG layout and bank-switching runtime Prior to this commit the linker always shipped a single 16 KB PRG bank regardless of the declared mapper, so the README's MMC1/UxROM/ MMC3 support was aspirational. This commit gives the three banked mappers a real multi-bank ROM layout: * RomBuilder.set_prg_banks() writes any number of 16 KB banks back-to-back so the iNES header reflects the true PRG size. * Linker.link_banked() places switchable banks first, fixed bank last, so the fixed bank maps to $C000-$FFFF (the address window where vectors and the runtime live). * runtime::gen_mapper_init() emits reset-time mapper config: MMC1 serial-writes a control-register value that pins the last bank at $C000 with the correct mirroring, UxROM relies on the power-on default, MMC3 writes the $8000/$8001/$A000/$E000 registers to get a known PRG and mirroring state. * runtime::gen_bank_select() is a mapper-specific subroutine (callable with the target bank in A) that maps any physical bank to $8000-$BFFF. * runtime::gen_bank_trampoline() generates a cross-bank call stub in the fixed bank that saves the caller's bank, switches, JSRs the target, and restores the fixed bank. * The CLI and integration helper thread declared `bank X: prg` declarations through to the linker so MMC1/UxROM/MMC3 programs actually produce multi-bank ROMs. Coverage: * Runtime unit tests (18 new): mapper init patterns for every supported mapper, bank-select signatures, trampoline dispatch order, UxROM bus-conflict table contents. * RomBuilder tests (6 new): multi-bank layout, padding, byte-level fidelity, per-bank size validation, legacy single-bank fallback. * Linker tests (13 new): multi-bank ROM sizes across MMC1/ UxROM/MMC3, fixed-bank placement, switchable-bank payload fidelity, bank-select subroutine detection, NROM rejection of switchable banks. * Integration e2e tests (16 new): compile real .ne sources through the full pipeline and assert on iNES headers, mapper init signatures in the fixed bank, vector locations, and a regression check against `examples/mmc1_banked.ne`. Total: 474 tests pass under `cargo test` with `RUSTFLAGS="-D warnings"`. https://claude.ai/code/session_01UCressA5e8k1XsuoJYLav2
2026-04-13 01:50:51 +00:00
/// Set the PRG ROM data as a single bank. Will be padded to fill
/// 16 KB or 32 KB. Equivalent to calling `set_prg_banks` with a
/// one- or two-element Vec depending on whether the data crosses
/// the 16 KB boundary.
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 set_prg(&mut self, data: Vec<u8>) {
banks: implement multi-bank PRG layout and bank-switching runtime Prior to this commit the linker always shipped a single 16 KB PRG bank regardless of the declared mapper, so the README's MMC1/UxROM/ MMC3 support was aspirational. This commit gives the three banked mappers a real multi-bank ROM layout: * RomBuilder.set_prg_banks() writes any number of 16 KB banks back-to-back so the iNES header reflects the true PRG size. * Linker.link_banked() places switchable banks first, fixed bank last, so the fixed bank maps to $C000-$FFFF (the address window where vectors and the runtime live). * runtime::gen_mapper_init() emits reset-time mapper config: MMC1 serial-writes a control-register value that pins the last bank at $C000 with the correct mirroring, UxROM relies on the power-on default, MMC3 writes the $8000/$8001/$A000/$E000 registers to get a known PRG and mirroring state. * runtime::gen_bank_select() is a mapper-specific subroutine (callable with the target bank in A) that maps any physical bank to $8000-$BFFF. * runtime::gen_bank_trampoline() generates a cross-bank call stub in the fixed bank that saves the caller's bank, switches, JSRs the target, and restores the fixed bank. * The CLI and integration helper thread declared `bank X: prg` declarations through to the linker so MMC1/UxROM/MMC3 programs actually produce multi-bank ROMs. Coverage: * Runtime unit tests (18 new): mapper init patterns for every supported mapper, bank-select signatures, trampoline dispatch order, UxROM bus-conflict table contents. * RomBuilder tests (6 new): multi-bank layout, padding, byte-level fidelity, per-bank size validation, legacy single-bank fallback. * Linker tests (13 new): multi-bank ROM sizes across MMC1/ UxROM/MMC3, fixed-bank placement, switchable-bank payload fidelity, bank-select subroutine detection, NROM rejection of switchable banks. * Integration e2e tests (16 new): compile real .ne sources through the full pipeline and assert on iNES headers, mapper init signatures in the fixed bank, vector locations, and a regression check against `examples/mmc1_banked.ne`. Total: 474 tests pass under `cargo test` with `RUSTFLAGS="-D warnings"`. https://claude.ai/code/session_01UCressA5e8k1XsuoJYLav2
2026-04-13 01:50:51 +00:00
// Preserve the legacy single-bank behaviour: if the data is
// <= 16 KB we emit exactly one 16 KB bank; if it's larger
// (but still <= 32 KB) we split into two consecutive banks
// so the iNES header byte 4 reflects 2, matching the old
// `set_prg` contract used by all current NROM tests.
if data.len() <= PRG_BANK_SIZE {
self.prg_banks = vec![data];
} else {
let mut first = data;
let second = first.split_off(PRG_BANK_SIZE);
self.prg_banks = vec![first, second];
}
}
/// Set the PRG ROM data as a list of 16 KB banks. Each bank will
/// be padded with $FF to fill its 16 KB slot. The banks are
/// written in the order provided — for mapper-specific layouts
/// the caller (usually the Linker) is responsible for placing
/// the fixed bank last.
///
/// # Panics
/// Panics if any single bank exceeds 16 KB, which would indicate
/// a compiler bug (the allocator is expected to overflow-check
/// before calling the rom builder).
pub fn set_prg_banks(&mut self, banks: Vec<Vec<u8>>) {
for (i, bank) in banks.iter().enumerate() {
assert!(
bank.len() <= PRG_BANK_SIZE,
"PRG bank {i} exceeds 16 KB ({} bytes)",
bank.len()
);
}
self.prg_banks = banks;
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
}
/// Set the CHR ROM data. Will be padded to fill 8 KB.
pub fn set_chr(&mut self, data: Vec<u8>) {
self.chr_data = data;
}
/// Build the complete .nes file.
pub fn build(self) -> Vec<u8> {
banks: implement multi-bank PRG layout and bank-switching runtime Prior to this commit the linker always shipped a single 16 KB PRG bank regardless of the declared mapper, so the README's MMC1/UxROM/ MMC3 support was aspirational. This commit gives the three banked mappers a real multi-bank ROM layout: * RomBuilder.set_prg_banks() writes any number of 16 KB banks back-to-back so the iNES header reflects the true PRG size. * Linker.link_banked() places switchable banks first, fixed bank last, so the fixed bank maps to $C000-$FFFF (the address window where vectors and the runtime live). * runtime::gen_mapper_init() emits reset-time mapper config: MMC1 serial-writes a control-register value that pins the last bank at $C000 with the correct mirroring, UxROM relies on the power-on default, MMC3 writes the $8000/$8001/$A000/$E000 registers to get a known PRG and mirroring state. * runtime::gen_bank_select() is a mapper-specific subroutine (callable with the target bank in A) that maps any physical bank to $8000-$BFFF. * runtime::gen_bank_trampoline() generates a cross-bank call stub in the fixed bank that saves the caller's bank, switches, JSRs the target, and restores the fixed bank. * The CLI and integration helper thread declared `bank X: prg` declarations through to the linker so MMC1/UxROM/MMC3 programs actually produce multi-bank ROMs. Coverage: * Runtime unit tests (18 new): mapper init patterns for every supported mapper, bank-select signatures, trampoline dispatch order, UxROM bus-conflict table contents. * RomBuilder tests (6 new): multi-bank layout, padding, byte-level fidelity, per-bank size validation, legacy single-bank fallback. * Linker tests (13 new): multi-bank ROM sizes across MMC1/ UxROM/MMC3, fixed-bank placement, switchable-bank payload fidelity, bank-select subroutine detection, NROM rejection of switchable banks. * Integration e2e tests (16 new): compile real .ne sources through the full pipeline and assert on iNES headers, mapper init signatures in the fixed bank, vector locations, and a regression check against `examples/mmc1_banked.ne`. Total: 474 tests pass under `cargo test` with `RUSTFLAGS="-D warnings"`. https://claude.ai/code/session_01UCressA5e8k1XsuoJYLav2
2026-04-13 01:50:51 +00:00
// Determine PRG size. If no banks were set, fall back to a
// single empty bank so the ROM has a valid 16 KB PRG slot.
let prg_banks_vec: Vec<Vec<u8>> = if self.prg_banks.is_empty() {
vec![Vec::new()]
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
} else {
banks: implement multi-bank PRG layout and bank-switching runtime Prior to this commit the linker always shipped a single 16 KB PRG bank regardless of the declared mapper, so the README's MMC1/UxROM/ MMC3 support was aspirational. This commit gives the three banked mappers a real multi-bank ROM layout: * RomBuilder.set_prg_banks() writes any number of 16 KB banks back-to-back so the iNES header reflects the true PRG size. * Linker.link_banked() places switchable banks first, fixed bank last, so the fixed bank maps to $C000-$FFFF (the address window where vectors and the runtime live). * runtime::gen_mapper_init() emits reset-time mapper config: MMC1 serial-writes a control-register value that pins the last bank at $C000 with the correct mirroring, UxROM relies on the power-on default, MMC3 writes the $8000/$8001/$A000/$E000 registers to get a known PRG and mirroring state. * runtime::gen_bank_select() is a mapper-specific subroutine (callable with the target bank in A) that maps any physical bank to $8000-$BFFF. * runtime::gen_bank_trampoline() generates a cross-bank call stub in the fixed bank that saves the caller's bank, switches, JSRs the target, and restores the fixed bank. * The CLI and integration helper thread declared `bank X: prg` declarations through to the linker so MMC1/UxROM/MMC3 programs actually produce multi-bank ROMs. Coverage: * Runtime unit tests (18 new): mapper init patterns for every supported mapper, bank-select signatures, trampoline dispatch order, UxROM bus-conflict table contents. * RomBuilder tests (6 new): multi-bank layout, padding, byte-level fidelity, per-bank size validation, legacy single-bank fallback. * Linker tests (13 new): multi-bank ROM sizes across MMC1/ UxROM/MMC3, fixed-bank placement, switchable-bank payload fidelity, bank-select subroutine detection, NROM rejection of switchable banks. * Integration e2e tests (16 new): compile real .ne sources through the full pipeline and assert on iNES headers, mapper init signatures in the fixed bank, vector locations, and a regression check against `examples/mmc1_banked.ne`. Total: 474 tests pass under `cargo test` with `RUSTFLAGS="-D warnings"`. https://claude.ai/code/session_01UCressA5e8k1XsuoJYLav2
2026-04-13 01:50:51 +00:00
self.prg_banks
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
};
banks: implement multi-bank PRG layout and bank-switching runtime Prior to this commit the linker always shipped a single 16 KB PRG bank regardless of the declared mapper, so the README's MMC1/UxROM/ MMC3 support was aspirational. This commit gives the three banked mappers a real multi-bank ROM layout: * RomBuilder.set_prg_banks() writes any number of 16 KB banks back-to-back so the iNES header reflects the true PRG size. * Linker.link_banked() places switchable banks first, fixed bank last, so the fixed bank maps to $C000-$FFFF (the address window where vectors and the runtime live). * runtime::gen_mapper_init() emits reset-time mapper config: MMC1 serial-writes a control-register value that pins the last bank at $C000 with the correct mirroring, UxROM relies on the power-on default, MMC3 writes the $8000/$8001/$A000/$E000 registers to get a known PRG and mirroring state. * runtime::gen_bank_select() is a mapper-specific subroutine (callable with the target bank in A) that maps any physical bank to $8000-$BFFF. * runtime::gen_bank_trampoline() generates a cross-bank call stub in the fixed bank that saves the caller's bank, switches, JSRs the target, and restores the fixed bank. * The CLI and integration helper thread declared `bank X: prg` declarations through to the linker so MMC1/UxROM/MMC3 programs actually produce multi-bank ROMs. Coverage: * Runtime unit tests (18 new): mapper init patterns for every supported mapper, bank-select signatures, trampoline dispatch order, UxROM bus-conflict table contents. * RomBuilder tests (6 new): multi-bank layout, padding, byte-level fidelity, per-bank size validation, legacy single-bank fallback. * Linker tests (13 new): multi-bank ROM sizes across MMC1/ UxROM/MMC3, fixed-bank placement, switchable-bank payload fidelity, bank-select subroutine detection, NROM rejection of switchable banks. * Integration e2e tests (16 new): compile real .ne sources through the full pipeline and assert on iNES headers, mapper init signatures in the fixed bank, vector locations, and a regression check against `examples/mmc1_banked.ne`. Total: 474 tests pass under `cargo test` with `RUSTFLAGS="-D warnings"`. https://claude.ai/code/session_01UCressA5e8k1XsuoJYLav2
2026-04-13 01:50:51 +00:00
let prg_banks = prg_banks_vec.len();
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 prg_size = prg_banks * PRG_BANK_SIZE;
// CHR: 1 bank (8 KB), or 0 if using CHR RAM
let chr_banks = usize::from(!self.chr_data.is_empty());
let chr_size = chr_banks * CHR_BANK_SIZE;
let mut rom = Vec::with_capacity(16 + prg_size + chr_size);
// iNES header (16 bytes)
rom.extend_from_slice(&INES_MAGIC);
rom.push(prg_banks as u8); // PRG ROM banks (16 KB units)
rom.push(chr_banks as u8); // CHR ROM banks (8 KB units)
// Flags 6: mirroring, mapper low nibble
let mut flags6 = match self.mirroring {
Mirroring::Horizontal => 0,
Mirroring::Vertical => 1,
};
flags6 |= (self.mapper & 0x0F) << 4;
rom.push(flags6);
// Flags 7: mapper high nibble
let flags7 = self.mapper & 0xF0;
rom.push(flags7);
// Bytes 8-15: padding zeros
rom.extend_from_slice(&[0u8; 8]);
banks: implement multi-bank PRG layout and bank-switching runtime Prior to this commit the linker always shipped a single 16 KB PRG bank regardless of the declared mapper, so the README's MMC1/UxROM/ MMC3 support was aspirational. This commit gives the three banked mappers a real multi-bank ROM layout: * RomBuilder.set_prg_banks() writes any number of 16 KB banks back-to-back so the iNES header reflects the true PRG size. * Linker.link_banked() places switchable banks first, fixed bank last, so the fixed bank maps to $C000-$FFFF (the address window where vectors and the runtime live). * runtime::gen_mapper_init() emits reset-time mapper config: MMC1 serial-writes a control-register value that pins the last bank at $C000 with the correct mirroring, UxROM relies on the power-on default, MMC3 writes the $8000/$8001/$A000/$E000 registers to get a known PRG and mirroring state. * runtime::gen_bank_select() is a mapper-specific subroutine (callable with the target bank in A) that maps any physical bank to $8000-$BFFF. * runtime::gen_bank_trampoline() generates a cross-bank call stub in the fixed bank that saves the caller's bank, switches, JSRs the target, and restores the fixed bank. * The CLI and integration helper thread declared `bank X: prg` declarations through to the linker so MMC1/UxROM/MMC3 programs actually produce multi-bank ROMs. Coverage: * Runtime unit tests (18 new): mapper init patterns for every supported mapper, bank-select signatures, trampoline dispatch order, UxROM bus-conflict table contents. * RomBuilder tests (6 new): multi-bank layout, padding, byte-level fidelity, per-bank size validation, legacy single-bank fallback. * Linker tests (13 new): multi-bank ROM sizes across MMC1/ UxROM/MMC3, fixed-bank placement, switchable-bank payload fidelity, bank-select subroutine detection, NROM rejection of switchable banks. * Integration e2e tests (16 new): compile real .ne sources through the full pipeline and assert on iNES headers, mapper init signatures in the fixed bank, vector locations, and a regression check against `examples/mmc1_banked.ne`. Total: 474 tests pass under `cargo test` with `RUSTFLAGS="-D warnings"`. https://claude.ai/code/session_01UCressA5e8k1XsuoJYLav2
2026-04-13 01:50:51 +00:00
// PRG ROM data: each bank padded to 16 KB, concatenated.
for mut bank in prg_banks_vec {
bank.resize(PRG_BANK_SIZE, 0xFF);
rom.extend_from_slice(&bank);
}
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 ROM data (padded to fill)
if chr_banks > 0 {
let mut chr = self.chr_data;
chr.resize(chr_size, 0x00);
rom.extend_from_slice(&chr);
}
rom
}
}
/// Validate that a byte slice looks like a valid iNES ROM.
pub fn validate_ines(data: &[u8]) -> Result<RomInfo, &'static str> {
if data.len() < 16 {
return Err("file too small for iNES header");
}
if data[0..4] != INES_MAGIC {
return Err("invalid iNES magic bytes");
}
let prg_banks = data[4] as usize;
let chr_banks = data[5] as usize;
let expected_size = 16 + prg_banks * PRG_BANK_SIZE + chr_banks * CHR_BANK_SIZE;
if data.len() < expected_size {
return Err("file too small for declared ROM banks");
}
let mirroring = if data[6] & 1 == 0 {
Mirroring::Horizontal
} else {
Mirroring::Vertical
};
let mapper = (data[6] >> 4) | (data[7] & 0xF0);
Ok(RomInfo {
prg_banks,
chr_banks,
mapper,
mirroring,
})
}
#[derive(Debug)]
pub struct RomInfo {
pub prg_banks: usize,
pub chr_banks: usize,
pub mapper: u8,
pub mirroring: Mirroring,
}
/// Map a `Mapper` enum variant to its iNES mapper number.
pub fn mapper_number(mapper: Mapper) -> u8 {
match mapper {
Mapper::NROM => 0,
Mapper::MMC1 => 1,
Mapper::UxROM => 2,
Mapper::MMC3 => 4,
}
}