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}