runmat_async/
runtime_error.rs1use 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}