diff options
| author | Miquel Sabaté Solà <mikisabate@gmail.com> | 2024-12-24 16:48:16 +0100 |
|---|---|---|
| committer | Miquel Sabaté Solà <mikisabate@gmail.com> | 2025-01-07 16:38:54 +0100 |
| commit | 9b5e5aa6d36a550bed84f85df8e1e46fcf9765e1 (patch) | |
| tree | 9748bd1059bb24fd5fb204ff9d91ae7531632beb /lib/xixanta/src/assembler.rs | |
| parent | f3c6b02fb2f2957b9b896c34d05abe75263c3f60 (diff) | |
| download | tools.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.rs | 260 |
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] |
