Skip to main content

runmat_async/
runtime_error.rs

1use std::error::Error as StdError;
2
3use miette::SourceSpan;
4use thiserror::Error;
5
6#[derive(Debug, Clone, Default, PartialEq, Eq)]
7pub struct ErrorContext {
8    pub builtin: Option<String>,
9    pub task_id: Option<String>,
10    pub call_frames: Vec<CallFrame>,
11    pub call_frames_elided: usize,
12    pub call_stack: Vec<String>,
13    pub phase: Option<String>,
14}
15
16#[derive(Debug, Clone, PartialEq, Eq)]
17pub struct CallFrame {
18    pub function: String,
19    pub source_id: Option<usize>,
20    pub span: Option<(usize, usize)>,
21}
22
23impl ErrorContext {
24    pub fn with_builtin(mut self, builtin: impl Into<String>) -> Self {
25        self.builtin = Some(builtin.into());
26        self
27    }
28
29    pub fn with_task_id(mut self, task_id: impl Into<String>) -> Self {
30        self.task_id = Some(task_id.into());
31        self
32    }
33
34    pub fn with_call_stack(mut self, call_stack: Vec<String>) -> Self {
35        self.call_stack = call_stack;
36        self
37    }
38
39    pub fn with_call_frames(mut self, call_frames: Vec<CallFrame>) -> Self {
40        self.call_frames = call_frames;
41        self
42    }
43
44    pub fn with_call_frames_elided(mut self, count: usize) -> Self {
45        self.call_frames_elided = count;
46        self
47    }
48
49    pub fn with_phase(mut self, phase: impl Into<String>) -> Self {
50        self.phase = Some(phase.into());
51        self
52    }
53}
54
55#[derive(Debug, Error)]
56#[error("{message}")]
57pub struct RuntimeError {
58    pub message: String,
59    pub span: Option<SourceSpan>,
60    #[source]
61    pub source: Option<Box<dyn StdError + Send + Sync>>,
62    pub identifier: Option<String>,
63    pub context: ErrorContext,
64}
65
66impl RuntimeError {
67    pub fn new(message: impl Into<String>) -> Self {
68        Self {
69            message: message.into(),
70            span: None,
71            source: None,
72            identifier: None,
73            context: ErrorContext::default(),
74        }
75    }
76
77    pub fn identifier(&self) -> Option<&str> {
78        self.identifier.as_deref()
79    }
80
81    pub fn message(&self) -> &str {
82        &self.message
83    }
84
85    pub fn contains(&self, needle: &str) -> bool {
86        self.message.contains(needle)
87    }
88
89    pub fn starts_with(&self, prefix: &str) -> bool {
90        self.message.starts_with(prefix)
91    }
92
93    pub fn format_diagnostic(&self) -> String {
94        self.format_diagnostic_with_source(None, None)
95    }
96
97    pub fn format_diagnostic_with_source(
98        &self,
99        source_name: Option<&str>,
100        source: Option<&str>,
101    ) -> String {
102        let mut lines = Vec::new();
103        lines.push(format!("error: {}", self.message));
104        let identifier = self
105            .identifier
106            .as_deref()
107            .or_else(|| infer_identifier(&self.message));
108        if let Some(identifier) = identifier {
109            lines.push(format!("id: {identifier}"));
110        }
111        if let Some(((source_name, source), span)) = source_name.zip(source).zip(self.span.as_ref())
112        {
113            let (line, col, line_text, caret) = render_span(source, span);
114            lines.push(format!("--> {source_name}:{line}:{col}"));
115            lines.push(format!("{line} | {line_text}"));
116            lines.push(format!("  | {caret}"));
117        }
118        if let Some(builtin) = self.context.builtin.as_deref() {
119            lines.push(format!("builtin: {builtin}"));
120        }
121        if let Some(task_id) = self.context.task_id.as_deref() {
122            lines.push(format!("task: {task_id}"));
123        }
124        if let Some(phase) = self.context.phase.as_deref() {
125            lines.push(format!("phase: {phase}"));
126        }
127        if !self.context.call_stack.is_empty() {
128            lines.push("callstack:".to_string());
129            for frame in &self.context.call_stack {
130                lines.push(format!("  {frame}"));
131            }
132        } else if !self.context.call_frames.is_empty() {
133            lines.push("callstack:".to_string());
134            if self.context.call_frames_elided > 0 {
135                lines.push(format!(
136                    "  ... {} frames elided ...",
137                    self.context.call_frames_elided
138                ));
139            }
140            for frame in &self.context.call_frames {
141                lines.push(format!("  {}", frame.function));
142            }
143        }
144        lines.join("\n")
145    }
146}
147
148impl miette::Diagnostic for RuntimeError {
149    fn code<'a>(&'a self) -> Option<Box<dyn std::fmt::Display + 'a>> {
150        Some(Box::new("runmat::runtime::error"))
151    }
152
153    fn labels(&self) -> Option<Box<dyn Iterator<Item = miette::LabeledSpan> + '_>> {
154        self.span.map(|span| {
155            Box::new(std::iter::once(miette::LabeledSpan::underline(span)))
156                as Box<dyn Iterator<Item = miette::LabeledSpan>>
157        })
158    }
159}
160
161impl From<String> for RuntimeError {
162    fn from(value: String) -> Self {
163        RuntimeError::new(value)
164    }
165}
166
167impl From<&str> for RuntimeError {
168    fn from(value: &str) -> Self {
169        RuntimeError::new(value)
170    }
171}
172
173pub struct RuntimeErrorBuilder {
174    error: RuntimeError,
175}
176
177impl RuntimeErrorBuilder {
178    pub fn with_identifier(mut self, identifier: impl Into<String>) -> Self {
179        self.error.identifier = Some(identifier.into());
180        self
181    }
182
183    pub fn with_builtin(mut self, builtin: impl Into<String>) -> Self {
184        self.error.context = self.error.context.with_builtin(builtin);
185        self
186    }
187
188    pub fn with_task_id(mut self, task_id: impl Into<String>) -> Self {
189        self.error.context = self.error.context.with_task_id(task_id);
190        self
191    }
192
193    pub fn with_call_stack(mut self, call_stack: Vec<String>) -> Self {
194        self.error.context = self.error.context.with_call_stack(call_stack);
195        self
196    }
197
198    pub fn with_call_frames(mut self, call_frames: Vec<CallFrame>) -> Self {
199        self.error.context = self.error.context.with_call_frames(call_frames);
200        self
201    }
202
203    pub fn with_call_frames_elided(mut self, count: usize) -> Self {
204        self.error.context = self.error.context.with_call_frames_elided(count);
205        self
206    }
207
208    pub fn with_phase(mut self, phase: impl Into<String>) -> Self {
209        self.error.context = self.error.context.with_phase(phase);
210        self
211    }
212
213    pub fn with_span(mut self, span: SourceSpan) -> Self {
214        self.error.span = Some(span);
215        self
216    }
217
218    pub fn with_source(mut self, source: impl StdError + Send + Sync + 'static) -> Self {
219        self.error.source = Some(Box::new(source));
220        self
221    }
222
223    pub fn build(self) -> RuntimeError {
224        self.error
225    }
226}
227
228pub fn runtime_error(message: impl Into<String>) -> RuntimeErrorBuilder {
229    RuntimeErrorBuilder {
230        error: RuntimeError::new(message),
231    }
232}
233
234fn infer_identifier(message: &str) -> Option<&'static str> {
235    if message.starts_with("Undefined function:") {
236        Some("RunMat:UndefinedFunction")
237    } else {
238        None
239    }
240}
241
242fn render_span(source: &str, span: &SourceSpan) -> (usize, usize, String, String) {
243    let offset = span.offset();
244    let len = span.len();
245    let mut line = 1;
246    let mut line_start = 0;
247    for (idx, ch) in source.char_indices() {
248        if idx >= offset {
249            break;
250        }
251        if ch == '\n' {
252            line += 1;
253            line_start = idx + 1;
254        }
255    }
256    let line_end = source[line_start..]
257        .find('\n')
258        .map(|rel| line_start + rel)
259        .unwrap_or(source.len());
260    let line_text = source[line_start..line_end].to_string();
261    let col = offset.saturating_sub(line_start) + 1;
262    let available = line_end.saturating_sub(offset).max(1);
263    let caret_len = len.max(1).min(available);
264    let caret = format!(
265        "{}{}",
266        " ".repeat(col.saturating_sub(1)),
267        "^".repeat(caret_len)
268    );
269    (line, col, line_text, caret)
270}