aboutsummaryrefslogtreecommitdiff
path: root/lib/xixanta/src/mapping.rs
diff options
context:
space:
mode:
Diffstat (limited to 'lib/xixanta/src/mapping.rs')
-rw-r--r--lib/xixanta/src/mapping.rs417
1 files changed, 318 insertions, 99 deletions
diff --git a/lib/xixanta/src/mapping.rs b/lib/xixanta/src/mapping.rs
index 657a94b..0a73648 100644
--- a/lib/xixanta/src/mapping.rs
+++ b/lib/xixanta/src/mapping.rs
@@ -1,169 +1,388 @@
use crate::assembler::Bundle;
+use crate::errors::EvalError;
lazy_static! {
- pub static ref EMPTY: Vec<Segment> = vec![Segment {
- name: String::from("CODE"),
- start: 0x0000,
- size: 0xFFFF,
- offset: 0,
- fill: None,
- bundles: vec![],
- }];
- pub static ref NROM: Vec<Segment> = vec![
- Segment {
+ /// An empty mapper used for testing purposes.
+ pub static ref EMPTY: Vec<Mapping> = vec![
+ Mapping {
name: String::from("HEADER"),
start: 0x0000,
size: 0x0010,
offset: 0,
fill: Some(0x00),
- bundles: vec![],
+ section_type: SectionType::Header,
+ segments: vec![Segment {
+ name: String::from("HEADER"),
+ len: 0,
+ offset: 0,
+ bundles: vec![],
+ }]
},
- Segment {
- name: String::from("VECTORS"),
- start: 0xFFFA,
- size: 0x0006,
+ Mapping {
+ name: String::from("ROM0"),
+ start: 0x8000,
+ size: 0x8000,
+ offset: 0,
+ fill: None,
+ section_type: SectionType::PrgRom,
+ segments: vec![Segment {
+ name: String::from("CODE"),
+ len: 0,
+ offset: 0,
+ bundles: vec![],
+ },]
+ },
+ ];
+
+ // Mapper for a simple NROM setup (e.g. Super Mario Bros).
+ pub static ref NROM: Vec<Mapping> = vec![
+ Mapping {
+ name: String::from("HEADER"),
+ start: 0x0000,
+ size: 0x0010,
offset: 0,
fill: Some(0x00),
- bundles: vec![],
+ section_type: SectionType::Header,
+ segments: vec![Segment {
+ name: String::from("HEADER"),
+ len: 0,
+ offset: 0,
+ bundles: vec![],
+ }]
},
- Segment {
- name: String::from("CODE"),
+ Mapping {
+ name: String::from("ROM0"),
start: 0x8000,
size: 0x7FFA,
offset: 0,
fill: Some(0x00),
- bundles: vec![],
+ section_type: SectionType::PrgRom,
+ segments: vec![Segment {
+ name: String::from("CODE"),
+ len: 0,
+ offset: 0,
+ bundles: vec![],
+ },]
+ },
+ Mapping {
+ name: String::from("ROMV"),
+ start: 0xFFFA,
+ size: 0x0006,
+ offset: 0,
+ fill: Some(0x00),
+ section_type: SectionType::PrgRom,
+ segments: vec![Segment {
+ name: String::from("VECTORS"),
+ len: 0,
+ offset: 0,
+ bundles: vec![],
+ },]
},
- Segment {
- name: String::from("CHARS"),
+ Mapping {
+ name: String::from("ROM2"),
start: 0x0000,
size: 0x2000,
offset: 0,
fill: Some(0x00),
- bundles: vec![],
- }
+ section_type: SectionType::ChrRom,
+ segments: vec![Segment {
+ name: String::from("CHARS"),
+ len: 0,
+ offset: 0,
+ bundles: vec![],
+ },]
+ },
];
- pub static ref NROM65: Vec<Segment> = vec![
- Segment {
+
+ // The same mapper as NROM, but it adds a "STARTUP" segment into the "ROM0"
+ // mapping so to behave the same as the default "cc65" configuration.
+ pub static ref NROM65: Vec<Mapping> = vec![
+ Mapping {
name: String::from("HEADER"),
start: 0x0000,
size: 0x0010,
offset: 0,
fill: Some(0x00),
- bundles: vec![],
+ section_type: SectionType::Header,
+ segments: vec![Segment {
+ name: String::from("HEADER"),
+ len: 0,
+ offset: 0,
+ bundles: vec![],
+ }]
},
- Segment {
- name: String::from("VECTORS"),
- start: 0xFFFA,
- size: 0x0006,
- offset: 0,
- fill: Some(0x00),
- bundles: vec![],
- },
- Segment {
- name: String::from("STARTUP"),
+ Mapping {
+ name: String::from("ROM0"),
start: 0x8000,
size: 0x7FFA,
offset: 0,
fill: Some(0x00),
- bundles: vec![],
+ section_type: SectionType::PrgRom,
+ segments: vec![
+ Segment {
+ name: String::from("STARTUP"),
+ len: 0,
+ offset: 0,
+ bundles: vec![],
+ },
+ Segment {
+ name: String::from("CODE"),
+ len: 0,
+ offset: 0,
+ bundles: vec![],
+ },
+ ]
},
- Segment {
- name: String::from("CODE"),
- start: 0x8000,
- size: 0x7FFA,
+ Mapping {
+ name: String::from("ROMV"),
+ start: 0xFFFA,
+ size: 0x0006,
offset: 0,
fill: Some(0x00),
- bundles: vec![],
+ section_type: SectionType::PrgRom,
+ segments: vec![Segment {
+ name: String::from("VECTORS"),
+ len: 0,
+ offset: 0,
+ bundles: vec![],
+ },]
},
- Segment {
- name: String::from("CHARS"),
+ Mapping {
+ name: String::from("ROM2"),
start: 0x0000,
size: 0x2000,
offset: 0,
fill: Some(0x00),
- bundles: vec![],
- }
+ section_type: SectionType::ChrRom,
+ segments: vec![Segment {
+ name: String::from("CHARS"),
+ len: 0,
+ offset: 0,
+ bundles: vec![],
+ },]
+ },
];
}
+/// The type of section that a Mapping represents.
+#[derive(Debug, Clone, Eq, Ord, PartialEq, PartialOrd)]
+pub enum SectionType {
+ /// The 16 initial bytes describing the header of the ROM file.
+ Header,
+
+ /// Bank to be stored in PRG ROM with a size multiple of 8KB.
+ PrgRom,
+
+ /// Bank to be stored in CHR ROM with a size multiple of 4KB.
+ ChrRom,
+}
+
+/// A segment inside of a memory mapping, used to organize the code inside of a
+/// given memory mapping. Note that a segment does not do anything else: it's
+/// just about organizing code inside of a mapping. It doesn't deal with how to
+/// fill a memory region, or where it starts in memory, or anything like that.
#[derive(Debug, Clone, Eq, Ord, PartialEq, PartialOrd)]
pub struct Segment {
+ /// Name of the segment.
pub name: String,
- pub start: u16,
- pub size: usize,
+
pub offset: usize,
- pub fill: Option<u8>,
+ pub len: usize,
+
+ /// Bundles that have been generated when assembling the nodes that have
+ /// been parsed by a previous step.
pub bundles: Vec<Bundle>,
}
-/* TODO
-#[derive(Debug)]
+impl Segment {
+ /// Returns the length of the segment by counting the bundles that have been
+ // pushed so far into the segment.
+ pub fn len(&self) -> usize {
+ self.bundles
+ .iter()
+ .fold(0, |acc, bundle| acc + bundle.size as usize)
+ }
+
+ /// Returns true if the given segment has no bundles in it, false otherwise.
+ pub fn is_empty(&self) -> bool {
+ self.len() == 0
+ }
+}
+
+/// A region in memory which has one or more segments in it, which in turn have
+/// the bundles that are to be generated in the end of an assembling operation.
+#[derive(Debug, Clone, Eq, Ord, PartialEq, PartialOrd)]
pub struct Mapping {
+ /// Name of the mapping.
+ pub name: String,
+
+ /// Address where the mapping will start when loaded on the
+ /// console/emulator. This is the address where instructions like `jmp` or
+ /// labels will rely on. Hence, it's not the address of the ROM file itself,
+ /// but the effective address where it will be loaded.
+ pub start: u16,
+
+ /// Size of the mapping. Note that this depends on the `section_type` value,
+ /// which is: exactly 0x10 for a header, multiples of 0x4000 for prg_rom,
+ /// and multiples of 0x2000 for chr_rom.
+ pub size: usize,
+
+ /// The total number of bytes that have been pushed across all its segments.
+ pub offset: usize,
+
+ /// Optional value to fill the mapping if the end size is lower than the
+ /// value on `size`. Set to `None` to skip filling the memory region for
+ /// this mapping.
+ pub fill: Option<u8>,
+
+ /// Segments for the memory region.
pub segments: Vec<Segment>,
- pub nodes: HashMap<String, Vec<Node>>,
- pub current: String,
- pub macros: HashMap<String, Vec<Node>>,
- pub current_macro: Option<String>,
+
+ /// What kind of memory region is being described by this mapping.
+ pub section_type: SectionType,
}
-impl Mapping {
- pub fn new(mut segments: Vec<Segment>) -> Self {
- segments.sort_by(|a, b| a.start.cmp(&b.start));
+/// Assert that the given mappings conform to a minimum standard.
+pub fn assert(mappings: &[Mapping]) {
+ assert!(
+ !mappings.is_empty(),
+ "We need at least one segment defined, the header"
+ );
+ assert!(
+ !mappings.first().unwrap().segments.is_empty(),
+ "We need at least one segment defined, the header"
+ );
+ assert_eq!(
+ mappings.first().unwrap().section_type,
+ SectionType::Header,
+ "First mapping section must be the header"
+ );
+ assert_eq!(
+ mappings.first().unwrap().size,
+ 0x10,
+ "The header must be exactly 16 bytes long"
+ );
- let mut nodes = HashMap::new();
- for segment in segments.iter() {
- nodes.insert(segment.name.clone(), vec![]);
- }
+ let prg_rom_len = mappings
+ .iter()
+ .filter(|m| m.section_type == SectionType::PrgRom)
+ .fold(0, |acc, x| acc + x.size);
+ assert!(prg_rom_len >= 0x4000, "PRG ROM must be at least 8KB long");
+ assert!(
+ prg_rom_len % 0x4000 == 0,
+ "PRG ROM must be formed by banks of exactly 8KB"
+ );
+}
- let current_segment = &segments.first().unwrap().name.clone();
+/// Validate some sanity checks on the given `mappings`. Only call this function
+/// after all bundles have been produced.
+pub fn validate(mappings: &[Mapping]) -> Result<(), EvalError> {
+ // Guaranteed by `crate::mapping::assert` to be the header.
+ let header: &Segment = mappings.first().unwrap().segments.first().unwrap();
- Mapping {
- segments,
- nodes,
- current: current_segment.to_string(),
- macros: HashMap::new(),
- current_macro: None,
- }
+ // Header must have at least six bytes with proper information provided by
+ // the programmer.
+ if header.len() < 6 {
+ return Err(EvalError {
+ line: 0,
+ message: String::from("The header must contain at least 6 bytes"),
+ global: true,
+ });
}
- pub fn reset(&mut self) {
- self.nodes = HashMap::new();
- for segment in self.segments.iter() {
- self.nodes.insert(segment.name.clone(), vec![]);
- }
+ // Now check that the length of the evaluated data matches the criteria
+ // stated on the ROM header that was evaluated as well.
+ let (header_prg_rom_size, header_chr_rom_size) = parse_header(header)?;
+ let prg_rom_len = mappings
+ .iter()
+ .filter(|m| m.section_type == SectionType::PrgRom)
+ .fold(0, |acc, x| {
+ acc + x.segments.iter().fold(0, |a, y| a + y.len())
+ });
+ let chr_rom_len = mappings
+ .iter()
+ .filter(|m| m.section_type == SectionType::ChrRom)
+ .fold(0, |acc, x| {
+ acc + x.segments.iter().fold(0, |a, y| a + y.len())
+ });
- self.current = self.segments.first().unwrap().name.clone();
-
- self.macros = HashMap::new();
- self.current_macro = None;
+ if header_prg_rom_size < prg_rom_len {
+ return Err(EvalError {
+ line: 0,
+ message: format!("PRG ROM size is expected to by {} bytes long, but a total of {} bytes were evaluated", header_prg_rom_size, prg_rom_len),
+ global: true,
+ });
+ }
+ if header_chr_rom_size < chr_rom_len {
+ return Err(EvalError {
+ line: 0,
+ message: format!("CHR ROM size is expected to by {} bytes long, but a total of {} bytes were evaluated", header_chr_rom_size, chr_rom_len),
+ global: true,
+ });
}
- pub fn switch(&mut self, id: &PString) -> Result<()> {
- if !self.nodes.contains_key(&id.value) {
- // TODO
- // return Err(
- // id.parser_error(format!("segment '{}' has not been defined", id.value).as_str())
- // );
- }
+ Ok(())
+}
- id.value.clone_into(&mut self.current);
- Ok(())
- }
+// Returns a tuple with the sizes for PRG and CHR ROM as described from the
+// computed header. This also does some sanity checks on the header.
+fn parse_header(header: &Segment) -> Result<(usize, usize), EvalError> {
+ let mut header_it = header.bundles.clone().into_iter();
- pub fn current(&self) -> &Vec<Node> {
- self.nodes.get(&self.current).unwrap()
+ // Validate the magic string: 'N', 'E', 'S', $1A
+ if header_it.next().unwrap().bytes[0] != b'N' {
+ return Err(EvalError {
+ line: 0,
+ message: String::from("First byte of the header must be 'N'"),
+ global: true,
+ });
}
-
- pub fn current_mut(&mut self) -> &mut Vec<Node> {
- self.nodes.get_mut(&self.current).unwrap()
+ if header_it.next().unwrap().bytes[0] != b'E' {
+ return Err(EvalError {
+ line: 0,
+ message: String::from("Second byte of the header must be 'E'"),
+ global: true,
+ });
+ }
+ if header_it.next().unwrap().bytes[0] != b'S' {
+ return Err(EvalError {
+ line: 0,
+ message: String::from("Third byte of the header must be 'S'"),
+ global: true,
+ });
}
+ if header_it.next().unwrap().bytes[0] != 26 {
+ return Err(EvalError {
+ line: 0,
+ message: String::from(
+ "Fourth byte of the header must be the MS-DOS termination character",
+ ),
+ global: true,
+ });
+ }
+
+ Ok((
+ header_it.next().unwrap().bytes[0] as usize * 0x4000,
+ header_it.next().unwrap().bytes[0] as usize * 0x2000,
+ ))
+}
- pub fn push(&mut self, node: Node) {
- match &self.current_macro {
- Some(m) => self.macros.get_mut(m).unwrap().push(node),
- None => self.nodes.get_mut(&self.current).unwrap().push(node),
+/// Returns the offset of the segment indexed by `segment_index` inside of
+/// `mapping`. That is, it returns back at which byte the given segment is going
+/// to be placed inside of the given mapping.
+///
+/// NOTE: this function is only useful if you already know that all the segments
+/// on the given mapping have a definitive size (i.e. they will not change in
+/// the future).
+pub fn segment_offset(mapping: &Mapping, segment_index: usize) -> u16 {
+ let mut count = 0;
+
+ for (idx, segment) in mapping.segments.iter().enumerate() {
+ if idx >= segment_index {
+ return count;
}
+ count += segment.offset as u16;
}
+
+ count
}
-*/