use std::{collections::HashSet, io, path::{Path, PathBuf}, rc::Rc};
use asca::rule::RuleGroup;
use colored::Colorize;
use super::super::util;
use super::super::seq::{ASCAConfig, Entry, InputKind, RuleFilter};
use super::super::parse::{parse_alias, parse_rsca, parse_wsca};
use super::lexer::{Position, Token, TokenKind};
pub(crate) struct Parser<'a> {
token_list: Vec<Token>,
pos: usize,
curr_tkn: Token,
path: &'a Path,
}
impl<'a> Parser<'a> {
pub(crate) fn new(lst: Vec<Token>, path: &'a Path) -> Self {
let mut s = Self {
token_list: lst,
pos: 0,
curr_tkn: Token { kind: TokenKind::Eof, value: Rc::default(), position: Position::new(0, 0, 0, 1 ) },
path,
};
s.curr_tkn = s.token_list[s.pos].clone();
s
}
fn has_more_tokens(&self) -> bool { self.pos < self.token_list.len() }
fn advance(&mut self) {
self.pos += 1;
self.curr_tkn = if self.has_more_tokens() {
self.token_list[self.pos].clone()
} else {
let last_pos = self.token_list.last().unwrap().position;
Token { kind: TokenKind::Eof, value: Rc::default(), position: Position::new(last_pos.s_line, last_pos.s_pos, last_pos.e_line, last_pos.e_line+1) }
};
if self.curr_tkn.kind == TokenKind::Comment {
self.advance();
}
}
fn peek(&self, knd: TokenKind) -> bool { self.curr_tkn.kind == knd }
#[allow(dead_code)]
fn peek_next(&self, knd: TokenKind) -> bool { self.token_list[self.pos+1].kind == knd }
fn expect(&mut self, knd: TokenKind) -> bool {
if self.curr_tkn.kind == knd {
self.advance();
true
} else {
false
}
}
fn eat(&mut self) -> Token {
let token = self.curr_tkn.clone();
self.advance();
token
}
fn eat_expect(&mut self, knd: TokenKind) -> Option<Token> {
if self.peek(knd) {
Some(self.eat())
} else {
None
}
}
fn skip_comments(&mut self) {
if self.peek(TokenKind::Comment) {
self.advance();
}
}
fn error(&self, message: String) -> io::Error {
io::Error::other(format!("{} {}", "Config Parse Error:".bright_red(), message))
}
fn line_lookahead(&self, knd: TokenKind) -> bool {
let mut pos = self.pos;
let line_num = self.curr_tkn.position.s_line;
while pos < self.token_list.len() && self.token_list[pos].position.s_line == line_num && self.token_list[pos].kind != TokenKind::Eof && self.token_list[pos].kind != TokenKind::Comment {
if self.token_list[pos].kind == knd {
return true
}
pos += 1;
}
false
}
fn get_ident(&mut self) -> io::Result<(Vec<Rc<str>>, Token)> {
let mut has_arrow = false;
let mut token_list = Vec::new();
let mut len = 0;
loop {
if let Some(colon) = self.eat_expect(TokenKind::Colon) {
if token_list.is_empty() {
return Err(self.error(format!("Expected tag, but received ':' at line {}:{}", colon.position.s_line, colon.position.s_pos)))
}
break;
}
if self.eat_expect(TokenKind::Gt).is_some() {
if has_arrow {
return Err(self.error("Can't have multiple arrows".to_string()))
}
has_arrow = true;
len = token_list.len();
continue;
}
if let Some(lit) = self.eat_expect(TokenKind::Literal) {
token_list.push(lit);
continue;
}
if token_list.is_empty() {
return Err(self.error(format!("Expected tag, but received '{}' at line {}:{}", self.curr_tkn.value, self.curr_tkn.position.s_line, self.curr_tkn.position.s_pos)))
} else if self.curr_tkn.kind == TokenKind::Eof {
return Err(self.error(format!("Unexpected end of file at line {}:{}", self.curr_tkn.position.s_line, self.curr_tkn.position.s_pos)))
} else {
return Err(self.error(format!("Unexpected token '{}' at line {}:{}", self.curr_tkn.kind, self.curr_tkn.position.s_line, self.curr_tkn.position.s_pos)))
}
}
self.skip_comments();
if !has_arrow {
match token_list.len().cmp(&1) {
std::cmp::Ordering::Equal => return Ok((vec![], token_list[0].clone())),
std::cmp::Ordering::Greater => return Err(self.error(format!(
"More than one tag found on line '{}:{}'",
self.token_list.last().unwrap().position.s_line,
self.token_list.last().unwrap().position.s_pos
))),
std::cmp::Ordering::Less => unreachable!(),
}
}
if token_list.len() > len + 1 {
return Err(self.error(format!(
"More than one tag found on line '{}:{}'",
self.token_list.last().unwrap().position.s_line,
self.token_list.last().unwrap().position.s_pos
)))
}
let tag = token_list[len].clone();
token_list.pop();
let inputs = token_list.into_iter().map(|tkn| {
tkn.value
}).collect();
Ok((inputs, tag))
}
fn get_entries(&mut self, tag: Rc<str>) -> io::Result<Vec<Entry>> {
let mut entries = Vec::new();
if let Some(e) = self.get_entry()? {
entries.push(e);
while self.has_more_tokens() {
match self.get_entry()? {
Some(e) => entries.push(e),
None => break,
}
}
}
if entries.is_empty() {
return Err(self.error(format!("No rule entries found for sequence '{tag}'")))
}
Ok(entries)
}
fn get_entry(&mut self) -> io::Result<Option<Entry>> {
if self.line_lookahead(TokenKind::Colon) {
return Ok(None)
}
let Some(rule) = self.eat_expect(TokenKind::Literal) else {
return Ok(None)
};
let filter = self.get_filter()?;
if !self.expect(TokenKind::Semi) {
return Err(self.error(format!("Expected semicolon, received {} at line {}:{}", self.curr_tkn.kind, self.curr_tkn.position.s_line, self.curr_tkn.position.s_pos)))
}
self.skip_comments();
let mut file_path = self.path.to_path_buf();
let rule_file = rule.value.trim();
file_path.set_file_name(rule_file);
if file_path.is_file() {
let entry_rules = parse_rsca(&file_path)?;
match filter {
Some(rf) => Ok(Some(self.parse_entry(entry_rules, rf, file_path, PathBuf::from(rule_file))?)),
None => Ok(Some(Entry::from(file_path, entry_rules))),
}
} else {
Err(self.error(format!("Cannot find {file_path:?} at line {}:{}", rule.position.s_line, rule.position.s_pos)))
}
}
fn get_filter(&mut self) -> io::Result<Option<RuleFilter>> {
match self.curr_tkn.kind {
TokenKind::Bang => { self.advance();
let pos = self.curr_tkn.position;
let list = self.get_filter_list()?;
match list.len().cmp(&1) {
std::cmp::Ordering::Greater => Ok(Some(RuleFilter::WithoutMult(list))),
std::cmp::Ordering::Equal => Ok(Some(RuleFilter::Without(list[0].clone()))),
std::cmp::Ordering::Less => Err(self.error(format!("Empty filter list at line {}:{}", pos.s_line, pos.s_pos))),
}
},
TokenKind::Tilde => { self.advance();
let pos = self.curr_tkn.position;
let list = self.get_filter_list()?;
match list.len().cmp(&1) {
std::cmp::Ordering::Greater => Ok(Some(RuleFilter::OnlyMult(list))),
std::cmp::Ordering::Equal => Ok(Some(RuleFilter::Only(list[0].clone()))),
std::cmp::Ordering::Less => Err(self.error(format!("Empty filter list at line {}:{}", pos.s_line, pos.s_pos))),
}
},
TokenKind::Gt => { self.advance();
let pos = self.curr_tkn.position;
let list = self.get_filter_list()?;
match list.len().cmp(&1) {
std::cmp::Ordering::Equal => Ok(Some(RuleFilter::After(list[0].clone()))),
std::cmp::Ordering::Greater => Err(self.error(format!("'Greater Than' operator cannot take more than one rule @ {}:{}", pos.s_line, pos.s_pos))),
std::cmp::Ordering::Less => Err(self.error(format!("Empty filter list at line {}:{}", pos.s_line, pos.s_pos))),
}
}
TokenKind::GtEq => { self.advance();
let pos = self.curr_tkn.position;
let list = self.get_filter_list()?;
match list.len().cmp(&1) {
std::cmp::Ordering::Equal => Ok(Some(RuleFilter::AfterInc(list[0].clone()))),
std::cmp::Ordering::Greater => Err(self.error(format!("'Greater Than Equal' operator cannot take more than one rule @ {}:{}", pos.s_line, pos.s_pos))),
std::cmp::Ordering::Less => Err(self.error(format!("Empty filter list at line {}:{}", pos.s_line, pos.s_pos))),
}
}
TokenKind::Lt => { self.advance();
let pos = self.curr_tkn.position;
let list = self.get_filter_list()?;
match list.len().cmp(&1) {
std::cmp::Ordering::Equal => Ok(Some(RuleFilter::Before(list[0].clone()))),
std::cmp::Ordering::Greater => Err(self.error(format!("'Less Than' operator cannot take more than one rule @ {}:{}", pos.s_line, pos.s_pos))),
std::cmp::Ordering::Less => Err(self.error(format!("Empty filter list at line {}:{}", pos.s_line, pos.s_pos))),
}
}
TokenKind::LtEq => { self.advance();
let pos = self.curr_tkn.position;
let list = self.get_filter_list()?;
match list.len().cmp(&1) {
std::cmp::Ordering::Equal => Ok(Some(RuleFilter::BeforeInc(list[0].clone()))),
std::cmp::Ordering::Greater => Err(self.error(format!("'Less Than Equal' operator cannot take more than one rule @ {}:{}", pos.s_line, pos.s_pos))),
std::cmp::Ordering::Less => Err(self.error(format!("Empty filter list at line {}:{}", pos.s_line, pos.s_pos))),
}
}
_ => Ok(None)
}
}
fn get_filter_list(&mut self) -> io::Result<Vec<String>>{
let mut filters = Vec::new();
if let Some(f) = self.eat_expect(TokenKind::String) {
filters.push(f.value.to_string());
loop {
if self.peek(TokenKind::Semi) || self.peek(TokenKind::Eof) { break; }
if !self.expect(TokenKind::Comma) {
let pos = self.curr_tkn.position;
return Err(self.error(format!("Expected comma, found {} at {}:{}", self.curr_tkn.kind, pos.s_line, pos.s_pos)))
}
match self.eat_expect(TokenKind::String) {
Some(f) => filters.push(f.value.to_string()),
None => if self.peek(TokenKind::Semi) || self.peek(TokenKind::Eof) {
break;
} else {
return Err(self.error(format!("Expected a rule name, found {} at line {}:{}", self.curr_tkn.kind, self.curr_tkn.position.s_line, self.curr_tkn.position.s_pos)))
},
}
}
}
if filters.is_empty() {
return Err(self.error(format!("Expected a rule name, found {} at line {}:{}", self.curr_tkn.kind, self.curr_tkn.position.s_line, self.curr_tkn.position.s_pos)))
}
Ok(filters)
}
fn parse_entry(&mut self, entry_rules: Vec<RuleGroup>, filter: RuleFilter, file_path: PathBuf, rule_file: PathBuf) -> io::Result<Entry> {
let mut file_path = file_path.clone();
let file_stem = rule_file.file_stem().expect("Caller insures file exists").to_str().unwrap();
file_path.set_file_name(format!("{}{}", file_stem, filter.as_file_string()));
match filter {
RuleFilter::Only(rule_str) => {
match entry_rules.iter().find(|r| r.name.to_lowercase() == rule_str.to_lowercase()).cloned() {
Some(rule) => Ok(Entry::from(file_path, vec![rule])),
None => Err(self.error(format!("Could not find rule '{}' in '{}'. Did you mean {}?", rule_str, rule_file.to_str().unwrap(), self.lev(entry_rules, &rule_str).yellow())))
}
},
RuleFilter::Without(rule_str) => {
let entries = entry_rules.iter().filter(|r| r.name.to_lowercase() != rule_str.to_lowercase()).cloned().collect::<Vec<_>>();
if entries.len() == entry_rules.len() {
return Err(self.error(format!("Could not find rule '{}' in '{}'. Did you mean {}?", rule_str, rule_file.to_str().unwrap(), self.lev(entry_rules, &rule_str).yellow())))
}
Ok(Entry::from(file_path, entries))
},
RuleFilter::Before(rule_str) => {
match entry_rules.iter().position(|r| r.name.to_lowercase() == rule_str.to_lowercase()) {
Some(pos) => Ok(Entry::from(file_path, entry_rules[0..pos].to_vec())),
None => Err(self.error(format!("Could not find rule '{}' in '{}'. Did you mean {}?", rule_str, rule_file.to_str().unwrap(), self.lev(entry_rules, &rule_str).yellow())))
}
},
RuleFilter::BeforeInc(rule_str) => {
match entry_rules.iter().position(|r| r.name.to_lowercase() == rule_str.to_lowercase()) {
Some(pos) => Ok(Entry::from(file_path, entry_rules[0..=pos].to_vec())),
None => Err(self.error(format!("Could not find rule '{}' in '{}'. Did you mean {}?", rule_str, rule_file.to_str().unwrap(), self.lev(entry_rules, &rule_str).yellow())))
}
},
RuleFilter::After(rule_str) => {
match entry_rules.iter().position(|r| r.name.to_lowercase() == rule_str.to_lowercase()) {
Some(pos) => Ok(Entry::from(file_path, entry_rules[pos+1..].to_vec())),
None => Err(self.error(format!("Could not find rule '{}' in '{}'. Did you mean {}?", rule_str, rule_file.to_str().unwrap(), self.lev(entry_rules, &rule_str).yellow())))
}
},
RuleFilter::AfterInc(rule_str) => {
match entry_rules.iter().position(|r| r.name.to_lowercase() == rule_str.to_lowercase()) {
Some(pos) => Ok(Entry::from(file_path, entry_rules[pos..].to_vec())),
None => Err(self.error(format!("Could not find rule '{}' in '{}'. Did you mean {}?", rule_str, rule_file.to_str().unwrap(), self.lev(entry_rules, &rule_str).yellow())))
}
},
RuleFilter::OnlyMult(filters) => {
let entries = filters.iter().map(|filter| {
match entry_rules.iter().find(|r| r.name.to_lowercase() == filter.to_lowercase()) {
Some(entry) => Ok(entry.clone()),
None => Err(self.error(format!("Could not find rule '{}' in '{}'. Did you mean {}?", filter, rule_file.to_str().unwrap(), self.lev(entry_rules.clone(), filter).yellow())))
}
}).collect::<io::Result<Vec<RuleGroup>>>()?;
Ok(Entry::from(file_path, entries))
},
RuleFilter::WithoutMult(mut filters) => {
filters = filters.iter().map(|f| f.to_lowercase()).collect();
let entries = entry_rules.iter().filter(|r| !filters.contains(&r.name.to_lowercase())).cloned().collect::<Vec<_>>();
if entries.len() == entry_rules.len() {
return Err(self.error(format!("Could not find any of the excluded rules in '{}'.\nMake sure the rule names match exactly!", rule_file.to_str().unwrap())))
} else if entries.len() != entry_rules.len() - filters.len() {
return Err(self.error(format!("Could not find one or more of the excluded rules in '{}'.\nMake sure the rule names match exactly!", rule_file.to_str().unwrap())))
}
Ok(Entry::from(file_path, entries))
},
}
}
fn lev(&self, entry_rules: Vec<RuleGroup>, filter: &str) -> String {
let mut best_lev = usize::MAX;
let mut best_match= "";
let names = entry_rules.iter().map(|e| &e.name);
for name in names {
let len = util::lev(name, filter);
if len < best_lev {
best_match = name;
best_lev = len;
}
}
best_match.to_string()
}
fn parse_inputs(&self, inputs: &[Rc<str>], tag: &Rc<str>) -> io::Result<(Option<Rc<str>>, Vec<InputKind>)> {
let mut alias = None;
let mut parsed_input = Vec::new();
for item in inputs {
let mut file_path = self.path.to_path_buf();
let item_file = item.trim();
file_path.set_file_name(item_file);
if file_path.is_file() {
if let Some(ext) = file_path.extension() {
match ext.to_str() {
Some("wsca") => { parsed_input.push(InputKind::WordFile(item.clone())); continue; },
Some("alias") => {
if alias.is_some() {
return Err(self.error(format!("A sequence can only have one alias file. seq '{}' @ '{}'", tag.yellow(), item.yellow())))
}
alias = Some(item.clone());
continue;
},
_ => {}
}
}
let maybe_alias = parse_alias(&file_path);
if maybe_alias.is_ok() {
if alias.is_some() {
return Err(self.error(format!("A sequence can only have one alias file. seq '{}' @ '{}'", tag.yellow(), item.yellow())))
}
alias = Some(item.clone());
continue;
}
match parse_wsca(&file_path) {
Ok(_) => parsed_input.push(InputKind::WordFile(item.clone())),
Err(e) => {
eprintln!("{} could not confirm that '{}' as an input to the sequence '{}' was to be a word or alias file due to one or more errors while parsing:", "asca:".bright_red(), item_file.yellow(), tag.yellow());
return Err(e)
},
}
} else if file_path.extension().is_some() {
return Err(self.error(format!("File {} could not be found. seq '{}' @ '{}'", format!("{file_path:?}").yellow(), tag.yellow(), item.yellow())))
} else {
parsed_input.push(InputKind::FromTag(item.clone()));
}
}
Ok((alias, parsed_input))
}
fn get_seq(&mut self) -> io::Result<Option<ASCAConfig>> {
self.skip_comments();
if self.curr_tkn.kind == TokenKind::Eof {
return Ok(None)
}
let (input, tag_token) = self.get_ident()?;
let tag = tag_token.value;
let (alias, input) = self.parse_inputs(&input, &tag)?;
let entries = self.get_entries(tag.clone())?;
Ok(Some(ASCAConfig { tag, alias, input, entries }))
}
pub(crate) fn parse(&mut self) -> io::Result<Vec<ASCAConfig>> {
let mut tag_set = HashSet::new();
let mut conf = Vec::new();
while self.curr_tkn.kind != TokenKind::Eof {
let Some(seq ) = self.get_seq()? else { break };
if !tag_set.insert(seq.tag.clone()) {
return Err(self.error(format!("tag '{}' declared more than once in config", seq.tag)))
}
conf.push(seq);
}
for c in &conf {
for from in c.get_from_tags() {
if !tag_set.contains(&from) {
return Err(self.error(format!("piped tag '{}' found in sequence '{}' does not exist", from.yellow(), c.tag.yellow())))
}
}
}
for pipe in conf.iter().filter(|c| !c.get_from_tags().is_empty()).collect::<Vec<_>>() {
if self.detect_tag_loop(&conf, pipe) {
return Err(self.error(format!("infinite pipeline loop detected in tag '{}'", pipe.tag)))
}
}
Ok(conf)
}
fn has_loop_rec(conf: &[ASCAConfig], set: HashSet<Rc<str>>, from: Rc<str>) -> bool {
let mut set = set;
let head = conf.iter().find(|c| c.tag == from).unwrap();
if !set.insert(from.clone()) {
return true
}
for from in head.get_from_tags() {
if from == head.tag || Self::has_loop_rec(conf, set.clone(), from.clone()) {
return true
}
}
false
}
fn detect_tag_loop(&self, conf: &[ASCAConfig], head: &ASCAConfig) -> bool {
for from in head.get_from_tags() {
if from == head.tag || Self::has_loop_rec(conf, HashSet::new(), from.clone()) {
return true
}
}
false
}
}
#[cfg(test)]
mod parser_tests {
use super::*;
use super::super::lexer::{Lexer, Token};
fn setup(test_str: &str) -> Vec<Token> {
match Lexer::new(&String::from(test_str).chars().collect::<Vec<_>>()).tokenise() {
Ok(tkns) => tkns,
Err(e) => {
println!("{}", e.to_string());
assert!(false);
unreachable!()
},
}
}
#[test]
fn test_simple_tag_loop() {
let test_input= String::from(
"beta > alpha:\n\
examples/indo-european/germanic/pgmc/pre.rsca;
alpha > beta:\n\
examples/indo-european/germanic/pgmc/pre.rsca ! \"Laryngeal colouring\";\n\
examples/indo-european/germanic/pgmc/pre.rsca ~ \"Cowgill's Law\";"
);
let maybe_result = Parser:: new(setup(&test_input), Path::new("")).parse();
assert!(maybe_result.is_err());
if let Err(e) = maybe_result {
assert_eq!(e.to_string(), io::Error::other(format!("{} infinite pipeline loop detected in tag 'alpha'", "Config Parse Error:".bright_red())).to_string())
} else {
assert!(false)
}
}
#[test]
fn test_pipe_does_not_exist() {
let test_input= String::from(
"beta > alpha:\n\
examples/indo-european/germanic/pgmc/pre.rsca;
gamma > beta:\n\
examples/indo-european/germanic/pgmc/pre.rsca ! \"Laryngeal colouring\";\n\
examples/indo-european/germanic/pgmc/pre.rsca ~ \"Cowgill's Law\";"
);
let maybe_result = Parser:: new(setup(&test_input), Path::new("")).parse();
assert!(maybe_result.is_err());
if let Err(e) = maybe_result {
assert_eq!(e.to_string(), io::Error::other(format!("{} piped tag '{}' found in sequence '{}' does not exist",
"Config Parse Error:".bright_red(), "gamma".yellow(), "beta".yellow())).to_string())
} else {
assert!(false)
}
}
#[test]
fn test_with_words() {
let test_input= String::from(
"examples/indo-european/pie-uvular-common.wsca examples/indo-european/pie-pronouns.wsca > beta:\n \
examples/indo-european/germanic/pgmc/pre.rsca ! \"Laryngeal colouring\";\n\
examples/indo-european/germanic/pgmc/pre.rsca ~ \"Dental Cluster Simplification\", \"Cowgill's Law\";"
);
let maybe_result = Parser:: new(setup(&test_input), Path::new("")).parse();
assert!(maybe_result.is_ok());
let result = maybe_result.unwrap();
assert_eq!(result[0].tag, "beta".into());
assert_eq!(result[0].alias, None);
assert_eq!(result[0].input, [
InputKind::WordFile("examples/indo-european/pie-uvular-common.wsca".into()),
InputKind::WordFile("examples/indo-european/pie-pronouns.wsca".into())
]);
assert_eq!(result[0].entries[0].name, PathBuf::from("examples/indo-european/germanic/pgmc/pre_excl_laryngeal-colouring"));
assert_eq!(result[0].entries[1].name, PathBuf::from("examples/indo-european/germanic/pgmc/pre_om_dcs-cl"));
}
#[test]
fn test_without_words() {
let test_input= String::from(
"beta: # test\n\
examples/indo-european/germanic/pgmc/pre.rsca ! \"Laryngeal colouring\"; # test\n\
examples/indo-european/germanic/pgmc/pre.rsca ~ \"Dental Cluster Simplification\", \"Cowgill's Law\";"
);
let maybe_result = Parser:: new(setup(&test_input), Path::new("")).parse();
assert!(maybe_result.is_ok());
let result = maybe_result.unwrap();
assert_eq!(result[0].tag, "beta".into());
assert_eq!(result[0].alias, None);
assert_eq!(result[0].input, []);
assert_eq!(result[0].entries[0].name, PathBuf::from("examples/indo-european/germanic/pgmc/pre_excl_laryngeal-colouring"));
assert_eq!(result[0].entries[1].name, PathBuf::from("examples/indo-european/germanic/pgmc/pre_om_dcs-cl"));
let test_input= String::from(
"beta: examples/indo-european/germanic/pgmc/pre.rsca ! \"Laryngeal colouring\"; examples/indo-european/germanic/pgmc/pre.rsca ~ \"Dental Cluster Simplification\", \"Cowgill's Law\";"
);
let maybe_result = Parser:: new(setup(&test_input), Path::new("")).parse();
assert!(maybe_result.is_ok());
let result = maybe_result.unwrap();
assert_eq!(result[0].tag, "beta".into());
assert_eq!(result[0].alias, None);
assert_eq!(result[0].input, []);
assert_eq!(result[0].entries[0].name, PathBuf::from("examples/indo-european/germanic/pgmc/pre_excl_laryngeal-colouring"));
assert_eq!(result[0].entries[1].name, PathBuf::from("examples/indo-european/germanic/pgmc/pre_om_dcs-cl"));
}
#[test]
fn test_mult() {
let test_input= String::from(
"alpha:\n\
examples/indo-european/germanic/pgmc/pre.rsca ! \"Laryngeal colouring\";\n\
examples/indo-european/germanic/pgmc/pre.rsca ~ \"Dental Cluster Simplification\", \"Cowgill's Law\";\n\
\n\
examples/indo-european/pie-uvular-common.wsca examples/indo-european/pie-pronouns.wsca > beta:\n\
examples/indo-european/germanic/pgmc/pre.rsca ! \"Laryngeal colouring\";\n\
examples/indo-european/germanic/pgmc/pre.rsca ~ \"Cowgill's Law\";"
);
let maybe_result = Parser:: new(setup(&test_input), Path::new("")).parse();
assert!(maybe_result.is_ok());
let result = maybe_result.unwrap();
assert_eq!(result[0].tag, "alpha".into());
assert_eq!(result[0].alias, None);
assert_eq!(result[0].input, []);
assert_eq!(result[0].entries[0].name, PathBuf::from("examples/indo-european/germanic/pgmc/pre_excl_laryngeal-colouring"));
assert_eq!(result[0].entries[1].name, PathBuf::from("examples/indo-european/germanic/pgmc/pre_om_dcs-cl"));
assert_eq!(result[1].tag, "beta".into());
assert_eq!(result[1].alias, None);
assert_eq!(result[1].input, [
InputKind::WordFile("examples/indo-european/pie-uvular-common.wsca".into()),
InputKind::WordFile("examples/indo-european/pie-pronouns.wsca".into())
]);
assert_eq!(result[1].entries[0].name, PathBuf::from("examples/indo-european/germanic/pgmc/pre_excl_laryngeal-colouring"));
assert_eq!(result[1].entries[1].name, PathBuf::from("examples/indo-european/germanic/pgmc/pre_only_cowgills-law"));
}
}