pulsar_utils/
error.rs

1// Copyright (C) 2024 Ethan Uppal. All rights reserved.
2use super::loc::{Region, RegionProvider};
3use colored::*;
4use std::{cell::RefCell, fmt::Display, io, rc::Rc};
5
6#[repr(i32)]
7#[derive(Clone, Copy, Debug)]
8pub enum ErrorCode {
9    UnknownError,
10    UnrecognizedCharacter,
11    UnexpectedEOF,
12    UnexpectedToken,
13    InvalidTopLevelConstruct,
14    ConstructShouldBeTopLevel,
15    InvalidTokenForStatement,
16    InvalidOperatorSyntax,
17    MalformedType,
18    UnboundName,
19    StaticAnalysisIssue
20}
21
22impl Display for ErrorCode {
23    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
24        (*self as i32).fmt(f)
25    }
26}
27
28impl Default for ErrorCode {
29    fn default() -> Self {
30        Self::UnknownError
31    }
32}
33
34#[derive(PartialEq, Eq, Debug)]
35pub enum Level {
36    Info,
37    Note,
38    Warning,
39    Error
40}
41
42impl Level {
43    fn color(&self) -> Color {
44        match self {
45            Self::Info => Color::BrightWhite,
46            Self::Note => Color::BrightYellow,
47            Self::Warning => Color::Yellow,
48            Self::Error => Color::BrightRed
49        }
50    }
51
52    fn form_header(&self, code: ErrorCode) -> ColoredString {
53        let string = self.to_string();
54        format!(
55            "{}[{}{:04}]",
56            string,
57            string.chars().next().unwrap().to_ascii_uppercase(),
58            code
59        )
60        .color(self.color())
61    }
62}
63
64impl Display for Level {
65    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
66        write!(
67            f,
68            "{}",
69            match self {
70                Self::Info => "info",
71                Self::Note => "note",
72                Self::Warning => "warning",
73                Self::Error => "error"
74            }
75        )
76    }
77}
78
79#[derive(PartialEq, Eq, Debug)]
80pub enum Style {
81    Primary,
82    Secondary
83}
84
85/// An error at a given location with various information and diagnostics. Use
86/// [`Error::fmt`] to obtain a printable version of the error.
87#[derive(Debug)]
88pub struct Error {
89    style: Style,
90    level: Level,
91    code: ErrorCode,
92    region: Option<Region>,
93    message: String,
94    explain: Option<String>,
95    fix: Option<String>
96}
97
98impl Display for Error {
99    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
100        // Only primary-style messages have a header indicating they are the
101        // root of an error message and not auxillary information
102        if self.style == Style::Primary {
103            write!(f, "{}: ", self.level.form_header(self.code).bold(),)?;
104            if self.region.is_some() {
105                write!(
106                    f,
107                    "{}: ",
108                    self.region.as_ref().unwrap().start.to_string().underline()
109                )?;
110            }
111        }
112        writeln!(f, "{}", self.message.bold())?;
113
114        // If there is no region associated with this error, then we have
115        // nothing more to print
116        if self.region.is_none() {
117            return Ok(());
118        }
119
120        // Otherwise, we print the region via a sequence of lines from the
121        // source.
122        let region = self.region.as_ref().unwrap();
123        let region_extra_lines = (region.end.line - region.start.line) as usize;
124        let show_lines_before = 1;
125        let show_lines_after = 1;
126        let mut already_explained = false;
127        writeln!(f, "{}", "     │  ".dimmed())?;
128        let (lines, current_line_pos) = region
129            .start
130            .lines(show_lines_before, region_extra_lines + show_lines_after);
131        let show_start_line =
132            region.start_line() - (show_lines_before as isize);
133        let region_line_sections =
134            region.find_intersection(&lines, show_start_line);
135        for (i, line) in lines.iter().enumerate() {
136            if i > 0 {
137                writeln!(f)?;
138            }
139            write!(
140                f,
141                "{}",
142                format!(
143                    "{: >4} │  ",
144                    i + (region.start.line as usize) - current_line_pos
145                )
146                .dimmed()
147            )?;
148            if i >= current_line_pos
149                && i <= current_line_pos + region_extra_lines
150            {
151                let line_section = &region_line_sections[i - current_line_pos];
152                let split_first = line_section.start;
153                let (part1, rest) = line.split_at(split_first as usize);
154                if !line.is_empty() {
155                    let split_second =
156                        split_first + (line_section.length() as isize) - 1;
157                    let (part2, part3) = if (split_second as usize)
158                        == line.len()
159                    {
160                        (rest, "")
161                    } else {
162                        rest.split_at((split_second - split_first + 1) as usize)
163                    };
164                    write!(
165                        f,
166                        "{}{}{}",
167                        part1,
168                        part2.color(self.level.color()),
169                        if part3.is_empty() {
170                            " ".on_color(self.level.color())
171                        } else {
172                            part3.into()
173                        }
174                    )?;
175                } else {
176                    write!(f, "{}{}", part1, " ".on_color(self.level.color()))?;
177                }
178                if let Some(explain) =
179                    &self.explain.as_ref().filter(|_| !already_explained)
180                {
181                    already_explained = true;
182                    fn create_error_pointer(length: usize) -> String {
183                        match length {
184                            0 => "".into(),
185                            n => format!("└{}", "─".repeat(n - 1))
186                        }
187                    }
188                    writeln!(f)?;
189                    write!(
190                        f,
191                        "{}  {}{} {}",
192                        "     │".dimmed(),
193                        " ".repeat(part1.len()),
194                        create_error_pointer(line_section.length())
195                            .color(self.level.color()),
196                        explain.bold().italic()
197                    )?;
198                }
199            } else {
200                write!(f, "{}", line)?;
201            }
202        }
203        write!(f, "\n{}", "     │  ".dimmed())?;
204        if let Some(fix) = &self.fix {
205            write!(f, "\nSuggestion: {}", fix.bold())?;
206        }
207        Ok(())
208    }
209}
210
211impl Default for Error {
212    fn default() -> Self {
213        Error {
214            style: Style::Primary,
215            level: Level::Error,
216            code: ErrorCode::default(),
217            region: None,
218            message: String::default(),
219            explain: None,
220            fix: None
221        }
222    }
223}
224
225/// An interface for fluently constructing errors.
226pub struct ErrorBuilder {
227    error: Error
228}
229
230impl ErrorBuilder {
231    /// Constructs a new error builder.
232    pub fn new() -> Self {
233        ErrorBuilder {
234            error: Error::default()
235        }
236    }
237
238    /// Uses the display style `style`.
239    pub fn of_style(mut self, style: Style) -> Self {
240        self.error.style = style;
241        self
242    }
243
244    /// Uses the severity level `level`.
245    pub fn at_level(mut self, level: Level) -> Self {
246        self.error.level = level;
247        self
248    }
249
250    /// Uses the error code `code`.
251    pub fn with_code(mut self, code: ErrorCode) -> Self {
252        self.error.code = code;
253        self
254    }
255
256    /// Locates the error at the region given by `region_provider`.
257    pub fn at_region<R: RegionProvider>(mut self, region_provider: &R) -> Self {
258        self.error.region = Some(region_provider.region());
259        self
260    }
261
262    /// Identifies the error as having no location.
263    pub fn without_loc(mut self) -> Self {
264        self.error.region = None;
265        self
266    }
267
268    /// Uses the main error description `message`.
269    pub fn message(mut self, message: String) -> Self {
270        self.error.message = message;
271        self
272    }
273
274    /// Marks this error as without a message and instead continuing a previous
275    /// error.
276    ///
277    /// Requires: the error is of secondary style, which must be set before this
278    /// function is called (see [`ErrorBuilder::of_style`]).
279    pub fn continues(self) -> Self {
280        assert!(self.error.style == Style::Secondary);
281        self.message("   ...".into())
282    }
283
284    /// Uses an explanatory note `explain`.
285    pub fn explain(mut self, explain: String) -> Self {
286        self.error.explain = Some(explain);
287        self
288    }
289
290    /// Uses a recommendation `fix`.
291    pub fn fix(mut self, fix: String) -> Self {
292        self.error.fix = Some(fix);
293        self
294    }
295
296    /// Produces the error.
297    pub fn build(self) -> Error {
298        self.error
299    }
300}
301
302/// A shared error manager with an error recording limit.
303pub struct ErrorManager {
304    max_count: usize,
305    primary_count: usize,
306    errors: Vec<Error>
307}
308
309impl ErrorManager {
310    /// Constructs a shared error manager that can record up to `max_count`
311    /// primary errors.
312    pub fn with_max_count(max_count: usize) -> Rc<RefCell<ErrorManager>> {
313        Rc::new(RefCell::new(ErrorManager {
314            max_count,
315            primary_count: 0,
316            errors: vec![]
317        }))
318    }
319
320    /// Whether the error manager has recorded an error.
321    pub fn has_errors(&self) -> bool {
322        !self.errors.is_empty()
323    }
324
325    /// Whether the error manager cannot take any further primary errors.
326    pub fn is_full(&self) -> bool {
327        self.primary_count == self.max_count
328    }
329
330    /// Records `error` and returns `true` unless `is_full()`.
331    pub fn record(&mut self, error: Error) -> bool {
332        if self.is_full() {
333            false
334        } else {
335            if error.style == Style::Primary && error.level == Level::Error {
336                self.primary_count += 1
337            }
338            self.errors.push(error);
339            true
340        }
341    }
342
343    /// Prints and clears all recorded errors to `output`.
344    pub fn consume_and_write<W: io::Write>(
345        &mut self, output: &mut W
346    ) -> io::Result<()> {
347        for (i, error) in self.errors.iter().enumerate() {
348            if error.style == Style::Primary && i > 0 {
349                output.write_all(&[b'\n'])?;
350            }
351            output.write_all(error.to_string().as_bytes())?;
352            output.write_all(&[b'\n'])?;
353            output.flush()?;
354        }
355        self.errors.clear();
356        self.primary_count = 0;
357        Ok(())
358    }
359}