tcss_core/
error.rs

1//! Error types for TCSS compiler
2//!
3//! This module defines comprehensive error types with source location tracking
4//! for helpful error messages.
5
6use std::fmt;
7
8/// Source location information for error reporting
9#[derive(Debug, Clone, PartialEq)]
10pub struct SourceLocation {
11    pub line: usize,
12    pub column: usize,
13    pub file: Option<String>,
14}
15
16impl SourceLocation {
17    pub fn new(line: usize, column: usize) -> Self {
18        Self {
19            line,
20            column,
21            file: None,
22        }
23    }
24
25    pub fn with_file(line: usize, column: usize, file: String) -> Self {
26        Self {
27            line,
28            column,
29            file: Some(file),
30        }
31    }
32
33    pub fn unknown() -> Self {
34        Self {
35            line: 0,
36            column: 0,
37            file: None,
38        }
39    }
40}
41
42impl fmt::Display for SourceLocation {
43    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
44        if let Some(file) = &self.file {
45            write!(f, "{}:{}:{}", file, self.line, self.column)
46        } else {
47            write!(f, "{}:{}", self.line, self.column)
48        }
49    }
50}
51
52/// Lexer errors
53#[derive(Debug, Clone, PartialEq)]
54pub enum LexerError {
55    UnexpectedCharacter { ch: char, location: SourceLocation },
56    UnterminatedString { location: SourceLocation },
57    InvalidNumber { text: String, location: SourceLocation },
58    InvalidEscape { sequence: String, location: SourceLocation },
59}
60
61impl fmt::Display for LexerError {
62    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
63        match self {
64            LexerError::UnexpectedCharacter { ch, location } => {
65                write!(f, "Unexpected character '{}' at {}", ch, location)
66            }
67            LexerError::UnterminatedString { location } => {
68                write!(f, "Unterminated string at {}", location)
69            }
70            LexerError::InvalidNumber { text, location } => {
71                write!(f, "Invalid number '{}' at {}", text, location)
72            }
73            LexerError::InvalidEscape { sequence, location } => {
74                write!(f, "Invalid escape sequence '{}' at {}", sequence, location)
75            }
76        }
77    }
78}
79
80/// Parser errors
81#[derive(Debug, Clone, PartialEq)]
82pub enum ParserError {
83    UnexpectedToken { expected: String, found: String, location: SourceLocation },
84    UnexpectedEOF { expected: String, location: SourceLocation },
85    InvalidSyntax { message: String, location: SourceLocation },
86    IndentationError { message: String, location: SourceLocation },
87    DuplicateDefinition { name: String, location: SourceLocation },
88}
89
90impl fmt::Display for ParserError {
91    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
92        match self {
93            ParserError::UnexpectedToken { expected, found, location } => {
94                write!(f, "Expected {} but found {} at {}", expected, found, location)
95            }
96            ParserError::UnexpectedEOF { expected, location } => {
97                write!(f, "Unexpected end of file, expected {} at {}", expected, location)
98            }
99            ParserError::InvalidSyntax { message, location } => {
100                write!(f, "Invalid syntax: {} at {}", message, location)
101            }
102            ParserError::IndentationError { message, location } => {
103                write!(f, "Indentation error: {} at {}", message, location)
104            }
105            ParserError::DuplicateDefinition { name, location } => {
106                write!(f, "Duplicate definition of '{}' at {}", name, location)
107            }
108        }
109    }
110}
111
112/// Execution errors
113#[derive(Debug, Clone, PartialEq)]
114pub enum ExecutionError {
115    UndefinedVariable { name: String, location: SourceLocation },
116    UndefinedFunction { name: String, location: SourceLocation },
117    TypeError { expected: String, found: String, location: SourceLocation },
118    DivisionByZero { location: SourceLocation },
119    InvalidOperation { operation: String, message: String, location: SourceLocation },
120    ArgumentCountMismatch { expected: usize, found: usize, location: SourceLocation },
121    StackOverflow { location: SourceLocation },
122}
123
124impl fmt::Display for ExecutionError {
125    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
126        match self {
127            ExecutionError::UndefinedVariable { name, location } => {
128                write!(f, "Undefined variable '{}' at {}", name, location)
129            }
130            ExecutionError::UndefinedFunction { name, location } => {
131                write!(f, "Undefined function '{}' at {}", name, location)
132            }
133            ExecutionError::TypeError { expected, found, location } => {
134                write!(f, "Type error: expected {} but found {} at {}", expected, found, location)
135            }
136            ExecutionError::DivisionByZero { location } => {
137                write!(f, "Division by zero at {}", location)
138            }
139            ExecutionError::InvalidOperation { operation, message, location } => {
140                write!(f, "Invalid operation '{}': {} at {}", operation, message, location)
141            }
142            ExecutionError::ArgumentCountMismatch { expected, found, location } => {
143                write!(f, "Function expects {} arguments but {} were provided at {}", expected, found, location)
144            }
145            ExecutionError::StackOverflow { location } => {
146                write!(f, "Stack overflow (possible infinite recursion) at {}", location)
147            }
148        }
149    }
150}
151
152/// Generator errors
153#[derive(Debug, Clone, PartialEq)]
154pub enum GeneratorError {
155    InvalidValue { message: String, location: SourceLocation },
156    UnsupportedFeature { feature: String, location: SourceLocation },
157}
158
159impl fmt::Display for GeneratorError {
160    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
161        match self {
162            GeneratorError::InvalidValue { message, location } => {
163                write!(f, "Invalid value: {} at {}", message, location)
164            }
165            GeneratorError::UnsupportedFeature { feature, location } => {
166                write!(f, "Unsupported feature '{}' at {}", feature, location)
167            }
168        }
169    }
170}
171
172/// Main TCSS error type
173#[derive(Debug, Clone, PartialEq)]
174pub enum TcssError {
175    Lexer(LexerError),
176    Parser(ParserError),
177    Execution(ExecutionError),
178    Generator(GeneratorError),
179    IO(String),
180}
181
182impl fmt::Display for TcssError {
183    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
184        match self {
185            TcssError::Lexer(e) => write!(f, "Lexer error: {}", e),
186            TcssError::Parser(e) => write!(f, "Parser error: {}", e),
187            TcssError::Execution(e) => write!(f, "Execution error: {}", e),
188            TcssError::Generator(e) => write!(f, "Generator error: {}", e),
189            TcssError::IO(e) => write!(f, "IO error: {}", e),
190        }
191    }
192}
193
194impl std::error::Error for TcssError {}
195
196impl From<LexerError> for TcssError {
197    fn from(error: LexerError) -> Self {
198        TcssError::Lexer(error)
199    }
200}
201
202impl From<ParserError> for TcssError {
203    fn from(error: ParserError) -> Self {
204        TcssError::Parser(error)
205    }
206}
207
208impl From<ExecutionError> for TcssError {
209    fn from(error: ExecutionError) -> Self {
210        TcssError::Execution(error)
211    }
212}
213
214impl From<GeneratorError> for TcssError {
215    fn from(error: GeneratorError) -> Self {
216        TcssError::Generator(error)
217    }
218}
219
220impl From<std::io::Error> for TcssError {
221    fn from(error: std::io::Error) -> Self {
222        TcssError::IO(error.to_string())
223    }
224}
225
226/// Result type for TCSS operations
227pub type TcssResult<T> = Result<T, TcssError>;
228
229/// Helper for creating error messages with code snippets
230pub struct ErrorFormatter {
231    source: String,
232}
233
234impl ErrorFormatter {
235    pub fn new(source: String) -> Self {
236        Self { source }
237    }
238
239    /// Format an error with source code context
240    pub fn format_error(&self, error: &TcssError) -> String {
241        let location = self.get_error_location(error);
242        let mut output = format!("{}\n", error);
243
244        if let Some(loc) = location {
245            if let Some(snippet) = self.get_code_snippet(&loc, 2) {
246                output.push_str("\n");
247                output.push_str(&snippet);
248            }
249
250            // Add helpful suggestions
251            if let Some(suggestion) = self.get_suggestion(error) {
252                output.push_str("\n");
253                output.push_str(&format!("💡 Suggestion: {}\n", suggestion));
254            }
255        }
256
257        output
258    }
259
260    /// Get a helpful suggestion for an error
261    fn get_suggestion(&self, error: &TcssError) -> Option<String> {
262        match error {
263            TcssError::Execution(ExecutionError::UndefinedVariable { name, .. }) => {
264                Some(format!("Did you forget to declare the variable '{}'? Use @var {} to declare it.", name, name))
265            }
266            TcssError::Execution(ExecutionError::UndefinedFunction { name, .. }) => {
267                Some(format!("Did you forget to define the function '{}'? Use @fn {}(...) to define it.", name, name))
268            }
269            TcssError::Execution(ExecutionError::DivisionByZero { .. }) => {
270                Some("Make sure the divisor is not zero.".to_string())
271            }
272            TcssError::Execution(ExecutionError::ArgumentCountMismatch { expected, found, .. }) => {
273                Some(format!("The function expects {} argument(s), but you provided {}.", expected, found))
274            }
275            TcssError::Parser(ParserError::IndentationError { .. }) => {
276                Some("Make sure your indentation is consistent (use spaces or tabs, not both).".to_string())
277            }
278            TcssError::Lexer(LexerError::UnterminatedString { .. }) => {
279                Some("Add a closing quote (\") to terminate the string.".to_string())
280            }
281            TcssError::Parser(ParserError::UnexpectedToken { expected, .. }) => {
282                Some(format!("Expected {} here.", expected))
283            }
284            _ => None,
285        }
286    }
287
288    /// Get the location from an error
289    fn get_error_location(&self, error: &TcssError) -> Option<SourceLocation> {
290        match error {
291            TcssError::Lexer(e) => match e {
292                LexerError::UnexpectedCharacter { location, .. } => Some(location.clone()),
293                LexerError::UnterminatedString { location } => Some(location.clone()),
294                LexerError::InvalidNumber { location, .. } => Some(location.clone()),
295                LexerError::InvalidEscape { location, .. } => Some(location.clone()),
296            },
297            TcssError::Parser(e) => match e {
298                ParserError::UnexpectedToken { location, .. } => Some(location.clone()),
299                ParserError::UnexpectedEOF { location, .. } => Some(location.clone()),
300                ParserError::InvalidSyntax { location, .. } => Some(location.clone()),
301                ParserError::IndentationError { location, .. } => Some(location.clone()),
302                ParserError::DuplicateDefinition { location, .. } => Some(location.clone()),
303            },
304            TcssError::Execution(e) => match e {
305                ExecutionError::UndefinedVariable { location, .. } => Some(location.clone()),
306                ExecutionError::UndefinedFunction { location, .. } => Some(location.clone()),
307                ExecutionError::TypeError { location, .. } => Some(location.clone()),
308                ExecutionError::DivisionByZero { location } => Some(location.clone()),
309                ExecutionError::InvalidOperation { location, .. } => Some(location.clone()),
310                ExecutionError::ArgumentCountMismatch { location, .. } => Some(location.clone()),
311                ExecutionError::StackOverflow { location } => Some(location.clone()),
312            },
313            TcssError::Generator(e) => match e {
314                GeneratorError::InvalidValue { location, .. } => Some(location.clone()),
315                GeneratorError::UnsupportedFeature { location, .. } => Some(location.clone()),
316            },
317            TcssError::IO(_) => None,
318        }
319    }
320
321    /// Get a code snippet around the error location
322    fn get_code_snippet(&self, location: &SourceLocation, context_lines: usize) -> Option<String> {
323        let lines: Vec<&str> = self.source.lines().collect();
324
325        if location.line == 0 || location.line > lines.len() {
326            return None;
327        }
328
329        let line_idx = location.line - 1;
330        let start = line_idx.saturating_sub(context_lines);
331        let end = (line_idx + context_lines + 1).min(lines.len());
332
333        let mut snippet = String::new();
334        let max_line_num_width = end.to_string().len();
335
336        for i in start..end {
337            let line_num = i + 1;
338            let line = lines[i];
339
340            if i == line_idx {
341                // Error line - highlight it
342                snippet.push_str(&format!("{:>width$} | {}\n",
343                    line_num, line, width = max_line_num_width));
344
345                // Add caret pointing to the error column
346                snippet.push_str(&format!("{:>width$} | ", "", width = max_line_num_width));
347                if location.column > 0 {
348                    snippet.push_str(&" ".repeat(location.column - 1));
349                }
350                snippet.push_str("^\n");
351            } else {
352                snippet.push_str(&format!("{:>width$} | {}\n",
353                    line_num, line, width = max_line_num_width));
354            }
355        }
356
357        Some(snippet)
358    }
359}
360
361