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    /// Arithmetic with no value at all: J refuses a NaN its own arithmetic
34    /// made (`_ - _`, `2 | _`), and names the failure this way. A NaN the
35    /// program itself wrote (`_.`) travels through unrefused, so this is
36    /// about the operation, not the operand.
37    Nan,
38    /// Present in the language, not implemented yet.
39    NotYet,
40    /// Absent from the language itself; will never exist.
41    Language,
42    /// Present in the language and closed by libjay's sandbox: the host
43    /// policy, not a property of J or APL, and not a queue position.
44    Sandbox,
45    /// Larger than libjay will allocate.
46    Limit,
47    Internal,
48}
49
50impl ErrorKind {
51    pub fn label(self) -> &'static str {
52        match self {
53            ErrorKind::Parse => "parse error",
54            ErrorKind::Rank => "rank error",
55            ErrorKind::Length => "length error",
56            ErrorKind::Shape => "shape error",
57            ErrorKind::Domain => "domain error",
58            ErrorKind::Type => "type error",
59            ErrorKind::Value => "value error",
60            ErrorKind::Nan => "NaN error",
61            ErrorKind::NotYet => "not supported yet",
62            ErrorKind::Language => "not in the language",
63            ErrorKind::Sandbox => "closed by the sandbox",
64            ErrorKind::Limit => "limit error",
65            ErrorKind::Internal => "internal error",
66        }
67    }
68}
69
70#[derive(Clone, Debug)]
71pub struct Error {
72    pub kind: ErrorKind,
73    pub msg: String,
74    pub span: Option<Span>,
75    pub notes: Vec<String>,
76}
77
78pub type Result<T> = std::result::Result<T, Error>;
79
80impl Error {
81    pub fn new(kind: ErrorKind, msg: impl Into<String>, span: Option<Span>) -> Self {
82        Error { kind, msg: msg.into(), span, notes: Vec::new() }
83    }
84
85    pub fn parse(msg: impl Into<String>, span: Span) -> Self {
86        Self::new(ErrorKind::Parse, msg, Some(span))
87    }
88
89    pub fn not_yet(what: impl fmt::Display, span: Span) -> Self {
90        Self::new(ErrorKind::NotYet, format!("{what} is not supported yet"), Some(span))
91    }
92
93    pub fn language(msg: impl Into<String>, span: Span) -> Self {
94        Self::new(ErrorKind::Language, msg, Some(span))
95    }
96
97    /// A feature the language has and libjay's sandbox does not open. The
98    /// message says what the feature would reach; the kind's label says who
99    /// closed it.
100    pub fn sandbox(msg: impl Into<String>, span: Span) -> Self {
101        Self::new(ErrorKind::Sandbox, msg, Some(span))
102    }
103
104    pub fn domain(msg: impl Into<String>, span: Span) -> Self {
105        Self::new(ErrorKind::Domain, msg, Some(span))
106    }
107
108    /// Arithmetic whose answer is a NaN nobody asked for. The message names
109    /// the operation and the pair that produced it, in the source language's
110    /// own spelling of the infinities.
111    pub fn nan(msg: impl Into<String>, span: Span) -> Self {
112        Self::new(ErrorKind::Nan, msg, Some(span))
113    }
114
115    pub fn internal(msg: impl Into<String>) -> Self {
116        Self::new(ErrorKind::Internal, msg, None)
117    }
118
119    pub fn note(mut self, note: impl Into<String>) -> Self {
120        self.notes.push(note.into());
121        self
122    }
123
124    /// Render with a caret line pointing into `src` (the display source).
125    pub fn render(&self, src: &str) -> String {
126        let mut out = format!("{}: {}", self.kind.label(), self.msg);
127        if let Some(span) = self.span && let Some((line, col_start, col_len)) = locate(src, span) {
128            out.push_str("\n  ");
129            out.push_str(line);
130            out.push_str("\n  ");
131            out.push_str(&" ".repeat(col_start));
132            out.push_str(&"^".repeat(col_len.max(1)));
133        }
134        for n in &self.notes {
135            out.push_str("\nnote: ");
136            out.push_str(n);
137        }
138        out
139    }
140}
141
142impl fmt::Display for Error {
143    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
144        write!(f, "{}: {}", self.kind.label(), self.msg)?;
145        for n in &self.notes {
146            write!(f, "\nnote: {n}")?;
147        }
148        Ok(())
149    }
150}
151
152impl std::error::Error for Error {}
153
154/// Find the source line containing `span` and the span's position in it,
155/// measured in characters (for caret alignment).
156fn locate(src: &str, span: Span) -> Option<(&str, usize, usize)> {
157    // A span that does not land on this source has no caret to draw; the
158    // message still stands on its own.
159    if span.start > src.len() || !src.is_char_boundary(span.start) {
160        return None;
161    }
162    let line_start = src[..span.start].rfind('\n').map(|i| i + 1).unwrap_or(0);
163    let line_end = src[span.start..].find('\n').map(|i| span.start + i).unwrap_or(src.len());
164    let line = &src[line_start..line_end];
165    let col_start = src[line_start..span.start].chars().count();
166    let span_end = span.end.min(line_end).max(span.start);
167    let col_len = if src.is_char_boundary(span_end) {
168        src[span.start..span_end].chars().count()
169    } else {
170        1
171    };
172    Some((line, col_start, col_len))
173}