Skip to main content

kore/
diagnostics.rs

1//! Pretty error reporting for KORE
2//! Shows source context with line numbers and error highlighting
3
4use crate::span::Span;
5use crate::error::KoreError;
6
7/// Diagnostic renderer for pretty error messages
8pub struct Diagnostics<'a> {
9    source: &'a str,
10    filename: &'a str,
11}
12
13impl<'a> Diagnostics<'a> {
14    pub fn new(source: &'a str, filename: &'a str) -> Self {
15        Self { source, filename }
16    }
17    
18    /// Format an error with source context
19    pub fn format_error(&self, error: &KoreError) -> String {
20        match error {
21            KoreError::Lexer { message, span } => self.format_with_context("Lexer Error", message, *span),
22            KoreError::Parser { message, span } => self.format_with_context("Parse Error", message, *span),
23            KoreError::Type { message, span } => self.format_with_context("Type Error", message, *span),
24            KoreError::Effect { message, span } => self.format_with_context("Effect Error", message, *span),
25            KoreError::Borrow { message, span } => self.format_with_context("Borrow Error", message, *span),
26            KoreError::Codegen { message, span } => self.format_with_context("Codegen Error", message, *span),
27            KoreError::Runtime { message } => format!(
28                "\n\x1b[1;31merror\x1b[0m: {}\n",
29                message
30            ),
31            KoreError::Io(e) => format!(
32                "\n\x1b[1;31merror\x1b[0m: IO error: {}\n",
33                e
34            ),
35        }
36    }
37    
38    fn format_with_context(&self, error_type: &str, message: &str, span: Span) -> String {
39        let (line_num, col, line_content) = self.get_line_info(span);
40        
41        let mut output = String::new();
42        
43        // Error header
44        output.push_str(&format!(
45            "\n\x1b[1;31merror[{}]\x1b[0m: {}\n",
46            error_type, message
47        ));
48        
49        // Location
50        output.push_str(&format!(
51            "  \x1b[1;34m-->\x1b[0m {}:{}:{}\n",
52            self.filename, line_num, col
53        ));
54        
55        // Separator
56        output.push_str("   \x1b[1;34m|\x1b[0m\n");
57        
58        // Source line
59        output.push_str(&format!(
60            "\x1b[1;34m{:>3} |\x1b[0m {}\n",
61            line_num, line_content
62        ));
63        
64        // Error pointer
65        let pointer_offset = col.saturating_sub(1);
66        let content_len = line_content.len();
67        let remaining_len = content_len.saturating_sub(pointer_offset);
68        let span_len = span.end.saturating_sub(span.start);
69        let pointer_len = span_len.min(remaining_len).max(1);
70        
71        output.push_str(&format!(
72            "   \x1b[1;34m|\x1b[0m {}\x1b[1;31m{}\x1b[0m\n",
73            " ".repeat(pointer_offset),
74            "^".repeat(pointer_len)
75        ));
76        
77        // Separator
78        output.push_str("   \x1b[1;34m|\x1b[0m\n");
79        
80        output
81    }
82    
83    /// Get line number, column, and line content for a span
84    fn get_line_info(&self, span: Span) -> (usize, usize, &str) {
85        let mut line_num = 1;
86        let mut line_start = 0;
87        
88        // Safety check for span bounds
89        let start = span.start.min(self.source.len());
90        
91        for (i, c) in self.source.char_indices() {
92            if i >= start {
93                break;
94            }
95            if c == '\n' {
96                line_num += 1;
97                line_start = i + 1;
98            }
99        }
100        
101        let col = start.saturating_sub(line_start) + 1;
102        
103        // Find line end
104        let line_end = if start < self.source.len() {
105            self.source[start..]
106                .find('\n')
107                .map(|i| start + i)
108                .unwrap_or(self.source.len())
109        } else {
110            self.source.len()
111        };
112        
113        let line_start = line_start.min(self.source.len());
114        let line_content = &self.source[line_start..line_end];
115        
116        (line_num, col, line_content)
117    }
118}
119
120/// Format an error without source context (for runtime errors)
121pub fn format_simple_error(error: &KoreError) -> String {
122    match error {
123        KoreError::Runtime { message } => format!("Runtime Error: {}", message),
124        _ => format!("{}", error),
125    }
126}
127
128#[cfg(test)]
129mod tests {
130    use super::*;
131    
132    #[test]
133    fn test_line_info() {
134        let source = "let x = 5\nlet y = x + 1\nprint(y)";
135        let diag = Diagnostics::new(source, "test.kr");
136        
137        // First line
138        let (line, col, content) = diag.get_line_info(Span::new(0, 3));
139        assert_eq!(line, 1);
140        assert_eq!(col, 1);
141        assert_eq!(content, "let x = 5");
142        
143        // Second line
144        let (line, col, content) = diag.get_line_info(Span::new(14, 15));
145        assert_eq!(line, 2);
146        assert_eq!(content, "let y = x + 1");
147    }
148}
149