1use std::sync::Arc;
6
7use antecedent_core::VariableId;
8
9use crate::algo::{bfs_reaches, kahn_order};
10use crate::error::GraphError;
11use crate::types::{DenseNodeId, MarkedEdge, NodeRef};
12use crate::workspace::GraphWorkspace;
13
14#[derive(Clone, Debug)]
16pub struct Dag {
17 nodes: Vec<NodeRef>,
18 children: Vec<Vec<DenseNodeId>>,
20 parents: Vec<Vec<DenseNodeId>>,
22 insert_ws: GraphWorkspace,
24}
25
26impl Dag {
27 #[must_use]
29 pub fn empty() -> Self {
30 Self {
31 nodes: Vec::new(),
32 children: Vec::new(),
33 parents: Vec::new(),
34 insert_ws: GraphWorkspace::default(),
35 }
36 }
37
38 #[must_use]
40 pub fn with_variables(n: u32) -> Self {
41 let mut g = Self::empty();
42 for i in 0..n {
43 let _ = g.add_node(NodeRef::Static(VariableId::from_raw(i)));
44 }
45 g
46 }
47
48 pub fn from_named_edges(
55 schema: &antecedent_core::CausalSchema,
56 edges: &[(&str, &str)],
57 ) -> Result<Self, GraphError> {
58 let n = crate::named::schema_node_count(schema)?;
59 let mut g = Self::with_variables(n);
60 for &(from_name, to_name) in edges {
61 let (from, to) = crate::named::resolve_named_edge(schema, from_name, to_name)?;
62 g.insert_directed(from, to)?;
63 }
64 Ok(g)
65 }
66
67 #[must_use]
69 pub fn node_count(&self) -> usize {
70 self.nodes.len()
71 }
72
73 #[must_use]
75 pub fn is_empty(&self) -> bool {
76 self.nodes.is_empty()
77 }
78
79 #[must_use]
81 pub fn nodes(&self) -> &[NodeRef] {
82 &self.nodes
83 }
84
85 pub fn add_node(&mut self, node: NodeRef) -> Result<DenseNodeId, GraphError> {
91 if !matches!(node, NodeRef::Static(_)) {
92 return Err(GraphError::InvalidEndpoints { message: "Dag accepts only Static nodes" });
93 }
94 let id = u32::try_from(self.nodes.len()).map_err(|_| GraphError::TooManyNodes)?;
95 self.nodes.push(node);
96 self.children.push(Vec::new());
97 self.parents.push(Vec::new());
98 Ok(DenseNodeId::from_raw(id))
99 }
100
101 pub fn insert_directed(
107 &mut self,
108 from: DenseNodeId,
109 to: DenseNodeId,
110 ) -> Result<(), GraphError> {
111 self.validate_node(from)?;
112 self.validate_node(to)?;
113 if self.children[from.as_usize()].contains(&to) {
114 return Err(GraphError::DuplicateEdge { from: from.raw(), to: to.raw() });
115 }
116 let mut ws = core::mem::take(&mut self.insert_ws);
117 let cycle = self.reaches_with(to, from, &mut ws);
118 self.insert_ws = ws;
119 if cycle {
120 return Err(GraphError::Cycle { from: from.raw(), to: to.raw() });
121 }
122 self.children[from.as_usize()].push(to);
123 self.parents[to.as_usize()].push(from);
124 Ok(())
125 }
126
127 pub(crate) fn insert_directed_unchecked(&mut self, from: DenseNodeId, to: DenseNodeId) {
130 self.children[from.as_usize()].push(to);
131 self.parents[to.as_usize()].push(from);
132 }
133
134 pub fn remove_directed(&mut self, from: DenseNodeId, to: DenseNodeId) {
136 if from.as_usize() >= self.node_count() || to.as_usize() >= self.node_count() {
137 return;
138 }
139 self.children[from.as_usize()].retain(|c| *c != to);
140 self.parents[to.as_usize()].retain(|p| *p != from);
141 }
142
143 #[must_use]
145 pub fn children(&self, id: DenseNodeId) -> &[DenseNodeId] {
146 &self.children[id.as_usize()]
147 }
148
149 #[must_use]
151 pub fn parents(&self, id: DenseNodeId) -> &[DenseNodeId] {
152 &self.parents[id.as_usize()]
153 }
154
155 #[must_use]
157 pub fn reaches(&self, from: DenseNodeId, to: DenseNodeId) -> bool {
158 if from == to {
159 return true;
160 }
161 let mut ws = GraphWorkspace::default();
162 self.reaches_with(from, to, &mut ws)
163 }
164
165 pub fn reaches_with(
167 &self,
168 from: DenseNodeId,
169 to: DenseNodeId,
170 ws: &mut GraphWorkspace,
171 ) -> bool {
172 bfs_reaches(&self.children, from, to, ws)
173 }
174
175 #[must_use]
177 pub fn topological_order(&self) -> Option<Vec<DenseNodeId>> {
178 kahn_order(&self.parents, &self.children)
179 }
180
181 pub fn validate(&self) -> Result<(), GraphError> {
187 if self.topological_order().is_none() {
188 return Err(GraphError::Cycle { from: 0, to: 0 });
189 }
190 Ok(())
191 }
192
193 fn validate_node(&self, id: DenseNodeId) -> Result<(), GraphError> {
194 if id.as_usize() >= self.node_count() {
195 Err(GraphError::UnknownNode { id: id.raw() })
196 } else {
197 Ok(())
198 }
199 }
200
201 pub fn edges(&self) -> impl Iterator<Item = MarkedEdge> + '_ {
203 self.children.iter().enumerate().flat_map(|(i, kids)| {
204 let from = DenseNodeId::from_raw(u32::try_from(i).expect("node fit"));
205 kids.iter().map(move |&to| MarkedEdge::directed(from, to))
206 })
207 }
208
209 pub fn directed_paths(
222 &self,
223 from: DenseNodeId,
224 to: DenseNodeId,
225 max_paths: usize,
226 max_len: usize,
227 ) -> Result<Vec<Vec<DenseNodeId>>, GraphError> {
228 self.directed_paths_with_budget(from, to, max_paths, max_len).map(|(paths, _)| paths)
229 }
230
231 pub fn directed_paths_with_budget(
242 &self,
243 from: DenseNodeId,
244 to: DenseNodeId,
245 max_paths: usize,
246 max_len: usize,
247 ) -> Result<(Vec<Vec<DenseNodeId>>, bool), GraphError> {
248 self.validate_node(from)?;
249 self.validate_node(to)?;
250 let mut out = Vec::new();
251 if max_paths == 0 || max_len == 0 {
252 return Ok((out, true));
254 }
255 let mut truncated = false;
256 let mut stack = vec![vec![from]];
257 while let Some(path) = stack.pop() {
258 if out.len() >= max_paths {
259 truncated = true;
260 break;
261 }
262 let last = *path.last().expect("nonempty");
263 if path.len() > 1 && last == to {
264 out.push(path);
265 continue;
266 }
267 if last == to && path.len() == 1 {
268 out.push(path);
269 continue;
270 }
271 if path.len() >= max_len {
272 truncated = true;
274 continue;
275 }
276 for &c in self.children(last) {
277 if path.contains(&c) {
278 continue;
279 }
280 let mut next = path.clone();
281 next.push(c);
282 stack.push(next);
283 }
284 }
285 Ok((out, truncated))
286 }
287}
288
289#[cfg(test)]
290mod tests {
291 use super::*;
292
293 #[test]
294 fn directed_paths_reports_max_paths_truncation() {
295 let mut g = Dag::with_variables(6);
297 for (u, v) in [(0, 4), (4, 5), (0, 1), (1, 3), (3, 5), (1, 2), (2, 5)] {
298 g.insert_directed(DenseNodeId::from_raw(u), DenseNodeId::from_raw(v)).unwrap();
299 }
300 let (t, y) = (DenseNodeId::from_raw(0), DenseNodeId::from_raw(5));
301
302 let (all, truncated) = g.directed_paths_with_budget(t, y, 64, 16).unwrap();
303 assert_eq!(all.len(), 3);
304 assert!(!truncated, "a budget that comfortably fits every path must not report truncation");
305
306 for cap in 1..=2 {
307 let (paths, truncated) = g.directed_paths_with_budget(t, y, cap, 16).unwrap();
308 assert_eq!(paths.len(), cap);
309 assert!(truncated, "max_paths={cap} dropped paths but reported none");
310 }
311 }
312
313 #[test]
314 fn directed_paths_reports_max_len_truncation() {
315 let mut g = Dag::with_variables(4);
317 for (u, v) in [(0, 1), (1, 2), (2, 3)] {
318 g.insert_directed(DenseNodeId::from_raw(u), DenseNodeId::from_raw(v)).unwrap();
319 }
320 let (t, y) = (DenseNodeId::from_raw(0), DenseNodeId::from_raw(3));
321 let (paths, truncated) = g.directed_paths_with_budget(t, y, 64, 3).unwrap();
322 assert!(paths.is_empty());
323 assert!(truncated, "max_len pruned the only path but reported no truncation");
324
325 let (paths, truncated) = g.directed_paths_with_budget(t, y, 64, 4).unwrap();
326 assert_eq!(paths.len(), 1);
327 assert!(!truncated);
328 }
329
330 #[test]
331 fn rejects_cycles() {
332 let mut g = Dag::with_variables(3);
333 let a = DenseNodeId::from_raw(0);
334 let b = DenseNodeId::from_raw(1);
335 let c = DenseNodeId::from_raw(2);
336 g.insert_directed(a, b).unwrap();
337 g.insert_directed(b, c).unwrap();
338 assert!(matches!(g.insert_directed(c, a), Err(GraphError::Cycle { .. })));
339 }
340
341 #[test]
342 fn topological_order_respects_edges() {
343 let mut g = Dag::with_variables(3);
344 g.insert_directed(DenseNodeId::from_raw(0), DenseNodeId::from_raw(1)).unwrap();
345 g.insert_directed(DenseNodeId::from_raw(1), DenseNodeId::from_raw(2)).unwrap();
346 let order = g.topological_order().unwrap();
347 let pos = |id: u32| order.iter().position(|n| n.raw() == id).unwrap();
348 assert!(pos(0) < pos(1) && pos(1) < pos(2));
349 }
350
351 #[test]
352 fn traversal_workspace_reuses_frontier_capacity() {
353 let mut dag = Dag::with_variables(1_000);
354 for i in 0..999 {
355 dag.insert_directed(DenseNodeId::from_raw(i), DenseNodeId::from_raw(i + 1)).unwrap();
356 }
357 let mut ws = GraphWorkspace::default();
358 assert!(dag.reaches_with(DenseNodeId::from_raw(0), DenseNodeId::from_raw(999), &mut ws));
359 let ptr = ws.frontier.as_ptr();
360 let cap = ws.frontier.capacity();
361 for _ in 0..50 {
362 assert!(dag.reaches_with(
363 DenseNodeId::from_raw(0),
364 DenseNodeId::from_raw(999),
365 &mut ws
366 ));
367 assert_eq!(ws.frontier.as_ptr(), ptr);
368 assert_eq!(ws.frontier.capacity(), cap);
369 }
370 }
371}
372
373#[derive(Clone, Debug)]
375pub struct DagReview {
376 pub graph: Dag,
378 pub pending_edges: Arc<[(VariableId, VariableId)]>,
380 pub algorithm: Arc<str>,
382}
383
384impl DagReview {
385 #[must_use]
387 pub fn from_dag(graph: Dag, algorithm: impl Into<Arc<str>>) -> Self {
388 let mut pending = Vec::new();
389 for e in graph.edges() {
390 if let Some((from, to)) = e.parent_child() {
391 if let (Some(fv), Some(tv)) =
392 (variable_id_of(&graph, from), variable_id_of(&graph, to))
393 {
394 pending.push((fv, tv));
395 }
396 }
397 }
398 Self { graph, pending_edges: Arc::from(pending), algorithm: algorithm.into() }
399 }
400
401 #[must_use]
403 pub fn accept_edge(mut self, from: VariableId, to: VariableId) -> Self {
404 let pending: Vec<_> =
405 self.pending_edges.iter().copied().filter(|e| *e != (from, to)).collect();
406 self.pending_edges = Arc::from(pending);
407 self
408 }
409
410 #[must_use]
412 pub fn accept_all(mut self) -> Self {
413 self.pending_edges = Arc::from([]);
414 self
415 }
416
417 #[must_use]
419 pub fn is_complete(&self) -> bool {
420 self.pending_edges.is_empty()
421 }
422
423 pub fn try_into_dag(self) -> Result<Dag, GraphError> {
429 if !self.is_complete() {
430 return Err(GraphError::InvalidEndpoints {
431 message: "cannot finish DagReview while pending edges remain",
432 });
433 }
434 Ok(self.graph)
435 }
436}
437
438fn variable_id_of(dag: &Dag, id: DenseNodeId) -> Option<VariableId> {
439 match dag.nodes().get(id.as_usize()) {
440 Some(NodeRef::Static(v)) => Some(*v),
441 _ => None,
442 }
443}