aboutsummaryrefslogtreecommitdiff
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
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>
-rw-r--r--Cargo.lock96
-rw-r--r--lib/xixanta/Cargo.toml3
-rw-r--r--lib/xixanta/fuzz/Cargo.lock90
-rw-r--r--lib/xixanta/src/assembler.rs260
-rw-r--r--lib/xixanta/src/node.rs13
-rw-r--r--lib/xixanta/src/object.rs13
-rw-r--r--lib/xixanta/src/opcodes.rs51
-rw-r--r--lib/xixanta/src/parser.rs98
8 files changed, 568 insertions, 56 deletions
diff --git a/Cargo.lock b/Cargo.lock
index e598cc4..bbc3dbf 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -58,6 +58,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c042108f3ed77fd83760a5fd79b53be043192bb3b9dba91d8c574c0ada7850c8"
[[package]]
+name = "byteorder"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
+
+[[package]]
+name = "cfg-if"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
+
+[[package]]
name = "clap"
version = "4.5.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -110,6 +122,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
[[package]]
+name = "getrandom"
+version = "0.2.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "wasi",
+]
+
+[[package]]
name = "hashbrown"
version = "0.15.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -148,6 +171,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
[[package]]
+name = "libc"
+version = "0.2.169"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a"
+
+[[package]]
name = "memchr"
version = "2.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -177,6 +206,15 @@ dependencies = [
]
[[package]]
+name = "ppv-lite86"
+version = "0.2.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04"
+dependencies = [
+ "zerocopy",
+]
+
+[[package]]
name = "proc-macro2"
version = "1.0.89"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -195,6 +233,36 @@ dependencies = [
]
[[package]]
+name = "rand"
+version = "0.8.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
+dependencies = [
+ "libc",
+ "rand_chacha",
+ "rand_core",
+]
+
+[[package]]
+name = "rand_chacha"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
+dependencies = [
+ "ppv-lite86",
+ "rand_core",
+]
+
+[[package]]
+name = "rand_core"
+version = "0.6.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
+dependencies = [
+ "getrandom",
+]
+
+[[package]]
name = "readrom"
version = "1.0.0"
dependencies = [
@@ -297,6 +365,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
[[package]]
+name = "wasi"
+version = "0.11.0+wasi-snapshot-preview1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
+
+[[package]]
name = "windows-sys"
version = "0.59.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -383,5 +457,27 @@ name = "xixanta"
version = "0.1.0"
dependencies = [
"lazy_static",
+ "rand",
"toml",
]
+
+[[package]]
+name = "zerocopy"
+version = "0.7.35"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0"
+dependencies = [
+ "byteorder",
+ "zerocopy-derive",
+]
+
+[[package]]
+name = "zerocopy-derive"
+version = "0.7.35"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
diff --git a/lib/xixanta/Cargo.toml b/lib/xixanta/Cargo.toml
index 66f0a76..6d2c66a 100644
--- a/lib/xixanta/Cargo.toml
+++ b/lib/xixanta/Cargo.toml
@@ -8,8 +8,7 @@ edition.workspace = true
license.workspace = true
rust-version.workspace = true
-# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
-
[dependencies]
lazy_static = "1.5.0"
toml = { version = "0.8", features = ["preserve_order"] }
+rand = "0.8.5"
diff --git a/lib/xixanta/fuzz/Cargo.lock b/lib/xixanta/fuzz/Cargo.lock
index eb897dd..5122055 100644
--- a/lib/xixanta/fuzz/Cargo.lock
+++ b/lib/xixanta/fuzz/Cargo.lock
@@ -9,6 +9,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7d5a26814d8dcb93b0e5a0ff3c6d80a8843bafb21b39e8e18a6f05471870e110"
[[package]]
+name = "byteorder"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
+
+[[package]]
name = "cc"
version = "1.1.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -20,12 +26,29 @@ dependencies = [
]
[[package]]
+name = "cfg-if"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
+
+[[package]]
name = "equivalent"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
[[package]]
+name = "getrandom"
+version = "0.2.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "wasi",
+]
+
+[[package]]
name = "hashbrown"
version = "0.15.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -86,6 +109,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775"
[[package]]
+name = "ppv-lite86"
+version = "0.2.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04"
+dependencies = [
+ "zerocopy",
+]
+
+[[package]]
name = "proc-macro2"
version = "1.0.92"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -104,6 +136,36 @@ dependencies = [
]
[[package]]
+name = "rand"
+version = "0.8.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
+dependencies = [
+ "libc",
+ "rand_chacha",
+ "rand_core",
+]
+
+[[package]]
+name = "rand_chacha"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
+dependencies = [
+ "ppv-lite86",
+ "rand_core",
+]
+
+[[package]]
+name = "rand_core"
+version = "0.6.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
+dependencies = [
+ "getrandom",
+]
+
+[[package]]
name = "serde"
version = "1.0.216"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -191,6 +253,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83"
[[package]]
+name = "wasi"
+version = "0.11.0+wasi-snapshot-preview1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
+
+[[package]]
name = "winnow"
version = "0.6.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -204,6 +272,7 @@ name = "xixanta"
version = "0.1.0"
dependencies = [
"lazy_static",
+ "rand",
"toml",
]
@@ -214,3 +283,24 @@ dependencies = [
"libfuzzer-sys",
"xixanta",
]
+
+[[package]]
+name = "zerocopy"
+version = "0.7.35"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0"
+dependencies = [
+ "byteorder",
+ "zerocopy-derive",
+]
+
+[[package]]
+name = "zerocopy-derive"
+version = "0.7.35"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
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]
diff --git a/lib/xixanta/src/node.rs b/lib/xixanta/src/node.rs
index 6afb424..f919ade 100644
--- a/lib/xixanta/src/node.rs
+++ b/lib/xixanta/src/node.rs
@@ -138,6 +138,8 @@ pub enum ControlType {
Word,
Addr,
IncBin,
+ StartRepeat,
+ EndRepeat,
}
impl fmt::Display for ControlType {
@@ -156,6 +158,8 @@ impl fmt::Display for ControlType {
ControlType::Word => write!(f, ".word/.dw"),
ControlType::Addr => write!(f, ".addr"),
ControlType::IncBin => write!(f, ".incbin"),
+ ControlType::StartRepeat => write!(f, ".repeat"),
+ ControlType::EndRepeat => write!(f, ".endrepeat"),
}
}
}
@@ -293,6 +297,9 @@ impl NodeType {
NodeType::Control(ControlType::StartScope) => {
Some(NodeType::Control(ControlType::EndScope))
}
+ NodeType::Control(ControlType::StartRepeat) => {
+ Some(NodeType::Control(ControlType::EndRepeat))
+ }
_ => None,
}
}
@@ -353,10 +360,12 @@ impl PNode {
match self.node_type {
NodeType::Control(ControlType::StartMacro)
| NodeType::Control(ControlType::StartProc)
- | NodeType::Control(ControlType::StartScope) => NodeBodyType::Starts,
+ | NodeType::Control(ControlType::StartScope)
+ | NodeType::Control(ControlType::StartRepeat) => NodeBodyType::Starts,
NodeType::Control(ControlType::EndMacro)
| NodeType::Control(ControlType::EndProc)
- | NodeType::Control(ControlType::EndScope) => NodeBodyType::Ends,
+ | NodeType::Control(ControlType::EndScope)
+ | NodeType::Control(ControlType::EndRepeat) => NodeBodyType::Ends,
_ => NodeBodyType::None,
}
}
diff --git a/lib/xixanta/src/object.rs b/lib/xixanta/src/object.rs
index 506e632..0d8bcc6 100644
--- a/lib/xixanta/src/object.rs
+++ b/lib/xixanta/src/object.rs
@@ -350,13 +350,15 @@ impl Context {
match node.node_type {
NodeType::Control(ControlType::StartMacro)
| NodeType::Control(ControlType::StartProc)
- | NodeType::Control(ControlType::StartScope) => {
+ | NodeType::Control(ControlType::StartScope)
+ | NodeType::Control(ControlType::StartRepeat) => {
self.context_push(node.left.as_ref().unwrap());
Ok(true)
}
NodeType::Control(ControlType::EndMacro)
| NodeType::Control(ControlType::EndProc)
- | NodeType::Control(ControlType::EndScope) => {
+ | NodeType::Control(ControlType::EndScope)
+ | NodeType::Control(ControlType::EndRepeat) => {
self.context_pop(&node.value)?;
Ok(true)
}
@@ -485,7 +487,7 @@ impl Context {
.iter()
.position(|n| n.as_str() == name)
.unwrap_or(0);
- if index < 2 {
+ if index < 1 {
GLOBAL_CONTEXT
} else {
self.stack.get(index - 1).unwrap()
@@ -517,6 +519,11 @@ impl Context {
fn to_human_with(&self, name: &str) -> String {
if name == GLOBAL_CONTEXT {
"the global scope".to_string()
+ } else if name.starts_with(".repeat-") {
+ // The ".repeat-" generated scope is randomly generated and is not
+ // clear to the human eye. Hence, just hide out the name for this
+ // case.
+ "the current scope".to_string()
} else {
format!("'{}'", name)
}
diff --git a/lib/xixanta/src/opcodes.rs b/lib/xixanta/src/opcodes.rs
index 24cbade..6c0aa0d 100644
--- a/lib/xixanta/src/opcodes.rs
+++ b/lib/xixanta/src/opcodes.rs
@@ -54,11 +54,26 @@ pub struct ShortEntry {
// pub affected_on_page: bool,
// }
+/// The representation of a Control statement/expression.
#[derive(Debug)]
pub struct Control {
+ /// Type of control statement/expression.
pub control_type: ControlType,
- pub has_identifier: bool,
- pub required_args: Option<usize>,
+
+ /// Whether there is an expected identifier here, either by the parser or
+ /// from an assembler perspective (e.g. 'Foo' in '.proc Foo'). A None value
+ /// means that there is no identifier expected, Some(true) means that the
+ /// identifier is not real but has to be created on the fly by the parser
+ /// (e.g. .repeat).
+ pub has_identifier: Option<bool>,
+
+ /// Minimum and maximum number of arguments accepted by this control
+ /// statement, or None if undefined (e.g. a .macro which has an undefined
+ /// number of arguments).
+ pub required_args: Option<(usize, usize)>,
+
+ /// True if the context has to change after evaluating this control
+ /// statement.
pub touches_context: bool,
}
@@ -716,21 +731,23 @@ lazy_static! {
pub static ref CONTROL_FUNCTIONS: HashMap<String, Control> = {
let mut functions = HashMap::new();
- functions.insert(String::from(".hibyte"), Control { control_type: ControlType::Hibyte, has_identifier: false, required_args: Some(1), touches_context: false });
- functions.insert(String::from(".lobyte"), Control { control_type: ControlType::Lobyte, has_identifier: false, required_args: Some(1), touches_context: false });
- functions.insert(String::from(".macro"), Control { control_type: ControlType::StartMacro, has_identifier: true, required_args: None, touches_context: true });
- functions.insert(String::from(".proc"), Control { control_type: ControlType::StartProc, has_identifier: true, required_args: Some(0), touches_context: true });
- functions.insert(String::from(".scope"), Control { control_type: ControlType::StartScope, has_identifier: true, required_args: Some(0), touches_context: true });
- functions.insert(String::from(".endscope"), Control { control_type: ControlType::EndScope, has_identifier: false, required_args: Some(0), touches_context: true });
- functions.insert(String::from(".endproc"), Control { control_type: ControlType::EndProc, has_identifier: false, required_args: Some(0), touches_context: true });
- functions.insert(String::from(".endmacro"), Control { control_type: ControlType::EndMacro, has_identifier: false, required_args: Some(0), touches_context: true });
- functions.insert(String::from(".segment"), Control { control_type: ControlType::Segment, has_identifier: false, required_args: Some(1), touches_context: false });
- functions.insert(String::from(".byte"), Control { control_type: ControlType::Byte, has_identifier: false, required_args: None, touches_context: false });
- functions.insert(String::from(".db"), Control { control_type: ControlType::Byte, has_identifier: false, required_args: None, touches_context: false });
- functions.insert(String::from(".word"), Control { control_type: ControlType::Word, has_identifier: false, required_args: None, touches_context: false });
- functions.insert(String::from(".dw"), Control { control_type: ControlType::Word, has_identifier: false, required_args: None, touches_context: false });
- functions.insert(String::from(".addr"), Control { control_type: ControlType::Addr, has_identifier: false, required_args: None, touches_context: false });
- functions.insert(String::from(".incbin"), Control { control_type: ControlType::IncBin, has_identifier: false, required_args: Some(1), touches_context: false });
+ functions.insert(String::from(".hibyte"), Control { control_type: ControlType::Hibyte, has_identifier: None, required_args: Some((1, 1)), touches_context: false });
+ functions.insert(String::from(".lobyte"), Control { control_type: ControlType::Lobyte, has_identifier: None, required_args: Some((1, 1)), touches_context: false });
+ functions.insert(String::from(".macro"), Control { control_type: ControlType::StartMacro, has_identifier: Some(false), required_args: None, touches_context: true });
+ functions.insert(String::from(".proc"), Control { control_type: ControlType::StartProc, has_identifier: Some(false), required_args: Some((0, 0)), touches_context: true });
+ functions.insert(String::from(".scope"), Control { control_type: ControlType::StartScope, has_identifier: Some(false), required_args: Some((0, 0)), touches_context: true });
+ functions.insert(String::from(".endscope"), Control { control_type: ControlType::EndScope, has_identifier: None, required_args: Some((0, 0)), touches_context: true });
+ functions.insert(String::from(".endproc"), Control { control_type: ControlType::EndProc, has_identifier: None, required_args: Some((0, 0)), touches_context: true });
+ functions.insert(String::from(".endmacro"), Control { control_type: ControlType::EndMacro, has_identifier: None, required_args: Some((0, 0)), touches_context: true });
+ functions.insert(String::from(".segment"), Control { control_type: ControlType::Segment, has_identifier: None, required_args: Some((1, 1)), touches_context: false });
+ functions.insert(String::from(".byte"), Control { control_type: ControlType::Byte, has_identifier: None, required_args: None, touches_context: false });
+ functions.insert(String::from(".db"), Control { control_type: ControlType::Byte, has_identifier: None, required_args: None, touches_context: false });
+ functions.insert(String::from(".word"), Control { control_type: ControlType::Word, has_identifier: None, required_args: None, touches_context: false });
+ functions.insert(String::from(".dw"), Control { control_type: ControlType::Word, has_identifier: None, required_args: None, touches_context: false });
+ functions.insert(String::from(".addr"), Control { control_type: ControlType::Addr, has_identifier: None, required_args: None, touches_context: false });
+ 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
};
diff --git a/lib/xixanta/src/parser.rs b/lib/xixanta/src/parser.rs
index 8429c2e..c171624 100644
--- a/lib/xixanta/src/parser.rs
+++ b/lib/xixanta/src/parser.rs
@@ -1,6 +1,7 @@
use crate::errors::ParseError;
use crate::node::{NodeBodyType, NodeType, OperationType, PNode, PString};
use crate::opcodes::{CONTROL_FUNCTIONS, INSTRUCTIONS};
+use rand::distributions::{Alphanumeric, DistString};
use std::cmp::Ordering;
use std::io::{self, BufRead, Read};
@@ -82,8 +83,10 @@ impl Parser {
}
}
+ /// Returns the first layer of nodes that have been parsed. Note that this
+ /// function only makes sense to be called whenever parsing is done,
+ /// otherwise results will be incomplete in (most probably) unexpected ways.
pub fn nodes(&self) -> Vec<PNode> {
- // println!("{:#?}", self.nodes);
self.nodes.first().unwrap().to_vec()
}
@@ -916,6 +919,11 @@ impl Parser {
}
}
+ // Generate a unique identifier with the given prefix.
+ fn unique_identifier(&self, prefix: String) -> String {
+ prefix + &String::from("-") + &Alphanumeric.sample_string(&mut rand::thread_rng(), 16)
+ }
+
// Returns a NodeType::Control node with whatever could be parsed
// considering the given `id` and rest of the `line`.
fn parse_control(&mut self, id: PString, line: &str) -> Result<PNode, ParseError> {
@@ -933,11 +941,21 @@ impl Parser {
// If this control function has an identifier (e.g. `.macro
// Identifier(args...)`), let's parse it now.
- if control.has_identifier {
+ if control.has_identifier.is_some() {
self.skip_whitespace(line);
left = Some(Box::new(PNode {
node_type: NodeType::Value,
- value: self.parse_identifier(line)?.0,
+ // The identifier is actually there or does it have to be generated?
+ value: if control.has_identifier.unwrap() {
+ PString {
+ value: self.unique_identifier(control.control_type.to_string()),
+ line: self.line,
+ start: id.start,
+ end: id.end,
+ }
+ } else {
+ self.parse_identifier(line)?.0
+ },
left: None,
right: None,
args: None,
@@ -950,7 +968,7 @@ impl Parser {
// required by the function.
let args = self.parse_arguments(line)?;
if let Some(args_required) = control.required_args {
- if args.len() != args_required {
+ if args.len() < args_required.0 || args.len() > args_required.1 {
return Err(self.parser_error(
format!("wrong number of arguments for function '{}'", id.value).as_str(),
));
@@ -2153,6 +2171,78 @@ inc $20
}
#[test]
+ fn parse_repeat_control() {
+ let mut parser = Parser::default();
+ let err = parser.parse(".repeat\n.endrepeat".as_bytes()).unwrap_err();
+ assert_eq!(
+ err.first().unwrap().message,
+ "wrong number of arguments for function '.repeat'"
+ );
+
+ let mut parser = Parser::default();
+ let err = parser
+ .parse(".repeat 1, 2, 3\n.endrepeat".as_bytes())
+ .unwrap_err();
+ assert_eq!(
+ err.first().unwrap().message,
+ "wrong number of arguments for function '.repeat'"
+ );
+
+ // Minimum required argument.
+
+ parser = Parser::default();
+ let mut line = ".repeat 2\n.endrepeat";
+
+ assert!(parser.parse(line.as_bytes()).is_ok());
+ let mut control = parser.nodes.last().unwrap().first().unwrap();
+ assert_node(
+ control,
+ NodeType::Control(ControlType::StartRepeat),
+ line,
+ ".repeat",
+ );
+ assert!(control
+ .left
+ .as_ref()
+ .unwrap()
+ .value
+ .value
+ .starts_with(".repeat-"));
+ assert!(control.right.is_some());
+
+ let mut args = control.args.clone().unwrap();
+ assert_eq!(args.len(), 1);
+ assert_node(args.first().unwrap(), NodeType::Value, line, "2");
+
+ // Maximum allowed arguments.
+
+ parser = Parser::default();
+ line = ".repeat 2, I\n.endrepeat";
+
+ assert!(parser.parse(line.as_bytes()).is_ok());
+ control = parser.nodes.last().unwrap().first().unwrap();
+ assert_node(
+ control,
+ NodeType::Control(ControlType::StartRepeat),
+ line,
+ ".repeat",
+ );
+ assert!(control
+ .left
+ .as_ref()
+ .unwrap()
+ .value
+ .value
+ .starts_with(".repeat-"));
+ assert!(control.right.is_some());
+
+ args = control.args.clone().unwrap();
+ assert_eq!(args.len(), 2);
+ assert_node(args.first().unwrap(), NodeType::Value, line, "2");
+ assert_node(args.last().unwrap(), NodeType::Value, line, "I");
+ }
+
+ #[test]
fn parse_unknown_control() {
let mut parser = Parser::default();
let mut err = parser.parse(".".as_bytes()).unwrap_err();