tyche/
parse.rs

1//! Parser generator functions and implementations of [`str::FromStr`] for all dice and expression data structures.
2//! Requires the `parse` feature (enabled by default).
3//!
4//! The parser generators generate parsers for parsing dice, dice modifiers, modifier conditions, and full mathematical
5//! dice expressions (such as `4d8 + 2d6x - 3`) from strings. They're all made with [chumsky] and are almost entirely
6//! zero-copy. A parser can be used directly by calling [`Parser::parse()`] on it.
7//!
8//! # Examples
9//!
10//! ## Parsing Dice
11//! ```
12//! use tyche::Dice;
13//!
14//! let dice: Dice = "6d8x".parse()?;
15//! assert_eq!(dice, Dice::builder().count(6).sides(8).explode(None, true).build());
16//! # Ok::<(), tyche::parse::Error>(())
17//! ```
18//!
19//! ## Parsing expressions
20//! ```
21//! use tyche::{Dice, Expr};
22//!
23//! let expr: Expr = "6d8x + 4d6 - 3".parse()?;
24//! assert_eq!(
25//! 	expr,
26//! 	Expr::Sub(
27//! 		Box::new(Expr::Add(
28//! 			Box::new(Expr::Dice(
29//! 				Dice::builder().count(6).sides(8).explode(None, true).build()
30//! 			)),
31//! 			Box::new(Expr::Dice(Dice::new(4, 6))),
32//! 		)),
33//! 		Box::new(Expr::Num(3)),
34//! 	)
35//! );
36//! # Ok::<(), tyche::parse::Error>(())
37//! ```
38
39use alloc::{
40	boxed::Box,
41	fmt, format,
42	string::{String, ToString},
43	vec::Vec,
44};
45use core::str;
46
47use chumsky::prelude::*;
48
49use crate::{
50	dice::{
51		modifier::{Condition, Modifier},
52		Dice,
53	},
54	expr::Expr,
55};
56
57/// Generates a parser that specifically handles dice with or without modifiers like "d20", "2d20kh", "8d6x", etc.
58#[must_use]
59pub fn dice_part<'src>() -> impl Parser<'src, &'src str, Dice, extra::Err<Rich<'src, char>>> + Copy {
60	// Parser for dice literals
61	text::int(10)
62		.labelled("dice count")
63		.or_not()
64		.then_ignore(just('d'))
65		.then(text::int(10).labelled("dice sides"))
66		.then(modifier_list_part())
67		.try_map(|((count, sides), modifiers), span| {
68			let count = count
69				.unwrap_or("1")
70				.parse()
71				.map_err(|err| Rich::custom(span, format!("Dice count: {err}")))?;
72			let sides = sides
73				.parse()
74				.map_err(|err| Rich::custom(span, format!("Dice sides: {err}")))?;
75
76			Ok(Dice {
77				count,
78				sides,
79				modifiers,
80			})
81		})
82		.labelled("dice set")
83}
84
85/// Generates a parser that specifically handles dice with or without modifiers like "d20", "2d20kh", "8d6x", etc.
86/// and expects end of input
87#[must_use]
88pub fn dice<'src>() -> impl Parser<'src, &'src str, Dice, extra::Err<Rich<'src, char>>> + Copy {
89	dice_part().then_ignore(end())
90}
91
92/// Generates a parser that specifically handles dice modifiers with conditions like "r1", "xo>4", "kh", etc.
93#[must_use]
94pub fn modifier_part<'src>() -> impl Parser<'src, &'src str, Modifier, extra::Err<Rich<'src, char>>> + Copy {
95	// Parser for dice modifier conditions
96	let condition = condition_part();
97
98	// Parser for dice modifiers
99	choice((
100		// Reroll dice (e.g. r1, rr1, r<=2)
101		just('r')
102			.ignored()
103			.then(just('r').ignored().or_not().map(|r| r.is_some()))
104			.then(condition)
105			.map(|(((), recurse), cond)| Modifier::Reroll { cond, recurse }),
106		// Exploding dice (e.g. x, xo, x>4)
107		just('x')
108			.ignored()
109			.then(just('o').ignored().or_not().map(|o| o.is_none()))
110			.then(condition.or_not())
111			.map(|(((), recurse), cond)| Modifier::Explode { cond, recurse }),
112		// Keep lowest (e.g. kl, kl2)
113		just("kl")
114			.ignored()
115			.then(text::int(10).labelled("keep lowest count").or_not())
116			.try_map(|((), count), span| {
117				let count = count
118					.unwrap_or("1")
119					.parse()
120					.map_err(|err| Rich::custom(span, format!("Keep lowest count: {err}")))?;
121				Ok(Modifier::KeepLow(count))
122			}),
123		// Keep highest (e.g. k, kh, kh2)
124		just('k')
125			.ignored()
126			.then_ignore(just('h').or_not())
127			.then(text::int(10).labelled("keep highest count").or_not())
128			.try_map(|((), count), span| {
129				let count = count
130					.unwrap_or("1")
131					.parse()
132					.map_err(|err| Rich::custom(span, format!("Keep highest count: {err}")))?;
133				Ok(Modifier::KeepHigh(count))
134			}),
135		// Min (e.g. min3)
136		just("min")
137			.ignored()
138			.then(text::int(10).labelled("min roll value"))
139			.try_map(|((), min): ((), &str), span| {
140				let min = min
141					.parse()
142					.map_err(|err| Rich::custom(span, format!("Minimum: {err}")))?;
143				Ok(Modifier::Min(min))
144			}),
145		// Max (e.g. max4)
146		just("max")
147			.ignored()
148			.then(text::int(10).labelled("max roll value"))
149			.try_map(|((), max): ((), &str), span| {
150				let max = max
151					.parse()
152					.map_err(|err| Rich::custom(span, format!("Maximum: {err}")))?;
153				Ok(Modifier::Max(max))
154			}),
155	))
156	.labelled("dice modifier")
157}
158
159/// Generates a parser that specifically handles dice modifiers with conditions like "r1", "xo>4", "kh", etc.
160/// and expects end of input
161#[must_use]
162pub fn modifier<'src>() -> impl Parser<'src, &'src str, Modifier, extra::Err<Rich<'src, char>>> + Copy {
163	modifier_part().then_ignore(end())
164}
165
166/// Generates a parser that specifically handles dice modifier lists with conditions like "r1kh4", "r1xo>4kh4", etc.
167#[must_use]
168pub fn modifier_list_part<'src>() -> impl Parser<'src, &'src str, Vec<Modifier>, extra::Err<Rich<'src, char>>> + Copy {
169	modifier_part().repeated().collect()
170}
171
172/// Generates a parser that specifically handles dice modifier lists with conditions like "r1kh4", "r1xo>4kh4", etc.
173/// and expects end of input
174#[must_use]
175pub fn modifier_list<'src>() -> impl Parser<'src, &'src str, Vec<Modifier>, extra::Err<Rich<'src, char>>> + Copy {
176	modifier_list_part().then_ignore(end())
177}
178
179/// Generates a parser that specifically handles dice modifier conditions like "<3", ">=5", "=1", "1", etc.
180#[must_use]
181pub fn condition_part<'src>() -> impl Parser<'src, &'src str, Condition, extra::Err<Rich<'src, char>>> + Copy {
182	choice((
183		just(">=").to(Condition::Gte as fn(u8) -> _),
184		just("<=").to(Condition::Lte as fn(u8) -> _),
185		just('>').to(Condition::Gt as fn(u8) -> _),
186		just('<').to(Condition::Lt as fn(u8) -> _),
187		just('=').to(Condition::Eq as fn(u8) -> _),
188	))
189	.labelled("condition symbol")
190	.or_not()
191	.then(text::int::<&'src str, _, _>(10).labelled("condition number"))
192	.try_map(|(condfn, val), span| {
193		let val = val
194			.parse()
195			.map_err(|err| Rich::custom(span, format!("Modifier condition: {err}")))?;
196		Ok(condfn.map_or_else(|| Condition::Eq(val), |condfn| condfn(val)))
197	})
198	.labelled("modifier condition")
199}
200
201/// Generates a parser that specifically handles dice modifier conditions like "<3", ">=5", "=1", "1", etc.
202/// and expects end of input
203#[must_use]
204pub fn condition<'src>() -> impl Parser<'src, &'src str, Condition, extra::Err<Rich<'src, char>>> + Copy {
205	condition_part().then_ignore(end())
206}
207
208/// Generates a parser that handles full expressions including mathematical operations, grouping with parentheses,
209/// dice, etc.
210#[must_use]
211pub fn expr_part<'src>() -> impl Parser<'src, &'src str, Expr, extra::Err<Rich<'src, char>>> + Clone {
212	// Helper function for operators
213	let op = |c| just(c).padded();
214
215	recursive(|expr| {
216		// Parser for numbers
217		let int = text::int(10)
218			.try_map(|s: &str, span| {
219				s.parse()
220					.map(Expr::Num)
221					.map_err(|err| Rich::custom(span, format!("{err}")))
222			})
223			.labelled("number");
224
225		// Parser for dice literals
226		let dice = dice_part().map(Expr::Dice);
227
228		// Parser for expressions enclosed in parentheses
229		let atom = dice
230			.or(int)
231			.or(expr.delimited_by(just('('), just(')')).labelled("group"))
232			.padded();
233
234		// Parser for negative sign
235		let unary = op('-').repeated().foldr(atom, |_op, rhs| Expr::Neg(Box::new(rhs)));
236
237		// Parser for multiplication and division (round up or down)
238		let product = unary.clone().foldl(
239			choice((
240				op('*').to(Expr::Mul as fn(_, _) -> _),
241				op('/').to(Expr::DivDown as fn(_, _) -> _),
242				op('\\').to(Expr::DivUp as fn(_, _) -> _),
243			))
244			.then(unary)
245			.repeated(),
246			|lhs, (op, rhs)| op(Box::new(lhs), Box::new(rhs)),
247		);
248
249		// Parser for addition and subtraction operators
250		product.clone().foldl(
251			choice((
252				op('+').to(Expr::Add as fn(_, _) -> _),
253				op('-').to(Expr::Sub as fn(_, _) -> _),
254			))
255			.then(product)
256			.repeated(),
257			|lhs, (op, rhs)| op(Box::new(lhs), Box::new(rhs)),
258		)
259	})
260}
261
262/// Generates a parser that handles full expressions including mathematical operations, grouping with parentheses,
263/// dice, etc. and expects end of input
264#[must_use]
265pub fn expr<'src>() -> impl Parser<'src, &'src str, Expr, extra::Err<Rich<'src, char>>> + Clone {
266	expr_part().then_ignore(end())
267}
268
269/// Error that can occur while parsing a string into a dice or expression-related structure via [`alloc::str::FromStr`].
270#[derive(Debug, Clone)]
271#[non_exhaustive]
272pub struct Error {
273	/// Details of the originating one or more [`Rich`]s that occurred during parsing
274	pub details: String,
275}
276
277#[allow(clippy::absolute_paths)]
278impl core::error::Error for Error {
279	fn description(&self) -> &str {
280		&self.details
281	}
282}
283
284impl fmt::Display for Error {
285	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
286		write!(f, "{}", self.details)
287	}
288}
289
290impl str::FromStr for Dice {
291	type Err = Error;
292
293	fn from_str(s: &str) -> Result<Self, Self::Err> {
294		let lc = s.to_lowercase();
295		let result = dice().parse(&lc).into_result().map_err(|errs| Error {
296			details: errs.iter().map(ToString::to_string).collect::<Vec<_>>().join("; "),
297		});
298		result
299	}
300}
301
302impl str::FromStr for Modifier {
303	type Err = Error;
304
305	fn from_str(s: &str) -> Result<Self, Self::Err> {
306		let lc = s.to_lowercase();
307		let result = modifier().parse(&lc).into_result().map_err(|errs| Error {
308			details: errs.iter().map(ToString::to_string).collect::<Vec<_>>().join("; "),
309		});
310		result
311	}
312}
313
314impl str::FromStr for Condition {
315	type Err = Error;
316
317	fn from_str(s: &str) -> Result<Self, Self::Err> {
318		condition().parse(s).into_result().map_err(|errs| Error {
319			details: errs.iter().map(ToString::to_string).collect::<Vec<_>>().join("; "),
320		})
321	}
322}
323
324impl str::FromStr for Expr {
325	type Err = Error;
326
327	fn from_str(s: &str) -> Result<Self, Self::Err> {
328		let lc = s.to_lowercase();
329		let result = expr().parse(&lc).into_result().map_err(|errs| Error {
330			details: errs.iter().map(ToString::to_string).collect::<Vec<_>>().join("; "),
331		});
332		result
333	}
334}
335
336/// Trait to allow convenient access to a parser generator for any implementing type
337pub trait GenParser<T> {
338	/// Generates a parser for this type that expects end of input. Requires the `parse` feature (enabled by default).
339	#[must_use]
340	fn parser<'src>() -> impl Parser<'src, &'src str, T, extra::Err<Rich<'src, char>>> + Clone;
341
342	/// Generates a parser for this type that parses up to the end of valid input. Requires the `parse` feature
343	/// (enabled by default).
344	#[must_use]
345	fn part_parser<'src>() -> impl Parser<'src, &'src str, T, extra::Err<Rich<'src, char>>> + Clone;
346}
347
348impl GenParser<Dice> for Dice {
349	#[inline]
350	#[allow(refining_impl_trait)]
351	fn parser<'src>() -> impl Parser<'src, &'src str, Self, extra::Err<Rich<'src, char>>> + Copy {
352		dice()
353	}
354
355	#[inline]
356	#[allow(refining_impl_trait)]
357	fn part_parser<'src>() -> impl Parser<'src, &'src str, Self, extra::Err<Rich<'src, char>>> + Copy {
358		dice_part()
359	}
360}
361
362impl GenParser<Modifier> for Modifier {
363	#[inline]
364	#[allow(refining_impl_trait)]
365	fn parser<'src>() -> impl Parser<'src, &'src str, Self, extra::Err<Rich<'src, char>>> + Copy {
366		modifier()
367	}
368
369	#[inline]
370	#[allow(refining_impl_trait)]
371	fn part_parser<'src>() -> impl Parser<'src, &'src str, Self, extra::Err<Rich<'src, char>>> + Copy {
372		modifier_part()
373	}
374}
375
376impl GenParser<Condition> for Condition {
377	#[inline]
378	#[allow(refining_impl_trait)]
379	fn parser<'src>() -> impl Parser<'src, &'src str, Self, extra::Err<Rich<'src, char>>> + Copy {
380		condition()
381	}
382
383	#[inline]
384	#[allow(refining_impl_trait)]
385	fn part_parser<'src>() -> impl Parser<'src, &'src str, Self, extra::Err<Rich<'src, char>>> + Copy {
386		condition_part()
387	}
388}
389
390impl GenParser<Expr> for Expr {
391	#[inline]
392	fn parser<'src>() -> impl Parser<'src, &'src str, Self, extra::Err<Rich<'src, char>>> + Clone {
393		expr()
394	}
395
396	#[inline]
397	fn part_parser<'src>() -> impl Parser<'src, &'src str, Self, extra::Err<Rich<'src, char>>> + Clone {
398		expr_part()
399	}
400}