dice_parser/error.rs
1use std::{
2 fmt::{Debug, Display, Formatter},
3 num::TryFromIntError,
4};
5
6use crate::ast::RollSpec;
7
8/// Errors that can occur when parsing or evaluating dice expressions.
9///
10/// This enum covers all possible error conditions, including parse errors,
11/// invalid roll specifications, and arithmetic overflow.
12///
13/// # Variants
14///
15/// - `Overflow`: Integer overflow during calculation
16/// - `InvalidSpec`: Invalid roll specification (e.g., trying to keep more dice than rolled)
17/// - `ParseError`: Error parsing the input string
18/// - `SyntaxError`: Syntax error in the dice expression
19/// - `TrailingInput`: Unexpected characters after the expression
20///
21/// # Examples
22///
23/// ```
24/// use dice_parser::DiceExpr;
25///
26/// // Parse error - invalid syntax
27/// let err = DiceExpr::parse("2d").unwrap_err();
28///
29/// // Syntax error - negative dice count
30/// let err = DiceExpr::parse("-2d6").unwrap_err();
31///
32/// // Trailing input error
33/// let err = DiceExpr::parse("2d6 extra").unwrap_err();
34/// ```
35#[derive(Clone)]
36pub enum DiceError {
37 /// Integer overflow occurred during calculation.
38 Overflow(String),
39 /// Invalid roll specification.
40 ///
41 /// This error occurs when a `RollSpec` is invalid, such as trying to
42 /// keep more dice than were rolled.
43 InvalidSpec(RollSpec, String),
44 /// Parse error with details about what went wrong.
45 ParseError {
46 /// The kind of parse error.
47 kind: ParseErrorKind,
48 /// The input string that failed to parse.
49 input: String,
50 /// The byte position where the error started.
51 start: usize,
52 /// The byte position where the error ended (if applicable).
53 stop: Option<usize>,
54 },
55 /// Syntax error in the dice expression.
56 SyntaxError {
57 /// The input string with the syntax error.
58 input: String,
59 /// The byte position where the error started.
60 start: usize,
61 /// The byte position where the error ended (if applicable).
62 stop: Option<usize>,
63 /// Description of what was expected.
64 expected: Option<String>,
65 },
66 /// Unexpected characters found after the expression.
67 TrailingInput {
68 /// The input string with trailing characters.
69 input: String,
70 /// The byte position where trailing input begins.
71 pos: usize,
72 },
73}
74
75/// The specific kind of parse error that occurred.
76#[derive(Debug, Clone)]
77pub enum ParseErrorKind {
78 /// Expected a number but found something else.
79 ExpectedNumber,
80 /// Failed to parse an i32 value.
81 InvalidI32,
82 /// Failed to parse a u32 value.
83 InvalidU32,
84}
85
86impl std::error::Error for DiceError {}
87
88impl Debug for DiceError {
89 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
90 std::fmt::Display::fmt(self, f)
91 }
92}
93
94impl Display for DiceError {
95 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
96 match self {
97 DiceError::Overflow(msg) => write!(f, "dice overflow error: {}", msg),
98 DiceError::InvalidSpec(spec, msg) => {
99 write!(f, "invalid RollSpec error: {}, {:?}", msg, spec)
100 }
101 DiceError::ParseError {
102 kind,
103 input,
104 start,
105 stop,
106 } => match kind {
107 ParseErrorKind::ExpectedNumber => {
108 writeln!(f, "expected number in input, found nothing here:")?;
109 writeln!(f, "{}", input)?;
110 let mut indicator: String = " ".repeat(*start);
111 match stop {
112 Some(i) => indicator.push_str(&("^".repeat((i - start).max(1)))),
113 None => indicator.push('^'),
114 }
115 write!(f, "{}", indicator)
116 }
117 ParseErrorKind::InvalidU32 => {
118 writeln!(f, "invalid u32 literal in input, parse errored here:")?;
119 writeln!(f, "{}", input)?;
120 let mut indicator: String = " ".repeat(*start);
121 match stop {
122 Some(i) => indicator.push_str(&("^".repeat((i - start).max(1)))),
123 None => indicator.push('^'),
124 }
125 write!(f, "{}", indicator)
126 }
127 ParseErrorKind::InvalidI32 => {
128 writeln!(f, "invalid i32 literal in input, parse errored here:")?;
129 writeln!(f, "{}", input)?;
130 let mut indicator: String = " ".repeat(*start);
131 match stop {
132 Some(i) => indicator.push_str(&("^".repeat((i - start).max(1)))),
133 None => indicator.push('^'),
134 }
135 write!(f, "{}", indicator)
136 }
137 },
138 DiceError::SyntaxError {
139 input,
140 start,
141 stop,
142 expected,
143 } => {
144 writeln!(f, "syntax error in dice expression here:")?;
145 writeln!(f, "{}", input)?;
146 let mut indicator = " ".repeat(*start);
147 match stop {
148 Some(i) => indicator.push_str(&("^".repeat((i - start).max(1)))),
149 None => indicator.push('^'),
150 }
151 if let Some(exp) = expected {
152 writeln!(f, "{}", indicator)?;
153 write!(f, "expected: {}", exp)
154 } else {
155 write!(f, "{}", indicator)
156 }
157 }
158 DiceError::TrailingInput { input, pos } => {
159 writeln!(f, "trailing input encountered in expression:")?;
160 writeln!(f, "{}", input)?;
161 let mut indicator = " ".repeat(*pos);
162 indicator.push_str(&("^".repeat(input.chars().count() - pos)));
163 write!(f, "{}", indicator)
164 }
165 }
166 }
167}
168
169impl From<TryFromIntError> for DiceError {
170 fn from(value: TryFromIntError) -> Self {
171 DiceError::Overflow(value.to_string())
172 }
173}