ascii_dag/graph.rs
1//! Core DAG (Directed Acyclic Graph) data structure.
2//!
3//! This module provides the fundamental graph structure with nodes and edges.
4//!
5//! ## Performance Characteristics
6//!
7//! - **Node/Edge Insertion**: O(1) amortized with HashMap and cached adjacency lists
8//! - **Child/Parent Lookups**: O(1) via cached adjacency lists (not O(E) iteration)
9//! - **ID→Index Mapping**: O(1) via HashMap (not O(N) scan)
10//! - **Node Width**: O(1) via pre-computed cache
11//!
12//! ## Memory Overhead
13//!
14//! Per node:
15//! - ~100 bytes (node data, caches, adjacency list headers)
16//!
17//! Per edge:
18//! - ~16 bytes (adjacency list entries, both directions)
19//!
20//! ## Security
21//!
22//! - No unsafe code
23//! - For untrusted input, consider limiting maximum nodes/edges to prevent resource exhaustion
24//! - Maximum node ID: `usize::MAX` (up to 20 decimal digits)
25
26use alloc::{string::String, vec::Vec};
27
28#[cfg(feature = "std")]
29use std::collections::{HashMap, HashSet};
30
31#[cfg(not(feature = "std"))]
32use alloc::collections::{BTreeMap as HashMap, BTreeSet as HashSet};
33
34/// Rendering mode for the DAG visualization.
35#[derive(Debug, Clone, Copy, PartialEq, Eq)]
36pub enum RenderMode {
37 /// Render chains vertically (takes more vertical space)
38 Vertical,
39
40 /// Render chains horizontally when possible (compact, one-line for simple chains)
41 Horizontal,
42
43 /// Auto-detect: horizontal for simple chains, vertical for complex graphs
44 Auto,
45}
46
47impl Default for RenderMode {
48 fn default() -> Self {
49 RenderMode::Auto
50 }
51}
52
53/// A Directed Acyclic Graph (DAG) with ASCII rendering capabilities.
54///
55/// # Examples
56///
57/// ```
58/// use ascii_dag::graph::DAG;
59///
60/// let mut dag = DAG::new();
61/// dag.add_node(1, "Start");
62/// dag.add_node(2, "End");
63/// dag.add_edge(1, 2);
64///
65/// let output = dag.render();
66/// assert!(output.contains("Start"));
67/// assert!(output.contains("End"));
68/// ```
69#[derive(Clone)]
70pub struct DAG<'a> {
71 pub(crate) nodes: Vec<(usize, &'a str)>,
72 pub(crate) edges: Vec<(usize, usize)>,
73 pub(crate) render_mode: RenderMode,
74 pub(crate) auto_created: HashSet<usize>, // Track auto-created nodes for visual distinction (O(1) lookups)
75 pub(crate) id_to_index: HashMap<usize, usize>, // Cache id→index mapping (O(1) lookups)
76 pub(crate) node_widths: Vec<usize>, // Cached formatted widths
77 pub(crate) children: Vec<Vec<usize>>, // Adjacency list: children[idx] = child indices
78 pub(crate) parents: Vec<Vec<usize>>, // Adjacency list: parents[idx] = parent indices
79}
80
81impl<'a> Default for DAG<'a> {
82 fn default() -> Self {
83 Self {
84 nodes: Vec::new(),
85 edges: Vec::new(),
86 render_mode: RenderMode::default(),
87 auto_created: HashSet::new(),
88 id_to_index: HashMap::new(),
89 node_widths: Vec::new(),
90 children: Vec::new(),
91 parents: Vec::new(),
92 }
93 }
94}
95
96impl<'a> DAG<'a> {
97 /// Create a new empty DAG.
98 ///
99 /// # Examples
100 ///
101 /// ```
102 /// use ascii_dag::graph::DAG;
103 /// let dag = DAG::new();
104 /// ```
105 pub fn new() -> Self {
106 Self::default()
107 }
108
109 /// Create a DAG from pre-defined nodes and edges (batch construction).
110 ///
111 /// This is more efficient than using the builder API for static graphs.
112 ///
113 /// # Examples
114 ///
115 /// ```
116 /// use ascii_dag::graph::DAG;
117 ///
118 /// let dag = DAG::from_edges(
119 /// &[(1, "A"), (2, "B"), (3, "C")],
120 /// &[(1, 2), (2, 3)]
121 /// );
122 /// ```
123 pub fn from_edges(nodes: &[(usize, &'a str)], edges: &[(usize, usize)]) -> Self {
124 let mut dag = Self {
125 nodes: nodes.to_vec(),
126 edges: Vec::new(),
127 render_mode: RenderMode::default(),
128 auto_created: HashSet::new(),
129 id_to_index: HashMap::new(),
130 node_widths: Vec::new(),
131 children: Vec::new(),
132 parents: Vec::new(),
133 };
134
135 // Build id_to_index map and widths cache
136 for (idx, &(id, label)) in dag.nodes.iter().enumerate() {
137 dag.id_to_index.insert(id, idx);
138 let width = dag.compute_node_width(id, label);
139 dag.node_widths.push(width);
140 }
141
142 // Initialize adjacency lists
143 dag.children.resize(dag.nodes.len(), Vec::new());
144 dag.parents.resize(dag.nodes.len(), Vec::new());
145
146 // Add edges (may auto-create missing nodes)
147 for &(from, to) in edges {
148 dag.add_edge(from, to);
149 }
150
151 dag
152 }
153
154 /// Set the rendering mode.
155 ///
156 /// # Examples
157 ///
158 /// ```
159 /// use ascii_dag::graph::{DAG, RenderMode};
160 ///
161 /// let mut dag = DAG::new();
162 /// dag.set_render_mode(RenderMode::Horizontal);
163 /// ```
164 pub fn set_render_mode(&mut self, mode: RenderMode) {
165 self.render_mode = mode;
166 }
167
168 /// Create a DAG with a specific render mode.
169 ///
170 /// # Examples
171 ///
172 /// ```
173 /// use ascii_dag::graph::{DAG, RenderMode};
174 ///
175 /// let dag = DAG::with_mode(RenderMode::Horizontal);
176 /// ```
177 pub fn with_mode(mode: RenderMode) -> Self {
178 Self {
179 nodes: Vec::new(),
180 edges: Vec::new(),
181 render_mode: mode,
182 auto_created: HashSet::new(),
183 id_to_index: HashMap::new(),
184 node_widths: Vec::new(),
185 children: Vec::new(),
186 parents: Vec::new(),
187 }
188 }
189
190 /// Add a node to the DAG.
191 ///
192 /// If the node was previously auto-created by `add_edge`, this will promote it
193 /// by setting its label and removing the auto-created flag.
194 ///
195 /// # Examples
196 ///
197 /// ```
198 /// use ascii_dag::graph::DAG;
199 ///
200 /// let mut dag = DAG::new();
201 /// dag.add_node(1, "MyNode");
202 /// ```
203 pub fn add_node(&mut self, id: usize, label: &'a str) {
204 // Check if node already exists (could be auto-created) - O(1) with HashMap
205 if let Some(&idx) = self.id_to_index.get(&id) {
206 // Promote auto-created node to explicit node
207 self.nodes[idx] = (id, label);
208 // Remove from auto_created set - O(1)
209 self.auto_created.remove(&id);
210 // Update cached width
211 let width = self.compute_node_width(id, label);
212 self.node_widths[idx] = width;
213 } else {
214 // Brand new node
215 let idx = self.nodes.len();
216 self.nodes.push((id, label));
217 self.id_to_index.insert(id, idx);
218 let width = self.compute_node_width(id, label);
219 self.node_widths.push(width);
220 // Extend adjacency lists
221 self.children.push(Vec::new());
222 self.parents.push(Vec::new());
223 }
224 }
225
226 /// Add an edge from one node to another.
227 ///
228 /// If either node doesn't exist, it will be auto-created as a placeholder.
229 /// You can later call `add_node` to provide a label for auto-created nodes.
230 ///
231 /// # Examples
232 ///
233 /// ```
234 /// use ascii_dag::graph::DAG;
235 ///
236 /// let mut dag = DAG::new();
237 /// dag.add_node(1, "A");
238 /// dag.add_node(2, "B");
239 /// dag.add_edge(1, 2); // A -> B
240 /// ```
241 pub fn add_edge(&mut self, from: usize, to: usize) {
242 self.ensure_node_exists(from);
243 self.ensure_node_exists(to);
244 self.edges.push((from, to));
245
246 // Update adjacency lists (O(1) lookups)
247 if let (Some(&from_idx), Some(&to_idx)) =
248 (self.id_to_index.get(&from), self.id_to_index.get(&to))
249 {
250 self.children[from_idx].push(to_idx);
251 self.parents[to_idx].push(from_idx);
252 }
253 }
254
255 /// Ensure a node exists, auto-creating if missing.
256 /// Auto-created nodes will be visually distinct (rendered with ⟨⟩ instead of [])
257 /// until explicitly defined with add_node.
258 fn ensure_node_exists(&mut self, id: usize) {
259 // O(1) lookup with HashMap
260 if !self.id_to_index.contains_key(&id) {
261 #[cfg(feature = "warnings")]
262 {
263 eprintln!(
264 "[ascii-dag] Warning: Node {} missing - auto-creating as placeholder. \
265 Call add_node({}, \"label\") before add_edge() to provide a label.",
266 id, id
267 );
268 }
269
270 // Create node with empty label
271 let idx = self.nodes.len();
272 self.nodes.push((id, ""));
273 self.auto_created.insert(id); // O(1) insert
274 self.id_to_index.insert(id, idx); // O(1) insert
275 let width = self.compute_node_width(id, "");
276 self.node_widths.push(width);
277 // Extend adjacency lists
278 self.children.push(Vec::new());
279 self.parents.push(Vec::new());
280 }
281 }
282
283 /// Check if a node was auto-created (for visual distinction)
284 pub(crate) fn is_auto_created(&self, id: usize) -> bool {
285 self.auto_created.contains(&id) // O(1) with HashSet
286 }
287
288 /// Write an unsigned integer to a string buffer without allocation.
289 /// This avoids format! bloat in no_std builds.
290 #[inline]
291 pub(crate) fn write_usize(buf: &mut String, mut n: usize) {
292 if n == 0 {
293 buf.push('0');
294 return;
295 }
296 let mut digits = [0u8; 20]; // Max digits for u64
297 let mut i = 0;
298 while n > 0 {
299 digits[i] = (n % 10) as u8 + b'0';
300 n /= 10;
301 i += 1;
302 }
303 // Write in reverse order
304 while i > 0 {
305 i -= 1;
306 buf.push(digits[i] as char);
307 }
308 }
309
310 /// Count digits in a number (for width calculation)
311 #[inline]
312 fn count_digits(mut n: usize) -> usize {
313 if n == 0 {
314 return 1;
315 }
316 let mut count = 0;
317 while n > 0 {
318 count += 1;
319 n /= 10;
320 }
321 count
322 }
323
324 /// Compute the formatted width of a node
325 pub(crate) fn compute_node_width(&self, id: usize, label: &str) -> usize {
326 if label.is_empty() || self.is_auto_created(id) {
327 // ⟨ID⟩ format
328 2 + Self::count_digits(id) // ⟨ + digits + ⟩
329 } else {
330 // [Label] format
331 2 + label.chars().count() // [ + label + ]
332 }
333 }
334
335 /// Write a formatted node directly to output buffer (avoids intermediate String allocation)
336 #[inline]
337 pub(crate) fn write_node(&self, output: &mut String, id: usize, label: &str) {
338 if label.is_empty() || self.is_auto_created(id) {
339 output.push('⟨');
340 Self::write_usize(output, id);
341 output.push('⟩');
342 } else {
343 output.push('[');
344 output.push_str(label);
345 output.push(']');
346 }
347 }
348
349 /// Get children of a node (returns IDs, not indices).
350 /// Uses cached adjacency lists for O(1) lookup instead of O(E) iteration.
351 pub(crate) fn get_children(&self, node_id: usize) -> Vec<usize> {
352 if let Some(&idx) = self.id_to_index.get(&node_id) {
353 // Convert child indices back to IDs
354 self.children[idx]
355 .iter()
356 .map(|&child_idx| self.nodes[child_idx].0)
357 .collect()
358 } else {
359 Vec::new()
360 }
361 }
362
363 /// Get parents of a node (returns IDs, not indices).
364 /// Uses cached adjacency lists for O(1) lookup instead of O(E) iteration.
365 pub(crate) fn get_parents(&self, node_id: usize) -> Vec<usize> {
366 if let Some(&idx) = self.id_to_index.get(&node_id) {
367 // Convert parent indices back to IDs
368 self.parents[idx]
369 .iter()
370 .map(|&parent_idx| self.nodes[parent_idx].0)
371 .collect()
372 } else {
373 Vec::new()
374 }
375 }
376
377 /// Get children indices directly (no ID conversion) - faster for internal use.
378 ///
379 /// Reserved for future optimization. Currently unused but available for
380 /// performance-critical paths that work with node indices directly.
381 #[inline]
382 #[allow(dead_code)]
383 pub(crate) fn get_children_indices(&self, node_idx: usize) -> &[usize] {
384 &self.children[node_idx]
385 }
386
387 /// Get parent indices directly (no ID conversion) - faster for internal use.
388 ///
389 /// Reserved for future optimization. Currently unused but available for
390 /// performance-critical paths that work with node indices directly.
391 #[inline]
392 #[allow(dead_code)]
393 pub(crate) fn get_parents_indices(&self, node_idx: usize) -> &[usize] {
394 &self.parents[node_idx]
395 }
396
397 /// Get node index from ID using O(1) HashMap lookup
398 #[inline]
399 pub(crate) fn node_index(&self, id: usize) -> Option<usize> {
400 self.id_to_index.get(&id).copied()
401 }
402
403 /// Get cached width for a node index
404 #[inline]
405 pub(crate) fn get_node_width(&self, idx: usize) -> usize {
406 self.node_widths.get(idx).copied().unwrap_or(0)
407 }
408
409 /// Estimate the buffer size needed for rendering.
410 ///
411 /// Use this to pre-allocate a buffer for [`render_to`](Self::render_to).
412 ///
413 /// # Examples
414 ///
415 /// ```
416 /// use ascii_dag::graph::DAG;
417 ///
418 /// let dag = DAG::from_edges(
419 /// &[(1, "A"), (2, "B")],
420 /// &[(1, 2)]
421 /// );
422 ///
423 /// let size = dag.estimate_size();
424 /// let mut buffer = String::with_capacity(size);
425 /// dag.render_to(&mut buffer);
426 /// ```
427 pub fn estimate_size(&self) -> usize {
428 // Rough estimate: nodes * avg_label_size + edges * connection_chars + box
429 self.nodes.len() * 25 + self.edges.len() * 15 + 200
430 }
431}