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 pub gpu_gather_retry: GpuGatherRetry,
65}
66
67#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
69pub enum GpuGatherRetry {
70 #[default]
72 Legacy,
73 Never,
75 Requested,
77}
78
79impl RuntimeError {
80 pub fn new(message: impl Into<String>) -> Self {
81 Self {
82 message: message.into(),
83 span: None,
84 source: None,
85 identifier: None,
86 context: ErrorContext::default(),
87 gpu_gather_retry: GpuGatherRetry::Legacy,
88 }
89 }
90
91 pub fn identifier(&self) -> Option<&str> {
92 self.identifier.as_deref()
93 }
94
95 pub fn message(&self) -> &str {
96 &self.message
97 }
98
99 pub fn gpu_gather_retry(&self) -> GpuGatherRetry {
100 self.gpu_gather_retry
101 }
102
103 pub fn contains(&self, needle: &str) -> bool {
104 self.message.contains(needle)
105 }
106
107 pub fn starts_with(&self, prefix: &str) -> bool {
108 self.message.starts_with(prefix)
109 }
110
111 pub fn format_diagnostic(&self) -> String {
112 self.format_diagnostic_with_source(None, None)
113 }
114
115 pub fn format_diagnostic_with_source(
116 &self,
117 source_name: Option<&str>,
118 source: Option<&str>,
119 ) -> String {
120 let mut lines = Vec::new();
121 lines.push(format!("error: {}", self.message));
122 let identifier = self
123 .identifier
124 .as_deref()
125 .or_else(|| infer_identifier(&self.message));
126 if let Some(identifier) = identifier {
127 lines.push(format!("id: {identifier}"));
128 }
129 if let Some(((source_name, source), span)) = source_name.zip(source).zip(self.span.as_ref())
130 {
131 let (line, col, line_text, caret) = render_span(source, span);
132 lines.push(format!("--> {source_name}:{line}:{col}"));
133 lines.push(format!("{line} | {line_text}"));
134 lines.push(format!(" | {caret}"));
135 }
136 if let Some(builtin) = self.context.builtin.as_deref() {
137 lines.push(format!("builtin: {builtin}"));
138 }
139 if let Some(task_id) = self.context.task_id.as_deref() {
140 lines.push(format!("task: {task_id}"));
141 }
142 if let Some(phase) = self.context.phase.as_deref() {
143 lines.push(format!("phase: {phase}"));
144 }
145 if !self.context.call_stack.is_empty() {
146 lines.push("callstack:".to_string());
147 for frame in &self.context.call_stack {
148 lines.push(format!(" {frame}"));
149 }
150 } else if !self.context.call_frames.is_empty() {
151 lines.push("callstack:".to_string());
152 if self.context.call_frames_elided > 0 {
153 lines.push(format!(
154 " ... {} frames elided ...",
155 self.context.call_frames_elided
156 ));
157 }
158 for frame in &self.context.call_frames {
159 lines.push(format!(" {}", frame.function));
160 }
161 }
162 lines.join("\n")
163 }
164}
165
166impl miette::Diagnostic for RuntimeError {
167 fn code<'a>(&'a self) -> Option<Box<dyn std::fmt::Display + 'a>> {
168 Some(Box::new("runmat::runtime::error"))
169 }
170
171 fn labels(&self) -> Option<Box<dyn Iterator<Item = miette::LabeledSpan> + '_>> {
172 self.span.map(|span| {
173 Box::new(std::iter::once(miette::LabeledSpan::underline(span)))
174 as Box<dyn Iterator<Item = miette::LabeledSpan>>
175 })
176 }
177}
178
179impl From<String> for RuntimeError {
180 fn from(value: String) -> Self {
181 RuntimeError::new(value)
182 }
183}
184
185impl From<&str> for RuntimeError {
186 fn from(value: &str) -> Self {
187 RuntimeError::new(value)
188 }
189}
190
191pub struct RuntimeErrorBuilder {
192 error: RuntimeError,
193}
194
195impl RuntimeErrorBuilder {
196 pub fn with_identifier(mut self, identifier: impl Into<String>) -> Self {
197 self.error.identifier = Some(identifier.into());
198 self
199 }
200
201 pub fn with_gpu_gather_retry(mut self, policy: GpuGatherRetry) -> Self {
202 self.error.gpu_gather_retry = policy;
203 self
204 }
205
206 pub fn with_builtin(mut self, builtin: impl Into<String>) -> Self {
207 self.error.context = self.error.context.with_builtin(builtin);
208 self
209 }
210
211 pub fn with_task_id(mut self, task_id: impl Into<String>) -> Self {
212 self.error.context = self.error.context.with_task_id(task_id);
213 self
214 }
215
216 pub fn with_call_stack(mut self, call_stack: Vec<String>) -> Self {
217 self.error.context = self.error.context.with_call_stack(call_stack);
218 self
219 }
220
221 pub fn with_call_frames(mut self, call_frames: Vec<CallFrame>) -> Self {
222 self.error.context = self.error.context.with_call_frames(call_frames);
223 self
224 }
225
226 pub fn with_call_frames_elided(mut self, count: usize) -> Self {
227 self.error.context = self.error.context.with_call_frames_elided(count);
228 self
229 }
230
231 pub fn with_phase(mut self, phase: impl Into<String>) -> Self {
232 self.error.context = self.error.context.with_phase(phase);
233 self
234 }
235
236 pub fn with_span(mut self, span: SourceSpan) -> Self {
237 self.error.span = Some(span);
238 self
239 }
240
241 pub fn with_source(mut self, source: impl StdError + Send + Sync + 'static) -> Self {
242 self.error.source = Some(Box::new(source));
243 self
244 }
245
246 pub fn build(self) -> RuntimeError {
247 self.error
248 }
249}
250
251pub fn runtime_error(message: impl Into<String>) -> RuntimeErrorBuilder {
252 RuntimeErrorBuilder {
253 error: RuntimeError::new(message),
254 }
255}
256
257fn infer_identifier(message: &str) -> Option<&'static str> {
258 if message.starts_with("Undefined function:") {
259 Some("RunMat:UndefinedFunction")
260 } else {
261 None
262 }
263}
264
265fn render_span(source: &str, span: &SourceSpan) -> (usize, usize, String, String) {
266 let offset = span.offset();
267 let len = span.len();
268 let mut line = 1;
269 let mut line_start = 0;
270 for (idx, ch) in source.char_indices() {
271 if idx >= offset {
272 break;
273 }
274 if ch == '\n' {
275 line += 1;
276 line_start = idx + 1;
277 }
278 }
279 let line_end = source[line_start..]
280 .find('\n')
281 .map(|rel| line_start + rel)
282 .unwrap_or(source.len());
283 let line_text = source[line_start..line_end].to_string();
284 let col = offset.saturating_sub(line_start) + 1;
285 let available = line_end.saturating_sub(offset).max(1);
286 let caret_len = len.max(1).min(available);
287 let caret = format!(
288 "{}{}",
289 " ".repeat(col.saturating_sub(1)),
290 "^".repeat(caret_len)
291 );
292 (line, col, line_text, caret)
293}