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