Skip to main content

ascii_dag/
errors.rs

1//! Structured error codes and diagnostics following the Waddling ErrorChain
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`, `Plan`, `Canvas`, `Sink`, `Label` |
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`, `ExceedsMaxExtent` |
30//! | 021 | NOT_FOUND | Referenced element not found | `NodeNotFound`, `SubgraphNotFound` |
31//! | 026 | EXHAUSTED | Resource exhausted | `ArenaOom`, `BuilderFailed` |
32//! | 031 | INVISIBLE | Output element will not be rendered | `WARN_LABEL_INVISIBLE` (crate extension) |
33//! | 033 | EXCESSIVE | Value kept but past its useful range | `WARN_CONFIG_EXCESSIVE` (crate extension) |
34//! | 034 | DEFERRED | Declared intent not honored in this release | `WARN_PORT_ON_SELF_LOOP` (crate extension) |
35//! | 035 | UNROUTABLE | Declared placement could not be honored | `WARN_PORT_UNROUTABLE` (crate extension) |
36//!
37//! All codes are composed from named macro building blocks at compile time via
38//! `wdp!`. Any unrecognised token causes a compile error.
39
40#[cfg(feature = "alloc")]
41use alloc::boxed::Box;
42use core::fmt;
43
44// ── WDP code composition ────────────────────────────────────────────────
45//
46// Each axis is a macro that maps named tokens to string literals.
47// `wdp!` threads them through `concat!` — zero-cost, zero-allocation.
48//
49// Adding a typo like `wdp!(E.Graph.Nod.003)` fails at compile time because
50// `primary!(Nod)` has no matching arm.
51
52/// Maps severity tokens to their WDP string (Part 1).
53///
54/// - `E` — Error: operation failed, needs attention (blocking)
55/// - `W` — Warning: potential issue, operation continues (non-blocking)
56macro_rules! severity {
57    (E) => {
58        "E"
59    };
60    (W) => {
61        "W"
62    };
63}
64
65/// Maps component tokens to their WDP string (Part 2).
66///
67/// - `Graph`       — graph construction and validation (allocation-agnostic)
68/// - `ArenaLayout` — arena-based Sugiyama layout
69/// - `Render`      — unified render engine (plan / canvas / sink)
70/// - `Arena`       — arena allocator itself (reserved for future)
71/// - `Layout`      — heap/std layout (reserved for future)
72macro_rules! component {
73    (Graph) => {
74        "Graph"
75    };
76    (ArenaLayout) => {
77        "ArenaLayout"
78    };
79    (Render) => {
80        "Render"
81    };
82    (Arena) => {
83        "Arena"
84    };
85    (Layout) => {
86        "Layout"
87    };
88}
89
90/// Maps primary tokens to their WDP string (Part 3).
91///
92/// Each primary represents a failure domain within its component:
93///
94/// **Under `Graph`:**
95/// - `Node`     — node existence / data issues
96/// - `Edge`     — edge existence / data issues
97/// - `Dag`      — acyclicity / DAG constraint issues
98/// - `Subgraph` — subgraph / cluster issues
99///
100/// **Under `ArenaLayout`:**
101/// - `Alloc`   — arena allocator issues (OOM, capacity)
102/// - `Builder` — IR builder issues
103/// - `Node`    — node-count capacity issues (index type overflow)
104/// - `Level`   — level-depth capacity issues
105/// - `Extent`  — cross-axis extent past the coordinate type
106///
107/// **Under `Graph`, diagnostics:**
108/// - `Port`    — declared edge attachment sides
109///
110/// **Under `Render`:**
111/// - `Plan`   — render-plan build issues (caller plan buffer/arena)
112/// - `Canvas` — band canvas issues (caller cell/color buffers)
113/// - `Sink`   — output sink issues (caller byte buffer)
114/// - `Workspace` — composer/planner workspace issues (caller chunk)
115/// - `Label`  — edge-label placement diagnostics
116macro_rules! primary {
117    (Node) => {
118        "Node"
119    };
120    (Edge) => {
121        "Edge"
122    };
123    (Dag) => {
124        "Dag"
125    };
126    (Subgraph) => {
127        "Subgraph"
128    };
129    (Alloc) => {
130        "Alloc"
131    };
132    (Builder) => {
133        "Builder"
134    };
135    (Level) => {
136        "Level"
137    };
138    (Extent) => {
139        "Extent"
140    };
141    (Port) => {
142        "Port"
143    };
144    (Label) => {
145        "Label"
146    };
147    (Plan) => {
148        "Plan"
149    };
150    (Canvas) => {
151        "Canvas"
152    };
153    (Workspace) => {
154        "Workspace"
155    };
156    (Sink) => {
157        "Sink"
158    };
159}
160
161/// Maps sequence tokens to their WDP numeric code (Part 4).
162///
163/// These follow the conventions in `6-SEQUENCE-CONVENTIONS.md §4`:
164///
165/// | Seq | Name | Category | WDP meaning |
166/// |-----|------|----------|-------------|
167/// | 001 | MISSING | Input/Data | Required data not provided |
168/// | 002 | MISMATCH | Input/Data | Type or length mismatch |
169/// | 003 | INVALID | Input/Data | Validation check failed |
170/// | 004 | OVERFLOW | Input/Data | Value too large / size exceeded |
171/// | 007 | DUPLICATE | Input/Data | Duplicate entry |
172/// | 009 | UNSUPPORTED | Input/Data | Feature not supported |
173/// | 021 | NOT_FOUND | Resource | Referenced element not found |
174/// | 026 | EXHAUSTED | Resource | Resource exhausted (OOM, capacity) |
175/// | 031 | INVISIBLE | Output | Element will not be rendered (crate extension) |
176/// | 032 | FAILED | Output | Downstream sink reported failure (crate extension) |
177/// | 033 | EXCESSIVE | Config | Value kept but past its useful range (crate extension) |
178/// | 034 | DEFERRED | Intent | Declared intent not honored in this release (crate extension) |
179/// | 035 | UNROUTABLE | Placement | Declared placement could not be honored (crate extension) |
180macro_rules! sequence {
181    (MISSING) => {
182        "001"
183    };
184    (MISMATCH) => {
185        "002"
186    };
187    (INVALID) => {
188        "003"
189    };
190    (OVERFLOW) => {
191        "004"
192    };
193    (DUPLICATE) => {
194        "007"
195    };
196    (UNSUPPORTED) => {
197        "009"
198    };
199    (NOT_FOUND) => {
200        "021"
201    };
202    (EXHAUSTED) => {
203        "026"
204    };
205    (INVISIBLE) => {
206        "031"
207    };
208    (FAILED) => {
209        "032"
210    };
211    (EXCESSIVE) => {
212        "033"
213    };
214    (DEFERRED) => {
215        "034"
216    };
217    (UNROUTABLE) => {
218        "035"
219    };
220}
221
222/// Compose a WDP Level 0 error code from four named tokens.
223///
224/// Unrecognised tokens cause a compile error — no typo can slip through.
225///
226/// ```ignore
227/// const CODE: &str = wdp!(E.Graph.Dag.INVALID);
228/// assert_eq!(CODE, "E.Graph.Dag.003");
229/// ```
230macro_rules! wdp {
231    ($sev:ident . $comp:ident . $pri:ident . $seq:ident) => {
232        concat!(
233            severity!($sev),
234            ".",
235            component!($comp),
236            ".",
237            primary!($pri),
238            ".",
239            sequence!($seq)
240        )
241    };
242}
243
244// ── Composed error codes ────────────────────────────────────────────────
245//
246// Each constant is unique — no two share the same Component.Primary.Sequence.
247//
248// Graph component:
249//   Graph.Node.001      EmptyGraph        (MISSING)
250//   Graph.Node.021      NodeNotFound      (NOT_FOUND)
251//   Graph.Dag.003       CycleDetected     (INVALID)
252//   Graph.Subgraph.003  SubgraphCycle     (INVALID)
253//   Graph.Subgraph.021  SubgraphNotFound  (NOT_FOUND)
254//
255// ArenaLayout component:
256//   ArenaLayout.Alloc.026     ArenaOom          (EXHAUSTED)
257//   ArenaLayout.Builder.026   BuilderFailed     (EXHAUSTED)
258//   ArenaLayout.Node.004      ExceedsMaxNodes   (OVERFLOW)
259//   ArenaLayout.Level.004     ExceedsMaxLevels  (OVERFLOW)
260//   ArenaLayout.Extent.004    ExceedsMaxExtent  (OVERFLOW)
261//
262// Render component:
263//   Render.Plan.026    RenderPlanOom         (EXHAUSTED)
264//   Render.Canvas.026  RenderCanvasTooSmall  (EXHAUSTED)
265//   Render.Sink.026    RenderOutputTooSmall  (EXHAUSTED)
266//   Render.Workspace.026  RenderWorkspaceTooSmall (EXHAUSTED)
267//   Render.Sink.032    RenderSinkFailed      (FAILED)
268
269/// Graph is empty — no nodes present.
270///
271/// `E.Graph.Node.001` — Sequence 001 = MISSING: "required data not provided."
272pub const EMPTY_GRAPH: &str = wdp!(E.Graph.Node.MISSING);
273
274/// Referenced node ID does not exist in the graph.
275///
276/// `E.Graph.Node.021` — Sequence 021 = NOT_FOUND: "referenced element not found."
277pub const NODE_NOT_FOUND: &str = wdp!(E.Graph.Node.NOT_FOUND);
278
279/// Cycle detected in a graph that requires acyclicity.
280///
281/// `E.Graph.Dag.003` — Sequence 003 = INVALID: "validation check failed"
282/// (the DAG constraint is violated).
283pub const CYCLE_DETECTED: &str = wdp!(E.Graph.Dag.INVALID);
284
285/// Subgraph nesting would create a cycle in the hierarchy.
286///
287/// `E.Graph.Subgraph.003` — Sequence 003 = INVALID.
288pub const SUBGRAPH_CYCLE: &str = wdp!(E.Graph.Subgraph.INVALID);
289
290/// Referenced subgraph ID does not exist.
291///
292/// `E.Graph.Subgraph.021` — Sequence 021 = NOT_FOUND.
293pub const SUBGRAPH_NOT_FOUND: &str = wdp!(E.Graph.Subgraph.NOT_FOUND);
294
295/// Arena allocator ran out of memory.
296///
297/// `E.ArenaLayout.Alloc.026` — Sequence 026 = EXHAUSTED: "resource exhausted."
298pub const ARENA_OOM: &str = wdp!(E.ArenaLayout.Alloc.EXHAUSTED);
299
300/// IR builder failed to allocate output structures.
301///
302/// `E.ArenaLayout.Builder.026` — Sequence 026 = EXHAUSTED.
303pub const BUILDER_FAILED: &str = wdp!(E.ArenaLayout.Builder.EXHAUSTED);
304
305/// Node/edge count exceeds the index type's capacity.
306///
307/// `E.ArenaLayout.Node.004` — Sequence 004 = OVERFLOW: "value too large / size exceeded."
308pub const EXCEEDS_MAX_NODES: &str = wdp!(E.ArenaLayout.Node.OVERFLOW);
309
310/// Graph depth exceeds the maximum supported levels.
311///
312/// `E.ArenaLayout.Level.004` — Sequence 004 = OVERFLOW.
313pub const EXCEEDS_MAX_LEVELS: &str = wdp!(E.ArenaLayout.Level.OVERFLOW);
314
315/// The layout's cross-axis extent exceeds the arena coordinate type.
316///
317/// `E.ArenaLayout.Extent.004` — Sequence 004 = OVERFLOW.
318pub const EXCEEDS_MAX_EXTENT: &str = wdp!(E.ArenaLayout.Extent.OVERFLOW);
319
320/// Caller-provided render-plan buffer/arena is too small (no_std path).
321///
322/// `E.Render.Plan.026` — Sequence 026 = EXHAUSTED: "resource exhausted."
323pub const RENDER_PLAN_OOM: &str = wdp!(E.Render.Plan.EXHAUSTED);
324
325/// Caller-provided band buffer is smaller than `width × band_rows`.
326///
327/// `E.Render.Canvas.026` — Sequence 026 = EXHAUSTED.
328pub const RENDER_CANVAS_TOO_SMALL: &str = wdp!(E.Render.Canvas.EXHAUSTED);
329
330/// Caller-provided output byte buffer is too small for the render.
331///
332/// `E.Render.Sink.026` — Sequence 026 = EXHAUSTED.
333pub const RENDER_OUTPUT_TOO_SMALL: &str = wdp!(E.Render.Sink.EXHAUSTED);
334
335/// The caller-provided composer/planner workspace is too small.
336pub const RENDER_WORKSPACE_TOO_SMALL: &str = wdp!(E.Render.Workspace.EXHAUSTED);
337
338/// The caller's output sink reported a write failure.
339pub const RENDER_SINK_FAILED: &str = wdp!(E.Render.Sink.FAILED);
340
341// ── Warning codes (severity W — non-blocking, WDP Part 1) ───────────────
342//
343// Warnings share the error codes' Component.Primary.Sequence space:
344//   Graph.Node.021  NodeAutoCreated  (NOT_FOUND — referenced, so created)
345//   Graph.Node.007  AutoReplaced     (DUPLICATE — AUTO-involved replace)
346//   Graph.Dag.003   ConfigClamped    (INVALID — config value out of range)
347
348/// An edge referenced a node that was never added; a placeholder was
349/// auto-created (rendered as `⟨⟩` until explicitly defined).
350///
351/// `W.Graph.Node.021` — Sequence 021 = NOT_FOUND, severity W =
352/// non-blocking warning, emitted into the diagnostics channel under
353/// the implicit missing-node policy.
354pub const WARN_NODE_AUTO_CREATED: &str = wdp!(W.Graph.Node.NOT_FOUND);
355
356/// A layout-configuration value was outside its valid range and was
357/// CLAMPED (e.g. an absurd `crossing_reduction_passes`, likely a
358/// negative value cast to `usize`).
359///
360/// `W.Graph.Dag.003` — Sequence 003 = INVALID. A condition code: the
361/// setter records the note against the current configuration, and
362/// every diagnostic-aware layout run reports it until the value is
363/// fixed. (The 0.10 `W.Graph.Node.007` duplicate-replacement stderr
364/// warning has no diagnostic successor — that point event is
365/// delivered by the `NodeInsertion` receipt at the call site.)
366pub const WARN_CONFIG_CLAMPED: &str = wdp!(W.Graph.Dag.INVALID);
367
368/// A layout-configuration value is unreasonably high but was KEPT
369/// (e.g. `crossing_reduction_passes` past diminishing returns) — a
370/// distinct condition from a clamp, under a distinct code, so the
371/// `(code, subject)` identity never collapses the two.
372///
373/// `W.Graph.Dag.033` — Sequence 033 = EXCESSIVE (crate extension,
374/// like 031/032). A condition code, reported per run until fixed.
375pub const WARN_CONFIG_EXCESSIVE: &str = wdp!(W.Graph.Dag.EXCESSIVE);
376
377/// A declared side on a self-loop is not honored in this release: the
378/// loop keeps its marker cell.
379///
380/// `W.Graph.Port.034` — Sequence 034 = DEFERRED (crate extension). A
381/// condition code, reported per run until fixed.
382pub const WARN_PORT_ON_SELF_LOOP: &str = wdp!(W.Graph.Port.DEFERRED);
383
384/// A declared side could not be routed (no free lane beside the node,
385/// or the face cell was taken) and the end attached on its role's own
386/// face instead.
387///
388/// `W.Graph.Port.035` — Sequence 035 = UNROUTABLE (crate extension).
389/// A condition code, reported per run until fixed.
390pub const WARN_PORT_UNROUTABLE: &str = wdp!(W.Graph.Port.UNROUTABLE);
391
392/// An edge label will not paint AND the legend is disabled (the
393/// default) — the label appears nowhere in the output. With
394/// `LabelOverflow::Legend` set, unplaced labels are listed below
395/// the graph instead and this warning stays silent. The message names
396/// the edge by index and endpoint ids only — label TEXT is caller
397/// data (possibly secret, possibly control characters) and is never
398/// written to stderr.
399///
400/// `W.Render.Label.031` — Sequence 031 = INVISIBLE (Ash's ruling: the
401/// warning names the outcome the user must act on, not the cause —
402/// 026 EXHAUSTED fit only the out-of-space case). Emitted into the
403/// diagnostic context, once per affected label per PLAN BUILD: plans
404/// are stateless, so planning the same layout again re-emits.
405pub const WARN_LABEL_INVISIBLE: &str = wdp!(W.Render.Label.INVISIBLE);
406
407// ── GraphError ──────────────────────────────────────────────────────────
408
409/// Unified error type for all graph and layout operations.
410///
411/// Each variant carries a WDP error code accessible via [`GraphError::code()`],
412/// and an actionable hint via [`GraphError::hint()`].
413///
414/// # WDP Code Mapping
415///
416/// | Variant | WDP Code | Meaning |
417/// |---------|----------|---------|
418/// | `EmptyGraph` | `E.Graph.Node.001` | MISSING — no nodes |
419/// | `NodeNotFound` | `E.Graph.Node.021` | NOT_FOUND — node absent |
420/// | `CycleDetected` | `E.Graph.Dag.003` | INVALID — DAG constraint violated |
421/// | `SubgraphCycle` | `E.Graph.Subgraph.003` | INVALID — nesting cycle |
422/// | `SubgraphNotFound` | `E.Graph.Subgraph.021` | NOT_FOUND — subgraph absent |
423/// | `ArenaOom` | `E.ArenaLayout.Alloc.026` | EXHAUSTED — arena memory |
424/// | `BuilderFailed` | `E.ArenaLayout.Builder.026` | EXHAUSTED — builder alloc |
425/// | `ExceedsMaxNodes` | `E.ArenaLayout.Node.004` | OVERFLOW — index type full |
426/// | `ExceedsMaxLevels` | `E.ArenaLayout.Level.004` | OVERFLOW — too deep |
427/// | `ExceedsMaxExtent` | `E.ArenaLayout.Extent.004` | OVERFLOW — canvas past the coordinate type |
428/// | `RenderPlanOom` | `E.Render.Plan.026` | EXHAUSTED — plan buffer |
429/// | `RenderCanvasTooSmall` | `E.Render.Canvas.026` | EXHAUSTED — band buffer |
430/// | `RenderOutputTooSmall` | `E.Render.Sink.026` | EXHAUSTED — output buffer |
431/// | `RenderWorkspaceTooSmall` | `E.Render.Workspace.026` | EXHAUSTED — composer workspace |
432/// | `RenderSinkFailed` | `E.Render.Sink.032` | FAILED — caller's writer errored |
433///
434/// # Examples
435///
436/// ```
437/// use ascii_dag::GraphError;
438///
439/// let err = GraphError::EmptyGraph;
440/// assert_eq!(err.code(), "E.Graph.Node.001");
441/// assert!(!err.hint().is_empty());
442/// ```
443#[derive(Debug, Clone, PartialEq, Eq)]
444#[non_exhaustive]
445pub enum GraphError {
446    /// The graph has no nodes.
447    ///
448    /// **WDP:** `E.Graph.Node.001` (MISSING)
449    EmptyGraph,
450
451    /// A referenced node ID does not exist in the graph.
452    ///
453    /// **WDP:** `E.Graph.Node.021` (NOT_FOUND)
454    NodeNotFound(usize),
455
456    /// The graph contains a cycle but acyclicity was required.
457    ///
458    /// **WDP:** `E.Graph.Dag.003` (INVALID)
459    CycleDetected,
460
461    /// Subgraph nesting would create a cycle in the hierarchy.
462    ///
463    /// **WDP:** `E.Graph.Subgraph.003` (INVALID)
464    SubgraphCycle,
465
466    /// A referenced subgraph ID does not exist.
467    ///
468    /// **WDP:** `E.Graph.Subgraph.021` (NOT_FOUND)
469    SubgraphNotFound(usize),
470
471    /// The arena allocator ran out of memory.
472    ///
473    /// **WDP:** `E.ArenaLayout.Alloc.026` (EXHAUSTED)
474    ///
475    /// Use `Graph::estimate_layout_arena_size()` to compute the required size.
476    ArenaOom,
477
478    /// The IR builder failed to allocate output structures.
479    ///
480    /// **WDP:** `E.ArenaLayout.Builder.026` (EXHAUSTED)
481    BuilderFailed,
482
483    /// The graph has more nodes or edges than the selected index type supports.
484    ///
485    /// **WDP:** `E.ArenaLayout.Node.004` (OVERFLOW)
486    ExceedsMaxNodes {
487        /// Actual node or edge count.
488        count: usize,
489        /// Maximum supported by the current index type.
490        max: usize,
491    },
492
493    /// The graph's longest path exceeds the maximum level depth.
494    ///
495    /// **WDP:** `E.ArenaLayout.Level.004` (OVERFLOW)
496    ExceedsMaxLevels {
497        /// Actual depth of the longest path.
498        depth: usize,
499        /// Maximum supported levels.
500        max: usize,
501    },
502
503    /// The layout's cross-axis extent — its packed nodes plus the cells
504    /// port routing opens beside them — exceeds the arena coordinate
505    /// type.
506    ///
507    /// **WDP:** `E.ArenaLayout.Extent.004` (OVERFLOW)
508    ExceedsMaxExtent {
509        /// The extent the layout needs.
510        extent: usize,
511        /// Maximum representable by the coordinate type.
512        max: usize,
513    },
514
515    /// The caller-provided render-plan buffer is too small.
516    ///
517    /// **WDP:** `E.Render.Plan.026` (EXHAUSTED)
518    ///
519    /// Use `estimate_render_arena_size()` to compute the required size.
520    RenderPlanOom,
521
522    /// The caller-provided band buffer holds fewer than
523    /// `width × band_rows` cells.
524    ///
525    /// **WDP:** `E.Render.Canvas.026` (EXHAUSTED)
526    RenderCanvasTooSmall {
527        /// Cells required (`width × band_rows`).
528        needed: usize,
529        /// Cells provided.
530        got: usize,
531    },
532
533    /// The caller-provided output byte buffer filled up mid-render.
534    ///
535    /// **WDP:** `E.Render.Sink.026` (EXHAUSTED)
536    RenderOutputTooSmall,
537
538    /// A fixed composer workspace cannot hold this scene's
539    /// composition. Units are BYTES — explicit in the field names so
540    /// they can never be confused with the cell counts of
541    /// [`RenderCanvasTooSmall`](Self::RenderCanvasTooSmall).
542    ///
543    /// **WDP:** `E.Render.Workspace.026` (EXHAUSTED)
544    RenderWorkspaceTooSmall {
545        /// Bytes the composition needs (`usize::MAX`: the requirement
546        /// itself overflowed — the scene cannot fit any workspace).
547        needed_bytes: usize,
548        /// Bytes the workspace holds.
549        got_bytes: usize,
550    },
551
552    /// The caller's `fmt::Write` sink reported an error mid-render.
553    /// The failure originated downstream of the renderer (the writer
554    /// itself); rendering state is unaffected and the render may be
555    /// retried with a healthy sink. Previously a writer failure could
556    /// only surface as a bare `fmt::Error` with no code.
557    ///
558    /// **WDP:** `E.Render.Sink.032` (FAILED)
559    RenderSinkFailed,
560}
561
562impl GraphError {
563    /// WDP Level 0 error code.
564    ///
565    /// Format: `Severity.Component.Primary.Sequence`
566    ///
567    /// Every variant maps to a unique code — no two variants share the same
568    /// `Component.Primary.Sequence` triple.
569    #[inline]
570    pub fn code(&self) -> &'static str {
571        match self {
572            Self::EmptyGraph => EMPTY_GRAPH,
573            Self::NodeNotFound(_) => NODE_NOT_FOUND,
574            Self::CycleDetected => CYCLE_DETECTED,
575            Self::SubgraphCycle => SUBGRAPH_CYCLE,
576            Self::SubgraphNotFound(_) => SUBGRAPH_NOT_FOUND,
577            Self::ArenaOom => ARENA_OOM,
578            Self::BuilderFailed => BUILDER_FAILED,
579            Self::ExceedsMaxNodes { .. } => EXCEEDS_MAX_NODES,
580            Self::ExceedsMaxLevels { .. } => EXCEEDS_MAX_LEVELS,
581            Self::ExceedsMaxExtent { .. } => EXCEEDS_MAX_EXTENT,
582            Self::RenderPlanOom => RENDER_PLAN_OOM,
583            Self::RenderCanvasTooSmall { .. } => RENDER_CANVAS_TOO_SMALL,
584            Self::RenderOutputTooSmall => RENDER_OUTPUT_TOO_SMALL,
585            Self::RenderWorkspaceTooSmall { .. } => RENDER_WORKSPACE_TOO_SMALL,
586            Self::RenderSinkFailed => RENDER_SINK_FAILED,
587        }
588    }
589
590    /// Actionable hint for resolving this error.
591    #[inline]
592    pub fn hint(&self) -> &'static str {
593        match self {
594            Self::EmptyGraph => "Add at least one node before computing layout",
595            Self::NodeNotFound(_) => "Call add_node() before referencing this node ID",
596            Self::CycleDetected => "Enable cycle breaking or remove the back edge",
597            Self::SubgraphCycle => {
598                "A subgraph cannot be nested inside itself or its own descendant"
599            }
600            Self::SubgraphNotFound(_) => "Call add_subgraph() before referencing this subgraph ID",
601            Self::ArenaOom => "Increase the arena buffer size; use estimate_layout_arena_size()",
602            Self::BuilderFailed => "Increase the output arena buffer size",
603            Self::ExceedsMaxNodes { .. } => {
604                "Use a larger index type (arena-idx-u32) or reduce graph size"
605            }
606            Self::ExceedsMaxLevels { .. } => {
607                "Reduce chain depth or use a different layout strategy"
608            }
609            Self::ExceedsMaxExtent { .. } => {
610                "Narrow the widest level (node widths, spacing, side ports) or use the heap layout"
611            }
612            Self::RenderPlanOom => "Increase the render arena; use estimate_render_arena_size()",
613            Self::RenderCanvasTooSmall { .. } => {
614                "Provide at least width × band_rows cells; lower band_rows_cap to shrink bands"
615            }
616            Self::RenderOutputTooSmall => {
617                "Provide a larger output buffer; plan width × height plus escapes when colored"
618            }
619            Self::RenderWorkspaceTooSmall { .. } => {
620                "Size the workspace with CompositionRequirements::workspace_bytes(); \
621                 lower band_rows_cap to shrink it"
622            }
623            Self::RenderSinkFailed => {
624                "The failure came from the caller's writer, not the renderer; \
625                 check the sink and retry"
626            }
627        }
628    }
629}
630
631impl fmt::Display for GraphError {
632    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
633        write!(f, "[{}] ", self.code())?;
634        match self {
635            Self::EmptyGraph => write!(f, "graph has no nodes"),
636            Self::NodeNotFound(id) => write!(f, "node {} not found", id),
637            Self::CycleDetected => write!(f, "cycle detected in graph"),
638            Self::SubgraphCycle => write!(f, "subgraph nesting would create a cycle"),
639            Self::SubgraphNotFound(id) => write!(f, "subgraph {} not found", id),
640            Self::ArenaOom => write!(f, "arena out of memory"),
641            Self::BuilderFailed => write!(f, "IR builder allocation failed"),
642            Self::ExceedsMaxNodes { count, max } => {
643                write!(
644                    f,
645                    "node/edge count {} exceeds index-type max {}",
646                    count, max
647                )
648            }
649            Self::ExceedsMaxLevels { depth, max } => {
650                write!(f, "graph depth {} exceeds max levels {}", depth, max)
651            }
652            Self::ExceedsMaxExtent { extent, max } => {
653                write!(f, "layout extent {} exceeds coordinate max {}", extent, max)
654            }
655            Self::RenderPlanOom => write!(f, "render plan buffer exhausted"),
656            Self::RenderCanvasTooSmall { needed, got } => {
657                write!(f, "band buffer holds {} cells, needs {}", got, needed)
658            }
659            Self::RenderOutputTooSmall => write!(f, "render output buffer exhausted"),
660            Self::RenderWorkspaceTooSmall {
661                needed_bytes,
662                got_bytes,
663            } => {
664                write!(
665                    f,
666                    "composer workspace holds {} bytes, needs {}",
667                    got_bytes, needed_bytes
668                )
669            }
670            Self::RenderSinkFailed => write!(f, "output sink reported a write failure"),
671        }
672    }
673}
674
675#[cfg(feature = "std")]
676impl std::error::Error for GraphError {}
677
678// ── ErrorChain (error + causal chain) ───────────────────────────────────
679
680#[cfg(feature = "alloc")]
681/// A diagnostic pairs a [`GraphError`] with an optional causal chain.
682///
683/// Both the outer error and every inner cause carry their own WDP code,
684/// forming a chain that provides context (what failed) at each level:
685///
686/// ```text
687/// [E.ArenaLayout.Builder.026] IR builder allocation failed
688///   caused by: [E.ArenaLayout.Alloc.026] arena out of memory
689/// ```
690///
691/// # Construction
692///
693/// ```
694/// use ascii_dag::errors::{GraphError, ErrorChain};
695///
696/// // Leaf diagnostic (no cause)
697/// let d = ErrorChain::from(GraphError::ArenaOom);
698/// assert_eq!(d.code(), "E.ArenaLayout.Alloc.026");
699/// assert!(d.cause().is_none());
700///
701/// // Chained diagnostic
702/// let d = ErrorChain::from(GraphError::BuilderFailed)
703///     .caused_by(GraphError::ArenaOom);
704/// assert_eq!(d.code(), "E.ArenaLayout.Builder.026");
705/// assert_eq!(d.cause().unwrap().code(), "E.ArenaLayout.Alloc.026");
706/// ```
707///
708/// # Display
709///
710/// `Display` renders the full chain, indenting each cause level:
711///
712/// ```
713/// use ascii_dag::errors::{GraphError, ErrorChain};
714///
715/// let d = ErrorChain::from(GraphError::BuilderFailed)
716///     .caused_by(GraphError::ArenaOom);
717/// let msg = d.to_string();
718/// assert!(msg.contains("caused by:"));
719/// ```
720#[derive(Debug, Clone, PartialEq, Eq)]
721pub struct ErrorChain {
722    /// The error at this level.
723    error: GraphError,
724    /// Optional inner cause (each cause is itself a full `ErrorChain`).
725    cause: Option<Box<ErrorChain>>,
726}
727
728#[cfg(feature = "alloc")]
729impl ErrorChain {
730    /// Create a leaf diagnostic (no cause).
731    #[inline]
732    pub fn new(error: GraphError) -> Self {
733        Self { error, cause: None }
734    }
735
736    /// Attach a cause to this diagnostic (builder pattern).
737    ///
738    /// The cause is wrapped in its own `ErrorChain` with no further inner
739    /// cause.  For deeper chains, use [`caused_by_chain`](Self::caused_by_chain).
740    #[inline]
741    pub fn caused_by(self, cause: GraphError) -> Self {
742        self.caused_by_chain(ErrorChain::new(cause))
743    }
744
745    /// Attach an existing `ErrorChain` as the cause.
746    #[inline]
747    pub fn caused_by_chain(mut self, cause: ErrorChain) -> Self {
748        self.cause = Some(Box::new(cause));
749        self
750    }
751
752    /// The [`GraphError`] at this level of the chain.
753    #[inline]
754    pub fn error(&self) -> &GraphError {
755        &self.error
756    }
757
758    /// WDP code of the outermost error.
759    #[inline]
760    pub fn code(&self) -> &'static str {
761        self.error.code()
762    }
763
764    /// Hint for the outermost error.
765    #[inline]
766    pub fn hint(&self) -> &'static str {
767        self.error.hint()
768    }
769
770    /// The inner cause, if any.
771    #[inline]
772    pub fn cause(&self) -> Option<&ErrorChain> {
773        self.cause.as_deref()
774    }
775
776    /// Iterate over the full chain: self, then cause, then cause's cause, etc.
777    pub fn chain(&self) -> DiagnosticChain<'_> {
778        DiagnosticChain {
779            current: Some(self),
780        }
781    }
782}
783
784#[cfg(feature = "alloc")]
785impl From<GraphError> for ErrorChain {
786    #[inline]
787    fn from(error: GraphError) -> Self {
788        Self::new(error)
789    }
790}
791
792#[cfg(feature = "alloc")]
793impl fmt::Display for ErrorChain {
794    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
795        // Outer error
796        fmt::Display::fmt(&self.error, f)?;
797
798        // Walk the cause chain, indenting each level
799        let mut depth = 1;
800        let mut current = self.cause.as_deref();
801        while let Some(diag) = current {
802            writeln!(f)?;
803            for _ in 0..depth {
804                write!(f, "  ")?;
805            }
806            write!(f, "caused by: {}", diag.error)?;
807            current = diag.cause.as_deref();
808            depth += 1;
809        }
810        Ok(())
811    }
812}
813
814#[cfg(all(feature = "alloc", feature = "std"))]
815impl std::error::Error for ErrorChain {
816    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
817        self.cause.as_deref().map(|d| d as &dyn std::error::Error)
818    }
819}
820
821/// Iterator over the [`ErrorChain`] chain (outer → innermost cause).
822#[cfg(feature = "alloc")]
823pub struct DiagnosticChain<'a> {
824    current: Option<&'a ErrorChain>,
825}
826
827#[cfg(feature = "alloc")]
828impl<'a> Iterator for DiagnosticChain<'a> {
829    type Item = &'a ErrorChain;
830
831    fn next(&mut self) -> Option<Self::Item> {
832        let diag = self.current?;
833        self.current = diag.cause.as_deref();
834        Some(diag)
835    }
836}
837
838#[cfg(test)]
839mod tests {
840    use super::*;
841
842    /// The Render component's codes compose exactly as documented.
843    #[test]
844    fn render_component_codes() {
845        assert_eq!(GraphError::RenderPlanOom.code(), "E.Render.Plan.026");
846        assert_eq!(
847            GraphError::RenderCanvasTooSmall { needed: 0, got: 0 }.code(),
848            "E.Render.Canvas.026"
849        );
850        assert_eq!(GraphError::RenderOutputTooSmall.code(), "E.Render.Sink.026");
851        for e in [
852            GraphError::RenderPlanOom,
853            GraphError::RenderCanvasTooSmall { needed: 8, got: 4 },
854            GraphError::RenderOutputTooSmall,
855        ] {
856            assert!(
857                !e.hint().is_empty(),
858                "{}: hint must be actionable",
859                e.code()
860            );
861        }
862    }
863}