1
0
Fork 0
mirror of https://github.com/imjasonh/nescript synced 2026-07-10 01:37:45 +00:00

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
This commit is contained in:
Claude 2026-04-11 22:07:56 +00:00
parent 1fca6864ac
commit 39ca246151
No known key found for this signature in database
32 changed files with 6306 additions and 0 deletions

139
src/rom/mod.rs Normal file
View file

@ -0,0 +1,139 @@
#[cfg(test)]
mod tests;
use crate::parser::ast::Mirroring;
/// 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.
pub struct RomBuilder {
prg_data: Vec<u8>,
chr_data: Vec<u8>,
mapper: u8,
mirroring: Mirroring,
}
impl RomBuilder {
pub fn new(mirroring: Mirroring) -> Self {
Self {
prg_data: Vec::new(),
chr_data: Vec::new(),
mapper: 0, // NROM
mirroring,
}
}
#[allow(dead_code)]
pub fn set_mapper(&mut self, mapper: u8) {
self.mapper = mapper;
}
/// Set the PRG ROM data. Will be padded to fill 16 KB or 32 KB.
pub fn set_prg(&mut self, data: Vec<u8>) {
self.prg_data = data;
}
/// 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> {
// Determine PRG size: 1 bank (16 KB) or 2 banks (32 KB)
let prg_banks = if self.prg_data.len() > PRG_BANK_SIZE {
2
} else {
1
};
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]);
// PRG ROM data (padded to fill)
let mut prg = self.prg_data;
prg.resize(prg_size, 0xFF);
rom.extend_from_slice(&prg);
// 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,
}