use chumsky::prelude::*;
use super::{
TaleExtra,
atoms::{
CELL_ENDINGS, COLON, NEWLINES, NOTHING, PERIOD_OR_SEMICOLON, TABS, chomp_disjoint_newlines,
chomp_separator, dice, ident, ident_normalize,
},
expressions::{arithmetic, number_range_list},
statements::seq_or_statement,
};
use crate::{
ast::{Atom, Expr, RcNode, Script, Statement, Table, TableGroup, TableRows, full_rc_node},
lexer::Token,
};
pub fn script<'src>() -> impl Parser<'src, &'src [Token], RcNode<Statement>, TaleExtra<'src>> + Clone
{
just(Token::Script)
.then(just(Token::Colon))
.ignore_then(ident().map_with(full_rc_node))
.then_ignore(chomp_disjoint_newlines(NOTHING))
.then(
just(Token::Tabs)
.or_not()
.ignore_then(seq_or_statement(NEWLINES))
.then_ignore(chomp_disjoint_newlines(PERIOD_OR_SEMICOLON))
.repeated()
.collect::<Vec<_>>(),
)
.then_ignore(just(Token::End).then(just(Token::Script)))
.then_ignore(just(Token::NewLines).ignored().or(end()))
.map_with(|(name, statements), extra| {
let value = Script::new(name, statements);
Statement::Script(full_rc_node(value, extra))
})
.map_with(full_rc_node)
.boxed()
.labelled("Script Definition")
.as_context()
}
pub fn table<'src>() -> impl Parser<'src, &'src [Token], RcNode<Statement>, TaleExtra<'src>> + Clone
{
just(Token::Table)
.then(just(Token::Colon))
.ignore_then(ident().map_with(full_rc_node))
.then_ignore(chomp_disjoint_newlines(NOTHING))
.then(table_headings())
.then(table_rows())
.map_with(|((name, (roll, tags)), rows), extra| {
let roll = if roll.inner_t().is_empty() {
full_rc_node(rows.inner_t().calc_roll(), extra)
} else {
roll
};
let table = full_rc_node(Table::new(name, roll, tags, rows), extra);
full_rc_node(Statement::Table(table), extra)
})
.boxed()
.labelled("Table Definition")
.as_context()
}
pub fn table_group<'src>()
-> impl Parser<'src, &'src [Token], RcNode<Statement>, TaleExtra<'src>> + Clone {
just(Token::Table)
.then(just(Token::Group))
.then(just(Token::Colon))
.ignore_then(ident().map_with(full_rc_node::<Atom, Atom>))
.then_ignore(chomp_disjoint_newlines(NOTHING))
.then(
tags_directive().or_not().map_with(|maybe_tags, extra| {
maybe_tags.unwrap_or(full_rc_node(Vec::new(), extra))
}),
)
.then(sub_tables_row())
.then(table_group_rows())
.then_ignore(
just(Token::End)
.then(just(Token::Table))
.then(just(Token::Group).or_not()),
)
.then_ignore(just(Token::NewLines).ignored().or(end()))
.try_map_with(|(((name, tags), (roll, sub_names)), sub_rows), extra| {
if sub_names.len() != sub_rows.len() {
return Err(Rich::custom(
extra.span(),
"Table Group rows must all have same number of columns",
));
}
let sub_tables = sub_names
.into_iter()
.zip(sub_rows)
.map(|(sub_name, rows)| {
let full_name =
full_rc_node(ident_normalize(&name.inner_t(), &sub_name), extra);
let table_value = Table::new(full_name, roll.clone(), tags.clone(), rows);
full_rc_node(table_value, extra)
})
.collect();
let value: RcNode<TableGroup> =
full_rc_node(TableGroup::new(name, tags, sub_tables), extra);
Ok(full_rc_node(value, extra))
})
.boxed()
.labelled("Table Group Definition")
.as_context()
}
fn sub_tables_row<'src>()
-> impl Parser<'src, &'src [Token], (RcNode<Expr>, Vec<Atom>), TaleExtra<'src>> + Clone {
dice()
.map_with(full_rc_node)
.then_ignore(just(Token::Tabs))
.then(ident().separated_by(just(Token::Tabs)).collect::<Vec<_>>())
.then_ignore(chomp_disjoint_newlines(NOTHING))
.boxed()
}
fn table_group_rows<'src>()
-> impl Parser<'src, &'src [Token], Vec<RcNode<TableRows>>, TaleExtra<'src>> + Clone {
row_key(NOTHING, TABS)
.then(
seq_or_statement(CELL_ENDINGS).labelled("Table Group Cell")
.separated_by(chomp_separator(PERIOD_OR_SEMICOLON, TABS))
.collect::<Vec<_>>(),
)
.map(|(key, items)| {
items
.into_iter()
.map(|item| (key.clone(), item))
.collect::<Vec<_>>()
})
.then_ignore(chomp_disjoint_newlines(PERIOD_OR_SEMICOLON)).labelled("Table Group Row")
.repeated()
.at_least(1)
.collect::<Vec<_>>()
.try_map_with(|rows, extra| {
let width = rows[0].len();
for (idx, row) in rows.iter().enumerate() {
if row.len() != width {
let err = Err(Rich::custom(
SimpleSpan::new((), 9999..77777),
format!("Table Group rows must all have same number of columns, row {} has {} columns but expected {width}",
idx + 1, row.len()
),
));
return err;
}
}
let iter_rows = rows
.into_iter()
.map(std::iter::IntoIterator::into_iter)
.collect::<Vec<_>>();
let mut columns: Vec<Vec<_>> = Vec::new();
for (rn, row) in iter_rows.into_iter().enumerate() {
for (cn, item) in row.enumerate() {
if rn == 0 {
columns.push(vec![item]);
} else {
columns[cn].push(item);
}
}
}
let columns = columns
.into_iter()
.map(|column| full_rc_node(TableRows::Keyed(column), extra))
.collect();
Ok(columns)
})
.boxed()
.labelled("Table Group Rows")
.as_context()
}
fn table_rows<'src>() -> impl Parser<'src, &'src [Token], RcNode<TableRows>, TaleExtra<'src>> + Clone
{
choice((
table_list(),
table_keyed_form(),
table_block_cell_form(),
table_flat_rows(),
end_table()
.map_with(|(), extra| full_rc_node(TableRows::Empty, extra))
.then_ignore(chomp_disjoint_newlines(NOTHING).or(end())),
))
.boxed()
.labelled("Table Rows")
}
fn end_table<'src>() -> impl Parser<'src, &'src [Token], (), TaleExtra<'src>> + Clone {
just(Token::End).then(just(Token::Table)).ignored()
}
fn table_list<'src>() -> impl Parser<'src, &'src [Token], RcNode<TableRows>, TaleExtra<'src>> + Clone
{
just(Token::List)
.then(just(Token::Colon))
.ignore_then(
ident()
.map_with(|_, extra| {
let span = extra.span().into_range();
Atom::Str(extra.state().get_source_slice(&span).to_string())
})
.separated_by(just(Token::Comma))
.collect::<Vec<_>>(),
)
.map_with(full_rc_node)
.labelled("Table Rows (List)")
.as_context()
.then_ignore(chomp_disjoint_newlines(NOTHING).then(end_table()).or_not())
.then_ignore(chomp_disjoint_newlines(NOTHING).or(end()))
.boxed()
}
fn table_flat_rows<'src>()
-> impl Parser<'src, &'src [Token], RcNode<TableRows>, TaleExtra<'src>> + Clone {
seq_or_statement(CELL_ENDINGS)
.then_ignore(chomp_disjoint_newlines(PERIOD_OR_SEMICOLON))
.repeated()
.at_least(1)
.collect()
.map_with(|rows, extra| full_rc_node(TableRows::Flat(rows), extra))
.labelled("Table Rows (Flat)")
.as_context()
.then_ignore(end_table())
.then_ignore(chomp_disjoint_newlines(NOTHING).or(end()))
.boxed()
}
fn table_keyed_form<'src>()
-> impl Parser<'src, &'src [Token], RcNode<TableRows>, TaleExtra<'src>> + Clone {
row_key(NOTHING, TABS)
.then(seq_or_statement(NEWLINES))
.then_ignore(chomp_disjoint_newlines(NOTHING))
.repeated()
.at_least(1)
.collect()
.map_with(|rows, extra| full_rc_node(TableRows::Keyed(rows), extra))
.labelled("Table Rows (Keyed)")
.as_context()
.then_ignore(end_table())
.then_ignore(chomp_disjoint_newlines(NOTHING).or(end()))
.boxed()
}
fn table_block_cell_form<'src>()
-> impl Parser<'src, &'src [Token], RcNode<TableRows>, TaleExtra<'src>> + Clone {
row_key(NOTHING, TABS) .or(
row_key(COLON, NEWLINES) .then_ignore(
chomp_disjoint_newlines(TABS)
.or_not()
.then(just(Token::Tabs)),
),
)
.then(
seq_or_statement(NEWLINES)
.then_ignore(chomp_disjoint_newlines(NOTHING))
.separated_by(just(Token::Tabs))
.at_least(1)
.collect()
.map_with(|statements: Vec<RcNode<Statement>>, extra| {
full_rc_node(Statement::Sequence(full_rc_node(statements, extra)), extra)
}),
)
.repeated()
.at_least(1)
.collect()
.map_with(|rows, extra| full_rc_node(TableRows::Keyed(rows), extra))
.labelled("Table Rows (Block)")
.as_context()
.then_ignore(end_table())
.then_ignore(chomp_disjoint_newlines(NOTHING).or(end()))
.boxed()
}
fn table_headings<'src>()
-> impl Parser<'src, &'src [Token], (RcNode<Expr>, RcNode<Vec<Atom>>), TaleExtra<'src>> + Clone {
let roll_directive = just(Token::Roll)
.then(just(Token::Colon))
.ignore_then(arithmetic())
.then_ignore(chomp_disjoint_newlines(NOTHING))
.map_with(|item, extra| (item, full_rc_node(Vec::new(), extra)))
.boxed();
let tags_directive =
tags_directive().map_with(|item, extra| (full_rc_node(Expr::Empty, extra), item));
roll_directive
.clone()
.then(tags_directive.clone().or_not())
.map(|(roll, tag)| {
match (roll, tag) {
((roll_full, _), Some((_, tag_full))) => (roll_full, tag_full),
((roll_full, tag_empty), None) => (roll_full, tag_empty),
}
})
.or(tags_directive
.then(roll_directive.or_not())
.map(|(tag, roll)| match (roll, tag) {
(Some((roll_full, _)), (_, tag_full)) => (roll_full, tag_full),
(None, (roll_empty, tag_full)) => (roll_empty, tag_full),
}))
.or_not()
.map_with(|heading, extra| {
heading.unwrap_or((
full_rc_node(Expr::Empty, extra),
full_rc_node(Vec::new(), extra),
))
})
.boxed()
.labelled("Table Headings")
}
fn tags_directive<'src>()
-> impl Parser<'src, &'src [Token], RcNode<Vec<Atom>>, TaleExtra<'src>> + Clone {
just(Token::Tag)
.then(just(Token::Colon))
.ignore_then(
ident()
.map(|id| Atom::Str(id.to_lowercase()))
.separated_by(just(Token::Comma))
.collect::<Vec<_>>()
.map_with(full_rc_node),
)
.then_ignore(chomp_disjoint_newlines(NOTHING))
.boxed()
}
fn row_key<'src>(
chomp_tokens: &'static [Token],
end_tokens: &'static [Token],
) -> impl Parser<'src, &'src [Token], RcNode<Expr>, TaleExtra<'src>> + Clone {
number_range_list()
.then_ignore(chomp_separator(chomp_tokens, end_tokens))
.or(ident()
.and_is(end_table().not())
.map_with(full_rc_node)
.then_ignore(chomp_separator(chomp_tokens, end_tokens)))
.boxed()
}
#[cfg(test)]
#[allow(unused_must_use)]
mod tests {
use super::*;
use crate::{
state::ParserState,
tests::{grubbed_parser, stubbed_parser},
};
#[test]
fn parse_script() {
let source = "Script: Example
Set Phasers to stun
End Script";
let mut p_state = ParserState::from_source(source.into());
let tokens = p_state.tokens();
let output = stubbed_parser(&mut p_state, &tokens, script());
assert_eq!("Script: `example`, 1 Statement", format!("{output}"));
}
#[test]
fn parse_table() {
let source = "Table: Colors
List: Red, Orange, Yellow, Green, Blue, Purple
";
let mut p_state = ParserState::from_source(source.into());
let tokens = p_state.tokens();
let output = stubbed_parser(&mut p_state, &tokens, table());
assert_eq!("Table: `colors`, 1d6, 6 Items", format!("{output}"));
let source = "Table: Stub
Roll: d20
End Table
";
let mut p_state = ParserState::from_source(source.into());
let tokens = p_state.tokens();
let output = stubbed_parser(&mut p_state, &tokens, table());
assert_eq!("Table: `stub`, 1d20, Empty", format!("{output}"));
let source = "Table: Basic
Pork
Beef
Chicken
End Table
";
let mut p_state = ParserState::from_source(source.into());
let tokens = p_state.tokens();
let output = stubbed_parser(&mut p_state, &tokens, table());
assert_eq!("Table: `basic`, 1d3, 3 Rows", format!("{output}"));
let source = "Table: keyed
1\tis the loneliest number
2\tCan be as bad as one
3-5,7\tProbably pretty garbage too...
6\tlastly
End Table";
let mut p_state = ParserState::from_source(source.into());
let tokens = p_state.tokens();
let output = stubbed_parser(&mut p_state, &tokens, table());
assert_eq!("Table: `keyed`, 1d7, 4 Rows", format!("{output}"));
}
#[test]
fn parse_table_rows() {
let source = "Elves Immortal, wisest and fairest of all beings.
Dwarves Great miners and craftsmen of the mountain halls.
Humans Who above all else desire power.
End Table";
let mut p_state = ParserState::from_source(source.into());
let tokens = p_state.tokens();
let output = stubbed_parser(&mut p_state, &tokens, table_rows());
assert_eq!("3 Rows", output);
let source = r#"1–6 —
7–11 1d6 rolls on Table: "Magic Items #3"
12–18 [2 rolls on Table: "Magic Items #3", 1d2 rolls on Table: "Magic Items #10"]
19–20 1d4 rolls on Table: "Magic Items #9"
End Table"#;
let mut p_state = ParserState::from_source(source.into());
let tokens = p_state.tokens();
let output = stubbed_parser(&mut p_state, &tokens, table_rows());
assert_eq!("4 Rows", output);
}
#[test]
fn parse_table_block_rows() {
let source =
"Reprisals:
An enemy faction moves against you or yours. Pay them 1 cred per Tier, allow them to mess with you, or fight back. If you have no faction with negative status, you avoid entanglements right now.
Ship Trouble:
// Are comments a problem?
A ship system acts up. Damage a system (the GM will tell you which).
You may repair the system as normal, though you have to deal with the consequences of the damage at the time it occurs. This entanglement can happen while in flight between planets or systems, or on the way to or from a job.
Unquiet Black:
An alien or Way creature finds its way on board. Acquire the services of a mystic or exterminator to destroy or banish it, or deal with it yourself.
Treat the magnitude (see page 278) of the Way creature as equal to the crew’s wanted level in the system. Parasites, cargo you weren’t told was alive, strange creatures hiding in unmapped lanes, and bizarre physics effects from using your jump drives way past capacity can all apply here.
End Table";
let mut p_state = ParserState::from_source(source.into());
let tokens = p_state.tokens();
let output = stubbed_parser(&mut p_state, &tokens, table_block_cell_form());
assert_eq!("3 Rows", output);
let source = "1-2 Vacant. No one seems to be visiting this place.
[+0 to size roll]
[+2 to crime roll]
3-6 Groups. Visitors are a rarity, though a few might be around.
[+1 to size roll]
[+1 to crime roll]
7-14 Crowds. It is typical to see some new visitors most days.
[+2 to size roll]
[+0 to crime roll]
15-18 Droves. There are lots of new faces on a regular basis.
[+3 to size roll]
[-1 to crime roll]
19-20 Masses. New people are everywhere, coming and going at all times.
[+4 to size roll]
[-2 to crime roll]
End Table
";
let mut p_state = ParserState::from_source(source.into());
let tokens = p_state.tokens();
let output = stubbed_parser(&mut p_state, &tokens, table_block_cell_form());
assert_eq!("5 Rows", output);
}
#[test]
fn parse_table_group() {
let source = "Table Group: minimal
1d3\texample
1\ta
2\tb
3\tc
End Table Group\n";
let mut p_state = ParserState::from_source(source.into());
let tokens = p_state.tokens();
let output = stubbed_parser(&mut p_state, &tokens, table_group());
assert_eq!(
"TableGroup: `minimal`\n\
\t`minimal example`, 1d3, 3 Rows\n",
output
);
let source = "Table Group: Animals
Tags: animals
1d3\tHouse\tBarn\tForest
1\tCat\tCow\tSquirrel
2\tDog\tHorse\tRabbit
3\tMouse\tPig\tDeer
End Table Group\n";
let mut p_state = ParserState::from_source(source.into());
let tokens = p_state.tokens();
let output = stubbed_parser(&mut p_state, &tokens, table_group());
assert_eq!(
"TableGroup: `animals`\n\
\t`animals house`, 1d3, 3 Rows\n\
\t`animals barn`, 1d3, 3 Rows\n\
\t`animals forest`, 1d3, 3 Rows\n",
output
);
let source = "Table Group: Broken
1d1\ttwo\theadings
1\tThree\tRow\tItems
End Table Group\n";
let mut p_state = ParserState::from_source(source.into());
let tokens = p_state.tokens();
let output = stubbed_parser(&mut p_state, &tokens, table_group());
assert_eq!(
"[TaleError { kind: Parse, span: 0..155, position: (1, 0), msg: \"Table Group rows \
must all have same number of columns In: [Table Group Definition]\" }]",
output
);
}
#[test]
fn parse_sub_tables_row() {
let source = "1d6\tColor\tShape\tSize\n";
let mut p_state = ParserState::from_source(source.into());
let tokens = p_state.tokens();
let output = grubbed_parser(&mut p_state, &tokens, sub_tables_row());
assert_eq!(1, output.matches("Dice(1, 6)").count());
assert_eq!(3, output.matches("Ident(").count());
}
#[test]
fn parse_table_group_rows() {
let source = "1\ta
2\tb
3\tc
";
let mut p_state = ParserState::from_source(source.into());
let tokens = p_state.tokens();
let output = grubbed_parser(&mut p_state, &tokens, table_group_rows());
eprintln!("{output}");
assert_eq!(1, output.matches("Keyed(").count());
assert_eq!(3, output.matches("List(").count());
assert_eq!(3, output.matches("Atom(Ident").count());
let source = "1\tCat\tCow\tSquirrel
2\tDog\tHorse\tRabbit
3\tMouse\tPig\tDeer
";
let mut p_state = ParserState::from_source(source.into());
let tokens = p_state.tokens();
let output = grubbed_parser(&mut p_state, &tokens, table_group_rows());
assert_eq!(3, output.matches("Keyed(").count());
assert_eq!(9, output.matches("List(").count());
assert_eq!(9, output.matches("Atom(Ident").count());
let source = "1\ta\tz
2\tb
3\tc
";
let mut p_state = ParserState::from_source(source.into());
let tokens = p_state.tokens();
let output = grubbed_parser(&mut p_state, &tokens, table_group_rows());
assert_eq!(
"[TaleError { kind: Parse, span: 69..70, position: (3, 31), msg: \"Table Group rows \
must all have same number of columns, row 2 has 1 columns but expected 2 In: \
[Table Group Rows]\" }]",
output
);
}
#[test]
fn parse_table_headings() {
let source = "";
let mut p_state = ParserState::from_source(source.into());
let tokens = p_state.tokens();
let output = grubbed_parser(&mut p_state, &tokens, table_headings());
assert!(output.starts_with("(Node"));
assert!(output.contains("value: Empty"));
assert!(output.contains("value: []"));
assert!(output.ends_with("} })"));
let source = "Roll: 1d8\n";
let mut p_state = ParserState::from_source(source.into());
let tokens = p_state.tokens();
let output = grubbed_parser(&mut p_state, &tokens, table_headings());
assert!(output.starts_with("(Node"));
assert!(output.contains("Atom(Dice(1, 8))"));
assert!(output.contains("value: []"));
assert!(output.ends_with("} })"));
let source = "Tags: Dark, Stormy\n";
let mut p_state = ParserState::from_source(source.into());
let tokens = p_state.tokens();
let output = grubbed_parser(&mut p_state, &tokens, table_headings());
eprintln!("{output}");
assert!(output.starts_with("(Node"));
assert!(output.contains("value: Empty"));
assert!(output.contains("[Str(\"dark\")"));
assert!(output.contains("Str(\"stormy\")]"));
assert!(output.ends_with("} })"));
let source = "Roll: 1d6\nTags: This, That, The Other Thing\n";
let mut p_state = ParserState::from_source(source.into());
let tokens = p_state.tokens();
let output = grubbed_parser(&mut p_state, &tokens, table_headings());
eprintln!("{output}");
assert!(output.starts_with("(Node"));
assert!(output.contains("Atom(Dice(1, 6))"));
assert!(output.contains("[Str(\"this\")"));
assert!(output.contains("Str(\"that\")"));
assert!(output.contains("Str(\"the other thing\")]"));
assert!(output.ends_with("} })"));
let source = "Tags: the other, way around\nRoll: 2d20\n";
let mut p_state = ParserState::from_source(source.into());
let tokens = p_state.tokens();
let output = grubbed_parser(&mut p_state, &tokens, table_headings());
eprintln!("{output}");
assert!(output.starts_with("(Node"));
assert!(output.contains("Atom(Dice(2, 20)"));
assert!(output.contains("[Str(\"the other\")"));
assert!(output.contains("Str(\"way around\")]"));
assert!(output.ends_with("} })"));
let source = "Tag: @";
let mut p_state = ParserState::from_source(source.into());
let tokens = p_state.tokens();
let output = grubbed_parser(&mut p_state, &tokens, table_headings());
assert_eq!(
"[TaleError { kind: Parse, span: 5..6, position: (1, 5), msg: \"found 'At' expected \
Identity, or Separator( [] -> [NewLines] )\" }]",
output
);
let source = "roll: @";
let mut p_state = ParserState::from_source(source.into());
let tokens = p_state.tokens();
let output = grubbed_parser(&mut p_state, &tokens, table_headings());
assert_eq!(
"[TaleError { kind: Parse, span: 6..7, position: (1, 6), msg: \"found 'At' \
expected Arithmetic Expression\" }]",
output
);
}
#[test]
fn parse_row_key() {
let source = "22\t";
let mut p_state = ParserState::from_source(source.into());
let tokens = p_state.tokens();
let output = stubbed_parser(&mut p_state, &tokens, row_key(NOTHING, TABS));
assert_eq!("[22]", format!("{output}"));
let source = "4,6-8\t";
let mut p_state = ParserState::from_source(source.into());
let tokens = p_state.tokens();
let output = stubbed_parser(&mut p_state, &tokens, row_key(NOTHING, TABS));
assert_eq!("[4, 6, 7, 8]", format!("{output}"));
let source = "Elves\t";
let mut p_state = ParserState::from_source(source.into());
let tokens = p_state.tokens();
let output = stubbed_parser(&mut p_state, &tokens, row_key(NOTHING, TABS));
assert_eq!("`elves`", format!("{output}"));
let source = "`Dwarves`\t";
let mut p_state = ParserState::from_source(source.into());
let tokens = p_state.tokens();
let output = stubbed_parser(&mut p_state, &tokens, row_key(NOTHING, TABS));
assert_eq!("`dwarves`", format!("{output}"));
let source = "4 Non Blondes\t";
let mut p_state = ParserState::from_source(source.into());
let tokens = p_state.tokens();
let output = stubbed_parser(&mut p_state, &tokens, row_key(NOTHING, TABS));
assert_eq!("`4 non blondes`", format!("{output}"));
let source = "3-4 Business Days\t";
let mut p_state = ParserState::from_source(source.into());
let tokens = p_state.tokens();
let output = stubbed_parser(&mut p_state, &tokens, row_key(NOTHING, TABS));
assert_eq!(
"[TaleError { kind: Parse, span: 4..12, position: (1, 4), msg: \"found \
'Word(\\\"Business\\\")' expected 'Comma', or Separator( [] -> [Tabs] )\" }]",
format!("{output}")
);
}
}