Skip to main content

palladium/errors/
mod.rs

1// Error handling for Palladium compiler
2// "Even legends make mistakes, but they handle them gracefully"
3
4use thiserror::Error;
5
6pub mod pretty;
7pub mod reporter;
8pub mod suggestions;
9
10pub type Result<T> = std::result::Result<T, CompileError>;
11
12#[derive(Error, Debug)]
13pub enum CompileError {
14    // Lexer errors
15    #[error("Unexpected character '{ch}' at line {line}, column {col}")]
16    UnexpectedChar {
17        ch: char,
18        line: usize,
19        col: usize,
20        span: Option<Span>,
21    },
22
23    #[error("Unterminated string literal at line {line}")]
24    UnterminatedString { line: usize, span: Option<Span> },
25
26    // Parser errors
27    #[error("Unexpected token: expected {expected}, found {found}")]
28    UnexpectedToken {
29        expected: String,
30        found: String,
31        span: Option<Span>,
32    },
33
34    #[error("Syntax error: {message}")]
35    SyntaxError { message: String, span: Option<Span> },
36
37    // Type errors
38    #[error("Type mismatch: expected {expected}, found {found}")]
39    TypeMismatch {
40        expected: String,
41        found: String,
42        span: Option<Span>,
43    },
44
45    #[error("Undefined variable: {name}")]
46    UndefinedVariable { name: String, span: Option<Span> },
47
48    #[error("Undefined function: {name}")]
49    UndefinedFunction { name: String, span: Option<Span> },
50
51    #[error("Function {name} expects {expected} arguments, but {found} were provided")]
52    ArgumentCountMismatch {
53        name: String,
54        expected: usize,
55        found: usize,
56        span: Option<Span>,
57    },
58
59    // Codegen errors
60    #[error("Code generation failed: {message}")]
61    CodegenError { message: String },
62
63    // IO errors
64    #[error("IO error: {0}")]
65    IoError(#[from] std::io::Error),
66
67    // Generic error
68    #[error("{0}")]
69    Generic(String),
70
71    // Missing semicolon
72    #[error("Missing semicolon after statement")]
73    MissingSemicolon { span: Option<Span> },
74
75    // Invalid function signature
76    #[error("Invalid function signature")]
77    InvalidFunctionSignature { message: String, span: Option<Span> },
78
79    // Borrow checker errors
80    #[error("Borrow checker error: {message}")]
81    BorrowChecker { message: String, span: Option<Span> },
82
83    #[error("Use of moved value: {name}")]
84    UseOfMovedValue { name: String, span: Option<Span> },
85
86    #[error("Use of uninitialized value: {name}")]
87    UseOfUninitializedValue { name: String, span: Option<Span> },
88
89    #[error("Cannot move out of borrowed content")]
90    CannotMoveOutOfBorrowedContent { span: Option<Span> },
91
92    // Unsafe operation errors
93    #[error("Unsafe operation '{operation}' requires unsafe block")]
94    UnsafeOperation { operation: String, span: Span },
95
96    #[error("Conflicting borrows: {message}")]
97    ConflictingBorrows { message: String, span: Option<Span> },
98
99    #[error("Lifetime error: {message}")]
100    LifetimeError { message: String, span: Option<Span> },
101
102    // Pattern matching errors
103    #[error("Non-exhaustive match: missing patterns {}", missing_patterns.join(", "))]
104    NonExhaustiveMatch {
105        missing_patterns: Vec<String>,
106        span: Option<Span>,
107    },
108
109    #[error("Unreachable pattern: {}", patterns.join(", "))]
110    UnreachablePattern {
111        patterns: Vec<String>,
112        span: Option<Span>,
113    },
114}
115
116/// Source location information
117#[derive(Debug, Clone, Copy, PartialEq, Eq)]
118pub struct Span {
119    pub start: usize,
120    pub end: usize,
121    pub line: usize,
122    pub column: usize,
123}
124
125impl Span {
126    pub fn new(start: usize, end: usize, line: usize, column: usize) -> Self {
127        Self {
128            start,
129            end,
130            line,
131            column,
132        }
133    }
134
135    pub fn dummy() -> Self {
136        Self {
137            start: 0,
138            end: 0,
139            line: 0,
140            column: 0,
141        }
142    }
143
144    pub fn extend_to(&self, other: &Span) -> Self {
145        Self {
146            start: self.start.min(other.start),
147            end: self.end.max(other.end),
148            line: self.line.min(other.line),
149            column: if self.line < other.line {
150                self.column
151            } else {
152                self.column.min(other.column)
153            },
154        }
155    }
156}
157
158impl CompileError {
159    /// Convert this error into a diagnostic with helpful suggestions
160    pub fn to_diagnostic(&self) -> Diagnostic {
161        match self {
162            CompileError::UnexpectedChar {
163                ch,
164                line,
165                col,
166                span,
167            } => Diagnostic::error(format!(
168                "Unexpected character '{}' at line {}, column {}",
169                ch, line, col
170            ))
171            .with_span(span.unwrap_or(Span::new(0, 1, *line, *col)))
172            .with_note("Palladium only allows ASCII letters, numbers, and common symbols")
173            .with_suggestion("Remove or replace this character with a valid one", None),
174
175            CompileError::UnterminatedString { line, span } => {
176                Diagnostic::error(format!("Unterminated string literal at line {}", line))
177                    .with_span(span.unwrap_or(Span::new(0, 0, *line, 1)))
178                    .with_note("Strings must be closed with a matching quote")
179                    .with_suggestion(
180                        "Add a closing quote (\") to end the string",
181                        Some("\"".to_string()),
182                    )
183            }
184
185            CompileError::UnexpectedToken {
186                expected,
187                found,
188                span,
189            } => Diagnostic::error(format!("Expected {}, but found {}", expected, found))
190                .with_span(span.unwrap_or(Span::dummy()))
191                .with_note("The syntax requires a specific token here")
192                .with_suggestion(
193                    format!("Replace '{}' with '{}'", found, expected),
194                    Some(expected.clone()),
195                ),
196
197            CompileError::SyntaxError { message, span } => Diagnostic::error(message.clone())
198                .with_span(span.unwrap_or(Span::dummy()))
199                .with_note("Check the language syntax rules"),
200
201            CompileError::TypeMismatch {
202                expected,
203                found,
204                span,
205            } => {
206                let mut diag = Diagnostic::error(format!(
207                    "Type mismatch: expected {}, found {}",
208                    expected, found
209                ))
210                .with_span(span.unwrap_or(Span::dummy()))
211                .with_note("Types must match exactly in Palladium");
212
213                // Add specific suggestions based on common type mismatches
214                match (expected.as_str(), found.as_str()) {
215                    ("int", "string") => {
216                        diag = diag.with_suggestion(
217                            "Convert the string to an integer using parse_int()",
218                            None,
219                        );
220                    }
221                    ("string", "int") => {
222                        diag = diag.with_suggestion(
223                            "Convert the integer to a string using to_string()",
224                            None,
225                        );
226                    }
227                    ("bool", _) => {
228                        diag =
229                            diag.with_suggestion("Use 'true' or 'false' for boolean values", None);
230                    }
231                    _ => {}
232                }
233
234                diag
235            }
236
237            CompileError::UndefinedVariable { name, span } => {
238                Diagnostic::error(format!("Undefined variable: {}", name))
239                    .with_span(span.unwrap_or(Span::dummy()))
240                    .with_note("Variables must be declared before use")
241                    .with_suggestion(
242                        format!("Did you mean to declare it? Try: let {} = ...;", name),
243                        None,
244                    )
245                    .with_context_lines(3)
246            }
247
248            CompileError::UndefinedFunction { name, span } => {
249                let mut diag = Diagnostic::error(format!("Undefined function: {}", name))
250                    .with_span(span.unwrap_or(Span::dummy()))
251                    .with_note("Functions must be defined before they are called");
252
253                // Suggest common function names if they're close
254                match name.as_str() {
255                    "print" => {
256                        diag = diag.with_suggestion(
257                            "Did you mean 'println'? The print function is called 'println' in Palladium",
258                            Some("println".to_string())
259                        );
260                    }
261                    "printf" => {
262                        diag = diag.with_suggestion(
263                            "Palladium uses 'println' instead of 'printf'",
264                            Some("println".to_string()),
265                        );
266                    }
267                    _ => {
268                        diag = diag.with_suggestion(
269                            format!("Define the function first: fn {}() {{ ... }}", name),
270                            None,
271                        );
272                    }
273                }
274
275                diag
276            }
277
278            CompileError::ArgumentCountMismatch {
279                name,
280                expected,
281                found,
282                span,
283            } => {
284                let mut diag = Diagnostic::error(format!(
285                    "Function '{}' expects {} argument{}, but {} {} provided",
286                    name,
287                    expected,
288                    if *expected == 1 { "" } else { "s" },
289                    found,
290                    if *found == 1 { "was" } else { "were" }
291                ))
292                .with_span(span.unwrap_or(Span::dummy()));
293
294                if *found < *expected {
295                    diag = diag.with_suggestion(
296                        format!(
297                            "Add {} more argument{}",
298                            expected - found,
299                            if expected - found == 1 { "" } else { "s" }
300                        ),
301                        None,
302                    );
303                } else {
304                    diag = diag.with_suggestion(
305                        format!(
306                            "Remove {} argument{}",
307                            found - expected,
308                            if found - expected == 1 { "" } else { "s" }
309                        ),
310                        None,
311                    );
312                }
313
314                diag
315            }
316
317            CompileError::MissingSemicolon { span } => {
318                Diagnostic::error("Missing semicolon after statement")
319                    .with_span(span.unwrap_or(Span::dummy()))
320                    .with_note("Every statement in Palladium must end with a semicolon")
321                    .with_suggestion(
322                        "Add a semicolon (;) at the end of this line",
323                        Some(";".to_string()),
324                    )
325            }
326
327            CompileError::InvalidFunctionSignature { message, span } => Diagnostic::error(format!(
328                "Invalid function signature: {}",
329                message
330            ))
331            .with_span(span.unwrap_or(Span::dummy()))
332            .with_note(
333                "Function signatures must follow the pattern: fn name(param: Type) -> ReturnType",
334            )
335            .with_suggestion(
336                "Example: fn add(x: int, y: int) -> int { return x + y; }",
337                None,
338            ),
339
340            CompileError::NonExhaustiveMatch {
341                missing_patterns,
342                span,
343            } => {
344                let mut diag = Diagnostic::error("Non-exhaustive match expression")
345                    .with_span(span.unwrap_or(Span::dummy()))
346                    .with_note("All possible patterns must be covered in a match expression");
347
348                if missing_patterns.len() == 1 {
349                    diag = diag.with_suggestion(
350                        format!("Add a pattern for: {}", missing_patterns[0]),
351                        None,
352                    );
353                } else if missing_patterns.len() <= 3 {
354                    diag = diag.with_suggestion(
355                        format!("Add patterns for: {}", missing_patterns.join(", ")),
356                        None,
357                    );
358                } else {
359                    diag = diag.with_suggestion(
360                        "Add remaining patterns or use a wildcard pattern (_) to match all other cases",
361                        None
362                    );
363                }
364
365                diag
366            }
367
368            CompileError::UnreachablePattern { patterns: _, span } => Diagnostic::error(
369                "Unreachable pattern detected",
370            )
371            .with_span(span.unwrap_or(Span::dummy()))
372            .with_note(
373                "This pattern can never be matched because previous patterns cover all cases",
374            )
375            .with_suggestion("Remove this pattern or reorder the patterns", None),
376
377            _ => {
378                // Default diagnostic for other errors
379                Diagnostic::error(self.to_string())
380            }
381        }
382    }
383}
384
385/// A diagnostic message with source location
386#[derive(Debug)]
387pub struct Diagnostic {
388    pub level: DiagnosticLevel,
389    pub message: String,
390    pub span: Option<Span>,
391    pub notes: Vec<String>,
392    pub suggestions: Vec<Suggestion>,
393    pub context_lines: usize, // Number of lines to show before/after the error
394}
395
396/// A suggestion for fixing an error
397#[derive(Debug)]
398pub struct Suggestion {
399    pub message: String,
400    pub replacement: Option<String>,
401    pub span: Option<Span>,
402}
403
404#[derive(Debug, Clone, Copy)]
405pub enum DiagnosticLevel {
406    Error,
407    Warning,
408    Info,
409    Help,
410}
411
412impl Diagnostic {
413    pub fn error(message: impl Into<String>) -> Self {
414        Self {
415            level: DiagnosticLevel::Error,
416            message: message.into(),
417            span: None,
418            notes: Vec::new(),
419            suggestions: Vec::new(),
420            context_lines: 2,
421        }
422    }
423
424    pub fn with_span(mut self, span: Span) -> Self {
425        self.span = Some(span);
426        self
427    }
428
429    pub fn with_note(mut self, note: impl Into<String>) -> Self {
430        self.notes.push(note.into());
431        self
432    }
433
434    pub fn with_suggestion(
435        mut self,
436        message: impl Into<String>,
437        replacement: Option<String>,
438    ) -> Self {
439        self.suggestions.push(Suggestion {
440            message: message.into(),
441            replacement,
442            span: self.span,
443        });
444        self
445    }
446
447    pub fn with_context_lines(mut self, lines: usize) -> Self {
448        self.context_lines = lines;
449        self
450    }
451}