1use alloc::string::String;
16use alloc::vec;
17use alloc::vec::Vec;
18
19use crate::link::LinkPolicy;
20
21#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
23pub struct NodeId(pub u32);
24
25#[derive(Debug, Clone, Copy, PartialEq, Eq)]
29pub struct PadId {
30 pub node: NodeId,
31 pub index: u8,
32}
33
34impl From<NodeId> for PadId {
35 fn from(node: NodeId) -> Self {
36 PadId { node, index: 0 }
37 }
38}
39
40#[derive(Debug, Clone, Copy, PartialEq, Eq)]
46pub enum NodeKind {
47 Source,
48 Transform,
49 Sink,
50 Tee(u8),
51 Muxer(u8),
52 FaninSink(u8),
53 FanoutSrc(u8),
54}
55
56impl NodeKind {
57 pub fn in_pads(self) -> u8 {
59 match self {
60 NodeKind::Source | NodeKind::FanoutSrc(_) => 0,
61 NodeKind::Transform | NodeKind::Sink | NodeKind::Tee(_) => 1,
62 NodeKind::Muxer(n) | NodeKind::FaninSink(n) => n,
63 }
64 }
65
66 pub fn out_pads(self) -> u8 {
68 match self {
69 NodeKind::Sink | NodeKind::FaninSink(_) => 0,
70 NodeKind::Source | NodeKind::Transform | NodeKind::Muxer(_) => 1,
71 NodeKind::Tee(n) | NodeKind::FanoutSrc(n) => n,
72 }
73 }
74}
75
76#[derive(Debug, Clone, Copy, PartialEq, Eq)]
79pub enum PadDir {
80 In,
81 Out,
82}
83
84#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
92pub enum FanOutPolicy {
93 #[default]
95 FailLoud,
96 AllowBranchDrop,
98}
99
100#[derive(Debug, Clone, Copy, PartialEq, Eq)]
106pub struct NodeIdOffset(u32);
107
108impl NodeIdOffset {
109 pub fn apply(self, node: NodeId) -> NodeId {
111 NodeId(node.0 + self.0)
112 }
113
114 pub fn apply_pad(self, pad: PadId) -> PadId {
116 PadId {
117 node: self.apply(pad.node),
118 index: pad.index,
119 }
120 }
121}
122
123#[derive(Debug, Clone, Copy, PartialEq, Eq)]
126pub struct Edge {
127 pub src: PadId,
128 pub dst: PadId,
129 pub policy: LinkPolicy,
130 pub capacity: Option<usize>,
134}
135
136#[derive(Debug, Clone, Copy)]
138pub struct Tee(NodeId);
139
140impl Tee {
141 pub fn node(self) -> NodeId {
142 self.0
143 }
144 pub fn input(self) -> PadId {
145 PadId {
146 node: self.0,
147 index: 0,
148 }
149 }
150 pub fn out(self, index: u8) -> PadId {
151 PadId {
152 node: self.0,
153 index,
154 }
155 }
156}
157
158#[derive(Debug, Clone, Copy)]
163pub struct Demux(NodeId);
164
165impl Demux {
166 pub fn node(self) -> NodeId {
167 self.0
168 }
169 pub fn input(self) -> PadId {
170 PadId {
171 node: self.0,
172 index: 0,
173 }
174 }
175 pub fn out(self, index: u8) -> PadId {
176 PadId {
177 node: self.0,
178 index,
179 }
180 }
181}
182
183#[derive(Debug, Clone, Copy)]
185pub struct Muxer(NodeId);
186
187impl Muxer {
188 pub fn node(self) -> NodeId {
189 self.0
190 }
191 pub fn input(self, index: u8) -> PadId {
192 PadId {
193 node: self.0,
194 index,
195 }
196 }
197 pub fn output(self) -> PadId {
198 PadId {
199 node: self.0,
200 index: 0,
201 }
202 }
203}
204
205#[derive(Debug, Clone, Copy)]
210pub struct FaninSink(NodeId);
211
212impl FaninSink {
213 pub fn node(self) -> NodeId {
214 self.0
215 }
216 pub fn input(self, index: u8) -> PadId {
217 PadId {
218 node: self.0,
219 index,
220 }
221 }
222}
223
224#[derive(Debug, Clone, Copy)]
228pub struct FanoutSrc(NodeId);
229
230impl FanoutSrc {
231 pub fn node(self) -> NodeId {
232 self.0
233 }
234 pub fn output(self, index: u8) -> PadId {
235 PadId {
236 node: self.0,
237 index,
238 }
239 }
240}
241
242#[derive(Debug, Clone, PartialEq, Eq)]
244pub enum GraphError {
245 UnknownNode(NodeId),
247 PadOutOfRange {
249 node: NodeId,
250 index: u8,
251 direction: PadDir,
252 },
253 UnlinkedPad {
255 node: NodeId,
256 index: u8,
257 direction: PadDir,
258 },
259 PadCountMismatch {
262 node: NodeId,
263 index: u8,
264 direction: PadDir,
265 },
266 OrphanNode(NodeId),
268 Cycle { nodes: Vec<NodeId> },
270 DuplicateGhostPad {
274 node: NodeId,
275 index: u8,
276 direction: PadDir,
277 },
278 DegenerateFanNode(NodeId),
282}
283
284struct Node<E> {
285 kind: NodeKind,
286 element: Option<E>,
288 fanout: FanOutPolicy,
291 name: Option<String>,
294 log_category: Option<String>,
297 #[cfg(feature = "runtime")]
299 control: Option<crate::controller::ControlProgram>,
300}
301
302pub struct Graph<E> {
305 nodes: Vec<Node<E>>,
306 edges: Vec<Edge>,
307}
308
309impl<E> Default for Graph<E> {
310 fn default() -> Self {
311 Self::new()
312 }
313}
314
315impl<E> Graph<E> {
316 pub fn new() -> Self {
317 Self {
318 nodes: Vec::new(),
319 edges: Vec::new(),
320 }
321 }
322
323 pub fn add_source(&mut self, element: E) -> NodeId {
324 self.push(NodeKind::Source, Some(element))
325 }
326
327 pub fn add_transform(&mut self, element: E) -> NodeId {
328 self.push(NodeKind::Transform, Some(element))
329 }
330
331 pub fn add_sink(&mut self, element: E) -> NodeId {
332 self.push(NodeKind::Sink, Some(element))
333 }
334
335 pub fn add_tee(&mut self, outputs: u8) -> Tee {
336 Tee(self.push(NodeKind::Tee(outputs), None))
337 }
338
339 pub fn add_tee_with_policy(&mut self, outputs: u8, policy: FanOutPolicy) -> Tee {
344 let id = self.push(NodeKind::Tee(outputs), None);
345 self.nodes[id.0 as usize].fanout = policy;
346 Tee(id)
347 }
348
349 pub fn add_muxer(&mut self, element: E, inputs: u8) -> Muxer {
350 Muxer(self.push(NodeKind::Muxer(inputs), Some(element)))
351 }
352
353 pub fn add_fanin_sink(&mut self, element: E, inputs: u8) -> FaninSink {
360 FaninSink(self.push(NodeKind::FaninSink(inputs), Some(element)))
361 }
362
363 pub fn add_fanout_src(&mut self, element: E, outputs: u8) -> FanoutSrc {
369 FanoutSrc(self.push(NodeKind::FanoutSrc(outputs), Some(element)))
370 }
371
372 pub fn add_demux(&mut self, element: E, outputs: u8) -> Demux {
380 Demux(self.push(NodeKind::Tee(outputs), Some(element)))
381 }
382
383 fn push(&mut self, kind: NodeKind, element: Option<E>) -> NodeId {
384 let id = NodeId(self.nodes.len() as u32);
385 self.nodes.push(Node {
386 kind,
387 element,
388 fanout: FanOutPolicy::FailLoud,
389 name: None,
390 log_category: None,
391 #[cfg(feature = "runtime")]
392 control: None,
393 });
394 id
395 }
396
397 pub fn set_node_name(&mut self, node: NodeId, name: String) {
400 self.nodes[node.0 as usize].name = Some(name);
401 }
402
403 pub fn node_name(&self, node: NodeId) -> Option<&str> {
405 self.nodes
406 .get(node.0 as usize)
407 .and_then(|n| n.name.as_deref())
408 }
409
410 pub fn set_node_log_category(&mut self, node: NodeId, category: String) {
414 self.nodes[node.0 as usize].log_category = Some(category);
415 }
416
417 pub fn node_log_category(&self, node: NodeId) -> Option<&str> {
419 self.nodes
420 .get(node.0 as usize)
421 .and_then(|n| n.log_category.as_deref())
422 }
423
424 pub fn node_by_name(&self, name: &str) -> Option<NodeId> {
427 self.nodes
428 .iter()
429 .position(|n| n.name.as_deref() == Some(name))
430 .map(|i| NodeId(i as u32))
431 }
432
433 #[cfg(feature = "runtime")]
441 pub fn set_node_control(&mut self, node: NodeId, program: crate::controller::ControlProgram) {
442 self.nodes[node.0 as usize].control = Some(program);
443 }
444
445 pub fn link(&mut self, from: impl Into<PadId>, to: impl Into<PadId>) -> Result<(), GraphError> {
447 self.link_with(from, to, LinkPolicy::Block)
448 }
449
450 pub fn link_with(
452 &mut self,
453 from: impl Into<PadId>,
454 to: impl Into<PadId>,
455 policy: LinkPolicy,
456 ) -> Result<(), GraphError> {
457 self.link_full(from, to, policy, None)
458 }
459
460 pub fn link_full(
464 &mut self,
465 from: impl Into<PadId>,
466 to: impl Into<PadId>,
467 policy: LinkPolicy,
468 capacity: Option<usize>,
469 ) -> Result<(), GraphError> {
470 let (src, dst) = (from.into(), to.into());
471 self.check_pad(src, PadDir::Out)?;
472 self.check_pad(dst, PadDir::In)?;
473 self.edges.push(Edge {
474 src,
475 dst,
476 policy,
477 capacity,
478 });
479 Ok(())
480 }
481
482 pub fn edges(&self) -> &[Edge] {
486 &self.edges
487 }
488
489 pub fn insert_on_edge(&mut self, edge_idx: usize, element: E) -> NodeId {
499 let new = self.push(NodeKind::Transform, Some(element));
500 let old_dst = self.edges[edge_idx].dst;
501 let policy = self.edges[edge_idx].policy;
502 let capacity = self.edges[edge_idx].capacity;
503 self.edges[edge_idx].dst = PadId {
505 node: new,
506 index: 0,
507 };
508 self.edges.push(Edge {
510 src: PadId {
511 node: new,
512 index: 0,
513 },
514 dst: old_dst,
515 policy,
516 capacity,
517 });
518 new
519 }
520
521 pub fn node_count(&self) -> usize {
525 self.nodes.len()
526 }
527
528 pub fn node_kind(&self, node: NodeId) -> Option<NodeKind> {
530 self.nodes.get(node.0 as usize).map(|n| n.kind)
531 }
532
533 pub fn element(&self, node: NodeId) -> Option<&E> {
536 self.nodes
537 .get(node.0 as usize)
538 .and_then(|n| n.element.as_ref())
539 }
540
541 pub fn merge(&mut self, inner: Graph<E>) -> NodeIdOffset {
550 let offset = NodeIdOffset(self.nodes.len() as u32);
551 self.nodes.extend(inner.nodes);
552 for e in inner.edges {
553 self.edges.push(Edge {
554 src: offset.apply_pad(e.src),
555 dst: offset.apply_pad(e.dst),
556 policy: e.policy,
557 capacity: e.capacity,
558 });
559 }
560 offset
561 }
562
563 pub fn add_bin(&mut self, bin: Bin<E>) -> BinInstance {
569 let Bin {
570 graph,
571 ghost_in,
572 ghost_out,
573 } = bin;
574 let offset = self.merge(graph);
575 BinInstance {
576 ghost_in: ghost_in.into_iter().map(|p| offset.apply_pad(p)).collect(),
577 ghost_out: ghost_out.into_iter().map(|p| offset.apply_pad(p)).collect(),
578 }
579 }
580
581 fn kind_of(&self, node: NodeId) -> Result<NodeKind, GraphError> {
582 self.nodes
583 .get(node.0 as usize)
584 .map(|n| n.kind)
585 .ok_or(GraphError::UnknownNode(node))
586 }
587
588 fn check_pad(&self, pad: PadId, direction: PadDir) -> Result<(), GraphError> {
589 let kind = self.kind_of(pad.node)?;
590 let count = match direction {
591 PadDir::In => kind.in_pads(),
592 PadDir::Out => kind.out_pads(),
593 };
594 if pad.index >= count {
595 return Err(GraphError::PadOutOfRange {
596 node: pad.node,
597 index: pad.index,
598 direction,
599 });
600 }
601 Ok(())
602 }
603
604 pub fn finish(self) -> Result<ValidatedGraph<E>, GraphError> {
606 let n = self.nodes.len();
607 let mut in_edges: Vec<Vec<usize>> = vec![Vec::new(); n];
608 let mut out_edges: Vec<Vec<usize>> = vec![Vec::new(); n];
609 for (eid, e) in self.edges.iter().enumerate() {
610 out_edges[e.src.node.0 as usize].push(eid);
611 in_edges[e.dst.node.0 as usize].push(eid);
612 }
613
614 for (i, node) in self.nodes.iter().enumerate() {
615 let id = NodeId(i as u32);
616 if in_edges[i].is_empty() && out_edges[i].is_empty() {
617 return Err(GraphError::OrphanNode(id));
618 }
619 if matches!(
620 node.kind,
621 NodeKind::Tee(0)
622 | NodeKind::Muxer(0)
623 | NodeKind::FaninSink(0)
624 | NodeKind::FanoutSrc(0)
625 ) {
626 return Err(GraphError::DegenerateFanNode(id));
627 }
628 check_pads(
629 node.kind.in_pads(),
630 in_edges[i].iter().map(|&e| self.edges[e].dst.index),
631 id,
632 PadDir::In,
633 )?;
634 check_pads(
635 node.kind.out_pads(),
636 out_edges[i].iter().map(|&e| self.edges[e].src.index),
637 id,
638 PadDir::Out,
639 )?;
640 }
641
642 let topo = topo_sort(n, &in_edges, &out_edges, &self.edges)?;
643 Ok(ValidatedGraph {
644 nodes: self.nodes,
645 edges: self.edges,
646 topo,
647 in_edges,
648 out_edges,
649 })
650 }
651}
652
653impl<E> core::fmt::Debug for Graph<E> {
654 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
655 let kinds: Vec<NodeKind> = self.nodes.iter().map(|n| n.kind).collect();
656 f.debug_struct("Graph")
657 .field("nodes", &kinds)
658 .field("edges", &self.edges)
659 .finish()
660 }
661}
662
663pub struct Bin<E> {
674 graph: Graph<E>,
675 ghost_in: Vec<PadId>,
676 ghost_out: Vec<PadId>,
677}
678
679impl<E> Default for Bin<E> {
680 fn default() -> Self {
681 Self::new()
682 }
683}
684
685impl<E> Bin<E> {
686 pub fn new() -> Self {
687 Self {
688 graph: Graph::new(),
689 ghost_in: Vec::new(),
690 ghost_out: Vec::new(),
691 }
692 }
693
694 pub fn add_source(&mut self, element: E) -> NodeId {
695 self.graph.add_source(element)
696 }
697
698 pub fn add_transform(&mut self, element: E) -> NodeId {
699 self.graph.add_transform(element)
700 }
701
702 pub fn add_sink(&mut self, element: E) -> NodeId {
703 self.graph.add_sink(element)
704 }
705
706 pub fn add_tee(&mut self, outputs: u8) -> Tee {
707 self.graph.add_tee(outputs)
708 }
709
710 pub fn add_muxer(&mut self, element: E, inputs: u8) -> Muxer {
711 self.graph.add_muxer(element, inputs)
712 }
713
714 pub fn link(&mut self, from: impl Into<PadId>, to: impl Into<PadId>) -> Result<(), GraphError> {
715 self.graph.link(from, to)
716 }
717
718 pub fn link_with(
719 &mut self,
720 from: impl Into<PadId>,
721 to: impl Into<PadId>,
722 policy: LinkPolicy,
723 ) -> Result<(), GraphError> {
724 self.graph.link_with(from, to, policy)
725 }
726
727 pub fn ghost_input(&mut self, interior: impl Into<PadId>) -> Result<(), GraphError> {
730 let pad = interior.into();
731 self.graph.check_pad(pad, PadDir::In)?;
732 if self.ghost_in.contains(&pad) {
733 return Err(GraphError::DuplicateGhostPad {
734 node: pad.node,
735 index: pad.index,
736 direction: PadDir::In,
737 });
738 }
739 self.ghost_in.push(pad);
740 Ok(())
741 }
742
743 pub fn ghost_output(&mut self, interior: impl Into<PadId>) -> Result<(), GraphError> {
746 let pad = interior.into();
747 self.graph.check_pad(pad, PadDir::Out)?;
748 if self.ghost_out.contains(&pad) {
749 return Err(GraphError::DuplicateGhostPad {
750 node: pad.node,
751 index: pad.index,
752 direction: PadDir::Out,
753 });
754 }
755 self.ghost_out.push(pad);
756 Ok(())
757 }
758}
759
760impl<E> core::fmt::Debug for Bin<E> {
761 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
762 f.debug_struct("Bin")
763 .field("graph", &self.graph)
764 .field("ghost_in", &self.ghost_in)
765 .field("ghost_out", &self.ghost_out)
766 .finish()
767 }
768}
769
770#[derive(Debug, Clone)]
773pub struct BinInstance {
774 ghost_in: Vec<PadId>,
775 ghost_out: Vec<PadId>,
776}
777
778impl BinInstance {
779 pub fn input(&self, i: usize) -> PadId {
781 self.ghost_in[i]
782 }
783
784 pub fn output(&self, i: usize) -> PadId {
786 self.ghost_out[i]
787 }
788
789 pub fn input_count(&self) -> usize {
791 self.ghost_in.len()
792 }
793
794 pub fn output_count(&self) -> usize {
796 self.ghost_out.len()
797 }
798}
799
800pub struct ValidatedGraph<E> {
804 nodes: Vec<Node<E>>,
805 edges: Vec<Edge>,
806 topo: Vec<NodeId>,
807 in_edges: Vec<Vec<usize>>,
808 out_edges: Vec<Vec<usize>>,
809}
810
811impl<E> ValidatedGraph<E> {
812 pub fn node_count(&self) -> usize {
813 self.nodes.len()
814 }
815
816 pub fn edge_count(&self) -> usize {
817 self.edges.len()
818 }
819
820 pub fn topo(&self) -> &[NodeId] {
822 &self.topo
823 }
824
825 pub fn kind(&self, node: NodeId) -> NodeKind {
826 self.nodes[node.0 as usize].kind
827 }
828
829 pub fn fanout_policy(&self, node: NodeId) -> FanOutPolicy {
831 self.nodes[node.0 as usize].fanout
832 }
833
834 pub fn node_name(&self, node: NodeId) -> Option<&str> {
837 self.nodes[node.0 as usize].name.as_deref()
838 }
839
840 pub fn node_log_category(&self, node: NodeId) -> Option<&str> {
843 self.nodes[node.0 as usize].log_category.as_deref()
844 }
845
846 pub fn edge(&self, id: usize) -> &Edge {
847 &self.edges[id]
848 }
849
850 pub fn edges(&self) -> &[Edge] {
853 &self.edges
854 }
855
856 pub fn in_edges(&self, node: NodeId) -> &[usize] {
858 &self.in_edges[node.0 as usize]
859 }
860
861 pub fn out_edges(&self, node: NodeId) -> &[usize] {
863 &self.out_edges[node.0 as usize]
864 }
865
866 pub fn take_element(&mut self, node: NodeId) -> Option<E> {
869 self.nodes[node.0 as usize].element.take()
870 }
871
872 #[cfg(feature = "runtime")]
876 pub fn take_node_control(&mut self, node: NodeId) -> Option<crate::controller::ControlProgram> {
877 self.nodes[node.0 as usize].control.take()
878 }
879
880 pub fn element(&self, node: NodeId) -> Option<&E> {
883 self.nodes[node.0 as usize].element.as_ref()
884 }
885
886 pub fn element_mut(&mut self, node: NodeId) -> Option<&mut E> {
889 self.nodes[node.0 as usize].element.as_mut()
890 }
891}
892
893impl<E> core::fmt::Debug for ValidatedGraph<E> {
894 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
895 let kinds: Vec<NodeKind> = self.nodes.iter().map(|n| n.kind).collect();
896 f.debug_struct("ValidatedGraph")
897 .field("nodes", &kinds)
898 .field("edges", &self.edges)
899 .field("topo", &self.topo)
900 .finish()
901 }
902}
903
904fn check_pads(
906 count: u8,
907 indices: impl Iterator<Item = u8>,
908 node: NodeId,
909 direction: PadDir,
910) -> Result<(), GraphError> {
911 let mut seen = vec![0u32; count as usize];
912 for idx in indices {
913 seen[idx as usize] += 1;
915 }
916 for (idx, &c) in seen.iter().enumerate() {
917 let index = idx as u8;
918 if c == 0 {
919 return Err(GraphError::UnlinkedPad {
920 node,
921 index,
922 direction,
923 });
924 }
925 if c > 1 {
926 return Err(GraphError::PadCountMismatch {
927 node,
928 index,
929 direction,
930 });
931 }
932 }
933 Ok(())
934}
935
936fn topo_sort(
939 n: usize,
940 in_edges: &[Vec<usize>],
941 out_edges: &[Vec<usize>],
942 edges: &[Edge],
943) -> Result<Vec<NodeId>, GraphError> {
944 let mut indeg: Vec<usize> = in_edges.iter().map(|e| e.len()).collect();
945 let mut queue: Vec<usize> = (0..n).filter(|&i| indeg[i] == 0).collect();
946 let mut topo: Vec<NodeId> = Vec::with_capacity(n);
947 let mut processed = vec![false; n];
948
949 let mut head = 0;
950 while head < queue.len() {
951 let node = queue[head];
952 head += 1;
953 processed[node] = true;
954 topo.push(NodeId(node as u32));
955 for &eid in &out_edges[node] {
956 let succ = edges[eid].dst.node.0 as usize;
957 indeg[succ] -= 1;
958 if indeg[succ] == 0 {
959 queue.push(succ);
960 }
961 }
962 }
963
964 if topo.len() < n {
965 let nodes = (0..n)
966 .filter(|&i| !processed[i])
967 .map(|i| NodeId(i as u32))
968 .collect();
969 return Err(GraphError::Cycle { nodes });
970 }
971 Ok(topo)
972}
973
974#[cfg(test)]
975mod tests {
976 use super::*;
977
978 type G = Graph<&'static str>;
980
981 #[test]
982 fn linear_chain_validates_in_topo_order() {
983 let mut g = G::new();
984 let src = g.add_source("src");
985 let tx = g.add_transform("tx");
986 let sink = g.add_sink("sink");
987 g.link(src, tx).unwrap();
988 g.link(tx, sink).unwrap();
989 let v = g.finish().expect("linear chain validates");
990 assert_eq!(v.topo(), &[src, tx, sink]);
991 assert_eq!(v.in_edges(src).len(), 0);
992 assert_eq!(v.out_edges(sink).len(), 0);
993 }
994
995 #[test]
996 fn fan_out_through_tee_validates() {
997 let mut g = G::new();
998 let src = g.add_source("src");
999 let tee = g.add_tee(2);
1000 let a = g.add_sink("a");
1001 let b = g.add_sink("b");
1002 g.link(src, tee.input()).unwrap();
1003 g.link(tee.out(0), a).unwrap();
1004 g.link(tee.out(1), b).unwrap();
1005 let v = g.finish().expect("fan-out validates");
1006 assert_eq!(v.out_edges(tee.node()).len(), 2);
1007 let pos = |n: NodeId| v.topo().iter().position(|&x| x == n).unwrap();
1009 assert!(pos(src) < pos(tee.node()));
1010 assert!(pos(tee.node()) < pos(a) && pos(tee.node()) < pos(b));
1011 }
1012
1013 #[test]
1014 fn fan_in_through_muxer_validates() {
1015 let mut g = G::new();
1016 let s0 = g.add_source("s0");
1017 let s1 = g.add_source("s1");
1018 let mux = g.add_muxer("mux", 2);
1019 let sink = g.add_sink("sink");
1020 g.link(s0, mux.input(0)).unwrap();
1021 g.link(s1, mux.input(1)).unwrap();
1022 g.link(mux.output(), sink).unwrap();
1023 let v = g.finish().expect("fan-in validates");
1024 assert_eq!(v.in_edges(mux.node()).len(), 2);
1025 }
1026
1027 #[test]
1028 fn tee_to_muxer_diamond_validates() {
1029 let mut g = G::new();
1030 let src = g.add_source("src");
1031 let tee = g.add_tee(2);
1032 let a = g.add_transform("a");
1033 let b = g.add_transform("b");
1034 let mux = g.add_muxer("mux", 2);
1035 let sink = g.add_sink("sink");
1036 g.link(src, tee.input()).unwrap();
1037 g.link(tee.out(0), a).unwrap();
1038 g.link(tee.out(1), b).unwrap();
1039 g.link(a, mux.input(0)).unwrap();
1040 g.link(b, mux.input(1)).unwrap();
1041 g.link(mux.output(), sink).unwrap();
1042 let v = g.finish().expect("diamond validates");
1043 assert_eq!(v.node_count(), 6);
1044 let pos = |n: NodeId| v.topo().iter().position(|&x| x == n).unwrap();
1045 assert!(pos(a) < pos(mux.node()) && pos(b) < pos(mux.node()));
1046 }
1047
1048 #[test]
1049 fn cycle_is_rejected() {
1050 let mut g = G::new();
1052 let a = g.add_transform("a");
1053 let b = g.add_transform("b");
1054 g.link(a, b).unwrap();
1055 g.link(b, a).unwrap();
1056 match g.finish() {
1057 Err(GraphError::Cycle { nodes }) => {
1058 assert_eq!(nodes.len(), 2);
1059 assert!(nodes.contains(&a) && nodes.contains(&b));
1060 }
1061 other => panic!("expected Cycle, got {other:?}"),
1062 }
1063 }
1064
1065 #[test]
1066 fn unlinked_pad_is_rejected() {
1067 let mut g = G::new();
1069 let src = g.add_source("src");
1070 let tee = g.add_tee(2);
1071 let a = g.add_sink("a");
1072 g.link(src, tee.input()).unwrap();
1073 g.link(tee.out(0), a).unwrap();
1074 match g.finish() {
1075 Err(GraphError::UnlinkedPad {
1076 node,
1077 index,
1078 direction,
1079 }) => {
1080 assert_eq!((node, index, direction), (tee.node(), 1, PadDir::Out));
1081 }
1082 other => panic!("expected UnlinkedPad, got {other:?}"),
1083 }
1084 }
1085
1086 #[test]
1087 fn double_linked_pad_is_rejected() {
1088 let mut g = G::new();
1090 let s0 = g.add_source("s0");
1091 let s1 = g.add_source("s1");
1092 let sink = g.add_sink("sink");
1093 g.link(s0, sink).unwrap();
1094 g.link(s1, sink).unwrap();
1095 match g.finish() {
1096 Err(GraphError::PadCountMismatch {
1097 node,
1098 index,
1099 direction,
1100 }) => {
1101 assert_eq!((node, index, direction), (sink, 0, PadDir::In));
1102 }
1103 other => panic!("expected PadCountMismatch, got {other:?}"),
1104 }
1105 }
1106
1107 #[test]
1108 fn orphan_node_is_rejected() {
1109 let mut g = G::new();
1110 let src = g.add_source("src");
1111 let sink = g.add_sink("sink");
1112 let _orphan = g.add_transform("orphan");
1113 g.link(src, sink).unwrap();
1114 assert_eq!(g.finish().err(), Some(GraphError::OrphanNode(NodeId(2))));
1115 }
1116
1117 #[test]
1118 fn zero_output_tee_is_rejected() {
1119 let mut g = G::new();
1121 let src = g.add_source("src");
1122 let tee = g.add_tee(0);
1123 g.link(src, tee.input()).unwrap();
1124 assert_eq!(
1125 g.finish().err(),
1126 Some(GraphError::DegenerateFanNode(tee.node()))
1127 );
1128 }
1129
1130 #[test]
1131 fn pad_index_out_of_range_is_rejected_at_link() {
1132 let mut g = G::new();
1133 let src = g.add_source("src");
1134 let tee = g.add_tee(2);
1135 let s = g.add_sink("s");
1136 g.link(src, tee.input()).unwrap();
1137 assert_eq!(
1139 g.link(tee.out(2), s).err(),
1140 Some(GraphError::PadOutOfRange {
1141 node: tee.node(),
1142 index: 2,
1143 direction: PadDir::Out
1144 })
1145 );
1146 }
1147
1148 #[test]
1149 fn take_element_moves_payload_once() {
1150 let mut g = G::new();
1151 let src = g.add_source("src");
1152 let sink = g.add_sink("sink");
1153 g.link(src, sink).unwrap();
1154 let mut v = g.finish().unwrap();
1155 assert_eq!(v.take_element(src), Some("src"));
1156 assert_eq!(v.take_element(src), None, "payload taken only once");
1157 }
1158
1159 #[test]
1160 fn merge_offsets_node_ids_and_edges() {
1161 let mut host = G::new();
1164 let h0 = host.add_source("h0");
1165 assert_eq!(h0, NodeId(0));
1166
1167 let mut inner = G::new();
1168 let i0 = inner.add_transform("i0");
1169 let i1 = inner.add_sink("i1");
1170 inner.link(i0, i1).unwrap();
1171
1172 let off = host.merge(inner);
1173 assert_eq!(off.apply(i0), NodeId(1));
1175 assert_eq!(off.apply(i1), NodeId(2));
1176 host.link(h0, off.apply(i0)).unwrap();
1178 let v = host.finish().expect("merged graph validates");
1179 assert_eq!(v.node_count(), 3);
1180 assert_eq!(v.topo(), &[NodeId(0), NodeId(1), NodeId(2)]);
1181 assert_eq!(v.element(NodeId(1)), Some(&"i0"));
1183 }
1184
1185 #[test]
1186 fn add_bin_flattens_with_ghost_pads() {
1187 let mut bin: Bin<&'static str> = Bin::new();
1190 let a = bin.add_transform("a");
1191 let b = bin.add_transform("b");
1192 bin.link(a, b).unwrap();
1193 bin.ghost_input(a).unwrap();
1194 bin.ghost_output(b).unwrap();
1195
1196 let mut g = G::new();
1197 let src = g.add_source("src");
1198 let sink = g.add_sink("sink");
1199 let inst = g.add_bin(bin);
1200 assert_eq!(inst.input_count(), 1);
1201 assert_eq!(inst.output_count(), 1);
1202 g.link(src, inst.input(0)).unwrap();
1203 g.link(inst.output(0), sink).unwrap();
1204
1205 let v = g.finish().expect("flattened bin validates");
1206 assert_eq!(v.node_count(), 4);
1208 let pos = |n: NodeId| v.topo().iter().position(|&x| x == n).unwrap();
1209 assert!(pos(src) < pos(inst.input(0).node));
1210 assert!(pos(inst.output(0).node) < pos(sink));
1211 }
1212
1213 #[test]
1214 fn bin_ghosts_an_interior_tee_output() {
1215 let mut bin: Bin<&'static str> = Bin::new();
1217 let tx = bin.add_transform("tx");
1218 let tee = bin.add_tee(2);
1219 bin.link(tx, tee.input()).unwrap();
1220 bin.ghost_input(tx).unwrap();
1221 bin.ghost_output(tee.out(0)).unwrap();
1222 bin.ghost_output(tee.out(1)).unwrap();
1223
1224 let mut g = G::new();
1225 let src = g.add_source("src");
1226 let a = g.add_sink("a");
1227 let b = g.add_sink("b");
1228 let inst = g.add_bin(bin);
1229 g.link(src, inst.input(0)).unwrap();
1230 g.link(inst.output(0), a).unwrap();
1231 g.link(inst.output(1), b).unwrap();
1232 let v = g.finish().expect("bin with a ghosted tee validates");
1233 assert_eq!(inst.output(0).node, inst.output(1).node);
1235 assert_ne!(inst.output(0).index, inst.output(1).index);
1236 assert_eq!(v.out_edges(inst.output(0).node).len(), 2);
1237 }
1238
1239 #[test]
1240 fn duplicate_ghost_pad_is_rejected() {
1241 let mut bin: Bin<&'static str> = Bin::new();
1242 let a = bin.add_transform("a");
1243 bin.ghost_output(a).unwrap();
1244 assert_eq!(
1245 bin.ghost_output(a),
1246 Err(GraphError::DuplicateGhostPad {
1247 node: a,
1248 index: 0,
1249 direction: PadDir::Out
1250 }),
1251 "the same interior pad cannot back two ghosts",
1252 );
1253 }
1254
1255 #[test]
1256 fn ghost_pad_out_of_range_is_rejected() {
1257 let mut bin: Bin<&'static str> = Bin::new();
1258 let a = bin.add_transform("a");
1259 assert!(matches!(
1261 bin.ghost_output(PadId { node: a, index: 1 }),
1262 Err(GraphError::PadOutOfRange { .. }),
1263 ));
1264 }
1265}