somni_expr/
error.rs

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