aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMiquel Sabaté Solà <mikisabate@gmail.com>2025-08-28 08:39:08 +0200
committerMiquel Sabaté Solà <mikisabate@gmail.com>2025-08-28 09:00:56 +0200
commitb93267c56565582b9a85e7a1255568e16abcafd2 (patch)
treec987acc61a8f83694be425df440e472068f99582
parent165cbcf2f92ea0aa992e96d5feb8aacd9752ac47 (diff)
downloadtools.nes-b93267c56565582b9a85e7a1255568e16abcafd2.tar.gz
tools.nes-b93267c56565582b9a85e7a1255568e16abcafd2.zip
nasm: Error out when using WRAM when not available
This commit introduces the ability to inspect the temptative header before producing the actual output, and with that it checks whether the Working RAM is being advertised or not. If it is not being advertised but the assembler detected memory accesses to that region, then we are in trouble and we should error out. Signed-off-by: Miquel Sabaté Solà <mikisabate@gmail.com>
-rw-r--r--Cargo.lock1
-rw-r--r--crates/nasm/Cargo.toml1
-rw-r--r--crates/nasm/src/main.rs31
-rw-r--r--lib/xixanta/src/assembler.rs26
4 files changed, 58 insertions, 1 deletions
diff --git a/Cargo.lock b/Cargo.lock
index 05d7010..ca14786 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -10,6 +10,7 @@ version = "0.1.0"
name = "nasm"
version = "0.1.0"
dependencies = [
+ "header",
"xixanta",
]
diff --git a/crates/nasm/Cargo.toml b/crates/nasm/Cargo.toml
index f0f11db..f1294f7 100644
--- a/crates/nasm/Cargo.toml
+++ b/crates/nasm/Cargo.toml
@@ -6,3 +6,4 @@ authors = ["Miquel Sabaté Solà <mikisabate@gmail.com>"]
[dependencies]
xixanta.workspace = true
+header.workspace = true
diff --git a/crates/nasm/src/main.rs b/crates/nasm/src/main.rs
index 3864587..6ca6d27 100644
--- a/crates/nasm/src/main.rs
+++ b/crates/nasm/src/main.rs
@@ -1,3 +1,4 @@
+use header::Header;
use std::fs::File;
use std::io::{self, Write};
use std::path::Path;
@@ -205,8 +206,36 @@ fn main() {
error_count += 1;
}
- // If everything was right, just deliver the bundles.
+ // Deliver the bundles unless the header is borked.
if error_count == 0 {
+ // Fetch the header first.
+ let mut temptative_header = vec![];
+ for b in &res.bundles {
+ for i in 0..b.size {
+ temptative_header.push(b.bytes[i as usize]);
+ }
+ if temptative_header.len() >= 0x10 {
+ break;
+ }
+ }
+
+ // Validate the resulting header.
+ match Header::try_from(temptative_header.as_slice()) {
+ Ok(header) => {
+ if res.accessing_working_ram && !header.has_persistent_memory {
+ die(
+ "requires Working RAM but the ROM header does not advertise it".to_string(),
+ );
+ }
+ }
+ Err(e) => {
+ die(format!(
+ "output would produce a malformed NES/Famicom ROM: {e}"
+ ));
+ }
+ };
+
+ // And now deliver the bundles.
for b in res.bundles {
for i in 0..b.size {
if let Err(e) = output.write_all(&[b.bytes[i as usize]]) {
diff --git a/lib/xixanta/src/assembler.rs b/lib/xixanta/src/assembler.rs
index 23d4219..5ab0e3d 100644
--- a/lib/xixanta/src/assembler.rs
+++ b/lib/xixanta/src/assembler.rs
@@ -83,6 +83,10 @@ struct Assembler<'a> {
// Sources that have been evaluated for the current session. This is
// directly tied to `Parser::sources`.
sources: Vec<SourceInfo>,
+
+ // Whether the assembler has detected accesses to Working RAM or not
+ // (0x6000-0x7FFF).
+ accessing_working_ram: bool,
}
/// All the information that a caller needs after calling either
@@ -104,6 +108,10 @@ pub struct AssemblerResult {
/// The resulting mappings after assembling a source. You can count on
/// fields like `offset` if `errors` is empty.
pub mappings: Vec<Mapping>,
+
+ /// Whether the resulting ROM needs Working RAM to be available in order to
+ /// work. Ignore this field if `errors` is not empty.
+ pub accessing_working_ram: bool,
}
/// Read the contents from the `reader` as a source file and produce a list of
@@ -131,6 +139,7 @@ pub fn assemble(
}],
warnings: vec![],
mappings: vec![],
+ accessing_working_ram: false,
};
}
};
@@ -161,6 +170,7 @@ pub fn assemble_with_mapping(
errors,
warnings: asm.warnings,
mappings: asm.mappings,
+ accessing_working_ram: asm.accessing_working_ram,
};
}
@@ -176,6 +186,7 @@ pub fn assemble_with_mapping(
errors: e.into(),
warnings: vec![],
mappings: asm.mappings,
+ accessing_working_ram: asm.accessing_working_ram,
};
}
}
@@ -188,6 +199,7 @@ pub fn assemble_with_mapping(
errors,
warnings: asm.warnings,
mappings: asm.mappings,
+ accessing_working_ram: asm.accessing_working_ram,
};
}
@@ -201,6 +213,7 @@ pub fn assemble_with_mapping(
errors,
warnings: asm.warnings,
mappings: asm.mappings,
+ accessing_working_ram: asm.accessing_working_ram,
};
}
@@ -212,6 +225,7 @@ pub fn assemble_with_mapping(
errors,
warnings: asm.warnings,
mappings: asm.mappings,
+ accessing_working_ram: asm.accessing_working_ram,
};
}
@@ -222,12 +236,14 @@ pub fn assemble_with_mapping(
errors: vec![],
warnings: asm.warnings,
mappings: asm.mappings,
+ accessing_working_ram: asm.accessing_working_ram,
},
Err(errors) => AssemblerResult {
bundles: vec![],
errors,
warnings: asm.warnings,
mappings: asm.mappings,
+ accessing_working_ram: asm.accessing_working_ram,
},
}
}
@@ -249,6 +265,7 @@ impl<'a> Assembler<'a> {
repeats_seen: 0,
warnings: vec![],
sources: vec![],
+ accessing_working_ram: false,
}
}
@@ -2003,6 +2020,15 @@ impl<'a> Assembler<'a> {
None => (AddressingMode::Implied, Bundle::new(true)),
};
+ // If the instruction is accessing memory and the bundle is resolved, we
+ // can start gathering info on the memory being used.
+ if bundle.resolved && !matches!(mode, AddressingMode::Implied | AddressingMode::Immediate) {
+ let value = bundle.value() as usize;
+ if (0x6000..0x8000).contains(&value) {
+ self.accessing_working_ram = true;
+ }
+ }
+
let mnemonic = node.value.value.to_lowercase();
match INSTRUCTIONS.get(&mnemonic) {
Some(entries) => match entries.get(&mode) {