Skip to main content

rl_lang/utils/
errors.rs

1use std::sync::Arc;
2
3use ariadne::{Color, Label, Report, ReportKind, Source};
4
5use crate::utils::source::SourceFile;
6use crate::utils::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}
35
36/// provides an error category with optional error context
37#[derive(Debug, Clone)]
38pub struct ErrorReason {
39    /// error category
40    error_type: Reason,
41    /// optional lines of error output
42    data: Option<Vec<String>>,
43}
44
45/// the error category
46#[derive(Clone, Copy, Debug)]
47pub enum Reason {
48    /// error occured during parsing
49    Parse,
50    /// error occured when building the ast
51    AST,
52    /// error occured during lexing
53    Lexer,
54    /// error occured during evaluation
55    Interpreter,
56    /// error orginated from utils
57    Utils,
58    /// error occured during compilation
59    Compile,
60    /// error occured during runtime
61    Runtime,
62}
63
64impl Error {
65    /// builder-style constructor for span-aware errors.
66    /// the `span` becomes the primary anchor of the report.
67    pub fn at(kind: Reason, message: impl Into<String>, span: Span) -> Self {
68        let message = message.into();
69        #[cfg(feature = "debug")]
70        log::debug!("Error: {}", message);
71        Self {
72            message: message.clone(),
73            line: None,
74            reason: Some(ErrorReason::init(kind, None)),
75            detail: Some(Box::new(ErrorDetail {
76                primary: (span, message),
77                labels: Vec::new(),
78                source: None,
79                source_name: None,
80                help: None,
81            })),
82        }
83    }
84
85    /// override the primary label text (defaults to the error message).
86    pub fn with_primary_label(mut self, label: impl Into<String>) -> Self {
87        if let Some(d) = &mut self.detail {
88            d.primary.1 = label.into();
89        }
90        self
91    }
92
93    /// add a secondary label to the report.
94    pub fn with_label(mut self, span: Span, label: impl Into<String>) -> Self {
95        if let Some(d) = &mut self.detail {
96            d.labels.push((span, label.into()));
97        }
98        self
99    }
100
101    /// attach the source string so ariadne can render snippets.
102    pub fn with_source(mut self, source: Arc<String>) -> Self {
103        if let Some(d) = &mut self.detail {
104            d.source = Some(source);
105        }
106        self
107    }
108
109    /// attach a human-readable source name (e.g. file path).
110    pub fn with_source_name(mut self, name: impl Into<String>) -> Self {
111        if let Some(d) = &mut self.detail {
112            d.source_name = Some(name.into());
113        }
114        self
115    }
116
117    /// attach a help/hint line shown beneath the snippet (e.g. "did you mean foo?").
118    pub fn with_help(mut self, help: impl Into<String>) -> Self {
119        if let Some(d) = &mut self.detail {
120            d.help = Some(help.into());
121        }
122        self
123    }
124
125    /// attach both the source text and name from a [`SourceFile`].
126    pub fn with_source_file(mut self, file: &SourceFile) -> Self {
127        if let Some(d) = &mut self.detail {
128            d.source = Some(Arc::clone(&file.text));
129            d.source_name = Some(file.name.to_string());
130        }
131        self
132    }
133
134    /// prints the error and exits via panic so existing call sites and the REPL keep working.
135    ///
136    /// uses ariadne when `source` and a primary span are available; falls back to the legacy
137    /// text format otherwise.
138    pub fn print_error(&self) {
139        self.report_to_stderr();
140        panic!("rl error");
141    }
142
143    /// renders the error to stderr without terminating. used by call sites that already
144    /// own their control flow (e.g. anything returning `Result`).
145    pub fn report_to_stderr(&self) {
146        if let Some(d) = &self.detail
147            && let Some(src) = &d.source
148        {
149            let name: &str = d.source_name.as_deref().unwrap_or("<source>");
150            let (sp, primary_label) = &d.primary;
151            let mut builder = Report::build(ReportKind::Error, (name, sp.start..sp.end))
152                .with_message(&self.message)
153                .with_label(
154                    Label::new((name, sp.start..sp.end))
155                        .with_message(primary_label)
156                        .with_color(Color::Red),
157                );
158            for (lsp, label) in &d.labels {
159                builder = builder.with_label(
160                    Label::new((name, lsp.start..lsp.end))
161                        .with_message(label)
162                        .with_color(Color::Yellow),
163                );
164            }
165            if let Some(help) = &d.help {
166                builder = builder.with_help(help);
167            }
168            let _ = builder.finish().eprint((name, Source::from(src.as_str())));
169            return;
170        }
171
172        self.fallback_text();
173    }
174
175    /// legacy text rendering kept verbatim from the original implementation.
176    fn fallback_text(&self) {
177        match &self.line {
178            Some(l) => println!("[{}) Error: {}]", l, self.message),
179            None => println!("[Error: {}]", self.message),
180        }
181
182        if let Some(r) = &self.reason {
183            match &r.data {
184                Some(d) => {
185                    println!("[{}]", r.get_type_string());
186                    for l in d {
187                        println!("{}", l);
188                    }
189                }
190                _ => println!("[{}]", r.get_type_string()),
191            }
192        }
193    }
194
195    /// Extracts the primary [`Span`] of this error, if one was set.
196    pub fn span(&self) -> Option<crate::utils::span::Span> {
197        self.detail.as_ref().map(|d| d.primary.0)
198    }
199}
200
201impl ErrorReason {
202    /// creates a new [`ErrorReason`] with category type and optional data
203    ///
204    /// # Example
205    ///
206    /// ```rust
207    /// use rl_lang::utils::errors::{ErrorReason, Reason};
208    /// ErrorReason::init(Reason::Lexer, Some(vec!["unknown token `$`".to_string()]));
209    /// ```
210    pub fn init(error_type: Reason, data: Option<Vec<String>>) -> Self {
211        Self { error_type, data }
212    }
213
214    /// returns the display of category type
215    fn get_type_string(&self) -> String {
216        match &self.error_type {
217            Reason::Parse => "Parse Error",
218            Reason::AST => "AST Error",
219            Reason::Lexer => "Lexer Error",
220            Reason::Interpreter => "Interpreter Error",
221            Reason::Utils => "Utils Error",
222            Reason::Compile => "Compile Error",
223            Reason::Runtime => "Runtime Error",
224        }
225        .to_string()
226    }
227}
228
229impl Error {
230    /// Returns the raw error message string.
231    pub fn message(&self) -> &str {
232        &self.message
233    }
234}