rl_utils/errors.rs
1use std::sync::Arc;
2
3use ariadne::{Color, Label, Report, ReportKind, Source};
4
5use crate::source::SourceFile;
6use crate::span::Span;
7
8/// heavy optional fields, heap-allocated so `Error` stays small on the stack
9#[derive(Debug, Clone)]
10struct ErrorDetail {
11 /// primary span (anchor of the ariadne report) and its label text
12 primary: (Span, String),
13 /// secondary spans with labels
14 labels: Vec<(Span, String)>,
15 /// source string for rendering; supplied by the subsystem that built the error
16 source: Option<Arc<String>>,
17 /// source file name shown in the report header
18 source_name: Option<String>,
19 /// optional help/hint line shown after the snippet (e.g. "did you mean foo?")
20 help: Option<String>,
21}
22
23/// represents an Interpreter error with optional line number and error category
24#[derive(Debug, Clone)]
25pub struct Error {
26 /// readable message
27 message: String,
28 /// line number of error in source file (legacy; superseded by `detail.primary.0` when present)
29 line: Option<usize>,
30 /// the category and optional context of the error
31 reason: Option<ErrorReason>,
32 /// boxed span-aware detail; `None` for legacy errors that have no span
33 detail: Option<Box<ErrorDetail>>,
34 /// file name + 1-indexed (line, col), set from a [`crate::line_index::LineIndex`]
35 /// when no source text is available to render an ariadne snippet (e.g.
36 /// errors raised while running compiled `.rlc` bytecode, which embeds a
37 /// `LineIndex` but not the original source). Used by [`Error::fallback_text`]
38 /// to print a `file:line:col` diagnostic instead of a bare message.
39 location: Option<(Arc<str>, usize, usize)>,
40}
41
42/// provides an error category with optional error context
43#[derive(Debug, Clone)]
44pub struct ErrorReason {
45 /// error category
46 error_type: Reason,
47 /// optional lines of error output
48 data: Option<Vec<String>>,
49}
50
51/// the error category
52#[derive(Clone, Copy, Debug)]
53pub enum Reason {
54 /// error occured during parsing
55 Parse,
56 /// error occured when building the ast
57 AST,
58 /// error occured during lexing
59 Lexer,
60 /// error occured during evaluation
61 Interpreter,
62 /// error orginated from utils
63 Utils,
64 /// error occured during compilation
65 Compile,
66 /// error occured during runtime
67 Runtime,
68}
69
70impl Error {
71 /// builder-style constructor for span-aware errors.
72 /// the `span` becomes the primary anchor of the report.
73 pub fn at(kind: Reason, message: impl Into<String>, span: Span) -> Self {
74 let message = message.into();
75 #[cfg(feature = "debug")]
76 log::debug!("Error: {}", message);
77 Self {
78 message: message.clone(),
79 line: None,
80 reason: Some(ErrorReason::init(kind, None)),
81 detail: Some(Box::new(ErrorDetail {
82 primary: (span, message),
83 labels: Vec::new(),
84 source: None,
85 source_name: None,
86 help: None,
87 })),
88 location: None,
89 }
90 }
91
92 /// Attaches a `file:line:col` fallback location, resolved from a
93 /// [`crate::line_index::LineIndex`] against this error's primary span.
94 /// Used when no source text is available to render a full ariadne
95 /// snippet (see [`Error::fallback_text`]); a no-op when full source
96 /// is later attached via [`Error::with_source`] /
97 /// [`Error::with_source_file`], since [`Error::report_to_stderr`]
98 /// prefers the ariadne path whenever source is present.
99 pub fn with_location_from(mut self, index: &crate::line_index::LineIndex) -> Self {
100 if let Some(span) = self.span() {
101 let (line, col) = index.line_col(span.start);
102 self.location = Some((Arc::clone(index.source_name()), line, col));
103 }
104 self
105 }
106
107 /// override the primary label text (defaults to the error message).
108 pub fn with_primary_label(mut self, label: impl Into<String>) -> Self {
109 if let Some(d) = &mut self.detail {
110 d.primary.1 = label.into();
111 }
112 self
113 }
114
115 /// (Re-)anchors the primary span of this report at `span`. Unlike
116 /// [`Error::at`], this works on an error that was built without span
117 /// context (e.g. deep inside generic conversion code with no access to
118 /// the call site) - the caller sets the real location once it's known.
119 pub fn with_span(mut self, span: Span) -> Self {
120 match &mut self.detail {
121 Some(d) => d.primary.0 = span,
122 None => {
123 self.detail = Some(Box::new(ErrorDetail {
124 primary: (span, self.message.clone()),
125 labels: Vec::new(),
126 source: None,
127 source_name: None,
128 help: None,
129 }));
130 }
131 }
132 self
133 }
134
135 /// add a secondary label to the report.
136 pub fn with_label(mut self, span: Span, label: impl Into<String>) -> Self {
137 if let Some(d) = &mut self.detail {
138 d.labels.push((span, label.into()));
139 }
140 self
141 }
142
143 /// attach the source string so ariadne can render snippets.
144 pub fn with_source(mut self, source: Arc<String>) -> Self {
145 if let Some(d) = &mut self.detail {
146 d.source = Some(source);
147 }
148 self
149 }
150
151 /// attach a human-readable source name (e.g. file path).
152 pub fn with_source_name(mut self, name: impl Into<String>) -> Self {
153 if let Some(d) = &mut self.detail {
154 d.source_name = Some(name.into());
155 }
156 self
157 }
158
159 /// attach a help/hint line shown beneath the snippet (e.g. "did you mean foo?").
160 pub fn with_help(mut self, help: impl Into<String>) -> Self {
161 if let Some(d) = &mut self.detail {
162 d.help = Some(help.into());
163 }
164 self
165 }
166
167 /// attach both the source text and name from a [`SourceFile`].
168 pub fn with_source_file(mut self, file: &SourceFile) -> Self {
169 if let Some(d) = &mut self.detail {
170 d.source = Some(Arc::clone(&file.text));
171 d.source_name = Some(file.name.to_string());
172 }
173 self
174 }
175
176 /// prints the error and exits via panic so existing call sites and the REPL keep working.
177 ///
178 /// uses ariadne when `source` and a primary span are available; falls back to the legacy
179 /// text format otherwise.
180 pub fn print_error(&self) {
181 self.report_to_stderr();
182 panic!("rl error");
183 }
184
185 /// renders the error to stderr without terminating. used by call sites that already
186 /// own their control flow (e.g. anything returning `Result`).
187 pub fn report_to_stderr(&self) {
188 if let Some(d) = &self.detail
189 && let Some(src) = &d.source
190 {
191 let name: &str = d.source_name.as_deref().unwrap_or("<source>");
192 let (sp, primary_label) = &d.primary;
193 let mut builder = Report::build(ReportKind::Error, (name, sp.start..sp.end))
194 .with_message(&self.message)
195 .with_label(
196 Label::new((name, sp.start..sp.end))
197 .with_message(primary_label)
198 .with_color(Color::Red),
199 );
200 for (lsp, label) in &d.labels {
201 builder = builder.with_label(
202 Label::new((name, lsp.start..lsp.end))
203 .with_message(label)
204 .with_color(Color::Yellow),
205 );
206 }
207 if let Some(help) = &d.help {
208 builder = builder.with_help(help);
209 }
210 let _ = builder.finish().eprint((name, Source::from(src.as_str())));
211 return;
212 }
213
214 self.fallback_text();
215 }
216
217 /// text rendering used when no source is available to render an
218 /// ariadne snippet. Prefers a precise `file:line:col` location
219 /// (set via [`Error::with_location_from`]) over the legacy bare
220 /// `[N) Error: ...]` / `[Error: ...]` format, which is now only a
221 /// fallback for errors that have neither source nor a line index.
222 fn fallback_text(&self) {
223 match (&self.location, &self.line) {
224 (Some((name, line, col)), _) => {
225 println!("{}:{}:{}: [Error: {}]", name, line, col, self.message)
226 }
227 (None, Some(l)) => println!("[{}) Error: {}]", l, self.message),
228 (None, None) => println!("[Error: {}]", self.message),
229 }
230
231 if let Some(r) = &self.reason {
232 match &r.data {
233 Some(d) => {
234 println!("[{}]", r.get_type_string());
235 for l in d {
236 println!("{}", l);
237 }
238 }
239 _ => println!("[{}]", r.get_type_string()),
240 }
241 }
242 }
243
244 /// Extracts the primary [`Span`] of this error, if one was set.
245 pub fn span(&self) -> Option<crate::span::Span> {
246 self.detail.as_ref().map(|d| d.primary.0)
247 }
248}
249
250impl ErrorReason {
251 /// creates a new [`ErrorReason`] with category type and optional data
252 ///
253 /// # Example
254 ///
255 /// ```rust
256 /// use rl_utils::errors::{ErrorReason, Reason};
257 /// ErrorReason::init(Reason::Lexer, Some(vec!["unknown token `$`".to_string()]));
258 /// ```
259 pub fn init(error_type: Reason, data: Option<Vec<String>>) -> Self {
260 Self { error_type, data }
261 }
262
263 /// returns the display of category type
264 fn get_type_string(&self) -> String {
265 match &self.error_type {
266 Reason::Parse => "Parse Error",
267 Reason::AST => "AST Error",
268 Reason::Lexer => "Lexer Error",
269 Reason::Interpreter => "Interpreter Error",
270 Reason::Utils => "Utils Error",
271 Reason::Compile => "Compile Error",
272 Reason::Runtime => "Runtime Error",
273 }
274 .to_string()
275 }
276}
277
278impl Error {
279 /// Returns the raw error message string.
280 pub fn message(&self) -> &str {
281 &self.message
282 }
283}