Skip to main content

dfir_lang/graph/
flat_to_partitioned.rs

1//! Subgraph partioning algorithm
2
3use 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;
15
16/// Find edge barriers: edges whose destination operator declares an input delay type.
17///
18/// Returns:
19/// - Tick/TickLazy/Loop/LoopLazy edges keyed by edge ID (for topo-sort exclusion and handoff marking).
20/// - All barrier (src, dst) node pairs (for the enemies set).
21fn 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
46/// Find handoff reference access group ordering constraints: for the same handoff target, operators in
47/// lower access groups must run before operators in higher access groups.
48fn 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                    // TODO(mingwei): handle with diagnostics.
56                    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    // Modality (color) of nodes, push or pull.
76    // TODO(mingwei)? This does NOT consider `DelayType` barriers (which generally imply `Pull`),
77    // which makes it inconsistant with the final output in `as_code()`. But this doesn't create
78    // any bugs since we exclude `DelayType` edges from joining subgraphs anyway.
79    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    // Pre-compute all predecessor edges for the topological sort.
88    let mut all_preds: SecondaryMap<GraphNodeId, Vec<GraphNodeId>> = SecondaryMap::new();
89
90    // Pipe predecessors (excluding tick edges which are cross-tick).
91    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    // Handoff references: producer must run before consumer.
98    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                // Extra ordering: if the ref target is a handoff, its pipe consumers
103                // depend on the borrower (borrower runs before consumer).
104                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    // Access group ordering.
118    for &(src, dst) in access_group_pairs {
119        all_preds.entry(dst).unwrap().or_default().push(src);
120    }
121
122    // Build enemies: all node pairs that must not be in the same subgraph.
123    let enemies = edge_barrier_pairs
124        .iter()
125        .copied()
126        .chain(access_group_pairs.iter().copied())
127        .chain(partitioned_graph.node_ids().flat_map(|dst| {
128            partitioned_graph
129                .node_handoff_references(dst)
130                .iter()
131                .filter_map(|r| r.node_id)
132                .map(move |src| (src, dst))
133        }));
134
135    let mut subgraph_unionfind = SubgraphMerge::<GraphNodeId>::new(
136        partitioned_graph.node_ids(),
137        |node_id| all_preds.get(node_id).into_iter().flatten().copied(),
138        enemies,
139    )
140    .map_err(|cycle| {
141        let span = cycle
142            .first()
143            .map(|&node_id| partitioned_graph.node(node_id).span())
144            .unwrap_or_else(proc_macro2::Span::call_site);
145        let node_cycle = cycle
146            .iter()
147            .map(|&node_id| partitioned_graph.node(node_id).to_pretty_string())
148            .collect::<Vec<_>>();
149        Diagnostic::spanned(
150            span,
151            Level::Error,
152            format!(
153                "Cyclical dataflow within a tick is not supported. Use `defer_tick()` or `defer_tick_lazy()` to break the cycle across ticks. \
154                Cycle: {:?}",
155                node_cycle,
156            ),
157        )
158    })?;
159
160    // Will contain all edges which need handoffs added. Starts out with all edges and
161    // we remove from this set as we combine nodes into subgraphs.
162    let mut handoff_edges: BTreeSet<GraphEdgeId> = partitioned_graph.edge_ids().collect();
163    // Would sort edges here for priority (for now, no sort/priority).
164
165    // Each edge gets looked at in order. However we may not know if a linear
166    // chain of operators is PUSH vs PULL until we look at the ends. A fancier
167    // algorithm would know to handle linear chains from the outside inward.
168    // But instead we just run through the edges in a loop until no more
169    // progress is made. Could have some sort of O(N^2) pathological worst
170    // case.
171    let mut progress = true;
172    while progress {
173        progress = false;
174        // TODO(mingwei): Could this iterate `handoff_edges` instead? (Modulo ownership). Then no case (1) below.
175        for (edge_id, (src, dst)) in partitioned_graph.edges().collect::<Vec<_>>() {
176            // Ignore existing handoffs, remove `edge_id` from `handoff_edges`.
177            if matches!(partitioned_graph.node(src), GraphNode::Handoff { .. })
178                || matches!(partitioned_graph.node(dst), GraphNode::Handoff { .. })
179            {
180                handoff_edges.remove(&edge_id);
181                continue;
182            }
183
184            // Ignore (1) already added edges as well as (2) new self-cycles. (Unless reference edge).
185            if subgraph_unionfind.same_set(src, dst) {
186                // Note that the _edge_ `edge_id` might not be in the subgraph even when both `src` and `dst` are. This prevents case 2.
187                // Handoffs will be inserted later for this self-loop.
188                continue;
189            }
190
191            // Do not connect across loop contexts.
192            if partitioned_graph.node_loop(src) != partitioned_graph.node_loop(dst) {
193                continue;
194            }
195
196            if can_connect_colorize(&mut node_color, src, dst) {
197                // At this point we have selected this edge and its src & dst to be
198                // within a single subgraph.
199                let ok = subgraph_unionfind.try_merge(src, dst);
200                if ok {
201                    assert!(handoff_edges.remove(&edge_id));
202                    progress = true;
203                }
204            }
205        }
206    }
207
208    Ok((subgraph_unionfind, handoff_edges))
209}
210
211/// Find subgraphs and insert handoffs.
212fn make_subgraphs(
213    partitioned_graph: &mut DfirGraph,
214    tick_edges: &mut SecondaryMap<GraphEdgeId, DelayType>,
215    edge_barrier_pairs: &[(GraphNodeId, GraphNodeId)],
216    access_group_pairs: &[(GraphNodeId, GraphNodeId)],
217) -> Result<(), Diagnostic> {
218    // Algorithm:
219    // 1. Each node begins as its own subgraph.
220    // 2. Collect edges. (Future optimization: sort so edges which should not be split across a handoff come first).
221    // 3. For each edge, try to join `(to, from)` into the same subgraph.
222
223    // TODO(mingwei):
224    // self.partitioned_graph.assert_valid();
225
226    let (subgraph_merge, handoff_edges) = find_subgraph_unionfind(
227        partitioned_graph,
228        tick_edges,
229        edge_barrier_pairs,
230        access_group_pairs,
231    )?;
232
233    // Insert handoffs between subgraphs (or on subgraph self-loop edges)
234    for edge_id in handoff_edges {
235        let (src_id, dst_id) = partitioned_graph.edge(edge_id);
236
237        // Already has a handoff, no need to insert one.
238        let src_node = partitioned_graph.node(src_id);
239        let dst_node = partitioned_graph.node(dst_id);
240        if matches!(src_node, GraphNode::Handoff { .. })
241            || matches!(dst_node, GraphNode::Handoff { .. })
242        {
243            continue;
244        }
245
246        let hoff = GraphNode::Handoff {
247            kind: HandoffKind::Vec,
248            src_span: src_node.span(),
249            dst_span: dst_node.span(),
250        };
251        let (_node_id, out_edge_id) = partitioned_graph.insert_intermediate_node(edge_id, hoff);
252
253        // Update tick_edges for inserted node.
254        if let Some(delay_type) = tick_edges.remove(edge_id) {
255            tick_edges.insert(out_edge_id, delay_type);
256        }
257    }
258
259    // Register subgraphs. SubgraphMerge maintains operators in topo-sorted order per subgraph.
260    // Filter out handoff nodes — they are not part of any subgraph.
261    // Collect subgraph IDs in flat topological order.
262    let mut flat_toposort = Vec::new();
263    for nodes in subgraph_merge.subgraphs() {
264        if nodes.is_empty() {
265            continue;
266        }
267        // Skip single-node "subgraphs" that are handoff nodes.
268        if nodes
269            .iter()
270            .any(|&n| matches!(partitioned_graph.node(n), GraphNode::Handoff { .. }))
271        {
272            continue;
273        }
274        let sg_id = partitioned_graph.insert_subgraph(nodes.to_vec()).unwrap();
275        flat_toposort.push(sg_id);
276    }
277
278    // Rearrange the flat toposort to make loop subgraphs contiguous.
279    let subgraph_toposort = make_loops_contiguous(partitioned_graph, &flat_toposort);
280    partitioned_graph.set_subgraph_toposort(subgraph_toposort);
281
282    Ok(())
283}
284
285/// Rearranges a flat topological order of subgraphs so that all subgraphs within
286/// a loop are contiguous, while preserving the relative topological order.
287///
288/// Pre-computes for each loop the set of descendant subgraphs (in flat-order),
289/// then recurses through the loop hierarchy. At each level, subgraphs directly
290/// at that level are emitted in place; when a child loop is first encountered,
291/// all of its descendants are emitted recursively.
292///
293/// Correctness: subgraphs from different loop contexts that appear interleaved in
294/// the flat order have no dependency between them (data can only enter/exit a loop
295/// via windowing/unwindowing operators, never between siblings), so reordering them
296/// for contiguity preserves the topological invariant.
297fn make_loops_contiguous(
298    graph: &DfirGraph,
299    flat_order: &[GraphSubgraphId],
300) -> Vec<GraphSubgraphId> {
301    use std::collections::HashMap;
302
303    // Pre-compute: for each loop, collect *all* descendant subgraphs in flat-order (not just direct).
304    // Each sg_id is inserted into every ancestor loop.
305    let mut loop_descendants = HashMap::<GraphLoopId, Vec<GraphSubgraphId>>::new();
306    for &sg_id in flat_order {
307        let mut current = graph.subgraph_loop(sg_id);
308        while let Some(loop_id) = current {
309            loop_descendants.entry(loop_id).or_default().push(sg_id);
310            current = graph.loop_parent(loop_id);
311        }
312    }
313
314    fn helper(
315        graph: &DfirGraph,
316        flat_order: &[GraphSubgraphId],
317        current_loop: Option<GraphLoopId>,
318        loop_descendants: &mut HashMap<GraphLoopId, Vec<GraphSubgraphId>>,
319        output: &mut Vec<GraphSubgraphId>,
320    ) {
321        for &sg_id in flat_order {
322            let sg_loop = graph.subgraph_loop(sg_id);
323            if current_loop == sg_loop {
324                // Directly at this level — emit in place.
325                output.push(sg_id);
326                continue;
327            }
328            let sg_loop = sg_loop.expect("root-level subgraph cannot be within a loop: `sg_loop == None` implies `current_loop == None`");
329            if current_loop == graph.loop_parent(sg_loop) {
330                // In a direct child loop of current_loop, recurse if we haven't yet (tracked by `loop_descendant`
331                // entry removal).
332                if let Some(inner_order) = loop_descendants.remove(&sg_loop) {
333                    helper(graph, &inner_order, Some(sg_loop), loop_descendants, output);
334                }
335            }
336            // else: Deeper nested — skip, will be handled by recursion into its ancestor.
337        }
338    }
339
340    let mut output = Vec::with_capacity(flat_order.len());
341    helper(graph, flat_order, None, &mut loop_descendants, &mut output);
342    output
343}
344
345/// Set `src` or `dst` color if `None` based on the other (if possible):
346/// `None` indicates an op could be pull or push i.e. unary-in & unary-out.
347/// So in that case we color `src` or `dst` based on its newfound neighbor (the other one).
348///
349/// Returns if `src` and `dst` can be in the same subgraph.
350fn can_connect_colorize(
351    node_color: &mut SparseSecondaryMap<GraphNodeId, Color>,
352    src: GraphNodeId,
353    dst: GraphNodeId,
354) -> bool {
355    // Pull -> Pull
356    // Push -> Push
357    // Pull -> [Computation] -> Push
358    // Push -> [Handoff] -> Pull
359    let can_connect = match (node_color.get(src), node_color.get(dst)) {
360        // Linear chain, can't connect because it may cause future conflicts.
361        // But if it doesn't in the _future_ we can connect it (once either/both ends are determined).
362        (None, None) => false,
363
364        // Infer left side.
365        (None, Some(Color::Pull | Color::Comp)) => {
366            node_color.insert(src, Color::Pull);
367            true
368        }
369        (None, Some(Color::Push | Color::Hoff)) => {
370            node_color.insert(src, Color::Push);
371            true
372        }
373
374        // Infer right side.
375        (Some(Color::Pull | Color::Hoff), None) => {
376            node_color.insert(dst, Color::Pull);
377            true
378        }
379        (Some(Color::Comp | Color::Push), None) => {
380            node_color.insert(dst, Color::Push);
381            true
382        }
383
384        // Both sides already specified.
385        (Some(Color::Pull), Some(Color::Pull)) => true,
386        (Some(Color::Pull), Some(Color::Comp)) => true,
387        (Some(Color::Pull), Some(Color::Push)) => true,
388
389        (Some(Color::Comp), Some(Color::Pull)) => false,
390        (Some(Color::Comp), Some(Color::Comp)) => false,
391        (Some(Color::Comp), Some(Color::Push)) => true,
392
393        (Some(Color::Push), Some(Color::Pull)) => false,
394        (Some(Color::Push), Some(Color::Comp)) => false,
395        (Some(Color::Push), Some(Color::Push)) => true,
396
397        // Handoffs are not part of subgraphs.
398        (Some(Color::Hoff), Some(_)) => false,
399        (Some(_), Some(Color::Hoff)) => false,
400    };
401    can_connect
402}
403
404/// Marks tick-boundary (`defer_tick` / `defer_tick_lazy`) handoffs with their delay type
405/// for double-buffered codegen in `as_code`.
406///
407/// Also remaps `DelayType::Tick` → `DelayType::Loop` (and `TickLazy` → `LoopLazy`) for
408/// handoffs whose consumer is inside a nested loop. This allows a single `defer_tick()`
409/// operator to work in both tick-level and loop-iteration contexts.
410fn mark_tick_boundary_handoffs(
411    partitioned_graph: &mut DfirGraph,
412    tick_edges: &SecondaryMap<GraphEdgeId, DelayType>,
413) {
414    let tick_handoffs: Vec<_> = partitioned_graph
415        .nodes()
416        .filter_map(|(hoff_id, hoff)| {
417            if !matches!(hoff, GraphNode::Handoff { .. }) {
418                return None;
419            }
420            if partitioned_graph.node_degree_out(hoff_id) == 0 {
421                return None;
422            }
423            let (succ_edge, _) = partitioned_graph.node_successors(hoff_id).next().unwrap();
424            let &delay_type = tick_edges.get(succ_edge)?;
425
426            // Remap Tick/TickLazy → Loop/LoopLazy if the consumer is in a nested loop.
427            let consumer_loop = partitioned_graph
428                .node_successors(hoff_id)
429                .next()
430                .and_then(|(_, succ)| partitioned_graph.node_loop(succ));
431            let effective_delay_type = if let Some(loop_id) = consumer_loop {
432                if partitioned_graph.loop_parent(loop_id).is_some() {
433                    // Nested loop: remap to loop-level delay.
434                    match delay_type {
435                        DelayType::Tick => DelayType::Loop,
436                        DelayType::TickLazy => DelayType::LoopLazy,
437                        other => other,
438                    }
439                } else {
440                    delay_type
441                }
442            } else {
443                delay_type
444            };
445
446            Some((hoff_id, effective_delay_type))
447        })
448        .collect();
449
450    for (hoff_id, delay_type) in tick_handoffs {
451        partitioned_graph.set_handoff_delay_type(hoff_id, delay_type);
452    }
453}
454
455/// Main method for this module. Partitions a flat [`DfirGraph`] into one with subgraphs.
456///
457/// Returns an error if an intra-tick cycle exists in the graph.
458pub fn partition_graph(flat_graph: DfirGraph) -> Result<DfirGraph, Diagnostic> {
459    let (mut tick_edges, edge_barrier_pairs) = find_edge_barriers(&flat_graph);
460    let access_group_pairs = find_access_group_ordering(&flat_graph);
461    let mut partitioned_graph = flat_graph;
462
463    // Partition into subgraphs and insert handoffs.
464    make_subgraphs(
465        &mut partitioned_graph,
466        &mut tick_edges,
467        &edge_barrier_pairs,
468        &access_group_pairs,
469    )?;
470
471    // Mark tick-boundary handoffs for double-buffering.
472    mark_tick_boundary_handoffs(&mut partitioned_graph, &tick_edges);
473
474    Ok(partitioned_graph)
475}
476
477#[cfg(test)]
478mod tests {
479    use super::*;
480    use crate::graph::GraphNode;
481
482    fn make_op_node(graph: &mut DfirGraph, loop_ctx: Option<GraphLoopId>) -> GraphNodeId {
483        let operator: crate::parse::Operator = syn::parse_quote! { identity() };
484        graph.insert_node(GraphNode::Operator(operator), None, loop_ctx)
485    }
486
487    /// Test that subgraphs within a loop are contiguous after rearrangement,
488    /// even when the flat input order interleaves them with unrelated subgraphs.
489    #[test]
490    fn test_make_loops_contiguous_basic() {
491        let mut graph = DfirGraph::default();
492
493        // Create a loop.
494        let loop_id = graph.insert_loop(None);
495
496        // Create operator nodes: two outside the loop, two inside.
497        let op_a = make_op_node(&mut graph, None);
498        let op_b = make_op_node(&mut graph, None);
499        let op_c = make_op_node(&mut graph, Some(loop_id));
500        let op_d = make_op_node(&mut graph, Some(loop_id));
501
502        // Create subgraphs (one per operator node).
503        let sg_a = graph.insert_subgraph(vec![op_a]).unwrap();
504        let sg_b = graph.insert_subgraph(vec![op_b]).unwrap();
505        let sg_c = graph.insert_subgraph(vec![op_c]).unwrap();
506        let sg_d = graph.insert_subgraph(vec![op_d]).unwrap();
507
508        // Flat order: A, C, B, D — the loop subgraphs (C, D) are not contiguous.
509        let flat_order = vec![sg_a, sg_c, sg_b, sg_d];
510        let result = make_loops_contiguous(&graph, &flat_order);
511
512        // After rearrangement: A, C, D, B — loop subgraphs are now contiguous.
513        assert_eq!(result, vec![sg_a, sg_c, sg_d, sg_b]);
514    }
515
516    /// Test that two independent sibling loops each have their subgraphs contiguous.
517    #[test]
518    fn test_make_loops_contiguous_independent_loops() {
519        let mut graph = DfirGraph::default();
520
521        let loop1 = graph.insert_loop(None);
522        let loop2 = graph.insert_loop(None);
523
524        // Loop1: C, D. Loop2: E, F.
525        let op_c = make_op_node(&mut graph, Some(loop1));
526        let op_d = make_op_node(&mut graph, Some(loop1));
527        let op_e = make_op_node(&mut graph, Some(loop2));
528        let op_f = make_op_node(&mut graph, Some(loop2));
529
530        let sg_c = graph.insert_subgraph(vec![op_c]).unwrap();
531        let sg_d = graph.insert_subgraph(vec![op_d]).unwrap();
532        let sg_e = graph.insert_subgraph(vec![op_e]).unwrap();
533        let sg_f = graph.insert_subgraph(vec![op_f]).unwrap();
534
535        // Flat order interleaves the two loops: C, E, D, F.
536        let flat_order = vec![sg_c, sg_e, sg_d, sg_f];
537        let result = make_loops_contiguous(&graph, &flat_order);
538
539        // Both loops should be contiguous. Loop1 appears first (C seen first).
540        let pos_c = result.iter().position(|&s| s == sg_c).unwrap();
541        let pos_d = result.iter().position(|&s| s == sg_d).unwrap();
542        let pos_e = result.iter().position(|&s| s == sg_e).unwrap();
543        let pos_f = result.iter().position(|&s| s == sg_f).unwrap();
544
545        // C before D, E before F (relative order preserved).
546        assert!(pos_c < pos_d);
547        assert!(pos_e < pos_f);
548
549        // Contiguity: C and D are adjacent, E and F are adjacent.
550        assert_eq!(pos_d - pos_c, 1, "Loop1 subgraphs must be contiguous");
551        assert_eq!(pos_f - pos_e, 1, "Loop2 subgraphs must be contiguous");
552    }
553
554    /// Test nested loops: inner loop subgraphs are contiguous within the outer loop block.
555    #[test]
556    fn test_make_loops_contiguous_nested() {
557        let mut graph = DfirGraph::default();
558
559        let outer = graph.insert_loop(None);
560        let inner = graph.insert_loop(Some(outer));
561
562        // Outer loop has: op_a, then inner loop (op_b, op_c), then op_d.
563        let op_a = make_op_node(&mut graph, Some(outer));
564        let op_b = make_op_node(&mut graph, Some(inner));
565        let op_c = make_op_node(&mut graph, Some(inner));
566        let op_d = make_op_node(&mut graph, Some(outer));
567
568        let sg_a = graph.insert_subgraph(vec![op_a]).unwrap();
569        let sg_b = graph.insert_subgraph(vec![op_b]).unwrap();
570        let sg_c = graph.insert_subgraph(vec![op_c]).unwrap();
571        let sg_d = graph.insert_subgraph(vec![op_d]).unwrap();
572
573        // Flat order: A, B, C, D (already valid).
574        let flat_order = vec![sg_a, sg_b, sg_c, sg_d];
575        let result = make_loops_contiguous(&graph, &flat_order);
576
577        // Expected: A, B, C, D (all contiguous within outer, B/C contiguous within inner).
578        assert_eq!(result, vec![sg_a, sg_b, sg_c, sg_d]);
579    }
580
581    /// Test nested loops with interleaving: outer-level subgraph between inner loop items.
582    #[test]
583    fn test_make_loops_contiguous_nested_interleaved() {
584        let mut graph = DfirGraph::default();
585
586        let outer = graph.insert_loop(None);
587        let inner = graph.insert_loop(Some(outer));
588
589        let op_a = make_op_node(&mut graph, Some(outer));
590        let op_b = make_op_node(&mut graph, Some(inner));
591        let op_c = make_op_node(&mut graph, Some(inner));
592        let op_d = make_op_node(&mut graph, Some(outer));
593
594        let sg_a = graph.insert_subgraph(vec![op_a]).unwrap();
595        let sg_b = graph.insert_subgraph(vec![op_b]).unwrap();
596        let sg_c = graph.insert_subgraph(vec![op_c]).unwrap();
597        let sg_d = graph.insert_subgraph(vec![op_d]).unwrap();
598
599        // Flat order: A, B, D, C — D (outer) is between B and C (inner).
600        let flat_order = vec![sg_a, sg_b, sg_d, sg_c];
601        let result = make_loops_contiguous(&graph, &flat_order);
602
603        // After rearrangement within outer: A, B, C, D — inner loop (B, C) contiguous.
604        assert_eq!(result, vec![sg_a, sg_b, sg_c, sg_d]);
605    }
606}