diff --git a/renrs-gui/src/lib.rs b/renrs-gui/src/lib.rs index 3a18476..b285d60 100644 --- a/renrs-gui/src/lib.rs +++ b/renrs-gui/src/lib.rs @@ -41,7 +41,7 @@ impl App { } fn next(&mut self) { - if let Some(renrs::Event::Say { name, text }) = self.state.next() { + if let Some(renrs::parser::event::Event::Say { name, text }) = self.state.next() { self.text = match name { Some(name) => format!("{name}: {text}"), None => text, diff --git a/renrs/src/lib.rs b/renrs/src/lib.rs index aebc99a..6ec26d6 100644 --- a/renrs/src/lib.rs +++ b/renrs/src/lib.rs @@ -1,424 +1,10 @@ -use core::panic; -use std::{fs, path::PathBuf, collections::HashMap, rc::Rc, cell::RefCell, hash::Hash}; +use std::{path::PathBuf, rc::Rc, cell::RefCell}; -use pest::Parser; -use pest_derive::Parser; -use regex::Regex; - -#[derive(Parser)] -#[grammar = "rpy.pest"] -struct RpyParser; - -// Raw script tokens -#[derive(Debug, Clone)] -enum Token { - Keyword(String), - Str(String), - Array(Vec), - Boolean(bool), - Number(f64), -} - -impl Token { - fn print(&self) -> &str { - use Token::*; - match &self { - Keyword(keyword) => keyword, - Str(_) => "String", - Array(_) => "Array", - Boolean(_) => "Boolean", - Number(_) => "Number", - } - } -} - -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 { - let mut next = match self.next { - Some(next) => next, - None => return None, - }; - if !self.evaluate_control() { - return None; - } - let mut result = None; - let count = self.elements.len(); - for element in &mut self.elements[next..] { - match element { - BlockElement::Command(command) => { - result = Some(command.clone()); - next += 1; - break; - }, - BlockElement::Block(block) => { - match block.borrow_mut().next() { - Some(command) => { - result = Some(command.clone()); - break; - }, - None => { - next += 1; - } - } - } - }; - } - self.next = if count >= next { - Some(next) - } else { - None - }; - result - } - - fn evaluate_control(&self) -> bool { - use Control::*; - self.control.as_ref().map_or(true, |control| match control { - 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, - } - } -} - -#[derive(Debug)] -enum BlockElement { - Command(Command), - Block(Rc>), -} - -#[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, Clone)] -#[allow(dead_code)] -enum Command { - Say { name: Option, text: String }, - Eat { food: String, politely: bool }, - Define { variable: String, value: Token }, -} - -impl Command { - fn execute(&self, context: &Rc>) -> Option { - use Command::*; - 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)) - } -} - -#[derive(Debug)] -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 -// ====== - -type Pair<'a> = pest::iterators::Pair<'a, Rule>; - -// Read file into commands -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) -> Rc> { - let file = RpyParser::parse(Rule::File, script) - .expect("unsuccessful parse") - .next() - .unwrap(); - parse_block(file, None) -} - -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 pair.into_inner() { - elements.push(match pair.as_rule() { - Rule::Control => { - if control.is_some() { - panic!("control statement should not be empty"); - } - control = Some(parse_control(pair)); - continue; - }, - 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 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; - } - subblock_rc - }), - Rule::EOI => break, // end - _ => unreachable!(), - }); - } - elements - }; - if is_root { - block.variables = Some(definitions); - } - } - block_rc -} - -fn parse_line(pair: Pair) -> Vec { - pair - .into_inner() - .map(|pair| parse_token(pair)) - .collect() -} - -fn parse_control(pair: Pair) -> Control { - use Control::*; - use Token::*; - let line = parse_line(pair); - macro_rules! unknown { - () => { - panic!("Unknown control {}", describe_line(&line)) - }; - } - match line.as_slice() { - [Keyword(control), Boolean(condition)] if control.eq("if") => If { - condition: *condition, - }, - _ => unknown!(), - } -} - -fn parse_command(pair: Pair) -> Command { - use Command::*; - use Token::*; - let line = parse_line(pair); - macro_rules! unknown { - () => { - panic!("Unknown command {}", describe_line(&line)) - }; - } - match line.as_slice() { - [Str(text)] => Say { - name: None, - text: text.to_owned(), - }, - [Str(name), Str(text)] => Say { - 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 { - [Boolean(politely)] => *politely, - _ => unknown!(), - }, - }, - _ => unknown!(), - } -} - -// Line description e.g. [String, Keyword, Array] -// Used in parse_command as feedback for invalid commands -fn describe_line(line: &[Token]) -> String { - let mut description = "[".to_owned(); - let mut iter = line.iter(); - description.push_str(&format!("{}", iter.next().unwrap().print())); - for token in iter { - description.push_str(&format!(", {}", token.print())); - } - description.push_str("]"); - description -} - -fn parse_token(pair: Pair) -> Token { - let token = pair.as_rule(); - macro_rules! contents { - () => { - pair.into_inner().next().unwrap() - }; - } - match token { - Rule::String => { - let contents = contents!(); - Token::Str(match contents.as_rule() { - Rule::SingleQuoteStringData => contents.as_str().replace("\\'", "'"), - Rule::DoubleQuoteStringData => contents.as_str().replace("\\\"", "\""), - _ => unreachable!(), - }) - } - Rule::Array => { - let contents = contents!(); - let mut array = Vec::new(); - for token in contents.into_inner() { - array.push(parse_token(token)); - } - Token::Array(array) - } - Rule::Boolean => Token::Boolean(match pair.as_str() { - "True" => true, - "False" => false, - _ => unreachable!(), - }), - Rule::Number => Token::Number(pair.as_str().parse().unwrap()), - Rule::Keyword => Token::Keyword(pair.as_str().to_owned()), - __ => unreachable!(), - } -} - -// ===== -// State -// ===== +pub mod parser; +use parser::block::CommandBlock; +use parser::command::Command; +use parser::event::Event; +use parser::parse_file; pub struct State { script: Rc>, @@ -444,4 +30,4 @@ impl State { fn next_command(&mut self) -> Option { self.script.borrow_mut().next() } -} +} \ No newline at end of file diff --git a/renrs/src/parser/block.rs b/renrs/src/parser/block.rs new file mode 100644 index 0000000..6602d20 --- /dev/null +++ b/renrs/src/parser/block.rs @@ -0,0 +1,162 @@ +use std::{rc::Rc, cell::RefCell, collections::HashMap}; + +use super::{command::{Command, parse_command}, Pair, token::Token, Rule, control::{parse_control, Control}}; + + +#[derive(Debug)] +pub struct CommandBlock { + parent: Option>>, + control: Option, + elements: Vec, + next: Option, + variables: Option>>>, +} + +impl CommandBlock { + pub fn next(&mut self) -> Option { + let mut next = match self.next { + Some(next) => next, + None => return None, + }; + if !self.evaluate_control() { + return None; + } + let mut result = None; + let count = self.elements.len(); + for element in &mut self.elements[next..] { + match element { + BlockElement::Command(command) => { + result = Some(command.clone()); + next += 1; + break; + }, + BlockElement::Block(block) => { + match block.borrow_mut().next() { + Some(command) => { + result = Some(command.clone()); + break; + }, + None => { + next += 1; + } + } + } + }; + } + self.next = if count >= next { + Some(next) + } else { + None + }; + result + } + + fn evaluate_control(&self) -> bool { + use Control::*; + self.control.as_ref().map_or(true, |control| match control { + If { condition } => *condition, + }) + } + + pub 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, + } + } +} + +#[derive(Debug)] +enum BlockElement { + Command(Command), + Block(Rc>), +} + +pub 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 pair.into_inner() { + elements.push(match pair.as_rule() { + Rule::Control => { + if control.is_some() { + panic!("control statement should not be empty"); + } + control = Some(parse_control(pair)); + continue; + }, + 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 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() { + // TODO: Sublock-scoped variables + 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; + } + subblock_rc + }), + Rule::EOI => break, // end + _ => unreachable!(), + }); + } + elements + }; + if is_root { + block.variables = Some(definitions); + } + } + block_rc +} \ No newline at end of file diff --git a/renrs/src/parser/command.rs b/renrs/src/parser/command.rs new file mode 100644 index 0000000..235bfc9 --- /dev/null +++ b/renrs/src/parser/command.rs @@ -0,0 +1,58 @@ +use std::{rc::Rc, cell::RefCell}; + +use super::{token::Token, block::CommandBlock, event::Event, Pair, utils::{describe_line, parse_line}}; + +#[derive(Debug, Clone)] +#[allow(dead_code)] +pub enum Command { + Say { name: Option, text: String }, + Eat { food: String, politely: bool }, + Define { variable: String, value: Token }, +} + +impl Command { + pub fn execute(&self, context: &Rc>) -> Option { + use Command::*; + 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)) + } +} + +pub fn parse_command(pair: Pair) -> Command { + use Command::*; + use Token::*; + let line = parse_line(pair); + macro_rules! unknown { + () => { + panic!("Unknown command {}", describe_line(&line)) + }; + } + match line.as_slice() { + [Str(text)] => Say { + name: None, + text: text.to_owned(), + }, + [Str(name), Str(text)] => Say { + 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 { + [Boolean(politely)] => *politely, + _ => unknown!(), + }, + }, + _ => unknown!(), + } +} \ No newline at end of file diff --git a/renrs/src/parser/control.rs b/renrs/src/parser/control.rs new file mode 100644 index 0000000..06bb5bd --- /dev/null +++ b/renrs/src/parser/control.rs @@ -0,0 +1,32 @@ +use super::{token::Token, Pair, utils::{describe_line, parse_line}}; + +#[derive(Debug)] +#[allow(dead_code)] +pub enum Control { + If { condition: bool }, +} + +impl Control { + pub fn has_variable_scope(&self) -> bool { + match self { + _ => false, + } + } +} + +pub fn parse_control(pair: Pair) -> Control { + use Control::*; + use Token::*; + let line = parse_line(pair); + macro_rules! unknown { + () => { + panic!("Unknown control {}", describe_line(&line)) + }; + } + match line.as_slice() { + [Keyword(control), Boolean(condition)] if control.eq("if") => If { + condition: *condition, + }, + _ => unknown!(), + } +} \ No newline at end of file diff --git a/renrs/src/parser/event.rs b/renrs/src/parser/event.rs new file mode 100644 index 0000000..d1b84e4 --- /dev/null +++ b/renrs/src/parser/event.rs @@ -0,0 +1,46 @@ +use std::{rc::Rc, cell::RefCell, collections::HashMap}; + +use regex::Regex; + +use super::{block::CommandBlock, token::Token}; + +#[derive(Debug)] +pub enum Event { + Say { name: Option, text: String }, +} + +impl Event { + pub fn is_blocking(&self) -> bool { + use Event::*; + match self { + Say { .. } => true, + } + } + + pub 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() +} \ No newline at end of file diff --git a/renrs/src/parser/mod.rs b/renrs/src/parser/mod.rs new file mode 100644 index 0000000..8bb7fc8 --- /dev/null +++ b/renrs/src/parser/mod.rs @@ -0,0 +1,33 @@ +pub mod token; +pub mod command; +pub mod control; +pub mod event; +pub mod block; +mod utils; + +pub type Pair<'a> = pest::iterators::Pair<'a, Rule>; + +use std::{path::PathBuf, rc::Rc, cell::RefCell, fs}; + +pub use pest::Parser; +use pest_derive::Parser; + +use block::{CommandBlock, parse_block}; + +#[derive(Parser)] +#[grammar = "rpy.pest"] +pub struct RpyParser; + +// Read file into commands +pub fn parse_file(file_path: &PathBuf) -> Rc> { + let unparsed_file = fs::read_to_string(file_path).expect("cannot find file"); + parse(&unparsed_file) +} + +pub fn parse(script: &str) -> Rc> { + let file = RpyParser::parse(Rule::File, script) + .expect("unsuccessful parse") + .next() + .unwrap(); + parse_block(file, None) +} \ No newline at end of file diff --git a/renrs/src/parser/token.rs b/renrs/src/parser/token.rs new file mode 100644 index 0000000..dc436c0 --- /dev/null +++ b/renrs/src/parser/token.rs @@ -0,0 +1,82 @@ +use super::{Rule, Pair}; + +// Raw script tokens +#[derive(Debug, Clone)] +pub enum Token { + Keyword(String), + Str(String), + Array(Vec), + Boolean(bool), + Number(f64), +} + +impl Token { + pub fn print(&self) -> &str { + use Token::*; + match &self { + Keyword(keyword) => keyword, + Str(_) => "String", + Array(_) => "Array", + Boolean(_) => "Boolean", + Number(_) => "Number", + } + } +} + +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(), + } + } +} + +pub fn parse_token(pair: Pair) -> Token { + let token = pair.as_rule(); + macro_rules! contents { + () => { + pair.into_inner().next().unwrap() + }; + } + match token { + Rule::String => { + let contents = contents!(); + Token::Str(match contents.as_rule() { + Rule::SingleQuoteStringData => contents.as_str().replace("\\'", "'"), + Rule::DoubleQuoteStringData => contents.as_str().replace("\\\"", "\""), + _ => unreachable!(), + }) + } + Rule::Array => { + let contents = contents!(); + let mut array = Vec::new(); + for token in contents.into_inner() { + array.push(parse_token(token)); + } + Token::Array(array) + } + Rule::Boolean => Token::Boolean(match pair.as_str() { + "True" => true, + "False" => false, + _ => unreachable!(), + }), + Rule::Number => Token::Number(pair.as_str().parse().unwrap()), + Rule::Keyword => Token::Keyword(pair.as_str().to_owned()), + __ => unreachable!(), + } +} \ No newline at end of file diff --git a/renrs/src/parser/utils.rs b/renrs/src/parser/utils.rs new file mode 100644 index 0000000..566a909 --- /dev/null +++ b/renrs/src/parser/utils.rs @@ -0,0 +1,21 @@ +use super::{Pair, token::{Token, parse_token}}; + +pub fn parse_line(pair: Pair) -> Vec { + pair + .into_inner() + .map(|pair| parse_token(pair)) + .collect() +} + +// Line description e.g. [String, Keyword, Array] +// Used in parse_command as feedback for invalid commands +pub fn describe_line(line: &[Token]) -> String { + let mut description = "[".to_owned(); + let mut iter = line.iter(); + description.push_str(&format!("{}", iter.next().unwrap().print())); + for token in iter { + description.push_str(&format!(", {}", token.print())); + } + description.push_str("]"); + description +} \ No newline at end of file