Skip to main content

palladium/errors/
reporter.rs

1// Error reporter for Palladium
2// "Making errors helpful, not scary"
3
4use super::{Diagnostic, DiagnosticLevel, Span, Suggestion};
5use std::cmp::{max, min};
6use std::fs;
7
8pub struct ErrorReporter {
9    source_file: String,
10    source_content: String,
11}
12
13impl ErrorReporter {
14    pub fn new(source_file: String) -> std::io::Result<Self> {
15        let source_content = fs::read_to_string(&source_file)?;
16        Ok(Self {
17            source_file,
18            source_content,
19        })
20    }
21
22    pub fn report(&self, diagnostic: &Diagnostic) {
23        // Print header with error level and message
24        let (level_color, level_text) = match diagnostic.level {
25            DiagnosticLevel::Error => ("31", "error"),
26            DiagnosticLevel::Warning => ("33", "warning"),
27            DiagnosticLevel::Info => ("36", "info"),
28            DiagnosticLevel::Help => ("32", "help"),
29        };
30
31        eprintln!(
32            "\x1b[1;{}m{}\x1b[0m\x1b[1m: {}\x1b[0m",
33            level_color, level_text, diagnostic.message
34        );
35
36        // Show source location if available
37        if let Some(span) = diagnostic.span {
38            self.show_source_snippet_with_context(span, diagnostic.context_lines);
39        }
40
41        // Print notes
42        for note in &diagnostic.notes {
43            eprintln!("\x1b[1;36m  = note:\x1b[0m {}", note);
44        }
45
46        // Print suggestions
47        for suggestion in &diagnostic.suggestions {
48            self.show_suggestion(suggestion);
49        }
50
51        eprintln!(); // Empty line after error
52    }
53
54    #[allow(dead_code)]
55    fn show_source_snippet(&self, span: Span) {
56        self.show_source_snippet_with_context(span, 0);
57    }
58
59    fn show_source_snippet_with_context(&self, span: Span, context_lines: usize) {
60        // Show file location with better formatting
61        eprintln!(
62            "\x1b[1;34m  --> \x1b[0m{}:{}:{}",
63            self.source_file, span.line, span.column
64        );
65
66        let lines: Vec<&str> = self.source_content.lines().collect();
67        if span.line == 0 || span.line > lines.len() {
68            return;
69        }
70
71        // Calculate range of lines to show
72        let start_line = max(1, span.line.saturating_sub(context_lines));
73        let end_line = min(lines.len(), span.line + context_lines);
74
75        // Find the width needed for line numbers
76        let line_num_width = end_line.to_string().len();
77
78        eprintln!("{}\x1b[1;34m |\x1b[0m", " ".repeat(line_num_width));
79
80        // Show lines with context
81        for line_num in start_line..=end_line {
82            let line_text = lines[line_num - 1];
83
84            if line_num == span.line {
85                // Highlight the error line
86                eprintln!(
87                    "\x1b[1;34m{:>width$} |\x1b[0m {}",
88                    line_num,
89                    line_text,
90                    width = line_num_width
91                );
92
93                // Show error indicator
94                let mut indicator = String::new();
95                indicator.push_str(&" ".repeat(line_num_width));
96                indicator.push_str(" \x1b[1;34m|\x1b[0m ");
97
98                // Add spaces to align with error position
99                for (i, ch) in line_text.chars().enumerate() {
100                    if i < span.column.saturating_sub(1) {
101                        if ch == '\t' {
102                            indicator.push('\t');
103                        } else {
104                            indicator.push(' ');
105                        }
106                    } else {
107                        break;
108                    }
109                }
110
111                // Add error markers with better visibility
112                indicator.push_str("\x1b[1;31m^");
113                let error_len = if span.end > span.start {
114                    span.end - span.start
115                } else {
116                    // Try to highlight the whole token if we can
117                    self.estimate_token_length(&line_text[span.column.saturating_sub(1)..])
118                        .max(1)
119                };
120
121                for _ in 1..error_len {
122                    indicator.push('~');
123                }
124                indicator.push_str("\x1b[0m");
125
126                eprintln!("{}", indicator);
127            } else {
128                // Context lines in dimmed color
129                eprintln!(
130                    "\x1b[2;34m{:>width$} |\x1b[0m\x1b[2m {}\x1b[0m",
131                    line_num,
132                    line_text,
133                    width = line_num_width
134                );
135            }
136        }
137
138        eprintln!("{}\x1b[1;34m |\x1b[0m", " ".repeat(line_num_width));
139    }
140
141    fn show_suggestion(&self, suggestion: &Suggestion) {
142        eprintln!("\x1b[1;32m  = help:\x1b[0m {}", suggestion.message);
143
144        if let Some(ref replacement) = suggestion.replacement {
145            if let Some(span) = suggestion.span {
146                // Show the suggested fix inline
147                let lines: Vec<&str> = self.source_content.lines().collect();
148                if span.line > 0 && span.line <= lines.len() {
149                    let line = lines[span.line - 1];
150                    let mut fixed_line = String::new();
151
152                    // Build the fixed line
153                    if span.column > 1 {
154                        fixed_line.push_str(&line[..span.column - 1]);
155                    }
156                    fixed_line.push_str(replacement);
157                    if span.end < line.len() {
158                        fixed_line.push_str(&line[span.end..]);
159                    }
160
161                    eprintln!("\x1b[1;32m         Suggested fix:\x1b[0m");
162                    eprintln!("\x1b[32m         {}\x1b[0m", fixed_line);
163                }
164            }
165        }
166    }
167
168    fn estimate_token_length(&self, text: &str) -> usize {
169        // Estimate the length of the token at the beginning of the text
170        let mut len = 0;
171        for ch in text.chars() {
172            if ch.is_alphanumeric() || ch == '_' {
173                len += 1;
174            } else if len > 0 {
175                break;
176            } else if !ch.is_whitespace() {
177                // Single character token
178                return 1;
179            }
180        }
181        len
182    }
183}
184
185/// Helper to create common diagnostics
186pub struct DiagnosticBuilder;
187
188impl DiagnosticBuilder {
189    pub fn type_mismatch(expected: &str, found: &str, span: Span) -> Diagnostic {
190        Diagnostic::error(format!(
191            "type mismatch: expected {}, found {}",
192            expected, found
193        ))
194        .with_span(span)
195        .with_note("types must match exactly")
196        .with_context_lines(2)
197    }
198
199    pub fn undefined_variable(name: &str, span: Span) -> Diagnostic {
200        Diagnostic::error(format!("undefined variable: {}", name))
201            .with_span(span)
202            .with_note("variables must be declared before use")
203            .with_suggestion(
204                format!("did you mean to declare it? Try: let {} = ...;", name),
205                None,
206            )
207            .with_context_lines(3)
208    }
209
210    pub fn missing_semicolon(span: Span) -> Diagnostic {
211        Diagnostic::error("expected ';' after statement")
212            .with_span(span)
213            .with_note("each statement must end with a semicolon")
214            .with_suggestion(
215                "add a semicolon at the end of this line",
216                Some(";".to_string()),
217            )
218    }
219
220    pub fn wrong_arg_count(func: &str, expected: usize, found: usize, span: Span) -> Diagnostic {
221        let msg = if expected == 1 {
222            format!(
223                "function '{}' expects {} argument, but {} were provided",
224                func, expected, found
225            )
226        } else {
227            format!(
228                "function '{}' expects {} arguments, but {} were provided",
229                func, expected, found
230            )
231        };
232
233        let mut diag = Diagnostic::error(msg)
234            .with_span(span)
235            .with_note(format!("function signature: {}(...)", func))
236            .with_context_lines(2);
237
238        if found < expected {
239            diag = diag.with_suggestion(
240                format!(
241                    "add {} more argument{}",
242                    expected - found,
243                    if expected - found == 1 { "" } else { "s" }
244                ),
245                None,
246            );
247        } else {
248            diag = diag.with_suggestion(
249                format!(
250                    "remove {} argument{}",
251                    found - expected,
252                    if found - expected == 1 { "" } else { "s" }
253                ),
254                None,
255            );
256        }
257
258        diag
259    }
260}