lc3_toolchain/
error.rs

1use crate::ast::parse::Rule;
2
3pub fn print_error(filename: &str, source: &str, error: pest::error::Error<Rule>) {
4    use codespan_reporting::diagnostic::{Diagnostic, Label};
5    use codespan_reporting::files::SimpleFile;
6    use codespan_reporting::term::{self, Config};
7    use pest::error::LineColLocation;
8
9    let file = SimpleFile::new(filename, source);
10
11    // Get proper span information from the error
12    let (start_offset, end_offset) = match &error.line_col {
13        LineColLocation::Pos((line, col)) => {
14            // For single position errors, convert line/col to offset
15            let line_offsets: Vec<usize> = source
16                .char_indices()
17                .filter_map(|(i, c)| if c == '\n' { Some(i) } else { None })
18                .collect();
19
20            let line_idx = line - 1; // convert to 0-based index
21            let line_start = if line_idx == 0 {
22                0
23            } else {
24                line_offsets[line_idx - 1] + 1
25            };
26            let offset = line_start + col - 1;
27
28            (offset, offset + 1) // Make a single character span
29        }
30        LineColLocation::Span((start_line, start_col), (end_line, end_col)) => {
31            // For span errors, calculate offsets for both ends
32            let line_offsets: Vec<usize> = source
33                .char_indices()
34                .filter_map(|(i, c)| if c == '\n' { Some(i) } else { None })
35                .collect();
36
37            // Calculate start offset
38            let start_line_idx = start_line - 1; // convert to 0-based index
39            let start_line_offset = if start_line_idx == 0 {
40                0
41            } else {
42                line_offsets[start_line_idx - 1] + 1
43            };
44            let start_pos = start_line_offset + start_col - 1;
45
46            // Calculate end offset
47            let end_line_idx = end_line - 1; // convert to 0-based index
48            let end_line_offset = if end_line_idx == 0 {
49                0
50            } else {
51                line_offsets[end_line_idx - 1] + 1
52            };
53            let end_pos = end_line_offset + end_col;
54
55            (start_pos, end_pos)
56        }
57    };
58
59    // Get the problematic part of the input for better error messages
60    let error_text = if end_offset > start_offset && end_offset <= source.len() {
61        source[start_offset..end_offset].to_string()
62    } else {
63        "".to_string()
64    };
65
66    // Create a more descriptive message based on the error type
67    let message = match &error.variant {
68        pest::error::ErrorVariant::ParsingError {
69            positives,
70            negatives,
71        } => {
72            if !positives.is_empty() {
73                format!("Expected {}", format_rules(positives))
74            } else if !negatives.is_empty() {
75                format!("Unexpected {}", format_rules(negatives))
76            } else {
77                "Parsing error".to_string()
78            }
79        }
80        pest::error::ErrorVariant::CustomError { message } => message.clone(),
81    };
82
83    // Create notes with additional context
84    let mut notes = Vec::new();
85
86    match &error.variant {
87        pest::error::ErrorVariant::ParsingError {
88            positives,
89            negatives,
90        } => {
91            if !positives.is_empty() && !negatives.is_empty() {
92                notes.push(format!(
93                    "Found `{}`, but expected {}",
94                    if error_text.is_empty() {
95                        "???"
96                    } else {
97                        &error_text
98                    },
99                    format_rules(positives)
100                ));
101            }
102        }
103        _ => {}
104    }
105
106    // Create the diagnostic
107    let mut diagnostic = Diagnostic::error()
108        .with_message("Syntax error")
109        .with_labels(vec![
110            Label::primary((), start_offset..end_offset).with_message(message),
111        ]);
112
113    // Add notes if there are any
114    if !notes.is_empty() {
115        diagnostic = diagnostic.with_notes(notes);
116    }
117
118    // Emit the diagnostic
119    let writer = term::termcolor::StandardStream::stderr(term::termcolor::ColorChoice::Auto);
120    let config = Config::default();
121    term::emit(&mut writer.lock(), &config, &file, &diagnostic).unwrap();
122}
123
124// Helper function to format rules in a readable way
125fn format_rules(rules: &[Rule]) -> String {
126    if rules.is_empty() {
127        return "nothing".to_string();
128    }
129
130    let rule_strings: Vec<String> = rules.iter().map(|rule| format!("`{:?}`", rule)).collect();
131
132    if rule_strings.len() == 1 {
133        rule_strings[0].clone()
134    } else {
135        let last = rule_strings.last().unwrap();
136        let rest = &rule_strings[..rule_strings.len() - 1];
137        format!("{} or {}", rest.join(", "), last)
138    }
139}