Skip to main content

fiddler_script/
error.rs

1//! Error types for FiddlerScript.
2//!
3//! This module defines all error types used throughout the interpreter:
4//! - [`LexError`] - Errors during tokenization
5//! - [`ParseError`] - Errors during parsing
6//! - [`RuntimeError`] - Errors during execution
7//! - [`FiddlerError`] - Top-level error that wraps all error types
8
9use thiserror::Error;
10
11/// Position in source code for error reporting.
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
13pub struct Position {
14    /// Line number (1-indexed)
15    pub line: usize,
16    /// Column number (1-indexed)
17    pub column: usize,
18    /// Byte offset in source
19    pub offset: usize,
20}
21
22impl Position {
23    /// Create a new position.
24    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/// Errors that occur during lexical analysis (tokenization).
40#[derive(Debug, Error, Clone, PartialEq)]
41pub enum LexError {
42    /// Unexpected character encountered
43    #[error("Unexpected character '{0}' at {1}")]
44    UnexpectedCharacter(char, Position),
45
46    /// Unterminated string literal
47    #[error("Unterminated string starting at {0}")]
48    UnterminatedString(Position),
49
50    /// Invalid escape sequence in string
51    #[error("Invalid escape sequence '\\{0}' at {1}")]
52    InvalidEscape(char, Position),
53
54    /// Invalid number literal
55    #[error("Invalid number literal at {0}")]
56    InvalidNumber(Position),
57}
58
59/// Errors that occur during parsing.
60#[derive(Debug, Error, Clone, PartialEq)]
61pub enum ParseError {
62    /// Unexpected token encountered
63    #[error("Unexpected token {0} at {1}, expected {2}")]
64    UnexpectedToken(String, Position, String),
65
66    /// Unexpected end of input
67    #[error("Unexpected end of input at {0}")]
68    UnexpectedEof(Position),
69
70    /// Expected expression but found something else
71    #[error("Expected expression at {0}")]
72    ExpectedExpression(Position),
73
74    /// Expected identifier
75    #[error("Expected identifier at {0}")]
76    ExpectedIdentifier(Position),
77
78    /// Expected semicolon
79    #[error("Expected ';' at {0}")]
80    ExpectedSemicolon(Position),
81
82    /// Invalid assignment target
83    #[error("Invalid assignment target at {0}")]
84    InvalidAssignmentTarget(Position),
85}
86
87/// Errors that occur during runtime execution.
88#[derive(Debug, Error, Clone, PartialEq)]
89pub enum RuntimeError {
90    /// Undefined variable
91    #[error("Undefined variable '{name}'{}", format_position(*.position))]
92    UndefinedVariable {
93        /// The name of the undefined variable
94        name: String,
95        /// Source position where the error occurred
96        position: Option<Position>,
97    },
98
99    /// Undefined function
100    #[error("Undefined function '{name}'{}", format_position(*.position))]
101    UndefinedFunction {
102        /// The name of the undefined function
103        name: String,
104        /// Source position where the error occurred
105        position: Option<Position>,
106    },
107
108    /// Type mismatch in operation
109    #[error("Type mismatch: {message}{}", format_position(*.position))]
110    TypeMismatch {
111        /// Description of the type mismatch
112        message: String,
113        /// Source position where the error occurred
114        position: Option<Position>,
115    },
116
117    /// Division by zero
118    #[error("Division by zero{}", format_position(*.position))]
119    DivisionByZero {
120        /// Source position where the error occurred
121        position: Option<Position>,
122    },
123
124    /// Wrong number of arguments
125    #[error("Expected {expected} arguments but got {actual}{}", format_position(*.position))]
126    WrongArgumentCount {
127        /// Expected number of arguments
128        expected: usize,
129        /// Actual number of arguments provided
130        actual: usize,
131        /// Source position where the error occurred
132        position: Option<Position>,
133    },
134
135    /// Invalid argument to function
136    #[error("Invalid argument: {message}")]
137    InvalidArgument {
138        /// Description of the invalid argument
139        message: String,
140    },
141
142    /// Return from function (used internally for control flow)
143    #[error("Return outside of function")]
144    ReturnOutsideFunction,
145
146    /// Stack overflow from too deep recursion
147    #[error("Stack overflow: maximum recursion depth ({max_depth}) exceeded")]
148    StackOverflow {
149        /// The maximum recursion depth that was exceeded
150        max_depth: usize,
151    },
152}
153
154/// Format an optional position for error messages.
155fn format_position(pos: Option<Position>) -> String {
156    match pos {
157        Some(p) => format!(" at {}", p),
158        None => String::new(),
159    }
160}
161
162// Convenience constructors for backwards compatibility
163impl RuntimeError {
164    /// Create an UndefinedVariable error without position.
165    pub fn undefined_variable(name: impl Into<String>) -> Self {
166        RuntimeError::UndefinedVariable {
167            name: name.into(),
168            position: None,
169        }
170    }
171
172    /// Create an UndefinedVariable error with position.
173    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    /// Create an UndefinedFunction error without position.
181    pub fn undefined_function(name: impl Into<String>) -> Self {
182        RuntimeError::UndefinedFunction {
183            name: name.into(),
184            position: None,
185        }
186    }
187
188    /// Create a TypeMismatch error without position.
189    pub fn type_mismatch(message: impl Into<String>) -> Self {
190        RuntimeError::TypeMismatch {
191            message: message.into(),
192            position: None,
193        }
194    }
195
196    /// Create a TypeMismatch error with position.
197    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    /// Create an InvalidArgument error.
205    pub fn invalid_argument(message: impl Into<String>) -> Self {
206        RuntimeError::InvalidArgument {
207            message: message.into(),
208        }
209    }
210
211    /// Create a WrongArgumentCount error without position.
212    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/// Top-level error type that wraps all FiddlerScript errors.
222#[derive(Debug, Error, Clone, PartialEq)]
223pub enum FiddlerError {
224    /// Lexer error
225    #[error("Lex error: {0}")]
226    Lex(#[from] LexError),
227
228    /// Parser error
229    #[error("Parse error: {0}")]
230    Parse(#[from] ParseError),
231
232    /// Runtime error
233    #[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}