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:
parent
1fca6864ac
commit
39ca246151
32 changed files with 6306 additions and 0 deletions
103
src/linker/tests.rs
Normal file
103
src/linker/tests.rs
Normal 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"
|
||||
);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue