1use std::{
14 collections::{HashMap, HashSet},
15 fmt::{self, Write as _},
16 sync::{Arc, Mutex},
17};
18
19use thiserror::Error as ThisError;
20
21use crate::{
22 contract::{InputContract, OutputContract, PortContract},
23 element::ElementType,
24 log::{Level, enabled},
25 pp_log::{PpLog, pp_info},
26};
27
28#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
31pub struct ElementId(u64);
32
33impl ElementId {
34 #[cfg(test)]
35 pub(crate) const fn for_test(value: u64) -> Self {
36 Self(value)
37 }
38}
39
40impl fmt::Display for ElementId {
41 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
42 self.0.fmt(f)
43 }
44}
45
46#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
48pub struct EdgeId(u64);
49
50#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
53pub struct BranchId(u64);
54
55impl fmt::Display for BranchId {
56 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
57 self.0.fmt(f)
58 }
59}
60
61#[derive(Debug, Clone, PartialEq, Eq)]
62pub struct NodeInfo {
65 pub id: ElementId,
67 pub element_type: ElementType,
69 pub name: Arc<str>,
71}
72
73#[derive(Debug, Clone, PartialEq, Eq)]
74pub struct PortRef {
76 pub element: ElementId,
78 pub port: Arc<str>,
80}
81
82#[derive(Debug, Clone, PartialEq, Eq)]
83pub struct EdgeInfo {
86 pub id: EdgeId,
88 pub branch_id: BranchId,
90 pub from: PortRef,
92 pub to: PortRef,
94}
95
96#[derive(Debug, Clone)]
97pub struct GraphSnapshot {
105 pub revision: u64,
108 pub nodes: Vec<NodeInfo>,
110 pub edges: Vec<EdgeInfo>,
112}
113
114impl GraphSnapshot {
115 pub fn node(&self, id: ElementId) -> Option<&NodeInfo> {
117 self.nodes.iter().find(|node| node.id == id)
118 }
119
120 pub fn terminal_ids(&self) -> Vec<ElementId> {
126 self.nodes
127 .iter()
128 .filter(|node| {
129 node.element_type != ElementType::Tee
130 && self.edges.iter().any(|edge| edge.to.element == node.id)
131 && !self.edges.iter().any(|edge| edge.from.element == node.id)
132 })
133 .map(|node| node.id)
134 .collect()
135 }
136
137 pub(crate) fn terminal_ids_from(&self, root: ElementId) -> Vec<ElementId> {
143 let terminals: HashSet<_> = self.terminal_ids().into_iter().collect();
144 let mut found = Vec::new();
145 let mut visiting = vec![root];
146 let mut visited = HashSet::new();
147 while let Some(current) = visiting.pop() {
148 if !visited.insert(current) {
149 continue;
150 }
151 if terminals.contains(¤t) {
152 found.push(current);
153 continue;
154 }
155 visiting.extend(
156 self.edges
157 .iter()
158 .filter(|edge| edge.from.element == current)
159 .map(|edge| edge.to.element),
160 );
161 }
162 found
163 }
164
165 pub fn topology(&self) -> String {
169 self.paths()
170 .into_iter()
171 .map(|path| {
172 path.into_iter()
173 .filter_map(|id| self.node(id))
174 .map(|node| format!("{:?}({})", node.element_type, node.name))
175 .collect::<Vec<_>>()
176 .join(" - ")
177 })
178 .collect::<Vec<_>>()
179 .join("\n")
180 }
181
182 pub(crate) fn topology_diagram(&self) -> String {
186 let roots: Vec<_> = self
187 .nodes
188 .iter()
189 .filter(|node| !self.edges.iter().any(|edge| edge.to.element == node.id))
190 .collect();
191 let mut output = String::new();
192
193 for (index, root) in roots.iter().enumerate() {
194 let is_last = index + 1 == roots.len();
195 let child_indent = if roots.len() == 1 {
196 let _ = write!(output, "{:?}({})#{}", root.element_type, root.name, root.id);
197 String::new()
198 } else {
199 let connector = if is_last { "└── " } else { "├── " };
200 let _ = write!(
201 output,
202 "{connector}{:?}({})#{}",
203 root.element_type, root.name, root.id
204 );
205 if is_last {
206 " ".to_owned()
207 } else {
208 "│ ".to_owned()
209 }
210 };
211 self.render_diagram_children(
212 root.id,
213 &child_indent,
214 &mut HashSet::from([root.id]),
215 &mut output,
216 );
217 if !is_last {
218 output.push('\n');
219 }
220 }
221
222 output
223 }
224
225 fn render_diagram_children(
226 &self,
227 parent: ElementId,
228 indent: &str,
229 visiting: &mut HashSet<ElementId>,
230 output: &mut String,
231 ) {
232 let children: Vec<_> = self
233 .edges
234 .iter()
235 .filter(|edge| edge.from.element == parent)
236 .collect();
237
238 for (index, edge) in children.iter().enumerate() {
239 let Some(child) = self.node(edge.to.element) else {
240 continue;
241 };
242 let is_last = index + 1 == children.len();
243 let connector = if is_last { "└── " } else { "├── " };
244 let link = format!("[{}] → ", edge.from.port);
245 let _ = write!(
246 output,
247 "\n{indent}{connector}{link}{:?}({})#{}",
248 child.element_type, child.name, child.id
249 );
250
251 if visiting.insert(child.id) {
252 let continuation = if is_last { " " } else { "│ " };
253 let child_indent =
254 format!("{indent}{continuation}{}", " ".repeat(link.chars().count()));
255 self.render_diagram_children(child.id, &child_indent, visiting, output);
256 visiting.remove(&child.id);
257 }
258 }
259 }
260
261 fn paths(&self) -> Vec<Vec<ElementId>> {
262 let leaves: Vec<_> = self
263 .nodes
264 .iter()
265 .filter(|node| !self.edges.iter().any(|edge| edge.from.element == node.id))
266 .collect();
267 let mut rendered = Vec::new();
268 for leaf in leaves {
269 self.paths_to(leaf.id, &mut HashSet::new(), &mut Vec::new(), &mut rendered);
270 }
271 rendered
272 }
273
274 fn paths_to(
275 &self,
276 current: ElementId,
277 visiting: &mut HashSet<ElementId>,
278 suffix: &mut Vec<ElementId>,
279 paths: &mut Vec<Vec<ElementId>>,
280 ) {
281 if !visiting.insert(current) {
282 return;
283 }
284 suffix.push(current);
285 let upstream: Vec<_> = self
286 .edges
287 .iter()
288 .filter(|edge| edge.to.element == current)
289 .map(|edge| edge.from.element)
290 .collect();
291 if upstream.is_empty() {
292 let mut path = suffix.clone();
293 path.reverse();
294 paths.push(path);
295 } else {
296 for parent in upstream {
297 self.paths_to(parent, visiting, suffix, paths);
298 }
299 }
300 suffix.pop();
301 visiting.remove(¤t);
302 }
303}
304
305pub(crate) fn log_topology(pp_log: &PpLog, event: &str, snapshot: &GraphSnapshot) {
314 if !enabled(Level::Info) {
315 return;
316 }
317 pp_info!(pp_log: pp_log, "{event}\n{}", snapshot.topology_diagram());
318}
319
320#[derive(Debug, ThisError, PartialEq, Eq)]
321pub enum GraphError {
327 #[error("source pad index {index} is out of range (source has {pad_count} pads)")]
329 PadOutOfRange {
330 index: usize,
332 pad_count: usize,
334 },
335
336 #[error("source pad '{0}' is already linked")]
338 PadAlreadyLinked(String),
339
340 #[error("element {0} is not attached to this pipeline")]
342 ParentNotAttached(ElementId),
343
344 #[error("element {0} is already attached to this pipeline")]
346 NodeAlreadyAttached(ElementId),
347
348 #[error("branch {0} is not attached")]
350 BranchNotAttached(BranchId),
351
352 #[error("a pipeline timeline operation is in progress")]
355 TimelineOperationInProgress,
356
357 #[error("a branch must contain at least one element")]
359 EmptyBranch,
360
361 #[error("{producer} produces {produced}, which {consumer} cannot accept (it takes {accepted})")]
366 IncompatibleLink {
367 producer: Arc<str>,
369 produced: PortContract,
371 consumer: Arc<str>,
373 accepted: PortContract,
375 },
376
377 #[error("ChainBuilder::pipe requires exactly one output pad, but {name} has {count}")]
380 NotSingleOutput {
381 name: Arc<str>,
383 count: usize,
385 },
386}
387
388#[derive(Debug, Clone)]
389pub(crate) struct PlannedEdge {
390 pub from: PortRef,
391 pub to: PortRef,
392}
393
394#[derive(Debug, Clone)]
399pub(crate) struct ResolvedFlow {
400 pub producer: Arc<str>,
401 pub contract: PortContract,
402}
403
404#[derive(Debug)]
406pub(crate) enum Incoming {
407 Known(Option<ResolvedFlow>),
409 FromParent,
413}
414
415#[derive(Debug, Clone, Copy)]
418pub(crate) struct PortContracts {
419 pub input: InputContract,
420 pub output: OutputContract,
421}
422
423#[derive(Debug)]
424pub(crate) struct BranchPlan {
425 pub nodes: Vec<NodeInfo>,
426 pub edges: Vec<PlannedEdge>,
427 pub root: ElementId,
428 pub contracts: HashMap<ElementId, PortContracts>,
433}
434
435impl BranchPlan {
436 pub(crate) fn resolve(
449 &self,
450 incoming: Option<ResolvedFlow>,
451 ) -> Result<HashMap<ElementId, Option<ResolvedFlow>>, GraphError> {
452 let mut outgoing_by_node = HashMap::new();
453 let name_of = |id: ElementId| {
454 self.nodes
455 .iter()
456 .find(|node| node.id == id)
457 .map(|node| node.name.clone())
458 .unwrap_or_else(|| "<unknown>".into())
459 };
460
461 let mut visited = HashSet::new();
464 let mut pending = vec![(self.root, incoming)];
465 while let Some((id, flow)) = pending.pop() {
466 if !visited.insert(id) {
467 continue;
468 }
469 let Some(contracts) = self.contracts.get(&id) else {
470 continue;
471 };
472
473 if let (Some(flow), InputContract::Fixed(accepted)) = (&flow, contracts.input)
474 && !accepted.accepts(&flow.contract)
475 {
476 return Err(GraphError::IncompatibleLink {
477 producer: flow.producer.clone(),
478 produced: flow.contract,
479 consumer: name_of(id),
480 accepted,
481 });
482 }
483
484 let outgoing = match contracts.output {
485 OutputContract::Fixed(contract) => Some(ResolvedFlow {
486 producer: name_of(id),
487 contract,
488 }),
489 OutputContract::Passthrough => flow,
490 OutputContract::Unknown => None,
491 };
492
493 outgoing_by_node.insert(id, outgoing.clone());
496 for edge in self.edges.iter().filter(|edge| edge.from.element == id) {
497 pending.push((edge.to.element, outgoing.clone()));
498 }
499 }
500 Ok(outgoing_by_node)
501 }
502}
503
504#[derive(Debug)]
505struct BranchRecord {
506 parent: ElementId,
507 owned_nodes: HashSet<ElementId>,
508}
509
510#[derive(Default)]
511struct GraphState {
512 next_element_id: u64,
513 next_edge_id: u64,
514 next_branch_id: u64,
515 revision: u64,
516 nodes: Vec<NodeInfo>,
517 edges: Vec<EdgeInfo>,
518 branches: HashMap<BranchId, BranchRecord>,
519 outgoing: HashMap<ElementId, Option<ResolvedFlow>>,
526}
527
528#[derive(Clone, Default)]
531pub struct PipelineGraph(Arc<Mutex<GraphState>>);
532
533impl PipelineGraph {
534 pub fn new() -> Self {
536 Self::default()
537 }
538
539 pub fn snapshot(&self) -> GraphSnapshot {
542 let state = self.0.lock().unwrap();
543 GraphSnapshot {
544 revision: state.revision,
545 nodes: state.nodes.clone(),
546 edges: state.edges.clone(),
547 }
548 }
549
550 #[cfg(test)]
551 pub(crate) fn resolved_output_count(&self) -> usize {
552 self.0.lock().unwrap().outgoing.len()
553 }
554
555 pub fn branch_containing(&self, element: ElementId) -> Option<BranchId> {
560 let state = self.0.lock().unwrap();
561 state
562 .branches
563 .iter()
564 .find_map(|(id, branch)| branch.owned_nodes.contains(&element).then_some(*id))
565 }
566
567 pub(crate) fn reserve_element_id(&self) -> ElementId {
568 let mut state = self.0.lock().unwrap();
569 state.next_element_id += 1;
570 ElementId(state.next_element_id)
571 }
572
573 pub(crate) fn add_source(&self, element_type: ElementType, name: Arc<str>) -> ElementId {
574 let id = self.reserve_element_id();
575 let mut state = self.0.lock().unwrap();
576 state.nodes.push(NodeInfo {
577 id,
578 element_type,
579 name,
580 });
581 state.revision += 1;
582 id
583 }
584
585 pub(crate) fn attach_with(
588 &self,
589 parent: ElementId,
590 from_port: Arc<str>,
591 incoming: Incoming,
592 plan: BranchPlan,
593 attach_runtime: impl FnOnce(BranchId) -> Result<(), GraphError>,
594 ) -> Result<BranchId, GraphError> {
595 let mut state = self.0.lock().unwrap();
596 if !state.nodes.iter().any(|node| node.id == parent) {
597 return Err(GraphError::ParentNotAttached(parent));
598 }
599 if plan.nodes.is_empty() {
600 return Err(GraphError::EmptyBranch);
601 }
602 for node in &plan.nodes {
603 if state.nodes.iter().any(|current| current.id == node.id) {
604 return Err(GraphError::NodeAlreadyAttached(node.id));
605 }
606 }
607
608 let incoming = match incoming {
611 Incoming::Known(flow) => flow,
612 Incoming::FromParent => state.outgoing.get(&parent).cloned().flatten(),
613 };
614 let outgoing = plan.resolve(incoming)?;
615
616 state.next_branch_id += 1;
617 let branch_id = BranchId(state.next_branch_id);
618 attach_runtime(branch_id)?;
619
620 let mut edges = Vec::with_capacity(plan.edges.len() + 1);
621 state.next_edge_id += 1;
622 edges.push(EdgeInfo {
623 id: EdgeId(state.next_edge_id),
624 branch_id,
625 from: PortRef {
626 element: parent,
627 port: from_port,
628 },
629 to: PortRef {
630 element: plan.root,
631 port: "sink".into(),
632 },
633 });
634 for edge in plan.edges {
635 state.next_edge_id += 1;
636 edges.push(EdgeInfo {
637 id: EdgeId(state.next_edge_id),
638 branch_id,
639 from: edge.from,
640 to: edge.to,
641 });
642 }
643
644 let owned_nodes = plan.nodes.iter().map(|node| node.id).collect();
645 state.outgoing.extend(outgoing);
646 state.nodes.extend(plan.nodes);
647 state.edges.extend(edges);
648 state.branches.insert(
649 branch_id,
650 BranchRecord {
651 parent,
652 owned_nodes,
653 },
654 );
655 state.revision += 1;
656 Ok(branch_id)
657 }
658
659 pub(crate) fn detach_with(
662 &self,
663 branch_id: BranchId,
664 detach_runtime: impl FnOnce() -> Result<(), GraphError>,
665 ) -> Result<(), GraphError> {
666 let mut state = self.0.lock().unwrap();
667 if !state.branches.contains_key(&branch_id) {
668 return Err(GraphError::BranchNotAttached(branch_id));
669 }
670 detach_runtime()?;
671
672 let mut removed_branches = HashSet::from([branch_id]);
673 let mut removed_nodes = HashSet::new();
674 loop {
675 for id in removed_branches.clone() {
676 if let Some(branch) = state.branches.get(&id) {
677 removed_nodes.extend(branch.owned_nodes.iter().copied());
678 }
679 }
680 let before = removed_branches.len();
681 for (id, branch) in &state.branches {
682 if removed_nodes.contains(&branch.parent) {
683 removed_branches.insert(*id);
684 }
685 }
686 if removed_branches.len() == before {
687 break;
688 }
689 }
690
691 state
692 .branches
693 .retain(|id, _| !removed_branches.contains(id));
694 state.nodes.retain(|node| !removed_nodes.contains(&node.id));
695 state
696 .edges
697 .retain(|edge| !removed_branches.contains(&edge.branch_id));
698 state
699 .outgoing
700 .retain(|element, _| !removed_nodes.contains(element));
701 state.revision += 1;
702 Ok(())
703 }
704}