diff options
Diffstat (limited to 'lib/xixanta')
| -rw-r--r-- | lib/xixanta/Cargo.toml | 3 | ||||
| -rw-r--r-- | lib/xixanta/fuzz/Cargo.lock | 90 | ||||
| -rw-r--r-- | lib/xixanta/src/assembler.rs | 260 | ||||
| -rw-r--r-- | lib/xixanta/src/node.rs | 13 | ||||
| -rw-r--r-- | lib/xixanta/src/object.rs | 13 | ||||
| -rw-r--r-- | lib/xixanta/src/opcodes.rs | 51 | ||||
| -rw-r--r-- | lib/xixanta/src/parser.rs | 98 |
7 files changed, 472 insertions, 56 deletions
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(); |
