1use std::collections::BTreeSet;
4
5use itertools::Itertools;
6use slotmap::{SecondaryMap, SparseSecondaryMap};
7
8use super::meta_graph::DfirGraph;
9use super::ops::DelayType;
10use super::{
11 Color, GraphEdgeId, GraphLoopId, GraphNode, GraphNodeId, GraphSubgraphId, HandoffKind,
12};
13use crate::diagnostic::{Diagnostic, Level};
14use crate::graph::graph_algorithms::{SubgraphMerge, validate_topo_sort};
15
16fn find_edge_barriers(
22 partitioned_graph: &DfirGraph,
23) -> (
24 SecondaryMap<GraphEdgeId, DelayType>,
25 Vec<(GraphNodeId, GraphNodeId)>,
26) {
27 let mut tick_edges = SecondaryMap::new();
28 let mut barrier_pairs = Vec::new();
29
30 for (edge_id, (src, dst)) in partitioned_graph.edges() {
31 let Some(op_inst) = partitioned_graph.node_op_inst(dst) else {
32 continue;
33 };
34 let (_src_port, dst_port) = partitioned_graph.edge_ports(edge_id);
35 let Some(delay_type) = (op_inst.op_constraints.input_delaytype_fn)(dst_port) else {
36 continue;
37 };
38
39 barrier_pairs.push((src, dst));
40 tick_edges.insert(edge_id, delay_type);
41 }
42
43 (tick_edges, barrier_pairs)
44}
45
46fn find_access_group_ordering(partitioned_graph: &DfirGraph) -> Vec<(GraphNodeId, GraphNodeId)> {
49 let mut pairs = Vec::new();
50 let refs_by_target = partitioned_graph.node_handoff_reference_groups();
51 for (_handoff, groups) in refs_by_target {
52 for (group_a, group_b) in groups.values().tuple_windows() {
53 for &(node_a, _, _) in group_a {
54 for &(node_b, _, _) in group_b {
55 assert_ne!(
57 node_a, node_b,
58 "encounted conflicted or cyclical handoff references\n{:?}\n{:?}",
59 group_a, group_b,
60 );
61 pairs.push((node_a, node_b));
62 }
63 }
64 }
65 }
66 pairs
67}
68
69fn find_subgraph_unionfind(
70 partitioned_graph: &DfirGraph,
71 tick_edges: &SecondaryMap<GraphEdgeId, DelayType>,
72 edge_barrier_pairs: &[(GraphNodeId, GraphNodeId)],
73 access_group_pairs: &[(GraphNodeId, GraphNodeId)],
74) -> Result<(SubgraphMerge<GraphNodeId>, BTreeSet<GraphEdgeId>), Diagnostic> {
75 let mut node_color = partitioned_graph
80 .node_ids()
81 .filter_map(|node_id| {
82 let op_color = partitioned_graph.node_color(node_id)?;
83 Some((node_id, op_color))
84 })
85 .collect::<SparseSecondaryMap<_, _>>();
86
87 let mut all_preds: SecondaryMap<GraphNodeId, Vec<GraphNodeId>> = SecondaryMap::new();
89
90 for (edge_id, (src, dst)) in partitioned_graph.edges() {
92 if !tick_edges.contains_key(edge_id) {
93 all_preds.entry(dst).unwrap().or_default().push(src);
94 }
95 }
96
97 for node_id in partitioned_graph.node_ids() {
99 for handoff_ref in partitioned_graph.node_handoff_references(node_id).iter() {
100 if let Some(src) = handoff_ref.node_id {
101 all_preds.entry(node_id).unwrap().or_default().push(src);
102 if let GraphNode::Handoff { .. } = partitioned_graph.node(src) {
105 for (_edge, consumer) in partitioned_graph.node_successors(src) {
106 all_preds
107 .entry(consumer)
108 .unwrap()
109 .or_default()
110 .push(node_id);
111 }
112 }
113 }
114 }
115 }
116
117 for &(src, dst) in access_group_pairs {
119 all_preds.entry(dst).unwrap().or_default().push(src);
120 }
121
122 {
136 for (edge_id, (src_id, dst_id)) in partitioned_graph.edges() {
137 if tick_edges.contains_key(edge_id) {
138 continue;
139 }
140 let src_loop = partitioned_graph.node_loop(src_id);
141 let dst_loop = partitioned_graph.node_loop(dst_id);
142 if let Some(dst_loop_some) = dst_loop
143 && src_loop == partitioned_graph.loop_parent(dst_loop_some)
144 {
145 for &inside_dst_id in partitioned_graph.loop_nodes(dst_loop_some) {
146 all_preds
147 .entry(inside_dst_id)
148 .unwrap()
149 .or_default()
150 .push(src_id);
151 }
152 }
153 }
154 }
155
156 let enemies = edge_barrier_pairs
158 .iter()
159 .copied()
160 .chain(access_group_pairs.iter().copied())
161 .chain(partitioned_graph.node_ids().flat_map(|dst| {
162 partitioned_graph
163 .node_handoff_references(dst)
164 .iter()
165 .filter_map(|r| r.node_id)
166 .map(move |src| (src, dst))
167 }));
168
169 let mut subgraph_unionfind = SubgraphMerge::<GraphNodeId>::new(
170 partitioned_graph.node_ids(),
171 |node_id| all_preds.get(node_id).into_iter().flatten().copied(),
172 enemies,
173 )
174 .map_err(|cycle| {
175 let span = cycle
176 .first()
177 .map(|&node_id| partitioned_graph.node(node_id).span())
178 .unwrap_or_else(proc_macro2::Span::call_site);
179 let node_cycle = cycle
180 .iter()
181 .map(|&node_id| partitioned_graph.node(node_id).to_pretty_string())
182 .collect::<Vec<_>>();
183 Diagnostic::spanned(
184 span,
185 Level::Error,
186 format!(
187 "Cyclical dataflow within a tick is not supported. Use `defer_tick()` or `defer_tick_lazy()` to break the cycle across ticks. \
188 Cycle: {:?}",
189 node_cycle,
190 ),
191 )
192 })?;
193
194 let mut handoff_edges: BTreeSet<GraphEdgeId> = partitioned_graph.edge_ids().collect();
197 let mut progress = true;
206 while progress {
207 progress = false;
208 for (edge_id, (src, dst)) in partitioned_graph.edges().collect::<Vec<_>>() {
210 if matches!(partitioned_graph.node(src), GraphNode::Handoff { .. })
212 || matches!(partitioned_graph.node(dst), GraphNode::Handoff { .. })
213 {
214 handoff_edges.remove(&edge_id);
215 continue;
216 }
217
218 if subgraph_unionfind.same_set(src, dst) {
220 continue;
223 }
224
225 if partitioned_graph.node_loop(src) != partitioned_graph.node_loop(dst) {
227 continue;
228 }
229
230 if can_connect_colorize(&mut node_color, src, dst) {
231 let ok = subgraph_unionfind.try_merge(src, dst);
234 if ok {
235 assert!(handoff_edges.remove(&edge_id));
236 progress = true;
237 }
238 }
239 }
240 }
241
242 Ok((subgraph_unionfind, handoff_edges))
243}
244
245fn make_subgraphs(
247 partitioned_graph: &mut DfirGraph,
248 tick_edges: &mut SecondaryMap<GraphEdgeId, DelayType>,
249 edge_barrier_pairs: &[(GraphNodeId, GraphNodeId)],
250 access_group_pairs: &[(GraphNodeId, GraphNodeId)],
251) -> Result<(), Diagnostic> {
252 let (subgraph_merge, handoff_edges) = find_subgraph_unionfind(
253 partitioned_graph,
254 tick_edges,
255 edge_barrier_pairs,
256 access_group_pairs,
257 )?;
258
259 for edge_id in handoff_edges {
261 let (src_id, dst_id) = partitioned_graph.edge(edge_id);
262
263 let src_node = partitioned_graph.node(src_id);
265 let dst_node = partitioned_graph.node(dst_id);
266 if matches!(src_node, GraphNode::Handoff { .. })
267 || matches!(dst_node, GraphNode::Handoff { .. })
268 {
269 continue;
270 }
271
272 let hoff = GraphNode::Handoff {
273 kind: HandoffKind::Vec,
274 src_span: src_node.span(),
275 dst_span: dst_node.span(),
276 };
277 let (_node_id, out_edge_id) = partitioned_graph.insert_intermediate_node(edge_id, hoff);
278
279 if let Some(delay_type) = tick_edges.remove(edge_id) {
281 tick_edges.insert(out_edge_id, delay_type);
282 }
283 }
284
285 let mut flat_toposort = Vec::new();
289 for nodes in subgraph_merge.subgraphs() {
290 if nodes.is_empty() {
291 continue;
292 }
293 if nodes
295 .iter()
296 .any(|&n| matches!(partitioned_graph.node(n), GraphNode::Handoff { .. }))
297 {
298 continue;
299 }
300 let sg_id = partitioned_graph.insert_subgraph(nodes.to_vec()).unwrap();
301 flat_toposort.push(sg_id);
302 }
303
304 let subgraph_toposort = make_loops_contiguous(partitioned_graph, &flat_toposort);
306
307 assert!(
310 validate_topo_sort(
311 subgraph_toposort
312 .iter()
313 .flat_map(|&sg_id| partitioned_graph.subgraph(sg_id))
314 .copied(),
315 |succ_id| {
316 partitioned_graph
317 .node_predecessors(succ_id)
318 .filter_map(|(edge_id, pred_id)| {
319 (!tick_edges.contains_key(edge_id)).then_some(pred_id)
320 })
321 .map(|pred_id| {
322 if matches!(partitioned_graph.node(pred_id), GraphNode::Handoff { .. }) {
324 partitioned_graph
325 .node_predecessor_nodes(pred_id)
326 .next()
327 .unwrap()
328 } else {
329 pred_id
330 }
331 })
332 }
333 )
334 .is_ok(),
335 "bug: toposort is invalid after `make_loops_contiguous`.",
336 );
337
338 partitioned_graph.set_subgraph_toposort(subgraph_toposort);
339
340 Ok(())
341}
342
343fn make_loops_contiguous(
353 graph: &DfirGraph,
354 flat_order: &[GraphSubgraphId],
355) -> Vec<GraphSubgraphId> {
356 use std::collections::HashMap;
357
358 let mut loop_descendants = HashMap::<GraphLoopId, Vec<GraphSubgraphId>>::new();
361 for &sg_id in flat_order {
362 let mut current = graph.subgraph_loop(sg_id);
363 while let Some(loop_id) = current {
364 loop_descendants.entry(loop_id).or_default().push(sg_id);
365 current = graph.loop_parent(loop_id);
366 }
367 }
368
369 fn helper(
370 graph: &DfirGraph,
371 flat_order: &[GraphSubgraphId],
372 current_loop: Option<GraphLoopId>,
373 loop_descendants: &mut HashMap<GraphLoopId, Vec<GraphSubgraphId>>,
374 output: &mut Vec<GraphSubgraphId>,
375 ) {
376 for &sg_id in flat_order {
377 let sg_loop = graph.subgraph_loop(sg_id);
378 if current_loop == sg_loop {
379 output.push(sg_id);
381 continue;
382 }
383 let sg_loop = sg_loop.expect("root-level subgraph cannot be within a loop: `sg_loop == None` implies `current_loop == None`");
384 if current_loop == graph.loop_parent(sg_loop) {
385 if let Some(inner_order) = loop_descendants.remove(&sg_loop) {
388 helper(graph, &inner_order, Some(sg_loop), loop_descendants, output);
389 }
390 }
391 }
393 }
394
395 let mut output = Vec::with_capacity(flat_order.len());
396 helper(graph, flat_order, None, &mut loop_descendants, &mut output);
397 output
398}
399
400fn can_connect_colorize(
406 node_color: &mut SparseSecondaryMap<GraphNodeId, Color>,
407 src: GraphNodeId,
408 dst: GraphNodeId,
409) -> bool {
410 let can_connect = match (node_color.get(src), node_color.get(dst)) {
415 (None, None) => false,
418
419 (None, Some(Color::Pull | Color::Comp)) => {
421 node_color.insert(src, Color::Pull);
422 true
423 }
424 (None, Some(Color::Push | Color::Hoff)) => {
425 node_color.insert(src, Color::Push);
426 true
427 }
428
429 (Some(Color::Pull | Color::Hoff), None) => {
431 node_color.insert(dst, Color::Pull);
432 true
433 }
434 (Some(Color::Comp | Color::Push), None) => {
435 node_color.insert(dst, Color::Push);
436 true
437 }
438
439 (Some(Color::Pull), Some(Color::Pull)) => true,
441 (Some(Color::Pull), Some(Color::Comp)) => true,
442 (Some(Color::Pull), Some(Color::Push)) => true,
443
444 (Some(Color::Comp), Some(Color::Pull)) => false,
445 (Some(Color::Comp), Some(Color::Comp)) => false,
446 (Some(Color::Comp), Some(Color::Push)) => true,
447
448 (Some(Color::Push), Some(Color::Pull)) => false,
449 (Some(Color::Push), Some(Color::Comp)) => false,
450 (Some(Color::Push), Some(Color::Push)) => true,
451
452 (Some(Color::Hoff), Some(_)) => false,
454 (Some(_), Some(Color::Hoff)) => false,
455 };
456 can_connect
457}
458
459fn mark_tick_boundary_handoffs(
466 partitioned_graph: &mut DfirGraph,
467 tick_edges: &SecondaryMap<GraphEdgeId, DelayType>,
468) {
469 let tick_handoffs: Vec<_> = partitioned_graph
470 .nodes()
471 .filter_map(|(hoff_id, hoff)| {
472 if !matches!(hoff, GraphNode::Handoff { .. }) {
473 return None;
474 }
475 if partitioned_graph.node_degree_out(hoff_id) == 0 {
476 return None;
477 }
478 let (succ_edge, _) = partitioned_graph.node_successors(hoff_id).next().unwrap();
479 let &delay_type = tick_edges.get(succ_edge)?;
480
481 let consumer_loop = partitioned_graph
483 .node_successors(hoff_id)
484 .next()
485 .and_then(|(_, succ)| partitioned_graph.node_loop(succ));
486 let effective_delay_type = if let Some(loop_id) = consumer_loop {
487 if partitioned_graph.loop_parent(loop_id).is_some() {
488 match delay_type {
490 DelayType::Tick => DelayType::Loop,
491 DelayType::TickLazy => DelayType::LoopLazy,
492 other => other,
493 }
494 } else {
495 delay_type
496 }
497 } else {
498 delay_type
499 };
500
501 Some((hoff_id, effective_delay_type))
502 })
503 .collect();
504
505 for (hoff_id, delay_type) in tick_handoffs {
506 partitioned_graph.set_handoff_delay_type(hoff_id, delay_type);
507 }
508}
509
510pub fn partition_graph(flat_graph: DfirGraph) -> Result<DfirGraph, Diagnostic> {
514 let (mut tick_edges, edge_barrier_pairs) = find_edge_barriers(&flat_graph);
515 let access_group_pairs = find_access_group_ordering(&flat_graph);
516 let mut partitioned_graph = flat_graph;
517
518 make_subgraphs(
520 &mut partitioned_graph,
521 &mut tick_edges,
522 &edge_barrier_pairs,
523 &access_group_pairs,
524 )?;
525
526 mark_tick_boundary_handoffs(&mut partitioned_graph, &tick_edges);
528
529 Ok(partitioned_graph)
530}
531
532#[cfg(test)]
533mod tests {
534 use super::*;
535 use crate::graph::GraphNode;
536
537 #[test]
555 fn test_make_loops_contiguous_ingress_toposort() {
556 use crate::graph::{FlatGraphBuilder, FlatGraphBuilderOutput};
557
558 let mut builder = FlatGraphBuilder::new();
559 builder.add_dfir(
561 syn::parse_quote! {
562 inp1 = source_iter([1, 2, 3]);
563 },
564 None,
565 None,
566 );
567 let loop_id = builder.insert_loop(None);
568 builder.add_dfir(
571 syn::parse_quote! {
572 inp1 -> batch() -> for_each(std::mem::drop);
573 },
574 Some(loop_id),
575 None,
576 );
577 builder.add_dfir(
580 syn::parse_quote! {
581 inp2 = source_iter([4, 5, 6]);
582 },
583 None,
584 None,
585 );
586 builder.add_dfir(
588 syn::parse_quote! {
589 inp2 -> batch() -> for_each(std::mem::drop);
590 },
591 Some(loop_id),
592 None,
593 );
594
595 let FlatGraphBuilderOutput { flat_graph, .. } =
596 builder.build().expect("should build without errors");
597 let partitioned = partition_graph(flat_graph).expect("should partition without errors");
598
599 let order = partitioned.subgraph_toposort();
602 let pos: std::collections::HashMap<_, _> =
603 order.iter().enumerate().map(|(i, &sg)| (sg, i)).collect();
604
605 for (hoff_id, node) in partitioned.nodes() {
606 if !matches!(node, GraphNode::Handoff { .. }) {
607 continue;
608 }
609 if partitioned.handoff_delay_type(hoff_id).is_some() {
611 continue;
612 }
613 let Some((_, pred)) = partitioned.node_predecessors(hoff_id).next() else {
614 continue;
615 };
616 let Some((_, succ)) = partitioned.node_successors(hoff_id).next() else {
617 continue;
618 };
619 let (Some(pred_sg), Some(succ_sg)) = (
620 partitioned.node_subgraph(pred),
621 partitioned.node_subgraph(succ),
622 ) else {
623 continue;
624 };
625 let (Some(&pp), Some(&sp)) = (pos.get(&pred_sg), pos.get(&succ_sg)) else {
626 continue;
627 };
628 assert!(
629 pp < sp,
630 "toposort violated: producer subgraph {pred_sg:?} (pos {pp}) emitted after \
631 consumer subgraph {succ_sg:?} (pos {sp}) via handoff {hoff_id:?}; order = {order:?}",
632 );
633 }
634 }
635
636 fn make_op_node(graph: &mut DfirGraph, loop_ctx: Option<GraphLoopId>) -> GraphNodeId {
637 let operator: crate::parse::Operator = syn::parse_quote! { identity() };
638 graph.insert_node(GraphNode::Operator(operator), None, loop_ctx)
639 }
640
641 #[test]
644 fn test_make_loops_contiguous_basic() {
645 let mut graph = DfirGraph::default();
646
647 let loop_id = graph.insert_loop(None);
649
650 let op_a = make_op_node(&mut graph, None);
652 let op_b = make_op_node(&mut graph, None);
653 let op_c = make_op_node(&mut graph, Some(loop_id));
654 let op_d = make_op_node(&mut graph, Some(loop_id));
655
656 let sg_a = graph.insert_subgraph(vec![op_a]).unwrap();
658 let sg_b = graph.insert_subgraph(vec![op_b]).unwrap();
659 let sg_c = graph.insert_subgraph(vec![op_c]).unwrap();
660 let sg_d = graph.insert_subgraph(vec![op_d]).unwrap();
661
662 let flat_order = vec![sg_a, sg_c, sg_b, sg_d];
664 let result = make_loops_contiguous(&graph, &flat_order);
665
666 assert_eq!(result, vec![sg_a, sg_c, sg_d, sg_b]);
668 }
669
670 #[test]
672 fn test_make_loops_contiguous_independent_loops() {
673 let mut graph = DfirGraph::default();
674
675 let loop1 = graph.insert_loop(None);
676 let loop2 = graph.insert_loop(None);
677
678 let op_c = make_op_node(&mut graph, Some(loop1));
680 let op_d = make_op_node(&mut graph, Some(loop1));
681 let op_e = make_op_node(&mut graph, Some(loop2));
682 let op_f = make_op_node(&mut graph, Some(loop2));
683
684 let sg_c = graph.insert_subgraph(vec![op_c]).unwrap();
685 let sg_d = graph.insert_subgraph(vec![op_d]).unwrap();
686 let sg_e = graph.insert_subgraph(vec![op_e]).unwrap();
687 let sg_f = graph.insert_subgraph(vec![op_f]).unwrap();
688
689 let flat_order = vec![sg_c, sg_e, sg_d, sg_f];
691 let result = make_loops_contiguous(&graph, &flat_order);
692
693 let pos_c = result.iter().position(|&s| s == sg_c).unwrap();
695 let pos_d = result.iter().position(|&s| s == sg_d).unwrap();
696 let pos_e = result.iter().position(|&s| s == sg_e).unwrap();
697 let pos_f = result.iter().position(|&s| s == sg_f).unwrap();
698
699 assert!(pos_c < pos_d);
701 assert!(pos_e < pos_f);
702
703 assert_eq!(pos_d - pos_c, 1, "Loop1 subgraphs must be contiguous");
705 assert_eq!(pos_f - pos_e, 1, "Loop2 subgraphs must be contiguous");
706 }
707
708 #[test]
710 fn test_make_loops_contiguous_nested() {
711 let mut graph = DfirGraph::default();
712
713 let outer = graph.insert_loop(None);
714 let inner = graph.insert_loop(Some(outer));
715
716 let op_a = make_op_node(&mut graph, Some(outer));
718 let op_b = make_op_node(&mut graph, Some(inner));
719 let op_c = make_op_node(&mut graph, Some(inner));
720 let op_d = make_op_node(&mut graph, Some(outer));
721
722 let sg_a = graph.insert_subgraph(vec![op_a]).unwrap();
723 let sg_b = graph.insert_subgraph(vec![op_b]).unwrap();
724 let sg_c = graph.insert_subgraph(vec![op_c]).unwrap();
725 let sg_d = graph.insert_subgraph(vec![op_d]).unwrap();
726
727 let flat_order = vec![sg_a, sg_b, sg_c, sg_d];
729 let result = make_loops_contiguous(&graph, &flat_order);
730
731 assert_eq!(result, vec![sg_a, sg_b, sg_c, sg_d]);
733 }
734
735 #[test]
737 fn test_make_loops_contiguous_nested_interleaved() {
738 let mut graph = DfirGraph::default();
739
740 let outer = graph.insert_loop(None);
741 let inner = graph.insert_loop(Some(outer));
742
743 let op_a = make_op_node(&mut graph, Some(outer));
744 let op_b = make_op_node(&mut graph, Some(inner));
745 let op_c = make_op_node(&mut graph, Some(inner));
746 let op_d = make_op_node(&mut graph, Some(outer));
747
748 let sg_a = graph.insert_subgraph(vec![op_a]).unwrap();
749 let sg_b = graph.insert_subgraph(vec![op_b]).unwrap();
750 let sg_c = graph.insert_subgraph(vec![op_c]).unwrap();
751 let sg_d = graph.insert_subgraph(vec![op_d]).unwrap();
752
753 let flat_order = vec![sg_a, sg_b, sg_d, sg_c];
755 let result = make_loops_contiguous(&graph, &flat_order);
756
757 assert_eq!(result, vec![sg_a, sg_b, sg_c, sg_d]);
759 }
760}