Skip to main content

celox_analysis/
cfg_order.rs

1use std::hash::Hash;
2
3use fxhash::FxHashMap;
4
5/// Return blocks in dominator-tree preorder.
6///
7/// The order keeps each block after its immediate dominator, while preserving
8/// a deterministic CFG order for sibling subtrees. Blocks unreachable from
9/// `entry` are appended in key order so callers still get a total ordering.
10pub fn dominance_order<K, I, F>(entry: K, blocks: I, mut successors: F) -> Vec<K>
11where
12    K: Copy + Eq + Hash + Ord,
13    I: IntoIterator<Item = K>,
14    F: FnMut(K) -> Vec<K>,
15{
16    let mut ids = blocks.into_iter().collect::<Vec<_>>();
17    ids.sort_unstable();
18    ids.dedup();
19
20    if ids.is_empty() {
21        return ids;
22    }
23
24    let Some(entry_index) = ids.iter().position(|&id| id == entry) else {
25        return ids;
26    };
27    let index = ids
28        .iter()
29        .enumerate()
30        .map(|(index, &id)| (id, index))
31        .collect::<FxHashMap<_, _>>();
32
33    let successors = ids
34        .iter()
35        .map(|&id| {
36            let mut successors = successors(id)
37                .into_iter()
38                .filter_map(|successor| index.get(&successor).copied())
39                .collect::<Vec<_>>();
40            successors.dedup();
41            successors
42        })
43        .collect::<Vec<_>>();
44
45    // Discover the reachable CFG and record a reverse-postorder. The
46    // discovery order is retained for deterministic dominator-tree siblings.
47    let mut discovered = vec![false; ids.len()];
48    let mut discovery_order = Vec::new();
49    let mut postorder = Vec::new();
50    discovered[entry_index] = true;
51    discovery_order.push(entry_index);
52    let mut stack = vec![(entry_index, false)];
53    while let Some((block, expanded)) = stack.pop() {
54        if expanded {
55            postorder.push(block);
56            continue;
57        }
58
59        if block != entry_index {
60            discovery_order.push(block);
61        }
62        stack.push((block, true));
63        for &successor in successors[block].iter().rev() {
64            if discovered[successor] {
65                continue;
66            }
67            discovered[successor] = true;
68            stack.push((successor, false));
69        }
70    }
71    postorder.reverse();
72
73    let mut predecessors = vec![Vec::new(); ids.len()];
74    for (block, successors) in successors.iter().enumerate() {
75        if !discovered[block] {
76            continue;
77        }
78        for &successor in successors {
79            if discovered[successor] {
80                predecessors[successor].push(block);
81            }
82        }
83    }
84
85    // Cooper-Harvey-Kennedy immediate dominators. RPO numbering makes the
86    // intersect operation compact and also handles backedges correctly.
87    let mut rpo_number = vec![usize::MAX; ids.len()];
88    for (number, &block) in postorder.iter().enumerate() {
89        rpo_number[block] = number;
90    }
91    let mut idom = vec![None; ids.len()];
92    idom[entry_index] = Some(entry_index);
93
94    let intersect = |mut left: usize, mut right: usize, idom: &[Option<usize>]| {
95        while left != right {
96            while rpo_number[left] > rpo_number[right] {
97                left = idom[left].expect("processed dominator must have an idom");
98            }
99            while rpo_number[right] > rpo_number[left] {
100                right = idom[right].expect("processed dominator must have an idom");
101            }
102        }
103        left
104    };
105
106    loop {
107        let mut changed = false;
108        for &block in postorder.iter().skip(1) {
109            let mut processed = predecessors[block]
110                .iter()
111                .copied()
112                .filter(|&predecessor| idom[predecessor].is_some());
113            let Some(first) = processed.next() else {
114                continue;
115            };
116            let next = processed.fold(first, |current, predecessor| {
117                intersect(current, predecessor, &idom)
118            });
119            if idom[block] != Some(next) {
120                idom[block] = Some(next);
121                changed = true;
122            }
123        }
124        if !changed {
125            break;
126        }
127    }
128
129    let mut discovery_position = vec![usize::MAX; ids.len()];
130    for (position, &block) in discovery_order.iter().enumerate() {
131        discovery_position[block] = position;
132    }
133
134    // A normal merge is best printed after all of its sibling arms: T, F, M.
135    // Loop headers also have multiple predecessors, but one of those edges is
136    // a backedge from a block they dominate; keep those headers at the front
137    // so a loop remains readable and reachable in the traversal.
138    let is_merge = (0..ids.len())
139        .map(|block| {
140            predecessors[block].len() > 1
141                && !predecessors[block]
142                    .iter()
143                    .copied()
144                    .any(|predecessor| dominates_index(block, predecessor, &idom))
145        })
146        .collect::<Vec<_>>();
147
148    let mut children = vec![Vec::new(); ids.len()];
149    for &block in &discovery_order {
150        if block == entry_index {
151            continue;
152        }
153        if let Some(parent) = idom[block] {
154            children[parent].push(block);
155        }
156    }
157    for siblings in &mut children {
158        siblings.sort_unstable_by_key(|&block| (is_merge[block], discovery_position[block]));
159    }
160
161    let mut order = Vec::with_capacity(ids.len());
162    let mut tree_stack = vec![entry_index];
163    while let Some(block) = tree_stack.pop() {
164        order.push(block);
165        tree_stack.extend(children[block].iter().rev().copied());
166    }
167
168    // A malformed or disconnected graph should still produce a total order.
169    order.extend(
170        ids.iter()
171            .enumerate()
172            .filter(|(index, _)| !discovered[*index])
173            .map(|(index, _)| index),
174    );
175    order.into_iter().map(|index| ids[index]).collect()
176}
177
178fn dominates_index(dominator: usize, mut block: usize, idom: &[Option<usize>]) -> bool {
179    loop {
180        if dominator == block {
181            return true;
182        }
183        let Some(parent) = idom[block] else {
184            return false;
185        };
186        if parent == block {
187            return false;
188        }
189        block = parent;
190    }
191}
192
193#[cfg(test)]
194mod tests {
195    use super::dominance_order;
196
197    #[test]
198    fn emits_dominator_tree_preorder_instead_of_numeric_order() {
199        let order = dominance_order(0, 0..6, |block| match block {
200            0 => vec![4, 1],
201            1 => vec![2],
202            2 => vec![3],
203            4 => vec![5],
204            _ => Vec::new(),
205        });
206
207        assert_eq!(order, vec![0, 4, 5, 1, 2, 3]);
208    }
209
210    #[test]
211    fn appends_unreachable_blocks_deterministically() {
212        let order = dominance_order(0, [4, 0, 3, 2, 1], |block| match block {
213            0 => vec![2],
214            _ => Vec::new(),
215        });
216
217        assert_eq!(order, vec![0, 2, 1, 3, 4]);
218    }
219
220    #[test]
221    fn keeps_a_join_in_dominator_tree_preorder() {
222        let order = dominance_order(0, 0..5, |block| match block {
223            0 => vec![3, 1],
224            1 => vec![2],
225            2 => vec![4],
226            3 => vec![2],
227            _ => Vec::new(),
228        });
229
230        assert_eq!(order, vec![0, 3, 1, 2, 4]);
231    }
232
233    #[test]
234    fn keeps_loop_headers_before_their_backedge_body() {
235        let order = dominance_order(0, 0..4, |block| match block {
236            0 => vec![1],
237            1 => vec![2, 3],
238            2 => vec![1],
239            _ => Vec::new(),
240        });
241
242        assert_eq!(order, vec![0, 1, 2, 3]);
243    }
244}