1use 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
12macro_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#[derive(Debug, Clone, PartialEq, Eq)]
48#[non_exhaustive]
49pub enum Expr {
50 Num(i32),
52
53 Dice(Dice),
55
56 Neg(Box<Self>),
58
59 Add(Box<Self>, Box<Self>),
61
62 Sub(Box<Self>, Box<Self>),
64
65 Mul(Box<Self>, Box<Self>),
67
68 DivDown(Box<Self>, Box<Self>),
70
71 DivUp(Box<Self>, Box<Self>),
73}
74
75op_type_impl!(Expr);
76
77impl Expr {
78 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 #[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 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 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
149 write!(f, "{}", self.describe(None))
150 }
151}
152
153#[derive(Debug, Clone, PartialEq, Eq)]
155#[non_exhaustive]
156pub enum Evaled<'a> {
157 Num(i32),
159
160 Dice(Rolled<'a>),
162
163 Neg(Box<Self>),
165
166 Add(Box<Self>, Box<Self>),
168
169 Sub(Box<Self>, Box<Self>),
171
172 Mul(Box<Self>, Box<Self>),
174
175 DivDown(Box<Self>, Box<Self>),
177
178 DivUp(Box<Self>, Box<Self>),
180}
181
182op_type_impl!(Evaled<'_>);
183
184impl Evaled<'_> {
185 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 #[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 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
283 write!(f, "{}", self.describe(None))
284 }
285}
286
287#[derive(thiserror::Error, Debug)]
289#[non_exhaustive]
290pub enum EvalError {
291 #[error("dice error while evaluating \"{0}\": {1}")]
293 Dice(Expr, #[source] DiceError),
294}
295
296#[derive(thiserror::Error, Debug)]
298#[non_exhaustive]
299pub enum CalcError {
300 #[error("dice error while calculating ({0}): {1}")]
302 Dice(Evaled<'static>, #[source] DiceError),
303
304 #[error("integer overflow while calculating {0}")]
306 Overflow(Evaled<'static>),
307
308 #[error("division error while calculating {0}")]
310 Division(Evaled<'static>),
311}
312
313#[derive(Debug, Clone, Copy, PartialEq, Eq)]
315#[allow(clippy::exhaustive_enums)]
316pub enum OpType {
317 Value,
319
320 Unary,
322
323 Additive,
325
326 Multiplicative,
328}
329
330pub trait HasOpType {
332 fn op_type(&self) -> OpType;
334
335 fn is_value(&self) -> bool;
337
338 fn is_unary(&self) -> bool;
340
341 fn is_additive(&self) -> bool;
343
344 fn is_multiplicative(&self) -> bool;
346
347 fn is_binary(&self) -> bool {
349 self.is_additive() || self.is_multiplicative()
350 }
351}
352
353pub trait Describe {
356 #[must_use]
360 fn describe(&self, list_limit: Option<usize>) -> String;
361}
362
363trait DescribeBinaryExpr: HasOpType + Describe {
366 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#[must_use]
398fn paren_wrap(mut text: String) -> String {
399 text.insert(0, '(');
400 text.push(')');
401 text
402}