Get code blocks working

main
Elnu 1 year ago
parent 59f0f8258a
commit f178baf1e6

@ -1,3 +1,13 @@
"Bob sat on the bench."
"Bob" "Good morning!"
"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!"

@ -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;
}

@ -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<Token>),
@ -31,6 +32,73 @@ impl Token {
use Token::*;
// Parsing types
type Line = Vec<Token>;
// Indented command block
#[derive(Debug)]
struct Block<T> {
elements: Vec<BlockElement<T>>,
next: Option<usize>,
}
impl<T> Block<T> {
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<T> Default for Block<T> {
fn default() -> Self {
Self {
elements: Vec::new(),
next: Some(0),
}
}
}
#[derive(Debug)]
enum BlockElement<T> {
Command(T),
Block(Block<T>),
}
type LineBlock = Block<Line>;
type CommandBlock = Block<Command>;
type Script = CommandBlock;
// Parsed script commands
#[derive(Debug)]
#[allow(dead_code)]
@ -55,76 +123,144 @@ pub enum Event {
Say { name: Option<String>, text: String },
}
// ==========================================
// Step 1 parsing
// converting from pest pairs to Token blocks
// ==========================================
// Tokenize raw script string
fn tokenize(script: &str) -> Vec<Vec<Token>> {
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<Rule>) -> 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<Vec<Token>> {
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<Token>) -> 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<Token>) -> String {
description
}
// Parse file into commands
fn parse_file(file_path: PathBuf) -> Vec<Command> {
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<Command>,
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<Command> {
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()
}
}

@ -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 }
block_content = _{
line ~ (whitespace+ ~ line)* ~ (NEWLINE | EOI) ~ block*
}
Loading…
Cancel
Save