Skip to main content

vyre_foundation/
diagnostics.rs

1//! Shared structured diagnostic protocol for compiler and workflow boundaries.
2
3use std::borrow::Cow;
4use std::fmt::Write as _;
5
6use serde::{Deserialize, Deserializer, Serialize};
7
8fn deserialize_cow_static<'de, D>(deserializer: D) -> Result<Cow<'static, str>, D::Error>
9where
10    D: Deserializer<'de>,
11{
12    String::deserialize(deserializer).map(Cow::Owned)
13}
14
15fn deserialize_optional_cow_static<'de, D>(
16    deserializer: D,
17) -> Result<Option<Cow<'static, str>>, D::Error>
18where
19    D: Deserializer<'de>,
20{
21    Option::<String>::deserialize(deserializer).map(|value| value.map(Cow::Owned))
22}
23
24/// Severity of a diagnostic.
25#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
26#[serde(rename_all = "lowercase")]
27#[non_exhaustive]
28pub enum Severity {
29    /// A hard failure. The rejected product must not be used.
30    Error,
31    /// A soft failure attached to a usable product.
32    Warning,
33    /// Informational context attached to another diagnostic.
34    Note,
35}
36
37impl Severity {
38    /// Stable human-readable severity label.
39    #[must_use]
40    pub const fn label(self) -> &'static str {
41        match self {
42            Self::Error => "error",
43            Self::Warning => "warning",
44            Self::Note => "note",
45        }
46    }
47}
48
49/// Compiler or workflow stage that produced a diagnostic.
50#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
51#[serde(rename_all = "snake_case")]
52#[non_exhaustive]
53pub enum DiagnosticStage {
54    /// Semantic or structural validation.
55    Validate,
56    /// Semantic optimization.
57    Optimize,
58    /// Whole-graph planning and selection.
59    Plan,
60    /// Verified descriptor lowering.
61    Lower,
62    /// Target payload emission.
63    Emit,
64    /// Artifact admission and authentication.
65    Admit,
66    /// Device-specific materialization.
67    Materialize,
68    /// Typed submission.
69    Submit,
70    /// Completion and readback.
71    Complete,
72}
73
74/// Whether and where a failed workflow may be retried.
75#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
76#[serde(rename_all = "snake_case")]
77#[non_exhaustive]
78pub enum RetryClass {
79    /// Repeating the operation cannot succeed without changing its inputs.
80    Never,
81    /// Retry on the same device generation may succeed.
82    SameDevice,
83    /// Retry only after acquiring a new device generation.
84    NewDevice,
85    /// Recompile the source graph before retrying.
86    RecompileSource,
87}
88
89/// Stable, machine-readable diagnostic code.
90#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
91#[serde(transparent)]
92pub struct DiagnosticCode(
93    #[serde(deserialize_with = "deserialize_cow_static")] pub Cow<'static, str>,
94);
95
96impl DiagnosticCode {
97    /// Construct a code from a stable static string.
98    #[must_use]
99    pub const fn new(code: &'static str) -> Self {
100        Self(Cow::Borrowed(code))
101    }
102
103    /// Construct a code from validated owned data.
104    #[must_use]
105    pub fn from_owned(code: String) -> Self {
106        Self(Cow::Owned(code))
107    }
108
109    /// Return the raw stable code.
110    #[must_use]
111    pub fn as_str(&self) -> &str {
112        &self.0
113    }
114}
115
116impl std::fmt::Display for DiagnosticCode {
117    fn fmt(&self, output: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
118        output.write_str(&self.0)
119    }
120}
121
122/// Typed location of a diagnostic inside source, graph, or artifact state.
123#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
124pub struct OpLocation {
125    /// Stable operation or pass identifier when available.
126    #[serde(deserialize_with = "deserialize_cow_static")]
127    pub op_id: Cow<'static, str>,
128    /// Zero-based operand index.
129    #[serde(skip_serializing_if = "Option::is_none", default)]
130    pub operand_idx: Option<u32>,
131    /// Attribute name.
132    #[serde(
133        skip_serializing_if = "Option::is_none",
134        default,
135        deserialize_with = "deserialize_optional_cow_static"
136    )]
137    pub attr_name: Option<Cow<'static, str>>,
138    /// Typed graph node identity.
139    #[serde(skip_serializing_if = "Option::is_none", default)]
140    pub graph_node: Option<u32>,
141    /// Typed graph value identity.
142    #[serde(skip_serializing_if = "Option::is_none", default)]
143    pub graph_value: Option<u32>,
144    /// Canonical request, source, or artifact path.
145    #[serde(skip_serializing_if = "Option::is_none", default)]
146    pub path: Option<String>,
147    /// Byte span inside the source path.
148    #[serde(skip_serializing_if = "Option::is_none", default)]
149    pub source_span: Option<[u32; 2]>,
150}
151
152impl OpLocation {
153    /// Build a location that identifies an operation or pass.
154    #[must_use]
155    pub fn op(op_id: impl Into<Cow<'static, str>>) -> Self {
156        Self {
157            op_id: op_id.into(),
158            operand_idx: None,
159            attr_name: None,
160            graph_node: None,
161            graph_value: None,
162            path: None,
163            source_span: None,
164        }
165    }
166
167    /// Attach a specific operand index.
168    #[must_use]
169    pub fn with_operand(mut self, index: u32) -> Self {
170        self.operand_idx = Some(index);
171        self
172    }
173
174    /// Attach a specific attribute name.
175    #[must_use]
176    pub fn with_attr(mut self, name: impl Into<Cow<'static, str>>) -> Self {
177        self.attr_name = Some(name.into());
178        self
179    }
180
181    /// Attach a typed graph node identity.
182    #[must_use]
183    pub const fn with_graph_node(mut self, node: u32) -> Self {
184        self.graph_node = Some(node);
185        self
186    }
187
188    /// Attach a typed graph value identity.
189    #[must_use]
190    pub const fn with_graph_value(mut self, value: u32) -> Self {
191        self.graph_value = Some(value);
192        self
193    }
194
195    /// Attach a canonical source or artifact path.
196    #[must_use]
197    pub fn with_path(mut self, path: impl Into<String>) -> Self {
198        self.path = Some(path.into());
199        self
200    }
201}
202
203/// Structured cause preserved across owner boundaries.
204#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
205pub struct DiagnosticCause {
206    /// Stable cause family, such as `device_lost` or `version_skew`.
207    pub kind: String,
208    /// Deterministic cause detail.
209    pub detail: String,
210}
211
212/// Serializable diagnostic shared by compiler, AOT, runtime, and drivers.
213#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
214pub struct Diagnostic {
215    /// Severity of the diagnostic.
216    pub severity: Severity,
217    /// Stable machine-readable code.
218    pub code: DiagnosticCode,
219    /// Stage that produced the diagnostic.
220    pub stage: DiagnosticStage,
221    /// Deterministic failure detail.
222    #[serde(deserialize_with = "deserialize_cow_static")]
223    pub message: Cow<'static, str>,
224    /// Typed source, graph, operation, or artifact location.
225    #[serde(skip_serializing_if = "Option::is_none", default)]
226    pub location: Option<OpLocation>,
227    /// Corrective action the caller can apply.
228    #[serde(
229        skip_serializing_if = "Option::is_none",
230        default,
231        deserialize_with = "deserialize_optional_cow_static"
232    )]
233    pub suggested_fix: Option<Cow<'static, str>>,
234    /// Structured cause retained from the owning stage.
235    #[serde(skip_serializing_if = "Option::is_none", default)]
236    pub cause: Option<DiagnosticCause>,
237    /// Retry policy for this failure.
238    pub retry: RetryClass,
239    /// Optional stable documentation URL.
240    #[serde(
241        skip_serializing_if = "Option::is_none",
242        default,
243        deserialize_with = "deserialize_optional_cow_static"
244    )]
245    pub doc_url: Option<Cow<'static, str>>,
246}
247
248impl Diagnostic {
249    /// Construct an error diagnostic at validation stage with no retry.
250    #[must_use]
251    pub fn error(code: &'static str, message: impl Into<Cow<'static, str>>) -> Self {
252        Self::new(Severity::Error, code, message)
253    }
254
255    /// Construct a warning diagnostic at validation stage with no retry.
256    #[must_use]
257    pub fn warning(code: &'static str, message: impl Into<Cow<'static, str>>) -> Self {
258        Self::new(Severity::Warning, code, message)
259    }
260
261    /// Construct a note diagnostic at validation stage with no retry.
262    #[must_use]
263    pub fn note(code: &'static str, message: impl Into<Cow<'static, str>>) -> Self {
264        Self::new(Severity::Note, code, message)
265    }
266
267    fn new(severity: Severity, code: &'static str, message: impl Into<Cow<'static, str>>) -> Self {
268        Self {
269            severity,
270            code: DiagnosticCode::new(code),
271            stage: DiagnosticStage::Validate,
272            message: message.into(),
273            location: None,
274            suggested_fix: None,
275            cause: None,
276            retry: RetryClass::Never,
277            doc_url: None,
278        }
279    }
280
281    /// Set the owning workflow stage.
282    #[must_use]
283    pub const fn with_stage(mut self, stage: DiagnosticStage) -> Self {
284        self.stage = stage;
285        self
286    }
287
288    /// Attach a typed location.
289    #[must_use]
290    pub fn with_location(mut self, location: OpLocation) -> Self {
291        self.location = Some(location);
292        self
293    }
294
295    /// Attach a corrective action.
296    #[must_use]
297    pub fn with_fix(mut self, fix: impl Into<Cow<'static, str>>) -> Self {
298        self.suggested_fix = Some(fix.into());
299        self
300    }
301
302    /// Attach a structured cause.
303    #[must_use]
304    pub fn with_cause(mut self, kind: impl Into<String>, detail: impl Into<String>) -> Self {
305        self.cause = Some(DiagnosticCause {
306            kind: kind.into(),
307            detail: detail.into(),
308        });
309        self
310    }
311
312    /// Set the retry policy.
313    #[must_use]
314    pub const fn with_retry(mut self, retry: RetryClass) -> Self {
315        self.retry = retry;
316        self
317    }
318
319    /// Attach a documentation URL.
320    #[must_use]
321    pub fn with_doc_url(mut self, url: impl Into<Cow<'static, str>>) -> Self {
322        self.doc_url = Some(url.into());
323        self
324    }
325
326    /// Render a deterministic rustc-style diagnostic.
327    #[must_use]
328    pub fn render_human(&self) -> String {
329        let mut output = String::with_capacity(256);
330        let _ = write!(
331            output,
332            "{}[{}]({:?}): {}",
333            self.severity.label(),
334            self.code,
335            self.stage,
336            self.message
337        );
338        if let Some(location) = &self.location {
339            output.push_str("\n  --> op `");
340            output.push_str(&location.op_id);
341            output.push('`');
342            if let Some(index) = location.operand_idx {
343                let _ = write!(output, " operand[{index}]");
344            }
345            if let Some(attribute) = &location.attr_name {
346                output.push_str(" attr `");
347                output.push_str(attribute);
348                output.push('`');
349            }
350            if let Some(path) = &location.path {
351                output.push_str(" at ");
352                output.push_str(path);
353            }
354        }
355        if let Some(fix) = &self.suggested_fix {
356            output.push_str("\n  = help: ");
357            output.push_str(fix);
358        }
359        if let Some(cause) = &self.cause {
360            let _ = write!(output, "\n  = cause[{}]: {}", cause.kind, cause.detail);
361        }
362        if let Some(url) = &self.doc_url {
363            output.push_str("\n  = note: ");
364            output.push_str(url);
365        }
366        output
367    }
368
369    /// Serialize this diagnostic as canonical JSON.
370    #[must_use]
371    pub fn to_json(&self) -> String {
372        serde_json::to_string(self).expect("Diagnostic serialization is infallible")
373    }
374}
375
376impl std::fmt::Display for Diagnostic {
377    fn fmt(&self, output: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
378        output.write_str(&self.render_human())
379    }
380}