1
0
Fork 0
mirror of https://github.com/imjasonh/nescript synced 2026-07-08 08:55:38 +00:00

tooling: add --no-opt CLI flag and criterion compile benchmarks

Adds two items from the "Code quality / tooling" section of
docs/future-work.md. Both make it easier to chase regressions
without touching codegen.

- `nescript build --no-opt` skips the IR optimizer pass so
  optimizer-introduced miscompiles can be bisected against the
  unoptimized output. Threaded through CompileOptions and gated
  at the single optimizer call site in src/main.rs. Covered by a
  new integration test that compiles the same program twice
  (opt on / opt off) and asserts both outputs are valid iNES
  ROMs with matching headers and reset vectors.

- A criterion-based `benches/compile.rs` harness that times the
  full parse -> analyze -> lower -> optimize -> codegen -> link
  pipeline on every examples/*.ne file. Sources are pre-read
  into memory so file I/O stays off the hot loop, and each
  example gets its own Criterion group for easy regression
  spotting.

Committed ROM bytes under examples/*.nes are unchanged; the
emulator goldens under tests/emulator/goldens/ are untouched.
This commit is contained in:
Claude 2026-04-14 01:43:51 +00:00
parent d2cfce595b
commit 65a63f9a68
No known key found for this signature in database
5 changed files with 740 additions and 2 deletions

View file

@ -1722,3 +1722,109 @@ fn e2e_banked_chr_rom_is_preserved() {
// Default smiley is non-zero in its first 16 bytes.
assert_ne!(&rom[chr_start..chr_start + 16], &[0u8; 16]);
}
/// Same as `compile_banked` but lets the caller toggle whether the IR
/// optimizer runs. Used to cover the `--no-opt` CLI flag: compiling
/// with the optimizer disabled must still produce a valid iNES ROM.
fn compile_banked_with_opts(source: &str, optimize: bool) -> Vec<u8> {
let (program, diags) = nescript::parser::parse(source);
assert!(
diags.is_empty(),
"unexpected parse errors: {diags:?}\nsource:\n{source}"
);
let program = program.expect("parse should succeed");
let analysis = analyzer::analyze(&program);
assert!(
analysis.diagnostics.iter().all(|d| !d.is_error()),
"unexpected analysis errors: {:?}",
analysis.diagnostics
);
let mut ir_program = ir::lower(&program, &analysis);
if optimize {
nescript::optimizer::optimize(&mut ir_program);
}
let sprites = assets::resolve_sprites(&program, Path::new("."))
.expect("sprite resolution should succeed");
let sfx = assets::resolve_sfx(&program).expect("sfx resolution should succeed");
let music = assets::resolve_music(&program).expect("music resolution should succeed");
let palettes = assets::resolve_palettes(&program);
let backgrounds = assets::resolve_backgrounds(&program);
let codegen = IrCodeGen::new(&analysis.var_allocations, &ir_program)
.with_sprites(&sprites)
.with_audio(&sfx, &music);
let mut instructions = codegen.generate(&ir_program);
nescript::codegen::peephole::optimize(&mut instructions);
let linker = Linker::with_mapper(program.game.mirroring, program.game.mapper);
let switchable_banks: Vec<PrgBank> = program
.banks
.iter()
.filter(|b| b.bank_type == BankType::Prg)
.map(|b| PrgBank::empty(&b.name))
.collect();
linker.link_banked_with_ppu(
&instructions,
&sprites,
&sfx,
&music,
&palettes,
&backgrounds,
&switchable_banks,
)
}
#[test]
fn no_opt_still_produces_valid_rom() {
// Acceptance test for the `--no-opt` CLI flag. Skipping the IR
// optimizer must still produce a byte-valid iNES ROM that links
// against the runtime, uses the declared mapper, and carries a
// plausible vector table. This guards the compile path the flag
// opens up so optimizer bisection remains a usable workflow.
let source = r#"
game "NoOpt" { mapper: NROM }
var counter: u8 = 0
var doubled: u8 = 0
fun double(x: u8) -> u8 {
return x + x
}
on frame {
counter += 1
doubled = double(counter)
if button.a {
counter = 0
}
wait_frame
}
start Main
"#;
let rom_opt = compile_banked_with_opts(source, true);
let rom_noopt = compile_banked_with_opts(source, false);
// Both outputs must be valid iNES ROMs with matching headers —
// the optimizer only affects PRG codegen, not the CHR/header
// layout the linker produces.
let info_opt = rom::validate_ines(&rom_opt).expect("opt ROM should be valid iNES");
let info_noopt = rom::validate_ines(&rom_noopt).expect("noopt ROM should be valid iNES");
assert_eq!(info_opt.mapper, 0);
assert_eq!(info_noopt.mapper, 0);
assert_eq!(info_opt.prg_banks, info_noopt.prg_banks);
assert_eq!(info_opt.chr_banks, info_noopt.chr_banks);
assert_eq!(rom_opt.len(), rom_noopt.len());
// The reset vector should still point into the fixed PRG bank
// in both builds — the optimizer has no say in where the reset
// handler lands.
let prg_end = 16 + 16384;
let reset_opt = u16::from_le_bytes([rom_opt[prg_end - 4], rom_opt[prg_end - 3]]);
let reset_noopt = u16::from_le_bytes([rom_noopt[prg_end - 4], rom_noopt[prg_end - 3]]);
assert_eq!(reset_opt, 0xC000);
assert_eq!(reset_noopt, 0xC000);
}