aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Cargo.lock3
-rw-r--r--README.md7
-rw-r--r--crates/readrom/Cargo.toml1
-rw-r--r--crates/readrom/README.md136
-rw-r--r--crates/readrom/src/main.rs275
-rw-r--r--lib/xixanta/src/assembler.rs2
-rw-r--r--lib/xixanta/src/opcodes.rs2180
-rwxr-xr-xscripts/test-e2e.sh26
-rw-r--r--tests/expected/readrom/a3b2-address.txt1
-rw-r--r--tests/expected/readrom/header.txt11
-rw-r--r--tests/expected/readrom/jetpac-full-nmi.txt123
11 files changed, 2752 insertions, 13 deletions
diff --git a/Cargo.lock b/Cargo.lock
index ca14786..fe984a6 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1,6 +1,6 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
-version = 3
+version = 4
[[package]]
name = "header"
@@ -19,6 +19,7 @@ name = "readrom"
version = "1.0.0"
dependencies = [
"header",
+ "xixanta",
]
[[package]]
diff --git a/README.md b/README.md
index 771f9a5..80f171d 100644
--- a/README.md
+++ b/README.md
@@ -22,10 +22,9 @@ will invoke `nasm` with more pedantic features like its address sanitizer.
## `readrom`
-The `readrom` program reads a given ROM file and shows all the information that
-can be gathered from it. For now this only applies to information on the header,
-but in the future we might want to add disassembling user-specified segments,
-for example.
+`readrom` is an objdump-like utility that reads a given ROM file and shows
+information about it. Read more about it in
+[./crates/readrom/README.md](./crates/readrom/README.md).
## License
diff --git a/crates/readrom/Cargo.toml b/crates/readrom/Cargo.toml
index ff2f04b..90f02ea 100644
--- a/crates/readrom/Cargo.toml
+++ b/crates/readrom/Cargo.toml
@@ -7,3 +7,4 @@ authors.workspace = true
[dependencies]
header.workspace = true
+xixanta.workspace = true
diff --git a/crates/readrom/README.md b/crates/readrom/README.md
new file mode 100644
index 0000000..5252cb7
--- /dev/null
+++ b/crates/readrom/README.md
@@ -0,0 +1,136 @@
+`readrom` is an objdump-like utility that reads a given NES/Famicom ROM file and
+shows information about it. You can run it by simply passing a ROM file to
+it. For example, for the [Jetpac NTSC ROM
+file](https://github.com/mssola/jetpac.nes/releases/tag/v1.0):
+
+```
+$ readrom jetpac.NTSC.nes
+# => output
+Header:
+ Kind: NES 2.0
+ PRG ROM size: 32768 bytes (32KB)
+ CHR ROM size: 8192 bytes (8KB)
+ Mapper: NROM
+ Mirroring: Horizontal
+ CPU/PPU timing: NTSC
+Vectors:
+ NMI: 0xa2f8
+ Reset: 0xa3c2
+ IRQ: 0xa3c1
+```
+
+But this utility can do some more complex things. For example, you can tell it
+to disassemble a subroutine via the `-d/--disassemble` flag. If the above output
+is telling us that NMI code starts at 0xa2f8, we can try:
+
+```
+$ readrom -d '$a2f8' jetpac.NTSC.nes
+# => output
+$A2F8: 24 20 bit $20
+```
+
+Note that hexadecimal values can also be formatted like this `0xa2fb` or simply
+`a2fb`, whatever feels more convenient to use. In any case, they should be
+written as a 16-bit address.
+
+All of that being said, poking for addresses can be tedious. That's why you can
+also pass names that you already know. For that, you will need to also pass the
+`-n/--nasm-directory`, pointing to the path to your `.nasm/` directory. With
+that, if you have assembled the ROM file with `nasm` with the `--write-info`
+flag, you will have something like this:
+
+```
+# On the jetpac.nes repository
+$ nasm --write-info -o jetpac.NTSC.nes src/jetpac.s
+$ readrom -d nmi -n .nasm/ jetpac.NTSC.nes
+# => output
+$A2F8: 24 20 bit $20
+$A2FA: 30 01 bmi @save_registers
+$A2FC: 40 rti
+
+ @save_registers:
+$A2FD: 48 pha
+$A2FE: 8A txa
+$A2FF: 48 pha
+$A300: 98 tya
+$A301: 48 pha
+$A302: A9 00 lda #$00
+$A304: 8D 03 20 sta $2003
+$A307: A9 02 lda #$02
+$A309: 8D 14 40 sta $4014
+$A30C: 24 28 bit $28
+$A30E: 10 03 bpl @check_pause
+$A310: 20 EE 8C jsr nmi_update_scores
+
+ @check_pause:
+$A313: 24 38 bit $38
+$A315: 50 03 bvc @increase_rand
+$A317: 20 62 A2 jsr nmi_hud_toggle_pause
+
+# and much more...
+```
+
+Moreover, if you have built your ROM file with the `--asan` flag from `nasm`,
+then you will have the `memory.txt` file inside of `.nasm/`. This tool is able
+to pick up this file and decorate some of the values from the previous
+output. Hence:
+
+```
+# On the jetpac.nes repository
+$ nasm --write-info --asan -o jetpac.NTSC.nes src/jetpac.s
+$ readrom -d nmi -n .nasm/ jetpac.NTSC.nes
+# => output
+$A2F8: 24 20 bit Globals::zp_flags
+$A2FA: 30 01 bmi @save_registers
+$A2FC: 40 rti
+
+ @save_registers:
+$A2FD: 48 pha
+$A2FE: 8A txa
+$A2FF: 48 pha
+$A300: 98 tya
+$A301: 48 pha
+$A302: A9 00 lda #$00
+$A304: 8D 03 20 sta OAM::m_address
+$A307: A9 02 lda #$02
+$A309: 8D 14 40 sta OAM::m_dma
+$A30C: 24 28 bit Globals::zp_extra_flags
+$A30E: 10 03 bpl @check_pause
+$A310: 20 EE 8C jsr nmi_update_scores
+
+ @check_pause:
+$A313: 24 38 bit Driver::zp_flags
+$A315: 50 03 bvc @increase_rand
+$A317: 20 62 A2 jsr nmi_hud_toggle_pause
+
+# and much more...
+```
+
+Now you can see the same output as before, but the first instruction reads as
+`bit Globals::zp_flags` instead of `bit $20`, because `readrom` now knows that
+the memory address `$20` is associated with this variable.
+
+Moreover, and as you can tell, by default `readrom` will print things in a
+human-readable format. But you can also tell it to just write all the bytes with
+the `--raw` flag so you can further manipulate the related bytes with another
+tool. For example:
+
+```
+# On the jetpac.nes repository
+$ nasm --write-info -o jetpac.NTSC.nes src/jetpac.s
+$ readrom -d nmi -n .nasm/ --raw jetpac.NTSC.nes | hexdump -C
+# => output
+00000000 24 20 30 01 40 48 8a 48 98 48 a9 00 8d 03 20 a9 |$ 0.@H.H.H.... .|
+00000010 02 8d 14 40 24 28 10 03 20 ee 8c 24 38 50 03 20 |...@$(.. ..$8P. |
+00000020 62 a2 e6 0a a9 08 25 20 d0 6e 24 2e 10 0b a9 00 |b.....% .n$.....|
+00000030 50 02 a9 70 85 04 20 bc a2 a5 50 29 08 f0 34 2c |P..p.. ...P)..4,|
+00000040 02 20 a9 28 8d 06 20 a9 4b 8d 06 20 a5 53 18 69 |. .(.. .K.. .S.i|
+00000050 10 8d 07 20 24 27 10 15 2c 02 20 a9 28 8d 06 20 |... $'..,. .(.. |
+00000060 a9 56 8d 06 20 a5 54 18 69 10 8d 07 20 a5 50 29 |.V.. .T.i... .P)|
+00000070 f7 85 50 a5 20 aa 29 20 f0 13 a5 cb d0 06 20 78 |..P. .) ...... x|
+00000080 89 4c 7f a3 20 59 91 a5 20 29 df 85 20 8a 29 01 |.L.. Y.. ).. .).|
+00000090 d0 06 a5 30 f0 02 c6 30 24 20 50 13 a9 bf 25 20 |...0...0$ P...% |
+000000a0 85 20 2c 02 20 a5 81 8d 01 20 a5 80 8d 00 20 2c |. ,. .... .... ,|
+000000b0 02 20 a9 00 8d 05 20 8d 05 20 20 1d 8b a9 7f 25 |. .... .. ....%|
+000000c0 20 85 20 68 a8 68 aa 68 40 | . h.h.h@|
+```
diff --git a/crates/readrom/src/main.rs b/crates/readrom/src/main.rs
index 3c87417..e2f4343 100644
--- a/crates/readrom/src/main.rs
+++ b/crates/readrom/src/main.rs
@@ -1,6 +1,9 @@
use header::{Header, Kind};
+use std::collections::HashMap;
use std::fs::File;
-use std::io::{ErrorKind, Read};
+use std::io::{BufRead, BufReader, BufWriter, ErrorKind, Read, Write};
+use std::path::PathBuf;
+use xixanta::opcodes::OPCODES;
/// Version for this program.
const VERSION: &str = "0.1.0";
@@ -9,15 +12,21 @@ const VERSION: &str = "0.1.0";
struct Args {
file: String,
header: bool,
+ disassemble: Option<String>,
+ nasm: Option<String>,
+ raw: bool,
}
fn print_help() {
println!("Display information about NES/Famicom ROM files.\n");
println!("usage: readrom [OPTIONS] <FILE>\n");
println!("Options:");
- println!(" -h, --help\t\tPrint this message.");
- println!(" -H, --header\t\tJust print the ROM header and quit.");
- println!(" -v, --version\t\tPrint the version of this program.");
+ println!(" -d, --disassemble <ADDRESS>\tDisassemble starting from the given ADDRESS.");
+ println!(" -h, --help\t\t\tPrint this message.");
+ println!(" -H, --header\t\t\tJust print the ROM header and quit.");
+ println!(" -n, --nasm-directory <PATH>\tPath to the .nasm/ directory.");
+ println!(" -r, --raw\t\t\tPrint bytes with no formatting at all when disassembling.");
+ println!(" -v, --version\t\t\tPrint the version of this program.");
std::process::exit(0);
}
@@ -30,8 +39,14 @@ fn parse_arguments() -> Args {
// Skip command name.
args.next();
- for arg in args {
+ while let Some(arg) = args.next() {
match arg.as_str() {
+ "-d" | "--disassemble" => match args.next() {
+ Some(a) => res.disassemble = Some(a),
+ None => die(
+ "you need to specify an address for the '-d/--disassemble' flag".to_string(),
+ ),
+ },
"-h" | "--help" => print_help(),
"-H" | "--header" => {
if res.header {
@@ -39,6 +54,11 @@ fn parse_arguments() -> Args {
}
res.header = true;
}
+ "-n" | "--nasm" => match args.next() {
+ Some(a) => res.nasm = Some(a),
+ None => die("you need to specify a file for the '-n/--nasm' flag".to_string()),
+ },
+ "-r" | "--raw" => res.raw = true,
"-v" | "--version" => {
println!("readrom {VERSION}");
std::process::exit(0);
@@ -140,6 +160,240 @@ fn die(message: String) {
std::process::exit(1);
}
+// Print a block of code from the given open ROM 'file'. The range is indicated
+// by 'start' and an optional 'end'. If 'end' is None, then the block of code
+// will only span a single instruction. Moreover, the 'memories' and the
+// 'addresses' maps can help assist the printing with the information taken from
+// the .nasm/memory.txt and .nasm/addresses.txt files respectively. Finally, set
+// 'raw' to true if you want all bytes to be printed directly into the stdout,
+// otherwise a human-readable format will be used.
+fn print_range(
+ mut file: File,
+ start: usize,
+ end: Option<usize>,
+ memories: HashMap<usize, String>,
+ addresses: HashMap<usize, String>,
+ raw: bool,
+ filter: Option<&str>,
+) -> Result<(), String> {
+ let mut bytes = Vec::new();
+ file.read_to_end(&mut bytes).map_err(|e| e.to_string())?;
+
+ // Fetch the bytes to be printed.
+
+ // NOTE: minus 0x8000 to account for non-ROM address, plus 0x10 to skip the
+ // header from the file.
+ let range_start = start
+ .checked_sub(0x8000)
+ .map(|val| val + 0x10)
+ .ok_or_else(|| format!("bad start address {:#x}", start))?;
+
+ // The 'end' of the range depends on whether the user is just printing a
+ // single instruction or it's really trying to print a proper range.
+ let res = match end {
+ Some(e) => {
+ let range_end = e
+ .checked_sub(0x8000)
+ .map(|val| val + 0x10)
+ .ok_or_else(|| format!("bad end address {:#x}", e))?;
+ bytes.get(range_start..range_end).unwrap_or(&[])
+ }
+ // If it's just one instruction, take the opcode byte and some more to
+ // account for the maximum size of an instruction on this platform.
+ None => bytes.get(range_start..range_start + 3).unwrap_or(&[]),
+ };
+
+ // Printing raw: blindessly spit bytes to stdout.
+ if raw {
+ let mut writer = BufWriter::new(std::io::stdout());
+ writer
+ .write_all(&res[..res.len()])
+ .map_err(|_| "cannot write to the stdout".to_string())?;
+ return Ok(());
+ }
+
+ // Print into a more human-readable shape.
+
+ let mut iter = res.iter();
+ let mut current_address = start;
+ while let Some(byte) = iter.next() {
+ // Given the opcode, fetch the instruction object for it, and how much
+ // the address should be advanced after printing the instruction.
+ let (instr, size, formatted) = match OPCODES.get(byte) {
+ Some(tpl) => {
+ let mut ins = tpl.clone();
+ let mut formatted = format!("{:02X}\t", byte);
+
+ match ins.size {
+ 2 => {
+ ins.bytes[0] = *iter.next().unwrap_or(&0);
+ formatted = format!("{:02X} {:02X}\t", byte, ins.bytes[0]);
+ }
+ 3 => {
+ ins.bytes[0] = *iter.next().unwrap_or(&0);
+ ins.bytes[1] = *iter.next().unwrap_or(&0);
+ formatted =
+ format!("{:02X} {:02X} {:02X}", byte, ins.bytes[0], ins.bytes[1]);
+ }
+ _ => {}
+ }
+
+ (
+ ins.to_human(current_address, filter, &memories, &addresses),
+ ins.size,
+ formatted,
+ )
+ }
+ None => (byte.to_string(), 1, byte.to_string()),
+ };
+
+ // Do we actually know of a label which points to the current address?
+ // If so, show it now.
+ if let Some(address_name) = addresses.get(&current_address)
+ && current_address != start
+ {
+ println!(
+ "\n {}:",
+ address_name.split("::").last().unwrap_or(address_name)
+ );
+ }
+
+ // And print our awesome line :)
+ println!("${:4X}:\t{}\t{} ", current_address, formatted, instr);
+ current_address += size as usize;
+
+ if end.is_none() {
+ break;
+ }
+ }
+
+ // Sometimes there is a label marking the end of the code, which is set
+ // after the last instruction. Show these labels too as some branch
+ // instructions can use it.
+ if let Some(address_name) = addresses.get(&current_address) {
+ println!(
+ "\n {}:",
+ address_name.split("::").last().unwrap_or(address_name)
+ );
+ }
+
+ Ok(())
+}
+
+fn parse_hex_value(address: &str) -> Option<usize> {
+ match address.len() {
+ // Simple 'a2fb' format.
+ 4 => {
+ if let Ok(val) = usize::from_str_radix(address, 16) {
+ return Some(val);
+ }
+ }
+ // nasm's '$a2fb' format.
+ 5 => {
+ if let Some(addr) = address.get(1..)
+ && let Ok(val) = usize::from_str_radix(addr, 16)
+ {
+ return Some(val);
+ }
+ }
+ // Standard '0xa2fb' format.
+ 6 => {
+ if let Some(addr) = address.get(2..)
+ && let Ok(val) = usize::from_str_radix(addr, 16)
+ {
+ return Some(val);
+ }
+ }
+ _ => {}
+ }
+
+ None
+}
+
+fn do_disassemble(
+ input: File,
+ address: &str,
+ nasm_path: &Option<String>,
+ raw: bool,
+) -> Result<(), String> {
+ let mut start = None;
+ let mut end = None;
+ let mut is_nasm_path = true;
+ let mut addresses: HashMap<usize, String> = HashMap::default();
+ let mut memories: HashMap<usize, String> = HashMap::default();
+
+ // Fill up the 'addresses' and the 'memories' maps.
+ if let Some(path) = nasm_path {
+ if let Ok(file) = File::open(PathBuf::from(path).join("addresses.txt")) {
+ let reader = BufReader::new(file);
+ for line in reader.lines() {
+ let line = line.map_err(|e| e.to_string())?;
+ let columns: Vec<&str> = line.split(',').map(|s| s.trim()).collect();
+ if columns.len() != 3 {
+ return Err("badly formatted address file".to_string());
+ }
+
+ let parsed_start = usize::from_str_radix(columns[1], 16)
+ .map_err(|_| format!("invalid hex value: '{}'", columns[1]))?;
+ addresses.insert(parsed_start, columns[0].to_string());
+
+ if columns[0] == address {
+ start = Some(parsed_start);
+ end = Some(
+ usize::from_str_radix(columns[2], 16)
+ .map_err(|_| format!("invalid hex value: '{}'", columns[2]))?,
+ );
+ }
+ }
+ }
+
+ // If the memory.txt file is available, fill up the 'memories' hash.
+ if let Ok(file) = File::open(PathBuf::from(path).join("memory.txt")) {
+ let reader = BufReader::new(file);
+ for line in reader.lines() {
+ let line = line.map_err(|e| e.to_string())?;
+ if line.is_empty() || line.starts_with("---") {
+ break;
+ }
+
+ let (left, right) = line.split_once(':').unwrap();
+ let start = match left.trim().split_once('-') {
+ Some((start, _)) => usize::from_str_radix(start.get(1..).unwrap(), 16).unwrap(),
+ None => usize::from_str_radix(left.get(1..).unwrap(), 16).unwrap(),
+ };
+ memories.insert(start, right.trim().to_string());
+ }
+ }
+ } else {
+ is_nasm_path = false;
+ }
+
+ // If this is just a numeric value, take it as is.
+ if let Some(start) = parse_hex_value(address) {
+ return print_range(input, start, None, memories, addresses, raw, None);
+ }
+
+ // Otherwise, print the full range if possible.
+ if !is_nasm_path {
+ Err("you need to use the '-n/--nasm-directory' on disassembly".to_string())
+ } else if addresses.is_empty() {
+ Err("failed to open the .nasm/addresses.txt file".to_string())
+ } else {
+ match start {
+ Some(s) => print_range(
+ input,
+ s,
+ Some(end.unwrap()),
+ memories,
+ addresses,
+ raw,
+ Some(format!("{address}::").as_str()),
+ ),
+ None => Err(format!("could not find address '{address}'")),
+ }
+ }
+}
+
fn main() {
let args = parse_arguments();
@@ -148,7 +402,16 @@ fn main() {
return;
};
- // Header.
+ // Check whether the user wanted to disassemble something.
+
+ if let Some(address) = args.disassemble {
+ if let Err(e) = do_disassemble(input, &address, &args.nasm, args.raw) {
+ die(e);
+ }
+ std::process::exit(0);
+ }
+
+ // Nope. Then let's just print information about it. First the header.
let mut buf = vec![0u8; 0x10];
if let Err(e) = input.read_exact(&mut buf) {
diff --git a/lib/xixanta/src/assembler.rs b/lib/xixanta/src/assembler.rs
index 959840d..91f1e14 100644
--- a/lib/xixanta/src/assembler.rs
+++ b/lib/xixanta/src/assembler.rs
@@ -1429,7 +1429,7 @@ impl<'a> Assembler<'a> {
// Using unwrap() is safe as it only returns an error on
// badly formatted addresses. This will not happen at
// this point.
- let resolved = self.context.resolve_label(&self.mappings, &bundle).unwrap();
+ let resolved = self.context.resolve_label(&self.mappings, bundle).unwrap();
let val = resolved.bundle.value() as usize;
let end = match self.address_ends.get(&full_name) {
diff --git a/lib/xixanta/src/opcodes.rs b/lib/xixanta/src/opcodes.rs
index bc4f1d4..d5bb97a 100644
--- a/lib/xixanta/src/opcodes.rs
+++ b/lib/xixanta/src/opcodes.rs
@@ -37,6 +37,69 @@ impl fmt::Display for AddressingMode {
}
}
+/// All the regular instructions that the NES/Famicom can actually run.
+#[derive(Clone, Debug)]
+pub enum InstructionIdentifier {
+ Adc,
+ And,
+ Asl,
+ Bcc,
+ Bcs,
+ Bit,
+ Beq,
+ Bmi,
+ Bne,
+ Bpl,
+ Brk,
+ Bvc,
+ Bvs,
+ Clc,
+ Cld,
+ Cli,
+ Clv,
+ Cmp,
+ Cpx,
+ Cpy,
+ Dec,
+ Dex,
+ Dey,
+ Eor,
+ Inc,
+ Inx,
+ Iny,
+ Jmp,
+ Jsr,
+ Lda,
+ Ldx,
+ Ldy,
+ Lsr,
+ Nop,
+ Ora,
+ Pha,
+ Php,
+ Pla,
+ Plp,
+ Rti,
+ Rts,
+ Rol,
+ Ror,
+ Sbc,
+ Sec,
+ Sed,
+ Sei,
+ Sta,
+ Stx,
+ Sty,
+ Tax,
+ Tay,
+ Tsx,
+ Txa,
+ Txs,
+ Tya,
+}
+
+/// An entry to the 'INSTRUCTIONS' map, which holds some values relevant for
+/// understanding the associated instruction.
#[derive(Debug)]
pub struct ShortEntry {
pub cycles: u8,
@@ -45,6 +108,202 @@ pub struct ShortEntry {
pub affected_on_page: bool,
}
+/// An Instruction from the Ricoh 2A03 chip. This is mainly used in the
+/// 'OPCODES' map, and it's an expansion of the 'ShortEntry' struct.
+#[derive(Clone, Debug)]
+pub struct Instruction {
+ pub identifier: InstructionIdentifier,
+ pub addressing_mode: AddressingMode,
+ pub cycles: u8,
+ pub opcode: u8,
+ pub size: u8,
+ pub affected_on_page: bool,
+ pub bytes: [u8; 2],
+}
+
+impl Instruction {
+ /// Returns the value that can be obtained by evaluating the 'bytes' member
+ /// as a little endian decimal.
+ pub fn value(&self) -> usize {
+ match self.size {
+ 2 => self.bytes[0] as usize,
+ 3 => self.bytes[0] as usize + ((self.bytes[1] as usize) << 8),
+ _ => 0,
+ }
+ }
+
+ /// Returns true if this is a branching instruction.
+ pub fn is_branch(&self) -> bool {
+ matches!(
+ self.identifier,
+ InstructionIdentifier::Bcc
+ | InstructionIdentifier::Bcs
+ | InstructionIdentifier::Beq
+ | InstructionIdentifier::Bmi
+ | InstructionIdentifier::Bne
+ | InstructionIdentifier::Bpl
+ | InstructionIdentifier::Bvc
+ | InstructionIdentifier::Bvs
+ )
+ }
+
+ /// Given the 'current' target address, and the helper 'memories' and
+ /// 'addresses' maps, attempt to return a String representation of this
+ /// instruction which is as human-readable as possible. This involves trying
+ /// to resolve addresses/values from either maps.
+ pub fn to_human(
+ &self,
+ current: usize,
+ filter: Option<&str>,
+ memories: &HashMap<usize, String>,
+ addresses: &HashMap<usize, String>,
+ ) -> String {
+ let id = match self.identifier {
+ InstructionIdentifier::Adc => "adc",
+ InstructionIdentifier::And => "and",
+ InstructionIdentifier::Asl => "asl",
+ InstructionIdentifier::Bcc => "bcc",
+ InstructionIdentifier::Bcs => "bcs",
+ InstructionIdentifier::Bit => "bit",
+ InstructionIdentifier::Beq => "beq",
+ InstructionIdentifier::Bne => "bne",
+ InstructionIdentifier::Bpl => "bpl",
+ InstructionIdentifier::Bmi => "bmi",
+ InstructionIdentifier::Brk => "brk",
+ InstructionIdentifier::Bvc => "bvc",
+ InstructionIdentifier::Bvs => "bvs",
+ InstructionIdentifier::Clc => "clc",
+ InstructionIdentifier::Cld => "cld",
+ InstructionIdentifier::Cli => "cli",
+ InstructionIdentifier::Clv => "clv",
+ InstructionIdentifier::Cmp => "cmp",
+ InstructionIdentifier::Cpx => "cpx",
+ InstructionIdentifier::Cpy => "cpy",
+ InstructionIdentifier::Dec => "dec",
+ InstructionIdentifier::Dex => "dex",
+ InstructionIdentifier::Dey => "dey",
+ InstructionIdentifier::Eor => "eor",
+ InstructionIdentifier::Inc => "inc",
+ InstructionIdentifier::Inx => "inx",
+ InstructionIdentifier::Iny => "iny",
+ InstructionIdentifier::Jmp => "jmp",
+ InstructionIdentifier::Jsr => "jsr",
+ InstructionIdentifier::Lda => "lda",
+ InstructionIdentifier::Ldx => "ldx",
+ InstructionIdentifier::Ldy => "ldy",
+ InstructionIdentifier::Lsr => "lsr",
+ InstructionIdentifier::Nop => "nop",
+ InstructionIdentifier::Ora => "ora",
+ InstructionIdentifier::Pha => "pha",
+ InstructionIdentifier::Php => "php",
+ InstructionIdentifier::Pla => "pla",
+ InstructionIdentifier::Plp => "plp",
+ InstructionIdentifier::Rol => "rol",
+ InstructionIdentifier::Ror => "ror",
+ InstructionIdentifier::Rti => "rti",
+ InstructionIdentifier::Rts => "rts",
+ InstructionIdentifier::Sbc => "sbc",
+ InstructionIdentifier::Sec => "sec",
+ InstructionIdentifier::Sed => "sed",
+ InstructionIdentifier::Sei => "sei",
+ InstructionIdentifier::Sta => "sta",
+ InstructionIdentifier::Stx => "stx",
+ InstructionIdentifier::Sty => "sty",
+ InstructionIdentifier::Tax => "tax",
+ InstructionIdentifier::Tay => "tay",
+ InstructionIdentifier::Tsx => "tsx",
+ InstructionIdentifier::Txa => "txa",
+ InstructionIdentifier::Txs => "txs",
+ InstructionIdentifier::Tya => "tya",
+ };
+
+ // On most addressing modes, if the current value is actually found on
+ // the map of known reserved memory addresses, then we can blindly try
+ // to fetch it first. For relative, zeropage and absolute things are
+ // more nuanced, so we delay this check in these scenarios.
+ if matches!(
+ self.addressing_mode,
+ AddressingMode::IndexedX
+ | AddressingMode::IndexedY
+ | AddressingMode::ZeropageIndexedX
+ | AddressingMode::ZeropageIndexedY
+ | AddressingMode::Indirect
+ | AddressingMode::IndirectX
+ | AddressingMode::IndirectY
+ ) {
+ let val = self.value();
+ if let Some(name) = memories.get(&val) {
+ return format!("{id} {name}");
+ }
+ }
+
+ match self.addressing_mode {
+ AddressingMode::Immediate => {
+ let val = self.value();
+ if val > u8::MAX as usize {
+ format!("{id} #${:04X}", val)
+ } else {
+ format!("{id} #${:02X}", val)
+ }
+ }
+ AddressingMode::RelativeOrZeropage | AddressingMode::Absolute => {
+ let val = self.value();
+
+ // If this is a branch instruction, then we compute the final
+ // address via the two's complement encoding of the given byte
+ // plus the current address + 2 bytes to account for the branch
+ // instruction itself. If all of that gives us a known address,
+ // then use it, otherwise treat it as a relative/zeropage
+ // nomenclature.
+ if self.is_branch() {
+ let target = current as isize + (val as i8) as isize + 2;
+
+ if let Some(name) = addresses.get(&(target as usize)) {
+ let clean_name = match filter {
+ Some(f) => name.strip_prefix(f).unwrap_or(name),
+ None => name,
+ };
+ return format!("{id} {clean_name}");
+ }
+ return format!("{id} ${:02X}", val);
+ }
+
+ // There's no relative address to compute, so we check whether
+ // this is a known absolute address.
+ if let Some(name) = addresses.get(&val) {
+ // let last_name = name.split("::").last().unwrap_or(name);
+ // return format!("{id} {name}");
+ let clean_name = match filter {
+ Some(f) => name.strip_prefix(f).unwrap_or(name),
+ None => name,
+ };
+ return format!("{id} {clean_name}");
+ }
+
+ // Is it on the map ok known memory addresses?
+ let val = self.value();
+ if let Some(name) = memories.get(&val) {
+ return format!("{id} {name}");
+ }
+
+ if val > u8::MAX as usize {
+ format!("{id} ${:04X}", val)
+ } else {
+ format!("{id} ${:02X}", val)
+ }
+ }
+ AddressingMode::IndexedX => format!("{} ${:04X}, x", id, self.value()),
+ AddressingMode::IndexedY => format!("{} ${:04X}, y", id, self.value()),
+ AddressingMode::ZeropageIndexedX => format!("{} ${:02X}, x", id, self.value()),
+ AddressingMode::ZeropageIndexedY => format!("{} ${:02X}, y", id, self.value()),
+ AddressingMode::Indirect => format!("{} (${:02X})", id, self.value()),
+ AddressingMode::IndirectX => format!("{} (${:02X}, x)", id, self.value()),
+ AddressingMode::IndirectY => format!("{} (${:02X}), y", id, self.value()),
+ AddressingMode::Implied => id.to_string(),
+ }
+ }
+}
+
/// The representation of a Control statement/expression.
#[derive(Debug)]
pub struct Control {
@@ -2062,3 +2321,1924 @@ pub static CONTROL_FUNCTIONS: LazyLock<HashMap<String, Control>> = LazyLock::new
functions
});
+
+/// For a given byte opcode, return the Instruction associated with it.
+pub static OPCODES: LazyLock<HashMap<u8, Instruction>> = LazyLock::new(|| {
+ let mut ops = HashMap::new();
+
+ // adc
+ ops.insert(
+ 0x69,
+ Instruction {
+ identifier: InstructionIdentifier::Adc,
+ addressing_mode: AddressingMode::Immediate,
+ cycles: 2,
+ opcode: 0x69,
+ size: 2,
+ affected_on_page: false,
+ bytes: [0, 0],
+ },
+ );
+ ops.insert(
+ 0x65,
+ Instruction {
+ identifier: InstructionIdentifier::Adc,
+ addressing_mode: AddressingMode::RelativeOrZeropage,
+ cycles: 3,
+ opcode: 0x65,
+ size: 2,
+ affected_on_page: false,
+ bytes: [0, 0],
+ },
+ );
+ ops.insert(
+ 0x75,
+ Instruction {
+ identifier: InstructionIdentifier::Adc,
+ addressing_mode: AddressingMode::ZeropageIndexedX,
+ cycles: 4,
+ opcode: 0x75,
+ size: 2,
+ affected_on_page: false,
+ bytes: [0, 0],
+ },
+ );
+ ops.insert(
+ 0x6D,
+ Instruction {
+ identifier: InstructionIdentifier::Adc,
+ addressing_mode: AddressingMode::Absolute,
+ cycles: 4,
+ opcode: 0x6D,
+ size: 3,
+ affected_on_page: false,
+ bytes: [0, 0],
+ },
+ );
+ ops.insert(
+ 0x7D,
+ Instruction {
+ identifier: InstructionIdentifier::Adc,
+ addressing_mode: AddressingMode::IndexedX,
+ cycles: 4,
+ opcode: 0x7D,
+ size: 3,
+ affected_on_page: true,
+ bytes: [0, 0],
+ },
+ );
+ ops.insert(
+ 0x79,
+ Instruction {
+ identifier: InstructionIdentifier::Adc,
+ addressing_mode: AddressingMode::IndexedY,
+ cycles: 4,
+ opcode: 0x79,
+ size: 3,
+ affected_on_page: true,
+ bytes: [0, 0],
+ },
+ );
+ ops.insert(
+ 0x61,
+ Instruction {
+ identifier: InstructionIdentifier::Adc,
+ addressing_mode: AddressingMode::IndirectX,
+ cycles: 6,
+ opcode: 0x61,
+ size: 2,
+ affected_on_page: false,
+ bytes: [0, 0],
+ },
+ );
+ ops.insert(
+ 0x71,
+ Instruction {
+ identifier: InstructionIdentifier::Adc,
+ addressing_mode: AddressingMode::IndirectY,
+ cycles: 5,
+ opcode: 0x71,
+ size: 2,
+ affected_on_page: true,
+ bytes: [0, 0],
+ },
+ );
+
+ // and
+ ops.insert(
+ 0x29,
+ Instruction {
+ identifier: InstructionIdentifier::And,
+ addressing_mode: AddressingMode::Immediate,
+ cycles: 2,
+ opcode: 0x29,
+ size: 2,
+ affected_on_page: false,
+ bytes: [0, 0],
+ },
+ );
+ ops.insert(
+ 0x25,
+ Instruction {
+ identifier: InstructionIdentifier::And,
+ addressing_mode: AddressingMode::RelativeOrZeropage,
+ cycles: 3,
+ opcode: 0x25,
+ size: 2,
+ affected_on_page: false,
+ bytes: [0, 0],
+ },
+ );
+ ops.insert(
+ 0x35,
+ Instruction {
+ identifier: InstructionIdentifier::And,
+ addressing_mode: AddressingMode::ZeropageIndexedX,
+ cycles: 4,
+ opcode: 0x35,
+ size: 2,
+ affected_on_page: false,
+ bytes: [0, 0],
+ },
+ );
+ ops.insert(
+ 0x2D,
+ Instruction {
+ identifier: InstructionIdentifier::And,
+ addressing_mode: AddressingMode::Absolute,
+ cycles: 4,
+ opcode: 0x2D,
+ size: 3,
+ affected_on_page: false,
+ bytes: [0, 0],
+ },
+ );
+ ops.insert(
+ 0x3D,
+ Instruction {
+ identifier: InstructionIdentifier::And,
+ addressing_mode: AddressingMode::IndexedX,
+ cycles: 4,
+ opcode: 0x3D,
+ size: 3,
+ affected_on_page: true,
+ bytes: [0, 0],
+ },
+ );
+ ops.insert(
+ 0x39,
+ Instruction {
+ identifier: InstructionIdentifier::And,
+ addressing_mode: AddressingMode::IndexedY,
+ cycles: 4,
+ opcode: 0x39,
+ size: 3,
+ affected_on_page: true,
+ bytes: [0, 0],
+ },
+ );
+ ops.insert(
+ 0x21,
+ Instruction {
+ identifier: InstructionIdentifier::And,
+ addressing_mode: AddressingMode::IndirectX,
+ cycles: 6,
+ opcode: 0x21,
+ size: 2,
+ affected_on_page: false,
+ bytes: [0, 0],
+ },
+ );
+ ops.insert(
+ 0x31,
+ Instruction {
+ identifier: InstructionIdentifier::And,
+ addressing_mode: AddressingMode::IndirectY,
+ cycles: 5,
+ opcode: 0x31,
+ size: 2,
+ affected_on_page: true,
+ bytes: [0, 0],
+ },
+ );
+
+ // asl
+ ops.insert(
+ 0x0A,
+ Instruction {
+ identifier: InstructionIdentifier::Asl,
+ addressing_mode: AddressingMode::Implied,
+ cycles: 2,
+ opcode: 0x0A,
+ size: 1,
+ affected_on_page: false,
+ bytes: [0, 0],
+ },
+ );
+ ops.insert(
+ 0x06,
+ Instruction {
+ identifier: InstructionIdentifier::Asl,
+ addressing_mode: AddressingMode::RelativeOrZeropage,
+ cycles: 5,
+ opcode: 0x06,
+ size: 2,
+ affected_on_page: false,
+ bytes: [0, 0],
+ },
+ );
+ ops.insert(
+ 0x16,
+ Instruction {
+ identifier: InstructionIdentifier::Asl,
+ addressing_mode: AddressingMode::ZeropageIndexedX,
+ cycles: 6,
+ opcode: 0x16,
+ size: 2,
+ affected_on_page: false,
+ bytes: [0, 0],
+ },
+ );
+ ops.insert(
+ 0x0E,
+ Instruction {
+ identifier: InstructionIdentifier::Asl,
+ addressing_mode: AddressingMode::Absolute,
+ cycles: 6,
+ opcode: 0x0E,
+ size: 3,
+ affected_on_page: false,
+ bytes: [0, 0],
+ },
+ );
+
+ ops.insert(
+ 0x1E,
+ Instruction {
+ identifier: InstructionIdentifier::Asl,
+ addressing_mode: AddressingMode::IndexedX,
+ cycles: 7,
+ opcode: 0x1E,
+ size: 3,
+ affected_on_page: false,
+ bytes: [0, 0],
+ },
+ );
+
+ // bit
+ ops.insert(
+ 0x24,
+ Instruction {
+ identifier: InstructionIdentifier::Bit,
+ addressing_mode: AddressingMode::RelativeOrZeropage,
+ cycles: 3,
+ opcode: 0x24,
+ size: 2,
+ affected_on_page: false,
+ bytes: [0, 0],
+ },
+ );
+ ops.insert(
+ 0x2C,
+ Instruction {
+ identifier: InstructionIdentifier::Bit,
+ addressing_mode: AddressingMode::Absolute,
+ cycles: 4,
+ opcode: 0x2C,
+ size: 3,
+ affected_on_page: false,
+ bytes: [0, 0],
+ },
+ );
+
+ // bcc
+ ops.insert(
+ 0x90,
+ Instruction {
+ identifier: InstructionIdentifier::Bcc,
+ addressing_mode: AddressingMode::RelativeOrZeropage,
+ cycles: 2,
+ opcode: 0x90,
+ size: 2,
+ affected_on_page: true,
+ bytes: [0, 0],
+ },
+ );
+
+ // bcs
+ ops.insert(
+ 0xB0,
+ Instruction {
+ identifier: InstructionIdentifier::Bcs,
+ addressing_mode: AddressingMode::RelativeOrZeropage,
+ cycles: 2,
+ opcode: 0xB0,
+ size: 2,
+ affected_on_page: true,
+ bytes: [0, 0],
+ },
+ );
+
+ // beq
+ ops.insert(
+ 0xF0,
+ Instruction {
+ identifier: InstructionIdentifier::Beq,
+ addressing_mode: AddressingMode::RelativeOrZeropage,
+ cycles: 2,
+ opcode: 0xF0,
+ size: 2,
+ affected_on_page: true,
+ bytes: [0, 0],
+ },
+ );
+
+ // bne
+ ops.insert(
+ 0xD0,
+ Instruction {
+ identifier: InstructionIdentifier::Bne,
+ addressing_mode: AddressingMode::RelativeOrZeropage,
+ cycles: 2,
+ opcode: 0xD0,
+ size: 2,
+ affected_on_page: true,
+ bytes: [0, 0],
+ },
+ );
+
+ // bpl
+ ops.insert(
+ 0x10,
+ Instruction {
+ identifier: InstructionIdentifier::Bpl,
+ addressing_mode: AddressingMode::RelativeOrZeropage,
+ cycles: 2,
+ opcode: 0x10,
+ size: 2,
+ affected_on_page: true,
+ bytes: [0, 0],
+ },
+ );
+
+ // bmi
+ ops.insert(
+ 0x30,
+ Instruction {
+ identifier: InstructionIdentifier::Bmi,
+ addressing_mode: AddressingMode::RelativeOrZeropage,
+ cycles: 2,
+ opcode: 0x30,
+ size: 2,
+ affected_on_page: true,
+ bytes: [0, 0],
+ },
+ );
+
+ // brk
+ ops.insert(
+ 0x00,
+ Instruction {
+ identifier: InstructionIdentifier::Brk,
+ addressing_mode: AddressingMode::Implied,
+ cycles: 7,
+ size: 1,
+ opcode: 0x00,
+ affected_on_page: false,
+ bytes: [0, 0],
+ },
+ );
+
+ // bvs
+ ops.insert(
+ 0x70,
+ Instruction {
+ identifier: InstructionIdentifier::Bvs,
+ addressing_mode: AddressingMode::RelativeOrZeropage,
+ cycles: 2,
+ opcode: 0x70,
+ size: 2,
+ affected_on_page: true,
+ bytes: [0, 0],
+ },
+ );
+
+ // bvc
+ ops.insert(
+ 0x50,
+ Instruction {
+ identifier: InstructionIdentifier::Bvc,
+ addressing_mode: AddressingMode::RelativeOrZeropage,
+ cycles: 2,
+ opcode: 0x50,
+ size: 2,
+ affected_on_page: true,
+ bytes: [0, 0],
+ },
+ );
+
+ // clc
+ ops.insert(
+ 0x18,
+ Instruction {
+ identifier: InstructionIdentifier::Clc,
+ addressing_mode: AddressingMode::Implied,
+ cycles: 2,
+ opcode: 0x18,
+ size: 1,
+ affected_on_page: false,
+ bytes: [0, 0],
+ },
+ );
+
+ // cld
+ ops.insert(
+ 0xD8,
+ Instruction {
+ identifier: InstructionIdentifier::Cld,
+ addressing_mode: AddressingMode::Implied,
+ cycles: 2,
+ opcode: 0xD8,
+ size: 1,
+ affected_on_page: false,
+ bytes: [0, 0],
+ },
+ );
+
+ // cli
+ ops.insert(
+ 0x58,
+ Instruction {
+ identifier: InstructionIdentifier::Cli,
+ addressing_mode: AddressingMode::Implied,
+ cycles: 2,
+ opcode: 0x58,
+ size: 1,
+ affected_on_page: false,
+ bytes: [0, 0],
+ },
+ );
+
+ // clv
+ ops.insert(
+ 0xB8,
+ Instruction {
+ identifier: InstructionIdentifier::Clv,
+ addressing_mode: AddressingMode::Implied,
+ cycles: 2,
+ opcode: 0xB8,
+ size: 1,
+ affected_on_page: false,
+ bytes: [0, 0],
+ },
+ );
+
+ // cmp
+ ops.insert(
+ 0xC9,
+ Instruction {
+ identifier: InstructionIdentifier::Cmp,
+ addressing_mode: AddressingMode::Immediate,
+ cycles: 2,
+ opcode: 0xC9,
+ size: 2,
+ affected_on_page: false,
+ bytes: [0, 0],
+ },
+ );
+ ops.insert(
+ 0xC5,
+ Instruction {
+ identifier: InstructionIdentifier::Cmp,
+ addressing_mode: AddressingMode::RelativeOrZeropage,
+ cycles: 3,
+ opcode: 0xC5,
+ size: 2,
+ affected_on_page: false,
+ bytes: [0, 0],
+ },
+ );
+ ops.insert(
+ 0xD5,
+ Instruction {
+ identifier: InstructionIdentifier::Cmp,
+ addressing_mode: AddressingMode::ZeropageIndexedX,
+ cycles: 4,
+ opcode: 0xD5,
+ size: 2,
+ affected_on_page: false,
+ bytes: [0, 0],
+ },
+ );
+ ops.insert(
+ 0xCD,
+ Instruction {
+ identifier: InstructionIdentifier::Cmp,
+ addressing_mode: AddressingMode::Absolute,
+ cycles: 4,
+ opcode: 0xCD,
+ size: 3,
+ affected_on_page: false,
+ bytes: [0, 0],
+ },
+ );
+ ops.insert(
+ 0xDD,
+ Instruction {
+ identifier: InstructionIdentifier::Cmp,
+ addressing_mode: AddressingMode::IndexedX,
+ cycles: 4,
+ opcode: 0xDD,
+ size: 3,
+ affected_on_page: false,
+ bytes: [0, 0],
+ },
+ );
+ ops.insert(
+ 0xD9,
+ Instruction {
+ identifier: InstructionIdentifier::Cmp,
+ addressing_mode: AddressingMode::IndexedY,
+ cycles: 4,
+ opcode: 0xD9,
+ size: 3,
+ affected_on_page: true,
+ bytes: [0, 0],
+ },
+ );
+ ops.insert(
+ 0xC1,
+ Instruction {
+ identifier: InstructionIdentifier::Cmp,
+ addressing_mode: AddressingMode::IndirectX,
+ cycles: 6,
+ opcode: 0xC1,
+ size: 2,
+ affected_on_page: false,
+ bytes: [0, 0],
+ },
+ );
+ ops.insert(
+ 0xD1,
+ Instruction {
+ identifier: InstructionIdentifier::Cmp,
+ addressing_mode: AddressingMode::IndirectY,
+ cycles: 5,
+ opcode: 0xD1,
+ size: 2,
+ affected_on_page: true,
+ bytes: [0, 0],
+ },
+ );
+
+ // cpx
+ ops.insert(
+ 0xE0,
+ Instruction {
+ identifier: InstructionIdentifier::Cpx,
+ addressing_mode: AddressingMode::Immediate,
+ cycles: 2,
+ opcode: 0xE0,
+ size: 2,
+ affected_on_page: false,
+ bytes: [0, 0],
+ },
+ );
+ ops.insert(
+ 0xE4,
+ Instruction {
+ identifier: InstructionIdentifier::Cpx,
+ addressing_mode: AddressingMode::RelativeOrZeropage,
+ cycles: 3,
+ opcode: 0xE4,
+ size: 2,
+ affected_on_page: false,
+ bytes: [0, 0],
+ },
+ );
+ ops.insert(
+ 0xEC,
+ Instruction {
+ identifier: InstructionIdentifier::Cpx,
+ addressing_mode: AddressingMode::Absolute,
+ cycles: 4,
+ opcode: 0xEC,
+ size: 3,
+ affected_on_page: false,
+ bytes: [0, 0],
+ },
+ );
+
+ // cpy
+ ops.insert(
+ 0xC0,
+ Instruction {
+ identifier: InstructionIdentifier::Cpy,
+ addressing_mode: AddressingMode::Immediate,
+ cycles: 2,
+ opcode: 0xC0,
+ size: 2,
+ affected_on_page: false,
+ bytes: [0, 0],
+ },
+ );
+ ops.insert(
+ 0xC4,
+ Instruction {
+ identifier: InstructionIdentifier::Cpy,
+ addressing_mode: AddressingMode::RelativeOrZeropage,
+ cycles: 3,
+ opcode: 0xC4,
+ size: 2,
+ affected_on_page: false,
+ bytes: [0, 0],
+ },
+ );
+ ops.insert(
+ 0xCC,
+ Instruction {
+ identifier: InstructionIdentifier::Cpy,
+ addressing_mode: AddressingMode::Absolute,
+ cycles: 4,
+ opcode: 0xCC,
+ size: 3,
+ affected_on_page: false,
+ bytes: [0, 0],
+ },
+ );
+
+ // dec
+ ops.insert(
+ 0xC6,
+ Instruction {
+ identifier: InstructionIdentifier::Dec,
+ addressing_mode: AddressingMode::RelativeOrZeropage,
+ cycles: 5,
+ opcode: 0xC6,
+ size: 2,
+ affected_on_page: false,
+ bytes: [0, 0],
+ },
+ );
+ ops.insert(
+ 0xD6,
+ Instruction {
+ identifier: InstructionIdentifier::Dec,
+ addressing_mode: AddressingMode::ZeropageIndexedX,
+ cycles: 6,
+ opcode: 0xD6,
+ size: 2,
+ affected_on_page: false,
+ bytes: [0, 0],
+ },
+ );
+ ops.insert(
+ 0xCE,
+ Instruction {
+ identifier: InstructionIdentifier::Dec,
+ addressing_mode: AddressingMode::Absolute,
+ cycles: 6,
+ opcode: 0xCE,
+ size: 3,
+ affected_on_page: false,
+ bytes: [0, 0],
+ },
+ );
+ ops.insert(
+ 0xDE,
+ Instruction {
+ identifier: InstructionIdentifier::Dec,
+ addressing_mode: AddressingMode::IndexedX,
+ cycles: 7,
+ opcode: 0xDE,
+ size: 3,
+ affected_on_page: false,
+ bytes: [0, 0],
+ },
+ );
+
+ // dex
+ ops.insert(
+ 0xCA,
+ Instruction {
+ identifier: InstructionIdentifier::Dex,
+ addressing_mode: AddressingMode::Implied,
+ cycles: 2,
+ opcode: 0xCA,
+ size: 1,
+ affected_on_page: false,
+ bytes: [0, 0],
+ },
+ );
+
+ // dey
+ ops.insert(
+ 0x88,
+ Instruction {
+ identifier: InstructionIdentifier::Dey,
+ addressing_mode: AddressingMode::Implied,
+ cycles: 2,
+ opcode: 0x88,
+ size: 1,
+ affected_on_page: false,
+ bytes: [0, 0],
+ },
+ );
+
+ // eor
+ ops.insert(
+ 0x49,
+ Instruction {
+ identifier: InstructionIdentifier::Eor,
+ addressing_mode: AddressingMode::Immediate,
+ cycles: 2,
+ opcode: 0x49,
+ size: 2,
+ affected_on_page: false,
+ bytes: [0, 0],
+ },
+ );
+ ops.insert(
+ 0x45,
+ Instruction {
+ identifier: InstructionIdentifier::Eor,
+ addressing_mode: AddressingMode::RelativeOrZeropage,
+ cycles: 3,
+ opcode: 0x45,
+ size: 2,
+ affected_on_page: false,
+ bytes: [0, 0],
+ },
+ );
+ ops.insert(
+ 0x55,
+ Instruction {
+ identifier: InstructionIdentifier::Eor,
+ addressing_mode: AddressingMode::ZeropageIndexedX,
+ cycles: 4,
+ opcode: 0x55,
+ size: 2,
+ affected_on_page: false,
+ bytes: [0, 0],
+ },
+ );
+ ops.insert(
+ 0x4D,
+ Instruction {
+ identifier: InstructionIdentifier::Eor,
+ addressing_mode: AddressingMode::Absolute,
+ cycles: 4,
+ opcode: 0x4D,
+ size: 3,
+ affected_on_page: false,
+ bytes: [0, 0],
+ },
+ );
+ ops.insert(
+ 0x5D,
+ Instruction {
+ identifier: InstructionIdentifier::Eor,
+ addressing_mode: AddressingMode::IndexedX,
+ cycles: 4,
+ opcode: 0x5D,
+ size: 3,
+ affected_on_page: true,
+ bytes: [0, 0],
+ },
+ );
+ ops.insert(
+ 0x59,
+ Instruction {
+ identifier: InstructionIdentifier::Eor,
+ addressing_mode: AddressingMode::IndexedY,
+ cycles: 4,
+ opcode: 0x59,
+ size: 3,
+ affected_on_page: true,
+ bytes: [0, 0],
+ },
+ );
+ ops.insert(
+ 0x41,
+ Instruction {
+ identifier: InstructionIdentifier::Eor,
+ addressing_mode: AddressingMode::IndirectX,
+ cycles: 6,
+ opcode: 0x41,
+ size: 2,
+ affected_on_page: false,
+ bytes: [0, 0],
+ },
+ );
+ ops.insert(
+ 0x51,
+ Instruction {
+ identifier: InstructionIdentifier::Eor,
+ addressing_mode: AddressingMode::IndirectY,
+ cycles: 5,
+ opcode: 0x51,
+ size: 2,
+ affected_on_page: true,
+ bytes: [0, 0],
+ },
+ );
+
+ // inc
+ ops.insert(
+ 0xE6,
+ Instruction {
+ identifier: InstructionIdentifier::Inc,
+ addressing_mode: AddressingMode::RelativeOrZeropage,
+ cycles: 5,
+ opcode: 0xE6,
+ size: 2,
+ affected_on_page: false,
+ bytes: [0, 0],
+ },
+ );
+ ops.insert(
+ 0xF6,
+ Instruction {
+ identifier: InstructionIdentifier::Inc,
+ addressing_mode: AddressingMode::ZeropageIndexedX,
+ cycles: 6,
+ opcode: 0xF6,
+ size: 2,
+ affected_on_page: false,
+ bytes: [0, 0],
+ },
+ );
+ ops.insert(
+ 0xEE,
+ Instruction {
+ identifier: InstructionIdentifier::Inc,
+ addressing_mode: AddressingMode::Absolute,
+ cycles: 6,
+ opcode: 0xEE,
+ size: 3,
+ affected_on_page: false,
+ bytes: [0, 0],
+ },
+ );
+ ops.insert(
+ 0xFE,
+ Instruction {
+ identifier: InstructionIdentifier::Inc,
+ addressing_mode: AddressingMode::IndexedX,
+ cycles: 7,
+ opcode: 0xFE,
+ size: 3,
+ affected_on_page: false,
+ bytes: [0, 0],
+ },
+ );
+
+ // inx
+ ops.insert(
+ 0xE8,
+ Instruction {
+ identifier: InstructionIdentifier::Inx,
+ addressing_mode: AddressingMode::Implied,
+ cycles: 2,
+ opcode: 0xE8,
+ size: 1,
+ affected_on_page: false,
+ bytes: [0, 0],
+ },
+ );
+
+ // iny
+ ops.insert(
+ 0xC8,
+ Instruction {
+ identifier: InstructionIdentifier::Iny,
+ addressing_mode: AddressingMode::Implied,
+ cycles: 2,
+ opcode: 0xC8,
+ size: 1,
+ affected_on_page: false,
+ bytes: [0, 0],
+ },
+ );
+
+ // jmp
+ ops.insert(
+ 0x4C,
+ Instruction {
+ identifier: InstructionIdentifier::Jmp,
+ addressing_mode: AddressingMode::Absolute,
+ cycles: 3,
+ opcode: 0x4C,
+ size: 3,
+ affected_on_page: false,
+ bytes: [0, 0],
+ },
+ );
+
+ // jsr
+ ops.insert(
+ 0x20,
+ Instruction {
+ identifier: InstructionIdentifier::Jsr,
+ addressing_mode: AddressingMode::Absolute,
+ cycles: 6,
+ opcode: 0x20,
+ size: 3,
+ affected_on_page: false,
+ bytes: [0, 0],
+ },
+ );
+
+ // lda
+ ops.insert(
+ 0xA9,
+ Instruction {
+ identifier: InstructionIdentifier::Lda,
+ addressing_mode: AddressingMode::Immediate,
+ cycles: 2,
+ opcode: 0xA9,
+ size: 2,
+ affected_on_page: false,
+ bytes: [0, 0],
+ },
+ );
+ ops.insert(
+ 0xA5,
+ Instruction {
+ identifier: InstructionIdentifier::Lda,
+ addressing_mode: AddressingMode::RelativeOrZeropage,
+ cycles: 3,
+ opcode: 0xA5,
+ size: 2,
+ affected_on_page: false,
+ bytes: [0, 0],
+ },
+ );
+ ops.insert(
+ 0xB5,
+ Instruction {
+ identifier: InstructionIdentifier::Lda,
+ addressing_mode: AddressingMode::ZeropageIndexedX,
+ cycles: 4,
+ opcode: 0xB5,
+ size: 2,
+ affected_on_page: false,
+ bytes: [0, 0],
+ },
+ );
+ ops.insert(
+ 0xAD,
+ Instruction {
+ identifier: InstructionIdentifier::Lda,
+ addressing_mode: AddressingMode::Absolute,
+ cycles: 4,
+ opcode: 0xAD,
+ size: 3,
+ affected_on_page: false,
+ bytes: [0, 0],
+ },
+ );
+ ops.insert(
+ 0xBD,
+ Instruction {
+ identifier: InstructionIdentifier::Lda,
+ addressing_mode: AddressingMode::IndexedX,
+ cycles: 4,
+ opcode: 0xBD,
+ size: 3,
+ affected_on_page: true,
+ bytes: [0, 0],
+ },
+ );
+ ops.insert(
+ 0xB9,
+ Instruction {
+ identifier: InstructionIdentifier::Lda,
+ addressing_mode: AddressingMode::IndexedY,
+ cycles: 4,
+ opcode: 0xB9,
+ size: 3,
+ affected_on_page: true,
+ bytes: [0, 0],
+ },
+ );
+ ops.insert(
+ 0xA1,
+ Instruction {
+ identifier: InstructionIdentifier::Lda,
+ addressing_mode: AddressingMode::IndirectX,
+ cycles: 6,
+ opcode: 0xA1,
+ size: 2,
+ affected_on_page: false,
+ bytes: [0, 0],
+ },
+ );
+ ops.insert(
+ 0xB1,
+ Instruction {
+ identifier: InstructionIdentifier::Lda,
+ addressing_mode: AddressingMode::IndirectY,
+ cycles: 5,
+ opcode: 0xB1,
+ size: 2,
+ affected_on_page: true,
+ bytes: [0, 0],
+ },
+ );
+
+ // ldx
+ ops.insert(
+ 0xA2,
+ Instruction {
+ identifier: InstructionIdentifier::Ldx,
+ addressing_mode: AddressingMode::Immediate,
+ cycles: 2,
+ opcode: 0xA2,
+ size: 2,
+ affected_on_page: false,
+ bytes: [0, 0],
+ },
+ );
+ ops.insert(
+ 0xA6,
+ Instruction {
+ identifier: InstructionIdentifier::Ldx,
+ addressing_mode: AddressingMode::RelativeOrZeropage,
+ cycles: 3,
+ opcode: 0xA6,
+ size: 2,
+ affected_on_page: false,
+ bytes: [0, 0],
+ },
+ );
+ ops.insert(
+ 0xB6,
+ Instruction {
+ identifier: InstructionIdentifier::Ldx,
+ addressing_mode: AddressingMode::ZeropageIndexedY,
+ cycles: 4,
+ opcode: 0xB6,
+ size: 2,
+ affected_on_page: false,
+ bytes: [0, 0],
+ },
+ );
+ ops.insert(
+ 0xAE,
+ Instruction {
+ identifier: InstructionIdentifier::Ldx,
+ addressing_mode: AddressingMode::Absolute,
+ cycles: 4,
+ opcode: 0xAE,
+ size: 3,
+ affected_on_page: false,
+ bytes: [0, 0],
+ },
+ );
+ ops.insert(
+ 0xBE,
+ Instruction {
+ identifier: InstructionIdentifier::Ldx,
+ addressing_mode: AddressingMode::IndexedY,
+ cycles: 4,
+ opcode: 0xBE,
+ size: 3,
+ affected_on_page: true,
+ bytes: [0, 0],
+ },
+ );
+
+ // ldy
+ ops.insert(
+ 0xA0,
+ Instruction {
+ identifier: InstructionIdentifier::Ldy,
+ addressing_mode: AddressingMode::Immediate,
+ cycles: 2,
+ opcode: 0xA0,
+ size: 2,
+ affected_on_page: false,
+ bytes: [0, 0],
+ },
+ );
+ ops.insert(
+ 0xA4,
+ Instruction {
+ identifier: InstructionIdentifier::Ldy,
+ addressing_mode: AddressingMode::RelativeOrZeropage,
+ cycles: 3,
+ opcode: 0xA4,
+ size: 2,
+ affected_on_page: false,
+ bytes: [0, 0],
+ },
+ );
+ ops.insert(
+ 0xB4,
+ Instruction {
+ identifier: InstructionIdentifier::Ldy,
+ addressing_mode: AddressingMode::ZeropageIndexedX,
+ cycles: 4,
+ opcode: 0xB4,
+ size: 2,
+ affected_on_page: false,
+ bytes: [0, 0],
+ },
+ );
+ ops.insert(
+ 0xAC,
+ Instruction {
+ identifier: InstructionIdentifier::Ldy,
+ addressing_mode: AddressingMode::Absolute,
+ cycles: 4,
+ opcode: 0xAC,
+ size: 3,
+ affected_on_page: false,
+ bytes: [0, 0],
+ },
+ );
+ ops.insert(
+ 0xBC,
+ Instruction {
+ identifier: InstructionIdentifier::Ldy,
+ addressing_mode: AddressingMode::IndexedX,
+ cycles: 4,
+ opcode: 0xBC,
+ size: 3,
+ affected_on_page: true,
+ bytes: [0, 0],
+ },
+ );
+
+ // lsr
+ ops.insert(
+ 0x4A,
+ Instruction {
+ identifier: InstructionIdentifier::Lsr,
+ addressing_mode: AddressingMode::Implied,
+ cycles: 2,
+ opcode: 0x4A,
+ size: 1,
+ affected_on_page: false,
+ bytes: [0, 0],
+ },
+ );
+ ops.insert(
+ 0x46,
+ Instruction {
+ identifier: InstructionIdentifier::Lsr,
+ addressing_mode: AddressingMode::RelativeOrZeropage,
+ cycles: 5,
+ opcode: 0x46,
+ size: 2,
+ affected_on_page: false,
+ bytes: [0, 0],
+ },
+ );
+ ops.insert(
+ 0x56,
+ Instruction {
+ identifier: InstructionIdentifier::Lsr,
+ addressing_mode: AddressingMode::ZeropageIndexedX,
+ cycles: 6,
+ opcode: 0x56,
+ size: 2,
+ affected_on_page: false,
+ bytes: [0, 0],
+ },
+ );
+ ops.insert(
+ 0x4E,
+ Instruction {
+ identifier: InstructionIdentifier::Lsr,
+ addressing_mode: AddressingMode::Absolute,
+ cycles: 6,
+ opcode: 0x4E,
+ size: 3,
+ affected_on_page: false,
+ bytes: [0, 0],
+ },
+ );
+
+ ops.insert(
+ 0x5E,
+ Instruction {
+ identifier: InstructionIdentifier::Lsr,
+ addressing_mode: AddressingMode::IndexedX,
+ cycles: 7,
+ opcode: 0x5E,
+ size: 3,
+ affected_on_page: false,
+ bytes: [0, 0],
+ },
+ );
+
+ // nop
+ ops.insert(
+ 0xEA,
+ Instruction {
+ identifier: InstructionIdentifier::Nop,
+ addressing_mode: AddressingMode::Implied,
+ cycles: 2,
+ opcode: 0xEA,
+ size: 1,
+ affected_on_page: false,
+ bytes: [0, 0],
+ },
+ );
+
+ // ora
+ ops.insert(
+ 0x09,
+ Instruction {
+ identifier: InstructionIdentifier::Ora,
+ addressing_mode: AddressingMode::Immediate,
+ cycles: 2,
+ opcode: 0x09,
+ size: 2,
+ affected_on_page: false,
+ bytes: [0, 0],
+ },
+ );
+ ops.insert(
+ 0x05,
+ Instruction {
+ identifier: InstructionIdentifier::Ora,
+ addressing_mode: AddressingMode::RelativeOrZeropage,
+ cycles: 3,
+ opcode: 0x05,
+ size: 2,
+ affected_on_page: false,
+ bytes: [0, 0],
+ },
+ );
+ ops.insert(
+ 0x15,
+ Instruction {
+ identifier: InstructionIdentifier::Ora,
+ addressing_mode: AddressingMode::ZeropageIndexedX,
+ cycles: 4,
+ opcode: 0x15,
+ size: 2,
+ affected_on_page: false,
+ bytes: [0, 0],
+ },
+ );
+ ops.insert(
+ 0x0D,
+ Instruction {
+ identifier: InstructionIdentifier::Ora,
+ addressing_mode: AddressingMode::Absolute,
+ cycles: 4,
+ opcode: 0x0D,
+ size: 3,
+ affected_on_page: false,
+ bytes: [0, 0],
+ },
+ );
+ ops.insert(
+ 0x1D,
+ Instruction {
+ identifier: InstructionIdentifier::Ora,
+ addressing_mode: AddressingMode::IndexedX,
+ cycles: 4,
+ opcode: 0x1D,
+ size: 3,
+ affected_on_page: true,
+ bytes: [0, 0],
+ },
+ );
+ ops.insert(
+ 0x19,
+ Instruction {
+ identifier: InstructionIdentifier::Ora,
+ addressing_mode: AddressingMode::IndexedY,
+ cycles: 4,
+ opcode: 0x19,
+ size: 3,
+ affected_on_page: true,
+ bytes: [0, 0],
+ },
+ );
+ ops.insert(
+ 0x01,
+ Instruction {
+ identifier: InstructionIdentifier::Ora,
+ addressing_mode: AddressingMode::IndirectX,
+ cycles: 6,
+ opcode: 0x01,
+ size: 2,
+ affected_on_page: false,
+ bytes: [0, 0],
+ },
+ );
+ ops.insert(
+ 0x11,
+ Instruction {
+ identifier: InstructionIdentifier::Ora,
+ addressing_mode: AddressingMode::IndirectY,
+ cycles: 5,
+ opcode: 0x11,
+ size: 2,
+ affected_on_page: true,
+ bytes: [0, 0],
+ },
+ );
+
+ // pha
+ ops.insert(
+ 0x48,
+ Instruction {
+ identifier: InstructionIdentifier::Pha,
+ addressing_mode: AddressingMode::Implied,
+ cycles: 3,
+ size: 1,
+ opcode: 0x48,
+ affected_on_page: false,
+ bytes: [0, 0],
+ },
+ );
+
+ // php
+ ops.insert(
+ 0x08,
+ Instruction {
+ identifier: InstructionIdentifier::Php,
+ addressing_mode: AddressingMode::Implied,
+ cycles: 3,
+ size: 1,
+ opcode: 0x08,
+ affected_on_page: false,
+ bytes: [0, 0],
+ },
+ );
+
+ // pla
+ ops.insert(
+ 0x68,
+ Instruction {
+ identifier: InstructionIdentifier::Pla,
+ addressing_mode: AddressingMode::Implied,
+ cycles: 3,
+ size: 1,
+ opcode: 0x68,
+ affected_on_page: false,
+ bytes: [0, 0],
+ },
+ );
+
+ // plp
+ ops.insert(
+ 0x28,
+ Instruction {
+ identifier: InstructionIdentifier::Plp,
+ addressing_mode: AddressingMode::Implied,
+ cycles: 3,
+ size: 1,
+ opcode: 0x28,
+ affected_on_page: false,
+ bytes: [0, 0],
+ },
+ );
+
+ // rol
+ ops.insert(
+ 0x2A,
+ Instruction {
+ identifier: InstructionIdentifier::Rol,
+ addressing_mode: AddressingMode::Implied,
+ cycles: 2,
+ opcode: 0x2A,
+ size: 1,
+ affected_on_page: false,
+ bytes: [0, 0],
+ },
+ );
+ ops.insert(
+ 0x26,
+ Instruction {
+ identifier: InstructionIdentifier::Rol,
+ addressing_mode: AddressingMode::RelativeOrZeropage,
+ cycles: 5,
+ opcode: 0x26,
+ size: 2,
+ affected_on_page: false,
+ bytes: [0, 0],
+ },
+ );
+ ops.insert(
+ 0x36,
+ Instruction {
+ identifier: InstructionIdentifier::Rol,
+ addressing_mode: AddressingMode::ZeropageIndexedX,
+ cycles: 6,
+ opcode: 0x36,
+ size: 2,
+ affected_on_page: false,
+ bytes: [0, 0],
+ },
+ );
+ ops.insert(
+ 0x2E,
+ Instruction {
+ identifier: InstructionIdentifier::Rol,
+ addressing_mode: AddressingMode::Absolute,
+ cycles: 6,
+ opcode: 0x2E,
+ size: 3,
+ affected_on_page: false,
+ bytes: [0, 0],
+ },
+ );
+ ops.insert(
+ 0x3E,
+ Instruction {
+ identifier: InstructionIdentifier::Rol,
+ addressing_mode: AddressingMode::IndexedX,
+ cycles: 7,
+ opcode: 0x3E,
+ size: 3,
+ affected_on_page: false,
+ bytes: [0, 0],
+ },
+ );
+
+ // ror
+ ops.insert(
+ 0x6A,
+ Instruction {
+ identifier: InstructionIdentifier::Ror,
+ addressing_mode: AddressingMode::Implied,
+ cycles: 2,
+ opcode: 0x6A,
+ size: 1,
+ affected_on_page: false,
+ bytes: [0, 0],
+ },
+ );
+ ops.insert(
+ 0x66,
+ Instruction {
+ identifier: InstructionIdentifier::Ror,
+ addressing_mode: AddressingMode::RelativeOrZeropage,
+ cycles: 5,
+ opcode: 0x66,
+ size: 2,
+ affected_on_page: false,
+ bytes: [0, 0],
+ },
+ );
+ ops.insert(
+ 0x76,
+ Instruction {
+ identifier: InstructionIdentifier::Ror,
+ addressing_mode: AddressingMode::ZeropageIndexedX,
+ cycles: 6,
+ opcode: 0x76,
+ size: 2,
+ affected_on_page: false,
+ bytes: [0, 0],
+ },
+ );
+ ops.insert(
+ 0x6E,
+ Instruction {
+ identifier: InstructionIdentifier::Ror,
+ addressing_mode: AddressingMode::Absolute,
+ cycles: 6,
+ opcode: 0x6E,
+ size: 3,
+ affected_on_page: false,
+ bytes: [0, 0],
+ },
+ );
+ ops.insert(
+ 0x7E,
+ Instruction {
+ identifier: InstructionIdentifier::Ror,
+ addressing_mode: AddressingMode::IndexedX,
+ cycles: 7,
+ opcode: 0x7E,
+ size: 3,
+ affected_on_page: false,
+ bytes: [0, 0],
+ },
+ );
+
+ // rti
+ ops.insert(
+ 0x40,
+ Instruction {
+ identifier: InstructionIdentifier::Rti,
+ addressing_mode: AddressingMode::Implied,
+ cycles: 6,
+ opcode: 0x40,
+ size: 1,
+ affected_on_page: false,
+ bytes: [0, 0],
+ },
+ );
+
+ // rts
+ ops.insert(
+ 0x60,
+ Instruction {
+ identifier: InstructionIdentifier::Rts,
+ addressing_mode: AddressingMode::Implied,
+ cycles: 6,
+ opcode: 0x60,
+ size: 1,
+ affected_on_page: false,
+ bytes: [0, 0],
+ },
+ );
+
+ // sbc
+ ops.insert(
+ 0xE9,
+ Instruction {
+ identifier: InstructionIdentifier::Sbc,
+ addressing_mode: AddressingMode::Immediate,
+ cycles: 2,
+ opcode: 0xE9,
+ size: 2,
+ affected_on_page: false,
+ bytes: [0, 0],
+ },
+ );
+ ops.insert(
+ 0xE5,
+ Instruction {
+ identifier: InstructionIdentifier::Sbc,
+ addressing_mode: AddressingMode::RelativeOrZeropage,
+ cycles: 3,
+ opcode: 0xE5,
+ size: 2,
+ affected_on_page: false,
+ bytes: [0, 0],
+ },
+ );
+ ops.insert(
+ 0xF5,
+ Instruction {
+ identifier: InstructionIdentifier::Sbc,
+ addressing_mode: AddressingMode::ZeropageIndexedX,
+ cycles: 4,
+ opcode: 0xF5,
+ size: 2,
+ affected_on_page: false,
+ bytes: [0, 0],
+ },
+ );
+ ops.insert(
+ 0xED,
+ Instruction {
+ identifier: InstructionIdentifier::Sbc,
+ addressing_mode: AddressingMode::Absolute,
+ cycles: 4,
+ opcode: 0xED,
+ size: 3,
+ affected_on_page: false,
+ bytes: [0, 0],
+ },
+ );
+ ops.insert(
+ 0xFD,
+ Instruction {
+ identifier: InstructionIdentifier::Sbc,
+ addressing_mode: AddressingMode::IndexedX,
+ cycles: 4,
+ opcode: 0xFD,
+ size: 3,
+ affected_on_page: true,
+ bytes: [0, 0],
+ },
+ );
+ ops.insert(
+ 0xF9,
+ Instruction {
+ identifier: InstructionIdentifier::Sbc,
+ addressing_mode: AddressingMode::IndexedY,
+ cycles: 4,
+ opcode: 0xF9,
+ size: 3,
+ affected_on_page: true,
+ bytes: [0, 0],
+ },
+ );
+ ops.insert(
+ 0xE1,
+ Instruction {
+ identifier: InstructionIdentifier::Sbc,
+ addressing_mode: AddressingMode::IndirectX,
+ cycles: 6,
+ opcode: 0xE1,
+ size: 2,
+ affected_on_page: false,
+ bytes: [0, 0],
+ },
+ );
+ ops.insert(
+ 0xF1,
+ Instruction {
+ identifier: InstructionIdentifier::Sbc,
+ addressing_mode: AddressingMode::IndirectY,
+ cycles: 5,
+ opcode: 0xF1,
+ size: 2,
+ affected_on_page: true,
+ bytes: [0, 0],
+ },
+ );
+
+ // sec
+ ops.insert(
+ 0x38,
+ Instruction {
+ identifier: InstructionIdentifier::Sec,
+ addressing_mode: AddressingMode::Implied,
+ cycles: 2,
+ opcode: 0x38,
+ size: 1,
+ affected_on_page: false,
+ bytes: [0, 0],
+ },
+ );
+
+ // sed
+ ops.insert(
+ 0x38,
+ Instruction {
+ identifier: InstructionIdentifier::Sed,
+ addressing_mode: AddressingMode::Implied,
+ cycles: 2,
+ size: 1,
+ opcode: 0x38,
+ affected_on_page: false,
+ bytes: [0, 0],
+ },
+ );
+
+ // sei
+ ops.insert(
+ 0x78,
+ Instruction {
+ identifier: InstructionIdentifier::Sei,
+ addressing_mode: AddressingMode::Implied,
+ cycles: 2,
+ opcode: 0x78,
+ size: 1,
+ affected_on_page: false,
+ bytes: [0, 0],
+ },
+ );
+
+ // sta
+ ops.insert(
+ 0x85,
+ Instruction {
+ identifier: InstructionIdentifier::Sta,
+ addressing_mode: AddressingMode::RelativeOrZeropage,
+ cycles: 3,
+ opcode: 0x85,
+ size: 2,
+ affected_on_page: false,
+ bytes: [0, 0],
+ },
+ );
+ ops.insert(
+ 0x95,
+ Instruction {
+ identifier: InstructionIdentifier::Sta,
+ addressing_mode: AddressingMode::ZeropageIndexedX,
+ cycles: 4,
+ opcode: 0x95,
+ size: 2,
+ affected_on_page: false,
+ bytes: [0, 0],
+ },
+ );
+ ops.insert(
+ 0x8D,
+ Instruction {
+ identifier: InstructionIdentifier::Sta,
+ addressing_mode: AddressingMode::Absolute,
+ cycles: 4,
+ opcode: 0x8D,
+ size: 3,
+ affected_on_page: false,
+ bytes: [0, 0],
+ },
+ );
+ ops.insert(
+ 0x9D,
+ Instruction {
+ identifier: InstructionIdentifier::Sta,
+ addressing_mode: AddressingMode::IndexedX,
+ cycles: 5,
+ opcode: 0x9D,
+ size: 3,
+ affected_on_page: false,
+ bytes: [0, 0],
+ },
+ );
+ ops.insert(
+ 0x99,
+ Instruction {
+ identifier: InstructionIdentifier::Sta,
+ addressing_mode: AddressingMode::IndexedY,
+ cycles: 5,
+ opcode: 0x99,
+ size: 3,
+ affected_on_page: false,
+ bytes: [0, 0],
+ },
+ );
+ ops.insert(
+ 0x81,
+ Instruction {
+ identifier: InstructionIdentifier::Sta,
+ addressing_mode: AddressingMode::IndirectX,
+ cycles: 6,
+ opcode: 0x81,
+ size: 2,
+ affected_on_page: false,
+ bytes: [0, 0],
+ },
+ );
+ ops.insert(
+ 0x91,
+ Instruction {
+ identifier: InstructionIdentifier::Sta,
+ addressing_mode: AddressingMode::IndirectY,
+ cycles: 6,
+ opcode: 0x91,
+ size: 2,
+ affected_on_page: false,
+ bytes: [0, 0],
+ },
+ );
+
+ // stx
+ ops.insert(
+ 0x86,
+ Instruction {
+ identifier: InstructionIdentifier::Stx,
+ addressing_mode: AddressingMode::RelativeOrZeropage,
+ cycles: 3,
+ opcode: 0x86,
+ size: 2,
+ affected_on_page: false,
+ bytes: [0, 0],
+ },
+ );
+ ops.insert(
+ 0x96,
+ Instruction {
+ identifier: InstructionIdentifier::Stx,
+ addressing_mode: AddressingMode::ZeropageIndexedY,
+ cycles: 4,
+ opcode: 0x96,
+ size: 2,
+ affected_on_page: false,
+ bytes: [0, 0],
+ },
+ );
+ ops.insert(
+ 0x8E,
+ Instruction {
+ identifier: InstructionIdentifier::Stx,
+ addressing_mode: AddressingMode::Absolute,
+ cycles: 4,
+ opcode: 0x8E,
+ size: 3,
+ affected_on_page: false,
+ bytes: [0, 0],
+ },
+ );
+
+ // sty
+ ops.insert(
+ 0x84,
+ Instruction {
+ identifier: InstructionIdentifier::Sty,
+ addressing_mode: AddressingMode::RelativeOrZeropage,
+ cycles: 3,
+ opcode: 0x84,
+ size: 2,
+ affected_on_page: false,
+ bytes: [0, 0],
+ },
+ );
+ ops.insert(
+ 0x94,
+ Instruction {
+ identifier: InstructionIdentifier::Sty,
+ addressing_mode: AddressingMode::ZeropageIndexedX,
+ cycles: 4,
+ opcode: 0x94,
+ size: 2,
+ affected_on_page: false,
+ bytes: [0, 0],
+ },
+ );
+ ops.insert(
+ 0x8C,
+ Instruction {
+ identifier: InstructionIdentifier::Sty,
+ addressing_mode: AddressingMode::Absolute,
+ cycles: 4,
+ opcode: 0x8C,
+ size: 3,
+ affected_on_page: false,
+ bytes: [0, 0],
+ },
+ );
+
+ // tax
+ ops.insert(
+ 0xAA,
+ Instruction {
+ identifier: InstructionIdentifier::Tax,
+ addressing_mode: AddressingMode::Implied,
+ cycles: 2,
+ opcode: 0xAA,
+ size: 1,
+ affected_on_page: false,
+ bytes: [0, 0],
+ },
+ );
+
+ // tay
+ ops.insert(
+ 0xA8,
+ Instruction {
+ identifier: InstructionIdentifier::Tay,
+ addressing_mode: AddressingMode::Implied,
+ cycles: 2,
+ opcode: 0xA8,
+ size: 1,
+ affected_on_page: false,
+ bytes: [0, 0],
+ },
+ );
+
+ // tsx
+ ops.insert(
+ 0xBA,
+ Instruction {
+ identifier: InstructionIdentifier::Tsx,
+ addressing_mode: AddressingMode::Implied,
+ cycles: 2,
+ opcode: 0xBA,
+ size: 1,
+ affected_on_page: false,
+ bytes: [0, 0],
+ },
+ );
+
+ // txa
+ ops.insert(
+ 0x8A,
+ Instruction {
+ identifier: InstructionIdentifier::Txa,
+ addressing_mode: AddressingMode::Implied,
+ cycles: 2,
+ opcode: 0x8A,
+ size: 1,
+ affected_on_page: false,
+ bytes: [0, 0],
+ },
+ );
+
+ // txs
+ ops.insert(
+ 0x9A,
+ Instruction {
+ identifier: InstructionIdentifier::Txs,
+ addressing_mode: AddressingMode::Implied,
+ cycles: 2,
+ opcode: 0x9A,
+ size: 1,
+ affected_on_page: false,
+ bytes: [0, 0],
+ },
+ );
+
+ // tya
+ ops.insert(
+ 0x98,
+ Instruction {
+ identifier: InstructionIdentifier::Tya,
+ addressing_mode: AddressingMode::Implied,
+ cycles: 2,
+ opcode: 0x98,
+ size: 1,
+ affected_on_page: false,
+ bytes: [0, 0],
+ },
+ );
+
+ ops
+});
diff --git a/scripts/test-e2e.sh b/scripts/test-e2e.sh
index 2a72d38..c32d427 100755
--- a/scripts/test-e2e.sh
+++ b/scripts/test-e2e.sh
@@ -236,13 +236,37 @@ echo "LEVEL = 0" >> tests/jetpac.nes/config/generated.s
# NOTE: we use '--stats' as otherwise the 'segments.txt' file would be removed
# from the jetpac.nes repository, and the submodule would then be marked with
# "-dirty".
-$AS65 --no-errors --allow-unused --strict --stats -b ./target/debug/nasm -C tests/jetpac.nes/config/nrom.cfg -o /dev/null tests/jetpac.nes/src/jetpac.s 1>/dev/null
+$AS65 --no-errors --allow-unused --strict --stats -b ./target/debug/nasm -C tests/jetpac.nes/config/nrom.cfg -o tests/out/jetpac.NTSC.nes tests/jetpac.nes/src/jetpac.s 1>/dev/null
# Remove the generated.s file to avoid git from thinking we have modified the
# project in any meaningful way.
rm -f tests/jetpac.nes/config/generated.s
##
+# readrom
+
+echo "test: readrom: header (jetpac.nes)"
+./target/debug/readrom tests/out/jetpac.NTSC.nes > tests/out/jetpac.header.txt
+diff tests/out/jetpac.header.txt tests/expected/readrom/header.txt
+exit_code=$((exit_code + $?))
+
+echo "test: readrom: hexadecimal address (jetpac.nes)"
+./target/debug/readrom -d '$a3b2' -n tests/jetpac.nes/.nasm/ tests/out/jetpac.NTSC.nes > tests/out/jetpac-a3b2-address.txt
+diff tests/out/jetpac-a3b2-address.txt tests/expected/readrom/a3b2-address.txt
+exit_code=$((exit_code + $?))
+
+echo "test: readrom: full NMI disassembling (jetpac.nes)"
+./target/debug/readrom -d nmi -n tests/jetpac.nes/.nasm/ tests/out/jetpac.NTSC.nes > tests/out/jetpac-full-nmi.txt
+diff tests/out/jetpac-full-nmi.txt tests/expected/readrom/jetpac-full-nmi.txt
+exit_code=$((exit_code + $?))
+
+# Remove the dangling jetpac.nes ROM file.
+rm tests/out/jetpac.NTSC.nes
+
+# This file wasn't available originally on jetpac.nes
+rm -f tests/jetpac.nes/.nasm/addresses.txt
+
+##
# Done!
exit $exit_code
diff --git a/tests/expected/readrom/a3b2-address.txt b/tests/expected/readrom/a3b2-address.txt
new file mode 100644
index 0000000..fd86b61
--- /dev/null
+++ b/tests/expected/readrom/a3b2-address.txt
@@ -0,0 +1 @@
+$A3B2: 20 1D 8B jsr Sound::tick
diff --git a/tests/expected/readrom/header.txt b/tests/expected/readrom/header.txt
new file mode 100644
index 0000000..a2d4a81
--- /dev/null
+++ b/tests/expected/readrom/header.txt
@@ -0,0 +1,11 @@
+Header:
+ Kind: NES 2.0
+ PRG ROM size: 32768 bytes (32KB)
+ CHR ROM size: 8192 bytes (8KB)
+ Mapper: NROM
+ Mirroring: Horizontal
+ CPU/PPU timing: NTSC
+Vectors:
+ NMI: 0xa2f8
+ Reset: 0xa3c2
+ IRQ: 0xa3c1
diff --git a/tests/expected/readrom/jetpac-full-nmi.txt b/tests/expected/readrom/jetpac-full-nmi.txt
new file mode 100644
index 0000000..45c9bce
--- /dev/null
+++ b/tests/expected/readrom/jetpac-full-nmi.txt
@@ -0,0 +1,123 @@
+$A2F8: 24 20 bit Globals::zp_flags
+$A2FA: 30 01 bmi @save_registers
+$A2FC: 40 rti
+
+ @save_registers:
+$A2FD: 48 pha
+$A2FE: 8A txa
+$A2FF: 48 pha
+$A300: 98 tya
+$A301: 48 pha
+$A302: A9 00 lda #$00
+$A304: 8D 03 20 sta OAM::m_address
+$A307: A9 02 lda #$02
+$A309: 8D 14 40 sta OAM::m_dma
+$A30C: 24 28 bit Globals::zp_extra_flags
+$A30E: 10 03 bpl @check_pause
+$A310: 20 EE 8C jsr Score::nmi_update_scores
+
+ @check_pause:
+$A313: 24 38 bit Driver::zp_flags
+$A315: 50 03 bvc @increase_rand
+$A317: 20 62 A2 jsr Driver::nmi_hud_toggle_pause
+
+ @increase_rand:
+$A31A: E6 0A inc Prng::zp_rand
+$A31C: A9 08 lda #$08
+$A31E: 25 20 and Globals::zp_flags
+$A320: D0 6E bne @ppu_registers
+$A322: 24 2E bit Driver::zp_blink_status
+$A324: 10 0B bpl @update_lifes
+$A326: A9 00 lda #$00
+$A328: 50 02 bvc @set_blinking
+$A32A: A9 70 lda #$70
+
+ @set_blinking:
+$A32C: 85 04 sta Globals::zp_nmi_reserved
+$A32E: 20 BC A2 jsr Driver::nmi_blink_player_selection
+
+ @update_lifes:
+$A331: A5 50 lda Player::zp_state
+$A333: 29 08 and #$08
+$A335: F0 34 beq @shuttle_update
+$A337: 2C 02 20 bit PPU::m_status
+$A33A: A9 28 lda #$28
+$A33C: 8D 06 20 sta PPU::m_address
+$A33F: A9 4B lda #$4B
+$A341: 8D 06 20 sta PPU::m_address
+$A344: A5 53 lda Player::zp_lifes
+$A346: 18 clc
+$A347: 69 10 adc #$10
+$A349: 8D 07 20 sta PPU::m_data
+$A34C: 24 27 bit Globals::zp_multiplayer
+$A34E: 10 15 bpl @unset_life_flag
+$A350: 2C 02 20 bit PPU::m_status
+$A353: A9 28 lda #$28
+$A355: 8D 06 20 sta PPU::m_address
+$A358: A9 56 lda #$56
+$A35A: 8D 06 20 sta PPU::m_address
+$A35D: A5 54 lda $54
+$A35F: 18 clc
+$A360: 69 10 adc #$10
+$A362: 8D 07 20 sta PPU::m_data
+
+ @unset_life_flag:
+$A365: A5 50 lda Player::zp_state
+$A367: 29 F7 and #$F7
+$A369: 85 50 sta Player::zp_state
+
+ @shuttle_update:
+$A36B: A5 20 lda Globals::zp_flags
+$A36D: AA tax
+$A36E: 29 20 and #$20
+$A370: F0 13 beq @game_status
+$A372: A5 CB lda Items::zp_collected
+$A374: D0 06 bne @do_update_shuttle
+$A376: 20 78 89 jsr Background::nmi_clear_shuttle
+$A379: 4C 7F A3 jmp @unset_shuttle_flag
+
+ @do_update_shuttle:
+$A37C: 20 59 91 jsr Items::update_shuttle
+
+ @unset_shuttle_flag:
+$A37F: A5 20 lda Globals::zp_flags
+$A381: 29 DF and #$DF
+$A383: 85 20 sta Globals::zp_flags
+
+ @game_status:
+$A385: 8A txa
+$A386: 29 01 and #$01
+$A388: D0 06 bne @ppu_registers
+$A38A: A5 30 lda Title::zp_title_timer
+$A38C: F0 02 beq @ppu_registers
+$A38E: C6 30 dec Title::zp_title_timer
+
+ @ppu_registers:
+$A390: 24 20 bit Globals::zp_flags
+$A392: 50 13 bvc @scroll
+$A394: A9 BF lda #$BF
+$A396: 25 20 and Globals::zp_flags
+$A398: 85 20 sta Globals::zp_flags
+$A39A: 2C 02 20 bit PPU::m_status
+$A39D: A5 81 lda PPU::zp_mask
+$A39F: 8D 01 20 sta PPU::m_mask
+$A3A2: A5 80 lda PPU::zp_control
+$A3A4: 8D 00 20 sta PPU::m_control
+
+ @scroll:
+$A3A7: 2C 02 20 bit PPU::m_status
+$A3AA: A9 00 lda #$00
+$A3AC: 8D 05 20 sta PPU::m_scroll
+$A3AF: 8D 05 20 sta PPU::m_scroll
+$A3B2: 20 1D 8B jsr Sound::tick
+$A3B5: A9 7F lda #$7F
+$A3B7: 25 20 and Globals::zp_flags
+$A3B9: 85 20 sta Globals::zp_flags
+$A3BB: 68 pla
+$A3BC: A8 tay
+$A3BD: 68 pla
+$A3BE: AA tax
+$A3BF: 68 pla
+$A3C0: 40 rti
+
+ irq: