1
0
Fork 0
mirror of https://github.com/imjasonh/nescript synced 2026-07-09 17:28:00 +00:00
nescript/src/rom/tests.rs
Claude f4968256f4
review: tighten PRNG / void-intrinsic / FCEUX path handling
Follow-up cleanup on the cc65 parity batch. Addresses issues found
during a post-commit code review.

**Correctness fixes:**

- `rand8()` / `rand16()` at statement position (result discarded)
  were being eliminated by DCE because `op_dest` returned
  `Some(dest)` for Rand8/Rand16 even though the ops have a visible
  side effect — advancing the PRNG state. Now `op_dest` returns
  `None` for both, keeping the JSR regardless of liveness. New
  regression test `rand8_statement_survives_dce`.
- Void-only intrinsics (`poke`, `seed_rand`, `set_palette_brightness`)
  used in expression position (e.g. `var x = seed_rand(42)`) were
  panicking the linker with an unresolved `__ir_fn_X` label. The
  analyzer now emits E0203 with a clear message; new
  `void_intrinsic_in_expression_position_errors` test covers all
  three names.
- Statement-position `rand8()` / `rand16()` weren't lowered at all
  (they fell through to the default Call path). Now both lower to
  their IR op with a fresh temp that nothing reads; the JSR still
  runs so the PRNG state advances.
- `--fceux-labels foo.nes` was producing `foo.0.nl` because
  `PathBuf::with_extension` replaces instead of appends. Rewritten
  to literally append `.<bank>.nl` / `.ram.nl` to the OsString, so
  users get the FCEUX-expected `foo.nes.<bank>.nl` naming.
- Linker now asserts CNROM / AxROM don't accept user-declared
  switchable PRG banks — their page sizes don't fit the 16 KB per
  bank model, and silently producing a mis-sized ROM is worse than
  a loud panic.

**PRNG cleanup:**

- Removed the stream-of-consciousness comment block in `gen_prng`
  that described three abandoned algorithms before landing on the
  actual Galois LFSR.
- Simplified `__rand16` to a single JSR + LDX instead of two
  JSRs + TAY/TYA round-trip — a single shift already produces 16
  fresh bits, the doubled call just burned ~40 cycles. The golden
  PNG for `prng_demo` was regenerated to reflect the new sequence.
- Rewrote the `gen_prng` doc comment to accurately describe the
  algorithm as a Galois LFSR (it was mislabelled as xorshift).
- Rewrote the `gen_palette_brightness` doc comment with a proper
  table of level→mask mappings — the prior prose description
  didn't match the actual table values.

**Tests:**

- Three new unit tests in `linker::debug_symbols` covering the
  FCEUX `.nl` renderer: user-facing labels only, empty output when
  no user labels exist, and deterministic sorting in `.ram.nl`.
- Extended `nes2_mapper_high_nibble_in_byte_8_is_zero_for_small_mappers`
  to cover AxROM + CNROM.
- Renumbered priority list in future-work.md after removing the
  shipped sections (J, K, N, parts of V and Y).

All 737 tests + 40/40 emulator goldens still green.
2026-04-18 18:48:55 +00:00

319 lines
10 KiB
Rust

use super::*;
use crate::parser::ast::{HeaderFormat, Mirroring};
#[test]
fn build_minimal_rom() {
let mut builder = RomBuilder::new(Mirroring::Horizontal);
builder.set_prg(vec![0xEA; 100]); // 100 bytes of NOP
let rom = builder.build();
// Should have 16-byte header + 16 KB PRG
assert_eq!(rom.len(), 16 + 16384);
// Magic bytes
assert_eq!(&rom[0..4], b"NES\x1a");
// 1 PRG bank
assert_eq!(rom[4], 1);
// 0 CHR banks (CHR RAM)
assert_eq!(rom[5], 0);
}
#[test]
fn build_rom_with_chr() {
let mut builder = RomBuilder::new(Mirroring::Vertical);
builder.set_prg(vec![0xEA; 100]);
builder.set_chr(vec![0x00; 16]); // 1 tile
let rom = builder.build();
// 16-byte header + 16 KB PRG + 8 KB CHR
assert_eq!(rom.len(), 16 + 16384 + 8192);
assert_eq!(rom[5], 1); // 1 CHR bank
}
#[test]
fn horizontal_mirroring() {
let builder = RomBuilder::new(Mirroring::Horizontal);
let rom = builder.build();
assert_eq!(rom[6] & 1, 0);
}
#[test]
fn vertical_mirroring() {
let builder = RomBuilder::new(Mirroring::Vertical);
let rom = builder.build();
assert_eq!(rom[6] & 1, 1);
}
#[test]
fn prg_data_is_padded() {
let mut builder = RomBuilder::new(Mirroring::Horizontal);
builder.set_prg(vec![0xAA; 10]);
let rom = builder.build();
// First 10 bytes of PRG should be $AA
assert_eq!(&rom[16..26], &[0xAA; 10]);
// Rest should be $FF (fill byte)
assert_eq!(rom[26], 0xFF);
}
#[test]
fn validate_valid_rom() {
let builder = RomBuilder::new(Mirroring::Horizontal);
let rom = builder.build();
let info = validate_ines(&rom).unwrap();
assert_eq!(info.prg_banks, 1);
assert_eq!(info.chr_banks, 0);
assert_eq!(info.mapper, 0);
assert_eq!(info.mirroring, Mirroring::Horizontal);
}
#[test]
fn validate_bad_magic() {
let rom = vec![0x00; 100];
assert!(validate_ines(&rom).is_err());
}
#[test]
fn validate_too_small() {
let rom = vec![0x4E, 0x45, 0x53];
assert!(validate_ines(&rom).is_err());
}
#[test]
fn large_prg_uses_two_banks() {
let mut builder = RomBuilder::new(Mirroring::Horizontal);
builder.set_prg(vec![0xEA; 20000]); // > 16 KB
let rom = builder.build();
assert_eq!(rom[4], 2); // 2 PRG banks
assert_eq!(rom.len(), 16 + 32768);
}
#[test]
fn mapper_number_encoded() {
let mut builder = RomBuilder::new(Mirroring::Horizontal);
builder.set_mapper(1); // MMC1
let rom = builder.build();
let info = validate_ines(&rom).unwrap();
assert_eq!(info.mapper, 1);
}
#[test]
fn chr_data_is_padded() {
let mut builder = RomBuilder::new(Mirroring::Horizontal);
builder.set_chr(vec![0xBB; 16]); // 1 tile = 16 bytes
let rom = builder.build();
let chr_start = 16 + 16384; // after header + PRG
assert_eq!(&rom[chr_start..chr_start + 16], &[0xBB; 16]);
assert_eq!(rom[chr_start + 16], 0x00); // padded with zeros
}
#[test]
fn mapper_mmc1_encoded() {
let mut builder = RomBuilder::new(Mirroring::Horizontal);
builder.set_mapper(crate::rom::mapper_number(crate::parser::ast::Mapper::MMC1));
let rom = builder.build();
let info = validate_ines(&rom).unwrap();
assert_eq!(info.mapper, 1);
}
#[test]
fn mapper_uxrom_encoded() {
let mut builder = RomBuilder::new(Mirroring::Horizontal);
builder.set_mapper(crate::rom::mapper_number(crate::parser::ast::Mapper::UxROM));
let rom = builder.build();
let info = validate_ines(&rom).unwrap();
assert_eq!(info.mapper, 2);
}
#[test]
fn mapper_mmc3_encoded() {
let mut builder = RomBuilder::new(Mirroring::Horizontal);
builder.set_mapper(crate::rom::mapper_number(crate::parser::ast::Mapper::MMC3));
let rom = builder.build();
let info = validate_ines(&rom).unwrap();
assert_eq!(info.mapper, 4);
}
// ─── Multi-bank PRG layout ─────────────────────────────────────────
#[test]
fn set_prg_banks_with_two_banks_pads_and_concatenates() {
let mut builder = RomBuilder::new(Mirroring::Horizontal);
builder.set_prg_banks(vec![vec![0xAA, 0xAA, 0xAA], vec![0xBB, 0xBB]]);
let rom = builder.build();
// Header reports 2 banks.
assert_eq!(rom[4], 2);
// Total file size: 16 header + 2 * 16 KB
assert_eq!(rom.len(), 16 + 2 * 16384);
// First bank: 0xAA at the start, $FF padding, then second bank.
assert_eq!(&rom[16..19], &[0xAA, 0xAA, 0xAA]);
assert_eq!(rom[19], 0xFF); // padding continues in bank 0
// Second bank starts at 16 + 16384.
let bank1 = 16 + 16384;
assert_eq!(&rom[bank1..bank1 + 2], &[0xBB, 0xBB]);
assert_eq!(rom[bank1 + 2], 0xFF); // padding in bank 1
}
#[test]
fn set_prg_banks_with_four_banks_produces_64kb_prg() {
let mut builder = RomBuilder::new(Mirroring::Horizontal);
builder.set_prg_banks(vec![vec![0x00], vec![0x01], vec![0x02], vec![0x03]]);
let rom = builder.build();
assert_eq!(rom[4], 4, "header should report 4 PRG banks");
assert_eq!(rom.len(), 16 + 4 * 16384);
// Each bank's first byte should be the bank index.
for i in 0..4 {
let offset = 16 + i * 16384;
assert_eq!(rom[offset], i as u8, "bank {i} should start with byte {i}");
}
}
#[test]
#[should_panic(expected = "exceeds 16 KB")]
fn set_prg_banks_panics_when_bank_too_large() {
let mut builder = RomBuilder::new(Mirroring::Horizontal);
// 16 KB + 1 byte overflows a single 16 KB bank slot.
builder.set_prg_banks(vec![vec![0x00; 16385]]);
}
#[test]
fn set_prg_banks_preserves_content_exactly() {
// Verify byte-for-byte fidelity: if the caller provides bytes
// A, B, C in a bank, they must land at bank_start, bank_start+1,
// bank_start+2 with no rearrangement.
let mut builder = RomBuilder::new(Mirroring::Horizontal);
builder.set_prg_banks(vec![(0u8..=9).collect(), (100u8..=109).collect()]);
let rom = builder.build();
assert_eq!(&rom[16..26], &(0u8..=9).collect::<Vec<_>>()[..]);
let bank1 = 16 + 16384;
assert_eq!(
&rom[bank1..bank1 + 10],
&(100u8..=109).collect::<Vec<_>>()[..]
);
}
#[test]
fn validate_detects_multi_bank_prg() {
// A ROM built with 3 banks should validate with prg_banks=3.
let mut builder = RomBuilder::new(Mirroring::Vertical);
builder.set_mapper(1); // MMC1
builder.set_prg_banks(vec![vec![0x11; 10], vec![0x22; 10], vec![0x33; 10]]);
let rom = builder.build();
let info = validate_ines(&rom).unwrap();
assert_eq!(info.prg_banks, 3);
assert_eq!(info.mapper, 1);
assert_eq!(info.mirroring, Mirroring::Vertical);
}
#[test]
fn empty_prg_banks_fallback_to_single_bank() {
// If a caller doesn't call set_prg or set_prg_banks, the builder
// still produces a valid single-bank ROM so tests that only
// exercise the CHR path keep working.
let builder = RomBuilder::new(Mirroring::Horizontal);
let rom = builder.build();
assert_eq!(rom[4], 1, "default should be 1 PRG bank");
assert_eq!(rom.len(), 16 + 16384);
}
// ─── NES 2.0 header support ────────────────────────────────────────
#[test]
fn ines1_default_has_clear_nes2_marker() {
// Default header format is iNES 1.0 — byte 7 bits 2-3 must be 00.
let builder = RomBuilder::new(Mirroring::Horizontal);
let rom = builder.build();
assert_eq!(
rom[7] & 0x0C,
0x00,
"iNES 1.0 default must not set byte 7 bits 2-3"
);
// Bytes 8-15 must all be zero padding in iNES 1.0.
assert_eq!(&rom[8..16], &[0u8; 8]);
assert_eq!(rom.len(), 16 + 16384);
}
#[test]
fn nes2_header_sets_byte7_bits_2_3() {
// Opting into NES 2.0 must set byte 7 bits 2-3 to `10` (binary).
let mut builder = RomBuilder::new(Mirroring::Horizontal);
builder.enable_nes2();
let rom = builder.build();
assert_eq!(
rom[7] & 0x0C,
0x08,
"NES 2.0 header must set byte 7 bits 2-3 to 10"
);
// Header is still 16 bytes — NES 2.0 is not a longer header,
// it just reinterprets the existing bytes.
assert_eq!(rom.len(), 16 + 16384);
}
#[test]
fn nes2_header_populates_bytes_8_through_15() {
// Bytes 8-15 should all be zero for our tiny ROMs — we don't
// use submappers, oversized PRG/CHR, CHR RAM, or non-NTSC
// timing — but they must still be present (not omitted).
let mut builder = RomBuilder::new(Mirroring::Horizontal);
builder.enable_nes2();
let rom = builder.build();
assert_eq!(&rom[8..16], &[0u8; 8]);
}
#[test]
fn nes2_preserves_mapper_and_mirroring() {
// Opting into NES 2.0 should not disturb the mapper or
// mirroring fields in bytes 6-7.
let mut builder = RomBuilder::new(Mirroring::Vertical);
builder.set_mapper(crate::rom::mapper_number(crate::parser::ast::Mapper::MMC3));
builder.enable_nes2();
let rom = builder.build();
// Vertical mirroring keeps bit 0 of byte 6 set.
assert_eq!(rom[6] & 1, 1);
// Mapper 4 splits 0x40 across byte 6 high nibble and byte 7
// high nibble: 4 = 0b0100 → nibble 0 goes to byte 7.
let info = validate_ines(&rom).unwrap();
assert_eq!(info.mapper, 4);
assert_eq!(info.header_format, HeaderFormat::Nes2);
assert_eq!(info.mirroring, Mirroring::Vertical);
}
#[test]
fn validate_accepts_both_header_formats() {
// iNES 1.0 ROM validates and is marked as `Ines1`.
let ines1 = RomBuilder::new(Mirroring::Horizontal).build();
let info1 = validate_ines(&ines1).unwrap();
assert_eq!(info1.header_format, HeaderFormat::Ines1);
// NES 2.0 ROM validates and is marked as `Nes2`.
let mut b = RomBuilder::new(Mirroring::Horizontal);
b.enable_nes2();
let nes2 = b.build();
let info2 = validate_ines(&nes2).unwrap();
assert_eq!(info2.header_format, HeaderFormat::Nes2);
}
#[test]
fn nes2_mapper_high_nibble_in_byte_8_is_zero_for_small_mappers() {
// NEScript only supports u8 mapper numbers, so byte 8's low
// nibble (mapper bits 8-11) is always zero. Verify that
// explicitly so a future change that accidentally shifts bits
// into byte 8 is caught.
for mapper in [
crate::parser::ast::Mapper::NROM,
crate::parser::ast::Mapper::MMC1,
crate::parser::ast::Mapper::UxROM,
crate::parser::ast::Mapper::MMC3,
crate::parser::ast::Mapper::AxROM,
crate::parser::ast::Mapper::CNROM,
] {
let mut builder = RomBuilder::new(Mirroring::Horizontal);
builder.set_mapper(crate::rom::mapper_number(mapper));
builder.enable_nes2();
let rom = builder.build();
assert_eq!(rom[8], 0, "byte 8 should be zero for {mapper:?}");
}
}