Skip to main content

nomiscript/
error.rs

1use std::fmt;
2use std::io::IsTerminal;
3use std::ops::Range;
4
5use thiserror::Error;
6
7#[derive(Debug, Clone)]
8pub struct ParseErrorDetails {
9    pub message: String,
10    pub input: String,
11    pub span: Range<usize>,
12}
13
14impl ParseErrorDetails {
15    pub fn new(message: String, input: String, span: Range<usize>) -> Self {
16        Self {
17            message,
18            input,
19            span,
20        }
21    }
22
23    fn find_line_info(&self) -> (usize, usize, &str) {
24        let offset = self.span.start.min(self.input.len());
25        let before = &self.input[..offset];
26        let line_num = before.matches('\n').count() + 1;
27        let line_start = before.rfind('\n').map_or(0, |i| i + 1);
28        let line_end = self.input[offset..]
29            .find('\n')
30            .map_or(self.input.len(), |i| offset + i);
31        let line = &self.input[line_start..line_end];
32        let col = offset - line_start;
33        (line_num, col, line)
34    }
35
36    pub fn render(&self, use_color: bool) -> String {
37        let (line_num, col, line) = self.find_line_info();
38        let line_num_width = line_num.to_string().len().max(1);
39
40        let (red, cyan, reset) = if use_color {
41            ("\x1b[31m", "\x1b[36m", "\x1b[0m")
42        } else {
43            ("", "", "")
44        };
45
46        let mut output = String::new();
47        output.push_str(&format!("{red}error:{reset} {}\n", self.message));
48        output.push_str(&format!(
49            "{cyan}{:>width$} |{reset}\n",
50            "",
51            width = line_num_width
52        ));
53        output.push_str(&format!(
54            "{cyan}{line_num:>line_num_width$} |{reset} {line}\n"
55        ));
56        output.push_str(&format!(
57            "{cyan}{:>width$} |{reset} {}{red}^{reset}\n",
58            "",
59            " ".repeat(col),
60            width = line_num_width
61        ));
62        output
63    }
64
65    pub fn render_auto(&self) -> String {
66        self.render(std::io::stderr().is_terminal())
67    }
68}
69
70impl fmt::Display for ParseErrorDetails {
71    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
72        write!(f, "{}", self.render(false))
73    }
74}
75
76#[derive(Error, Debug)]
77pub enum Error {
78    #[error("{0}")]
79    Parse(ParseErrorDetails),
80
81    #[error("Compile error: {0}")]
82    Compile(String),
83
84    #[error("Runtime error: {0}")]
85    Runtime(String),
86
87    #[error("Undefined symbol: {0}")]
88    UndefinedSymbol(String),
89
90    #[error("Type error: expected {expected}, got {actual}")]
91    Type { expected: String, actual: String },
92
93    #[error("Arity error: {name} expects {expected} arguments, got {actual}")]
94    Arity {
95        name: String,
96        expected: usize,
97        actual: usize,
98    },
99}
100
101impl Error {
102    #[must_use]
103    pub fn parse(message: String, input: &str, span: Range<usize>) -> Self {
104        Self::Parse(ParseErrorDetails::new(message, input.to_string(), span))
105    }
106
107    #[must_use]
108    pub fn render(&self, use_color: bool) -> String {
109        match self {
110            Error::Parse(details) => details.render(use_color),
111            _ => self.to_string(),
112        }
113    }
114
115    #[must_use]
116    pub fn render_auto(&self) -> String {
117        match self {
118            Error::Parse(details) => details.render_auto(),
119            _ => self.to_string(),
120        }
121    }
122}
123
124pub type Result<T> = std::result::Result<T, Error>;