Skip to content

Commit c848860

Browse files
authored
Cranelift: Fix DFS iteration duplicate visitation bug (#11205)
When a single instruction could branch to the same successor multiple different ways (e.g. both sides of a `brif` go to the same block, or a block appears multiple times in a `br_table` or a `try_call`'s exception table) then our DFS traversal would mistakenly visit that block multiple times. This has largely not been an issue for analyses, just made them a tiny bit slower, but would be problematic for traversals that mutate the IR. Luckily the only traversal that is currently based on these DFS iteration helpers is the safepoints pass in `cranelift-frontend`, and that uses the post-order DFS iterator, which was not affected by this bug by chance. (My upcoming inliner, on the other hand, uses the pre-order DFS iterator and was affected by this bug.)
1 parent 10d3a22 commit c848860

File tree

1 file changed

+88
-27
lines changed

1 file changed

+88
-27
lines changed

cranelift/codegen/src/traversals.rs

Lines changed: 88 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -92,34 +92,41 @@ impl Iterator for DfsIter<'_> {
9292
type Item = (Event, ir::Block);
9393

9494
fn next(&mut self) -> Option<(Event, ir::Block)> {
95-
let (event, block) = self.dfs.stack.pop()?;
96-
97-
if event == Event::Enter && self.dfs.seen.insert(block) {
98-
self.dfs.stack.push((Event::Exit, block));
99-
self.dfs.stack.extend(
100-
self.func
101-
.block_successors(block)
102-
// Heuristic: chase the children in reverse. This puts
103-
// the first successor block first in the postorder, all
104-
// other things being equal, which tends to prioritize
105-
// loop backedges over out-edges, putting the edge-block
106-
// closer to the loop body and minimizing live-ranges in
107-
// linear instruction space. This heuristic doesn't have
108-
// any effect on the computation of dominators, and is
109-
// purely for other consumers of the postorder we cache
110-
// here.
111-
.rev()
112-
// This is purely an optimization to avoid additional
113-
// iterations of the loop, and is not required; it's
114-
// merely inlining the check from the outer conditional
115-
// of this case to avoid the extra loop iteration. This
116-
// also avoids potential excess stack growth.
117-
.filter(|block| !self.dfs.seen.contains(*block))
118-
.map(|block| (Event::Enter, block)),
119-
);
120-
}
95+
loop {
96+
let (event, block) = self.dfs.stack.pop()?;
97+
98+
if event == Event::Enter {
99+
let first_time_seeing = self.dfs.seen.insert(block);
100+
if !first_time_seeing {
101+
continue;
102+
}
103+
104+
self.dfs.stack.push((Event::Exit, block));
105+
self.dfs.stack.extend(
106+
self.func
107+
.block_successors(block)
108+
// Heuristic: chase the children in reverse. This puts
109+
// the first successor block first in the postorder, all
110+
// other things being equal, which tends to prioritize
111+
// loop backedges over out-edges, putting the edge-block
112+
// closer to the loop body and minimizing live-ranges in
113+
// linear instruction space. This heuristic doesn't have
114+
// any effect on the computation of dominators, and is
115+
// purely for other consumers of the postorder we cache
116+
// here.
117+
.rev()
118+
// This is purely an optimization to avoid additional
119+
// iterations of the loop, and is not required; it's
120+
// merely inlining the check from the outer conditional
121+
// of this case to avoid the extra loop iteration. This
122+
// also avoids potential excess stack growth.
123+
.filter(|block| !self.dfs.seen.contains(*block))
124+
.map(|block| (Event::Enter, block)),
125+
);
126+
}
121127

122-
Some((event, block))
128+
return Some((event, block));
129+
}
123130
}
124131
}
125132

@@ -215,4 +222,58 @@ mod tests {
215222
],
216223
);
217224
}
225+
226+
#[test]
227+
fn multiple_successors_to_the_same_block() {
228+
let _ = env_logger::try_init();
229+
230+
let mut func = Function::new();
231+
232+
let block0 = func.dfg.make_block();
233+
let block1 = func.dfg.make_block();
234+
235+
let mut cur = FuncCursor::new(&mut func);
236+
237+
// block0(v0):
238+
// v1 = iconst.i32 36
239+
// v2 = iconst.i32 42
240+
// br_if v0, block1(v1), block1(v2)
241+
cur.insert_block(block0);
242+
let v0 = cur.func.dfg.append_block_param(block0, I32);
243+
let v1 = cur.ins().iconst(ir::types::I32, 36);
244+
let v2 = cur.ins().iconst(ir::types::I32, 42);
245+
cur.ins()
246+
.brif(v0, block1, &[v1.into()], block1, &[v2.into()]);
247+
248+
// block1(v3: i32):
249+
// return v3
250+
cur.insert_block(block1);
251+
let v3 = cur.func.dfg.append_block_param(block1, I32);
252+
cur.ins().return_(&[v3]);
253+
254+
let mut dfs = Dfs::new();
255+
256+
// We should only enter `block1` once.
257+
assert_eq!(
258+
dfs.iter(&func).collect::<Vec<_>>(),
259+
vec![
260+
(Event::Enter, block0),
261+
(Event::Enter, block1),
262+
(Event::Exit, block1),
263+
(Event::Exit, block0),
264+
],
265+
);
266+
267+
// We should only iterate over `block1` once in a pre-order traversal.
268+
assert_eq!(
269+
dfs.pre_order_iter(&func).collect::<Vec<_>>(),
270+
vec![block0, block1],
271+
);
272+
273+
// We should only iterate over `block1` once in a post-order traversal.
274+
assert_eq!(
275+
dfs.post_order_iter(&func).collect::<Vec<_>>(),
276+
vec![block1, block0],
277+
);
278+
}
218279
}

0 commit comments

Comments
 (0)