1
0
Fork 0
mirror of https://github.com/imjasonh/nescript synced 2026-07-10 17:52:51 +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

103
src/linker/tests.rs Normal file
View file

@ -0,0 +1,103 @@
use super::*;
use crate::asm::{AddressingMode as AM, Instruction, Opcode::*};
use crate::parser::ast::Mirroring;
use crate::rom;
#[test]
fn link_produces_valid_ines() {
let linker = Linker::new(Mirroring::Horizontal);
let user_code = vec![
Instruction::new(NOP, AM::Label("__main_loop".into())),
Instruction::implied(NOP),
Instruction::new(JMP, AM::Label("__main_loop".into())),
];
let rom_data = linker.link(&user_code);
let info = rom::validate_ines(&rom_data).unwrap();
assert_eq!(info.prg_banks, 1);
assert_eq!(info.chr_banks, 1);
assert_eq!(info.mapper, 0);
}
#[test]
fn link_has_correct_vector_table() {
let linker = Linker::new(Mirroring::Horizontal);
let user_code = vec![Instruction::implied(NOP)];
let rom_data = linker.link(&user_code);
// Vector table is at the last 6 bytes of PRG ROM
// PRG starts at offset 16 in the .nes file
let prg_end = 16 + 16384;
let vector_start = prg_end - 6;
// NMI vector (2 bytes, little-endian)
let nmi = u16::from_le_bytes([rom_data[vector_start], rom_data[vector_start + 1]]);
// RESET vector
let reset = u16::from_le_bytes([rom_data[vector_start + 2], rom_data[vector_start + 3]]);
// IRQ vector
let irq = u16::from_le_bytes([rom_data[vector_start + 4], rom_data[vector_start + 5]]);
// All vectors should be in the $C000-$FFFF range
assert!(nmi >= 0xC000, "NMI vector {nmi:#06X} should be >= $C000");
assert!(
reset >= 0xC000,
"RESET vector {reset:#06X} should be >= $C000"
);
assert!(irq >= 0xC000, "IRQ vector {irq:#06X} should be >= $C000");
// RESET should point to the start of code ($C000)
assert_eq!(reset, 0xC000, "RESET should point to $C000");
}
#[test]
fn link_includes_chr_data() {
let linker = Linker::new(Mirroring::Horizontal);
let user_code = vec![Instruction::implied(NOP)];
let rom_data = linker.link(&user_code);
// CHR starts after PRG
let chr_start = 16 + 16384;
// First 16 bytes should be the smiley sprite
assert_ne!(
&rom_data[chr_start..chr_start + 16],
&[0u8; 16],
"CHR data should contain sprite tile"
);
}
#[test]
fn link_rom_size_correct() {
let linker = Linker::new(Mirroring::Horizontal);
let user_code = vec![Instruction::implied(NOP)];
let rom_data = linker.link(&user_code);
// 16 header + 16384 PRG + 8192 CHR
assert_eq!(rom_data.len(), 16 + 16384 + 8192);
}
#[test]
fn palette_load_writes_to_ppu() {
let linker = Linker::new(Mirroring::Horizontal);
let palette_insts = linker.gen_palette_load();
// Should write to PPU address register ($2006) twice
let ppu_addr_writes: Vec<_> = palette_insts
.iter()
.filter(|i| i.opcode == STA && i.mode == AM::Absolute(0x2006))
.collect();
assert_eq!(
ppu_addr_writes.len(),
2,
"should set PPU address (high and low bytes)"
);
// Should write 32 palette bytes to $2007
let ppu_data_writes: Vec<_> = palette_insts
.iter()
.filter(|i| i.opcode == STA && i.mode == AM::Absolute(0x2007))
.collect();
assert_eq!(
ppu_data_writes.len(),
32,
"should write all 32 palette bytes"
);
}