diff --git a/demo/demo.rpy b/demo/demo.rpy index 58d5e20..99c42ad 100644 --- a/demo/demo.rpy +++ b/demo/demo.rpy @@ -1,3 +1,13 @@ "Bob sat on the bench." "Bob" "Good morning!" -eat "potato" True \ No newline at end of file + "Bob" "I am in a block now!" + "Bob" "Isn't this cool?" +"Bob" "And now we're back in normal indentation." + "Bob" "We can even go multiple levels in!" + "Bob" "How far does the rabbit hole go?" + "Bob" "rabbit hole go?" + "Bob" "go?" +"Bob" "Not sure what came of me there." +"Bob" "I suppose I should eat a potato." +eat "potato" True +"Bob" "Yum!" \ No newline at end of file diff --git a/renrs-gui/src/lib.rs b/renrs-gui/src/lib.rs index 5b09f76..3a18476 100644 --- a/renrs-gui/src/lib.rs +++ b/renrs-gui/src/lib.rs @@ -81,7 +81,7 @@ impl App { thread::spawn(move || { let mut complete = false; - for i in 0..len { + for i in 0..(len + 1) { if *kill.lock().unwrap() { break; } diff --git a/renrs/src/lib.rs b/renrs/src/lib.rs index 0b6fd56..916dd69 100644 --- a/renrs/src/lib.rs +++ b/renrs/src/lib.rs @@ -1,3 +1,4 @@ +use core::panic; use std::{fs, path::PathBuf}; use pest::Parser; @@ -8,8 +9,8 @@ use pest_derive::Parser; struct RpyParser; // Raw script tokens -#[derive(Debug)] -pub enum Token { +#[derive(Debug, Clone)] +enum Token { Keyword(String), Str(String), Array(Vec), @@ -31,6 +32,73 @@ impl Token { use Token::*; +// Parsing types + +type Line = Vec; + +// Indented command block +#[derive(Debug)] +struct Block { + elements: Vec>, + next: Option, +} + +impl Block { + fn next(&mut self) -> Option<&T> { + let mut next = match self.next { + Some(next) => next, + None => 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); + next += 1; + break; + }, + BlockElement::Block(block) => { + match block.next() { + Some(command) => { + result = Some(command); + break; + }, + None => { + next += 1; + } + } + } + }; + } + self.next = if count >= next { + Some(next) + } else { + None + }; + result + } +} + +impl Default for Block { + fn default() -> Self { + Self { + elements: Vec::new(), + next: Some(0), + } + } +} + +#[derive(Debug)] +enum BlockElement { + Command(T), + Block(Block), +} + +type LineBlock = Block; +type CommandBlock = Block; +type Script = CommandBlock; + // Parsed script commands #[derive(Debug)] #[allow(dead_code)] @@ -55,76 +123,144 @@ pub enum Event { Say { name: Option, text: String }, } +// ========================================== +// Step 1 parsing +// converting from pest pairs to Token blocks +// ========================================== + // Tokenize raw script string -fn tokenize(script: &str) -> Vec> { +fn parse(script: &str) -> LineBlock { let file = RpyParser::parse(Rule::file, script) .expect("unsuccessful parse") .next() .unwrap(); - // TODO: Init with capacity - let mut lines = Vec::new(); - for line in file.into_inner() { - let mut tokens = Vec::new(); - match line.as_rule() { + parse_block(file) +} + +type Pair<'a> = pest::iterators::Pair<'a, Rule>; + +// Tokenize block +fn parse_block(pair: Pair) -> LineBlock { + let mut block = LineBlock::default(); + for element in pair.into_inner() { + block.elements.push(match element.as_rule() { + Rule::block => BlockElement::Block(parse_block(element)), Rule::line => { - for token in line.into_inner() { - tokens.push(parse_pair(token)); + let line = parse_line(element); + // TODO: For some reason a blank final line is always parsed + if line.len() == 0 { + continue; } - } - Rule::EOI => (), + BlockElement::Command(line) + }, + Rule::EOI => break, // end _ => unreachable!(), - } - // TODO: For some a blank final line is always parsed - if tokens.len() > 0 { - lines.push(tokens); - } + }); + } + block +} + +// Tokenize line +fn parse_line(pair: Pair) -> Line { + let mut tokens = Vec::new(); + for token in pair.into_inner() { + tokens.push(parse_token(token)); } - lines + tokens } -// Parse raw pest data into Token -fn parse_pair(pair: pest::iterators::Pair) -> Token { +// Tokenize token +fn parse_token(pair: Pair) -> Token { let token = pair.as_rule(); + macro_rules! contents { + () => { + pair.into_inner().next().unwrap() + }; + } match token { - Rule::token => {} - _ => panic!("Not a token!"), - }; - let contents = pair.into_inner().next().unwrap(); - let contents_rule = contents.as_rule(); - match contents_rule { Rule::string => { - let data = contents.into_inner().next().unwrap(); - Token::Str(match data.as_rule() { - Rule::single_quote_string_data => data.as_str().replace("\\'", "'"), - Rule::double_quote_string_data => data.as_str().replace("\\\"", "\""), + let contents = contents!(); + Token::Str(match contents.as_rule() { + Rule::single_quote_string_data => contents.as_str().replace("\\'", "'"), + Rule::double_quote_string_data => contents.as_str().replace("\\\"", "\""), _ => unreachable!(), }) } Rule::array => { + let contents = contents!(); let mut array = Vec::new(); for token in contents.into_inner() { - array.push(parse_pair(token)); + array.push(parse_token(token)); } Token::Array(array) } - Rule::boolean => Token::Boolean(match contents.as_str() { + Rule::boolean => Token::Boolean(match pair.as_str() { "True" => true, "False" => false, _ => unreachable!(), }), - Rule::number => Token::Number(contents.as_str().parse().unwrap()), - Rule::keyword => Token::Keyword(contents.as_str().to_owned()), + Rule::number => Token::Number(pair.as_str().parse().unwrap()), + Rule::keyword => Token::Keyword(pair.as_str().to_owned()), __ => unreachable!(), } } -// Tokenize file -fn tokenize_file(file_path: PathBuf) -> Vec> { - let unparsed_file = fs::read_to_string(file_path).expect("cannot find file"); - tokenize(&unparsed_file) + +// ============================================== +// Step 2 reading +// converting from Token blocks to Command blocks +// ============================================== + +// Read file into commands +fn read_file(file_path: &PathBuf) -> CommandBlock { + let line_block = { + let unparsed_file = fs::read_to_string(file_path).expect("cannot find file"); + parse(&unparsed_file) + }; + read_block(&line_block) +} + +// Read line block into command block +fn read_block(block: &LineBlock) -> CommandBlock { + CommandBlock { + elements: block.elements.iter().map(|element| match element { + BlockElement::Command(line) => BlockElement::Command(read_command(&line)), + BlockElement::Block(block) => BlockElement::Block(read_block(&block)), + }).collect(), + ..Default::default() + } +} + +// Read token array to command +fn read_command(line: &Line) -> Command { + 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), Str(food), tail @ ..] if keyword.eq("eat") => Eat { + food: food.to_owned(), + politely: match tail { + [Boolean(politely)] => *politely, + _ => unknown!(), + }, + }, + _ => unknown!(), + } } -fn describe_line(line: &Vec) -> String { +// Line description e.g. [String, Keyword, Array] +// Used in parse_command as feedback for invalid commands +fn describe_line(line: &Line) -> String { let mut description = "[".to_owned(); let mut iter = line.iter(); description.push_str(&format!("{}", iter.next().unwrap().print())); @@ -135,50 +271,18 @@ fn describe_line(line: &Vec) -> String { description } -// Parse file into commands -fn parse_file(file_path: PathBuf) -> Vec { - let token_lines = tokenize_file(file_path); - let mut commands = Vec::new(); - for line in token_lines { - macro_rules! unknown { - () => { - panic!("Unknown command {}", describe_line(&line)) - }; - } - commands.push(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), Str(food), tail @ ..] if keyword.eq("eat") => Eat { - food: food.to_owned(), - politely: match tail { - [Boolean(politely)] => *politely, - _ => unknown!(), - }, - }, - _ => unknown!(), - }); - } - commands -} +// ===== +// State +// ===== pub struct State { - command_queue: Vec, + script: Script, } impl State { pub fn from_file(file: PathBuf) -> State { State { - command_queue: { - let mut q = parse_file(file); - q.reverse(); - q - }, + script: read_file(&file), } } @@ -186,19 +290,18 @@ impl State { while let Some(command) = self.next_command() { if command.is_blocking() { return Some(match command { - Say { name, text } => Event::Say { name, text }, + Say { name, text } => Event::Say { + name: name.clone(), + text: text.clone() + }, _ => unimplemented!(), - }) + }); } } None } - fn next_command(&mut self) -> Option { - if self.command_queue.len() == 0 { - None - } else { - Some(self.command_queue.remove(self.command_queue.len() - 1)) - } + fn next_command(&mut self) -> Option<&Command> { + self.script.next() } } diff --git a/renrs/src/rpy.pest b/renrs/src/rpy.pest index da8e3c3..0834d09 100644 --- a/renrs/src/rpy.pest +++ b/renrs/src/rpy.pest @@ -1,6 +1,3 @@ -// underscores mark are silent rules, are ignored -WHITESPACE = _{ " " } - // characters are anything but newlines char = { !NEWLINE ~ ANY } @@ -12,7 +9,7 @@ token = { string | array | boolean | number | keyword } // KEYWORDS // has to be atomic for no implicit separate (spaces) -keyword = @{ (!(WHITESPACE | NEWLINE) ~ ANY)+ } +keyword = ${ (!(whitespace | NEWLINE) ~ ANY)+ } // STRING single_quote_string_data = @{ ( @@ -29,13 +26,13 @@ string = ${ } // ARRAY -array = { +array = ${ "[" ~ "]" - | "[" ~ NEWLINE* ~ token ~ ("," ~ NEWLINE* ~ token)* ~ NEWLINE* ~ "]" + | "[" ~ whitespace* ~ NEWLINE* ~ whitespace* ~ token ~ ("," ~ whitespace* ~ NEWLINE* ~ whitespace* ~ token)* ~ NEWLINE* ~ "]" } // BOOLEAN -boolean = { "True" | "False" } +boolean = ${ "True" | "False" } // NUMBER number = @{ @@ -49,6 +46,23 @@ number = @{ COMMENT = _{ "#" ~ char* } // lines are comprised of a statement -line = { token+ } +line = @{ (token ~ whitespace+)* ~ token } + +file = { SOI ~ NEWLINE* ~ block_content* ~ NEWLINE* ~ EOI } + +block = { + // The first line in the block + PEEK_ALL ~ PUSH(" "+ | "\t"+) ~ block_content ~ + // Subsequent lines in the block + (PEEK_ALL ~ block_content)* ~ + // Remove the last layer of indentation from the stack when exiting the block + DROP +} + +// can't be called WHITESPACE +// as this would mess up NewBlock +whitespace = _{ " " } -file = { SOI ~ (line ~ (NEWLINE+ ~ line)*)? ~ NEWLINE* ~ EOI } \ No newline at end of file +block_content = _{ + line ~ (whitespace+ ~ line)* ~ (NEWLINE | EOI) ~ block* +} \ No newline at end of file