1use thiserror::Error;
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
13pub struct Position {
14 pub line: usize,
16 pub column: usize,
18 pub offset: usize,
20}
21
22impl Position {
23 pub fn new(line: usize, column: usize, offset: usize) -> Self {
25 Self {
26 line,
27 column,
28 offset,
29 }
30 }
31}
32
33impl std::fmt::Display for Position {
34 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
35 write!(f, "line {}, column {}", self.line, self.column)
36 }
37}
38
39#[derive(Debug, Error, Clone, PartialEq)]
41pub enum LexError {
42 #[error("Unexpected character '{0}' at {1}")]
44 UnexpectedCharacter(char, Position),
45
46 #[error("Unterminated string starting at {0}")]
48 UnterminatedString(Position),
49
50 #[error("Invalid escape sequence '\\{0}' at {1}")]
52 InvalidEscape(char, Position),
53
54 #[error("Invalid number literal at {0}")]
56 InvalidNumber(Position),
57}
58
59#[derive(Debug, Error, Clone, PartialEq)]
61pub enum ParseError {
62 #[error("Unexpected token {0} at {1}, expected {2}")]
64 UnexpectedToken(String, Position, String),
65
66 #[error("Unexpected end of input at {0}")]
68 UnexpectedEof(Position),
69
70 #[error("Expected expression at {0}")]
72 ExpectedExpression(Position),
73
74 #[error("Expected identifier at {0}")]
76 ExpectedIdentifier(Position),
77
78 #[error("Expected ';' at {0}")]
80 ExpectedSemicolon(Position),
81
82 #[error("Invalid assignment target at {0}")]
84 InvalidAssignmentTarget(Position),
85}
86
87#[derive(Debug, Error, Clone, PartialEq)]
89pub enum RuntimeError {
90 #[error("Undefined variable '{name}'{}", format_position(*.position))]
92 UndefinedVariable {
93 name: String,
95 position: Option<Position>,
97 },
98
99 #[error("Undefined function '{name}'{}", format_position(*.position))]
101 UndefinedFunction {
102 name: String,
104 position: Option<Position>,
106 },
107
108 #[error("Type mismatch: {message}{}", format_position(*.position))]
110 TypeMismatch {
111 message: String,
113 position: Option<Position>,
115 },
116
117 #[error("Division by zero{}", format_position(*.position))]
119 DivisionByZero {
120 position: Option<Position>,
122 },
123
124 #[error("Expected {expected} arguments but got {actual}{}", format_position(*.position))]
126 WrongArgumentCount {
127 expected: usize,
129 actual: usize,
131 position: Option<Position>,
133 },
134
135 #[error("Invalid argument: {message}")]
137 InvalidArgument {
138 message: String,
140 },
141
142 #[error("Return outside of function")]
144 ReturnOutsideFunction,
145
146 #[error("Stack overflow: maximum recursion depth ({max_depth}) exceeded")]
148 StackOverflow {
149 max_depth: usize,
151 },
152}
153
154fn format_position(pos: Option<Position>) -> String {
156 match pos {
157 Some(p) => format!(" at {}", p),
158 None => String::new(),
159 }
160}
161
162impl RuntimeError {
164 pub fn undefined_variable(name: impl Into<String>) -> Self {
166 RuntimeError::UndefinedVariable {
167 name: name.into(),
168 position: None,
169 }
170 }
171
172 pub fn undefined_variable_at(name: impl Into<String>, position: Position) -> Self {
174 RuntimeError::UndefinedVariable {
175 name: name.into(),
176 position: Some(position),
177 }
178 }
179
180 pub fn undefined_function(name: impl Into<String>) -> Self {
182 RuntimeError::UndefinedFunction {
183 name: name.into(),
184 position: None,
185 }
186 }
187
188 pub fn type_mismatch(message: impl Into<String>) -> Self {
190 RuntimeError::TypeMismatch {
191 message: message.into(),
192 position: None,
193 }
194 }
195
196 pub fn type_mismatch_at(message: impl Into<String>, position: Position) -> Self {
198 RuntimeError::TypeMismatch {
199 message: message.into(),
200 position: Some(position),
201 }
202 }
203
204 pub fn invalid_argument(message: impl Into<String>) -> Self {
206 RuntimeError::InvalidArgument {
207 message: message.into(),
208 }
209 }
210
211 pub fn wrong_argument_count(expected: usize, actual: usize) -> Self {
213 RuntimeError::WrongArgumentCount {
214 expected,
215 actual,
216 position: None,
217 }
218 }
219}
220
221#[derive(Debug, Error, Clone, PartialEq)]
223pub enum FiddlerError {
224 #[error("Lex error: {0}")]
226 Lex(#[from] LexError),
227
228 #[error("Parse error: {0}")]
230 Parse(#[from] ParseError),
231
232 #[error("Runtime error: {0}")]
234 Runtime(#[from] RuntimeError),
235}
236
237#[cfg(test)]
238mod tests {
239 use super::*;
240
241 #[test]
242 fn test_position_display() {
243 let pos = Position::new(5, 10, 50);
244 assert_eq!(pos.to_string(), "line 5, column 10");
245 }
246
247 #[test]
248 fn test_lex_error_display() {
249 let err = LexError::UnexpectedCharacter('@', Position::new(1, 5, 4));
250 assert!(err.to_string().contains('@'));
251 assert!(err.to_string().contains("line 1"));
252 }
253
254 #[test]
255 fn test_parse_error_display() {
256 let err = ParseError::UnexpectedEof(Position::new(10, 1, 100));
257 assert!(err.to_string().contains("end of input"));
258 }
259
260 #[test]
261 fn test_runtime_error_display() {
262 let err = RuntimeError::undefined_variable("foo");
263 assert!(err.to_string().contains("foo"));
264 }
265
266 #[test]
267 fn test_fiddler_error_from_lex() {
268 let lex_err = LexError::InvalidNumber(Position::default());
269 let err: FiddlerError = lex_err.into();
270 assert!(matches!(err, FiddlerError::Lex(_)));
271 }
272}