aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-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();