use core::fmt;
use std::borrow::Cow;
use std::sync::Arc;
use serde::{Deserialize, Serialize};
use crate::diagnostics::{
Diagnostic, DiagnosticCode, DiagnosticStage, OpLocation, RetryClass, Severity,
};
#[derive(Debug, Clone, PartialEq, Eq, Hash, Ord, PartialOrd, Serialize)]
#[serde(transparent)]
pub struct ValidationCode(Cow<'static, str>);
impl<'de> Deserialize<'de> for ValidationCode {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let code = String::deserialize(deserializer)?;
let validation_code = Self(Cow::Owned(code));
if validation_code.phase().is_none() {
return Err(serde::de::Error::custom(format!(
"unknown validation code `{validation_code}`"
)));
}
Ok(validation_code)
}
}
const VALIDATION_RULES: &[(&str, ValidationPhase)] = &[
("V008", ValidationPhase::Node),
("V009", ValidationPhase::Memory),
("V010", ValidationPhase::Memory),
("V011", ValidationPhase::Node),
("V012", ValidationPhase::Expression),
("V013", ValidationPhase::Memory),
("V014", ValidationPhase::Memory),
("V016", ValidationPhase::Expression),
("V018", ValidationPhase::Limits),
("V019", ValidationPhase::Limits),
("V020", ValidationPhase::Expression),
("V021", ValidationPhase::Expression),
("V022", ValidationPhase::Expression),
("V023", ValidationPhase::Expression),
("V025", ValidationPhase::Memory),
("V027", ValidationPhase::Memory),
("V028", ValidationPhase::Type),
("V029", ValidationPhase::Expression),
("V030", ValidationPhase::Expression),
("V031", ValidationPhase::Node),
("V032", ValidationPhase::Node),
("V033", ValidationPhase::Limits),
("V034", ValidationPhase::Expression),
("V035", ValidationPhase::Type),
("V036", ValidationPhase::Node),
("V041", ValidationPhase::Expression),
("V042", ValidationPhase::Memory),
("V043", ValidationPhase::Memory),
("V044", ValidationPhase::Type),
("V045", ValidationPhase::Node),
("V046", ValidationPhase::Node),
("V047", ValidationPhase::Expression),
("V051", ValidationPhase::Expression),
("V052", ValidationPhase::Expression),
("V053", ValidationPhase::Expression),
("V054", ValidationPhase::Expression),
("V055", ValidationPhase::Memory),
("V056", ValidationPhase::Capability),
("V057", ValidationPhase::Memory),
("V058", ValidationPhase::Memory),
("V059", ValidationPhase::Memory),
("V060", ValidationPhase::Memory),
("V061", ValidationPhase::Memory),
("V063", ValidationPhase::Memory),
("V064", ValidationPhase::Memory),
("V065", ValidationPhase::Memory),
("V066", ValidationPhase::Expression),
("V067", ValidationPhase::Expression),
("V068", ValidationPhase::Expression),
("V070", ValidationPhase::Program),
("V083", ValidationPhase::Program),
("V084", ValidationPhase::Type),
("V085", ValidationPhase::Type),
("V086", ValidationPhase::Type),
("V087", ValidationPhase::Type),
("V088", ValidationPhase::Type),
("V089", ValidationPhase::Type),
("V090", ValidationPhase::Type),
("V091", ValidationPhase::Type),
("V092", ValidationPhase::Type),
("V093", ValidationPhase::Type),
("V094", ValidationPhase::Type),
("V095", ValidationPhase::Type),
("V096", ValidationPhase::Type),
("V097", ValidationPhase::Type),
("V098", ValidationPhase::Type),
("V099", ValidationPhase::Type),
("V100", ValidationPhase::Type),
("V101", ValidationPhase::Type),
("V102", ValidationPhase::Type),
("V103", ValidationPhase::Type),
("V104", ValidationPhase::Type),
("V105", ValidationPhase::Program),
("V106", ValidationPhase::Program),
("V107", ValidationPhase::Program),
("V108", ValidationPhase::Program),
("V109", ValidationPhase::Program),
("V110", ValidationPhase::Program),
("V111", ValidationPhase::Node),
("V112", ValidationPhase::Node),
("V113", ValidationPhase::Node),
("V114", ValidationPhase::Node),
("V115", ValidationPhase::Composition),
("V116", ValidationPhase::Composition),
("V117", ValidationPhase::Node),
("V118", ValidationPhase::Node),
("V119", ValidationPhase::Node),
("V120", ValidationPhase::Node),
("V121", ValidationPhase::Node),
("V122", ValidationPhase::Node),
("V123", ValidationPhase::Node),
("V124", ValidationPhase::Node),
("V125", ValidationPhase::Node),
("V126", ValidationPhase::Node),
("V127", ValidationPhase::Node),
("V128", ValidationPhase::Node),
("V129", ValidationPhase::Memory),
("V130", ValidationPhase::Program),
];
impl ValidationCode {
pub const V056: Self = Self(Cow::Borrowed("V056"));
#[must_use]
pub(crate) const fn new(code: &'static str) -> Self {
Self(Cow::Borrowed(code))
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
pub fn registered() -> impl ExactSizeIterator<Item = (&'static str, ValidationPhase)> + Clone {
VALIDATION_RULES.iter().copied()
}
#[must_use]
pub fn phase(&self) -> Option<ValidationPhase> {
VALIDATION_RULES
.iter()
.find_map(|(code, phase)| (*code == self.as_str()).then_some(*phase))
}
}
impl fmt::Display for ValidationCode {
fn fmt(&self, output: &mut fmt::Formatter<'_>) -> fmt::Result {
output.write_str(&self.0)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Ord, PartialOrd, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
pub enum ValidationPhase {
Program,
Node,
Expression,
Type,
Memory,
Capability,
Composition,
Limits,
}
impl ValidationPhase {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Program => "program",
Self::Node => "node",
Self::Expression => "expression",
Self::Type => "type",
Self::Memory => "memory",
Self::Capability => "capability",
Self::Composition => "composition",
Self::Limits => "limits",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[non_exhaustive]
pub enum ValidationLocation {
Program,
WorkgroupAxis(u8),
Buffer(Cow<'static, str>),
Node(u32),
Expression {
node: u32,
depth: u32,
},
Operand {
node: u32,
operand: u32,
},
Traversal {
ordinal: u64,
},
Operation(Cow<'static, str>),
}
impl ValidationLocation {
pub(crate) fn diagnostic_location(&self) -> OpLocation {
match self {
Self::Program => OpLocation::op("program"),
Self::WorkgroupAxis(axis) => {
OpLocation::op("program.workgroup_size").with_operand(u32::from(*axis))
}
Self::Buffer(name) => OpLocation::op("program.buffer").with_attr(name.clone()),
Self::Node(node) => OpLocation::op("program.node").with_graph_node(*node),
Self::Expression { node, depth } => OpLocation::op("program.expression")
.with_graph_node(*node)
.with_operand(*depth),
Self::Operand { node, operand } => OpLocation::op("program.expression")
.with_graph_node(*node)
.with_operand(*operand),
Self::Traversal { ordinal } => OpLocation::op("program.validation")
.with_graph_node(u32::try_from(*ordinal).unwrap_or(u32::MAX)),
Self::Operation(op_id) => OpLocation::op(op_id.clone()),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ValidationTraceEvent {
pub code: ValidationCode,
pub phase: ValidationPhase,
pub location: ValidationLocation,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct ValidationError {
code: ValidationCode,
phase: ValidationPhase,
location: ValidationLocation,
cause: Cow<'static, str>,
corrective_action: Cow<'static, str>,
retry: RetryClass,
}
#[derive(Deserialize)]
struct ValidationErrorWire {
code: ValidationCode,
phase: ValidationPhase,
location: ValidationLocation,
cause: Cow<'static, str>,
corrective_action: Cow<'static, str>,
retry: RetryClass,
}
impl<'de> Deserialize<'de> for ValidationError {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let wire = ValidationErrorWire::deserialize(deserializer)?;
if wire.code.phase() != Some(wire.phase) {
return Err(serde::de::Error::custom(format!(
"validation rule {} belongs to phase {:?}, not {:?}",
wire.code,
wire.code.phase(),
wire.phase
)));
}
if wire.retry != RetryClass::Never {
return Err(serde::de::Error::custom(format!(
"validation rule {} has invalid retry class {:?}",
wire.code, wire.retry
)));
}
Ok(Self {
code: wire.code,
phase: wire.phase,
location: wire.location,
cause: wire.cause,
corrective_action: wire.corrective_action,
retry: wire.retry,
})
}
}
impl ValidationError {
#[must_use]
pub(crate) fn new(
code: ValidationCode,
phase: ValidationPhase,
location: ValidationLocation,
cause: impl Into<Cow<'static, str>>,
corrective_action: impl Into<Cow<'static, str>>,
) -> Self {
assert_eq!(
code.phase(),
Some(phase),
"validation rule {code} emitted from the wrong phase"
);
Self {
code,
phase,
location,
cause: cause.into(),
corrective_action: corrective_action.into(),
retry: RetryClass::Never,
}
}
#[must_use]
pub fn unsupported_op(backend: &'static str, op_id: &Arc<str>, node_index: usize) -> Self {
Self::new(
ValidationCode::V056,
ValidationPhase::Capability,
ValidationLocation::Operation(Cow::Owned(op_id.to_string())),
format!(
"backend `{backend}` does not support operation `{op_id}` at node {node_index}"
),
format!(
"choose a backend whose capability set includes this operation, lower the program through a supported backend pipeline, or register an implementation for `{op_id}`"
),
)
}
#[must_use]
pub fn code(&self) -> &ValidationCode {
&self.code
}
#[must_use]
pub const fn phase(&self) -> ValidationPhase {
self.phase
}
#[must_use]
pub const fn location(&self) -> &ValidationLocation {
&self.location
}
#[must_use]
pub fn cause(&self) -> &str {
&self.cause
}
#[must_use]
pub fn corrective_action(&self) -> &str {
&self.corrective_action
}
#[must_use]
pub const fn retry(&self) -> RetryClass {
self.retry
}
pub(crate) fn set_location(&mut self, location: ValidationLocation) {
self.location = location;
}
#[must_use]
pub fn message(&self) -> Cow<'_, str> {
Cow::Owned(format!(
"{}: {}. Fix: {}",
self.code, self.cause, self.corrective_action
))
}
#[must_use]
pub fn trace_event(&self) -> ValidationTraceEvent {
ValidationTraceEvent {
code: self.code.clone(),
phase: self.phase,
location: self.location.clone(),
}
}
#[must_use]
pub fn diagnostic(&self) -> Diagnostic {
Diagnostic {
severity: Severity::Error,
code: DiagnosticCode::from_owned(self.code.as_str().to_string()),
stage: DiagnosticStage::Validate,
message: self.cause.clone(),
location: Some(self.location.diagnostic_location()),
suggested_fix: Some(self.corrective_action.clone()),
cause: Some(crate::diagnostics::DiagnosticCause {
kind: self.phase.as_str().to_string(),
detail: self.cause.to_string(),
}),
retry: self.retry,
doc_url: Some(Cow::Owned(format!(
"https://docs.vyre.dev/validator-errors#{}",
self.code.as_str().to_ascii_lowercase()
))),
}
}
}
impl From<&ValidationError> for Diagnostic {
fn from(issue: &ValidationError) -> Self {
issue.diagnostic()
}
}
impl From<ValidationError> for Diagnostic {
fn from(issue: ValidationError) -> Self {
issue.diagnostic()
}
}
impl fmt::Display for ValidationError {
fn fmt(&self, output: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(output, "vyre IR validation: {}", self.message())
}
}
impl std::error::Error for ValidationError {}
#[cfg(test)]
mod tests {
use super::*;
fn issue() -> ValidationError {
ValidationError::new(
ValidationCode::new("V028"),
ValidationPhase::Type,
ValidationLocation::Operand {
node: 7,
operand: 1,
},
"Fma operand has type i32, expected f32",
"cast the operand to f32",
)
}
#[test]
fn every_validation_family_is_enforced_at_the_shared_choke_point() {
let cases = [
("V105", ValidationPhase::Program),
("V112", ValidationPhase::Node),
("V012", ValidationPhase::Expression),
("V084", ValidationPhase::Type),
("V057", ValidationPhase::Memory),
("V056", ValidationPhase::Capability),
("V115", ValidationPhase::Composition),
("V018", ValidationPhase::Limits),
];
for (code, phase) in cases {
let issue = ValidationError::new(
ValidationCode::new(code),
phase,
ValidationLocation::Program,
"family mutation",
"restore the owning phase",
);
assert_eq!(issue.code.phase(), Some(phase));
}
}
#[test]
#[should_panic(expected = "emitted from the wrong phase")]
fn phase_mutation_fails_at_the_shared_choke_point() {
let _ = ValidationError::new(
ValidationCode::new("V105"),
ValidationPhase::Node,
ValidationLocation::Program,
"mutated rule owner",
"restore the program phase",
);
}
#[test]
fn typed_issue_projects_without_parsing_prose() {
let issue = issue();
assert_eq!(issue.code().as_str(), "V028");
assert_eq!(
issue.message(),
"V028: Fma operand has type i32, expected f32. Fix: cast the operand to f32"
);
assert_eq!(issue.trace_event().phase, ValidationPhase::Type);
let diagnostic = issue.diagnostic();
assert_eq!(diagnostic.code.as_str(), "V028");
assert_eq!(diagnostic.stage, DiagnosticStage::Validate);
assert_eq!(diagnostic.retry, RetryClass::Never);
assert_eq!(
diagnostic
.location
.as_ref()
.and_then(|location| location.graph_node),
Some(7)
);
assert_eq!(
diagnostic.suggested_fix.as_deref(),
Some("cast the operand to f32")
);
assert_eq!(
diagnostic.cause.as_ref().map(|cause| cause.kind.as_str()),
Some("type")
);
}
#[test]
fn serialization_preserves_every_issue_field() {
let issue = issue();
let encoded = serde_json::to_vec(&issue).expect("validation issue must serialize");
let decoded: ValidationError =
serde_json::from_slice(&encoded).expect("validation issue must deserialize");
assert_eq!(decoded, issue);
assert_eq!(decoded.diagnostic(), issue.diagnostic());
}
#[test]
fn deserialization_rejects_unknown_rule_identity() {
let encoded = serde_json::to_value(issue()).expect("issue must serialize");
let mut mutated = encoded;
mutated["code"] = serde_json::Value::String(format!("V{}", 999));
let error = serde_json::from_value::<ValidationError>(mutated)
.expect_err("unknown validation rule must fail closed");
assert!(error.to_string().contains("unknown validation code"));
}
#[test]
fn deserialization_rejects_phase_mutation() {
let encoded = serde_json::to_value(issue()).expect("issue must serialize");
let mut mutated = encoded;
mutated["phase"] = serde_json::Value::String("node".to_string());
let error = serde_json::from_value::<ValidationError>(mutated)
.expect_err("phase mutation must fail closed");
assert!(error.to_string().contains("belongs to phase"));
}
#[test]
fn unsupported_op_has_typed_capability_identity() {
let issue = ValidationError::unsupported_op("backend-a", &Arc::from("math::fma"), 3);
assert_eq!(issue.code().as_str(), "V056");
assert_eq!(issue.phase(), ValidationPhase::Capability);
assert!(issue.message().contains("backend-a"));
assert!(issue.message().contains("math::fma"));
assert!(issue.message().contains("3"));
assert!(issue.message().contains("Fix:"));
}
}