aboutsummaryrefslogtreecommitdiff
path: root/lib/xixanta/src/assembler.rs
diff options
context:
space:
mode:
authorMiquel Sabaté Solà <mikisabate@gmail.com>2024-12-24 16:48:16 +0100
committerMiquel Sabaté Solà <mikisabate@gmail.com>2025-01-07 16:38:54 +0100
commit9b5e5aa6d36a550bed84f85df8e1e46fcf9765e1 (patch)
tree9748bd1059bb24fd5fb204ff9d91ae7531632beb /lib/xixanta/src/assembler.rs
parentf3c6b02fb2f2957b9b896c34d05abe75263c3f60 (diff)
downloadtools.nes-9b5e5aa6d36a550bed84f85df8e1e46fcf9765e1.tar.gz
tools.nes-9b5e5aa6d36a550bed84f85df8e1e46fcf9765e1.zip
Add the .repeat control statement
This is a control statement which acts similarly as .proc/.macro/.scope, in which an inner block is allocated for it. Hence, all the previous work from 1f8a6becc7cd ("parser: Implement block bodies") and ec8b709fa24c ("Implement block bodies inside of the assembler") make this one out possible, as .repeat statements don't have an identifier that can be used for hashing. From the parser perspective this introduction raises two new things. First of all this control statement also needed a differentiation between the amount of required arguments, and the allowed ones, since there is a second optional argument to it. And second, even the identifier is not given, we have to generate one so to add a context for it. This was at first not needed, but introducing .repeat-only variables means that we have to have inner contexts which need to be named somehow so we can retrieve the context later when picking up the value for them again. This last thing brought the need for a new dependency: rand. This is used to generate a random string to identify the .repeat block. Signed-off-by: Miquel Sabaté Solà <mikisabate@gmail.com>
Diffstat (limited to 'lib/xixanta/src/assembler.rs')
-rw-r--r--lib/xixanta/src/assembler.rs260
1 files changed, 232 insertions, 28 deletions
diff --git a/lib/xixanta/src/assembler.rs b/lib/xixanta/src/assembler.rs
index 5c26c5f..c6615d2 100644
--- a/lib/xixanta/src/assembler.rs
+++ b/lib/xixanta/src/assembler.rs
@@ -72,6 +72,9 @@ pub struct Assembler<'a> {
// Same as macros_seen but for .proc's.
procs_seen: usize,
+ // Same as macros_seen but for .repeat's.
+ repeats_seen: usize,
+
// Warnings that have accumulated over the run.
warnings: Vec<Error>,
@@ -203,6 +206,7 @@ impl<'a> Assembler<'a> {
labels_seen: 0,
macros_seen: 0,
procs_seen: 0,
+ repeats_seen: 0,
warnings: vec![],
directories: vec![],
}
@@ -239,11 +243,11 @@ impl<'a> Assembler<'a> {
NodeType::Label => {
// There's no good reason to declare a named label inside of
// a macro. If that's the case, just error out.
- if self.macros_seen > 0 && !node.value.is_empty() {
+ if (self.macros_seen > 0 || self.repeats_seen > 0) && !node.value.is_empty() {
errors.push(Error::Eval(EvalError {
line: node.value.line,
message: format!(
- "using a named label ('{}') inside of a macro definition",
+ "using a named label ('{}') inside of a macro/repeat definition",
node.value.value
),
global: false,
@@ -255,9 +259,9 @@ impl<'a> Assembler<'a> {
}
}
NodeType::Assignment => {
- if self.macros_seen > 0 {
+ if self.macros_seen > 0 || self.repeats_seen > 0 {
errors.push(Error::Eval(EvalError {
- message: "cannot have assignments inside of macro definitions"
+ message: "cannot have assignments inside of macro/repeat definitions"
.to_string(),
line: node.value.line,
global: false,
@@ -316,7 +320,8 @@ impl<'a> Assembler<'a> {
}
// Same as NodeType::Label.
ControlType::StartProc => {
- if self.macros_seen > 0 || self.procs_seen > 0 {
+ if self.macros_seen > 0 || self.procs_seen > 0 || self.repeats_seen > 0
+ {
errors.push(Error::Context(ContextError {
message: "you cannot call '.proc' in this context".to_string(),
line: node.value.line,
@@ -339,7 +344,8 @@ impl<'a> Assembler<'a> {
}
}
ControlType::StartScope => {
- if self.macros_seen > 0 || self.procs_seen > 0 {
+ if self.macros_seen > 0 || self.procs_seen > 0 || self.repeats_seen > 0
+ {
errors.push(Error::Context(ContextError {
message: "you cannot call '.scope' in this context".to_string(),
line: node.value.line,
@@ -349,6 +355,14 @@ impl<'a> Assembler<'a> {
continue;
}
}
+ ControlType::StartRepeat => {
+ self.repeats_seen += 1;
+ }
+ ControlType::EndRepeat => {
+ if self.repeats_seen > 0 {
+ self.repeats_seen -= 1;
+ }
+ }
_ => {}
}
@@ -465,27 +479,35 @@ impl<'a> Assembler<'a> {
errors.push(Error::Eval(e));
}
- // If this is the start of a .scope statement, then go
- // inside of its body too if it exists (note that its
- // existence might not be guaranteed if the parser gave an
- // error on this block). Note that we do that too for
- // .proc's in its specialized branch, and we don't want to
- // do it for macros as they will be evaluated on a per call
- // basis.
- if matches!(control_type, ControlType::StartScope)
- && node.right.as_ref().is_some()
- {
- let scope_name = &node.left.as_ref().unwrap().value;
- let args = &node.right.as_ref().unwrap().args.as_ref().unwrap();
- if args.is_empty() {
- self.warnings.push(Error::Eval(EvalError {
- line: node.value.line,
- message: format!("empty .scope '{}'", scope_name.value),
- global: false,
- }));
- } else {
- self.bundle(args)?;
+ // On control statements which modify the context, there are
+ // some further evaluating to do.
+ match control_type {
+ ControlType::StartRepeat => {
+ self.evaluate_repeat_statement(node)?;
}
+ ControlType::StartScope => {
+ // If this is the start of a .scope statement, then go
+ // inside of its body too if it exists (note that its
+ // existence might not be guaranteed if the parser gave an
+ // error on this block). Note that we do that too for
+ // .proc's in its specialized branch, and we don't want to
+ // do it for macros as they will be evaluated on a per call
+ // basis.
+ if node.right.as_ref().is_some() {
+ let scope_name = &node.left.as_ref().unwrap().value;
+ let args = &node.right.as_ref().unwrap().args.as_ref().unwrap();
+ if args.is_empty() {
+ self.warnings.push(Error::Eval(EvalError {
+ line: node.value.line,
+ message: format!("empty .scope '{}'", scope_name.value),
+ global: false,
+ }));
+ } else {
+ self.bundle(args)?;
+ }
+ }
+ }
+ _ => {}
}
}
NodeType::Value | NodeType::Call => {
@@ -1043,7 +1065,7 @@ impl<'a> Assembler<'a> {
),
line: node.value.line,
global: false,
- })
+ });
}
}
}
@@ -1258,6 +1280,79 @@ impl<'a> Assembler<'a> {
Ok(())
}
+ // Evaluate the given node assuming it's a .repeat statement and push all
+ // the requested bundles from it..
+ fn evaluate_repeat_statement(&mut self, node: &'a PNode) -> Result<(), Vec<Error>> {
+ // First fetch the number of times the code block must be repeated.
+ let args = node.args.as_ref().unwrap();
+ let first = &args.first().unwrap().value.value;
+ let repeats = match first.parse::<usize>() {
+ Ok(n) => {
+ if n < 2 {
+ return Err(EvalError {
+ global: false,
+ line: node.value.line,
+ message: "pointless .repeat statement".to_string(),
+ }
+ .into());
+ } else if n > 255 {
+ return Err(EvalError {
+ global: false,
+ line: node.value.line,
+ message: "the number of iterations has to fit in a single byte".to_string(),
+ }
+ .into());
+ }
+ n
+ }
+ Err(_) => {
+ return Err(EvalError {
+ global: false,
+ line: node.value.line,
+ message: format!(
+ "first argument must be an integer, '{}' found instead",
+ first
+ ),
+ }
+ .into())
+ }
+ };
+
+ // The code that is to be repeated.
+ let code = &node.right.as_ref().unwrap().args.as_ref().unwrap();
+ if code.is_empty() {
+ self.warnings.push(Error::Eval(EvalError {
+ line: node.value.line,
+ message: "empty .repeat statement".to_string(),
+ global: false,
+ }));
+ return Ok(());
+ }
+
+ // Perform the action.
+ for i in 0..repeats {
+ // If an index was given, set it now as a .repeat variable with the
+ // loop index.
+ if args.len() == 2 {
+ self.context.set_variable(
+ &args.last().unwrap().value,
+ &Object {
+ bundle: Bundle::fill(i as u8),
+ mapping: self.current_mapping,
+ segment: self.current_segment,
+ object_type: ObjectType::Value,
+ },
+ true,
+ )?;
+ }
+
+ // And push all the bundles from the inner code.
+ self.bundle(code)?;
+ }
+
+ Ok(())
+ }
+
fn evaluate_control_expression(&mut self, node: &PNode) -> Result<Bundle, EvalError> {
match node.node_type {
NodeType::Control(ControlType::Hibyte) => self.evaluate_byte(node, true),
@@ -2990,7 +3085,7 @@ mod tests {
"#,
2,
false,
- "using a named label ('@label') inside of a macro definition",
+ "using a named label ('@label') inside of a macro/repeat definition",
);
}
@@ -3032,6 +3127,115 @@ mod tests {
assert_eq!(res[3].bytes[1], 0xFC);
}
+ // .repeat
+
+ #[test]
+ fn code_gets_repeated() {
+ let res = just_bundles(
+ r#".repeat 3
+nop
+.endrepeat
+"#,
+ );
+
+ assert_eq!(res.len(), 3);
+
+ for it in res.iter().take(2) {
+ assert_eq!(it.size, 1);
+ assert_eq!(it.bytes[0], 0xEA);
+ assert_eq!(it.bytes[1], 0x00);
+ assert_eq!(it.bytes[2], 0x00);
+ }
+ }
+
+ #[test]
+ fn variable_in_repeat() {
+ let res = just_bundles(
+ r#".repeat 2, I
+.repeat 2, J
+lda #I
+ldx #J
+.endrepeat
+.endrepeat"#,
+ );
+
+ assert_eq!(res.len(), 8);
+
+ // 00
+ assert_eq!(res[0].bytes[0], 0xA9);
+ assert_eq!(res[0].bytes[1], 0x00);
+ assert_eq!(res[1].bytes[0], 0xA2);
+ assert_eq!(res[1].bytes[1], 0x00);
+
+ // 01
+ assert_eq!(res[2].bytes[0], 0xA9);
+ assert_eq!(res[2].bytes[1], 0x00);
+ assert_eq!(res[3].bytes[0], 0xA2);
+ assert_eq!(res[3].bytes[1], 0x01);
+
+ // 10
+ assert_eq!(res[4].bytes[0], 0xA9);
+ assert_eq!(res[4].bytes[1], 0x01);
+ assert_eq!(res[5].bytes[0], 0xA2);
+ assert_eq!(res[5].bytes[1], 0x00);
+
+ // 11
+ assert_eq!(res[6].bytes[0], 0xA9);
+ assert_eq!(res[6].bytes[1], 0x01);
+ assert_eq!(res[7].bytes[0], 0xA2);
+ assert_eq!(res[7].bytes[1], 0x01);
+ }
+
+ #[test]
+ fn error_on_invalid_repeat() {
+ assert_error(
+ ".repeat 1\n.endrepeat",
+ 1,
+ false,
+ "pointless .repeat statement",
+ );
+ assert_error(
+ ".repeat a\n.endrepeat",
+ 1,
+ false,
+ "first argument must be an integer, 'a' found instead",
+ );
+ assert_error(
+ ".repeat 256\n.endrepeat",
+ 1,
+ false,
+ "the number of iterations has to fit in a single byte",
+ );
+ }
+
+ #[test]
+ fn warning_on_empty_repeat() {
+ let res = just_assemble(".repeat 2\n.endrepeat");
+
+ assert!(res.bundles[0x10..].is_empty());
+ assert!(res.errors.is_empty());
+
+ assert_eq!(res.warnings.len(), 2); // empty code and the one we are testing.
+ assert_eq!(
+ res.warnings.first().unwrap().to_string(),
+ "empty .repeat statement (line 4)"
+ );
+ }
+
+ #[test]
+ fn custom_human_message_on_unknown_variable_in_repeat() {
+ assert_error(
+ r#".repeat 2
+lda #Variable
+.endrepeat
+"#,
+ 2,
+ false,
+ "'e' is not a decimal value and could not find variable 'Variable' \
+ in the current scope either",
+ );
+ }
+
// Segments
#[test]