1use std::collections::{HashMap, HashSet};
7
8pub type PortName = String;
10
11#[derive(Debug, Clone)]
13pub enum PortSlotSpec {
14 Infinite,
16 Finite(u64),
18}
19
20#[derive(Debug, Clone)]
22pub struct Port {
23 pub slots_spec: PortSlotSpec,
25}
26
27#[derive(Debug, Clone)]
32pub enum PortState {
33 Empty,
35 Full,
37 NonEmpty,
39 NonFull,
41 Equals(u64),
43 LessThan(u64),
45 GreaterThan(u64),
47 EqualsOrLessThan(u64),
49 EqualsOrGreaterThan(u64),
51}
52
53#[derive(Debug, Clone)]
58pub enum SalvoConditionTerm {
59 Port { port_name: String, state: PortState },
61 And(Vec<Self>),
63 Or(Vec<Self>),
65 Not(Box<Self>),
67}
68
69pub fn evaluate_salvo_condition(
79 term: &SalvoConditionTerm,
80 port_packet_counts: &HashMap<PortName, u64>,
81 ports: &HashMap<PortName, Port>,
82) -> bool {
83 match term {
84 SalvoConditionTerm::Port { port_name, state } => {
85 let count = *port_packet_counts.get(port_name).unwrap_or(&0);
86 let port = ports.get(port_name);
87
88 match state {
89 PortState::Empty => count == 0,
90 PortState::Full => match port {
91 Some(p) => match p.slots_spec {
92 PortSlotSpec::Infinite => false, PortSlotSpec::Finite(max) => count >= max,
94 },
95 None => false,
96 },
97 PortState::NonEmpty => count > 0,
98 PortState::NonFull => match port {
99 Some(p) => match p.slots_spec {
100 PortSlotSpec::Infinite => true, PortSlotSpec::Finite(max) => count < max,
102 },
103 None => true,
104 },
105 PortState::Equals(n) => count == *n,
106 PortState::LessThan(n) => count < *n,
107 PortState::GreaterThan(n) => count > *n,
108 PortState::EqualsOrLessThan(n) => count <= *n,
109 PortState::EqualsOrGreaterThan(n) => count >= *n,
110 }
111 }
112 SalvoConditionTerm::And(terms) => {
113 terms.iter().all(|t| evaluate_salvo_condition(t, port_packet_counts, ports))
114 }
115 SalvoConditionTerm::Or(terms) => {
116 terms.iter().any(|t| evaluate_salvo_condition(t, port_packet_counts, ports))
117 }
118 SalvoConditionTerm::Not(inner) => {
119 !evaluate_salvo_condition(inner, port_packet_counts, ports)
120 }
121 }
122}
123
124pub type SalvoConditionName = String;
126
127#[derive(Debug, Clone)]
133pub struct SalvoCondition {
134 pub max_salvos: u64,
138 pub ports: Vec<PortName>,
140 pub term: SalvoConditionTerm,
142}
143
144fn collect_ports_from_term(term: &SalvoConditionTerm, ports: &mut HashSet<PortName>) {
146 match term {
147 SalvoConditionTerm::Port { port_name, .. } => {
148 ports.insert(port_name.clone());
149 }
150 SalvoConditionTerm::And(terms) | SalvoConditionTerm::Or(terms) => {
151 for t in terms {
152 collect_ports_from_term(t, ports);
153 }
154 }
155 SalvoConditionTerm::Not(inner) => {
156 collect_ports_from_term(inner, ports);
157 }
158 }
159}
160
161#[derive(Debug, Clone, PartialEq, thiserror::Error)]
163pub enum GraphValidationError {
164 #[error("edge {edge_source} -> {edge_target} references non-existent node '{missing_node}'")]
166 EdgeReferencesNonexistentNode {
167 edge_source: PortRef,
168 edge_target: PortRef,
169 missing_node: NodeName,
170 },
171 #[error("edge {edge_source} -> {edge_target} references non-existent port {missing_port}")]
173 EdgeReferencesNonexistentPort {
174 edge_source: PortRef,
175 edge_target: PortRef,
176 missing_port: PortRef,
177 },
178 #[error("edge source {edge_source} must be an output port")]
180 EdgeSourceNotOutputPort {
181 edge_source: PortRef,
182 edge_target: PortRef,
183 },
184 #[error("edge target {edge_target} must be an input port")]
186 EdgeTargetNotInputPort {
187 edge_source: PortRef,
188 edge_target: PortRef,
189 },
190 #[error("{condition_type} salvo condition '{condition_name}' on node '{node_name}' references non-existent port '{missing_port}'", condition_type = if *is_input_condition { "input" } else { "output" })]
192 SalvoConditionReferencesNonexistentPort {
193 node_name: NodeName,
194 condition_name: SalvoConditionName,
195 is_input_condition: bool,
196 missing_port: PortName,
197 },
198 #[error("{condition_type} salvo condition '{condition_name}' on node '{node_name}' has term referencing non-existent port '{missing_port}'", condition_type = if *is_input_condition { "input" } else { "output" })]
200 SalvoConditionTermReferencesNonexistentPort {
201 node_name: NodeName,
202 condition_name: SalvoConditionName,
203 is_input_condition: bool,
204 missing_port: PortName,
205 },
206 #[error("input salvo condition '{condition_name}' on node '{node_name}' has max_salvos={max_salvos}, but must be 1")]
208 InputSalvoConditionInvalidMaxSalvos {
209 node_name: NodeName,
210 condition_name: SalvoConditionName,
211 max_salvos: u64,
212 },
213 #[error("duplicate edge: {edge_source} -> {edge_target}")]
215 DuplicateEdge {
216 edge_source: PortRef,
217 edge_target: PortRef,
218 },
219}
220
221pub type NodeName = String;
223
224#[derive(Debug, Clone)]
233pub struct Node {
234 pub name: NodeName,
236 pub in_ports: HashMap<PortName, Port>,
238 pub out_ports: HashMap<PortName, Port>,
240 pub in_salvo_conditions: HashMap<SalvoConditionName, SalvoCondition>,
242 pub out_salvo_conditions: HashMap<SalvoConditionName, SalvoCondition>,
244}
245
246#[derive(Debug, Clone, PartialEq, Eq, Hash)]
248pub enum PortType {
249 Input,
251 Output,
253}
254
255#[derive(Debug, Clone, PartialEq, Eq, Hash)]
257pub struct PortRef {
258 pub node_name: NodeName,
260 pub port_type: PortType,
262 pub port_name: PortName,
264}
265
266impl std::fmt::Display for PortRef {
267 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
268 let port_type_str = match self.port_type {
269 PortType::Input => "in",
270 PortType::Output => "out",
271 };
272 write!(f, "{}.{}.{}", self.node_name, port_type_str, self.port_name)
273 }
274}
275
276#[derive(Debug, Clone, PartialEq, Eq, Hash)]
281pub struct Edge {
282 pub source: PortRef,
284 pub target: PortRef,
286}
287
288impl std::fmt::Display for Edge {
289 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
290 write!(f, "{} -> {}", self.source, self.target)
291 }
292}
293
294#[derive(Debug, Clone)]
331pub struct Graph {
332 nodes: HashMap<NodeName, Node>,
333 edges: HashSet<Edge>,
334 edges_by_tail: HashMap<PortRef, Edge>,
335 edges_by_head: HashMap<PortRef, Edge>,
336}
337
338impl Graph {
339 pub fn new(nodes: Vec<Node>, edges: Vec<Edge>) -> Self {
343 let nodes_map: HashMap<NodeName, Node> = nodes
344 .into_iter()
345 .map(|node| (node.name.clone(), node))
346 .collect();
347
348 let mut edges_set: HashSet<Edge> = HashSet::new();
349 let mut edges_by_tail: HashMap<PortRef, Edge> = HashMap::new();
350 let mut edges_by_head: HashMap<PortRef, Edge> = HashMap::new();
351
352 for edge in edges {
353 edges_by_tail.insert(edge.source.clone(), edge.clone());
354 edges_by_head.insert(edge.target.clone(), edge.clone());
355 edges_set.insert(edge);
356 }
357
358 Graph {
359 nodes: nodes_map,
360 edges: edges_set,
361 edges_by_tail,
362 edges_by_head,
363 }
364 }
365
366 pub fn nodes(&self) -> &HashMap<NodeName, Node> { &self.nodes }
368
369 pub fn edges(&self) -> &HashSet<Edge> { &self.edges }
371
372 pub fn get_edge_by_tail(&self, output_port_ref: &PortRef) -> Option<&Edge> {
374 self.edges_by_tail.get(output_port_ref)
375 }
376
377 pub fn get_edge_by_head(&self, input_port_ref: &PortRef) -> Option<&Edge> {
379 self.edges_by_head.get(input_port_ref)
380 }
381
382 pub fn validate(&self) -> Vec<GraphValidationError> {
386 let mut errors = Vec::new();
387
388 let mut seen_edges: HashSet<(&PortRef, &PortRef)> = HashSet::new();
390
391 for edge in &self.edges {
393 let source = &edge.source;
394 let target = &edge.target;
395
396 if !seen_edges.insert((source, target)) {
398 errors.push(GraphValidationError::DuplicateEdge {
399 edge_source: source.clone(),
400 edge_target: target.clone(),
401 });
402 continue;
403 }
404
405 let source_node = match self.nodes.get(&source.node_name) {
407 Some(node) => node,
408 None => {
409 errors.push(GraphValidationError::EdgeReferencesNonexistentNode {
410 edge_source: source.clone(),
411 edge_target: target.clone(),
412 missing_node: source.node_name.clone(),
413 });
414 continue;
415 }
416 };
417
418 let target_node = match self.nodes.get(&target.node_name) {
420 Some(node) => node,
421 None => {
422 errors.push(GraphValidationError::EdgeReferencesNonexistentNode {
423 edge_source: source.clone(),
424 edge_target: target.clone(),
425 missing_node: target.node_name.clone(),
426 });
427 continue;
428 }
429 };
430
431 if source.port_type != PortType::Output {
433 errors.push(GraphValidationError::EdgeSourceNotOutputPort {
434 edge_source: source.clone(),
435 edge_target: target.clone(),
436 });
437 } else if !source_node.out_ports.contains_key(&source.port_name) {
438 errors.push(GraphValidationError::EdgeReferencesNonexistentPort {
439 edge_source: source.clone(),
440 edge_target: target.clone(),
441 missing_port: source.clone(),
442 });
443 }
444
445 if target.port_type != PortType::Input {
447 errors.push(GraphValidationError::EdgeTargetNotInputPort {
448 edge_source: source.clone(),
449 edge_target: target.clone(),
450 });
451 } else if !target_node.in_ports.contains_key(&target.port_name) {
452 errors.push(GraphValidationError::EdgeReferencesNonexistentPort {
453 edge_source: source.clone(),
454 edge_target: target.clone(),
455 missing_port: target.clone(),
456 });
457 }
458 }
459
460 for (node_name, node) in &self.nodes {
462 for (cond_name, condition) in &node.in_salvo_conditions {
464 if condition.max_salvos != 1 {
466 errors.push(GraphValidationError::InputSalvoConditionInvalidMaxSalvos {
467 node_name: node_name.clone(),
468 condition_name: cond_name.clone(),
469 max_salvos: condition.max_salvos,
470 });
471 }
472
473 for port_name in &condition.ports {
475 if !node.in_ports.contains_key(port_name) {
476 errors.push(GraphValidationError::SalvoConditionReferencesNonexistentPort {
477 node_name: node_name.clone(),
478 condition_name: cond_name.clone(),
479 is_input_condition: true,
480 missing_port: port_name.clone(),
481 });
482 }
483 }
484
485 let mut term_ports = HashSet::new();
487 collect_ports_from_term(&condition.term, &mut term_ports);
488 for port_name in term_ports {
489 if !node.in_ports.contains_key(&port_name) {
490 errors.push(GraphValidationError::SalvoConditionTermReferencesNonexistentPort {
491 node_name: node_name.clone(),
492 condition_name: cond_name.clone(),
493 is_input_condition: true,
494 missing_port: port_name,
495 });
496 }
497 }
498 }
499
500 for (cond_name, condition) in &node.out_salvo_conditions {
502 for port_name in &condition.ports {
504 if !node.out_ports.contains_key(port_name) {
505 errors.push(GraphValidationError::SalvoConditionReferencesNonexistentPort {
506 node_name: node_name.clone(),
507 condition_name: cond_name.clone(),
508 is_input_condition: false,
509 missing_port: port_name.clone(),
510 });
511 }
512 }
513
514 let mut term_ports = HashSet::new();
516 collect_ports_from_term(&condition.term, &mut term_ports);
517 for port_name in term_ports {
518 if !node.out_ports.contains_key(&port_name) {
519 errors.push(GraphValidationError::SalvoConditionTermReferencesNonexistentPort {
520 node_name: node_name.clone(),
521 condition_name: cond_name.clone(),
522 is_input_condition: false,
523 missing_port: port_name,
524 });
525 }
526 }
527 }
528 }
529
530 errors
531 }
532}
533
534#[cfg(test)]
535mod tests {
536 use super::*;
537
538 fn simple_port() -> Port {
540 Port { slots_spec: PortSlotSpec::Infinite }
541 }
542
543 fn simple_node(name: &str, in_ports: Vec<&str>, out_ports: Vec<&str>) -> Node {
544 let in_ports_map: HashMap<PortName, Port> = in_ports
545 .iter()
546 .map(|p| (p.to_string(), simple_port()))
547 .collect();
548 let out_ports_map: HashMap<PortName, Port> = out_ports
549 .iter()
550 .map(|p| (p.to_string(), simple_port()))
551 .collect();
552
553 let mut in_salvo_conditions = HashMap::new();
555 if !in_ports.is_empty() {
556 in_salvo_conditions.insert(
557 "default".to_string(),
558 SalvoCondition {
559 max_salvos: 1,
560 ports: in_ports.iter().map(|s| s.to_string()).collect(),
561 term: SalvoConditionTerm::Port {
562 port_name: in_ports[0].to_string(),
563 state: PortState::NonEmpty,
564 },
565 },
566 );
567 }
568
569 Node {
570 name: name.to_string(),
571 in_ports: in_ports_map,
572 out_ports: out_ports_map,
573 in_salvo_conditions,
574 out_salvo_conditions: HashMap::new(),
575 }
576 }
577
578 fn edge(src_node: &str, src_port: &str, tgt_node: &str, tgt_port: &str) -> Edge {
579 Edge {
580 source: PortRef {
581 node_name: src_node.to_string(),
582 port_type: PortType::Output,
583 port_name: src_port.to_string(),
584 },
585 target: PortRef {
586 node_name: tgt_node.to_string(),
587 port_type: PortType::Input,
588 port_name: tgt_port.to_string(),
589 },
590 }
591 }
592
593 #[test]
594 fn test_valid_graph_passes_validation() {
595 let nodes = vec![
596 simple_node("A", vec![], vec!["out"]),
597 simple_node("B", vec!["in"], vec![]),
598 ];
599 let edges = vec![edge("A", "out", "B", "in")];
600 let graph = Graph::new(nodes, edges);
601
602 let errors = graph.validate();
603 assert!(errors.is_empty(), "Expected no errors, got: {:?}", errors);
604 }
605
606 #[test]
607 fn test_edge_references_nonexistent_source_node() {
608 let nodes = vec![
609 simple_node("B", vec!["in"], vec![]),
610 ];
611 let edges = vec![edge("A", "out", "B", "in")];
613 let graph = Graph::new(nodes, edges);
614
615 let errors = graph.validate();
616 assert_eq!(errors.len(), 1);
617 match &errors[0] {
618 GraphValidationError::EdgeReferencesNonexistentNode { missing_node, .. } => {
619 assert_eq!(missing_node, "A");
620 }
621 _ => panic!("Expected EdgeReferencesNonexistentNode, got: {:?}", errors[0]),
622 }
623 }
624
625 #[test]
626 fn test_edge_references_nonexistent_target_node() {
627 let nodes = vec![
628 simple_node("A", vec![], vec!["out"]),
629 ];
630 let edges = vec![edge("A", "out", "B", "in")];
632 let graph = Graph::new(nodes, edges);
633
634 let errors = graph.validate();
635 assert_eq!(errors.len(), 1);
636 match &errors[0] {
637 GraphValidationError::EdgeReferencesNonexistentNode { missing_node, .. } => {
638 assert_eq!(missing_node, "B");
639 }
640 _ => panic!("Expected EdgeReferencesNonexistentNode, got: {:?}", errors[0]),
641 }
642 }
643
644 #[test]
645 fn test_edge_references_nonexistent_source_port() {
646 let nodes = vec![
647 simple_node("A", vec![], vec!["out"]),
648 simple_node("B", vec!["in"], vec![]),
649 ];
650 let edges = vec![edge("A", "wrong_port", "B", "in")];
652 let graph = Graph::new(nodes, edges);
653
654 let errors = graph.validate();
655 assert_eq!(errors.len(), 1);
656 match &errors[0] {
657 GraphValidationError::EdgeReferencesNonexistentPort { missing_port, .. } => {
658 assert_eq!(missing_port.port_name, "wrong_port");
659 }
660 _ => panic!("Expected EdgeReferencesNonexistentPort, got: {:?}", errors[0]),
661 }
662 }
663
664 #[test]
665 fn test_edge_references_nonexistent_target_port() {
666 let nodes = vec![
667 simple_node("A", vec![], vec!["out"]),
668 simple_node("B", vec!["in"], vec![]),
669 ];
670 let edges = vec![edge("A", "out", "B", "wrong_port")];
672 let graph = Graph::new(nodes, edges);
673
674 let errors = graph.validate();
675 assert_eq!(errors.len(), 1);
676 match &errors[0] {
677 GraphValidationError::EdgeReferencesNonexistentPort { missing_port, .. } => {
678 assert_eq!(missing_port.port_name, "wrong_port");
679 }
680 _ => panic!("Expected EdgeReferencesNonexistentPort, got: {:?}", errors[0]),
681 }
682 }
683
684 #[test]
685 fn test_edge_source_must_be_output_port() {
686 let nodes = vec![
687 simple_node("A", vec!["in"], vec!["out"]),
688 simple_node("B", vec!["in"], vec![]),
689 ];
690 let edges = vec![Edge {
692 source: PortRef {
693 node_name: "A".to_string(),
694 port_type: PortType::Input, port_name: "in".to_string(),
696 },
697 target: PortRef {
698 node_name: "B".to_string(),
699 port_type: PortType::Input,
700 port_name: "in".to_string(),
701 },
702 }];
703 let graph = Graph::new(nodes, edges);
704
705 let errors = graph.validate();
706 assert!(errors.iter().any(|e| matches!(e, GraphValidationError::EdgeSourceNotOutputPort { .. })));
707 }
708
709 #[test]
710 fn test_edge_target_must_be_input_port() {
711 let nodes = vec![
712 simple_node("A", vec![], vec!["out"]),
713 simple_node("B", vec!["in"], vec!["out"]),
714 ];
715 let edges = vec![Edge {
717 source: PortRef {
718 node_name: "A".to_string(),
719 port_type: PortType::Output,
720 port_name: "out".to_string(),
721 },
722 target: PortRef {
723 node_name: "B".to_string(),
724 port_type: PortType::Output, port_name: "out".to_string(),
726 },
727 }];
728 let graph = Graph::new(nodes, edges);
729
730 let errors = graph.validate();
731 assert!(errors.iter().any(|e| matches!(e, GraphValidationError::EdgeTargetNotInputPort { .. })));
732 }
733
734 #[test]
735 fn test_input_salvo_condition_must_have_max_salvos_1() {
736 let mut node = simple_node("A", vec!["in"], vec![]);
737 node.in_salvo_conditions.get_mut("default").unwrap().max_salvos = 2;
739
740 let graph = Graph::new(vec![node], vec![]);
741
742 let errors = graph.validate();
743 assert_eq!(errors.len(), 1);
744 match &errors[0] {
745 GraphValidationError::InputSalvoConditionInvalidMaxSalvos { max_salvos, .. } => {
746 assert_eq!(*max_salvos, 2);
747 }
748 _ => panic!("Expected InputSalvoConditionInvalidMaxSalvos, got: {:?}", errors[0]),
749 }
750 }
751
752 #[test]
753 fn test_salvo_condition_ports_must_exist() {
754 let mut node = simple_node("A", vec!["in"], vec![]);
755 node.in_salvo_conditions.get_mut("default").unwrap().ports = vec!["nonexistent".to_string()];
757
758 let graph = Graph::new(vec![node], vec![]);
759
760 let errors = graph.validate();
761 assert!(errors.iter().any(|e| matches!(
762 e,
763 GraphValidationError::SalvoConditionReferencesNonexistentPort { missing_port, .. }
764 if missing_port == "nonexistent"
765 )));
766 }
767
768 #[test]
769 fn test_salvo_condition_term_ports_must_exist() {
770 let mut node = simple_node("A", vec!["in"], vec![]);
771 node.in_salvo_conditions.get_mut("default").unwrap().term = SalvoConditionTerm::Port {
773 port_name: "nonexistent".to_string(),
774 state: PortState::NonEmpty,
775 };
776
777 let graph = Graph::new(vec![node], vec![]);
778
779 let errors = graph.validate();
780 assert!(errors.iter().any(|e| matches!(
781 e,
782 GraphValidationError::SalvoConditionTermReferencesNonexistentPort { missing_port, .. }
783 if missing_port == "nonexistent"
784 )));
785 }
786
787 #[test]
788 fn test_output_salvo_condition_ports_must_exist() {
789 let mut node = simple_node("A", vec![], vec!["out"]);
790 node.out_salvo_conditions.insert(
792 "test".to_string(),
793 SalvoCondition {
794 max_salvos: 0,
795 ports: vec!["nonexistent".to_string()],
796 term: SalvoConditionTerm::Port {
797 port_name: "out".to_string(),
798 state: PortState::NonEmpty,
799 },
800 },
801 );
802
803 let graph = Graph::new(vec![node], vec![]);
804
805 let errors = graph.validate();
806 assert!(errors.iter().any(|e| matches!(
807 e,
808 GraphValidationError::SalvoConditionReferencesNonexistentPort {
809 is_input_condition: false,
810 missing_port,
811 ..
812 } if missing_port == "nonexistent"
813 )));
814 }
815
816 #[test]
817 fn test_complex_salvo_condition_term_validation() {
818 let mut node = simple_node("A", vec!["in1", "in2"], vec![]);
819 node.in_salvo_conditions.get_mut("default").unwrap().term = SalvoConditionTerm::And(vec![
821 SalvoConditionTerm::Port {
822 port_name: "in1".to_string(),
823 state: PortState::NonEmpty,
824 },
825 SalvoConditionTerm::Or(vec![
826 SalvoConditionTerm::Port {
827 port_name: "in2".to_string(),
828 state: PortState::NonEmpty,
829 },
830 SalvoConditionTerm::Not(Box::new(SalvoConditionTerm::Port {
831 port_name: "nonexistent".to_string(), state: PortState::Empty,
833 })),
834 ]),
835 ]);
836
837 let graph = Graph::new(vec![node], vec![]);
838
839 let errors = graph.validate();
840 assert!(errors.iter().any(|e| matches!(
841 e,
842 GraphValidationError::SalvoConditionTermReferencesNonexistentPort { missing_port, .. }
843 if missing_port == "nonexistent"
844 )));
845 }
846
847 #[test]
848 fn test_empty_graph_is_valid() {
849 let graph = Graph::new(vec![], vec![]);
850 let errors = graph.validate();
851 assert!(errors.is_empty());
852 }
853
854 #[test]
855 fn test_node_without_ports_is_valid() {
856 let node = Node {
857 name: "A".to_string(),
858 in_ports: HashMap::new(),
859 out_ports: HashMap::new(),
860 in_salvo_conditions: HashMap::new(),
861 out_salvo_conditions: HashMap::new(),
862 };
863 let graph = Graph::new(vec![node], vec![]);
864 let errors = graph.validate();
865 assert!(errors.is_empty());
866 }
867}