Skip to main content

jstd/graph/analysis/
sese.rs

1//! Canonical single-entry/single-exit regions and their program-structure tree.
2//!
3//! Regions are edge based: an entry edge dominates a region's exit edge, the
4//! exit post-dominates the entry, and both edges are cycle equivalent.  A
5//! synthetic exit (connected back to the root) makes the reachable CFG a
6//! flowgraph with one terminal.  Synthetic edges are analysis-only and never
7//! appear in the returned tree.
8//!
9//! Edge cycle-equivalence classes are computed by the
10//! Johnson--Pearson--Pingali undirected-DFS bracket-list algorithm. Canonical
11//! boundaries are then selected from each class using edge dominance and
12//! post-dominance. Test builds retain the direct cycle predicate as an oracle.
13
14use std::collections::{BTreeSet, VecDeque};
15
16use rustc_hash::{FxHashMap as HashMap, FxHashSet as HashSet};
17
18use crate::graph::{Graph, edge::Edge, node::Node};
19
20/// One canonical edge-based SESE region.
21#[derive(Clone, Debug, PartialEq, Eq)]
22pub struct SeseRegion<NodeId, EdgeId> {
23    /// Parent region; `None` only for the synthetic root region.
24    pub parent: Option<usize>,
25    /// Immediate child regions, in deterministic entry-edge order.
26    pub children: Vec<usize>,
27    /// Boundary edge entering this region (`None` for the root region).
28    pub entry_edge: Option<EdgeId>,
29    /// Boundary edge leaving this region (`None` for the root region).
30    pub exit_edge: Option<EdgeId>,
31    /// Nodes owned directly by this region (children are not repeated here).
32    pub nodes: Vec<NodeId>,
33    /// All original nodes contained by the region, including descendants.
34    pub contained_nodes: Vec<NodeId>,
35}
36
37/// Program-structure tree for the nodes reachable from a CFG entry.
38#[derive(Clone, Debug, PartialEq, Eq)]
39pub struct SeseTree<NodeId, EdgeId> {
40    pub regions: Vec<SeseRegion<NodeId, EdgeId>>,
41}
42
43impl<NodeId, EdgeId> SeseTree<NodeId, EdgeId> {
44    pub fn root(&self) -> &SeseRegion<NodeId, EdgeId> {
45        &self.regions[0]
46    }
47
48    pub fn has_nontrivial_regions(&self) -> bool {
49        self.regions.len() > 1
50    }
51}
52
53#[derive(Clone, Copy)]
54struct IEdge<E> {
55    from: usize,
56    to: usize,
57    original: Option<E>,
58}
59
60/// A valid edge-SESE candidate before canonical boundary selection.
61///
62/// This is intentionally crate-private: public [`compute_sese`] retains its
63/// canonical smallest-boundary semantics, while layout can retain useful
64/// non-canonical regions below a node hammock.
65#[derive(Clone, Debug, PartialEq, Eq)]
66pub struct SeseCandidate<NodeId, EdgeId> {
67    pub entry_edge: EdgeId,
68    pub exit_edge: EdgeId,
69    pub contained_nodes: Vec<NodeId>,
70}
71
72/// Enumerates every non-empty valid edge-SESE candidate.  This is the same
73/// dominance, post-dominance, JPP-cycle-class, and `region_nodes` test used by
74/// [`compute_sese`], before its canonical smallest-boundary filter.
75pub fn compute_sese_candidates<G>(
76    graph: &G,
77    root: G::NodeId,
78) -> Vec<SeseCandidate<G::NodeId, G::EdgeId>>
79where
80    G: Graph,
81    G::NodeId: Ord,
82    G::EdgeId: Ord,
83{
84    let mut ids = Vec::new();
85    let mut seen: HashSet<G::NodeId> = HashSet::default();
86    let mut queue = VecDeque::from([root]);
87    while let Some(id) = queue.pop_front() {
88        if !seen.insert(id) {
89            continue;
90        }
91        ids.push(id);
92        let mut succ: Vec<_> = graph
93            .get_node(id)
94            .into_iter()
95            .flat_map(|node| node.children().map(|edge| edge.node_id()))
96            .collect();
97        succ.sort();
98        queue.extend(succ);
99    }
100    ids.sort();
101
102    let index: HashMap<_, _> = ids.iter().enumerate().map(|(i, id)| (*id, i)).collect();
103    let synthetic_exit = ids.len();
104    let mut edges = Vec::new();
105    let mut original_edges: Vec<_> = graph
106        .edges()
107        .filter_map(|edge| {
108            let from = edge.from_id();
109            let to = edge.to_id();
110            (from != to && index.contains_key(&from) && index.contains_key(&to)).then_some((
111                edge.id(),
112                from,
113                to,
114            ))
115        })
116        .collect();
117    original_edges.sort_by_key(|(id, _, _)| *id);
118    for (id, from, to) in original_edges {
119        edges.push(IEdge {
120            from: index[&from],
121            to: index[&to],
122            original: Some(id),
123        });
124    }
125    let original_count = edges.len();
126    let mut has_out = vec![false; ids.len()];
127    for edge in &edges {
128        has_out[edge.from] = true;
129    }
130    let mut added_exit = false;
131    for (node, outgoing) in has_out.into_iter().enumerate() {
132        if !outgoing {
133            edges.push(IEdge {
134                from: node,
135                to: synthetic_exit,
136                original: None,
137            });
138            added_exit = true;
139        }
140    }
141    if !added_exit && !ids.is_empty() {
142        edges.push(IEdge {
143            from: ids.len() - 1,
144            to: synthetic_exit,
145            original: None,
146        });
147        added_exit = true;
148    }
149    if added_exit {
150        edges.push(IEdge {
151            from: synthetic_exit,
152            to: index[&root],
153            original: None,
154        });
155    }
156
157    let n = ids.len() + 1;
158    let entry = index[&root];
159    let cycle_classes = jpp_cycle_classes(&edges, n, entry);
160    let mut candidates = Vec::new();
161    for a in 0..original_count {
162        for b in 0..original_count {
163            if a == b || !edge_dominates(&edges, n, entry, a, b) {
164                continue;
165            }
166            if !edge_postdominates(&edges, n, synthetic_exit, b, a)
167                || cycle_classes[a] == 0
168                || cycle_classes[a] != cycle_classes[b]
169            {
170                continue;
171            }
172            let contained = region_nodes(&edges, n, a, b);
173            if !contained.is_empty() {
174                candidates.push(SeseCandidate {
175                    entry_edge: edges[a].original.unwrap(),
176                    exit_edge: edges[b].original.unwrap(),
177                    contained_nodes: contained.into_iter().map(|node| ids[node]).collect(),
178                });
179            }
180        }
181    }
182    candidates.sort_by_key(|candidate| {
183        (
184            std::cmp::Reverse(candidate.contained_nodes.len()),
185            candidate.entry_edge,
186            candidate.exit_edge,
187        )
188    });
189    candidates
190}
191
192/// Identifies canonical SESE regions in the directed subgraph reachable from
193/// `root`. Multiple exits are joined to an internal synthetic exit. Self-loops
194/// do not form region boundaries.
195pub fn compute_sese<G>(graph: &G, root: G::NodeId) -> SeseTree<G::NodeId, G::EdgeId>
196where
197    G: Graph,
198    G::NodeId: Ord,
199    G::EdgeId: Ord,
200{
201    let mut ids = Vec::new();
202    let mut seen: HashSet<G::NodeId> = HashSet::default();
203    let mut queue = VecDeque::from([root]);
204    while let Some(id) = queue.pop_front() {
205        if !seen.insert(id) {
206            continue;
207        }
208        ids.push(id);
209        let mut succ: Vec<_> = graph
210            .get_node(id)
211            .into_iter()
212            .flat_map(|node| node.children().map(|edge| edge.node_id()))
213            .collect();
214        succ.sort();
215        queue.extend(succ);
216    }
217    ids.sort();
218
219    let index: HashMap<_, _> = ids.iter().enumerate().map(|(i, id)| (*id, i)).collect();
220    let synthetic_exit = ids.len();
221    let mut edges = Vec::new();
222    let mut original_edges: Vec<_> = graph
223        .edges()
224        .filter_map(|edge| {
225            let from = edge.from_id();
226            let to = edge.to_id();
227            (from != to && index.contains_key(&from) && index.contains_key(&to)).then_some((
228                edge.id(),
229                from,
230                to,
231            ))
232        })
233        .collect();
234    original_edges.sort_by_key(|(id, _, _)| *id);
235    for (id, from, to) in original_edges {
236        edges.push(IEdge {
237            from: index[&from],
238            to: index[&to],
239            original: Some(id),
240        });
241    }
242
243    let mut has_out = vec![false; ids.len()];
244    for edge in &edges {
245        has_out[edge.from] = true;
246    }
247    let mut added_exit = false;
248    for (node, outgoing) in has_out.into_iter().enumerate() {
249        if !outgoing {
250            edges.push(IEdge {
251                from: node,
252                to: synthetic_exit,
253                original: None,
254            });
255            added_exit = true;
256        }
257    }
258    // A closed CFG can have no natural exit (for example, an infinite loop).
259    // Attach its deterministic last reachable node so the undirected JPP
260    // flowgraph still has a bracket for every non-root tree edge.
261    if !added_exit && !ids.is_empty() {
262        edges.push(IEdge {
263            from: ids.len() - 1,
264            to: synthetic_exit,
265            original: None,
266        });
267        added_exit = true;
268    }
269    if added_exit {
270        edges.push(IEdge {
271            from: synthetic_exit,
272            to: index[&root],
273            original: None,
274        });
275    }
276
277    let n = ids.len() + 1;
278    let entry = index[&root];
279    let original_count = edges
280        .iter()
281        .take_while(|edge| edge.original.is_some())
282        .count();
283    let cycle_classes = jpp_cycle_classes(&edges, n, entry);
284    #[cfg(test)]
285    for a in 0..original_count {
286        for b in 0..original_count {
287            if a != b {
288                assert_eq!(
289                    cycle_classes[a] == cycle_classes[b],
290                    cycle_equivalent(&edges, n, a, b),
291                    "cycle-class mismatch for internal edges {a} and {b}"
292                );
293            }
294        }
295    }
296    let mut candidates = Vec::<(usize, usize, BTreeSet<usize>)>::new();
297    for a in 0..original_count {
298        for b in 0..original_count {
299            if a == b || !edge_dominates(&edges, n, entry, a, b) {
300                continue;
301            }
302            if !edge_postdominates(&edges, n, synthetic_exit, b, a) {
303                continue;
304            }
305            if cycle_classes[a] == 0 || cycle_classes[a] != cycle_classes[b] {
306                continue;
307            }
308            #[cfg(test)]
309            debug_assert!(cycle_equivalent(&edges, n, a, b));
310            let contained = region_nodes(&edges, n, a, b);
311            if !contained.is_empty() {
312                candidates.push((a, b, contained));
313            }
314        }
315    }
316
317    // A canonical region is the smallest region for which an edge is an entry
318    // or exit boundary. A boundary may therefore be the exit of one canonical
319    // region and the entry of the next; consuming boundaries greedily would
320    // incorrectly merge sequential structured constructs.
321    candidates.sort_by_key(|(a, b, nodes)| (nodes.len(), *a, *b));
322    let mut smallest_for_boundary: HashMap<usize, usize> = HashMap::default();
323    for (index, (a, b, _)) in candidates.iter().enumerate() {
324        smallest_for_boundary.entry(*a).or_insert(index);
325        smallest_for_boundary.entry(*b).or_insert(index);
326    }
327    let mut selected: Vec<_> = candidates
328        .iter()
329        .enumerate()
330        .filter(|(index, (a, b, _))| {
331            smallest_for_boundary.get(a) == Some(index)
332                || smallest_for_boundary.get(b) == Some(index)
333        })
334        .map(|(_, candidate)| candidate.clone())
335        .collect();
336    // Defensive laminarity filter for malformed/irreducible flowgraphs. JPP
337    // canonical regions are laminar; crossing candidates belong to the root.
338    selected.retain(|(_, _, candidate)| {
339        !candidates.iter().any(|(_, _, other)| {
340            let intersects = candidate.iter().any(|node| other.contains(node));
341            intersects && !candidate.is_subset(other) && !other.is_subset(candidate)
342        })
343    });
344    selected.sort_by_key(|(a, b, nodes)| (std::cmp::Reverse(nodes.len()), *a, *b));
345
346    let mut regions = vec![SeseRegion {
347        parent: None,
348        children: Vec::new(),
349        entry_edge: None,
350        exit_edge: None,
351        nodes: Vec::new(),
352        contained_nodes: ids.clone(),
353    }];
354    let mut sets = vec![(0..ids.len()).collect::<BTreeSet<_>>()];
355    for (a, b, nodes) in selected {
356        let parent = sets
357            .iter()
358            .enumerate()
359            .filter(|(_, set)| nodes.is_subset(set))
360            .min_by_key(|(_, set)| set.len())
361            .map(|(i, _)| i)
362            .unwrap_or(0);
363        let id = regions.len();
364        regions.push(SeseRegion {
365            parent: Some(parent),
366            children: Vec::new(),
367            entry_edge: edges[a].original,
368            exit_edge: edges[b].original,
369            nodes: Vec::new(),
370            contained_nodes: nodes.iter().map(|i| ids[*i]).collect(),
371        });
372        sets.push(nodes);
373        regions[parent].children.push(id);
374    }
375    for region in &mut regions {
376        // Region ids are assigned in deterministic boundary-edge order.
377        region.children.sort_unstable();
378    }
379    // Assign each node to its deepest containing region.
380    for (node_index, node) in ids.iter().copied().enumerate() {
381        let owner = sets
382            .iter()
383            .enumerate()
384            .filter(|(_, set)| set.contains(&node_index))
385            .min_by_key(|(_, set)| set.len())
386            .map(|(id, _)| id)
387            .unwrap_or(0);
388        regions[owner].nodes.push(node);
389    }
390
391    SeseTree { regions }
392}
393
394/// Johnson--Pearson--Pingali cycle-equivalence classification. Capping edges
395/// are internal bracket sentinels and are discarded with the working graph.
396fn jpp_cycle_classes<E: Copy>(input: &[IEdge<E>], n: usize, root: usize) -> Vec<usize> {
397    let mut edges = input.to_vec();
398    let mut incident = vec![Vec::<usize>::new(); n];
399    for (id, edge) in edges.iter().enumerate() {
400        incident[edge.from].push(id);
401        incident[edge.to].push(id);
402    }
403
404    let mut parent = vec![None; n];
405    let mut parent_edge = vec![None; n];
406    let mut number = vec![usize::MAX; n];
407    let mut end = vec![0; n];
408    let mut preorder = Vec::new();
409    let mut tree = vec![false; edges.len()];
410    // Keeping the DFS arrays explicit makes the JPP state correspondence
411    // visible; wrapping them solely to reduce the parameter count obscures it.
412    #[allow(clippy::too_many_arguments)]
413    fn visit<E: Copy>(
414        node: usize,
415        edges: &[IEdge<E>],
416        incident: &[Vec<usize>],
417        parent: &mut [Option<usize>],
418        parent_edge: &mut [Option<usize>],
419        number: &mut [usize],
420        end: &mut [usize],
421        preorder: &mut Vec<usize>,
422        tree: &mut [bool],
423    ) {
424        number[node] = preorder.len();
425        preorder.push(node);
426        for &eid in &incident[node] {
427            if parent_edge[node] == Some(eid) {
428                continue;
429            }
430            let edge = edges[eid];
431            let other = if edge.from == node {
432                edge.to
433            } else {
434                edge.from
435            };
436            if number[other] == usize::MAX {
437                parent[other] = Some(node);
438                parent_edge[other] = Some(eid);
439                visit(
440                    other,
441                    edges,
442                    incident,
443                    parent,
444                    parent_edge,
445                    number,
446                    end,
447                    preorder,
448                    tree,
449                );
450                tree[eid] = true;
451            }
452        }
453        end[node] = preorder.len();
454    }
455    visit(
456        root,
457        &edges,
458        &incident,
459        &mut parent,
460        &mut parent_edge,
461        &mut number,
462        &mut end,
463        &mut preorder,
464        &mut tree,
465    );
466    let is_descendant = |node: usize, ancestor: usize| {
467        node != ancestor && number[ancestor] <= number[node] && number[node] < end[ancestor]
468    };
469
470    let mut hi = vec![usize::MAX; n];
471    let mut brackets = vec![Vec::<usize>::new(); n];
472    let mut classes = vec![0usize; edges.len()];
473    let mut recent_size = vec![0usize; edges.len()];
474    let mut recent_class = vec![0usize; edges.len()];
475    let mut capping = Vec::<usize>::new();
476    let mut next_class = 1usize;
477
478    for &node in preorder.iter().rev() {
479        let children: Vec<_> = preorder
480            .iter()
481            .copied()
482            .filter(|child| parent[*child] == Some(node))
483            .collect();
484        let hi0 = incident[node]
485            .iter()
486            .copied()
487            .filter(|eid| !tree[*eid])
488            .filter_map(|eid| {
489                let edge = edges[eid];
490                let other = if edge.from == node {
491                    edge.to
492                } else {
493                    edge.from
494                };
495                is_descendant(node, other).then_some(number[other])
496            })
497            .min()
498            .unwrap_or(usize::MAX);
499        let hi1 = children
500            .iter()
501            .map(|child| hi[*child])
502            .min()
503            .unwrap_or(usize::MAX);
504        hi[node] = hi0.min(hi1);
505        let mut skipped_hi_child = false;
506        let hi2 = children
507            .iter()
508            .filter_map(|child| {
509                if !skipped_hi_child && hi[*child] == hi1 {
510                    skipped_hi_child = true;
511                    None
512                } else {
513                    Some(hi[*child])
514                }
515            })
516            .min()
517            .unwrap_or(usize::MAX);
518
519        for child in children {
520            let child_brackets = std::mem::take(&mut brackets[child]);
521            brackets[node].extend(child_brackets);
522        }
523        for &eid in &capping {
524            let edge = edges[eid];
525            // This mirrors Edge::other in the reference algorithm: for a node
526            // not incident to the cap, its target is considered the child.
527            let child = if edge.to == node { edge.from } else { edge.to };
528            if is_descendant(child, node) {
529                brackets[node].retain(|candidate| *candidate != eid);
530            }
531        }
532        for &eid in &incident[node] {
533            if tree[eid] {
534                continue;
535            }
536            let edge = edges[eid];
537            let other = if edge.from == node {
538                edge.to
539            } else {
540                edge.from
541            };
542            if is_descendant(other, node) {
543                brackets[node].retain(|candidate| *candidate != eid);
544                if classes[eid] == 0 {
545                    classes[eid] = next_class;
546                    next_class += 1;
547                }
548            }
549        }
550        for &eid in &incident[node] {
551            if tree[eid] {
552                continue;
553            }
554            let edge = edges[eid];
555            let other = if edge.from == node {
556                edge.to
557            } else {
558                edge.from
559            };
560            if is_descendant(node, other) {
561                brackets[node].push(eid);
562            }
563        }
564        if hi2 < hi0 {
565            let eid = edges.len();
566            let ancestor = preorder[hi2];
567            edges.push(IEdge {
568                from: node,
569                to: ancestor,
570                original: None,
571            });
572            incident[node].push(eid);
573            incident[ancestor].push(eid);
574            tree.push(false);
575            classes.push(0);
576            recent_size.push(0);
577            recent_class.push(0);
578            capping.push(eid);
579            brackets[node].push(eid);
580        }
581        if let Some(parent_eid) = parent_edge[node] {
582            let Some(&top) = brackets[node].last() else {
583                // Not a proper flowgraph (typically a non-terminating sink
584                // SCC). The defining predicate remains exact for this case.
585                return direct_cycle_classes(input, n);
586            };
587            if recent_size[top] != brackets[node].len() {
588                recent_size[top] = brackets[node].len();
589                recent_class[top] = next_class;
590                next_class += 1;
591            }
592            classes[parent_eid] = recent_class[top];
593            if recent_size[top] == 1 {
594                classes[top] = classes[parent_eid];
595            }
596        }
597    }
598    classes.truncate(input.len());
599
600    // The bracket algorithm assumes a proper flowgraph. Optimized CFGs can
601    // violate that assumption through non-terminating SCCs and irreducible
602    // control flow. Validate its partition and use the defining cycle
603    // predicate as a correctness fallback for those components.
604    let valid = (0..input.len()).all(|a| {
605        (0..input.len()).all(|b| (classes[a] == classes[b]) == cycle_equivalent(input, n, a, b))
606    });
607    if valid {
608        return classes;
609    }
610
611    direct_cycle_classes(input, n)
612}
613
614fn direct_cycle_classes<E: Copy>(edges: &[IEdge<E>], n: usize) -> Vec<usize> {
615    let mut parent: Vec<_> = (0..edges.len()).collect();
616    fn find(parent: &mut [usize], mut node: usize) -> usize {
617        while parent[node] != node {
618            parent[node] = parent[parent[node]];
619            node = parent[node];
620        }
621        node
622    }
623    for a in 0..edges.len() {
624        for b in (a + 1)..edges.len() {
625            if cycle_equivalent(edges, n, a, b) {
626                let ra = find(&mut parent, a);
627                let rb = find(&mut parent, b);
628                parent[rb] = ra;
629            }
630        }
631    }
632    let mut labels = HashMap::default();
633    let mut next = 1usize;
634    (0..edges.len())
635        .map(|edge| {
636            let root = find(&mut parent, edge);
637            *labels.entry(root).or_insert_with(|| {
638                let label = next;
639                next += 1;
640                label
641            })
642        })
643        .collect()
644}
645
646fn reachable<E: Copy>(
647    edges: &[IEdge<E>],
648    n: usize,
649    start: usize,
650    skip: Option<usize>,
651    reverse: bool,
652) -> Vec<bool> {
653    let mut seen = vec![false; n];
654    let mut stack = vec![start];
655    while let Some(node) = stack.pop() {
656        if seen[node] {
657            continue;
658        }
659        seen[node] = true;
660        for (i, edge) in edges.iter().enumerate() {
661            if skip == Some(i) {
662                continue;
663            }
664            let (from, to) = if reverse {
665                (edge.to, edge.from)
666            } else {
667                (edge.from, edge.to)
668            };
669            if from == node && !seen[to] {
670                stack.push(to);
671            }
672        }
673    }
674    seen
675}
676
677fn edge_dominates<E: Copy>(edges: &[IEdge<E>], n: usize, root: usize, a: usize, b: usize) -> bool {
678    !reachable(edges, n, root, Some(a), false)[edges[b].from]
679}
680
681fn edge_postdominates<E: Copy>(
682    edges: &[IEdge<E>],
683    n: usize,
684    exit: usize,
685    b: usize,
686    a: usize,
687) -> bool {
688    !reachable(edges, n, exit, Some(b), true)[edges[a].to]
689}
690
691fn cycle_equivalent<E: Copy>(edges: &[IEdge<E>], n: usize, a: usize, b: usize) -> bool {
692    let a_cycle_without_b = reachable(edges, n, edges[a].to, Some(b), false)[edges[a].from];
693    let b_cycle_without_a = reachable(edges, n, edges[b].to, Some(a), false)[edges[b].from];
694    !a_cycle_without_b && !b_cycle_without_a
695}
696
697fn region_nodes<E: Copy>(
698    edges: &[IEdge<E>],
699    n: usize,
700    entry: usize,
701    exit: usize,
702) -> BTreeSet<usize> {
703    let forward = reachable(edges, n, edges[entry].to, Some(exit), false);
704    let backward = reachable(edges, n, edges[exit].from, Some(entry), true);
705    (0..n.saturating_sub(1))
706        .filter(|node| forward[*node] && backward[*node])
707        .collect()
708}
709
710#[cfg(test)]
711mod tests {
712    use jstd_derive::Identifier;
713    use proptest::prelude::*;
714
715    use super::*;
716    use crate::graph::owning::OwningGraph;
717
718    #[derive(Identifier)]
719    struct NodeId(usize);
720    #[derive(Identifier)]
721    struct EdgeId(usize);
722    type G = OwningGraph<NodeId, EdgeId, (), ()>;
723
724    #[test]
725    fn finds_diamond_between_canonical_boundary_edges() {
726        let mut graph = G::default();
727        let s = graph.make_node(());
728        let a = graph.make_node(());
729        let b = graph.make_node(());
730        let c = graph.make_node(());
731        let d = graph.make_node(());
732        let t = graph.make_node(());
733        let entry = graph.make_edge(s, a, ());
734        graph.make_edge(a, b, ());
735        graph.make_edge(a, c, ());
736        graph.make_edge(b, d, ());
737        graph.make_edge(c, d, ());
738        let exit = graph.make_edge(d, t, ());
739
740        let tree = compute_sese(&graph, s);
741        let region = tree
742            .regions
743            .iter()
744            .find(|region| region.entry_edge == Some(entry))
745            .expect("diamond region");
746        assert_eq!(region.exit_edge, Some(exit));
747        assert_eq!(region.contained_nodes, vec![a, b, c, d]);
748    }
749
750    #[test]
751    fn sequential_regions_may_share_a_boundary_edge() {
752        let mut graph = G::default();
753        let n: Vec<_> = (0..10).map(|_| graph.make_node(())).collect();
754        let first_entry = graph.make_edge(n[0], n[1], ());
755        graph.make_edge(n[1], n[2], ());
756        graph.make_edge(n[1], n[3], ());
757        graph.make_edge(n[2], n[4], ());
758        graph.make_edge(n[3], n[4], ());
759        let shared = graph.make_edge(n[4], n[5], ());
760        graph.make_edge(n[5], n[6], ());
761        graph.make_edge(n[5], n[7], ());
762        graph.make_edge(n[6], n[8], ());
763        graph.make_edge(n[7], n[8], ());
764        graph.make_edge(n[8], n[9], ());
765
766        let tree = compute_sese(&graph, n[0]);
767        assert!(tree.regions.iter().any(|region| {
768            region.entry_edge == Some(first_entry) && region.exit_edge == Some(shared)
769        }));
770        assert!(
771            tree.regions
772                .iter()
773                .any(|region| region.entry_edge == Some(shared))
774        );
775    }
776
777    #[test]
778    fn multiple_exits_use_an_internal_terminal_without_exposing_it() {
779        let mut graph = G::default();
780        let root = graph.make_node(());
781        let left = graph.make_node(());
782        let right = graph.make_node(());
783        graph.make_edge(root, left, ());
784        graph.make_edge(root, right, ());
785
786        let tree = compute_sese(&graph, root);
787        let mut owned: Vec<_> = tree
788            .regions
789            .iter()
790            .flat_map(|region| region.nodes.iter().copied())
791            .collect();
792        owned.sort();
793        assert_eq!(owned, vec![root, left, right]);
794        assert!(tree.regions.iter().all(|region| {
795            region.entry_edge.is_some() == region.exit_edge.is_some() || region.parent.is_none()
796        }));
797    }
798
799    #[test]
800    fn loop_body_is_a_canonical_region() {
801        let mut graph = G::default();
802        let s = graph.make_node(());
803        let header = graph.make_node(());
804        let body = graph.make_node(());
805        let after = graph.make_node(());
806        let t = graph.make_node(());
807        let entry = graph.make_edge(s, header, ());
808        graph.make_edge(header, body, ());
809        graph.make_edge(body, header, ());
810        let exit = graph.make_edge(header, after, ());
811        graph.make_edge(after, t, ());
812
813        let tree = compute_sese(&graph, s);
814        assert!(tree.regions.iter().any(|region| {
815            region.entry_edge == Some(entry)
816                && region.exit_edge == Some(exit)
817                && region.contained_nodes.contains(&header)
818                && region.contained_nodes.contains(&body)
819        }));
820    }
821
822    proptest! {
823        #![proptest_config(ProptestConfig::with_cases(128))]
824        #[test]
825        fn generated_program_structure_trees_are_laminar_and_total(
826            node_count in 2usize..=10,
827            extra_edges in proptest::collection::vec((0usize..16, 0usize..16), 0..24),
828        ) {
829            let mut graph = G::default();
830            let nodes: Vec<_> = (0..node_count).map(|_| graph.make_node(())).collect();
831            // A backbone makes every generated node reachable while the extra
832            // edges supply branches, loops, and irreducible cross-links.
833            for pair in nodes.windows(2) {
834                graph.make_edge(pair[0], pair[1], ());
835            }
836            for (from, to) in extra_edges {
837                graph.make_edge(nodes[from % node_count], nodes[to % node_count], ());
838            }
839
840            let tree = compute_sese(&graph, nodes[0]);
841            let mut owned: Vec<_> = tree.regions.iter()
842                .flat_map(|region| region.nodes.iter().copied())
843                .collect();
844            owned.sort();
845            prop_assert_eq!(&owned, &nodes);
846            for (id, region) in tree.regions.iter().enumerate() {
847                for &child in &region.children {
848                    prop_assert_eq!(tree.regions[child].parent, Some(id));
849                    prop_assert!(tree.regions[child].contained_nodes.iter()
850                        .all(|node| region.contained_nodes.contains(node)));
851                }
852            }
853            for (i, lhs) in tree.regions.iter().enumerate().skip(1) {
854                for rhs in tree.regions.iter().skip(i + 1) {
855                    let intersects = lhs.contained_nodes.iter()
856                        .any(|node| rhs.contained_nodes.contains(node));
857                    prop_assert!(!intersects
858                        || lhs.contained_nodes.iter().all(|node| rhs.contained_nodes.contains(node))
859                        || rhs.contained_nodes.iter().all(|node| lhs.contained_nodes.contains(node)));
860                }
861            }
862        }
863    }
864
865    #[test]
866    fn nested_regions_are_laminar_and_assign_every_node_once() {
867        let mut graph = G::default();
868        let nodes: Vec<_> = (0..8).map(|_| graph.make_node(())).collect();
869        for pair in nodes.windows(2) {
870            graph.make_edge(pair[0], pair[1], ());
871        }
872        graph.make_edge(nodes[1], nodes[4], ());
873        graph.make_edge(nodes[2], nodes[3], ());
874        graph.make_edge(nodes[4], nodes[6], ());
875
876        let tree = compute_sese(&graph, nodes[0]);
877        let owned: Vec<_> = tree
878            .regions
879            .iter()
880            .flat_map(|region| region.nodes.iter().copied())
881            .collect();
882        let mut unique = owned.clone();
883        unique.sort();
884        unique.dedup();
885        assert_eq!(owned.len(), nodes.len());
886        assert_eq!(unique, nodes);
887        for (id, region) in tree.regions.iter().enumerate().skip(1) {
888            let parent = region.parent.unwrap();
889            assert!(parent < id);
890            assert!(
891                region
892                    .contained_nodes
893                    .iter()
894                    .all(|node| { tree.regions[parent].contained_nodes.contains(node) })
895            );
896        }
897    }
898}