1use core::fmt;
4use std::borrow::Cow;
5use std::sync::Arc;
6
7use serde::{Deserialize, Serialize};
8
9use crate::diagnostics::{
10 Diagnostic, DiagnosticCode, DiagnosticStage, OpLocation, RetryClass, Severity,
11};
12
13#[derive(Debug, Clone, PartialEq, Eq, Hash, Ord, PartialOrd, Serialize)]
18#[serde(transparent)]
19pub struct ValidationCode(Cow<'static, str>);
20
21impl<'de> Deserialize<'de> for ValidationCode {
22 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
23 where
24 D: serde::Deserializer<'de>,
25 {
26 let code = String::deserialize(deserializer)?;
27 let validation_code = Self(Cow::Owned(code));
28 if validation_code.phase().is_none() {
29 return Err(serde::de::Error::custom(format!(
30 "unknown validation code `{validation_code}`"
31 )));
32 }
33 Ok(validation_code)
34 }
35}
36
37const VALIDATION_RULES: &[(&str, ValidationPhase)] = &[
38 ("V008", ValidationPhase::Node),
39 ("V009", ValidationPhase::Memory),
40 ("V010", ValidationPhase::Memory),
41 ("V011", ValidationPhase::Node),
42 ("V012", ValidationPhase::Expression),
43 ("V013", ValidationPhase::Memory),
44 ("V014", ValidationPhase::Memory),
45 ("V016", ValidationPhase::Expression),
46 ("V018", ValidationPhase::Limits),
47 ("V019", ValidationPhase::Limits),
48 ("V020", ValidationPhase::Expression),
49 ("V021", ValidationPhase::Expression),
50 ("V022", ValidationPhase::Expression),
51 ("V023", ValidationPhase::Expression),
52 ("V025", ValidationPhase::Memory),
53 ("V027", ValidationPhase::Memory),
54 ("V028", ValidationPhase::Type),
55 ("V029", ValidationPhase::Expression),
56 ("V030", ValidationPhase::Expression),
57 ("V031", ValidationPhase::Node),
58 ("V032", ValidationPhase::Node),
59 ("V033", ValidationPhase::Limits),
60 ("V034", ValidationPhase::Expression),
61 ("V035", ValidationPhase::Type),
62 ("V036", ValidationPhase::Node),
63 ("V041", ValidationPhase::Expression),
64 ("V042", ValidationPhase::Memory),
65 ("V043", ValidationPhase::Memory),
66 ("V044", ValidationPhase::Type),
67 ("V045", ValidationPhase::Node),
68 ("V046", ValidationPhase::Node),
69 ("V047", ValidationPhase::Expression),
70 ("V051", ValidationPhase::Expression),
71 ("V052", ValidationPhase::Expression),
72 ("V053", ValidationPhase::Expression),
73 ("V054", ValidationPhase::Expression),
74 ("V055", ValidationPhase::Memory),
75 ("V056", ValidationPhase::Capability),
76 ("V057", ValidationPhase::Memory),
77 ("V058", ValidationPhase::Memory),
78 ("V059", ValidationPhase::Memory),
79 ("V060", ValidationPhase::Memory),
80 ("V061", ValidationPhase::Memory),
81 ("V063", ValidationPhase::Memory),
82 ("V064", ValidationPhase::Memory),
83 ("V065", ValidationPhase::Memory),
84 ("V066", ValidationPhase::Expression),
85 ("V067", ValidationPhase::Expression),
86 ("V068", ValidationPhase::Expression),
87 ("V070", ValidationPhase::Program),
88 ("V083", ValidationPhase::Program),
89 ("V084", ValidationPhase::Type),
90 ("V085", ValidationPhase::Type),
91 ("V086", ValidationPhase::Type),
92 ("V087", ValidationPhase::Type),
93 ("V088", ValidationPhase::Type),
94 ("V089", ValidationPhase::Type),
95 ("V090", ValidationPhase::Type),
96 ("V091", ValidationPhase::Type),
97 ("V092", ValidationPhase::Type),
98 ("V093", ValidationPhase::Type),
99 ("V094", ValidationPhase::Type),
100 ("V095", ValidationPhase::Type),
101 ("V096", ValidationPhase::Type),
102 ("V097", ValidationPhase::Type),
103 ("V098", ValidationPhase::Type),
104 ("V099", ValidationPhase::Type),
105 ("V100", ValidationPhase::Type),
106 ("V101", ValidationPhase::Type),
107 ("V102", ValidationPhase::Type),
108 ("V103", ValidationPhase::Type),
109 ("V104", ValidationPhase::Type),
110 ("V105", ValidationPhase::Program),
111 ("V106", ValidationPhase::Program),
112 ("V107", ValidationPhase::Program),
113 ("V108", ValidationPhase::Program),
114 ("V109", ValidationPhase::Program),
115 ("V110", ValidationPhase::Program),
116 ("V111", ValidationPhase::Node),
117 ("V112", ValidationPhase::Node),
118 ("V113", ValidationPhase::Node),
119 ("V114", ValidationPhase::Node),
120 ("V115", ValidationPhase::Composition),
121 ("V116", ValidationPhase::Composition),
122 ("V117", ValidationPhase::Node),
123 ("V118", ValidationPhase::Node),
124 ("V119", ValidationPhase::Node),
125 ("V120", ValidationPhase::Node),
126 ("V121", ValidationPhase::Node),
127 ("V122", ValidationPhase::Node),
128 ("V123", ValidationPhase::Node),
129 ("V124", ValidationPhase::Node),
130 ("V125", ValidationPhase::Node),
131 ("V126", ValidationPhase::Node),
132 ("V127", ValidationPhase::Node),
133 ("V128", ValidationPhase::Node),
134 ("V129", ValidationPhase::Memory),
135 ("V130", ValidationPhase::Program),
136];
137
138impl ValidationCode {
139 pub const V056: Self = Self(Cow::Borrowed("V056"));
141
142 #[must_use]
144 pub(crate) const fn new(code: &'static str) -> Self {
145 Self(Cow::Borrowed(code))
146 }
147
148 #[must_use]
150 pub fn as_str(&self) -> &str {
151 &self.0
152 }
153
154 pub fn registered() -> impl ExactSizeIterator<Item = (&'static str, ValidationPhase)> + Clone {
159 VALIDATION_RULES.iter().copied()
160 }
161
162 #[must_use]
164 pub fn phase(&self) -> Option<ValidationPhase> {
165 VALIDATION_RULES
166 .iter()
167 .find_map(|(code, phase)| (*code == self.as_str()).then_some(*phase))
168 }
169}
170
171impl fmt::Display for ValidationCode {
172 fn fmt(&self, output: &mut fmt::Formatter<'_>) -> fmt::Result {
173 output.write_str(&self.0)
174 }
175}
176
177#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Ord, PartialOrd, Serialize, Deserialize)]
179#[serde(rename_all = "snake_case")]
180#[non_exhaustive]
181pub enum ValidationPhase {
182 Program,
184 Node,
186 Expression,
188 Type,
190 Memory,
192 Capability,
194 Composition,
196 Limits,
198}
199
200impl ValidationPhase {
201 #[must_use]
203 pub const fn as_str(self) -> &'static str {
204 match self {
205 Self::Program => "program",
206 Self::Node => "node",
207 Self::Expression => "expression",
208 Self::Type => "type",
209 Self::Memory => "memory",
210 Self::Capability => "capability",
211 Self::Composition => "composition",
212 Self::Limits => "limits",
213 }
214 }
215}
216
217#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
219#[non_exhaustive]
220pub enum ValidationLocation {
221 Program,
223 WorkgroupAxis(u8),
225 Buffer(Cow<'static, str>),
227 Node(u32),
229 Expression {
231 node: u32,
233 depth: u32,
235 },
236 Operand {
238 node: u32,
240 operand: u32,
242 },
243 Traversal {
245 ordinal: u64,
247 },
248 Operation(Cow<'static, str>),
250}
251
252impl ValidationLocation {
253 pub(crate) fn diagnostic_location(&self) -> OpLocation {
254 match self {
255 Self::Program => OpLocation::op("program"),
256 Self::WorkgroupAxis(axis) => {
257 OpLocation::op("program.workgroup_size").with_operand(u32::from(*axis))
258 }
259 Self::Buffer(name) => OpLocation::op("program.buffer").with_attr(name.clone()),
260 Self::Node(node) => OpLocation::op("program.node").with_graph_node(*node),
261 Self::Expression { node, depth } => OpLocation::op("program.expression")
262 .with_graph_node(*node)
263 .with_operand(*depth),
264 Self::Operand { node, operand } => OpLocation::op("program.expression")
265 .with_graph_node(*node)
266 .with_operand(*operand),
267 Self::Traversal { ordinal } => OpLocation::op("program.validation")
268 .with_graph_node(u32::try_from(*ordinal).unwrap_or(u32::MAX)),
269 Self::Operation(op_id) => OpLocation::op(op_id.clone()),
270 }
271 }
272}
273
274#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
276pub struct ValidationTraceEvent {
277 pub code: ValidationCode,
279 pub phase: ValidationPhase,
281 pub location: ValidationLocation,
283}
284
285#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
287pub struct ValidationError {
288 code: ValidationCode,
290 phase: ValidationPhase,
292 location: ValidationLocation,
294 cause: Cow<'static, str>,
296 corrective_action: Cow<'static, str>,
298 retry: RetryClass,
300}
301
302#[derive(Deserialize)]
303struct ValidationErrorWire {
304 code: ValidationCode,
305 phase: ValidationPhase,
306 location: ValidationLocation,
307 cause: Cow<'static, str>,
308 corrective_action: Cow<'static, str>,
309 retry: RetryClass,
310}
311
312impl<'de> Deserialize<'de> for ValidationError {
313 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
314 where
315 D: serde::Deserializer<'de>,
316 {
317 let wire = ValidationErrorWire::deserialize(deserializer)?;
318 if wire.code.phase() != Some(wire.phase) {
319 return Err(serde::de::Error::custom(format!(
320 "validation rule {} belongs to phase {:?}, not {:?}",
321 wire.code,
322 wire.code.phase(),
323 wire.phase
324 )));
325 }
326 if wire.retry != RetryClass::Never {
327 return Err(serde::de::Error::custom(format!(
328 "validation rule {} has invalid retry class {:?}",
329 wire.code, wire.retry
330 )));
331 }
332 Ok(Self {
333 code: wire.code,
334 phase: wire.phase,
335 location: wire.location,
336 cause: wire.cause,
337 corrective_action: wire.corrective_action,
338 retry: wire.retry,
339 })
340 }
341}
342
343impl ValidationError {
344 #[must_use]
346 pub(crate) fn new(
347 code: ValidationCode,
348 phase: ValidationPhase,
349 location: ValidationLocation,
350 cause: impl Into<Cow<'static, str>>,
351 corrective_action: impl Into<Cow<'static, str>>,
352 ) -> Self {
353 assert_eq!(
354 code.phase(),
355 Some(phase),
356 "validation rule {code} emitted from the wrong phase"
357 );
358 Self {
359 code,
360 phase,
361 location,
362 cause: cause.into(),
363 corrective_action: corrective_action.into(),
364 retry: RetryClass::Never,
365 }
366 }
367
368 #[must_use]
370 pub fn unsupported_op(backend: &'static str, op_id: &Arc<str>, node_index: usize) -> Self {
371 Self::new(
372 ValidationCode::V056,
373 ValidationPhase::Capability,
374 ValidationLocation::Operation(Cow::Owned(op_id.to_string())),
375 format!(
376 "backend `{backend}` does not support operation `{op_id}` at node {node_index}"
377 ),
378 format!(
379 "choose a backend whose capability set includes this operation, lower the program through a supported backend pipeline, or register an implementation for `{op_id}`"
380 ),
381 )
382 }
383
384 #[must_use]
386 pub fn code(&self) -> &ValidationCode {
387 &self.code
388 }
389
390 #[must_use]
392 pub const fn phase(&self) -> ValidationPhase {
393 self.phase
394 }
395
396 #[must_use]
398 pub const fn location(&self) -> &ValidationLocation {
399 &self.location
400 }
401
402 #[must_use]
404 pub fn cause(&self) -> &str {
405 &self.cause
406 }
407
408 #[must_use]
410 pub fn corrective_action(&self) -> &str {
411 &self.corrective_action
412 }
413
414 #[must_use]
416 pub const fn retry(&self) -> RetryClass {
417 self.retry
418 }
419
420 pub(crate) fn set_location(&mut self, location: ValidationLocation) {
421 self.location = location;
422 }
423
424 #[must_use]
426 pub fn message(&self) -> Cow<'_, str> {
427 Cow::Owned(format!(
428 "{}: {}. Fix: {}",
429 self.code, self.cause, self.corrective_action
430 ))
431 }
432
433 #[must_use]
435 pub fn trace_event(&self) -> ValidationTraceEvent {
436 ValidationTraceEvent {
437 code: self.code.clone(),
438 phase: self.phase,
439 location: self.location.clone(),
440 }
441 }
442
443 #[must_use]
445 pub fn diagnostic(&self) -> Diagnostic {
446 Diagnostic {
447 severity: Severity::Error,
448 code: DiagnosticCode::from_owned(self.code.as_str().to_string()),
449 stage: DiagnosticStage::Validate,
450 message: self.cause.clone(),
451 location: Some(self.location.diagnostic_location()),
452 suggested_fix: Some(self.corrective_action.clone()),
453 cause: Some(crate::diagnostics::DiagnosticCause {
454 kind: self.phase.as_str().to_string(),
455 detail: self.cause.to_string(),
456 }),
457 retry: self.retry,
458 doc_url: Some(Cow::Owned(format!(
459 "https://docs.vyre.dev/validator-errors#{}",
460 self.code.as_str().to_ascii_lowercase()
461 ))),
462 }
463 }
464}
465
466impl From<&ValidationError> for Diagnostic {
467 fn from(issue: &ValidationError) -> Self {
468 issue.diagnostic()
469 }
470}
471
472impl From<ValidationError> for Diagnostic {
473 fn from(issue: ValidationError) -> Self {
474 issue.diagnostic()
475 }
476}
477
478impl fmt::Display for ValidationError {
479 fn fmt(&self, output: &mut fmt::Formatter<'_>) -> fmt::Result {
480 write!(output, "vyre IR validation: {}", self.message())
481 }
482}
483
484impl std::error::Error for ValidationError {}
485
486#[cfg(test)]
487mod tests {
488 use super::*;
489
490 fn issue() -> ValidationError {
491 ValidationError::new(
492 ValidationCode::new("V028"),
493 ValidationPhase::Type,
494 ValidationLocation::Operand {
495 node: 7,
496 operand: 1,
497 },
498 "Fma operand has type i32, expected f32",
499 "cast the operand to f32",
500 )
501 }
502
503 #[test]
504 fn every_validation_family_is_enforced_at_the_shared_choke_point() {
505 let cases = [
506 ("V105", ValidationPhase::Program),
507 ("V112", ValidationPhase::Node),
508 ("V012", ValidationPhase::Expression),
509 ("V084", ValidationPhase::Type),
510 ("V057", ValidationPhase::Memory),
511 ("V056", ValidationPhase::Capability),
512 ("V115", ValidationPhase::Composition),
513 ("V018", ValidationPhase::Limits),
514 ];
515 for (code, phase) in cases {
516 let issue = ValidationError::new(
517 ValidationCode::new(code),
518 phase,
519 ValidationLocation::Program,
520 "family mutation",
521 "restore the owning phase",
522 );
523 assert_eq!(issue.code.phase(), Some(phase));
524 }
525 }
526
527 #[test]
528 #[should_panic(expected = "emitted from the wrong phase")]
529 fn phase_mutation_fails_at_the_shared_choke_point() {
530 let _ = ValidationError::new(
531 ValidationCode::new("V105"),
532 ValidationPhase::Node,
533 ValidationLocation::Program,
534 "mutated rule owner",
535 "restore the program phase",
536 );
537 }
538
539 #[test]
540 fn typed_issue_projects_without_parsing_prose() {
541 let issue = issue();
542 assert_eq!(issue.code().as_str(), "V028");
543 assert_eq!(
544 issue.message(),
545 "V028: Fma operand has type i32, expected f32. Fix: cast the operand to f32"
546 );
547 assert_eq!(issue.trace_event().phase, ValidationPhase::Type);
548
549 let diagnostic = issue.diagnostic();
550 assert_eq!(diagnostic.code.as_str(), "V028");
551 assert_eq!(diagnostic.stage, DiagnosticStage::Validate);
552 assert_eq!(diagnostic.retry, RetryClass::Never);
553 assert_eq!(
554 diagnostic
555 .location
556 .as_ref()
557 .and_then(|location| location.graph_node),
558 Some(7)
559 );
560 assert_eq!(
561 diagnostic.suggested_fix.as_deref(),
562 Some("cast the operand to f32")
563 );
564 assert_eq!(
565 diagnostic.cause.as_ref().map(|cause| cause.kind.as_str()),
566 Some("type")
567 );
568 }
569
570 #[test]
571 fn serialization_preserves_every_issue_field() {
572 let issue = issue();
573 let encoded = serde_json::to_vec(&issue).expect("validation issue must serialize");
574 let decoded: ValidationError =
575 serde_json::from_slice(&encoded).expect("validation issue must deserialize");
576 assert_eq!(decoded, issue);
577 assert_eq!(decoded.diagnostic(), issue.diagnostic());
578 }
579
580 #[test]
581 fn deserialization_rejects_unknown_rule_identity() {
582 let encoded = serde_json::to_value(issue()).expect("issue must serialize");
583 let mut mutated = encoded;
584 mutated["code"] = serde_json::Value::String(format!("V{}", 999));
585 let error = serde_json::from_value::<ValidationError>(mutated)
586 .expect_err("unknown validation rule must fail closed");
587 assert!(error.to_string().contains("unknown validation code"));
588 }
589
590 #[test]
591 fn deserialization_rejects_phase_mutation() {
592 let encoded = serde_json::to_value(issue()).expect("issue must serialize");
593 let mut mutated = encoded;
594 mutated["phase"] = serde_json::Value::String("node".to_string());
595 let error = serde_json::from_value::<ValidationError>(mutated)
596 .expect_err("phase mutation must fail closed");
597 assert!(error.to_string().contains("belongs to phase"));
598 }
599
600 #[test]
601 fn unsupported_op_has_typed_capability_identity() {
602 let issue = ValidationError::unsupported_op("backend-a", &Arc::from("math::fma"), 3);
603 assert_eq!(issue.code().as_str(), "V056");
604 assert_eq!(issue.phase(), ValidationPhase::Capability);
605 assert!(issue.message().contains("backend-a"));
606 assert!(issue.message().contains("math::fma"));
607 assert!(issue.message().contains("3"));
608 assert!(issue.message().contains("Fix:"));
609 }
610}