1
0
Fork 0
mirror of https://github.com/imjasonh/nescript synced 2026-07-07 00:22:50 +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

450
Cargo.lock generated
View file

@ -8,6 +8,21 @@ version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
[[package]]
name = "aho-corasick"
version = "1.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301"
dependencies = [
"memchr",
]
[[package]]
name = "anes"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299"
[[package]]
name = "anstream"
version = "1.0.0"
@ -80,6 +95,12 @@ version = "2.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af"
[[package]]
name = "bumpalo"
version = "3.20.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb"
[[package]]
name = "bytemuck"
version = "1.25.0"
@ -92,12 +113,45 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495"
[[package]]
name = "cast"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5"
[[package]]
name = "cfg-if"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
[[package]]
name = "ciborium"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e"
dependencies = [
"ciborium-io",
"ciborium-ll",
"serde",
]
[[package]]
name = "ciborium-io"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757"
[[package]]
name = "ciborium-ll"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9"
dependencies = [
"ciborium-io",
"half",
]
[[package]]
name = "clap"
version = "4.6.0"
@ -153,12 +207,85 @@ dependencies = [
"cfg-if",
]
[[package]]
name = "criterion"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2b12d017a929603d80db1831cd3a24082f8137ce19c69e6447f54f5fc8d692f"
dependencies = [
"anes",
"cast",
"ciborium",
"clap",
"criterion-plot",
"is-terminal",
"itertools",
"num-traits",
"once_cell",
"oorandom",
"plotters",
"rayon",
"regex",
"serde",
"serde_derive",
"serde_json",
"tinytemplate",
"walkdir",
]
[[package]]
name = "criterion-plot"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6b50826342786a51a89e2da3a28f1c32b06e387201bc2d19791f622c673706b1"
dependencies = [
"cast",
"itertools",
]
[[package]]
name = "crossbeam-deque"
version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51"
dependencies = [
"crossbeam-epoch",
"crossbeam-utils",
]
[[package]]
name = "crossbeam-epoch"
version = "0.9.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e"
dependencies = [
"crossbeam-utils",
]
[[package]]
name = "crossbeam-utils"
version = "0.8.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
[[package]]
name = "crunchy"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5"
[[package]]
name = "diff"
version = "0.1.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8"
[[package]]
name = "either"
version = "1.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
[[package]]
name = "fdeflate"
version = "0.3.7"
@ -178,12 +305,29 @@ dependencies = [
"miniz_oxide",
]
[[package]]
name = "half"
version = "2.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b"
dependencies = [
"cfg-if",
"crunchy",
"zerocopy",
]
[[package]]
name = "heck"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
[[package]]
name = "hermit-abi"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c"
[[package]]
name = "image"
version = "0.25.10"
@ -197,12 +341,60 @@ dependencies = [
"png",
]
[[package]]
name = "is-terminal"
version = "0.4.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46"
dependencies = [
"hermit-abi",
"libc",
"windows-sys",
]
[[package]]
name = "is_terminal_polyfill"
version = "1.70.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
[[package]]
name = "itertools"
version = "0.10.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473"
dependencies = [
"either",
]
[[package]]
name = "itoa"
version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
[[package]]
name = "js-sys"
version = "0.3.95"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2964e92d1d9dc3364cae4d718d93f227e3abb088e747d92e0395bfdedf1c12ca"
dependencies = [
"once_cell",
"wasm-bindgen",
]
[[package]]
name = "libc"
version = "0.2.185"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "52ff2c0fe9bc6cb6b14a0592c2ff4fa9ceb83eea9db979b0487cd054946a2b8f"
[[package]]
name = "memchr"
version = "2.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
[[package]]
name = "miniz_oxide"
version = "0.8.9"
@ -229,6 +421,7 @@ version = "0.1.0"
dependencies = [
"ariadne",
"clap",
"criterion",
"image",
"pretty_assertions",
]
@ -242,12 +435,52 @@ dependencies = [
"autocfg",
]
[[package]]
name = "once_cell"
version = "1.21.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
[[package]]
name = "once_cell_polyfill"
version = "1.70.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
[[package]]
name = "oorandom"
version = "11.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e"
[[package]]
name = "plotters"
version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5aeb6f403d7a4911efb1e33402027fc44f29b5bf6def3effcc22d7bb75f2b747"
dependencies = [
"num-traits",
"plotters-backend",
"plotters-svg",
"wasm-bindgen",
"web-sys",
]
[[package]]
name = "plotters-backend"
version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df42e13c12958a16b3f7f4386b9ab1f3e7933914ecea48da7139435263a4172a"
[[package]]
name = "plotters-svg"
version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "51bae2ac328883f7acdfea3d66a7c35751187f870bc81f94563733a154d7a670"
dependencies = [
"plotters-backend",
]
[[package]]
name = "png"
version = "0.18.1"
@ -295,6 +528,113 @@ dependencies = [
"proc-macro2",
]
[[package]]
name = "rayon"
version = "1.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d"
dependencies = [
"either",
"rayon-core",
]
[[package]]
name = "rayon-core"
version = "1.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91"
dependencies = [
"crossbeam-deque",
"crossbeam-utils",
]
[[package]]
name = "regex"
version = "1.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276"
dependencies = [
"aho-corasick",
"memchr",
"regex-automata",
"regex-syntax",
]
[[package]]
name = "regex-automata"
version = "0.4.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f"
dependencies = [
"aho-corasick",
"memchr",
"regex-syntax",
]
[[package]]
name = "regex-syntax"
version = "0.8.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a"
[[package]]
name = "rustversion"
version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
[[package]]
name = "same-file"
version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
dependencies = [
"winapi-util",
]
[[package]]
name = "serde"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
dependencies = [
"serde_core",
"serde_derive",
]
[[package]]
name = "serde_core"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "serde_json"
version = "1.0.149"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86"
dependencies = [
"itoa",
"memchr",
"serde",
"serde_core",
"zmij",
]
[[package]]
name = "simd-adler32"
version = "0.3.9"
@ -318,6 +658,16 @@ dependencies = [
"unicode-ident",
]
[[package]]
name = "tinytemplate"
version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc"
dependencies = [
"serde",
"serde_json",
]
[[package]]
name = "unicode-ident"
version = "1.0.24"
@ -336,6 +686,80 @@ version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
[[package]]
name = "walkdir"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b"
dependencies = [
"same-file",
"winapi-util",
]
[[package]]
name = "wasm-bindgen"
version = "0.2.118"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0bf938a0bacb0469e83c1e148908bd7d5a6010354cf4fb73279b7447422e3a89"
dependencies = [
"cfg-if",
"once_cell",
"rustversion",
"wasm-bindgen-macro",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-macro"
version = "0.2.118"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eeff24f84126c0ec2db7a449f0c2ec963c6a49efe0698c4242929da037ca28ed"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
]
[[package]]
name = "wasm-bindgen-macro-support"
version = "0.2.118"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9d08065faf983b2b80a79fd87d8254c409281cf7de75fc4b773019824196c904"
dependencies = [
"bumpalo",
"proc-macro2",
"quote",
"syn",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-shared"
version = "0.2.118"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5fd04d9e306f1907bd13c6361b5c6bfc7b3b3c095ed3f8a9246390f8dbdee129"
dependencies = [
"unicode-ident",
]
[[package]]
name = "web-sys"
version = "0.3.95"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4f2dfbb17949fa2088e5d39408c48368947b86f7834484e87b73de55bc14d97d"
dependencies = [
"js-sys",
"wasm-bindgen",
]
[[package]]
name = "winapi-util"
version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
dependencies = [
"windows-sys",
]
[[package]]
name = "windows-link"
version = "0.2.1"
@ -356,3 +780,29 @@ name = "yansi"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049"
[[package]]
name = "zerocopy"
version = "0.8.48"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9"
dependencies = [
"zerocopy-derive",
]
[[package]]
name = "zerocopy-derive"
version = "0.8.48"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "zmij"
version = "1.0.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"

View file

@ -23,6 +23,11 @@ image = { version = "0.25", default-features = false, features = ["png"] }
[dev-dependencies]
pretty_assertions = "1"
criterion = "0.5"
[[bench]]
name = "compile"
harness = false
[profile.release]
lto = true

162
benches/compile.rs Normal file
View file

@ -0,0 +1,162 @@
//! End-to-end compilation benchmarks.
//!
//! Each `examples/*.ne` file becomes its own Criterion group that
//! times the full `parse → analyze → lower → optimize → codegen →
//! peephole → link` pipeline the `nescript build` CLI runs. The goal
//! is to catch compile-time regressions — today every example
//! compiles in well under 100 ms, so a change that doubles that
//! shows up as a large red bar in `cargo bench`'s output.
//!
//! The harness pre-reads every source file into memory before any
//! measurement starts. Criterion's sample iterations then run only
//! the in-memory compile path, so disk I/O never shows up on the
//! hot loop.
use std::fs;
use std::path::{Path, PathBuf};
use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion};
use nescript::analyzer;
use nescript::assets;
use nescript::codegen::{peephole, IrCodeGen};
use nescript::ir;
use nescript::linker::{Linker, PrgBank};
use nescript::optimizer;
use nescript::parser;
use nescript::parser::ast::BankType;
/// Pre-loaded `.ne` source plus the directory it was read from. The
/// directory matters because sprite `@binary` / `@chr` paths resolve
/// relative to the source file — the current examples all use inline
/// CHR, but resolving relative to the right directory keeps the bench
/// honest if an example later grows an external asset.
struct Example {
name: String,
source: String,
source_dir: PathBuf,
}
/// Scan `examples/*.ne` at the repo root and load every source file
/// into memory. Sorted by file name so the benchmark output is
/// reproducible across runs.
fn load_examples() -> Vec<Example> {
let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
let examples_dir = manifest_dir.join("examples");
let mut entries: Vec<PathBuf> = fs::read_dir(&examples_dir)
.unwrap_or_else(|e| panic!("failed to read {}: {e}", examples_dir.display()))
.filter_map(Result::ok)
.map(|e| e.path())
.filter(|p| p.extension().is_some_and(|ext| ext == "ne"))
.collect();
entries.sort();
entries
.into_iter()
.map(|path| {
let source = fs::read_to_string(&path)
.unwrap_or_else(|e| panic!("failed to read {}: {e}", path.display()));
let name = path
.file_stem()
.and_then(|s| s.to_str())
.unwrap_or("unknown")
.to_string();
let source_dir = path
.parent()
.map_or_else(|| PathBuf::from("."), Path::to_path_buf);
Example {
name,
source,
source_dir,
}
})
.collect()
}
/// Run the full CLI compile pipeline on an in-memory source string.
/// Mirrors `compile` in `src/main.rs`: parse → analyze → IR lower →
/// optimize → IR codegen → peephole → link. Panics on any error so
/// a regression that breaks the pipeline surfaces immediately instead
/// of silently skewing the measurements.
fn compile_pipeline(source: &str, source_dir: &Path) -> Vec<u8> {
let preprocessed = parser::preprocess_source(source, None)
.unwrap_or_else(|e| panic!("preprocess failed: {e}"));
let (program, parse_diags) = parser::parse(&preprocessed);
assert!(
!parse_diags
.iter()
.any(nescript::errors::Diagnostic::is_error),
"parse errors: {parse_diags:?}"
);
let program = program.expect("parse produced no program");
let analysis = analyzer::analyze(&program);
assert!(
!analysis
.diagnostics
.iter()
.any(nescript::errors::Diagnostic::is_error),
"analysis errors: {:?}",
analysis.diagnostics
);
let mut ir_program = ir::lower(&program, &analysis);
optimizer::optimize(&mut ir_program);
let sprites = assets::resolve_sprites(&program, source_dir).expect("sprite resolution failed");
let sfx = assets::resolve_sfx(&program).expect("sfx resolution failed");
let music = assets::resolve_music(&program).expect("music resolution failed");
let palettes = assets::resolve_palettes(&program);
let backgrounds = assets::resolve_backgrounds(&program);
let mut instructions = IrCodeGen::new(&analysis.var_allocations, &ir_program)
.with_sprites(&sprites)
.with_audio(&sfx, &music)
.generate(&ir_program);
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,
)
}
/// Criterion entry point. One benchmark group per example so the
/// HTML report groups them sensibly and so individual regressions
/// are easy to spot.
fn bench_compile(c: &mut Criterion) {
let examples = load_examples();
assert!(
!examples.is_empty(),
"no examples found under examples/*.ne — benchmark would measure nothing"
);
for example in &examples {
let mut group = c.benchmark_group(format!("compile/{}", example.name));
group.bench_with_input(
BenchmarkId::from_parameter(&example.name),
example,
|b, ex| {
b.iter(|| compile_pipeline(&ex.source, &ex.source_dir));
},
);
group.finish();
}
}
criterion_group!(benches, bench_compile);
criterion_main!(benches);

View file

@ -42,6 +42,13 @@ enum Cli {
/// Dump a call graph showing which functions call which.
#[arg(long)]
call_graph: bool,
/// Skip the IR optimizer pass. Useful for bisecting
/// optimizer-introduced miscompiles: if a program misbehaves
/// with the optimizer on but works with `--no-opt`, the bug
/// lives in `src/optimizer/`.
#[arg(long)]
no_opt: bool,
},
/// Type-check a source file without building
Check {
@ -62,6 +69,7 @@ fn main() {
dump_ir,
memory_map,
call_graph,
no_opt,
} => {
let output = output.unwrap_or_else(|| input.with_extension("nes"));
match compile(
@ -72,6 +80,7 @@ fn main() {
dump_ir,
memory_map,
call_graph,
no_opt,
},
) {
Ok(rom) => {
@ -220,6 +229,7 @@ struct CompileOptions {
dump_ir: bool,
memory_map: bool,
call_graph: bool,
no_opt: bool,
}
fn compile(input: &PathBuf, opts: &CompileOptions) -> Result<Vec<u8>, ()> {
@ -228,6 +238,7 @@ fn compile(input: &PathBuf, opts: &CompileOptions) -> Result<Vec<u8>, ()> {
let dump_ir = opts.dump_ir;
let memory_map = opts.memory_map;
let call_graph = opts.call_graph;
let no_opt = opts.no_opt;
let raw_source = std::fs::read_to_string(input).map_err(|e| {
eprintln!("error: failed to read {}: {e}", input.display());
})?;
@ -265,9 +276,13 @@ fn compile(input: &PathBuf, opts: &CompileOptions) -> Result<Vec<u8>, ()> {
return Err(());
}
// IR lowering and optimization
// IR lowering and (optionally) optimization. `--no-opt` skips
// the IR optimizer pass entirely so optimizer-introduced
// miscompiles can be bisected against the unoptimized output.
let mut ir_program = ir::lower(&program, &analysis);
optimizer::optimize(&mut ir_program);
if !no_opt {
optimizer::optimize(&mut ir_program);
}
if dump_ir {
print!("{}", ir_program.pretty());

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);
}