1use 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
26#[serde(rename_all = "lowercase")]
27#[non_exhaustive]
28pub enum Severity {
29 Error,
31 Warning,
33 Note,
35}
36
37impl Severity {
38 #[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#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
51#[serde(rename_all = "snake_case")]
52#[non_exhaustive]
53pub enum DiagnosticStage {
54 Validate,
56 Optimize,
58 Plan,
60 Lower,
62 Emit,
64 Admit,
66 Materialize,
68 Submit,
70 Complete,
72}
73
74#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
76#[serde(rename_all = "snake_case")]
77#[non_exhaustive]
78pub enum RetryClass {
79 Never,
81 SameDevice,
83 NewDevice,
85 RecompileSource,
87}
88
89#[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 #[must_use]
99 pub const fn new(code: &'static str) -> Self {
100 Self(Cow::Borrowed(code))
101 }
102
103 #[must_use]
105 pub fn from_owned(code: String) -> Self {
106 Self(Cow::Owned(code))
107 }
108
109 #[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#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
124pub struct OpLocation {
125 #[serde(deserialize_with = "deserialize_cow_static")]
127 pub op_id: Cow<'static, str>,
128 #[serde(skip_serializing_if = "Option::is_none", default)]
130 pub operand_idx: Option<u32>,
131 #[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 #[serde(skip_serializing_if = "Option::is_none", default)]
140 pub graph_node: Option<u32>,
141 #[serde(skip_serializing_if = "Option::is_none", default)]
143 pub graph_value: Option<u32>,
144 #[serde(skip_serializing_if = "Option::is_none", default)]
146 pub path: Option<String>,
147 #[serde(skip_serializing_if = "Option::is_none", default)]
149 pub source_span: Option<[u32; 2]>,
150}
151
152impl OpLocation {
153 #[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 #[must_use]
169 pub fn with_operand(mut self, index: u32) -> Self {
170 self.operand_idx = Some(index);
171 self
172 }
173
174 #[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 #[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 #[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 #[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#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
205pub struct DiagnosticCause {
206 pub kind: String,
208 pub detail: String,
210}
211
212#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
214pub struct Diagnostic {
215 pub severity: Severity,
217 pub code: DiagnosticCode,
219 pub stage: DiagnosticStage,
221 #[serde(deserialize_with = "deserialize_cow_static")]
223 pub message: Cow<'static, str>,
224 #[serde(skip_serializing_if = "Option::is_none", default)]
226 pub location: Option<OpLocation>,
227 #[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 #[serde(skip_serializing_if = "Option::is_none", default)]
236 pub cause: Option<DiagnosticCause>,
237 pub retry: RetryClass,
239 #[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 #[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 #[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 #[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 #[must_use]
283 pub const fn with_stage(mut self, stage: DiagnosticStage) -> Self {
284 self.stage = stage;
285 self
286 }
287
288 #[must_use]
290 pub fn with_location(mut self, location: OpLocation) -> Self {
291 self.location = Some(location);
292 self
293 }
294
295 #[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 #[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 #[must_use]
314 pub const fn with_retry(mut self, retry: RetryClass) -> Self {
315 self.retry = retry;
316 self
317 }
318
319 #[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 #[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 #[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}