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    pub gpu_gather_retry: GpuGatherRetry,
65}
66
67/// Whether dispatcher-level legacy GPU gather-and-retry may transform this error.
68#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
69pub enum GpuGatherRetry {
70    /// Preserve the historical message-based fallback while callers migrate to an explicit policy.
71    #[default]
72    Legacy,
73    /// This is a final policy, semantic, or provider-integrity error and must be returned unchanged.
74    Never,
75    /// The implementation explicitly requests dispatcher-owned gather and one host retry.
76    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}