aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--crates/nasm/src/main.rs22
-rw-r--r--lib/xixanta/fuzz/fuzz_targets/fuzz_target_assembler.rs6
-rw-r--r--lib/xixanta/fuzz/fuzz_targets/fuzz_target_parser.rs2
-rw-r--r--lib/xixanta/src/assembler.rs309
-rw-r--r--lib/xixanta/src/errors.rs61
-rw-r--r--lib/xixanta/src/lib.rs15
-rw-r--r--lib/xixanta/src/mapping.rs57
-rw-r--r--lib/xixanta/src/node.rs8
-rw-r--r--lib/xixanta/src/object.rs91
-rw-r--r--lib/xixanta/src/opcodes.rs1
-rw-r--r--lib/xixanta/src/parser.rs396
-rwxr-xr-xscripts/test-e2e.sh3
-rw-r--r--tests/assets/diskun.chrbin0 -> 8192 bytes
-rw-r--r--tests/src/flicker/flicker.s280
-rw-r--r--tests/src/shared/diskun.s96
-rw-r--r--tests/src/shared/joypad.s86
16 files changed, 1141 insertions, 292 deletions
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<dyn Read>;
- 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: "<stdin>".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<Error>,
- // 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<PathBuf>,
+ // TODO
+ sources: Vec<SourceInfo>,
}
#[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<Mapping>,
- 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<Error>> {
@@ -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<Bundle, EvalError> {
- match self.context.get_variable(id, &self.mappings) {
+ fn evaluate_variable(&mut self, node: &PNode) -> Result<Bundle, EvalError> {
+ 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<ParseError> for Vec<Error> {
+ fn from(err: ParseError) -> Self {
+ vec![Error::Parse(err)]
+ }
+}
+
+impl From<ParseError> for Vec<ParseError> {
+ fn from(err: ParseError) -> Self {
+ vec![err]
+ }
+}
+
impl From<ContextError> for Vec<Error> {
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<ContextError> for EvalError {
line: err.line,
message: err.message,
global: false,
+ source: SourceInfo::default(), // TODO
}
}
}
@@ -97,9 +136,21 @@ impl From<ContextError> 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<Vec<PNode>>,
+
+ /// 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<Object, ContextError> {
+ pub fn get_variable(&self, id: &PString, mappings: &[Mapping]) -> Result<Object, String> {
// 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<Object, ContextError> {
+ ) -> Result<Object, String> {
// 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<Object, ContextError> {
+ pub fn resolve_label(&self, mappings: &[Mapping], object: &Object) -> Result<Object, String> {
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<bool, ContextError> {
+ pub fn change_context(&mut self, node: &PNode) -> Result<bool, String> {
// 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<Object, ContextError> {
+ ) -> Result<Object, String> {
// 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<NodeType>,
+
+ /// 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<SourceInfo>,
+
+ /// 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<ParseError>> {
+ pub fn parse(&mut self, reader: impl Read, source: SourceInfo) -> Result<(), Vec<ParseError>> {
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<ParseError>> {
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<ParseError>> {
// 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<ParseError>> {
+ // 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<ParseError>> {
+ // 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 #<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");
@@ -1751,7 +1958,7 @@ mod tests {
fn parse_control_no_args() {
for line in vec![".byte", " .byte", " label: .byte ; 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());
let node = parser.nodes.last().unwrap().last().unwrap();
assert_node(node, NodeType::Control(ControlType::Byte), line, ".byte");
@@ -1773,7 +1980,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(
@@ -1803,7 +2010,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::Control(ControlType::Byte), line, ".byte");
@@ -1821,7 +2028,7 @@ mod tests {
fn parse_byte_with_character_literals() {
let line = ".byte 'N', 'E', 'S', $1A";
let mut parser = Parser::default();
- assert!(parser.parse(line.as_bytes()).is_ok());
+ assert!(parser.parse(line.as_bytes(), SourceInfo::default()).is_ok());
let args = parser
.nodes
@@ -1873,7 +2080,7 @@ mod tests {
let real = String::from(line) + "\n.endscope";
let mut parser = Parser::default();
- assert!(parser.parse(real.as_bytes()).is_ok());
+ assert!(parser.parse(real.as_bytes(), SourceInfo::default()).is_ok());
let nodes = parser.nodes();
let node = &nodes[nodes.len() - 2];
@@ -1907,7 +2114,7 @@ mod tests {
let real = String::from(line) + "\n.endmacro";
let mut parser = Parser::default();
- assert!(parser.parse(real.as_bytes()).is_ok());
+ assert!(parser.parse(real.as_bytes(), SourceInfo::default()).is_ok());
let nodes = parser.nodes();
let node = &nodes[nodes.len() - 2];
@@ -1944,7 +2151,7 @@ mod tests {
let real = String::from(line) + "\n.endmacro";
let mut parser = Parser::default();
- assert!(parser.parse(real.as_bytes()).is_ok());
+ assert!(parser.parse(real.as_bytes(), SourceInfo::default()).is_ok());
let nodes = parser.nodes();
let node = &nodes[nodes.len() - 2];
@@ -1969,7 +2176,9 @@ mod tests {
#[test]
fn parse_control_unclosed() {
let mut parser = Parser::default();
- let err = parser.parse(".macro MACRO".as_bytes()).unwrap_err();
+ let err = parser
+ .parse(".macro MACRO".as_bytes(), SourceInfo::default())
+ .unwrap_err();
assert_eq!(
err.first().unwrap().message,
@@ -1980,7 +2189,9 @@ mod tests {
#[test]
fn parse_control_too_many_closes() {
let mut parser = Parser::default();
- let err = parser.parse(".endmacro".as_bytes()).unwrap_err();
+ let err = parser
+ .parse(".endmacro".as_bytes(), SourceInfo::default())
+ .unwrap_err();
assert_eq!(
err.first().unwrap().message,
@@ -1995,7 +2206,9 @@ mod tests {
.endscope
.endmacro"#;
let mut parser = Parser::default();
- let err = parser.parse(code.as_bytes()).unwrap_err();
+ let err = parser
+ .parse(code.as_bytes(), SourceInfo::default())
+ .unwrap_err();
assert_eq!(
err.first().unwrap().message,
@@ -2014,7 +2227,7 @@ nop
inc $20
.endmacro"#;
let mut parser = Parser::default();
- assert!(parser.parse(code.as_bytes()).is_ok());
+ assert!(parser.parse(code.as_bytes(), SourceInfo::default()).is_ok());
let nodes = parser.nodes();
assert_eq!(nodes.len(), 2); // .macro and .endmacro
@@ -2047,7 +2260,9 @@ inc $20
fn parse_control_bad_number_args() {
for line in vec![".hibyte", ".hibyte($20, $22)"].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,
@@ -2060,7 +2275,7 @@ inc $20
fn parse_control_in_instructions() {
for line in vec!["lda #.hibyte($2010)", " label: lda #.hibyte $2010 "].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");
@@ -2097,7 +2312,7 @@ inc $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_node(node, NodeType::Instruction, line, "lda");
@@ -2141,7 +2356,7 @@ inc $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_node(node, NodeType::Instruction, line, "lda");
@@ -2186,7 +2401,7 @@ inc $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_node(node, NodeType::Assignment, line, "lala");
@@ -2217,7 +2432,9 @@ inc $20
#[test]
fn parse_repeat_control() {
let mut parser = Parser::default();
- let err = parser.parse(".repeat\n.endrepeat".as_bytes()).unwrap_err();
+ let err = parser
+ .parse(".repeat\n.endrepeat".as_bytes(), SourceInfo::default())
+ .unwrap_err();
assert_eq!(
err.first().unwrap().message,
"wrong number of arguments for function '.repeat'"
@@ -2225,7 +2442,10 @@ inc $20
let mut parser = Parser::default();
let err = parser
- .parse(".repeat 1, 2, 3\n.endrepeat".as_bytes())
+ .parse(
+ ".repeat 1, 2, 3\n.endrepeat".as_bytes(),
+ SourceInfo::default(),
+ )
.unwrap_err();
assert_eq!(
err.first().unwrap().message,
@@ -2237,7 +2457,7 @@ inc $20
parser = Parser::default();
let mut line = ".repeat 2\n.endrepeat";
- assert!(parser.parse(line.as_bytes()).is_ok());
+ assert!(parser.parse(line.as_bytes(), SourceInfo::default()).is_ok());
let mut control = parser.nodes.last().unwrap().first().unwrap();
assert_node(
control,
@@ -2263,7 +2483,7 @@ inc $20
parser = Parser::default();
line = ".repeat 2, I\n.endrepeat";
- assert!(parser.parse(line.as_bytes()).is_ok());
+ assert!(parser.parse(line.as_bytes(), SourceInfo::default()).is_ok());
control = parser.nodes.last().unwrap().first().unwrap();
assert_node(
control,
@@ -2289,11 +2509,15 @@ inc $20
#[test]
fn parse_unknown_control() {
let mut parser = Parser::default();
- let mut err = parser.parse(".".as_bytes()).unwrap_err();
+ let mut err = parser
+ .parse(".".as_bytes(), SourceInfo::default())
+ .unwrap_err();
assert_eq!(err.first().unwrap().message, "unknown function '.'");
parser = Parser::default();
- err = parser.parse(".whatever".as_bytes()).unwrap_err();
+ err = parser
+ .parse(".whatever".as_bytes(), SourceInfo::default())
+ .unwrap_err();
assert_eq!(err.first().unwrap().message, "unknown function '.whatever'");
}
@@ -2309,7 +2533,7 @@ inc $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_node(node, NodeType::Value, line, "MACRO_CALL");
@@ -2331,7 +2555,7 @@ inc $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_node(node, NodeType::Call, line, "MACRO_CALL");
@@ -2352,7 +2576,7 @@ inc $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_node(node, NodeType::Call, line, "MACRO_CALL");
@@ -2376,7 +2600,7 @@ inc $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_node(node, NodeType::Call, line, "MACRO_CALL");
diff --git a/scripts/test-e2e.sh b/scripts/test-e2e.sh
index 89e787c..3f8729c 100755
--- a/scripts/test-e2e.sh
+++ b/scripts/test-e2e.sh
@@ -12,3 +12,6 @@ diff tests/out/sprite.nes tests/bin/sprite.nes
./target/debug/nasm -c unrom -o tests/out/chr-ram.nes tests/src/chr-ram.s
diff tests/out/chr-ram.nes tests/bin/chr-ram.nes
+
+./target/debug/nasm -c nrom -Werror -o tests/out/flicker.nes tests/src/flicker/flicker.s
+diff tests/out/flicker.nes tests/bin/flicker.nes
diff --git a/tests/assets/diskun.chr b/tests/assets/diskun.chr
new file mode 100644
index 0000000..3ed0dc4
--- /dev/null
+++ b/tests/assets/diskun.chr
Binary files differ
diff --git a/tests/src/flicker/flicker.s b/tests/src/flicker/flicker.s
new file mode 100644
index 0000000..82dcfb7
--- /dev/null
+++ b/tests/src/flicker/flicker.s
@@ -0,0 +1,280 @@
+;;;
+;; This is a simple example of sprite cycling, so sprites don't disappear on a
+;; scanline overflow. This example is extremely simple, and on a real game you'd
+;; take into account lots of things that would make this process less tedious
+;; and less cycle consuming.
+;; Taken from https://github.com/mssola/code.nes.
+
+.segment "HEADER"
+ .byte 'N', 'E', 'S', $1A
+
+ .byte $02, $01
+
+ .byte $00
+ .byte $00
+
+.segment "VECTORS"
+ .addr nmi, reset, irq
+
+.segment "CHARS"
+.incbin "../../assets/diskun.chr"
+
+.segment "CODE"
+
+.include "../shared/diskun.s"
+
+.proc main
+ jsr Diskun::init_palettes
+ jsr init_sprites
+
+ cli
+ lda #%10010000
+ sta $2000 ; PPUCTRL
+ lda #%00011110
+ sta $2001 ; PPUMASK
+
+@main_game_loop:
+ ;; NOTE: the logic is pretty simple: read the pad, move the player
+ ;; accordingly, and apply the flickering effect.
+ jsr joypad_read
+ jsr Diskun::update
+
+ ;; NOTE: comment this `jsr` out if you want to see what happens if no
+ ;; flickering effect is applied (spoiler alert: the last character will
+ ;; suddenly disappear :P).
+ ;;
+ ;; NOTE: this function is called mindlessly. In a real game you'd try to
+ ;; detect scanline overflows, or you will try to be more careful with sprite
+ ;; priorities and apply the flickering more carefully. Here it's applied for
+ ;; (almost) every sprite on each frame and we roll with it.
+ jsr apply_flicker
+
+ lda #%10000000
+ ora $20
+ sta $20
+@wait_for_render:
+ bit $20
+ bmi @wait_for_render
+
+ jmp @main_game_loop
+.endproc
+
+
+;;;
+;; NOTE: this is the actual meat of the example :D
+.proc apply_flicker
+ ;; We will store a 16-bit pointer to the first sprite that can be flickered.
+ ;; In this case, we will point to the first sprite from the first NPC. That
+ ;; is, we suppose that, for whatever reason, we don't want to apply the
+ ;; flickering effect for the player.
+ lda #$10
+ sta $40
+ lda #$02
+ sta $41
+
+ ;;;
+ ;; The algorithm is really straight-forward here: we will simply cycle the
+ ;; sprites starting from the one being pointed at $40-$41 until the last
+ ;; sprite. The main idea is that the OAM will show sprites on a scanline in
+ ;; order. Hence, those sprites that are left beyond the eigth on a scanline
+ ;; will suddenly disappear. Flickering is a technique by which sprites are
+ ;; rotated from OAM priority, and hence they won't disappear always, but
+ ;; just for a small amount of time: just enough to see them, creating a
+ ;; flickering effect.
+ ;;
+ ;; For this, we will reserve four bytes (1 sprite) for auxiliary temporary
+ ;; values ($42-$45) which will then be used in order to carry values along
+ ;; the cycle.
+
+ ;; We initialize the auxiliary bytes with the data from the last sprite to
+ ;; be iterated since these are the values to be stored for the first sprite
+ ;; now (that is, the last element is now the first one).
+ lda $24C
+ sta $42
+ lda $24D
+ sta $43
+ lda $24E
+ sta $44
+ lda $24F
+ sta $45
+
+@loop:
+ ;; For each of the four bytes from a sprite, pick the value stored on its
+ ;; auxiliary byte and save the old value into the auxiliary byte afterwards.
+ ;; This way the current byte holds the value from the previous sprite, but
+ ;; its value will be carry on into the next sprite.
+ .repeat 4, I
+ ldy #I
+ lda $42, y
+ pha
+ lda ($40), y
+ sta $42, y
+ pla
+ sta ($40), y
+ .endrepeat
+
+ ;; And move the sprite pointer 4 bytes (1 sprite). Note that we only need to
+ ;; move the low byte since we know beforehand that everything will be inside
+ ;; of the $2xx range.
+ lda $40
+ clc
+ adc #4
+ sta $40
+
+ ;; We know that the last byte from the last sprite is held at $24F. Thus, if
+ ;; the sprite pointer is already passed this point, we can break the loop.
+ ;; Otherwise just carry on.
+ cmp #$50
+ bne @loop
+
+ rts
+.endproc
+
+;;;
+;; NOTE: and from here on stuff that is not relevant for sprite flickering.
+
+nmi:
+ bit $20
+ bpl @next
+
+ pha
+ txa
+ pha
+ tya
+ pha
+
+ jsr Diskun::nmi_update
+
+ lda #$00
+ sta $2003 ; OAMADDR
+ lda #$02
+ sta $4014 ; OAMDMA
+
+ bit $2002 ; PPUSTATUS
+ lda #$00
+ sta $2005 ; PPUSCROLL
+ sta $2005 ; PPUSCROLL
+
+ lda #%01111111
+ and $20
+ sta $20
+
+ pla
+ tay
+ pla
+ tax
+ pla
+@next:
+ rti
+
+reset:
+ sei
+ cld
+
+ ldx #$40
+ stx $4017 ; APU Frame Counter
+
+ ldx #$ff
+ txs
+
+ inx
+ stx $2000 ; PPUCTRL
+ stx $2001 ; PPUMASK
+ stx $4010 ; APU DMC
+
+@vblankwait1:
+ bit $2002 ; PPUSTATUS
+ bpl @vblankwait1
+
+ ldx #0
+ lda #0
+@ram_reset_loop:
+ sta $000, x
+ sta $100, x
+ sta $300, x
+ sta $400, x
+ sta $500, x
+ sta $600, x
+ sta $700, x
+ inx
+ bne @ram_reset_loop ; if x overflows back to #00, then we are done.
+
+ lda #$ef
+@sprite_reset_loop:
+ sta $200, x
+ inx
+ bne @sprite_reset_loop
+
+ lda #$00
+ sta $2003 ; OAMADDR
+ lda #$02
+ sta $4014 ; OAMDMA
+
+@vblankwait2:
+ bit $2002 ; PPUSTATUS
+ bpl @vblankwait2
+
+ lda #$3F
+ sta $2006 ; PPUADDR
+ lda #$00
+ sta $2006 ; PPUADDR
+
+ lda #$0F
+ ldx #$20
+@palettes_reset_loop:
+ sta $2007 ; PPUDATA
+ dex
+ bne @palettes_reset_loop
+
+ jmp main
+
+irq:
+ rti
+
+.proc init_sprites
+ NUM_SPRITES = 20
+
+ lda #$40
+ sta Diskun::m_screen_y
+ lda #$46
+ sta Diskun::m_screen_x
+
+ ldx #$00
+@load_sprites_loop:
+ lda initial_sprite_data, x
+ sta $0200, x
+ inx
+ cpx #(4 * NUM_SPRITES)
+ bne @load_sprites_loop
+ rts
+initial_sprite_data:
+ ;; $200-$20F
+ .byte $40, $01, %00000000, $46
+ .byte $40, $01, %01000000, $4E
+ .byte $48, $11, %00000000, $46
+ .byte $48, $11, %01000000, $4E
+
+ ;; $210-$21F
+ .byte $60, $01, %00000000, $68
+ .byte $60, $01, %01000000, $70
+ .byte $68, $11, %00000000, $68
+ .byte $68, $11, %01000000, $70
+
+ ;; $220-$22F
+ .byte $60, $01, %00000000, $7A
+ .byte $60, $01, %01000000, $82
+ .byte $68, $11, %00000000, $7A
+ .byte $68, $11, %01000000, $82
+
+ ;; $230-$23F
+ .byte $60, $01, %00000000, $8C
+ .byte $60, $01, %01000000, $94
+ .byte $68, $11, %00000000, $8C
+ .byte $68, $11, %01000000, $94
+
+ ;; $240-$24F
+ .byte $60, $01, %00000000, $9E
+ .byte $60, $01, %01000000, $A6
+ .byte $68, $11, %00000000, $9E
+ .byte $68, $11, %01000000, $A6
+.endproc
diff --git a/tests/src/shared/diskun.s b/tests/src/shared/diskun.s
new file mode 100644
index 0000000..b644a83
--- /dev/null
+++ b/tests/src/shared/diskun.s
@@ -0,0 +1,96 @@
+;;; Simple sprite that can be moved with the dpad easily. Use it to have
+;;; something funny moving.
+;; Taken from https://github.com/mssola/code.nes.
+
+.include "joypad.s"
+
+.scope Diskun
+ m_screen_x = $30
+ m_screen_y = $31
+
+ .proc init_palettes
+ lda #$3F
+ sta $2006 ; PPUADDR
+ lda #$00
+ sta $2006 ; PPUADDR
+
+ ldx #0
+ @load_palettes_loop:
+ lda palettes, x
+ sta $2007 ; PPUDATA
+ inx
+ cpx #$20
+ bne @load_palettes_loop
+ rts
+ palettes:
+ DEFAULT_COLOR = $11
+
+ ;; Background
+ .byte DEFAULT_COLOR, $36, $17, $0F
+ .byte DEFAULT_COLOR, $00, $00, $00
+ .byte DEFAULT_COLOR, $00, $00, $00
+ .byte DEFAULT_COLOR, $00, $00, $00
+
+ ;; Foreground
+ .byte DEFAULT_COLOR, $28, $0F, $30
+ .byte DEFAULT_COLOR, $00, $00, $00
+ .byte DEFAULT_COLOR, $00, $00, $00
+ .byte DEFAULT_COLOR, $00, $00, $00
+
+ rts
+ .endproc
+
+ .proc update
+ lda #Joypad::BUTTON_UP
+ and Joypad::m_buttons1
+ beq @check_down
+
+ dec Diskun::m_screen_y
+ dec Diskun::m_screen_y
+ jmp @check_left
+ @check_down:
+ lda #Joypad::BUTTON_DOWN
+ and Joypad::m_buttons1
+ beq @check_left
+
+ inc Diskun::m_screen_y
+ inc Diskun::m_screen_y
+ @check_left:
+ lda #Joypad::BUTTON_LEFT
+ and Joypad::m_buttons1
+ beq @check_right
+
+ dec Diskun::m_screen_x
+ dec Diskun::m_screen_x
+ @check_right:
+ lda #Joypad::BUTTON_RIGHT
+ and Joypad::m_buttons1
+ beq @end
+
+ inc Diskun::m_screen_x
+ inc Diskun::m_screen_x
+ @end:
+ rts
+ .endproc
+
+ .proc nmi_update
+ lda Diskun::m_screen_x
+ sta $203
+ sta $20B
+ clc
+ adc #8
+ sta $207
+ sta $20F
+
+ lda Diskun::m_screen_y
+ sta $200
+ sta $204
+ clc
+ adc #8
+ sta $208
+ sta $20C
+
+ rts
+ .endproc
+.endscope
+
diff --git a/tests/src/shared/joypad.s b/tests/src/shared/joypad.s
new file mode 100644
index 0000000..afd0a68
--- /dev/null
+++ b/tests/src/shared/joypad.s
@@ -0,0 +1,86 @@
+;; Taken from https://github.com/mssola/code.nes.
+
+.scope Joypad
+ ;; Button masks.
+ BUTTON_A = 1 << 7
+ BUTTON_B = 1 << 6
+ BUTTON_SELECT = 1 << 5
+ BUTTON_START = 1 << 4
+ BUTTON_UP = 1 << 3
+ BUTTON_DOWN = 1 << 2
+ BUTTON_LEFT = 1 << 1
+ BUTTON_RIGHT = 1 << 0
+
+ ;; Port addresses for controllers.
+ JOYPAD1 = $4016
+ JOYPAD2 = $4017
+
+ ;; After running a `joypad_read_*` function these two variables will contain
+ ;; the given result.
+ m_buttons1 = $22
+ m_buttons2 = $23
+.endscope
+
+;;;
+;; Read the first joypad. This method is fast but it might be vulnerable to the
+;; DPCM bug (see: https://www.nesdev.org/wiki/Controller_reading_code).
+joypad_unsafe_read:
+ ldx #$00
+ ;; NOTE: fallthrough
+
+;;;
+;; Read the joypad as indexed by the X register (0 for controller 1; 1 for
+;; controller 2). This method is fast but it might be vulnerable to the DPCM bug
+;; (see: https://www.nesdev.org/wiki/Controller_reading_code).
+joypad_unsafe_read_x:
+ ;; Start the latch process.
+ lda #$01
+ sta Joypad::JOYPAD1
+ sta Joypad::m_buttons1, x ; Bit as a guard for the loop below.
+ lsr a
+ sta Joypad::JOYPAD1
+
+ ;; Now the joypad is ready to accept reads.
+@joypad_unsafe_read_x_loop:
+ lda Joypad::JOYPAD1, x
+ and #%00000011 ; Ignore bits other than controller.
+ cmp #$01 ; Set carry if and only if nonzero.
+ rol Joypad::m_buttons1, x ; Carry -> 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