From fcfe7171b5718822229b0845541f7c878b523c58 Mon Sep 17 00:00:00 2001 From: ElnuDev Date: Sun, 28 May 2023 12:23:50 -0700 Subject: [PATCH] Implement define statements --- demo/Cargo.lock | 9 +- demo/demo.rpy | 5 +- renrs/Cargo.toml | 1 + renrs/src/lib.rs | 205 ++++++++++++++++++++++++++++++++++++--------- renrs/src/rpy.pest | 2 +- 5 files changed, 175 insertions(+), 47 deletions(-) diff --git a/demo/Cargo.lock b/demo/Cargo.lock index ef0bc41..7a65e64 100644 --- a/demo/Cargo.lock +++ b/demo/Cargo.lock @@ -1739,9 +1739,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.8.1" +version = "1.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af83e617f331cc6ae2da5443c602dfa5af81e517212d9d611a5b3ba1777b5370" +checksum = "81ca098a9821bd52d6b24fd8b10bd081f47d39c22778cafaa75a2857a62c6390" dependencies = [ "aho-corasick", "memchr", @@ -1750,9 +1750,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.7.1" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5996294f19bd3aae0453a862ad728f60e6600695733dd5df01da90c54363a3c" +checksum = "436b050e76ed2903236f032a59761c1eb99e1b0aead2c257922771dab1fc8c78" [[package]] name = "renrs" @@ -1760,6 +1760,7 @@ version = "0.1.0" dependencies = [ "pest", "pest_derive", + "regex", ] [[package]] diff --git a/demo/demo.rpy b/demo/demo.rpy index d5f20ac..2334443 100644 --- a/demo/demo.rpy +++ b/demo/demo.rpy @@ -1,5 +1,6 @@ # Control testing -"Bob" "I will not say anything" +"Bob" "I will not say anything, [foo]" +define foo = "bar" if False: "Bob" "W-what? Why am I saying this?" if True: @@ -11,4 +12,6 @@ if True: "Bob" "...cereal!" if False: "Bob" "But I actually hate it." + define bar = "potato" + define foo = bar "Bob sat on the bench." \ No newline at end of file diff --git a/renrs/Cargo.toml b/renrs/Cargo.toml index e250776..2515810 100644 --- a/renrs/Cargo.toml +++ b/renrs/Cargo.toml @@ -9,3 +9,4 @@ edition = "2021" [dependencies] pest = "2.6.0" pest_derive = "2.6.0" +regex = "1.8.3" diff --git a/renrs/src/lib.rs b/renrs/src/lib.rs index f9df96b..aebc99a 100644 --- a/renrs/src/lib.rs +++ b/renrs/src/lib.rs @@ -1,8 +1,9 @@ use core::panic; -use std::{fs, path::PathBuf}; +use std::{fs, path::PathBuf, collections::HashMap, rc::Rc, cell::RefCell, hash::Hash}; use pest::Parser; use pest_derive::Parser; +use regex::Regex; #[derive(Parser)] #[grammar = "rpy.pest"] @@ -31,16 +32,41 @@ impl Token { } } +impl ToString for Token { + fn to_string(&self) -> String { + use Token::*; + match self { + Keyword(_) => panic!("keywords should not be used as values"), + Str(string) => string.to_owned(), + Array(array) => format!( + "[{}]", + array + .iter() + .map(|token| token.to_string()) + .collect::>() + .join(", ") + ), + Boolean(boolean) => match boolean { + true => "True", + false => "False", + }.to_owned(), + Number(number) => number.to_string(), + } + } +} + // Indented command block #[derive(Debug)] struct CommandBlock { + parent: Option>>, control: Option, elements: Vec, next: Option, + variables: Option>>>, } impl CommandBlock { - fn next(&mut self) -> Option<&Command> { + fn next(&mut self) -> Option { let mut next = match self.next { Some(next) => next, None => return None, @@ -53,14 +79,14 @@ impl CommandBlock { for element in &mut self.elements[next..] { match element { BlockElement::Command(command) => { - result = Some(&*command); + result = Some(command.clone()); next += 1; break; }, BlockElement::Block(block) => { - match block.next() { + match block.borrow_mut().next() { Some(command) => { - result = Some(command); + result = Some(command.clone()); break; }, None => { @@ -84,14 +110,32 @@ impl CommandBlock { If { condition } => *condition, }) } + + fn get_variables(&self) -> HashMap { + match &self.variables { + Some(variables) => { + let variables = variables.borrow().clone(); + if let Some(parent) = &self.parent { + let mut parent_variables = parent.borrow().get_variables(); + parent_variables.extend(variables.into_iter()); + parent_variables + } else { + variables + } + }, + None => HashMap::new(), + } + } } impl Default for CommandBlock { fn default() -> Self { Self { + parent: None, control: None, elements: Vec::new(), next: Some(0), + variables: None, } } } @@ -99,32 +143,43 @@ impl Default for CommandBlock { #[derive(Debug)] enum BlockElement { Command(Command), - Block(CommandBlock), + Block(Rc>), } -type Script = CommandBlock; - #[derive(Debug)] #[allow(dead_code)] enum Control { If { condition: bool }, } +impl Control { + fn has_variable_scope(&self) -> bool { + match self { + _ => false, + } + } +} + // Parsed script commands -#[derive(Debug)] +#[derive(Debug, Clone)] #[allow(dead_code)] enum Command { Say { name: Option, text: String }, Eat { food: String, politely: bool }, + Define { variable: String, value: Token }, } impl Command { - fn is_blocking(&self) -> bool { + fn execute(&self, context: &Rc>) -> Option { use Command::*; - match self { - Say { .. } => true, - _ => false, - } + Some(match self { + Say { name, text } => Event::Say { + name: name.clone(), + text: text.clone(), + }, + Define { .. } => panic!("define command should not be executed at runtime!"), + Eat { .. } => return None, + }.process(context)) } } @@ -133,6 +188,42 @@ pub enum Event { Say { name: Option, text: String }, } +impl Event { + fn is_blocking(&self) -> bool { + use Event::*; + match self { + Say { .. } => true, + } + } + + fn process(mut self, context: &Rc>) -> Self { + use Event::*; + match &mut self { + Say { name, text } => { + let context = context.borrow(); + let variables = context.get_variables(); + *name = name.as_deref().map(|name| interpolate_string(&name, &variables)); + *text = interpolate_string(&text, &variables); + } + } + self + } +} + +fn interpolate_string(input: &str, variables: &HashMap) -> String { + let re = Regex::new(r"\[(\w+)\]").unwrap(); + let interpolated_string = re.replace_all(input, |caps: ®ex::Captures| { + let var_name = &caps[1]; + if let Some(value) = variables.get(var_name) { + value.to_string() + } else { + format!("[{}]", var_name) + } + }); + + interpolated_string.into_owned() +} + // ====== // Parser // ====== @@ -140,25 +231,33 @@ pub enum Event { type Pair<'a> = pest::iterators::Pair<'a, Rule>; // Read file into commands -fn parse_file(file_path: &PathBuf) -> CommandBlock { +fn parse_file(file_path: &PathBuf) -> Rc> { let unparsed_file = fs::read_to_string(file_path).expect("cannot find file"); parse(&unparsed_file) } -fn parse(script: &str) -> CommandBlock { +fn parse(script: &str) -> Rc> { let file = RpyParser::parse(Rule::File, script) .expect("unsuccessful parse") .next() .unwrap(); - parse_block(file) + parse_block(file, None) } -fn parse_block(block: Pair) -> CommandBlock { - CommandBlock { - elements: { +fn parse_block(pair: Pair, definitions: Option>>>) -> Rc> { + //let variables: HashMap = HashMap::new(); + let is_root = definitions.is_none(); + let definitions = definitions.unwrap_or(Rc::new(RefCell::new(HashMap::new()))); + let block_rc = Rc::new(RefCell::new(CommandBlock { + variables: if is_root { Some(definitions.clone()) } else { None }, + ..Default::default() + })); + { + let mut block = block_rc.borrow_mut(); + block.elements = { let mut control = None; let mut elements = Vec::new(); - for pair in block.into_inner() { + for pair in pair.into_inner() { elements.push(match pair.as_rule() { Rule::Control => { if control.is_some() { @@ -167,24 +266,49 @@ fn parse_block(block: Pair) -> CommandBlock { control = Some(parse_control(pair)); continue; }, - Rule::Line => BlockElement::Command(parse_command(pair)), + Rule::Line => BlockElement::Command(match parse_command(pair) { + Command::Define { variable, value } => { + let mut value = value; + if let Token::Keyword(keyword) = value { + value = definitions + .borrow() + .get(&keyword) + .unwrap_or_else(|| panic!("undefined variable `{keyword}`")) + .clone(); + } + definitions.borrow_mut().insert(variable, value); + continue; + } + command => command, + }), Rule::Block => BlockElement::Block({ - let mut block = parse_block(pair); - if control.is_none() { - panic!("block should have control"); + let subblock_rc = parse_block(pair, Some(definitions.clone())); + { + let mut subblock = subblock_rc.borrow_mut(); + if let Some(control) = control.as_ref() { + if control.has_variable_scope() { + subblock.variables = Some(Rc::new(RefCell::new(HashMap::new()))); + } + } else { + panic!("block should have control"); + } + subblock.parent = Some(block_rc.clone()); + subblock.control = control; + control = None; } - block.control = control; - control = None; - block + subblock_rc }), Rule::EOI => break, // end _ => unreachable!(), }); } elements - }, - ..Default::default() + }; + if is_root { + block.variables = Some(definitions); + } } + block_rc } fn parse_line(pair: Pair) -> Vec { @@ -229,6 +353,10 @@ fn parse_command(pair: Pair) -> Command { name: Some(name.to_owned()), text: text.to_owned(), }, + [Keyword(keyword), Keyword(variable), Keyword(equals), value] if keyword.eq("define") && equals.eq("=") => Define { + variable: variable.to_owned(), + value: value.clone(), + }, [Keyword(keyword), Str(food), tail @ ..] if keyword.eq("eat") => Eat { food: food.to_owned(), politely: match tail { @@ -293,7 +421,7 @@ fn parse_token(pair: Pair) -> Token { // ===== pub struct State { - script: Script, + script: Rc>, } impl State { @@ -305,20 +433,15 @@ impl State { pub fn next(&mut self) -> Option { while let Some(command) = self.next_command() { - if command.is_blocking() { - return Some(match command { - Command::Say { name, text } => Event::Say { - name: name.clone(), - text: text.clone() - }, - _ => unimplemented!(), - }); + let event = command.execute(&self.script); + if event.as_ref().map_or(false, |event| event.is_blocking()) { + return event; } } None } - fn next_command(&mut self) -> Option<&Command> { - self.script.next() + fn next_command(&mut self) -> Option { + self.script.borrow_mut().next() } } diff --git a/renrs/src/rpy.pest b/renrs/src/rpy.pest index 6f2371f..333bc7a 100644 --- a/renrs/src/rpy.pest +++ b/renrs/src/rpy.pest @@ -43,7 +43,7 @@ Number = @{ // comments are a # followed by // any Number of non-newline characters -// TODO: Inline comments broken by no implicit whitespace +// FIXME: Inline comments broken by no implicit whitespace COMMENT = _{ "#" ~ char* } Colon = { ":" }