tyche/
expr.rs

1//! AST-like data structures for evaluating full mathematical dice expressions and working with their results.
2
3use alloc::{
4	boxed::Box,
5	format,
6	string::{String, ToString},
7};
8use core::fmt;
9
10use crate::dice::{roller::Roller, Dice, Error as DiceError, Rolled};
11
12/// Generates an implementation of [`HasOpType`] and [`DescribeBinaryExpr`] for an enum type.
13/// This is very tightly coupled with the expected variants:
14/// `Val`, `Dice`, `Neg`, `Add`, `Sub`, `Mul`, `DivDown`, and `DivUp`.
15macro_rules! op_type_impl {
16	($name:ty) => {
17		impl HasOpType for $name {
18			fn op_type(&self) -> OpType {
19				match self {
20					Self::Num(..) | Self::Dice(..) => OpType::Value,
21					Self::Neg(..) => OpType::Unary,
22					Self::Add(..) | Self::Sub(..) => OpType::Additive,
23					Self::Mul(..) | Self::DivDown(..) | Self::DivUp(..) => OpType::Multiplicative,
24				}
25			}
26
27			fn is_value(&self) -> bool {
28				matches!(self, Self::Num(..) | Self::Dice(..))
29			}
30
31			fn is_unary(&self) -> bool {
32				matches!(self, Self::Neg(..))
33			}
34
35			fn is_additive(&self) -> bool {
36				matches!(self, Self::Add(..) | Self::Sub(..))
37			}
38
39			fn is_multiplicative(&self) -> bool {
40				matches!(self, Self::Mul(..) | Self::DivDown(..) | Self::DivUp(..))
41			}
42		}
43	};
44}
45
46/// Tree structure of individual elements of a full mathematical dice expression
47#[derive(Debug, Clone, PartialEq, Eq)]
48#[non_exhaustive]
49pub enum Expr {
50	/// Standalone integer
51	Num(i32),
52
53	/// Dice literal
54	Dice(Dice),
55
56	/// Negation of an expression (makes the result of it negative)
57	Neg(Box<Self>),
58
59	/// Sum of two expressions
60	Add(Box<Self>, Box<Self>),
61
62	/// Difference of two expressions
63	Sub(Box<Self>, Box<Self>),
64
65	/// Product of two expressions
66	Mul(Box<Self>, Box<Self>),
67
68	/// Integer quotient of two expressions (rounded down)
69	DivDown(Box<Self>, Box<Self>),
70
71	/// Integer quotient of two expressions (rounded up)
72	DivUp(Box<Self>, Box<Self>),
73}
74
75op_type_impl!(Expr);
76
77impl Expr {
78	/// Evaluates the expression. For most types of expressions, this will directly result in a 1:1 equivalent
79	/// [`Evaled`], with the notable exception of [`Expr::Dice`]. For dice expressions, the dice they contain are
80	/// rolled, resulting in an [`Evaled::Dice`] with the [`Rolled`] set of dice.
81	///
82	/// # Errors
83	/// If there is an integer overflow or division error encountered during any operations, or if an error occurs
84	/// during dice rolling, an error variant will be returned.
85	pub fn eval(&self, rng: &mut impl Roller) -> Result<Evaled, EvalError> {
86		Ok(match self {
87			Self::Num(x) => Evaled::Num(*x),
88			Self::Dice(dice) => Evaled::Dice(rng.roll(dice, true).map_err(|err| EvalError::Dice(self.clone(), err))?),
89
90			Self::Neg(x) => Evaled::Neg(Box::new(x.eval(rng)?)),
91
92			Self::Add(a, b) => Evaled::Add(Box::new(a.eval(rng)?), Box::new(b.eval(rng)?)),
93			Self::Sub(a, b) => Evaled::Sub(Box::new(a.eval(rng)?), Box::new(b.eval(rng)?)),
94			Self::Mul(a, b) => Evaled::Mul(Box::new(a.eval(rng)?), Box::new(b.eval(rng)?)),
95			Self::DivDown(a, b) => Evaled::DivDown(Box::new(a.eval(rng)?), Box::new(b.eval(rng)?)),
96			Self::DivUp(a, b) => Evaled::DivUp(Box::new(a.eval(rng)?), Box::new(b.eval(rng)?)),
97		})
98	}
99
100	/// Checks whether the expression is deterministic (will always yield the same value with every evaluation).
101	/// A [`Self::Num`] will always return `true`, a [`Self::Dice`] will always return `false` unless the dice they
102	/// contain only have one side, and all unary and binary expressions forward the check to their children.
103	#[must_use]
104	pub fn is_deterministic(&self) -> bool {
105		match self {
106			Self::Num(..) => true,
107			Self::Dice(dice) => dice.sides == 1,
108			Self::Neg(x) => x.is_deterministic(),
109			Self::Add(a, b) | Self::Sub(a, b) | Self::Mul(a, b) | Self::DivDown(a, b) | Self::DivUp(a, b) => {
110				a.is_deterministic() && b.is_deterministic()
111			}
112		}
113	}
114}
115
116impl Describe for Expr {
117	/// Builds a full usable expression string from the expressions. Operations are grouped with parentheses whenever
118	/// the order of operations could be considered ambiguous, such as when mixing addition and multiplication together.
119	/// All strings output from this should result in the exact same expression tree when re-parsing them.
120	///
121	/// `list_limit` does not affect the output of this implementation in any way since there are no possible lists of
122	/// elements included, so it is always safe to pass `None`.
123	fn describe(&self, _list_limit: Option<usize>) -> String {
124		match self {
125			Self::Num(x) => x.to_string(),
126			Self::Dice(dice) => dice.to_string(),
127
128			Self::Neg(x) => match x.as_ref() {
129				Self::Num(..) | Self::Dice(..) => format!("-{}", x.describe(None)),
130				_ => format!("-({})", x.describe(None)),
131			},
132
133			Self::Add(a, b) => self.describe_binary_expr('+', a.as_ref(), b.as_ref(), None),
134			Self::Sub(a, b) => self.describe_binary_expr('-', a.as_ref(), b.as_ref(), None),
135			Self::Mul(a, b) => self.describe_binary_expr('*', a.as_ref(), b.as_ref(), None),
136			Self::DivDown(a, b) => self.describe_binary_expr('/', a.as_ref(), b.as_ref(), None),
137			Self::DivUp(a, b) => self.describe_binary_expr('\\', a.as_ref(), b.as_ref(), None),
138		}
139	}
140}
141
142impl fmt::Display for Expr {
143	/// Formats the value using the given formatter. [Read more][core::fmt::Debug::fmt()]
144	///
145	/// The output of this implementation is equivalent to [`Self::describe(None)`].
146	///
147	/// [`Self::describe(None)`]: Self::describe()
148	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
149		write!(f, "{}", self.describe(None))
150	}
151}
152
153/// Tree structure of individual elements of an evaluated mathematical dice expression
154#[derive(Debug, Clone, PartialEq, Eq)]
155#[non_exhaustive]
156pub enum Evaled<'a> {
157	/// Standalone integer
158	Num(i32),
159
160	/// Rolled dice
161	Dice(Rolled<'a>),
162
163	/// Negation of an expression (makes the result of it negative)
164	Neg(Box<Self>),
165
166	/// Sum of two expressions
167	Add(Box<Self>, Box<Self>),
168
169	/// Difference of two expressions
170	Sub(Box<Self>, Box<Self>),
171
172	/// Product of two expressions
173	Mul(Box<Self>, Box<Self>),
174
175	/// Integer quotient of two expressions (rounded down)
176	DivDown(Box<Self>, Box<Self>),
177
178	/// Integer quotient of two expressions (rounded up)
179	DivUp(Box<Self>, Box<Self>),
180}
181
182op_type_impl!(Evaled<'_>);
183
184impl Evaled<'_> {
185	/// Calculates the final result of the evaluated expression and all of its children (if any).
186	///
187	/// # Errors
188	/// If there is an integer overflow or division error, or an error calculating the total of a set of dice rolls,an
189	/// error variant will be returned.
190	pub fn calc(&self) -> Result<i32, CalcError> {
191		match self {
192			Self::Num(x) => Ok(*x),
193			Self::Dice(rolled) => Ok(rolled
194				.total()
195				.map_err(|err| CalcError::Dice(self.clone().into_owned(), err))?
196				.into()),
197
198			Self::Neg(x) => Ok(x
199				.calc()?
200				.checked_neg()
201				.ok_or_else(|| CalcError::Overflow(self.clone().into_owned()))?),
202
203			Self::Add(a, b) => a
204				.calc()?
205				.checked_add(b.calc()?)
206				.ok_or_else(|| CalcError::Overflow(self.clone().into_owned())),
207			Self::Sub(a, b) => a
208				.calc()?
209				.checked_sub(b.calc()?)
210				.ok_or_else(|| CalcError::Overflow(self.clone().into_owned())),
211			Self::Mul(a, b) => a
212				.calc()?
213				.checked_mul(b.calc()?)
214				.ok_or_else(|| CalcError::Overflow(self.clone().into_owned())),
215			Self::DivDown(a, b) => a
216				.calc()?
217				.checked_div(b.calc()?)
218				.ok_or_else(|| CalcError::Division(self.clone().into_owned())),
219			Self::DivUp(a, b) => {
220				let a_val = a.calc()?;
221				let b_val = b.calc()?;
222				let result = a_val
223					.checked_div(b_val)
224					.ok_or_else(|| CalcError::Division(self.clone().into_owned()))?;
225				let remainder = a_val
226					.checked_rem(b_val)
227					.ok_or_else(|| CalcError::Division(self.clone().into_owned()))?;
228				if remainder != 0 {
229					Ok(result
230						.checked_add(1)
231						.ok_or_else(|| CalcError::Overflow(self.clone().into_owned()))?)
232				} else {
233					Ok(result)
234				}
235			}
236		}
237	}
238
239	/// Moves all of self's owned data into a new instance and clones any unowned data in order to create a `'static`
240	/// instance of self.
241	#[must_use]
242	pub fn into_owned(self) -> Evaled<'static> {
243		match self {
244			Self::Num(x) => Evaled::Num(x),
245			Self::Dice(rolled) => Evaled::Dice(rolled.into_owned()),
246			Self::Neg(x) => Evaled::Neg(Box::new(x.into_owned())),
247			Self::Add(a, b) => Evaled::Add(Box::new(a.into_owned()), Box::new(b.into_owned())),
248			Self::Sub(a, b) => Evaled::Sub(Box::new(a.into_owned()), Box::new(b.into_owned())),
249			Self::Mul(a, b) => Evaled::Mul(Box::new(a.into_owned()), Box::new(b.into_owned())),
250			Self::DivDown(a, b) => Evaled::DivDown(Box::new(a.into_owned()), Box::new(b.into_owned())),
251			Self::DivUp(a, b) => Evaled::DivUp(Box::new(a.into_owned()), Box::new(b.into_owned())),
252		}
253	}
254}
255
256impl Describe for Evaled<'_> {
257	fn describe(&self, list_limit: Option<usize>) -> String {
258		match self {
259			Self::Num(x) => x.to_string(),
260			Self::Dice(roll) => roll.describe(list_limit),
261
262			Self::Neg(x) => match x.as_ref() {
263				Self::Num(..) | Self::Dice(..) => format!("-{}", x.describe(list_limit)),
264				_ => format!("-({})", x.describe(list_limit)),
265			},
266
267			Self::Add(a, b) => self.describe_binary_expr('+', a.as_ref(), b.as_ref(), list_limit),
268			Self::Sub(a, b) => self.describe_binary_expr('-', a.as_ref(), b.as_ref(), list_limit),
269			Self::Mul(a, b) => self.describe_binary_expr('*', a.as_ref(), b.as_ref(), list_limit),
270			Self::DivDown(a, b) => self.describe_binary_expr('/', a.as_ref(), b.as_ref(), list_limit),
271			Self::DivUp(a, b) => self.describe_binary_expr('\\', a.as_ref(), b.as_ref(), list_limit),
272		}
273	}
274}
275
276impl fmt::Display for Evaled<'_> {
277	/// Formats the value using the given formatter. [Read more][core::fmt::Debug::fmt()]
278	///
279	/// The output of this implementation is equivalent to [`Self::describe(None)`].
280	///
281	/// [`Self::describe(None)`]: Self::describe()
282	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
283		write!(f, "{}", self.describe(None))
284	}
285}
286
287/// Error that can occur during [`Expr::eval()`]
288#[derive(thiserror::Error, Debug)]
289#[non_exhaustive]
290pub enum EvalError {
291	/// Dice-related error (likely during rolling)
292	#[error("dice error while evaluating \"{0}\": {1}")]
293	Dice(Expr, #[source] DiceError),
294}
295
296/// Error that can occur during [`Evaled::calc()`]
297#[derive(thiserror::Error, Debug)]
298#[non_exhaustive]
299pub enum CalcError {
300	/// Dice-related error (likely during totalling)
301	#[error("dice error while calculating ({0}): {1}")]
302	Dice(Evaled<'static>, #[source] DiceError),
303
304	/// Integer overflow (likely during calculation of a sum or product)
305	#[error("integer overflow while calculating {0}")]
306	Overflow(Evaled<'static>),
307
308	/// Division-related error (likely division by zero)
309	#[error("division error while calculating {0}")]
310	Division(Evaled<'static>),
311}
312
313/// Operation type for an individual expression
314#[derive(Debug, Clone, Copy, PartialEq, Eq)]
315#[allow(clippy::exhaustive_enums)]
316pub enum OpType {
317	/// Single value, no operation
318	Value,
319
320	/// Unary operation
321	Unary,
322
323	/// Additive operation (sum or difference)
324	Additive,
325
326	/// Multiplicative operation (product or quotient)
327	Multiplicative,
328}
329
330/// Trait that offers [`OpType`]-related information
331pub trait HasOpType {
332	/// Gets the type of this expression.
333	fn op_type(&self) -> OpType;
334
335	/// Checks whether this expression is a single value.
336	fn is_value(&self) -> bool;
337
338	/// Checks whether this expression is a unary operation.
339	fn is_unary(&self) -> bool;
340
341	/// Checks whether this expression is an additive operation.
342	fn is_additive(&self) -> bool;
343
344	/// Checks whether this expression is a multiplicative operation.
345	fn is_multiplicative(&self) -> bool;
346
347	/// Checks whether this expression is a binary (additive or multiplicative) operation.
348	fn is_binary(&self) -> bool {
349		self.is_additive() || self.is_multiplicative()
350	}
351}
352
353/// Trait to allow creation of expanded descriptions with an optional max number of individual listed results where
354/// applicable
355pub trait Describe {
356	/// Builds a detailed expression string with additional information about non-deterministic elements.
357	/// Any elements of the expression that can have a different result between multiple evaluations or multiple results
358	/// should list all of the specific individual results that occurred (ideally, up to `list_limit` of them).
359	#[must_use]
360	fn describe(&self, list_limit: Option<usize>) -> String;
361}
362
363/// Trait for describing binary expressions with influence from own type.
364/// Used for, e.g. wrapping parentheses around parts of expressions based on [`OpType`] of self and the expression.
365trait DescribeBinaryExpr: HasOpType + Describe {
366	/// Builds a detailed description for a binary expression with parentheses added to disambiguate mixed
367	/// additive/multiplicative operations.
368	fn describe_binary_expr(
369		&self,
370		op: char,
371		a: &impl DescribeBinaryExpr,
372		b: &impl DescribeBinaryExpr,
373		list_limit: Option<usize>,
374	) -> String {
375		format!(
376			"{} {} {}",
377			match (self.op_type(), a.op_type()) {
378				(OpType::Additive | OpType::Unary, OpType::Multiplicative)
379				| (OpType::Multiplicative | OpType::Unary, OpType::Additive)
380				| (OpType::Unary, OpType::Unary) => paren_wrap(a.describe(list_limit)),
381				_ => a.describe(list_limit),
382			},
383			op,
384			match (self.op_type(), b.op_type()) {
385				(OpType::Additive | OpType::Unary, OpType::Multiplicative)
386				| (OpType::Multiplicative | OpType::Unary, OpType::Additive)
387				| (OpType::Unary, OpType::Unary) => paren_wrap(b.describe(list_limit)),
388				_ => b.describe(list_limit),
389			}
390		)
391	}
392}
393
394impl<T: HasOpType + Describe> DescribeBinaryExpr for T {}
395
396/// Wraps a string in parentheses.
397#[must_use]
398fn paren_wrap(mut text: String) -> String {
399	text.insert(0, '(');
400	text.push(')');
401	text
402}