1
0
Fork 0
mirror of https://github.com/imjasonh/nescript synced 2026-07-13 19:17:54 +00:00
nescript/src/linker/tests.rs

193 lines
6.4 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
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 link_with_sprites_places_chr_data() {
let linker = Linker::new(Mirroring::Horizontal);
let user_code = vec![Instruction::implied(NOP)];
let sprite_bytes: Vec<u8> = (0x20..0x30).collect(); // 16 bytes, one tile
let sprites = vec![SpriteData {
name: "Player".into(),
tile_index: 1,
chr_bytes: sprite_bytes.clone(),
}];
let rom_data = linker.link_with_assets(&user_code, &sprites);
// CHR starts right after the 16-byte iNES header and 16 KB PRG bank.
let chr_start = 16 + 16384;
// Tile 0 should still contain the built-in smiley (first 16 bytes, not
// all zero).
let tile0 = &rom_data[chr_start..chr_start + 16];
assert_ne!(
tile0, &[0u8; 16],
"default smiley should occupy tile index 0",
);
// Tile 1 (CHR offset 16) should contain the sprite's CHR bytes exactly.
let tile1 = &rom_data[chr_start + 16..chr_start + 32];
assert_eq!(tile1, sprite_bytes.as_slice());
// Tile 2 and beyond should be untouched (all zeros).
let tile2 = &rom_data[chr_start + 32..chr_start + 48];
assert_eq!(tile2, &[0u8; 16]);
}
#[test]
fn link_with_sprites_spanning_multiple_tiles() {
let linker = Linker::new(Mirroring::Horizontal);
let user_code = vec![Instruction::implied(NOP)];
// 32 bytes = 2 tiles. The linker should place them consecutively
// starting at the requested tile index.
let sprite_bytes: Vec<u8> = (0..32).collect();
let sprites = vec![SpriteData {
name: "Big".into(),
tile_index: 4,
chr_bytes: sprite_bytes.clone(),
}];
let rom_data = linker.link_with_assets(&user_code, &sprites);
let chr_start = 16 + 16384;
// Tile 4 starts at CHR offset 64.
let placed = &rom_data[chr_start + 64..chr_start + 64 + 32];
assert_eq!(placed, sprite_bytes.as_slice());
}
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
#[test]
fn link_splices_audio_tick_when_user_marker_present() {
// When user code contains the `__audio_used` marker label (the
// IR codegen emits this whenever it sees a `play`/`start_music`/
// `stop_music` op), the linker must splice a `JSR __audio_tick`
// into the NMI handler prologue AND link in the audio driver
// body so the JSR target exists.
let linker = Linker::new(Mirroring::Horizontal);
let user_code = vec![
// Pretend user code with the marker the codegen would emit.
Instruction::new(NOP, AM::Label("__audio_used".into())),
Instruction::implied(NOP),
];
let rom_data = linker.link(&user_code);
// The ROM should be valid even with the splice — the driver
// body has to fit in bank 0 without overflowing.
let info = rom::validate_ines(&rom_data).unwrap();
assert_eq!(info.prg_banks, 1);
}
#[test]
fn link_omits_audio_tick_when_no_marker() {
// User code without the marker should not pay any ROM cost
// for the audio driver. We can't easily inspect bytes, but we
// can at least verify the ROM builds and has a normal shape.
let linker = Linker::new(Mirroring::Horizontal);
let user_code = vec![Instruction::implied(NOP)];
let rom_data = linker.link(&user_code);
let info = rom::validate_ines(&rom_data).unwrap();
assert_eq!(info.prg_banks, 1);
}
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
#[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"
);
}