Skip to main content

vyre_driver/diagnostics/
types.rs

1use std::borrow::Cow;
2use std::fmt::Write as _;
3
4use serde::{Deserialize, Serialize};
5
6use super::serde_cow::{de_cow_static, de_opt_cow_static};
7
8/// Severity of a [`Diagnostic`].
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
10#[serde(rename_all = "lowercase")]
11#[non_exhaustive]
12pub enum Severity {
13    /// A hard failure; the caller must not use the program.
14    Error,
15    /// A soft failure; the program is usable but something is off.
16    Warning,
17    /// An informational follow-up attached to another diagnostic.
18    Note,
19}
20
21impl Severity {
22    /// Short label suitable for human rendering.
23    #[must_use]
24    pub const fn label(self) -> &'static str {
25        match self {
26            Severity::Error => "error",
27            Severity::Warning => "warning",
28            Severity::Note => "note",
29        }
30    }
31}
32
33/// Stable, machine-readable diagnostic code.
34#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
35#[serde(transparent)]
36pub struct DiagnosticCode(#[serde(deserialize_with = "de_cow_static")] pub Cow<'static, str>);
37
38impl DiagnosticCode {
39    /// Construct a code from a static string.
40    #[must_use]
41    pub const fn new(code: &'static str) -> Self {
42        Self(Cow::Borrowed(code))
43    }
44
45    /// The raw code string, for example `"E-INLINE-CYCLE"`.
46    #[must_use]
47    pub fn as_str(&self) -> &str {
48        &self.0
49    }
50}
51
52impl std::fmt::Display for DiagnosticCode {
53    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
54        f.write_str(&self.0)
55    }
56}
57
58/// Location of a diagnostic inside a `Program`.
59#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
60pub struct OpLocation {
61    /// The op identifier, for example `"math.add"`.
62    #[serde(deserialize_with = "de_cow_static")]
63    pub op_id: Cow<'static, str>,
64    /// Zero-based operand index, if the diagnostic is about a specific operand.
65    #[serde(skip_serializing_if = "Option::is_none", default)]
66    pub operand_idx: Option<u32>,
67    /// Attribute name, if the diagnostic is about a specific attribute.
68    #[serde(
69        skip_serializing_if = "Option::is_none",
70        default,
71        deserialize_with = "de_opt_cow_static"
72    )]
73    pub attr_name: Option<Cow<'static, str>>,
74}
75
76impl OpLocation {
77    /// Build a location that only identifies the op.
78    #[must_use]
79    pub fn op(op_id: impl Into<Cow<'static, str>>) -> Self {
80        Self {
81            op_id: op_id.into(),
82            operand_idx: None,
83            attr_name: None,
84        }
85    }
86
87    /// Attach a specific operand index.
88    #[must_use]
89    pub fn with_operand(mut self, idx: u32) -> Self {
90        self.operand_idx = Some(idx);
91        self
92    }
93
94    /// Attach a specific attribute name.
95    #[must_use]
96    pub fn with_attr(mut self, name: impl Into<Cow<'static, str>>) -> Self {
97        self.attr_name = Some(name.into());
98        self
99    }
100}
101
102/// A structured diagnostic.
103#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
104pub struct Diagnostic {
105    /// Severity of the diagnostic.
106    pub severity: Severity,
107    /// Stable machine-readable code.
108    pub code: DiagnosticCode,
109    /// The primary human-readable message.
110    #[serde(deserialize_with = "de_cow_static")]
111    pub message: Cow<'static, str>,
112    /// Optional op / operand / attribute location.
113    #[serde(skip_serializing_if = "Option::is_none", default)]
114    pub location: Option<OpLocation>,
115    /// Optional actionable fix the caller can apply.
116    #[serde(
117        skip_serializing_if = "Option::is_none",
118        default,
119        deserialize_with = "de_opt_cow_static"
120    )]
121    pub suggested_fix: Option<Cow<'static, str>>,
122    /// Optional documentation URL.
123    #[serde(
124        skip_serializing_if = "Option::is_none",
125        default,
126        deserialize_with = "de_opt_cow_static"
127    )]
128    pub doc_url: Option<Cow<'static, str>>,
129}
130
131impl Diagnostic {
132    /// Construct a new error-severity diagnostic.
133    #[must_use]
134    pub fn error(code: &'static str, message: impl Into<Cow<'static, str>>) -> Self {
135        Self {
136            severity: Severity::Error,
137            code: DiagnosticCode::new(code),
138            message: message.into(),
139            location: None,
140            suggested_fix: None,
141            doc_url: None,
142        }
143    }
144
145    /// Construct a new warning-severity diagnostic.
146    #[must_use]
147    pub fn warning(code: &'static str, message: impl Into<Cow<'static, str>>) -> Self {
148        Self {
149            severity: Severity::Warning,
150            code: DiagnosticCode::new(code),
151            message: message.into(),
152            location: None,
153            suggested_fix: None,
154            doc_url: None,
155        }
156    }
157
158    /// Construct a new note-severity diagnostic.
159    #[must_use]
160    pub fn note(code: &'static str, message: impl Into<Cow<'static, str>>) -> Self {
161        Self {
162            severity: Severity::Note,
163            code: DiagnosticCode::new(code),
164            message: message.into(),
165            location: None,
166            suggested_fix: None,
167            doc_url: None,
168        }
169    }
170
171    /// Attach an op location.
172    #[must_use]
173    pub fn with_location(mut self, loc: OpLocation) -> Self {
174        self.location = Some(loc);
175        self
176    }
177
178    /// Attach a suggested fix.
179    #[must_use]
180    pub fn with_fix(mut self, fix: impl Into<Cow<'static, str>>) -> Self {
181        self.suggested_fix = Some(fix.into());
182        self
183    }
184
185    /// Attach a documentation URL.
186    #[must_use]
187    pub fn with_doc_url(mut self, url: impl Into<Cow<'static, str>>) -> Self {
188        self.doc_url = Some(url.into());
189        self
190    }
191
192    /// Render the diagnostic as rustc-style human text.
193    #[must_use]
194    pub fn render_human(&self) -> String {
195        let mut out = String::with_capacity(256);
196        let _ = write!(
197            out,
198            "{}[{}]: {}",
199            self.severity.label(),
200            self.code,
201            self.message
202        );
203        if let Some(loc) = &self.location {
204            out.push_str("\n  --> op `");
205            out.push_str(&loc.op_id);
206            out.push('`');
207            if let Some(idx) = loc.operand_idx {
208                let _ = write!(out, " operand[{idx}]");
209            }
210            if let Some(attr) = &loc.attr_name {
211                out.push_str(" attr `");
212                out.push_str(attr);
213                out.push('`');
214            }
215        }
216        if let Some(fix) = &self.suggested_fix {
217            out.push_str("\n  = help: ");
218            out.push_str(fix);
219        }
220        if let Some(url) = &self.doc_url {
221            out.push_str("\n  = note: ");
222            out.push_str(url);
223        }
224        out
225    }
226
227    /// Serialize the diagnostic to a JSON string.
228    #[must_use]
229    pub fn to_json(&self) -> String {
230        match serde_json::to_string(self) {
231            Ok(json) => json,
232            Err(e) => format!(
233                r#"{{"error":"Diagnostic::to_json serialization failed","code":"{code}","message":"{message}","serde_error":"{serde_error}","fix":"Fix: inspect Diagnostic fields for non-serializable types; every field must implement Serialize."}}"#,
234                code = self.code.as_str().replace('"', "\\\""),
235                message = self.message.replace('"', "\\\""),
236                serde_error = e.to_string().replace('"', "\\\""),
237            ),
238        }
239    }
240}
241
242impl std::fmt::Display for Diagnostic {
243    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
244        f.write_str(&self.render_human())
245    }
246}