Skip to main content

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}