use std::borrow::Cow;
use std::fmt::Write as _;
use serde::{Deserialize, Deserializer, Serialize};
fn deserialize_cow_static<'de, D>(deserializer: D) -> Result<Cow<'static, str>, D::Error>
where
D: Deserializer<'de>,
{
String::deserialize(deserializer).map(Cow::Owned)
}
fn deserialize_optional_cow_static<'de, D>(
deserializer: D,
) -> Result<Option<Cow<'static, str>>, D::Error>
where
D: Deserializer<'de>,
{
Option::<String>::deserialize(deserializer).map(|value| value.map(Cow::Owned))
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
#[non_exhaustive]
pub enum Severity {
Error,
Warning,
Note,
}
impl Severity {
#[must_use]
pub const fn label(self) -> &'static str {
match self {
Self::Error => "error",
Self::Warning => "warning",
Self::Note => "note",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
pub enum DiagnosticStage {
Validate,
Optimize,
Plan,
Lower,
Emit,
Admit,
Materialize,
Submit,
Complete,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
pub enum RetryClass {
Never,
SameDevice,
NewDevice,
RecompileSource,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(transparent)]
pub struct DiagnosticCode(
#[serde(deserialize_with = "deserialize_cow_static")] pub Cow<'static, str>,
);
impl DiagnosticCode {
#[must_use]
pub const fn new(code: &'static str) -> Self {
Self(Cow::Borrowed(code))
}
#[must_use]
pub fn from_owned(code: String) -> Self {
Self(Cow::Owned(code))
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
}
impl std::fmt::Display for DiagnosticCode {
fn fmt(&self, output: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
output.write_str(&self.0)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct OpLocation {
#[serde(deserialize_with = "deserialize_cow_static")]
pub op_id: Cow<'static, str>,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub operand_idx: Option<u32>,
#[serde(
skip_serializing_if = "Option::is_none",
default,
deserialize_with = "deserialize_optional_cow_static"
)]
pub attr_name: Option<Cow<'static, str>>,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub graph_node: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub graph_value: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub path: Option<String>,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub source_span: Option<[u32; 2]>,
}
impl OpLocation {
#[must_use]
pub fn op(op_id: impl Into<Cow<'static, str>>) -> Self {
Self {
op_id: op_id.into(),
operand_idx: None,
attr_name: None,
graph_node: None,
graph_value: None,
path: None,
source_span: None,
}
}
#[must_use]
pub fn with_operand(mut self, index: u32) -> Self {
self.operand_idx = Some(index);
self
}
#[must_use]
pub fn with_attr(mut self, name: impl Into<Cow<'static, str>>) -> Self {
self.attr_name = Some(name.into());
self
}
#[must_use]
pub const fn with_graph_node(mut self, node: u32) -> Self {
self.graph_node = Some(node);
self
}
#[must_use]
pub const fn with_graph_value(mut self, value: u32) -> Self {
self.graph_value = Some(value);
self
}
#[must_use]
pub fn with_path(mut self, path: impl Into<String>) -> Self {
self.path = Some(path.into());
self
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct DiagnosticCause {
pub kind: String,
pub detail: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Diagnostic {
pub severity: Severity,
pub code: DiagnosticCode,
pub stage: DiagnosticStage,
#[serde(deserialize_with = "deserialize_cow_static")]
pub message: Cow<'static, str>,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub location: Option<OpLocation>,
#[serde(
skip_serializing_if = "Option::is_none",
default,
deserialize_with = "deserialize_optional_cow_static"
)]
pub suggested_fix: Option<Cow<'static, str>>,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub cause: Option<DiagnosticCause>,
pub retry: RetryClass,
#[serde(
skip_serializing_if = "Option::is_none",
default,
deserialize_with = "deserialize_optional_cow_static"
)]
pub doc_url: Option<Cow<'static, str>>,
}
impl Diagnostic {
#[must_use]
pub fn error(code: &'static str, message: impl Into<Cow<'static, str>>) -> Self {
Self::new(Severity::Error, code, message)
}
#[must_use]
pub fn warning(code: &'static str, message: impl Into<Cow<'static, str>>) -> Self {
Self::new(Severity::Warning, code, message)
}
#[must_use]
pub fn note(code: &'static str, message: impl Into<Cow<'static, str>>) -> Self {
Self::new(Severity::Note, code, message)
}
fn new(severity: Severity, code: &'static str, message: impl Into<Cow<'static, str>>) -> Self {
Self {
severity,
code: DiagnosticCode::new(code),
stage: DiagnosticStage::Validate,
message: message.into(),
location: None,
suggested_fix: None,
cause: None,
retry: RetryClass::Never,
doc_url: None,
}
}
#[must_use]
pub const fn with_stage(mut self, stage: DiagnosticStage) -> Self {
self.stage = stage;
self
}
#[must_use]
pub fn with_location(mut self, location: OpLocation) -> Self {
self.location = Some(location);
self
}
#[must_use]
pub fn with_fix(mut self, fix: impl Into<Cow<'static, str>>) -> Self {
self.suggested_fix = Some(fix.into());
self
}
#[must_use]
pub fn with_cause(mut self, kind: impl Into<String>, detail: impl Into<String>) -> Self {
self.cause = Some(DiagnosticCause {
kind: kind.into(),
detail: detail.into(),
});
self
}
#[must_use]
pub const fn with_retry(mut self, retry: RetryClass) -> Self {
self.retry = retry;
self
}
#[must_use]
pub fn with_doc_url(mut self, url: impl Into<Cow<'static, str>>) -> Self {
self.doc_url = Some(url.into());
self
}
#[must_use]
pub fn render_human(&self) -> String {
let mut output = String::with_capacity(256);
let _ = write!(
output,
"{}[{}]({:?}): {}",
self.severity.label(),
self.code,
self.stage,
self.message
);
if let Some(location) = &self.location {
output.push_str("\n --> op `");
output.push_str(&location.op_id);
output.push('`');
if let Some(index) = location.operand_idx {
let _ = write!(output, " operand[{index}]");
}
if let Some(attribute) = &location.attr_name {
output.push_str(" attr `");
output.push_str(attribute);
output.push('`');
}
if let Some(path) = &location.path {
output.push_str(" at ");
output.push_str(path);
}
}
if let Some(fix) = &self.suggested_fix {
output.push_str("\n = help: ");
output.push_str(fix);
}
if let Some(cause) = &self.cause {
let _ = write!(output, "\n = cause[{}]: {}", cause.kind, cause.detail);
}
if let Some(url) = &self.doc_url {
output.push_str("\n = note: ");
output.push_str(url);
}
output
}
#[must_use]
pub fn to_json(&self) -> String {
serde_json::to_string(self).expect("Diagnostic serialization is infallible")
}
}
impl std::fmt::Display for Diagnostic {
fn fmt(&self, output: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
output.write_str(&self.render_human())
}
}