From 8b1c270910fce13077864bf39d6e88971a6eb062 Mon Sep 17 00:00:00 2001 From: Miquel Sabaté Solà Date: Wed, 8 Jan 2025 12:34:47 +0100 Subject: Implement the .include statement MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This needed some heavy lifting when it comes to how files were located. This means that statements like .include/.incbin now take into consideration a new list made out of SourceInfo, which holds enough information to translate from which file a node comes from. This has also been added into errors, so they are more informative on what went wrong. In order to tests this, besides all the regular unit tests, a new e2e test has been added. Signed-off-by: Miquel Sabaté Solà --- crates/nasm/src/main.rs | 22 +- .../fuzz/fuzz_targets/fuzz_target_assembler.rs | 6 +- .../fuzz/fuzz_targets/fuzz_target_parser.rs | 2 +- lib/xixanta/src/assembler.rs | 309 +++++++++++----- lib/xixanta/src/errors.rs | 61 +++- lib/xixanta/src/lib.rs | 15 + lib/xixanta/src/mapping.rs | 57 +-- lib/xixanta/src/node.rs | 8 +- lib/xixanta/src/object.rs | 91 ++--- lib/xixanta/src/opcodes.rs | 1 + lib/xixanta/src/parser.rs | 396 ++++++++++++++++----- scripts/test-e2e.sh | 3 + tests/assets/diskun.chr | Bin 0 -> 8192 bytes tests/src/flicker/flicker.s | 280 +++++++++++++++ tests/src/shared/diskun.s | 96 +++++ tests/src/shared/joypad.s | 86 +++++ 16 files changed, 1141 insertions(+), 292 deletions(-) create mode 100644 tests/assets/diskun.chr create mode 100644 tests/src/flicker/flicker.s create mode 100644 tests/src/shared/diskun.s create mode 100644 tests/src/shared/joypad.s diff --git a/crates/nasm/src/main.rs b/crates/nasm/src/main.rs index 225a865..561e305 100644 --- a/crates/nasm/src/main.rs +++ b/crates/nasm/src/main.rs @@ -4,6 +4,7 @@ use std::fs::File; use std::io::{self, Read, Write}; use std::path::Path; use xixanta::assembler::assemble; +use xixanta::SourceInfo; /// Assembler for the 6502 microprocessor that targets the NES/Famicom. #[derive(ClapParser, Debug)] @@ -40,7 +41,7 @@ fn main() -> Result<()> { // Select the input stream and the current working directory. let input: Box; - let working_directory = match &args.file { + let source_info = match &args.file { Some(file) => { let path = Path::new(file); if !path.is_file() { @@ -48,13 +49,22 @@ fn main() -> Result<()> { } input = Box::new(File::open(file)?); - path.parent() - .with_context(|| String::from("Failed to find directory for given file"))? + SourceInfo { + working_directory: path + .parent() + .with_context(|| String::from("Failed to find directory for given file"))? + .to_path_buf(), + name: path.file_name().unwrap().to_str().unwrap().to_string(), + } } None => { input = Box::new(std::io::stdin()); - &std::env::current_dir() - .with_context(|| String::from("Could not fetch current directory"))? + SourceInfo { + working_directory: std::env::current_dir() + .with_context(|| String::from("Could not fetch current directory"))? + .to_path_buf(), + name: "".to_string(), + } } }; @@ -82,7 +92,7 @@ fn main() -> Result<()> { // And assemble. let mut error_count = 0; - let res = assemble(input, config.as_str(), working_directory.to_path_buf()); + let res = assemble(input, config.as_str(), source_info); // Print warnings and errors first, while also computing the amount of them // that exists. diff --git a/lib/xixanta/fuzz/fuzz_targets/fuzz_target_assembler.rs b/lib/xixanta/fuzz/fuzz_targets/fuzz_target_assembler.rs index 9b9119b..a402998 100644 --- a/lib/xixanta/fuzz/fuzz_targets/fuzz_target_assembler.rs +++ b/lib/xixanta/fuzz/fuzz_targets/fuzz_target_assembler.rs @@ -4,9 +4,5 @@ use libfuzzer_sys::fuzz_target; use xixanta::assembler::assemble; fuzz_target!(|data: &[u8]| { - let _ = assemble( - data, - "empty", - std::env::current_dir().unwrap().to_path_buf(), - ); + let _ = assemble(data, "empty", xixanta::SourceInfo::default()); }); diff --git a/lib/xixanta/fuzz/fuzz_targets/fuzz_target_parser.rs b/lib/xixanta/fuzz/fuzz_targets/fuzz_target_parser.rs index 442dbbf..da8a350 100644 --- a/lib/xixanta/fuzz/fuzz_targets/fuzz_target_parser.rs +++ b/lib/xixanta/fuzz/fuzz_targets/fuzz_target_parser.rs @@ -4,5 +4,5 @@ use libfuzzer_sys::fuzz_target; fuzz_target!(|data: &[u8]| { let mut parser = xixanta::parser::Parser::default(); - let _ = parser.parse(data); + let _ = parser.parse(data, xixanta::SourceInfo::default()); }); diff --git a/lib/xixanta/src/assembler.rs b/lib/xixanta/src/assembler.rs index 30547d2..d82be73 100644 --- a/lib/xixanta/src/assembler.rs +++ b/lib/xixanta/src/assembler.rs @@ -1,15 +1,15 @@ use crate::errors::{ContextError, ContextErrorReason, Error, EvalError}; use crate::mapping::{get_mapping_configuration, Mapping}; -use crate::node::{ControlType, NodeType, OperationType, PNode, PString}; +use crate::node::{ControlType, NodeType, OperationType, PNode}; use crate::object::{Bundle, Context, Object, ObjectType}; use crate::opcodes::{AddressingMode, INSTRUCTIONS}; use crate::parser::Parser; +use crate::SourceInfo; use std::cmp::Ordering; use std::collections::HashMap; use std::fs::File; use std::io::Read; use std::ops::Neg; -use std::path::PathBuf; /// The mode in which a literal is expressed. #[derive(Clone, PartialEq)] @@ -78,10 +78,8 @@ pub struct Assembler<'a> { // Warnings that have accumulated over the run. warnings: Vec, - // Stack of directories. The last directory is the current one, whereas the - // other elements come from previous contexts. This way we can implement a - // file that imports another file which in turn imports another file, etc. - directories: Vec, + // TODO + sources: Vec, } #[derive(Debug)] @@ -92,12 +90,12 @@ pub struct AssemblerResult { } /// Read the contents from the `reader` as a source file and produce a list of -/// bundles that can be formatted as binary data. You also need to pass the -/// initial working directory `init_directory`, as otherwise control statements -/// like ".import" or ".incbin" wouldn't know how to resolve relative paths. You -/// can specify the mapper to be used as an identifier in `mapping`, which will -/// be handled via `get_mapping_configuration`. -pub fn assemble(reader: impl Read, mapping: &str, init_directory: PathBuf) -> AssemblerResult { +/// bundles that can be formatted as binary data. You also need to pass +/// information of the source by means of `source`, as otherwise control +/// statements like ".import" or ".incbin" wouldn't know how to resolve relative +/// paths. You can specify the mapper to be used as an identifier in `mapping`, +/// which will be handled via `get_mapping_configuration`. +pub fn assemble(reader: impl Read, mapping: &str, source: SourceInfo) -> AssemblerResult { let config = match get_mapping_configuration(mapping) { Ok(config) => config, Err(e) => { @@ -107,35 +105,33 @@ pub fn assemble(reader: impl Read, mapping: &str, init_directory: PathBuf) -> As global: true, line: 0, message: e, + source, })], warnings: vec![], }; } }; - assemble_with_mapping(reader, config, init_directory) + assemble_with_mapping(reader, config, source) } /// Read the contents from the `reader` as a source file and produce a list of -/// bundles that can be formatted as binary data. You also need to pass the -/// initial working directory `init_directory`, as otherwise control statements -/// like ".import" or ".incbin" wouldn't know how to resolve relative paths. You -/// also need to provide the `mapping` as handled internally. If you are unsure -/// how to use it, just call `assemble`. +/// bundles that can be formatted as binary data. You also need to pass +/// information of the source by means of `source`, as otherwise control +/// statements like ".import" or ".incbin" wouldn't know how to resolve relative +/// paths. You also need to provide the `mapping` as handled internally. If you +/// are unsure how to use it, just call `assemble`. pub fn assemble_with_mapping( reader: impl Read, mapping: Vec, - init_directory: PathBuf, + source: SourceInfo, ) -> AssemblerResult { let mut asm = Assembler::new(mapping); - // Push the initial directory into our stack of directories. - asm.directories.push(init_directory); - // First of all, parse the input so we get a list of nodes we can work // with. let mut parser = Parser::default(); - if let Err(errors) = parser.parse(reader) { + if let Err(errors) = parser.parse(reader, source) { return AssemblerResult { bundles: vec![], errors: errors.iter().map(|e| Error::Parse(e.clone())).collect(), @@ -144,6 +140,7 @@ pub fn assemble_with_mapping( } let nodes = parser.nodes(); + asm.sources = parser.sources; // Build the context by iterating over the parsed nodes and checking // where scopes start/end, evaluating values for variables, labels, etc. @@ -208,27 +205,37 @@ impl<'a> Assembler<'a> { procs_seen: 0, repeats_seen: 0, warnings: vec![], - directories: vec![], + sources: vec![], } } // Define a new variable by taking the given `id`. This variable will only // be created if `id` is not empty. The function will error out if the given // name is already taken. - fn define_variable(&mut self, id: &PString) -> Result<(), ContextError> { - if id.is_empty() { + fn define_variable(&mut self, node: &PNode) -> Result<(), ContextError> { + if node.value.is_empty() { return Ok(()); } - self.context.set_variable( - id, + if let Err(message) = self.context.set_variable( + &node.value, &Object::new( self.current_mapping, self.current_segment, ObjectType::Address, ), false, - ) + ) { + return Err(ContextError { + message, + line: node.value.line, + global: false, + source: self.source_for(node), + reason: ContextErrorReason::BadScope, + }); + } + + Ok(()) } fn eval_context(&mut self, nodes: &'a [PNode]) -> Result<(), Vec> { @@ -250,11 +257,12 @@ impl<'a> Assembler<'a> { "using a named label ('{}') inside of a macro/repeat definition", node.value.value ), + source: self.source_for(node), global: false, })); continue; } - if let Err(err) = self.define_variable(&node.value) { + if let Err(err) = self.define_variable(node) { errors.push(Error::Context(err)); } } @@ -264,6 +272,7 @@ impl<'a> Assembler<'a> { message: "cannot have assignments inside of macro/repeat definitions" .to_string(), line: node.value.line, + source: self.source_for(node), global: false, })); continue; @@ -281,7 +290,13 @@ impl<'a> Assembler<'a> { }, false, ) { - errors.push(Error::Context(err)); + errors.push(Error::Context(ContextError { + message: err, + line: node.value.line, + global: false, + source: self.source_for(node), + reason: ContextErrorReason::BadScope, + })); } } Err(e) => errors.push(Error::Eval(e)), @@ -293,6 +308,7 @@ impl<'a> Assembler<'a> { message: format!("{} must be on the global scope", control_type), line: node.value.line, global: false, + source: self.source_for(node), reason: ContextErrorReason::BadScope, })); continue; @@ -327,13 +343,14 @@ impl<'a> Assembler<'a> { message: "you cannot call '.proc' in this context".to_string(), line: node.value.line, global: false, + source: self.source_for(node), reason: ContextErrorReason::BadStart, })); continue; } self.procs_seen += 1; - let proc_name = &node.left.as_ref().unwrap().value; + let proc_name = &node.left.as_ref().unwrap(); if let Err(err) = self.define_variable(proc_name) { errors.push(Error::Context(err)); } @@ -350,6 +367,7 @@ impl<'a> Assembler<'a> { errors.push(Error::Context(ContextError { message: "you cannot call '.scope' in this context".to_string(), line: node.value.line, + source: self.source_for(node), global: false, reason: ContextErrorReason::BadStart, })); @@ -369,8 +387,14 @@ impl<'a> Assembler<'a> { // If this control statement implies a context change, do it // now. - if let Err(err) = self.context.change_context(node) { - errors.push(Error::Context(err)); + if let Err(message) = self.context.change_context(node) { + errors.push(Error::Context(ContextError { + message, + line: node.value.line, + global: false, + source: self.source_for(node), + reason: ContextErrorReason::BadScope, + })); } // If this control statement actually has a body, go inside @@ -395,7 +419,7 @@ impl<'a> Assembler<'a> { // it's empty (i.e. anonymous label). In either case, the computed label // will be pushed into the context's list of known labels with the current // segment offset. - fn apply_segment_offset_to_label(&mut self, id: &PString) -> Result<(), ContextError> { + fn apply_segment_offset_to_label(&mut self, node: &PNode) -> Result<(), ContextError> { let segment = &self.mappings[self.current_mapping].segments[self.current_segment]; let value = segment.offset.to_le_bytes(); let object = Object { @@ -413,8 +437,16 @@ impl<'a> Assembler<'a> { object_type: ObjectType::Address, }; - if !id.is_empty() { - self.context.set_variable(id, &object, true)?; + if !node.value.is_empty() { + if let Err(message) = self.context.set_variable(&node.value, &object, true) { + return Err(ContextError { + message, + line: node.value.line, + global: false, + source: self.source_for(node), + reason: ContextErrorReason::BadScope, + }); + } } self.context.add_label(&object); @@ -433,7 +465,7 @@ impl<'a> Assembler<'a> { // of the offset, the effective address will only be available // after calling `Context::get_variable` NodeType::Label => { - if let Err(e) = self.apply_segment_offset_to_label(&node.value) { + if let Err(e) = self.apply_segment_offset_to_label(node) { errors.push(Error::Context(e)); } } @@ -441,12 +473,18 @@ impl<'a> Assembler<'a> { // introduces a new context. Hence, first act as a label, and // then open up its inner context. NodeType::Control(ControlType::StartProc) => { - let proc_name = &node.left.as_ref().unwrap().value; + let proc_name = &node.left.as_ref().unwrap(); if let Err(e) = self.apply_segment_offset_to_label(proc_name) { errors.push(Error::Context(e)); } - if let Err(e) = self.context.change_context(node) { - errors.push(Error::Context(e)); + if let Err(message) = self.context.change_context(node) { + errors.push(Error::Context(ContextError { + message, + line: node.value.line, + global: false, + source: self.source_for(node), + reason: ContextErrorReason::BadScope, + })); } // And now go inside of its body if it exists (note that its @@ -457,7 +495,8 @@ impl<'a> Assembler<'a> { if args.is_empty() { self.warnings.push(Error::Eval(EvalError { line: node.value.line, - message: format!("empty .proc '{}'", proc_name.value), + message: format!("empty .proc '{}'", proc_name.value.value), + source: self.source_for(node), global: false, })); } else { @@ -501,6 +540,7 @@ impl<'a> Assembler<'a> { self.warnings.push(Error::Eval(EvalError { line: node.value.line, message: format!("empty .scope '{}'", scope_name.value), + source: self.source_for(node), global: false, })); } else { @@ -572,7 +612,12 @@ impl<'a> Assembler<'a> { // Validate the mappings that have been evaluated before spitting it // out. if let Err(e) = crate::mapping::validate(&self.mappings) { - return Err(vec![Error::Eval(e)]); + return Err(vec![Error::Eval(EvalError { + line: 0, + global: true, + message: e, + source: self.sources[0].clone(), + })]); } let mut res = vec![]; @@ -583,6 +628,7 @@ impl<'a> Assembler<'a> { self.warnings.push(Error::Eval(EvalError { line: 0, message: format!("segment '{}' is empty", segment.name), + source: self.sources[0].clone(), global: true, })); } @@ -597,6 +643,7 @@ impl<'a> Assembler<'a> { "exceeding segment size for '{}'; expecting {} bytes and {} bytes have already been seen", mapping.name, mapping.size, mapping.offset, ), + source: self.sources[0].clone(), global: false, })); } @@ -625,6 +672,7 @@ impl<'a> Assembler<'a> { "could not find a macro with the name '{}'", node.value.value ), + source: self.source_for(node), global: false, })?; @@ -639,6 +687,7 @@ impl<'a> Assembler<'a> { "wrong number of arguments for '{}': {} required but {} given", node.value.value, macro_args, given_args, ), + source: self.source_for(node), global: false, })]); } @@ -658,8 +707,18 @@ impl<'a> Assembler<'a> { // Note that we overwrite the variable value from previous // calls, just in case a macro is applied multiple times and we // need to get the latest value. - self.context - .set_variable(&margs.next().unwrap().value, &obj, true)?; + if let Err(message) = + self.context + .set_variable(&margs.next().unwrap().value, &obj, true) + { + return Err(vec![Error::Context(ContextError { + line: node.value.line, + message, + source: self.source_for(node), + global: false, + reason: ContextErrorReason::BadScope, + })]); + } } } @@ -674,6 +733,7 @@ impl<'a> Assembler<'a> { self.warnings.push(Error::Eval(EvalError { line: node.value.line, message: format!("trying to apply empty macro '{}'", node.value.value), + source: self.source_for(node), global: false, })); } else { @@ -726,12 +786,13 @@ impl<'a> Assembler<'a> { Err(EvalError { message: "no prefix was given to operand".to_string(), line: node.value.line, + source: self.source_for(node), global: false, }) } else { // This is actually a valid identifier! Try to fetch the // variable. - match self.evaluate_variable(&node.value) { + match self.evaluate_variable(node) { Ok(v) => { self.literal_mode = Some(LiteralMode::Hexadecimal); Ok(v) @@ -742,6 +803,7 @@ impl<'a> Assembler<'a> { err.message ), line: node.value.line, + source: self.source_for(node), global: false, }), } @@ -751,6 +813,7 @@ impl<'a> Assembler<'a> { _ => Err(EvalError { message: format!("unexpected '{}' expression type", node.node_type), line: node.value.line, + source: self.source_for(node), global: false, }), } @@ -807,6 +870,7 @@ impl<'a> Assembler<'a> { return Err(EvalError { line: node.value.line, global: false, + source: self.source_for(node), message: "attempting to divide by zero".to_string(), }); } @@ -830,6 +894,7 @@ impl<'a> Assembler<'a> { return Err(EvalError { line: node.value.line, global: false, + source: self.source_for(node), message: "shift operator too big".to_string(), }); } @@ -842,6 +907,7 @@ impl<'a> Assembler<'a> { return Err(EvalError { line: node.value.line, global: false, + source: self.source_for(node), message: "shift operator too big".to_string(), }); } @@ -856,6 +922,7 @@ impl<'a> Assembler<'a> { return Err(EvalError { line: node.value.line, global: false, + source: self.source_for(node), message: "performing the operation would overflow a 16-bit integer".to_string(), }); } @@ -889,9 +956,10 @@ impl<'a> Assembler<'a> { &self.mappings, ) { Ok(object) => Ok(object.bundle), - Err(e) => Err(EvalError { + Err(message) => Err(EvalError { line: node.value.line, - message: e.message, + message, + source: self.source_for(node), global: false, }), } @@ -929,12 +997,13 @@ impl<'a> Assembler<'a> { size = 2; } _ => { - if self.evaluate_variable(&node.value).is_ok() { + if self.evaluate_variable(node).is_ok() { return Err(EvalError { message: format!( "you cannot use variables like '{}' in hexadecimal literals", node.value.value ), + source: self.source_for(node), line: node.value.line, global: false, }); @@ -942,6 +1011,7 @@ impl<'a> Assembler<'a> { return Err(EvalError { message: "expecting a number of 1 to 4 hexadecimal digits".to_string(), line: node.value.line, + source: self.source_for(node), global: false, }); } @@ -970,6 +1040,7 @@ impl<'a> Assembler<'a> { return Err(EvalError { message: "missing binary digits to get a full byte".to_string(), line: node.value.line, + source: self.source_for(node), global: false, }) } @@ -977,6 +1048,7 @@ impl<'a> Assembler<'a> { return Err(EvalError { message: "too many binary digits for a single byte".to_string(), line: node.value.line, + source: self.source_for(node), global: false, }) } @@ -988,13 +1060,14 @@ impl<'a> Assembler<'a> { let val = 1 << shift; value += val; } else if c != '0' { - if self.evaluate_variable(&node.value).is_ok() { + if self.evaluate_variable(node).is_ok() { return Err(EvalError { message: format!( "you cannot use variables like '{}' in binary literals", string ), line: node.value.line, + source: self.source_for(node), global: false, }); } @@ -1002,6 +1075,7 @@ impl<'a> Assembler<'a> { message: format!("bad binary format for '{}'", string), line: node.value.line, global: false, + source: self.source_for(node), }); } } @@ -1023,6 +1097,7 @@ impl<'a> Assembler<'a> { return Err(EvalError { message: "empty decimal literal".to_string(), line: node.value.line, + source: self.source_for(node), global: false, }); } @@ -1035,6 +1110,7 @@ impl<'a> Assembler<'a> { return Err(EvalError { message: "decimal value is too big".to_string(), line: node.value.line, + source: self.source_for(node), global: false, }); } @@ -1052,11 +1128,12 @@ impl<'a> Assembler<'a> { in variable definitions", string ), + source: self.source_for(node), line: node.value.line, global: false, }); } - match self.evaluate_variable(&node.value) { + match self.evaluate_variable(node) { Ok(v) => return Ok(v), Err(err) => { return Err(EvalError { @@ -1064,6 +1141,7 @@ impl<'a> Assembler<'a> { "'{}' is not a decimal value and {} either", c, err.message ), + source: self.source_for(node), line: node.value.line, global: false, }); @@ -1079,6 +1157,7 @@ impl<'a> Assembler<'a> { return Err(EvalError { message: "decimal value is too big".to_string(), line: node.value.line, + source: self.source_for(node), global: false, }); } @@ -1111,6 +1190,7 @@ impl<'a> Assembler<'a> { return Err(EvalError { message: "literal cannot embed another literal".to_string(), line: node.value.line, + source: self.source_for(node), global: false, }); } @@ -1120,6 +1200,7 @@ impl<'a> Assembler<'a> { return Err(EvalError { message: "literal cannot embed another literal".to_string(), line: node.value.line, + source: self.source_for(node), global: false, }); } @@ -1140,21 +1221,21 @@ impl<'a> Assembler<'a> { Some(c) => match c.to_digit(16) { Some(c) => Ok(c as u8), None => { - if (c.is_alphabetic() || c == '_') - && self.evaluate_variable(&source.value).is_ok() - { + if (c.is_alphabetic() || c == '_') && self.evaluate_variable(source).is_ok() { return Err(EvalError { message: format!( "you cannot use variables like '{}' in hexadecimal literals", source.value.value ), line: source.value.line, + source: self.source_for(source), global: false, }); } Err(EvalError { message: "could not convert digit to hexadecimal".to_string(), line: source.value.line, + source: self.source_for(source), global: false, }) } @@ -1162,6 +1243,7 @@ impl<'a> Assembler<'a> { None => Err(EvalError { message: "digit out of bounds".to_string(), line: source.value.line, + source: self.source_for(source), global: false, }), } @@ -1171,8 +1253,20 @@ impl<'a> Assembler<'a> { // This might just be a statement that changes the context (e.g. // ".macro", ".proc", etc.). In this case change the context and leave // early. - if self.context.change_context(node)? { - return Ok(()); + match self.context.change_context(node) { + Ok(changed) => { + if changed { + return Ok(()); + } + } + Err(message) => { + return Err(EvalError { + message, + line: node.value.line, + source: self.source_for(node), + global: false, + }); + } } // Otherwise, check the function that could act as a statement that @@ -1186,12 +1280,14 @@ impl<'a> Assembler<'a> { NodeType::Control(ControlType::IncBin) => { self.incbin(node.args.as_ref().unwrap().first().unwrap()) } + NodeType::Control(ControlType::IncludeSource) => Ok(()), _ => Err(EvalError { line: node.value.line, message: format!( "cannot handle control statement '{}' in this context", node.value.value ), + source: self.source_for(node), global: false, }), } @@ -1211,6 +1307,7 @@ impl<'a> Assembler<'a> { "path has to be written inside of double quotes ('{}' given instead)", value, ), + source: self.source_for(node), global: false, }); } @@ -1218,12 +1315,18 @@ impl<'a> Assembler<'a> { // The '.incbin' control assumes that paths are relative to the // directory of the current file. Hence, in order to make subsequent // `File` operations work in this way, set the current directory now. - if let Err(e) = std::env::set_current_dir(self.directories.last().unwrap()) { - return Err(EvalError { - line: node.value.line, - message: format!("could not move to the directory of '{}': {}", value, e), - global: false, - }); + match &self.sources.get(node.source) { + Some(source) => { + if let Err(e) = std::env::set_current_dir(&source.working_directory) { + return Err(EvalError { + line: node.value.line, + message: format!("could not move to the directory of '{}': {}", value, e), + source: self.source_for(node), + global: false, + }); + } + } + None => panic!("mismatch on the node source"), } // Fetch the actual path. @@ -1234,6 +1337,7 @@ impl<'a> Assembler<'a> { return Err(EvalError { global: false, line: node.value.line, + source: self.source_for(node), message: format!("could not include binary data: {}", e), }) } @@ -1252,12 +1356,14 @@ impl<'a> Assembler<'a> { return Err(EvalError { global: false, line: node.value.line, + source: self.source_for(node), message: format!("file '{}' is too big", path), }); } else if metadata.len() == 0 { return Err(EvalError { global: false, line: node.value.line, + source: self.source_for(node), message: format!("trying to include an empty file ('{}')", path), }); } @@ -1266,6 +1372,7 @@ impl<'a> Assembler<'a> { return Err(EvalError { global: false, line: node.value.line, + source: self.source_for(node), message: format!("could not include binary data: {}", e), }) } @@ -1293,6 +1400,7 @@ impl<'a> Assembler<'a> { return Err(EvalError { global: false, line: node.value.line, + source: self.source_for(node), message: "pointless .repeat statement".to_string(), } .into()); @@ -1300,6 +1408,7 @@ impl<'a> Assembler<'a> { return Err(EvalError { global: false, line: node.value.line, + source: self.source_for(node), message: "the number of iterations has to fit in a single byte".to_string(), } .into()); @@ -1314,6 +1423,7 @@ impl<'a> Assembler<'a> { "first argument must be an integer, '{}' found instead", first ), + source: self.source_for(node), } .into()) } @@ -1325,6 +1435,7 @@ impl<'a> Assembler<'a> { self.warnings.push(Error::Eval(EvalError { line: node.value.line, message: "empty .repeat statement".to_string(), + source: self.source_for(node), global: false, })); return Ok(()); @@ -1335,7 +1446,7 @@ impl<'a> Assembler<'a> { // If an index was given, set it now as a .repeat variable with the // loop index. if args.len() == 2 { - self.context.set_variable( + if let Err(e) = self.context.set_variable( &args.last().unwrap().value, &Object { bundle: Bundle::fill(i as u8), @@ -1344,7 +1455,15 @@ impl<'a> Assembler<'a> { object_type: ObjectType::Value, }, true, - )?; + ) { + return Err(EvalError { + line: node.value.line, + message: e, + source: self.source_for(node), + global: false, + } + .into()); + } } // And push all the bundles from the inner code. @@ -1364,6 +1483,7 @@ impl<'a> Assembler<'a> { "cannot handle control statement '{}' as an expression in this context", node.value.value ), + source: self.source_for(node), global: false, }), } @@ -1413,6 +1533,7 @@ impl<'a> Assembler<'a> { line: arg.value.line, message: "expecting an argument that fits into a byte" .to_string(), + source: self.source_for(node), global: false, }) } @@ -1434,6 +1555,7 @@ impl<'a> Assembler<'a> { "expecting at least one argument for '{}'", node.value.value.as_str(), ), + source: self.source_for(node), global: false, }) } @@ -1456,7 +1578,8 @@ impl<'a> Assembler<'a> { "segment declaration has to be written inside of double quotes ('{}' given instead)", val, ), - global: false, + source: self.source_for(node), + global: false, }); } @@ -1469,6 +1592,7 @@ impl<'a> Assembler<'a> { return Err(EvalError { line: node.value.line, message: "segment name contains bad characters".to_string(), + source: self.source_for(node), global: false, }); } @@ -1490,18 +1614,20 @@ impl<'a> Assembler<'a> { return Err(EvalError { line: node.value.line, message: format!("unknown segment '{}'", name), + source: self.source_for(node), global: false, }); } Ok(()) } - fn evaluate_variable(&mut self, id: &PString) -> Result { - match self.context.get_variable(id, &self.mappings) { + fn evaluate_variable(&mut self, node: &PNode) -> Result { + match self.context.get_variable(&node.value, &self.mappings) { Ok(value) => Ok(value.bundle), Err(e) => Err(EvalError { - message: e.message, - line: id.line, + message: e, + line: node.value.line, + source: self.source_for(node), global: false, }), } @@ -1532,15 +1658,17 @@ impl<'a> Assembler<'a> { "cannot use {} addressing mode for the instruction '{}'", mode, mnemonic ), + source: self.source_for(node), line: node.value.line, global: false, - }) + }); } }, None => { return Err(EvalError { message: format!("unknown instruction {}", mnemonic), line: node.value.line, + source: self.source_for(node), global: false, }); } @@ -1575,6 +1703,7 @@ impl<'a> Assembler<'a> { "it has to be either X addressing or Y addressing, not all at once" .to_string(), line: node.value.line, + source: self.source_for(node), global: false, }); } @@ -1585,6 +1714,7 @@ impl<'a> Assembler<'a> { message: "address can only be one byte long on indirect Y addressing" .to_string(), line: node.value.line, + source: self.source_for(node), global: false, }); } @@ -1593,6 +1723,7 @@ impl<'a> Assembler<'a> { Err(EvalError { message: "only the Y index is allowed on indirect Y addressing".to_string(), line: node.value.line, + source: self.source_for(node), global: false, }) } @@ -1606,6 +1737,7 @@ impl<'a> Assembler<'a> { "address can only be one byte long on indirect X addressing" .to_string(), line: node.value.line, + source: self.source_for(node), global: false, }); } @@ -1614,6 +1746,7 @@ impl<'a> Assembler<'a> { Err(EvalError { message: "only the X index is allowed on indirect X addressing".to_string(), line: node.value.line, + source: self.source_for(node), global: false, }) } @@ -1623,6 +1756,7 @@ impl<'a> Assembler<'a> { return Err(EvalError { message: "expecting a full 16-bit address".to_string(), line: node.value.line, + source: self.source_for(node), global: false, }); } @@ -1644,6 +1778,7 @@ impl<'a> Assembler<'a> { return Err(EvalError { message: "indexed addressing only works with addresses".to_string(), line: node.value.line, + source: self.source_for(node), global: false, }); } @@ -1706,6 +1841,7 @@ impl<'a> Assembler<'a> { _ => Err(EvalError { message: "can only use X and Y as indices".to_string(), line: node.value.line, + source: self.source_for(node), global: false, }), } @@ -1744,6 +1880,7 @@ impl<'a> Assembler<'a> { _ => Err(EvalError { message: "immediate is too big".to_string(), line: left_arm.value.line, + source: self.source_for(base), global: false, }), } @@ -1755,6 +1892,7 @@ impl<'a> Assembler<'a> { message: "left arm of instruction is neither an address nor an immediate" .to_string(), line: left_arm.value.line, + source: self.source_for(base), global: false, }), } @@ -1774,6 +1912,7 @@ impl<'a> Assembler<'a> { return Err(EvalError { line: node.value.line, message: "you cannot branch to this location: it's too far away".to_string(), + source: self.source_for(node), global: false, }); } @@ -1784,6 +1923,7 @@ impl<'a> Assembler<'a> { return Err(EvalError { line: node.value.line, message: "you cannot branch to this location: it's too far away".to_string(), + source: self.source_for(node), global: false, }); } @@ -1796,6 +1936,17 @@ impl<'a> Assembler<'a> { Ok(()) } + + // Builds a SourceInfo object based on the given node. + fn source_for(&self, node: &PNode) -> SourceInfo { + self.sources + .get(node.source) + .unwrap_or(&SourceInfo { + working_directory: self.sources[0].working_directory.clone(), + name: self.sources[0].name.clone(), + }) + .clone() + } } #[cfg(test)] @@ -1865,11 +2016,7 @@ mod tests { // the assembler will freak out. let real_line = minimal_header().to_string() + line; - assemble_with_mapping( - real_line.as_bytes(), - empty(), - std::env::current_dir().unwrap().to_path_buf(), - ) + assemble_with_mapping(real_line.as_bytes(), empty(), SourceInfo::default()) } // Like `just_assemble` but it only returns bundles passed the header. @@ -3313,7 +3460,7 @@ lda #Variable "# .as_bytes(), one_two().to_vec(), - std::env::current_dir().unwrap().to_path_buf(), + SourceInfo::default(), ); assert_eq!(res.bundles.len(), 0x11); @@ -3354,7 +3501,7 @@ lda #Variable "# .as_bytes(), one_two().to_vec(), - std::env::current_dir().unwrap().to_path_buf(), + SourceInfo::default(), ); let bundles = &res.bundles[0x11..]; @@ -3427,7 +3574,7 @@ lda #Variable "# .as_bytes(), one_two().to_vec(), - std::env::current_dir().unwrap().to_path_buf(), + SourceInfo::default(), ); let bundles = &res.bundles[0x12..]; // Ignoring HEADER + first two ONE @@ -3474,7 +3621,7 @@ lda #Variable "# .as_bytes(), one_two().to_vec(), - std::env::current_dir().unwrap().to_path_buf(), + SourceInfo::default(), ); let bundles = &res.bundles[0x11..]; // Ignoring HEADER + first nop @@ -3528,7 +3675,7 @@ lda #Variable "# .as_bytes(), one_two().to_vec(), - std::env::current_dir().unwrap().to_path_buf(), + SourceInfo::default(), ); let bundles = &res.bundles[0x10..]; @@ -3554,7 +3701,7 @@ lda #Variable "# .as_bytes(), one_two().to_vec(), - std::env::current_dir().unwrap().to_path_buf(), + SourceInfo::default(), ); assert_eq!( diff --git a/lib/xixanta/src/errors.rs b/lib/xixanta/src/errors.rs index ae55b83..3ca00bc 100644 --- a/lib/xixanta/src/errors.rs +++ b/lib/xixanta/src/errors.rs @@ -1,3 +1,4 @@ +use crate::SourceInfo; use std::fmt; #[derive(Debug, Clone, PartialEq)] @@ -7,6 +8,18 @@ pub enum Error { Eval(EvalError), } +impl From for Vec { + fn from(err: ParseError) -> Self { + vec![Error::Parse(err)] + } +} + +impl From for Vec { + fn from(err: ParseError) -> Self { + vec![err] + } +} + impl From for Vec { fn from(err: ContextError) -> Self { vec![Error::Context(err)] @@ -33,13 +46,24 @@ impl fmt::Display for Error { pub struct ParseError { pub line: usize, pub message: String, + pub source: SourceInfo, } impl std::error::Error for ParseError {} impl fmt::Display for ParseError { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "{} (line {})", self.message, self.line + 1) + if self.source.name.is_empty() { + write!(f, "{} (line {})", self.message, self.line + 1) + } else { + write!( + f, + "{} ({}: line {})", + self.message, + self.source.name, + self.line + 1 + ) + } } } @@ -61,6 +85,7 @@ pub struct ContextError { pub reason: ContextErrorReason, pub message: String, pub global: bool, + pub source: SourceInfo, } impl std::error::Error for ContextError {} @@ -68,9 +93,21 @@ impl std::error::Error for ContextError {} impl fmt::Display for ContextError { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { if self.global { - write!(f, "{}", self.message) - } else { + if self.source.name.is_empty() { + write!(f, "{}", self.message) + } else { + write!(f, "{} ({})", self.message, self.source.name) + } + } else if self.source.name.is_empty() { write!(f, "{} (line {})", self.message, self.line + 1) + } else { + write!( + f, + "{} ({}: line {})", + self.message, + self.source.name, + self.line + 1 + ) } } } @@ -80,6 +117,7 @@ pub struct EvalError { pub line: usize, pub message: String, pub global: bool, + pub source: SourceInfo, } impl std::error::Error for EvalError {} @@ -90,6 +128,7 @@ impl From for EvalError { line: err.line, message: err.message, global: false, + source: SourceInfo::default(), // TODO } } } @@ -97,9 +136,21 @@ impl From for EvalError { impl fmt::Display for EvalError { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { if self.global { - write!(f, "{}", self.message) - } else { + if self.source.name.is_empty() { + write!(f, "{}", self.message) + } else { + write!(f, "{} ({})", self.message, self.source.name) + } + } else if self.source.name.is_empty() { write!(f, "{} (line {})", self.message, self.line + 1) + } else { + write!( + f, + "{} ({}: line {})", + self.message, + self.source.name, + self.line + 1 + ) } } } diff --git a/lib/xixanta/src/lib.rs b/lib/xixanta/src/lib.rs index 0ad901f..95ead9c 100644 --- a/lib/xixanta/src/lib.rs +++ b/lib/xixanta/src/lib.rs @@ -1,6 +1,21 @@ #[macro_use] extern crate lazy_static; +#[derive(Clone, Debug, PartialEq)] +pub struct SourceInfo { + pub working_directory: std::path::PathBuf, + pub name: String, +} + +impl Default for SourceInfo { + fn default() -> Self { + SourceInfo { + working_directory: std::env::current_dir().unwrap().to_path_buf(), + name: "".to_string(), + } + } +} + pub mod assembler; pub mod errors; pub mod node; diff --git a/lib/xixanta/src/mapping.rs b/lib/xixanta/src/mapping.rs index 12b64c8..f86b513 100644 --- a/lib/xixanta/src/mapping.rs +++ b/lib/xixanta/src/mapping.rs @@ -1,4 +1,3 @@ -use crate::errors::EvalError; use crate::object::Bundle; use toml::{Table, Value}; @@ -279,18 +278,14 @@ fn validate_configuration(mappings: &[Mapping]) -> Result<(), String> { /// Perform some sanity checks on the given `mappings`. Only call this function /// after all bundles have been produced. -pub fn validate(mappings: &[Mapping]) -> Result<(), EvalError> { +pub fn validate(mappings: &[Mapping]) -> Result<(), String> { // Guaranteed by `crate::mapping::assert` to be the header. let header: &Segment = mappings.first().unwrap().segments.first().unwrap(); // 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, - }); + return Err(String::from("The header must contain at least 6 bytes")); } // Now check that the length of the evaluated data matches the criteria @@ -310,18 +305,16 @@ pub fn validate(mappings: &[Mapping]) -> Result<(), EvalError> { }); 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, - }); + return Err(format!( + "PRG ROM size is expected to by {} bytes long, but a total of {} bytes were evaluated", + header_prg_rom_size, prg_rom_len + )); } 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, - }); + return Err(format!( + "CHR ROM size is expected to by {} bytes long, but a total of {} bytes were evaluated", + header_chr_rom_size, chr_rom_len + )); } Ok(()) @@ -329,39 +322,23 @@ pub fn validate(mappings: &[Mapping]) -> Result<(), EvalError> { // 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> { +fn parse_header(header: &Segment) -> Result<(usize, usize), String> { let mut header_it = header.bundles.clone().into_iter(); // 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, - }); + return Err(String::from("First byte of the header must be 'N'")); } 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, - }); + return Err(String::from("Second byte of the header must be 'E'")); } 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, - }); + return Err(String::from("Third byte of the header must be 'S'")); } 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, - }); + return Err(String::from( + "Fourth byte of the header must be the MS-DOS termination character", + )); } Ok(( diff --git a/lib/xixanta/src/node.rs b/lib/xixanta/src/node.rs index f919ade..1ec2ed9 100644 --- a/lib/xixanta/src/node.rs +++ b/lib/xixanta/src/node.rs @@ -140,6 +140,7 @@ pub enum ControlType { IncBin, StartRepeat, EndRepeat, + IncludeSource, } impl fmt::Display for ControlType { @@ -160,6 +161,7 @@ impl fmt::Display for ControlType { ControlType::IncBin => write!(f, ".incbin"), ControlType::StartRepeat => write!(f, ".repeat"), ControlType::EndRepeat => write!(f, ".endrepeat"), + ControlType::IncludeSource => write!(f, ".include"), } } } @@ -305,7 +307,7 @@ impl NodeType { } } -/// A Position Node. This is a node on a binary tree which holds a PString as a +/// A Positioned Node. This is a node on a tree which holds a PString as a /// value. The node type determines the actual representation of the value and /// both childs (see the `NodeType` enum). Moreover, out of convenience, a node /// also holds an optional list of arguments, which simplifies arrangements such @@ -330,6 +332,10 @@ pub struct PNode { /// Convenience list used by some node types in order to express a list of /// optional arguments. pub args: Option>, + + /// Index on the list of SourceInfo's maintained by both the parser and the + /// assembler. + pub source: usize, } /// Whether there is a body for a given node and whether it starts or ends it. diff --git a/lib/xixanta/src/object.rs b/lib/xixanta/src/object.rs index 0d8bcc6..77ab8a6 100644 --- a/lib/xixanta/src/object.rs +++ b/lib/xixanta/src/object.rs @@ -1,4 +1,3 @@ -use crate::errors::{ContextError, ContextErrorReason}; use crate::mapping::Mapping; use crate::node::{ControlType, NodeType, PNode, PString}; use crate::opcodes::CONTROL_FUNCTIONS; @@ -174,7 +173,7 @@ impl Context { /// this `id` can be scoped or not, and this function will try to pick the /// variable from the right scope. The value itself will be resolved if the /// type is ObjectType::Address. - pub fn get_variable(&self, id: &PString, mappings: &[Mapping]) -> Result { + pub fn get_variable(&self, id: &PString, mappings: &[Mapping]) -> Result { // First of all, figure out the name of the scope and the real name of // the variable. If this was not scoped at all (None case when trying to // rsplit by the "::" operator), then we assume on the current scope. @@ -183,7 +182,7 @@ impl Context { None => (self.name(), id.value.as_str()), }; - self.get_variable_in_scope(id.line, scope_name, var_name, mappings) + self.get_variable_in_scope(scope_name, var_name, mappings) } // Get the `var_name` variable on the `scope_name` scope (or parents). For @@ -191,11 +190,10 @@ impl Context { // labels, and `line` when producing context errors. fn get_variable_in_scope( &self, - line: usize, scope_name: &str, var_name: &str, mappings: &[Mapping], - ) -> Result { + ) -> Result { // And with that, the only thing left is to find the scope and the // variable in it. match self.map.get(scope_name) { @@ -209,16 +207,11 @@ impl Context { // the scope hierarchy to see if we can fetch it there. For // that, though, we first prepare an error so we return the // original one, not the propagated one (see below). - let err = Err(ContextError { - message: format!( - "could not find variable '{}' in {}", - var_name, - self.to_human_with(scope_name) - ), - line, - reason: ContextErrorReason::UnknownVariable, - global: false, - }); + let err = Err(format!( + "could not find variable '{}' in {}", + var_name, + self.to_human_with(scope_name) + )); // If we are already in the global context and the object // was not found, just leave with an error. @@ -231,9 +224,7 @@ impl Context { // error so it better reflects the original scope where // this was first attempted. let parent = self.parent(scope_name); - if let Ok(object) = - self.get_variable_in_scope(line, parent, var_name, mappings) - { + if let Ok(object) = self.get_variable_in_scope(parent, var_name, mappings) { Ok(object) } else { err @@ -241,12 +232,7 @@ impl Context { } } }, - None => Err(ContextError { - message: format!("did not find scope '{}'", scope_name), - line, - reason: ContextErrorReason::BadScope, - global: false, - }), + None => Err(format!("did not find scope '{}'", scope_name)), } } @@ -255,11 +241,7 @@ impl Context { /// /// NOTE: this function asserts that the given `object` is of type /// ObjectType::Address, otherwise it doesn't make sense to call it. - pub fn resolve_label( - &self, - mappings: &[Mapping], - object: &Object, - ) -> Result { + pub fn resolve_label(&self, mappings: &[Mapping], object: &Object) -> Result { assert!(matches!(object.object_type, ObjectType::Address)); let mut ret = object.clone(); @@ -271,12 +253,7 @@ impl Context { // Avoid weird out of bound references for addresses. if addr > u16::MAX as usize { - return Err(ContextError { - line: 0, - message: format!("address {:x} is out of bounds", addr), - reason: ContextErrorReason::Bounds, - global: true, - }); + return Err(format!("address {:x} is out of bounds", addr)); } let addr_bytes = (addr as u16).to_le_bytes(); @@ -295,23 +272,18 @@ impl Context { id: &PString, object: &Object, overwrite: bool, - ) -> Result<(), ContextError> { + ) -> Result<(), String> { let scope_name = self.name().to_string(); let scope = self.map.get_mut(&scope_name).unwrap(); match scope.get_mut(&id.value) { Some(sc) => { if !overwrite { - return Err(ContextError { - message: format!( - "'{}' already defined in {}: you cannot re-assign names", - id.value, - self.to_human() - ), - line: id.line, - reason: ContextErrorReason::Redefinition, - global: false, - }); + return Err(format!( + "'{}' already defined in {}: you cannot re-assign names", + id.value, + self.to_human() + )); } *sc = object.clone(); } @@ -333,7 +305,7 @@ impl Context { /// Change the current context given a `node`. Returns true if the context /// has changed. - pub fn change_context(&mut self, node: &PNode) -> Result { + pub fn change_context(&mut self, node: &PNode) -> Result { // The parser already guarantees that the control node is // from a function that we already know, so calling `unwrap` // is not dangerous. @@ -406,7 +378,7 @@ impl Context { rel: isize, labels_seen: usize, mappings: &[Mapping], - ) -> Result { + ) -> Result { // Bound check: the given 'rel' parameter has a proper value. assert!( rel < 5 && rel > -5 && rel != 0, @@ -416,12 +388,7 @@ impl Context { // Bound check: you cannot reference a past label that doesn't exist. // This is the programmer's to blame, not on us, so don't assert. if labels_seen == 0 && rel < 0 { - return Err(ContextError { - line: 0, - message: "cannot reference an unknown previous label".to_string(), - reason: ContextErrorReason::Label, - global: false, - }); + return Err("cannot reference an unknown previous label".to_string()); } // Get the labels as referenced in the current context, and also the @@ -437,12 +404,7 @@ impl Context { // Bound check: is the programmer referencing an "out of bounds" label? // If so then it's a mistake on their part. if idx < 0 || idx >= labels.len() as isize { - return Err(ContextError { - line: 0, - message: "cannot reference bogus label (out of bounds)".to_string(), - reason: ContextErrorReason::Label, - global: false, - }); + return Err("cannot reference bogus label (out of bounds)".to_string()); } // Everything should be fine from here on, simply return the bundle that @@ -466,14 +428,9 @@ impl Context { } // Pops out the latest context that was pushed. - fn context_pop(&mut self, id: &PString) -> Result<(), ContextError> { + fn context_pop(&mut self, id: &PString) -> Result<(), String> { if self.stack.is_empty() { - return Err(ContextError { - message: format!("missplaced '{}' statement", id.value), - reason: ContextErrorReason::BadScope, - line: id.line, - global: false, - }); + return Err(format!("missplaced '{}' statement", id.value)); } self.stack.truncate(self.stack.len() - 1); diff --git a/lib/xixanta/src/opcodes.rs b/lib/xixanta/src/opcodes.rs index 6c0aa0d..593d279 100644 --- a/lib/xixanta/src/opcodes.rs +++ b/lib/xixanta/src/opcodes.rs @@ -748,6 +748,7 @@ lazy_static! { functions.insert(String::from(".incbin"), Control { control_type: ControlType::IncBin, has_identifier: None, required_args: Some((1, 1)), touches_context: false }); functions.insert(String::from(".repeat"), Control { control_type: ControlType::StartRepeat, has_identifier: Some(true), required_args: Some((1, 2)), touches_context: true }); functions.insert(String::from(".endrepeat"), Control { control_type: ControlType::EndRepeat, has_identifier: None, required_args: None, touches_context: true }); + functions.insert(String::from(".include"), Control { control_type: ControlType::IncludeSource, has_identifier: None, required_args: Some((1, 1)), touches_context: false }); functions }; diff --git a/lib/xixanta/src/parser.rs b/lib/xixanta/src/parser.rs index 0757505..fc53b08 100644 --- a/lib/xixanta/src/parser.rs +++ b/lib/xixanta/src/parser.rs @@ -1,9 +1,11 @@ use crate::errors::ParseError; -use crate::node::{NodeBodyType, NodeType, OperationType, PNode, PString}; +use crate::node::{ControlType, NodeBodyType, NodeType, OperationType, PNode, PString}; use crate::opcodes::{CONTROL_FUNCTIONS, INSTRUCTIONS}; +use crate::SourceInfo; use rand::distributions::{Alphanumeric, DistString}; use std::cmp::Ordering; use std::io::{self, BufRead, Read}; +use std::path; /// The Parser struct holds basic data for the current parsing session. #[derive(Default)] @@ -35,22 +37,46 @@ pub struct Parser { /// statements (e.g. '.endproc') go with their respective start ones (e.g. /// '.proc'), and they don't close another block. bodies: Vec, + + /// List of sources that have been parsed for this parsing session. Consume + /// it after calling `parse` in order to get the list of sources that have + /// been evaluated. Note that the `source` index on each PNode points to + /// this list. + pub sources: Vec, + + /// The index of the source that explicitely belongs to this session (and + /// not subsequent calls done afterwards). Initialized on `parse`. + current_source: usize, } impl Parser { /// Parse the input from the given `reader`. You can then access the results /// from the `nodes` field. Otherwise, a vector of ParseError's might be /// returned. - pub fn parse(&mut self, reader: impl Read) -> Result<(), Vec> { + pub fn parse(&mut self, reader: impl Read, source: SourceInfo) -> Result<(), Vec> { let mut errors = Vec::new(); + // Push the sources for the parsing session (note that this list might + // be initialized by the caller already). The `current_source` is simply + // the one we push upon initialization. + self.sources.push(SourceInfo { + working_directory: path::absolute(&source.working_directory) + .unwrap_or(source.working_directory), + name: source.name, + }); + self.current_source = self.sources.len() - 1; + + // Make sure that there is a first layer of nodes. self.nodes.push(vec![]); + // This is a line parser (i.e. there cannot be statements which span + // more than one line). Hence, consume the reader line by line and parse + // each one. for line in io::BufReader::new(reader).lines() { match line { Ok(l) => { - if let Err(err) = self.parse_line(l.as_str()) { - errors.push(err); + if let Err(mut err) = self.parse_line(l.as_str()) { + errors.append(&mut err); } } Err(_) => errors.push(self.parser_error("could not get line")), @@ -58,7 +84,7 @@ impl Parser { self.line += 1; } - // Are there any more statements which are begging for a closing + // Are there any more statements which are still waiting for a closing // statement? If so, then there's something wrong. if !self.bodies.is_empty() { errors.push( @@ -91,7 +117,7 @@ impl Parser { } // Parse a single `line` and push the parsed nodes into `self.nodes`. - fn parse_line(&mut self, line: &str) -> Result<(), ParseError> { + fn parse_line(&mut self, line: &str) -> Result<(), Vec> { self.column = 0; self.offset = 0; @@ -132,6 +158,7 @@ impl Parser { left: None, right: None, args: None, + source: self.current_source, }); self.skip_whitespace(l); @@ -147,7 +174,9 @@ impl Parser { self.offset = 0; (id, nt) = self.parse_identifier(l)?; if nt == NodeType::Label { - return Err(self.parser_error("cannot have multiple labels at the same location")); + return Err(self + .parser_error("cannot have multiple labels at the same location") + .into()); } self.skip_whitespace(l); @@ -210,8 +239,9 @@ impl Parser { // there is something wrong (e.g. "Bad:+"). if base_offset != self.offset { return Err(ParseError { - line: self.line, - message: "you cannot have a relative label inside of an identifier".to_string(), + line: self.line, + source: self.sources[self.current_source].clone(), + message: "you cannot have a relative label inside of an identifier".to_string(), }); } @@ -233,6 +263,7 @@ impl Parser { if size > 4 { return Err(ParseError { line: self.line, + source: self.sources[self.current_source].clone(), message: "you can only jump to a maximum of four relative labels".to_string(), }); } @@ -244,6 +275,7 @@ impl Parser { if next == '+' { "forward" } else { "backward" }; return Err(ParseError { line: self.line, + source: self.sources[self.current_source].clone(), message: format!( "{} relative label can only have '{}' characters", msg, next @@ -312,17 +344,17 @@ impl Parser { // Parse the top-level statement as found on the given `line` which has a // leading `id` positioned-string which may be an identifier. - fn parse_statement(&mut self, line: &str, id: PString) -> Result<(), ParseError> { + fn parse_statement(&mut self, line: &str, id: PString) -> Result<(), Vec> { // There are only two top-level statements: instructions and // assignments. Other kinds of expressions can also be used in the // middle of assignments or instructions, and so they have to be handled // as common expressions. Whether expressions make sense at the // different levels is something to be figured out by the assembler. match INSTRUCTIONS.get(&id.value) { - Some(_) => self.parse_instruction(line, id), + Some(_) => Ok(self.parse_instruction(line, id)?), None => { if line.contains('=') { - self.parse_assignment(line, id) + Ok(self.parse_assignment(line, id)?) } else { self.parse_other(line, id) } @@ -426,6 +458,7 @@ impl Parser { left, right, args: None, + source: self.current_source, })); // Do we have anything as a right arm? @@ -454,6 +487,7 @@ impl Parser { left, right, args: None, + source: self.current_source, }); Ok(()) @@ -494,6 +528,7 @@ impl Parser { left: Some(Box::new(left)), right: None, args: None, + source: self.current_source, }); Ok(()) @@ -501,15 +536,42 @@ impl Parser { // Parse statements which are neither an instruction nor an assignment. This // includes stuff like control statements. - fn parse_other(&mut self, line: &str, id: PString) -> Result<(), ParseError> { + fn parse_other(&mut self, line: &str, id: PString) -> Result<(), Vec> { + // The main job of this function is to parse the expression and + // afterwards deal with corner cases. So, let's first just parse this. let node = self.parse_expression_with_identifier(id, line)?; - let node_type = node.node_type.clone(); - let body_type = node.body_type(); + + // If this was an .include statement we have to handle it now as this is + // not a regular statement but more like a preprocessor statement which + // can lead to inner parsing sessions. + if matches!( + &node.node_type, + NodeType::Control(ControlType::IncludeSource) + ) { + // Validate that it's a top layer statement. + if self.nodes.len() > 1 { + return Err(ParseError { + line: node.value.line, + message: ".include statement cannot be inside of a code block".to_string(), + source: self.sources[self.current_source].clone(), + } + .into()); + } + + // Before including all the nodes from the referenced file, add the + // .include statement. This should be ignored by the assembler, but + // maybe other tools want to make use of it. + self.nodes.last_mut().unwrap().push(node.clone()); + + // And append nodes from the referenced source. + return self.include_source(&node); + } // The given statement might be a start/end one (e.g. '.proc' and // '.endproc'). In these cases there are some things to handle besides // pushing the node into the current level of nodes. - match body_type { + let node_type = node.node_type.clone(); + match node.body_type() { NodeBodyType::Starts => { self.bodies.push(node_type.closing_type().unwrap()); self.nodes.last_mut().unwrap().push(node); @@ -521,15 +583,18 @@ impl Parser { let expected_close = match self.bodies.pop() { Some(ec) => ec, None => { - return Err( - self.parser_error(format!("unexpected '{}'", node_type).as_str()) - ) + return Err(self + .parser_error(format!("unexpected '{}'", node_type).as_str()) + .into()) } }; if node_type != expected_close { - return Err(self.parser_error( - format!("expecting '{}', found '{}'", expected_close, node_type).as_str(), - )); + return Err(self + .parser_error( + format!("expecting '{}', found '{}'", expected_close, node_type) + .as_str(), + ) + .into()); } // Note that empty bodies are possible. This is left @@ -542,6 +607,7 @@ impl Parser { left: None, right: None, args: Some(nodes), + source: self.current_source, })); self.nodes.last_mut().unwrap().push(node); } @@ -551,6 +617,98 @@ impl Parser { Ok(()) } + // Consume the given `node` by assuming it's an `.include` statement. This + // will in turn produce a new parsing session for the given file if possible + // and push the parsed nodes from it to our current list. + fn include_source(&mut self, node: &PNode) -> Result<(), Vec> { + // First of all, parse the string as it was given and join it with our + // working directory. This way we construct the absolute path for the + // given file. + let file_path = self.fetch_path_from(node.args.as_ref().unwrap().first().unwrap())?; + let Some(current_source) = self.sources.get(self.current_source) else { + panic!("mismatch between the number of sources and the current one"); + }; + let abs_file = current_source.working_directory.join(file_path); + + // Validate that this is really a path that points to a file. + let path = std::path::Path::new(&abs_file); + if !path.is_file() { + return Err(ParseError { + line: node.value.line, + source: current_source.clone(), + message: "expecting a file ({})".to_string(), + } + .into()); + } + + // And open the file. This is the object to be passed as a reader for + // the recursive `parse` call, but it also allows us to construct the + // SourceInfo for the next session because we need to point to its + // parent in the file system. + let file = match std::fs::File::open(path) { + Ok(f) => f, + Err(e) => { + return Err(ParseError { + line: node.value.line, + source: current_source.clone(), + message: format!("could not open source file: {}", e), + } + .into()) + } + }; + let Some(parent) = path.parent() else { + return Err(ParseError { + line: node.value.line, + source: current_source.clone(), + message: "could not find out the parent directory for file".to_string(), + } + .into()); + }; + + // Set up a new parsing session, push the current sources, and call + // `parse`. + let mut parser = Parser { + sources: self.sources.clone(), + ..Default::default() + }; + parser.parse( + file, + SourceInfo { + working_directory: parent.to_path_buf(), + name: path.file_name().unwrap().to_str().unwrap().to_string(), + }, + )?; + + // If everything was fine on the previous parsing session, append the + // nodes from it and also grab its sources, since the previous parsing + // session might have done further parsing iterations of its own. + self.nodes.last_mut().unwrap().append(&mut parser.nodes()); + self.sources = parser.sources.clone(); + + Ok(()) + } + + // Returns a string containing the path being referenced in the given node. + // The path is assumed to be available directly inside of the value of the + // PNode. + fn fetch_path_from<'a>(&mut self, node: &'a PNode) -> Result<&'a str, ParseError> { + let value = &node.value.value; + + // Validate the path literal. + if value.len() < 3 || !value.starts_with('"') || !value.ends_with('"') { + return Err(ParseError { + line: node.value.line, + source: self.sources[self.current_source].clone(), + message: format!( + "path has to be written inside of double quotes ('{}' given instead)", + value, + ), + }); + } + + Ok(value[1..value.len() - 1].trim()) + } + // Parse any possible arguments for the given `line`. The offset is supposed // to be at a point where arguments might appear, either between parens or // not. @@ -765,6 +923,7 @@ impl Parser { left: None, right: Some(Box::new(right)), args: None, + source: self.current_source, }); } @@ -854,6 +1013,7 @@ impl Parser { if next == '#' || (start != '#' && (next == '$' || next == '%')) { return Err(ParseError { line: id.line, + source: self.sources[self.current_source].clone(), message: "bad literal syntax".to_string(), }); } @@ -880,6 +1040,7 @@ impl Parser { left: None, right: None, args: None, + source: self.current_source, })); // Fetch a trimmed version of the string that comes after the @@ -905,6 +1066,7 @@ impl Parser { left, right: Some(Box::new(right)), args: None, + source: self.current_source, }); } @@ -919,6 +1081,7 @@ impl Parser { left: None, right: None, args: if args.is_empty() { None } else { Some(args) }, + source: self.current_source, }); } @@ -931,6 +1094,7 @@ impl Parser { left: None, right: None, args: None, + source: self.current_source, }) } } @@ -975,6 +1139,7 @@ impl Parser { left: None, right: None, args: None, + source: self.current_source, })); } @@ -997,6 +1162,7 @@ impl Parser { left, right: None, args: if args.is_empty() { None } else { Some(args) }, + source: self.current_source, }) } @@ -1020,6 +1186,7 @@ impl Parser { if c.is_whitespace() { return Err(ParseError { line: id.line, + source: self.sources[self.current_source].clone(), message: "numeric literals cannot have white spaces".to_string(), }); } @@ -1035,6 +1202,7 @@ impl Parser { left: Some(Box::new(left)), right: None, args: None, + source: self.current_source, }) } @@ -1073,9 +1241,11 @@ impl Parser { left: None, right: None, args: None, + source: self.current_source, })), right: None, args: None, + source: self.current_source, }) } @@ -1083,6 +1253,7 @@ impl Parser { fn parser_error(&self, msg: &str) -> ParseError { ParseError { message: String::from(msg), + source: self.sources[self.current_source].clone(), line: self.line, } } @@ -1122,7 +1293,7 @@ mod tests { use crate::node::ControlType; fn assert_one_valid(parser: &mut Parser, line: &str) { - assert!(parser.parse(line.as_bytes()).is_ok()); + assert!(parser.parse(line.as_bytes(), SourceInfo::default()).is_ok()); assert!(parser.nodes.len() == 1); } @@ -1140,14 +1311,16 @@ mod tests { #[test] fn empty_line() { let mut parser = Parser::default(); - assert!(parser.parse("".as_bytes()).is_ok()); + assert!(parser.parse("".as_bytes(), SourceInfo::default()).is_ok()); assert_eq!(parser.nodes.last().unwrap().len(), 0); } #[test] fn spaced_line() { let mut parser = Parser::default(); - assert!(parser.parse(" ".as_bytes()).is_ok()); + assert!(parser + .parse(" ".as_bytes(), SourceInfo::default()) + .is_ok()); assert_eq!(parser.nodes.last().unwrap().len(), 0); } @@ -1155,7 +1328,7 @@ mod tests { fn just_a_comment_line() { for line in vec![";; This is a comment", " ;; Comment"].into_iter() { let mut parser = Parser::default(); - assert!(parser.parse(line.as_bytes()).is_ok()); + assert!(parser.parse(line.as_bytes(), SourceInfo::default()).is_ok()); assert_eq!(parser.nodes.last().unwrap().len(), 0); } } @@ -1165,7 +1338,7 @@ mod tests { #[test] fn anonymous_label() { let mut parser = Parser::default(); - assert!(parser.parse(":".as_bytes()).is_ok()); + assert!(parser.parse(":".as_bytes(), SourceInfo::default()).is_ok()); let mut nodes = parser.nodes.last().unwrap(); assert_eq!(nodes.len(), 1); @@ -1174,7 +1347,9 @@ mod tests { assert_eq!(nodes.first().unwrap().value.end, 0); parser = Parser::default(); - assert!(parser.parse(" :".as_bytes()).is_ok()); + assert!(parser + .parse(" :".as_bytes(), SourceInfo::default()) + .is_ok()); nodes = parser.nodes.last().unwrap(); assert_eq!(nodes.len(), 1); @@ -1186,7 +1361,9 @@ mod tests { #[test] fn named_label() { let mut parser = Parser::default(); - assert!(parser.parse("label:".as_bytes()).is_ok()); + assert!(parser + .parse("label:".as_bytes(), SourceInfo::default()) + .is_ok()); let mut nodes = parser.nodes.last().unwrap(); assert_eq!(nodes.len(), 1); @@ -1195,7 +1372,9 @@ mod tests { assert_eq!(nodes.first().unwrap().value.end, 5); parser = Parser::default(); - assert!(parser.parse(" label:".as_bytes()).is_ok()); + assert!(parser + .parse(" label:".as_bytes(), SourceInfo::default()) + .is_ok()); nodes = parser.nodes.last().unwrap(); assert_eq!(nodes.len(), 1); @@ -1209,7 +1388,7 @@ mod tests { let line = "label: dex"; let mut parser = Parser::default(); - assert!(parser.parse(line.as_bytes()).is_ok()); + assert!(parser.parse(line.as_bytes(), SourceInfo::default()).is_ok()); let nodes = parser.nodes(); assert_eq!(nodes.len(), 2); @@ -1229,7 +1408,7 @@ mod tests { fn parse_pound_literal() { for line in vec!["#20", " #20 ", " #20 ; Comment", " label: #20"].into_iter() { let mut parser = Parser::default(); - assert!(parser.parse(line.as_bytes()).is_ok()); + assert!(parser.parse(line.as_bytes(), SourceInfo::default()).is_ok()); let node = parser.nodes.last().unwrap().last().unwrap(); assert_eq!(node.node_type, NodeType::Literal); @@ -1247,7 +1426,7 @@ mod tests { fn parse_compound_literal() { let line = "#$20"; let mut parser = Parser::default(); - assert!(parser.parse(line.as_bytes()).is_ok()); + assert!(parser.parse(line.as_bytes(), SourceInfo::default()).is_ok()); let node = parser.nodes.last().unwrap().last().unwrap(); assert_eq!(node.node_type, NodeType::Literal); @@ -1273,7 +1452,7 @@ mod tests { fn parse_variable_in_literal() { let line = "#Variable"; let mut parser = Parser::default(); - assert!(parser.parse(line.as_bytes()).is_ok()); + assert!(parser.parse(line.as_bytes(), SourceInfo::default()).is_ok()); let node = parser.nodes.last().unwrap().last().unwrap(); assert_eq!(node.node_type, NodeType::Literal); @@ -1293,7 +1472,7 @@ mod tests { fn parse_paren_expression() { let line = "ldx #(Variable)"; let mut parser = Parser::default(); - assert!(parser.parse(line.as_bytes()).is_ok()); + assert!(parser.parse(line.as_bytes(), SourceInfo::default()).is_ok()); let instr = parser.nodes.last().unwrap().last().unwrap(); assert_eq!(instr.node_type, NodeType::Instruction); @@ -1318,14 +1497,18 @@ mod tests { fn parse_bad_literals() { for line in vec!["#", "#%", "$"].into_iter() { let mut parser = Parser::default(); - let err = parser.parse(line.as_bytes()).unwrap_err(); + let err = parser + .parse(line.as_bytes(), SourceInfo::default()) + .unwrap_err(); assert_eq!(err.first().unwrap().message, "invalid identifier"); } for line in vec!["$ 2", "#% 2", "# 2"].into_iter() { let mut parser = Parser::default(); - let err = parser.parse(line.as_bytes()).unwrap_err(); + let err = parser + .parse(line.as_bytes(), SourceInfo::default()) + .unwrap_err(); assert_eq!( err.first().unwrap().message, @@ -1335,7 +1518,9 @@ mod tests { for line in vec!["##2", "#$$2", "$$2", "#$#$2", "#$#2", "###"].into_iter() { let mut parser = Parser::default(); - let err = parser.parse(line.as_bytes()).unwrap_err(); + let err = parser + .parse(line.as_bytes(), SourceInfo::default()) + .unwrap_err(); assert_eq!(err.first().unwrap().message, "bad literal syntax"); } @@ -1355,7 +1540,7 @@ mod tests { .into_iter() { let mut parser = Parser::default(); - assert!(parser.parse(line.as_bytes()).is_ok()); + assert!(parser.parse(line.as_bytes(), SourceInfo::default()).is_ok()); let node = parser.nodes.last().unwrap().last().unwrap(); assert_node(node, NodeType::Instruction, line, "dex"); @@ -1441,7 +1626,7 @@ mod tests { .into_iter() { let mut parser = Parser::default(); - assert!(parser.parse(line.as_bytes()).is_ok()); + assert!(parser.parse(line.as_bytes(), SourceInfo::default()).is_ok()); let node = parser.nodes.last().unwrap().last().unwrap(); assert_node(node, NodeType::Instruction, line, "inc"); @@ -1468,7 +1653,7 @@ mod tests { .into_iter() { let mut parser = Parser::default(); - assert!(parser.parse(line.as_bytes()).is_ok()); + assert!(parser.parse(line.as_bytes(), SourceInfo::default()).is_ok()); let node = parser.nodes.last().unwrap().last().unwrap(); assert_node(node, NodeType::Instruction, line, "lda"); @@ -1493,7 +1678,7 @@ mod tests { .into_iter() { let mut parser = Parser::default(); - assert!(parser.parse(line.as_bytes()).is_ok()); + assert!(parser.parse(line.as_bytes(), SourceInfo::default()).is_ok()); let node = parser.nodes.last().unwrap().last().unwrap(); assert_node(node, NodeType::Instruction, line, "lda"); @@ -1510,7 +1695,9 @@ mod tests { fn bad_indirect_addressing_x() { let mut parser = Parser::default(); - let err = parser.parse("lda (Variable, x), y".as_bytes()).unwrap_err(); + let err = parser + .parse("lda (Variable, x), y".as_bytes(), SourceInfo::default()) + .unwrap_err(); assert_eq!(err.first().unwrap().message, "bad indirect addressing"); } @@ -1518,7 +1705,7 @@ mod tests { fn indirect_addressing_y() { for line in vec!["lda ($20), y"].into_iter() { let mut parser = Parser::default(); - assert!(parser.parse(line.as_bytes()).is_ok()); + assert!(parser.parse(line.as_bytes(), SourceInfo::default()).is_ok()); let node = parser.nodes.last().unwrap().last().unwrap(); assert_node(node, NodeType::Instruction, line, "lda"); @@ -1537,7 +1724,7 @@ mod tests { fn variable_in_instruction() { let line = "lda Variable, x"; let mut parser = Parser::default(); - assert!(parser.parse(line.as_bytes()).is_ok()); + assert!(parser.parse(line.as_bytes(), SourceInfo::default()).is_ok()); let node = parser.nodes.last().unwrap().last().unwrap(); assert_node(node, NodeType::Instruction, line, "lda"); @@ -1556,7 +1743,7 @@ mod tests { fn variable_literal_in_instruction() { let line = "lda #Variable, x"; let mut parser = Parser::default(); - assert!(parser.parse(line.as_bytes()).is_ok()); + assert!(parser.parse(line.as_bytes(), SourceInfo::default()).is_ok()); let node = parser.nodes.last().unwrap().last().unwrap(); assert_node(node, NodeType::Instruction, line, "lda"); @@ -1576,7 +1763,7 @@ mod tests { for var in vec!["Scope::Variable", "Scope::Inner::Variable"].into_iter() { let line = format!("lda #{}", var); let mut parser = Parser::default(); - assert!(parser.parse(line.as_bytes()).is_ok()); + assert!(parser.parse(line.as_bytes(), SourceInfo::default()).is_ok()); let node = parser.nodes.last().unwrap().last().unwrap(); assert_node(node, NodeType::Instruction, line.as_str(), "lda"); @@ -1596,7 +1783,9 @@ mod tests { fn bad_variable_scoping() { let mut parser = Parser::default(); - let err = parser.parse("adc #One:Variable".as_bytes()).unwrap_err(); + let err = parser + .parse("adc #One:Variable".as_bytes(), SourceInfo::default()) + .unwrap_err(); assert_eq!( err.first().unwrap().message, "not expecting a label defined here" @@ -1607,7 +1796,9 @@ mod tests { fn reserved_mnemonic_name() { let mut parser = Parser::default(); - let err = parser.parse("lda = $10".as_bytes()).unwrap_err(); + let err = parser + .parse("lda = $10".as_bytes(), SourceInfo::default()) + .unwrap_err(); assert_eq!( err.first().unwrap().message, "cannot use the reserved mnemonic 'lda' as a variable name" @@ -1619,7 +1810,7 @@ mod tests { for label in vec![":+", ":++", ":+++ ", ":++++", ":-", ":--", ":---", ":----"].into_iter() { let line = format!("jmp {}", label); let mut parser = Parser::default(); - assert!(parser.parse(line.as_bytes()).is_ok()); + assert!(parser.parse(line.as_bytes(), SourceInfo::default()).is_ok()); let node = parser.nodes.last().unwrap().last().unwrap(); assert_node(node, NodeType::Instruction, line.as_str(), "jmp"); @@ -1640,28 +1831,36 @@ mod tests { let mut parser = Parser::default(); let mut line = "jmp :+++++"; - let mut err = parser.parse(line.as_bytes()).unwrap_err(); + let mut err = parser + .parse(line.as_bytes(), SourceInfo::default()) + .unwrap_err(); assert_eq!( err.first().unwrap().message, "you can only jump to a maximum of four relative labels" ); line = "jmp :+-"; - err = parser.parse(line.as_bytes()).unwrap_err(); + err = parser + .parse(line.as_bytes(), SourceInfo::default()) + .unwrap_err(); assert_eq!( err.first().unwrap().message, "forward relative label can only have '+' characters" ); line = "jmp :-+-"; - err = parser.parse(line.as_bytes()).unwrap_err(); + err = parser + .parse(line.as_bytes(), SourceInfo::default()) + .unwrap_err(); assert_eq!( err.first().unwrap().message, "backward relative label can only have '-' characters" ); line = "jmp Identifier:++"; - err = parser.parse(line.as_bytes()).unwrap_err(); + err = parser + .parse(line.as_bytes(), SourceInfo::default()) + .unwrap_err(); assert_eq!( err.first().unwrap().message, "you cannot have a relative label inside of an identifier" @@ -1674,22 +1873,30 @@ mod tests { fn bad_assignments() { let mut parser = Parser::default(); - let mut err = parser.parse("abc = $10".as_bytes()).unwrap_err(); + let mut err = parser + .parse("abc = $10".as_bytes(), SourceInfo::default()) + .unwrap_err(); assert_eq!( err.first().unwrap().message, "cannot use names which are valid hexadecimal values such as 'abc'" ); parser = Parser::default(); - err = parser.parse("var =".as_bytes()).unwrap_err(); + err = parser + .parse("var =".as_bytes(), SourceInfo::default()) + .unwrap_err(); assert_eq!(err.first().unwrap().message, "incomplete assignment"); parser = Parser::default(); - err = parser.parse("var = ".as_bytes()).unwrap_err(); + err = parser + .parse("var = ".as_bytes(), SourceInfo::default()) + .unwrap_err(); assert_eq!(err.first().unwrap().message, "incomplete assignment"); parser = Parser::default(); - err = parser.parse("var = ; Comment".as_bytes()).unwrap_err(); + err = parser + .parse("var = ; Comment".as_bytes(), SourceInfo::default()) + .unwrap_err(); assert_eq!(err.first().unwrap().message, "incomplete assignment"); } @@ -1699,7 +1906,7 @@ mod tests { fn constant_expression_test() { let line = "ldx #(4 * NUM_SPRITES)"; let mut parser = Parser::default(); - assert!(parser.parse(line.as_bytes()).is_ok()); + assert!(parser.parse(line.as_bytes(), SourceInfo::default()).is_ok()); let node = parser.nodes.last().unwrap().last().unwrap(); assert_node(node, NodeType::Instruction, line, "ldx"); @@ -1724,7 +1931,7 @@ mod tests { fn unary_operator_test() { let line = "ldx # bit 0; bit 7 -> Carry + bcc @joypad_unsafe_read_x_loop + rts + +;;; +;; Safely read the first controller via a re-read algorithm. +joypad_read: + ldx #$00 + + ;; NOTE: uncomment these two lines to also read safely the second + ;; controller. + ;; + ;; jsr joypad_read_x + ;; inx + + ;; NOTE: fallthrough + +;;; +;; Safely read via a re-read algorithm the joypad as indexed by the X register +;; (0 for controller 1; 1 for controller 2). +joypad_read_x: + jsr joypad_unsafe_read_x + + ;; The main idea around a re-read algorithm is that you read the controller + ;; "unsafely" once, then you do it again and compare both reads. If they + ;; were the same then we are on the safe side. Otherwise we would need to + ;; loop until we get two identical reads. This sounds bad but in practice + ;; it's not so much (and hey, if it worked for Super Mario Bros. 3, it + ;; should work for us too :P). Otherwise there is the algorithm via OAM DMA, + ;; but it sure is tricky. +@joypad_read_x_reread: + lda Joypad::m_buttons1, x + pha + jsr joypad_unsafe_read_x + pla + cmp Joypad::m_buttons1, x + bne @joypad_read_x_reread + + rts -- cgit v1.2.3