ascii_dag/errors.rs
1//! Structured error codes and diagnostics following the Waddling Diagnostic
2//! Protocol (WDP) Level 0.
3//!
4//! Error codes follow the format `Severity.Component.Primary.Sequence`:
5//!
6//! ```text
7//! E.ArenaLayout.Node.004
8//! │ │ │ └── Sequence: 004 = OVERFLOW (WDP §6 convention)
9//! │ │ └────── Primary: Node (failure domain)
10//! │ └──────────────── Component: ArenaLayout (arena-based layout)
11//! └────────────────── Severity: E (error, blocking)
12//! ```
13//!
14//! ## WDP Part References
15//!
16//! | Part | Spec | Our tokens |
17//! |------|------|------------|
18//! | 1 — Severity | `1-SEVERITY.md` | `E` (Error, blocking), `W` (Warning, non-blocking) |
19//! | 2 — Component | `2-COMPONENT.md` | `Graph` (construction/validation), `ArenaLayout` (arena-based layout), `Arena` (arena allocator, reserved), `Layout` (heap/std layout, reserved) |
20//! | 3 — Primary | `3-PRIMARY.md` | `Node`, `Edge`, `Dag`, `Subgraph`, `Alloc`, `Builder`, `Level` |
21//! | 4 — Sequence | `4-SEQUENCE.md` + `6-SEQUENCE-CONVENTIONS.md` | See table below |
22//!
23//! ## Sequence Conventions Used (WDP §6 §4.1–§4.3)
24//!
25//! | Seq | Name | WDP meaning | Our usage |
26//! |-----|------|-------------|-----------|
27//! | 001 | MISSING | Required data not provided | `EmptyGraph` — no nodes in graph |
28//! | 003 | INVALID | Validation check failed | `CycleDetected`, `SubgraphCycle` |
29//! | 004 | OVERFLOW | Value too large | `ExceedsMaxNodes`, `ExceedsMaxLevels` |
30//! | 021 | NOT_FOUND | Referenced element not found | `NodeNotFound`, `SubgraphNotFound` |
31//! | 026 | EXHAUSTED | Resource exhausted | `ArenaOom`, `BuilderFailed` |
32//!
33//! All codes are composed from named macro building blocks at compile time via
34//! `wdp!`. Any unrecognised token causes a compile error.
35
36#[cfg(feature = "alloc")]
37use alloc::boxed::Box;
38use core::fmt;
39
40// ── WDP code composition ────────────────────────────────────────────────
41//
42// Each axis is a macro that maps named tokens to string literals.
43// `wdp!` threads them through `concat!` — zero-cost, zero-allocation.
44//
45// Adding a typo like `wdp!(E.Graph.Nod.003)` fails at compile time because
46// `primary!(Nod)` has no matching arm.
47
48/// Maps severity tokens to their WDP string (Part 1).
49///
50/// - `E` — Error: operation failed, needs attention (blocking)
51/// - `W` — Warning: potential issue, operation continues (non-blocking)
52macro_rules! severity {
53 (E) => {
54 "E"
55 };
56 (W) => {
57 "W"
58 };
59}
60
61/// Maps component tokens to their WDP string (Part 2).
62///
63/// - `Graph` — graph construction and validation (allocation-agnostic)
64/// - `ArenaLayout` — arena-based Sugiyama layout
65/// - `Arena` — arena allocator itself (reserved for future)
66/// - `Layout` — heap/standard layout (reserved for future)
67macro_rules! component {
68 (Graph) => {
69 "Graph"
70 };
71 (ArenaLayout) => {
72 "ArenaLayout"
73 };
74 (Arena) => {
75 "Arena"
76 };
77 (Layout) => {
78 "Layout"
79 };
80}
81
82/// Maps primary tokens to their WDP string (Part 3).
83///
84/// Each primary represents a failure domain within its component:
85///
86/// **Under `Graph`:**
87/// - `Node` — node existence / data issues
88/// - `Edge` — edge existence / data issues
89/// - `Dag` — acyclicity / DAG constraint issues
90/// - `Subgraph` — subgraph / cluster issues
91///
92/// **Under `ArenaLayout`:**
93/// - `Alloc` — arena allocator issues (OOM, capacity)
94/// - `Builder` — IR builder issues
95/// - `Node` — node-count capacity issues (index type overflow)
96/// - `Level` — level-depth capacity issues
97macro_rules! primary {
98 (Node) => {
99 "Node"
100 };
101 (Edge) => {
102 "Edge"
103 };
104 (Dag) => {
105 "Dag"
106 };
107 (Subgraph) => {
108 "Subgraph"
109 };
110 (Alloc) => {
111 "Alloc"
112 };
113 (Builder) => {
114 "Builder"
115 };
116 (Level) => {
117 "Level"
118 };
119}
120
121/// Maps sequence tokens to their WDP numeric code (Part 4).
122///
123/// These follow the conventions in `6-SEQUENCE-CONVENTIONS.md §4`:
124///
125/// | Seq | Name | Category | WDP meaning |
126/// |-----|------|----------|-------------|
127/// | 001 | MISSING | Input/Data | Required data not provided |
128/// | 002 | MISMATCH | Input/Data | Type or length mismatch |
129/// | 003 | INVALID | Input/Data | Validation check failed |
130/// | 004 | OVERFLOW | Input/Data | Value too large / size exceeded |
131/// | 007 | DUPLICATE | Input/Data | Duplicate entry |
132/// | 009 | UNSUPPORTED | Input/Data | Feature not supported |
133/// | 021 | NOT_FOUND | Resource | Referenced element not found |
134/// | 026 | EXHAUSTED | Resource | Resource exhausted (OOM, capacity) |
135macro_rules! sequence {
136 (MISSING) => {
137 "001"
138 };
139 (MISMATCH) => {
140 "002"
141 };
142 (INVALID) => {
143 "003"
144 };
145 (OVERFLOW) => {
146 "004"
147 };
148 (DUPLICATE) => {
149 "007"
150 };
151 (UNSUPPORTED) => {
152 "009"
153 };
154 (NOT_FOUND) => {
155 "021"
156 };
157 (EXHAUSTED) => {
158 "026"
159 };
160}
161
162/// Compose a WDP Level 0 error code from four named tokens.
163///
164/// Unrecognised tokens cause a compile error — no typo can slip through.
165///
166/// ```ignore
167/// const CODE: &str = wdp!(E.Graph.Dag.INVALID);
168/// assert_eq!(CODE, "E.Graph.Dag.003");
169/// ```
170macro_rules! wdp {
171 ($sev:ident . $comp:ident . $pri:ident . $seq:ident) => {
172 concat!(
173 severity!($sev),
174 ".",
175 component!($comp),
176 ".",
177 primary!($pri),
178 ".",
179 sequence!($seq)
180 )
181 };
182}
183
184// ── Composed error codes ────────────────────────────────────────────────
185//
186// Each constant is unique — no two share the same Component.Primary.Sequence.
187//
188// Graph component:
189// Graph.Node.001 EmptyGraph (MISSING)
190// Graph.Node.021 NodeNotFound (NOT_FOUND)
191// Graph.Dag.003 CycleDetected (INVALID)
192// Graph.Subgraph.003 SubgraphCycle (INVALID)
193// Graph.Subgraph.021 SubgraphNotFound (NOT_FOUND)
194//
195// ArenaLayout component:
196// ArenaLayout.Alloc.026 ArenaOom (EXHAUSTED)
197// ArenaLayout.Builder.026 BuilderFailed (EXHAUSTED)
198// ArenaLayout.Node.004 ExceedsMaxNodes (OVERFLOW)
199// ArenaLayout.Level.004 ExceedsMaxLevels (OVERFLOW)
200
201/// Graph is empty — no nodes present.
202///
203/// `E.Graph.Node.001` — Sequence 001 = MISSING: "required data not provided."
204pub const EMPTY_GRAPH: &str = wdp!(E.Graph.Node.MISSING);
205
206/// Referenced node ID does not exist in the graph.
207///
208/// `E.Graph.Node.021` — Sequence 021 = NOT_FOUND: "referenced element not found."
209pub const NODE_NOT_FOUND: &str = wdp!(E.Graph.Node.NOT_FOUND);
210
211/// Cycle detected in a graph that requires acyclicity.
212///
213/// `E.Graph.Dag.003` — Sequence 003 = INVALID: "validation check failed"
214/// (the DAG constraint is violated).
215pub const CYCLE_DETECTED: &str = wdp!(E.Graph.Dag.INVALID);
216
217/// Subgraph nesting would create a cycle in the hierarchy.
218///
219/// `E.Graph.Subgraph.003` — Sequence 003 = INVALID.
220pub const SUBGRAPH_CYCLE: &str = wdp!(E.Graph.Subgraph.INVALID);
221
222/// Referenced subgraph ID does not exist.
223///
224/// `E.Graph.Subgraph.021` — Sequence 021 = NOT_FOUND.
225pub const SUBGRAPH_NOT_FOUND: &str = wdp!(E.Graph.Subgraph.NOT_FOUND);
226
227/// Arena allocator ran out of memory.
228///
229/// `E.ArenaLayout.Alloc.026` — Sequence 026 = EXHAUSTED: "resource exhausted."
230pub const ARENA_OOM: &str = wdp!(E.ArenaLayout.Alloc.EXHAUSTED);
231
232/// IR builder failed to allocate output structures.
233///
234/// `E.ArenaLayout.Builder.026` — Sequence 026 = EXHAUSTED.
235pub const BUILDER_FAILED: &str = wdp!(E.ArenaLayout.Builder.EXHAUSTED);
236
237/// Node/edge count exceeds the index type's capacity.
238///
239/// `E.ArenaLayout.Node.004` — Sequence 004 = OVERFLOW: "value too large / size exceeded."
240pub const EXCEEDS_MAX_NODES: &str = wdp!(E.ArenaLayout.Node.OVERFLOW);
241
242/// Graph depth exceeds the maximum supported levels.
243///
244/// `E.ArenaLayout.Level.004` — Sequence 004 = OVERFLOW.
245pub const EXCEEDS_MAX_LEVELS: &str = wdp!(E.ArenaLayout.Level.OVERFLOW);
246
247// ── GraphError ──────────────────────────────────────────────────────────
248
249/// Unified error type for all graph and layout operations.
250///
251/// Each variant carries a WDP error code accessible via [`GraphError::code()`],
252/// and an actionable hint via [`GraphError::hint()`].
253///
254/// # WDP Code Mapping
255///
256/// | Variant | WDP Code | Meaning |
257/// |---------|----------|---------|
258/// | `EmptyGraph` | `E.Graph.Node.001` | MISSING — no nodes |
259/// | `NodeNotFound` | `E.Graph.Node.021` | NOT_FOUND — node absent |
260/// | `CycleDetected` | `E.Graph.Dag.003` | INVALID — DAG constraint violated |
261/// | `SubgraphCycle` | `E.Graph.Subgraph.003` | INVALID — nesting cycle |
262/// | `SubgraphNotFound` | `E.Graph.Subgraph.021` | NOT_FOUND — subgraph absent |
263/// | `ArenaOom` | `E.ArenaLayout.Alloc.026` | EXHAUSTED — arena memory |
264/// | `BuilderFailed` | `E.ArenaLayout.Builder.026` | EXHAUSTED — builder alloc |
265/// | `ExceedsMaxNodes` | `E.ArenaLayout.Node.004` | OVERFLOW — index type full |
266/// | `ExceedsMaxLevels` | `E.ArenaLayout.Level.004` | OVERFLOW — too deep |
267///
268/// # Examples
269///
270/// ```
271/// use ascii_dag::GraphError;
272///
273/// let err = GraphError::EmptyGraph;
274/// assert_eq!(err.code(), "E.Graph.Node.001");
275/// assert!(!err.hint().is_empty());
276/// ```
277#[derive(Debug, Clone, PartialEq, Eq)]
278pub enum GraphError {
279 /// The graph has no nodes.
280 ///
281 /// **WDP:** `E.Graph.Node.001` (MISSING)
282 EmptyGraph,
283
284 /// A referenced node ID does not exist in the graph.
285 ///
286 /// **WDP:** `E.Graph.Node.021` (NOT_FOUND)
287 NodeNotFound(usize),
288
289 /// The graph contains a cycle but acyclicity was required.
290 ///
291 /// **WDP:** `E.Graph.Dag.003` (INVALID)
292 CycleDetected,
293
294 /// Subgraph nesting would create a cycle in the hierarchy.
295 ///
296 /// **WDP:** `E.Graph.Subgraph.003` (INVALID)
297 SubgraphCycle,
298
299 /// A referenced subgraph ID does not exist.
300 ///
301 /// **WDP:** `E.Graph.Subgraph.021` (NOT_FOUND)
302 SubgraphNotFound(usize),
303
304 /// The arena allocator ran out of memory.
305 ///
306 /// **WDP:** `E.ArenaLayout.Alloc.026` (EXHAUSTED)
307 ///
308 /// Use `Graph::estimate_layout_arena_size()` to compute the required size.
309 ArenaOom,
310
311 /// The IR builder failed to allocate output structures.
312 ///
313 /// **WDP:** `E.ArenaLayout.Builder.026` (EXHAUSTED)
314 BuilderFailed,
315
316 /// The graph has more nodes or edges than the selected index type supports.
317 ///
318 /// **WDP:** `E.ArenaLayout.Node.004` (OVERFLOW)
319 ExceedsMaxNodes {
320 /// Actual node or edge count.
321 count: usize,
322 /// Maximum supported by the current index type.
323 max: usize,
324 },
325
326 /// The graph's longest path exceeds the maximum level depth.
327 ///
328 /// **WDP:** `E.ArenaLayout.Level.004` (OVERFLOW)
329 ExceedsMaxLevels {
330 /// Actual depth of the longest path.
331 depth: usize,
332 /// Maximum supported levels.
333 max: usize,
334 },
335}
336
337impl GraphError {
338 /// WDP Level 0 error code.
339 ///
340 /// Format: `Severity.Component.Primary.Sequence`
341 ///
342 /// Every variant maps to a unique code — no two variants share the same
343 /// `Component.Primary.Sequence` triple.
344 #[inline]
345 pub fn code(&self) -> &'static str {
346 match self {
347 Self::EmptyGraph => EMPTY_GRAPH,
348 Self::NodeNotFound(_) => NODE_NOT_FOUND,
349 Self::CycleDetected => CYCLE_DETECTED,
350 Self::SubgraphCycle => SUBGRAPH_CYCLE,
351 Self::SubgraphNotFound(_) => SUBGRAPH_NOT_FOUND,
352 Self::ArenaOom => ARENA_OOM,
353 Self::BuilderFailed => BUILDER_FAILED,
354 Self::ExceedsMaxNodes { .. } => EXCEEDS_MAX_NODES,
355 Self::ExceedsMaxLevels { .. } => EXCEEDS_MAX_LEVELS,
356 }
357 }
358
359 /// Actionable hint for resolving this error.
360 #[inline]
361 pub fn hint(&self) -> &'static str {
362 match self {
363 Self::EmptyGraph => "Add at least one node before computing layout",
364 Self::NodeNotFound(_) => "Call add_node() before referencing this node ID",
365 Self::CycleDetected => "Enable cycle breaking or remove the back edge",
366 Self::SubgraphCycle => {
367 "A subgraph cannot be nested inside itself or its own descendant"
368 }
369 Self::SubgraphNotFound(_) => "Call add_subgraph() before referencing this subgraph ID",
370 Self::ArenaOom => "Increase the arena buffer size; use estimate_layout_arena_size()",
371 Self::BuilderFailed => "Increase the output arena buffer size",
372 Self::ExceedsMaxNodes { .. } => {
373 "Use a larger index type (arena-idx-u32) or reduce graph size"
374 }
375 Self::ExceedsMaxLevels { .. } => {
376 "Reduce chain depth or use a different layout strategy"
377 }
378 }
379 }
380}
381
382impl fmt::Display for GraphError {
383 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
384 write!(f, "[{}] ", self.code())?;
385 match self {
386 Self::EmptyGraph => write!(f, "graph has no nodes"),
387 Self::NodeNotFound(id) => write!(f, "node {} not found", id),
388 Self::CycleDetected => write!(f, "cycle detected in graph"),
389 Self::SubgraphCycle => write!(f, "subgraph nesting would create a cycle"),
390 Self::SubgraphNotFound(id) => write!(f, "subgraph {} not found", id),
391 Self::ArenaOom => write!(f, "arena out of memory"),
392 Self::BuilderFailed => write!(f, "IR builder allocation failed"),
393 Self::ExceedsMaxNodes { count, max } => {
394 write!(
395 f,
396 "node/edge count {} exceeds index-type max {}",
397 count, max
398 )
399 }
400 Self::ExceedsMaxLevels { depth, max } => {
401 write!(f, "graph depth {} exceeds max levels {}", depth, max)
402 }
403 }
404 }
405}
406
407#[cfg(feature = "std")]
408impl std::error::Error for GraphError {}
409
410// ── Diagnostic (error + causal chain) ───────────────────────────────────
411
412#[cfg(feature = "alloc")]
413/// A diagnostic pairs a [`GraphError`] with an optional causal chain.
414///
415/// Both the outer error and every inner cause carry their own WDP code,
416/// forming a chain that provides context (what failed) at each level:
417///
418/// ```text
419/// [E.ArenaLayout.Builder.026] IR builder allocation failed
420/// caused by: [E.ArenaLayout.Alloc.026] arena out of memory
421/// ```
422///
423/// # Construction
424///
425/// ```
426/// use ascii_dag::errors::{GraphError, Diagnostic};
427///
428/// // Leaf diagnostic (no cause)
429/// let d = Diagnostic::from(GraphError::ArenaOom);
430/// assert_eq!(d.code(), "E.ArenaLayout.Alloc.026");
431/// assert!(d.cause().is_none());
432///
433/// // Chained diagnostic
434/// let d = Diagnostic::from(GraphError::BuilderFailed)
435/// .caused_by(GraphError::ArenaOom);
436/// assert_eq!(d.code(), "E.ArenaLayout.Builder.026");
437/// assert_eq!(d.cause().unwrap().code(), "E.ArenaLayout.Alloc.026");
438/// ```
439///
440/// # Display
441///
442/// `Display` renders the full chain, indenting each cause level:
443///
444/// ```
445/// use ascii_dag::errors::{GraphError, Diagnostic};
446///
447/// let d = Diagnostic::from(GraphError::BuilderFailed)
448/// .caused_by(GraphError::ArenaOom);
449/// let msg = d.to_string();
450/// assert!(msg.contains("caused by:"));
451/// ```
452#[derive(Debug, Clone, PartialEq, Eq)]
453pub struct Diagnostic {
454 /// The error at this level.
455 error: GraphError,
456 /// Optional inner cause (each cause is itself a full `Diagnostic`).
457 cause: Option<Box<Diagnostic>>,
458}
459
460#[cfg(feature = "alloc")]
461impl Diagnostic {
462 /// Create a leaf diagnostic (no cause).
463 #[inline]
464 pub fn new(error: GraphError) -> Self {
465 Self { error, cause: None }
466 }
467
468 /// Attach a cause to this diagnostic (builder pattern).
469 ///
470 /// The cause is wrapped in its own `Diagnostic` with no further inner
471 /// cause. For deeper chains, use [`caused_by_diagnostic`](Self::caused_by_diagnostic).
472 #[inline]
473 pub fn caused_by(self, cause: GraphError) -> Self {
474 self.caused_by_diagnostic(Diagnostic::new(cause))
475 }
476
477 /// Attach an existing `Diagnostic` as the cause.
478 #[inline]
479 pub fn caused_by_diagnostic(mut self, cause: Diagnostic) -> Self {
480 self.cause = Some(Box::new(cause));
481 self
482 }
483
484 /// The [`GraphError`] at this level of the chain.
485 #[inline]
486 pub fn error(&self) -> &GraphError {
487 &self.error
488 }
489
490 /// WDP code of the outermost error.
491 #[inline]
492 pub fn code(&self) -> &'static str {
493 self.error.code()
494 }
495
496 /// Hint for the outermost error.
497 #[inline]
498 pub fn hint(&self) -> &'static str {
499 self.error.hint()
500 }
501
502 /// The inner cause, if any.
503 #[inline]
504 pub fn cause(&self) -> Option<&Diagnostic> {
505 self.cause.as_deref()
506 }
507
508 /// Iterate over the full chain: self, then cause, then cause's cause, etc.
509 pub fn chain(&self) -> DiagnosticChain<'_> {
510 DiagnosticChain {
511 current: Some(self),
512 }
513 }
514}
515
516#[cfg(feature = "alloc")]
517impl From<GraphError> for Diagnostic {
518 #[inline]
519 fn from(error: GraphError) -> Self {
520 Self::new(error)
521 }
522}
523
524#[cfg(feature = "alloc")]
525impl fmt::Display for Diagnostic {
526 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
527 // Outer error
528 fmt::Display::fmt(&self.error, f)?;
529
530 // Walk the cause chain, indenting each level
531 let mut depth = 1;
532 let mut current = self.cause.as_deref();
533 while let Some(diag) = current {
534 writeln!(f)?;
535 for _ in 0..depth {
536 write!(f, " ")?;
537 }
538 write!(f, "caused by: {}", diag.error)?;
539 current = diag.cause.as_deref();
540 depth += 1;
541 }
542 Ok(())
543 }
544}
545
546#[cfg(all(feature = "alloc", feature = "std"))]
547impl std::error::Error for Diagnostic {
548 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
549 self.cause.as_deref().map(|d| d as &dyn std::error::Error)
550 }
551}
552
553/// Iterator over the [`Diagnostic`] chain (outer → innermost cause).
554#[cfg(feature = "alloc")]
555pub struct DiagnosticChain<'a> {
556 current: Option<&'a Diagnostic>,
557}
558
559#[cfg(feature = "alloc")]
560impl<'a> Iterator for DiagnosticChain<'a> {
561 type Item = &'a Diagnostic;
562
563 fn next(&mut self) -> Option<Self::Item> {
564 let diag = self.current?;
565 self.current = diag.cause.as_deref();
566 Some(diag)
567 }
568}