somni_expr/
error.rs

1//! Errors.
2
3use std::fmt::Display;
4
5use somni_parser::{
6    lexer::{LexerError, Location},
7    parser::ParserError,
8};
9
10use crate::EvalError;
11
12/// Represents an error that has a location in the source code.
13pub trait ErrorWithLocation: Display {
14    /// Returns the location of the error in the source code.
15    fn location(&self) -> Location;
16}
17
18impl ErrorWithLocation for LexerError {
19    fn location(&self) -> Location {
20        self.location
21    }
22}
23
24impl ErrorWithLocation for ParserError {
25    fn location(&self) -> Location {
26        self.location
27    }
28}
29impl ErrorWithLocation for EvalError {
30    fn location(&self) -> Location {
31        self.location
32    }
33}
34
35/// Marks the source code with the error location and message.
36pub struct MarkInSource<'s>(pub &'s str, pub Location, pub &'s str, pub &'s str);
37impl std::fmt::Display for MarkInSource<'_> {
38    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
39        let start_loc = location(self.0, self.1.start);
40        let end_loc = location(self.0, self.1.end);
41        mark_in_source(f, self.0, start_loc, end_loc, self.2, self.3)
42    }
43}
44
45fn mark_in_source(
46    f: &mut std::fmt::Formatter<'_>,
47    source: &str,
48    start_loc: (usize, usize),
49    end_loc: (usize, usize),
50    message: &str,
51    hint: &str,
52) -> Result<(), std::fmt::Error> {
53    let (start_line, start_col) = start_loc;
54    let (end_line, end_col) = end_loc;
55
56    let line = source.lines().nth(start_line - 1).unwrap();
57    let line_no_chars = start_line.ilog10() as usize + 1;
58    f.write_fmt(format_args!(
59        "{bold}{message}{reset}\n{:>width$}{blue}--->{reset} at line {start_line} column {start_col}\n",
60        "",
61        width = line_no_chars,
62        blue = "\x1b[1;36m",
63        bold = "\x1b[1m",
64        reset = "\x1b[0m",
65    ))?;
66    f.write_fmt(format_args!(
67        "{:>width$} {blue}|{reset}\n",
68        "",
69        width = line_no_chars,
70        blue = "\x1b[1;36m",
71        reset = "\x1b[0m",
72    ))?;
73    f.write_fmt(format_args!(
74        "{blue}{start_line} |{reset} {line}\n",
75        blue = "\x1b[1;36m",
76        reset = "\x1b[0m",
77    ))?;
78
79    let arrow_width = if start_line == end_line {
80        end_col - start_col
81    } else {
82        // TODO highlight the whole range
83        1
84    };
85
86    f.write_fmt(format_args!(
87        "{placeholder:>line_no_chars$} {blue}|{reset} {placeholder:>space_width$}{red}{arrow:^<arrow_width$} {hint}{reset}",
88        placeholder = "",
89        line_no_chars = line_no_chars,
90        space_width = start_col - 1,
91        arrow = "^",
92        arrow_width = arrow_width,
93        blue = "\x1b[1;36m",
94        red = "\x1b[1;31m",
95        reset = "\x1b[0m",
96    ))
97}
98
99fn location(source: &str, offset: usize) -> (usize, usize) {
100    let before_offset = &source[..offset];
101    let mut line = 1;
102    let mut col = 1;
103
104    for char in before_offset.chars() {
105        if char == '\n' {
106            line += 1;
107            col = 1;
108        } else if char == '\r' {
109            col = 1;
110        } else {
111            col += char.len_utf8();
112        }
113    }
114
115    (line, col)
116}
117
118#[cfg(test)]
119mod tests {
120    use super::*;
121
122    #[test]
123    fn test_source_location() {
124        let test_cases = [
125            ((1, 1), 0, "asdf"),
126            ((1, 4), 3, "asdf"),
127            ((2, 1), 5, "asdf\n"),
128            ((3, 4), 17, "asdf\njlk hsdf\nfoo"),
129            ((1, 5), 4, "1 + 2 * foo(3)"),
130        ];
131
132        for (expected, offset, source) in test_cases {
133            assert_eq!(location(source, offset), expected);
134        }
135    }
136}