Skip to main content

jay/
error.rs

1//! Error type carrying a position in the user's source expression.
2
3use std::fmt;
4
5/// Byte range into the display source of a compiled program.
6#[derive(Clone, Copy, Debug, PartialEq, Eq)]
7pub struct Span {
8    pub start: usize,
9    pub end: usize,
10}
11
12impl Span {
13    pub fn new(start: usize, end: usize) -> Self {
14        Span { start, end }
15    }
16
17    pub fn merge(a: Span, b: Span) -> Span {
18        Span { start: a.start.min(b.start), end: a.end.max(b.end) }
19    }
20}
21
22/// Broad class of a failure. `NotYet` and `Language` are deliberately
23/// distinct: the former is a promise, the latter is a property of J/APL.
24#[derive(Clone, Copy, Debug, PartialEq, Eq)]
25pub enum ErrorKind {
26    Parse,
27    Rank,
28    Length,
29    Shape,
30    Domain,
31    Type,
32    Value,
33    /// Present in the language, not implemented yet.
34    NotYet,
35    /// Absent from the language itself; will never exist.
36    Language,
37    /// Larger than libjay will allocate.
38    Limit,
39    Internal,
40}
41
42impl ErrorKind {
43    pub fn label(self) -> &'static str {
44        match self {
45            ErrorKind::Parse => "parse error",
46            ErrorKind::Rank => "rank error",
47            ErrorKind::Length => "length error",
48            ErrorKind::Shape => "shape error",
49            ErrorKind::Domain => "domain error",
50            ErrorKind::Type => "type error",
51            ErrorKind::Value => "value error",
52            ErrorKind::NotYet => "not supported yet",
53            ErrorKind::Language => "not in the language",
54            ErrorKind::Limit => "limit error",
55            ErrorKind::Internal => "internal error",
56        }
57    }
58}
59
60#[derive(Clone, Debug)]
61pub struct Error {
62    pub kind: ErrorKind,
63    pub msg: String,
64    pub span: Option<Span>,
65    pub notes: Vec<String>,
66}
67
68pub type Result<T> = std::result::Result<T, Error>;
69
70impl Error {
71    pub fn new(kind: ErrorKind, msg: impl Into<String>, span: Option<Span>) -> Self {
72        Error { kind, msg: msg.into(), span, notes: Vec::new() }
73    }
74
75    pub fn parse(msg: impl Into<String>, span: Span) -> Self {
76        Self::new(ErrorKind::Parse, msg, Some(span))
77    }
78
79    pub fn not_yet(what: impl fmt::Display, span: Span) -> Self {
80        Self::new(ErrorKind::NotYet, format!("{what} is not supported yet"), Some(span))
81    }
82
83    pub fn language(msg: impl Into<String>, span: Span) -> Self {
84        Self::new(ErrorKind::Language, msg, Some(span))
85    }
86
87    pub fn domain(msg: impl Into<String>, span: Span) -> Self {
88        Self::new(ErrorKind::Domain, msg, Some(span))
89    }
90
91    pub fn internal(msg: impl Into<String>) -> Self {
92        Self::new(ErrorKind::Internal, msg, None)
93    }
94
95    pub fn note(mut self, note: impl Into<String>) -> Self {
96        self.notes.push(note.into());
97        self
98    }
99
100    /// Render with a caret line pointing into `src` (the display source).
101    pub fn render(&self, src: &str) -> String {
102        let mut out = format!("{}: {}", self.kind.label(), self.msg);
103        if let Some(span) = self.span {
104            if let Some((line, col_start, col_len)) = locate(src, span) {
105                out.push_str("\n  ");
106                out.push_str(line);
107                out.push_str("\n  ");
108                out.push_str(&" ".repeat(col_start));
109                out.push_str(&"^".repeat(col_len.max(1)));
110            }
111        }
112        for n in &self.notes {
113            out.push_str("\nnote: ");
114            out.push_str(n);
115        }
116        out
117    }
118}
119
120impl fmt::Display for Error {
121    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
122        write!(f, "{}: {}", self.kind.label(), self.msg)?;
123        for n in &self.notes {
124            write!(f, "\nnote: {n}")?;
125        }
126        Ok(())
127    }
128}
129
130impl std::error::Error for Error {}
131
132/// Find the source line containing `span` and the span's position in it,
133/// measured in characters (for caret alignment).
134fn locate(src: &str, span: Span) -> Option<(&str, usize, usize)> {
135    // A span that does not land on this source has no caret to draw; the
136    // message still stands on its own.
137    if span.start > src.len() || !src.is_char_boundary(span.start) {
138        return None;
139    }
140    let line_start = src[..span.start].rfind('\n').map(|i| i + 1).unwrap_or(0);
141    let line_end = src[span.start..].find('\n').map(|i| span.start + i).unwrap_or(src.len());
142    let line = &src[line_start..line_end];
143    let col_start = src[line_start..span.start].chars().count();
144    let span_end = span.end.min(line_end).max(span.start);
145    let col_len = if src.is_char_boundary(span_end) {
146        src[span.start..span_end].chars().count()
147    } else {
148        1
149    };
150    Some((line, col_start, col_len))
151}