charon_lib/transform/control_flow/ullbc_to_llbc.rs
1//! ULLBC to LLBC
2//!
3//! We reconstruct the control-flow in the Unstructured LLBC.
4//!
5//! The reconstruction algorithm is not written to be efficient (its complexity
6//! is probably very bad), but it was not written to be: this is still an early
7//! stage and we want the algorithm to generate the best reconstruction as
8//! possible. We still need to test the algorithm on more interesting examples,
9//! and will consider making it more efficient once it is a bit mature and well
10//! tested.
11//! Also note that we more importantly focus on making the algorithm sound: the
12//! reconstructed program must always be equivalent to the original MIR program,
13//! and the fact that the reconstruction preserves this property must be obvious.
14use itertools::Itertools;
15use petgraph::algo::dijkstra;
16use petgraph::algo::dominators::{Dominators, simple_fast};
17use petgraph::graphmap::DiGraphMap;
18use petgraph::visit::{
19 Dfs, DfsPostOrder, EdgeFiltered, EdgeRef, GraphRef, IntoNeighbors, VisitMap, Visitable, Walker,
20};
21use smallvec::SmallVec;
22use std::cmp::Reverse;
23use std::collections::{HashMap, HashSet};
24use std::mem;
25
26use crate::ids::IndexVec;
27use crate::llbc_ast::{self as tgt, StatementId};
28use crate::transform::TransformCtx;
29use crate::transform::ctx::TransformPass;
30use crate::ullbc_ast::{self as src, BlockId};
31use crate::utils::ensure_sufficient_stack;
32use crate::{ast::*, register_error};
33
34pub enum StackAction<N> {
35 PopPath,
36 Explore(N),
37}
38pub struct DfsWithPath<N, VM> {
39 /// The stack of nodes to visit
40 pub stack: Vec<StackAction<N>>,
41 /// The map of discovered nodes
42 pub discovered: VM,
43 /// The path from start node to current node.
44 pub path: Vec<N>,
45}
46impl<N, VM> DfsWithPath<N, VM>
47where
48 N: Copy + PartialEq,
49 VM: VisitMap<N>,
50{
51 /// Create a new **DfsWithPath**, using the graph's visitor map, and put **start** in the stack
52 /// of nodes to visit.
53 pub fn new<G>(graph: G, start: N) -> Self
54 where
55 G: GraphRef + Visitable<NodeId = N, Map = VM>,
56 {
57 Self {
58 stack: vec![StackAction::Explore(start)],
59 discovered: graph.visit_map(),
60 path: vec![],
61 }
62 }
63
64 /// Return the next node in the dfs, or **None** if the traversal is done.
65 pub fn next<G>(&mut self, graph: G) -> Option<N>
66 where
67 G: IntoNeighbors<NodeId = N>,
68 {
69 while let Some(action) = self.stack.pop() {
70 match action {
71 StackAction::Explore(node) => {
72 if self.discovered.visit(node) {
73 self.path.push(node);
74 self.stack.push(StackAction::PopPath);
75 for succ in graph.neighbors(node) {
76 if !self.discovered.is_visited(&succ) {
77 self.stack.push(StackAction::Explore(succ));
78 }
79 }
80 return Some(node);
81 }
82 }
83 StackAction::PopPath => {
84 self.path.pop();
85 }
86 }
87 }
88 None
89 }
90}
91
92/// Arbitrary-precision numbers
93type BigUint = fraction::DynaInt<u64, fraction::BigUint>;
94type BigRational = fraction::Ratio<BigUint>;
95
96/// Control-Flow Graph
97type Cfg = DiGraphMap<src::BlockId, ()>;
98
99/// Information precomputed about a function's CFG.
100#[derive(Debug)]
101struct CfgInfo {
102 /// The CFG
103 pub cfg: Cfg,
104 /// The CFG where all the backward edges have been removed. Aka "forward CFG".
105 pub fwd_cfg: Cfg,
106 /// We consider the destination of the backward edges to be loop entries and
107 /// store them here.
108 pub loop_entries: HashSet<src::BlockId>,
109 /// The blocks whose terminators are a switch are stored here.
110 pub switch_blocks: HashSet<src::BlockId>,
111 /// Tree of which nodes dominates which other nodes.
112 #[expect(unused)]
113 pub dominator_tree: Dominators<BlockId>,
114 /// Computed data about each block.
115 pub block_data: IndexVec<BlockId, Box<BlockData>>,
116}
117
118#[derive(Debug)]
119struct BlockData {
120 pub id: BlockId,
121 pub span: Span,
122 /// The (unique) entrypoints of each loop. Unique because we error on irreducible cfgs.
123 pub is_loop_header: bool,
124 /// Whether this block is a switch.
125 pub is_switch: bool,
126 /// Whether this block is only reachable by taking an unwind edge.
127 pub is_unwind: bool,
128 /// Whether this block has multiple incoming control-flow edges in the forward graph.
129 pub is_merge_target: bool,
130 /// Order in a reverse postorder numbering. `None` if the block is unreachable.
131 pub reverse_postorder: Option<u32>,
132 /// Nodes that this block immediately dominates. Sorted by reverse_postorder_id, with largest
133 /// id first.
134 pub immediately_dominates: SmallVec<[BlockId; 2]>,
135 /// The nodes from `immediately_dominates` that are also merge targets. Sorted in the same
136 /// order.
137 pub immediately_dominated_merge_targets: SmallVec<[BlockId; 2]>,
138 /// List of loops inside of which this node is (loops are identified by their header). A node
139 /// is considered inside a loop if it is reachable from the loop header and if it can reach the
140 /// loop header using only the backwards edges into it (i.e. we don't count a path that enters
141 /// the loop header through a forward edge).
142 ///
143 /// Note that we might have to take a backward edge to reach the loop header, e.g.:
144 /// 'a: loop {
145 /// // ...
146 /// 'b: loop {
147 /// // ...
148 /// if true {
149 /// continue 'a;
150 /// } else {
151 /// if true {
152 /// break 'a;
153 /// }
154 /// // This node has to take two backward edges in order to reach the start of `'a`.
155 /// }
156 /// }
157 /// }
158 ///
159 /// The restriction on backwards edges is for the following case:
160 /// loop {
161 /// loop {
162 /// ..
163 /// }
164 /// // Not in inner loop
165 /// }
166 ///
167 /// This is sorted by path order from the graph root.
168 pub within_loops: SmallVec<[BlockId; 2]>,
169 /// Node from where we can only reach error nodes (panic, etc.)
170 // TODO: track more nicely the set of targets reachable from a node: panic, return, exit loop,
171 // continue loop (this is a partial order).
172 pub only_reach_error: bool,
173 /// List of reachable nodes, with the length of shortest path to them. Includes the current
174 /// node.
175 pub shortest_paths: hashbrown::HashMap<BlockId, usize>,
176 /// Let's say we put a quantity of water equal to 1 on the block, and the water flows downards.
177 /// Whenever there is a branching, the quantity of water gets equally divided between the
178 /// branches. When the control flows join, we put the water back together. The set below
179 /// computes the amount of water received by each descendant of the node.
180 ///
181 /// TODO: there must be a known algorithm which computes this, right?...
182 /// This is exactly this problems:
183 /// <https://stackoverflow.com/questions/78221666/algorithm-for-total-flow-through-weighted-directed-acyclic-graph>
184 /// TODO: the way I compute this is not efficient.
185 pub flow: IndexVec<BlockId, BigRational>,
186 /// Reconstructed information about loops and switches.
187 pub exit_info: ExitInfo,
188}
189
190#[derive(Debug, Default, Clone)]
191struct ExitInfo {
192 /// The loop exit
193 loop_exit: Option<src::BlockId>,
194 /// The switch exit.
195 switch_exit: Option<src::BlockId>,
196}
197
198/// Error indicating that the control-flow graph is not reducible. The contained block id is a
199/// block involved in an irreducible subgraph.
200struct Irreducible(BlockId);
201
202impl CfgInfo {
203 /// Build the CFGs (the "regular" CFG and the CFG without backward edges) and precompute a
204 /// bunch of graph information about the CFG.
205 fn build(ctx: &TransformCtx, body: &src::BodyContents) -> Result<Self, Irreducible> {
206 // The steps in this function follow a precise order, as each step typically requires the
207 // previous one.
208 let start_block = BlockId::ZERO;
209
210 let empty_flow = body.map_ref(|_| BigRational::new(0u64.into(), 1u64.into()));
211 let mut block_data: IndexVec<BlockId, _> = body.map_ref_indexed(|id, contents| {
212 Box::new(BlockData {
213 id,
214 span: contents.terminator.span,
215 is_loop_header: false,
216 is_switch: false,
217 is_unwind: false,
218 is_merge_target: false,
219 reverse_postorder: None,
220 immediately_dominates: Default::default(),
221 immediately_dominated_merge_targets: Default::default(),
222 within_loops: Default::default(),
223 only_reach_error: false,
224 shortest_paths: Default::default(),
225 flow: empty_flow.clone(),
226 exit_info: Default::default(),
227 })
228 });
229
230 // Build the node graph.
231 let mut cfg = Cfg::new();
232 let mut cfg_without_unwind = Cfg::new();
233 for (block_id, block) in body.iter_enumerated() {
234 cfg.add_node(block_id);
235 cfg_without_unwind.add_node(block_id);
236 for tgt in block.targets() {
237 cfg.add_edge(block_id, tgt, ());
238 }
239 for tgt in block.targets_ignoring_unwind() {
240 cfg_without_unwind.add_edge(block_id, tgt, ());
241 }
242 }
243
244 // Compute the dominator tree.
245 let dominator_tree = simple_fast(&cfg, start_block);
246
247 // Compute reverse postorder numbering.
248 for (i, block_id) in DfsPostOrder::new(&cfg, start_block).iter(&cfg).enumerate() {
249 let rev_post_id = body.len() - i;
250 block_data[block_id].reverse_postorder = Some(rev_post_id.try_into().unwrap());
251
252 // Store the dominator tree in `block_data`.
253 if let Some(dominator) = dominator_tree.immediate_dominator(block_id) {
254 block_data[dominator].immediately_dominates.push(block_id);
255 }
256 }
257
258 // Compute the forward graph (without backward edges).
259 let mut fwd_cfg = Cfg::new();
260 let mut loop_entries = HashSet::new();
261 let mut switch_blocks = HashSet::new();
262 for block_id in Dfs::new(&cfg, start_block).iter(&cfg) {
263 fwd_cfg.add_node(block_id);
264
265 if body[block_id].terminator.kind.is_switch() {
266 switch_blocks.insert(block_id);
267 block_data[block_id].is_switch = true;
268 }
269
270 // Iterate over edges into this node (so that we can determine whether this node is a
271 // loop header).
272 let mut incoming_fwd_edges = 0;
273 for from in cfg.neighbors_directed(block_id, petgraph::Direction::Incoming) {
274 // Check if the edge is a backward edge.
275 if block_data[from].reverse_postorder >= block_data[block_id].reverse_postorder {
276 // This is a backward edge
277 block_data[block_id].is_loop_header = true;
278 loop_entries.insert(block_id);
279 // A cfg is reducible iff the target of every back edge dominates the
280 // edge's source.
281 if !dominator_tree.dominators(from).unwrap().contains(&block_id) {
282 return Err(Irreducible(from));
283 }
284 } else {
285 incoming_fwd_edges += 1;
286 fwd_cfg.add_edge(from, block_id, ());
287 }
288 }
289
290 // Detect merge targets.
291 if incoming_fwd_edges >= 2 {
292 block_data[block_id].is_merge_target = true;
293 }
294 }
295
296 let reachable_without_unwind: HashSet<BlockId> = Dfs::new(&cfg_without_unwind, start_block)
297 .iter(&cfg_without_unwind)
298 .collect();
299
300 // Finish filling in information.
301 for block_id in DfsPostOrder::new(&fwd_cfg, start_block).iter(&fwd_cfg) {
302 let block = &body[block_id];
303 let targets = cfg.neighbors(block_id).collect_vec();
304 let fwd_targets = fwd_cfg.neighbors(block_id).collect_vec();
305
306 // Compute the nodes that are part of unwind paths.
307 block_data[block_id].is_unwind = !reachable_without_unwind.contains(&block_id);
308
309 // Compute the nodes that can only reach error nodes.
310 // The node can only reach error nodes if:
311 // - it is an error node;
312 // - or it has neighbors and they all lead to errors.
313 // Note that if there is a backward edge, `only_reach_error` cannot contain this
314 // node yet. In other words, this does not consider infinite loops as reaching an
315 // error node.
316 if block.terminator.is_error()
317 || (!targets.is_empty()
318 && targets.iter().all(|&tgt| block_data[tgt].only_reach_error))
319 {
320 block_data[block_id].only_reach_error = true;
321 }
322
323 // Compute the flows between each pair of nodes.
324 let mut flow: IndexVec<src::BlockId, BigRational> =
325 mem::take(&mut block_data[block_id].flow);
326 // The flow to self is 1.
327 flow[block_id] = BigRational::new(1u64.into(), 1u64.into());
328 // If a block has both regular and unwind targets, don't let the unwind path dilute
329 // the normal-control-flow heuristic used to pick switch exits. Unwind-only subgraphs
330 // still flow through their own targets.
331 let mut flow_targets = fwd_targets
332 .iter()
333 .copied()
334 .filter(|&child| !block_data[child].is_unwind)
335 .collect_vec();
336 if flow_targets.is_empty() {
337 flow_targets = fwd_targets;
338 }
339 // Divide the flow from each child to a given target block by the number of children.
340 // This is a sparse matrix multiplication and could be implemented using a linalg
341 // library.
342 let num_children: BigUint = flow_targets.len().into();
343 for child in flow_targets {
344 for grandchild in block_data[child].reachable_including_self() {
345 // Flow from `child` to `grandchild`
346 flow[grandchild] += &block_data[child].flow[grandchild] / &num_children;
347 }
348 }
349 block_data[block_id].flow = flow;
350
351 // Compute shortest paths to all reachable nodes in the forward graph.
352 block_data[block_id].shortest_paths = dijkstra(&fwd_cfg, block_id, None, |_| 1usize);
353
354 // Fill in the rest of the domination data.
355 let mut dominatees = mem::take(&mut block_data[block_id].immediately_dominates);
356 dominatees.sort_by_key(|&child| block_data[child].reverse_postorder);
357 dominatees.reverse();
358 block_data[block_id].immediately_dominates = dominatees;
359 block_data[block_id].immediately_dominated_merge_targets = block_data[block_id]
360 .immediately_dominates
361 .iter()
362 .copied()
363 .filter(|&child| block_data[child].is_merge_target)
364 .collect();
365 }
366
367 // Fill in the within_loop information. See the docs of `within_loops` to understand what
368 // we're computing.
369 let mut path_dfs = DfsWithPath::new(&cfg, start_block);
370 while let Some(block_id) = path_dfs.next(&cfg) {
371 // Store all the loops on the path to this
372 // node.
373 let mut within_loops: SmallVec<_> = path_dfs
374 .path
375 .iter()
376 .copied()
377 .filter(|&loop_id| block_data[loop_id].is_loop_header)
378 .collect();
379 // The loops that we can reach by taking a single backward edge.
380 let loops_directly_within = within_loops
381 .iter()
382 .copied()
383 .filter(|&loop_header| {
384 cfg.neighbors_directed(loop_header, petgraph::Direction::Incoming)
385 .any(|bid| block_data[block_id].shortest_paths.contains_key(&bid))
386 })
387 .collect_vec();
388 // The loops that we can reach by taking any number of backward edges.
389 let loops_indirectly_within: HashSet<_> = loops_directly_within
390 .iter()
391 .copied()
392 .flat_map(|loop_header| &block_data[loop_header].within_loops)
393 .chain(&loops_directly_within)
394 .copied()
395 .collect();
396 within_loops.retain(|id| loops_indirectly_within.contains(id));
397 block_data[block_id].within_loops = within_loops;
398 }
399
400 let mut cfg = CfgInfo {
401 cfg,
402 fwd_cfg,
403 loop_entries,
404 switch_blocks,
405 dominator_tree,
406 block_data,
407 };
408
409 // Pick an exit block for each loop, if we find one.
410 ExitInfo::compute_loop_exits(ctx, &mut cfg);
411
412 // Pick an exit block for each switch, if we find one.
413 ExitInfo::compute_switch_exits(&mut cfg);
414
415 Ok(cfg)
416 }
417
418 fn block_data(&self, block_id: BlockId) -> &BlockData {
419 &self.block_data[block_id]
420 }
421 // fn can_reach(&self, src: BlockId, tgt: BlockId) -> bool {
422 // self.block_data[src].shortest_paths.contains_key(&tgt)
423 // }
424 fn topo_rank(&self, block_id: BlockId) -> u32 {
425 self.block_data[block_id].reverse_postorder.unwrap()
426 }
427 #[expect(unused)]
428 fn is_backward_edge(&self, src: BlockId, tgt: BlockId) -> bool {
429 self.block_data[src].reverse_postorder >= self.block_data[tgt].reverse_postorder
430 && self.cfg.contains_edge(src, tgt)
431 }
432
433 /// Check if the node is within the given loop.
434 fn is_within_loop(&self, loop_header: src::BlockId, block_id: src::BlockId) -> bool {
435 self.block_data[block_id]
436 .within_loops
437 .contains(&loop_header)
438 }
439
440 /// Check if all paths from `src` to nodes in `target_set` go through `through_node`. If `src`
441 /// is already in `target_set`, we ignore that empty path.
442 fn all_paths_go_through(
443 &self,
444 src: src::BlockId,
445 through_node: src::BlockId,
446 target_set: &HashSet<src::BlockId>,
447 ) -> bool {
448 let graph = EdgeFiltered::from_fn(&self.fwd_cfg, |edge| edge.source() != through_node);
449 !Dfs::new(&graph, src)
450 .iter(&graph)
451 .skip(1) // skip src
452 .any(|bid| target_set.contains(&bid))
453 }
454}
455
456impl BlockData {
457 fn shortest_paths_including_self(&self) -> impl Iterator<Item = (BlockId, usize)> {
458 self.shortest_paths.iter().map(|(bid, d)| (*bid, *d))
459 }
460 fn shortest_paths_excluding_self(&self) -> impl Iterator<Item = (BlockId, usize)> {
461 self.shortest_paths_including_self()
462 .filter(move |&(bid, _)| bid != self.id)
463 }
464 fn reachable_including_self(&self) -> impl Iterator<Item = BlockId> {
465 self.shortest_paths_including_self().map(|(bid, _)| bid)
466 }
467 fn reachable_excluding_self(&self) -> impl Iterator<Item = BlockId> {
468 self.shortest_paths_excluding_self().map(|(bid, _)| bid)
469 }
470 #[expect(unused)]
471 fn can_reach_excluding_self(&self, other: BlockId) -> bool {
472 self.shortest_paths.contains_key(&other) && self.id != other
473 }
474}
475
476/// See [`ExitInfo::compute_loop_exit_ranks`].
477#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
478struct LoopExitRank {
479 /// Prefer regular blocks over blocks reachable only through unwind edges.
480 is_unwind_block: Reverse<bool>,
481 /// Number of paths we found going to this exit.
482 path_count: usize,
483 /// Distance from the loop header.
484 distance_from_header: Reverse<usize>,
485}
486
487impl ExitInfo {
488 /// Compute the first node on each path that exits the loop.
489 fn compute_loop_exit_starting_points(
490 cfg: &CfgInfo,
491 loop_header: src::BlockId,
492 ) -> Vec<src::BlockId> {
493 let mut loop_exits = Vec::new();
494 // Do a dfs from the loop header while keeping track of the path from the loop header to
495 // the current node.
496 let mut dfs = Dfs::new(&cfg.fwd_cfg, loop_header);
497 while let Some(block_id) = dfs.next(&cfg.fwd_cfg) {
498 // If we've exited all the loops after and including the target one, this node is an
499 // exit node for the target loop.
500 if !cfg.is_within_loop(loop_header, block_id) {
501 loop_exits.push(block_id);
502 // Don't explore any more paths from this node.
503 dfs.discovered.extend(cfg.fwd_cfg.neighbors(block_id));
504 }
505 }
506 loop_exits
507 }
508
509 /// Compute the loop exit candidates along with a rank.
510 ///
511 /// In the simple case, there is one exit node through which all exit paths go. We want to be
512 /// sure to catch that case, and when that's not possible we want to still find a node through
513 /// which a lot of exit paths go.
514 ///
515 /// To do that, we first count for each exit node how many exit paths go through it, and pick
516 /// the node with most occurrences. If there are many such nodes, we pick the one with shortest
517 /// distance from the loop header. Finally if there are still many such nodes, we keep the
518 /// first node found (the order in which we explore the graph is deterministic, and we use an
519 /// insertion-order hash map).
520 ///
521 /// Note that exit candidates will typically be referenced more than once for one loop. This
522 /// comes from the fact that whenever we reach a node outside the current loop, we register
523 /// this node as well as all its children as exit candidates.
524 /// Consider the following example:
525 /// ```text
526 /// while i < max {
527 /// if cond {
528 /// break;
529 /// }
530 /// s += i;
531 /// i += 1
532 /// }
533 /// // All the below nodes are exit candidates (each of them is referenced twice)
534 /// s += 1;
535 /// return s;
536 /// ```
537 fn compute_loop_exit_ranks(
538 cfg: &CfgInfo,
539 loop_header: src::BlockId,
540 ) -> SeqHashMap<src::BlockId, LoopExitRank> {
541 let mut loop_exits: SeqHashMap<BlockId, LoopExitRank> = SeqHashMap::new();
542 for block_id in Self::compute_loop_exit_starting_points(cfg, loop_header) {
543 for bid in cfg.block_data(block_id).reachable_including_self() {
544 loop_exits
545 .entry(bid)
546 .or_insert_with(|| LoopExitRank {
547 is_unwind_block: Reverse(cfg.block_data[bid].is_unwind),
548 path_count: 0,
549 distance_from_header: Reverse(
550 cfg.block_data[loop_header].shortest_paths[&bid],
551 ),
552 })
553 .path_count += 1;
554 }
555 }
556 loop_exits
557 }
558
559 /// A loop exit is any block reachable from the loop header that isn't inside the loop.
560 /// This function choses an exit for every loop. See `compute_loop_exit_ranks` for how we
561 /// select them.
562 ///
563 /// For example:
564 /// ```text
565 /// while ... {
566 /// ...
567 /// if ... {
568 /// // We can't reach the loop entry from here: this is an exit
569 /// // candidate
570 /// return;
571 /// }
572 /// }
573 /// // This is another exit candidate - and this is the one we want to use
574 /// // as the "real" exit...
575 /// ...
576 /// ```
577 ///
578 /// Once we listed all the exit candidates, we find the "best" one for every loop. The best
579 /// exit is the following one:
580 /// - it is the one which is used the most times (note that there can be
581 /// several candidates which are referenced strictly more than once: see the
582 /// comment below)
583 /// - if several exits have the same number of occurrences, we choose the one
584 /// for which we goto the "earliest" (earliest meaning that the goto is close to
585 /// the loop entry node in the AST). The reason is that all the loops should
586 /// have an outer if ... then ... else ... which executes the loop body or goes
587 /// to the exit (note that this is not necessarily the first
588 /// if ... then ... else ... we find: loop conditions can be arbitrary
589 /// expressions, containing branchings).
590 ///
591 /// # Several candidates for a loop exit:
592 /// =====================================
593 /// There used to be a sanity check to ensure there are no two different
594 /// candidates with exactly the same number of occurrences and distance from
595 /// the entry of the loop, if the number of occurrences is > 1.
596 ///
597 /// We removed it because it does happen, for instance here (the match
598 /// introduces an `unreachable` node, and it has the same number of
599 /// occurrences and the same distance to the loop entry as the `panic`
600 /// node):
601 ///
602 /// ```text
603 /// pub fn list_nth_mut_loop_pair<'a, T>(
604 /// mut ls: &'a mut List<T>,
605 /// mut i: u32,
606 /// ) -> &'a mut T {
607 /// loop {
608 /// match ls {
609 /// List::Nil => {
610 /// panic!() // <-- best candidate
611 /// }
612 /// List::Cons(x, tl) => {
613 /// if i == 0 {
614 /// return x;
615 /// } else {
616 /// ls = tl;
617 /// i -= 1;
618 /// }
619 /// }
620 /// _ => {
621 /// // Note that Rustc always introduces an unreachable branch after
622 /// // desugaring matches.
623 /// unreachable!(), // <-- best candidate
624 /// }
625 /// }
626 /// }
627 /// }
628 /// ```
629 ///
630 /// When this happens we choose an exit candidate whose edges don't necessarily
631 /// lead to an error (above there are none, so we don't choose any exits). Note
632 /// that this last condition is important to prevent loops from being unnecessarily
633 /// nested:
634 ///
635 /// ```text
636 /// pub fn nested_loops_enum(step_out: usize, step_in: usize) -> usize {
637 /// let mut s = 0;
638 ///
639 /// for _ in 0..128 { // We don't want this loop to be nested with the loops below
640 /// s += 1;
641 /// }
642 ///
643 /// for _ in 0..(step_out) {
644 /// for _ in 0..(step_in) {
645 /// s += 1;
646 /// }
647 /// }
648 ///
649 /// s
650 /// }
651 /// ```
652 fn compute_loop_exits(_ctx: &TransformCtx, cfg: &mut CfgInfo) {
653 for &loop_id in &cfg.loop_entries {
654 // Compute the candidates.
655 let loop_exits: SeqHashMap<BlockId, LoopExitRank> =
656 Self::compute_loop_exit_ranks(cfg, loop_id);
657 // We choose the exit with:
658 // - the most occurrences
659 // - the least total distance (if there are several possibilities)
660 // - doesn't necessarily lead to an error (panic, unreachable)
661 let best_exits: Vec<(BlockId, LoopExitRank)> =
662 loop_exits.into_iter().max_set_by_key(|&(_, rank)| rank);
663 // If there is exactly one best candidate, use it. Otherwise we need to split further.
664 let chosen_exit = match best_exits.into_iter().map(|(bid, _)| bid).exactly_one() {
665 Ok(best_exit) => Some(best_exit),
666 Err(best_exits) => {
667 // Remove the candidates which only lead to errors (panic or unreachable).
668 // If there is exactly one candidate we select it, otherwise we do not select any
669 // exit.
670 // We don't want to select any exit if we are in the below situation
671 // (all paths lead to errors). We added a sanity check below to
672 // catch the situations where there are several exits which don't
673 // lead to errors.
674 //
675 // Example:
676 // ========
677 // ```
678 // loop {
679 // match ls {
680 // List::Nil => {
681 // panic!() // <-- best candidate
682 // }
683 // List::Cons(x, tl) => {
684 // if i == 0 {
685 // return x;
686 // } else {
687 // ls = tl;
688 // i -= 1;
689 // }
690 // }
691 // _ => {
692 // unreachable!(); // <-- best candidate (Rustc introduces an `unreachable` case)
693 // }
694 // }
695 // }
696 // ```
697 // The `exactly_one` can fail, see `tests/ui/control-flow/ambiguous-loop-exit.rs`
698 best_exits
699 .filter(|&bid| !cfg.block_data[bid].only_reach_error)
700 .exactly_one()
701 .ok()
702 }
703 };
704 cfg.block_data[loop_id].exit_info.loop_exit = chosen_exit;
705 }
706 }
707
708 /// Let's consider the following piece of code:
709 /// ```text
710 /// if cond1 { ... } else { ... };
711 /// if cond2 { ... } else { ... };
712 /// ```
713 /// Once converted to MIR, the control-flow is destructured, which means we
714 /// have gotos everywhere. When reconstructing the control-flow, we have
715 /// to be careful about the point where we should join the two branches of
716 /// the first if.
717 /// For instance, if we don't notice they should be joined at some point (i.e,
718 /// whatever the branch we take, there is a moment when we go to the exact
719 /// same place, just before the second if), we might generate code like
720 /// this, with some duplicata:
721 /// ```text
722 /// if cond1 { ...; if cond2 { ... } else { ...} }
723 /// else { ...; if cond2 { ... } else { ...} }
724 /// ```
725 ///
726 /// Such a reconstructed program is valid, but it is definitely non-optimal:
727 /// it is very different from the original program (making it less clean and
728 /// clear), more bloated, and might involve duplicating the proof effort.
729 ///
730 /// For this reason, we need to find the "exit" of the first switch, which is
731 /// the point where the two branches join. Note that this can be a bit tricky,
732 /// because there may be more than two branches (if we do `switch(x) { ... }`),
733 /// and some of them might not join (if they contain a `break`, `panic`,
734 /// `return`, etc.).
735 ///
736 /// In order to compute the switch exits, we simply recursively compute a
737 /// topologically ordered set of "filtered successors" as follows (note
738 /// that we work in the CFG *without* back edges):
739 /// - for a block which doesn't branch (only one successor), the filtered
740 /// successors is the set of reachable nodes.
741 /// - for a block which branches, we compute the nodes reachable from all
742 /// the children, and find the "best" intersection between those.
743 /// Note that we find the "best" intersection (a pair of branches which
744 /// maximize the intersection of filtered successors) because some branches
745 /// might never join the control-flow of the other branches, if they contain
746 /// a `break`, `return`, `panic`, etc., like here:
747 /// ```text
748 /// if b { x = 3; } { return; }
749 /// y += x;
750 /// ...
751 /// ```
752 /// Note that with nested switches, the branches of the inner switches might
753 /// goto the exits of the outer switches: for this reason, we give precedence
754 /// to the outer switches.
755 fn compute_switch_exits(cfg: &mut CfgInfo) {
756 // We need to give precedence to the outer switches: we thus iterate
757 // over the switch blocks in topological order.
758 let mut exits_set = HashSet::new();
759 for bid in cfg
760 .switch_blocks
761 .iter()
762 .copied()
763 .sorted_unstable_by_key(|&bid| (cfg.topo_rank(bid), bid))
764 {
765 let block_data = &cfg.block_data[bid];
766 // Find the best successor: this is the node with the highest flow, and the lowest
767 // topological rank. If several nodes have the same flow, we want to take the highest
768 // one in the hierarchy: hence the use of the topological rank.
769 //
770 // Ex.:
771 // ```text
772 // A -- we start here
773 // |
774 // |---------------------------------------
775 // | | | |
776 // B:(0.25,-1) C:(0.25,-2) D:(0.25,-3) E:(0.25,-4)
777 // | | |
778 // |--------------------------
779 // |
780 // F:(0.75,-5)
781 // |
782 // |
783 // G:(0.75,-6)
784 // ```
785 // The "best" node (with the highest (flow, rank) in the graph above is F.
786 // If the switch is inside a loop, we also only consider exists that are inside that
787 // same loop. There must be one, otherwise the switch entry would not be inside the
788 // loop.
789 let current_loop = block_data.within_loops.last().copied();
790 let best_exit: Option<BlockId> = block_data
791 .reachable_excluding_self()
792 .filter(|&b| {
793 current_loop.is_none_or(|current_loop| cfg.is_within_loop(current_loop, b))
794 })
795 .max_by_key(|&id| {
796 let is_unwind_block = Reverse(cfg.block_data[id].is_unwind);
797 let flow = &block_data.flow[id];
798 let rank = Reverse(cfg.topo_rank(id));
799 ((is_unwind_block, flow, rank), id)
800 });
801 // We have an exit candidate: we first check that it was not already taken by an
802 // external switch.
803 //
804 // We then check that we can't reach the exit of an external switch from one of the
805 // branches, without going through the exit candidate. We do this by simply checking
806 // that we can't reach any of the exits of outer switches.
807 //
808 // The reason is that it can lead to code like the following:
809 // ```
810 // if ... { // if #1
811 // if ... { // if #2
812 // ...
813 // // here, we have a `goto b1`, where b1 is the exit
814 // // of if #2: we thus stop translating the blocks.
815 // }
816 // else {
817 // ...
818 // // here, we have a `goto b2`, where b2 is the exit
819 // // of if #1: we thus stop translating the blocks.
820 // }
821 // // We insert code for the block b1 here (which is the exit of
822 // // the exit of if #2). However, this block should only
823 // // be executed in the branch "then" of the if #2, not in
824 // // the branch "else".
825 // ...
826 // }
827 // else {
828 // ...
829 // }
830 // ```
831 if let Some(exit_id) = best_exit
832 && !exits_set.contains(&exit_id)
833 && cfg.all_paths_go_through(bid, exit_id, &exits_set)
834 {
835 exits_set.insert(exit_id);
836 cfg.block_data[bid].exit_info.switch_exit = Some(exit_id);
837 }
838 }
839 }
840}
841
842/// Iter over the last non-switch statements that may be executed on any branch of this block.
843/// Skips over `Nop`s.
844fn iter_tail_statements(block: &mut tgt::Block, f: &mut impl FnMut(&mut tgt::Statement)) {
845 let Some(st) = block
846 .statements
847 .iter_mut()
848 .rev()
849 .find(|st| !st.kind.is_nop())
850 else {
851 return;
852 };
853 if let tgt::StatementKind::Switch(switch) = &mut st.kind {
854 for block in switch.iter_targets_mut() {
855 iter_tail_statements(block, f);
856 }
857 } else {
858 f(st)
859 };
860}
861
862type Depth = usize;
863
864#[derive(Debug, Clone, Copy)]
865enum SpecialJumpKind {
866 /// This block can be reached by a `continue` to the given depth.
867 LoopContinue(Depth),
868 /// This block can be reached by a `break` to the given depth. This comes from a loop.
869 LoopBreak(Depth),
870 /// This block can be reached by a `break` to the given depth. This is a `loop` context
871 /// introduced only for forward jumps.
872 ForwardBreak(Depth),
873 /// This block can be reached by doing nothing as this is the next block that will be
874 /// translated. Only applies if this block is at the top of the stack.
875 NextBlock,
876}
877
878#[derive(Clone, Copy)]
879struct SpecialJump {
880 /// The relevant block.
881 target_block: BlockId,
882 /// How to translate a jump to the target block.
883 kind: SpecialJumpKind,
884}
885
886impl SpecialJump {
887 fn new(target_block: BlockId, kind: SpecialJumpKind) -> Self {
888 Self { target_block, kind }
889 }
890}
891
892impl std::fmt::Debug for SpecialJump {
893 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
894 write!(f, "SpecialJump({}, {:?})", self.target_block, self.kind)
895 }
896}
897
898/// How to handle blocks reachable from multiple branches.
899enum ReconstructMode {
900 /// Duplicate blocks reachable from multiple branches.
901 Duplicate,
902 /// Insert loops for the purpose of breaking forward to them to implement DAG-like control-flow
903 /// without duplicating blocks.
904 /// Based on the algorithm from "Beyond Relooper" (https://dl.acm.org/doi/10.1145/3547621).
905 ForwardBreak,
906}
907
908struct ReconstructCtx<'a> {
909 cfg: CfgInfo,
910 body: &'a src::ExprBody,
911 /// The depth of `loop` contexts we may `break`/`continue` to.
912 break_context_depth: Depth,
913 /// Stack of block ids that should be translated to special jumps (`break`/`continue`/do
914 /// nothing) in the current context.
915 /// The block where control-flow continues naturally after this block is kept at the top of the
916 /// stack.
917 special_jump_stack: Vec<SpecialJump>,
918 mode: ReconstructMode,
919}
920
921impl<'a> ReconstructCtx<'a> {
922 fn build(ctx: &TransformCtx, src_body: &'a src::ExprBody) -> Result<Self, Irreducible> {
923 // Compute all sorts of graph-related information about the control-flow graph, including
924 // reachability, the dominator tree, loop entries, and loop/switch exits.
925 let cfg = CfgInfo::build(ctx, &src_body.body)?;
926
927 // Translate the body by reconstructing the loops and the
928 // conditional branchings.
929 let allow_duplication = true;
930 Ok(ReconstructCtx {
931 cfg,
932 body: src_body,
933 break_context_depth: 0,
934 special_jump_stack: Vec::new(),
935 mode: if allow_duplication {
936 ReconstructMode::Duplicate
937 } else {
938 ReconstructMode::ForwardBreak
939 },
940 })
941 }
942
943 fn translate_statement(&self, st: &src::Statement) -> tgt::Statement {
944 let span = st.span;
945 let st = match st.kind.clone() {
946 src::StatementKind::Assign(place, rvalue) => tgt::StatementKind::Assign(place, rvalue),
947 src::StatementKind::SetDiscriminant(place, variant_id) => {
948 tgt::StatementKind::SetDiscriminant(place, variant_id)
949 }
950 src::StatementKind::StorageLive(var_id) => tgt::StatementKind::StorageLive(var_id),
951 src::StatementKind::StorageDead(var_id) => tgt::StatementKind::StorageDead(var_id),
952 src::StatementKind::Assert { assert, on_failure } => tgt::StatementKind::Assert {
953 assert,
954 on_failure: on_failure.clone(),
955 on_unwind: tgt::Statement::new(span, tgt::StatementKind::UnwindResume).into_block(),
956 },
957 src::StatementKind::PlaceMention(place) => tgt::StatementKind::PlaceMention(place),
958 src::StatementKind::Borrowck(statement) => tgt::StatementKind::Borrowck(statement),
959 src::StatementKind::Nop => tgt::StatementKind::Nop,
960 };
961 tgt::Statement::new(span, st)
962 }
963
964 /// Translate a jump to the given block. The span is used to create the jump statement, if any.
965 #[tracing::instrument(skip(self), ret, fields(stack = ?self.special_jump_stack))]
966 fn translate_jump(&mut self, span: Span, target_block: src::BlockId) -> tgt::Block {
967 match self
968 .special_jump_stack
969 .iter()
970 .rev()
971 .enumerate()
972 .find(|(_, j)| j.target_block == target_block)
973 {
974 Some((i, jump_target)) => {
975 let mk_block = |kind| tgt::Statement::new(span, kind).into_block();
976 match jump_target.kind {
977 // The top of the stack is where control-flow goes naturally, no need to add a
978 // `break`/`continue`. We only do that in `ForwardBreak` mode to avoid breaking aeneas.
979 SpecialJumpKind::LoopContinue(_)
980 | SpecialJumpKind::ForwardBreak(_)
981 | SpecialJumpKind::NextBlock
982 if i == 0 && matches!(self.mode, ReconstructMode::ForwardBreak) =>
983 {
984 mk_block(tgt::StatementKind::Nop)
985 }
986 SpecialJumpKind::LoopContinue(depth) => mk_block(tgt::StatementKind::Continue(
987 self.break_context_depth - depth,
988 )),
989 SpecialJumpKind::ForwardBreak(depth) | SpecialJumpKind::LoopBreak(depth) => {
990 mk_block(tgt::StatementKind::Break(self.break_context_depth - depth))
991 }
992 SpecialJumpKind::NextBlock if i == 0 => mk_block(tgt::StatementKind::Nop),
993 // Translate the block without a jump.
994 SpecialJumpKind::NextBlock => self.translate_block(target_block),
995 }
996 }
997 // Translate the block without a jump.
998 None => self.translate_block(target_block),
999 }
1000 }
1001
1002 fn translate_terminator(&mut self, terminator: &src::Terminator) -> tgt::Block {
1003 let src_span = terminator.span;
1004
1005 match &terminator.kind {
1006 src::TerminatorKind::Abort(kind) => {
1007 tgt::Statement::new(src_span, tgt::StatementKind::Abort(kind.clone())).into_block()
1008 }
1009 src::TerminatorKind::Return => {
1010 tgt::Statement::new(src_span, tgt::StatementKind::Return).into_block()
1011 }
1012 src::TerminatorKind::UnwindResume => {
1013 tgt::Statement::new(src_span, tgt::StatementKind::UnwindResume).into_block()
1014 }
1015 src::TerminatorKind::Call {
1016 call,
1017 target,
1018 on_unwind,
1019 } => {
1020 let on_unwind = self.translate_block(*on_unwind);
1021 let st = tgt::Statement::new(
1022 src_span,
1023 tgt::StatementKind::Call {
1024 call: call.clone(),
1025 on_unwind,
1026 },
1027 );
1028 let mut block = self.translate_jump(terminator.span, *target);
1029 block.statements.insert(0, st);
1030 block
1031 }
1032 src::TerminatorKind::Drop {
1033 kind,
1034 place,
1035 fn_ptr,
1036 target,
1037 on_unwind,
1038 } => {
1039 let on_unwind = self.translate_block(*on_unwind);
1040 let st = tgt::Statement::new(
1041 src_span,
1042 tgt::StatementKind::Drop {
1043 place: place.clone(),
1044 fn_ptr: fn_ptr.clone(),
1045 kind: *kind,
1046 on_unwind,
1047 },
1048 );
1049 let mut block = self.translate_jump(terminator.span, *target);
1050 block.statements.insert(0, st);
1051 block
1052 }
1053 src::TerminatorKind::Assert {
1054 assert,
1055 target,
1056 on_unwind,
1057 } => {
1058 let on_unwind = self.translate_block(*on_unwind);
1059 let st = tgt::StatementKind::Assert {
1060 assert: assert.clone(),
1061 on_failure: AbortKind::Panic(None),
1062 on_unwind,
1063 };
1064 let target = self.translate_jump(terminator.span, *target);
1065 tgt::Statement::new(src_span, st).into_block().merge(target)
1066 }
1067 src::TerminatorKind::InlineAsm {
1068 asm,
1069 targets,
1070 on_unwind,
1071 } => {
1072 let targets = targets
1073 .iter()
1074 .map(|target| self.translate_jump(terminator.span, *target))
1075 .collect();
1076 let on_unwind = self.translate_block(*on_unwind);
1077 let st = tgt::StatementKind::InlineAsm {
1078 asm: asm.clone(),
1079 targets,
1080 on_unwind,
1081 };
1082 tgt::Statement::new(src_span, st).into_block()
1083 }
1084 src::TerminatorKind::Goto { target } => self.translate_jump(terminator.span, *target),
1085 src::TerminatorKind::Switch { discr, targets } => {
1086 // Translate the target expressions
1087 let switch = match &targets {
1088 src::SwitchTargets::If(then_tgt, else_tgt) => {
1089 let then_block = self.translate_jump(terminator.span, *then_tgt);
1090 let else_block = self.translate_jump(terminator.span, *else_tgt);
1091 tgt::Switch::If(discr.clone(), then_block, else_block)
1092 }
1093 src::SwitchTargets::SwitchInt(int_ty, targets, otherwise) => {
1094 // Note that some branches can be grouped together, like
1095 // here:
1096 // ```
1097 // match e {
1098 // E::V1 | E::V2 => ..., // Grouped
1099 // E::V3 => ...
1100 // }
1101 // ```
1102 // We detect this by checking if a block has already been
1103 // translated as one of the branches of the switch.
1104 //
1105 // Rk.: note there may be intermediate gotos depending
1106 // on the MIR we use. Typically, we manage to detect the
1107 // grouped branches with Optimized MIR, but not with Promoted
1108 // MIR. See the comment in "tests/src/matches.rs".
1109
1110 // We link block ids to:
1111 // - vector of matched integer values
1112 // - translated blocks
1113 let mut branches: SeqHashMap<src::BlockId, (Vec<Literal>, tgt::Block)> =
1114 SeqHashMap::new();
1115
1116 // Translate the children expressions
1117 for (v, bid) in targets.iter() {
1118 // Check if the block has already been translated:
1119 // if yes, it means we need to group branches
1120 if branches.contains_key(bid) {
1121 // Already translated: add the matched value to
1122 // the list of values
1123 let branch = branches.get_mut(bid).unwrap();
1124 branch.0.push(v.clone());
1125 } else {
1126 // Not translated: translate it
1127 let block = self.translate_jump(terminator.span, *bid);
1128 // We use the terminator span information in case then
1129 // then statement is `None`
1130 branches.insert(*bid, (vec![v.clone()], block));
1131 }
1132 }
1133 let targets_blocks: Vec<(Vec<Literal>, tgt::Block)> =
1134 branches.into_iter().map(|(_, x)| x).collect();
1135
1136 let otherwise_block = self.translate_jump(terminator.span, *otherwise);
1137
1138 // Translate
1139 tgt::Switch::SwitchInt(
1140 discr.clone(),
1141 *int_ty,
1142 targets_blocks,
1143 otherwise_block,
1144 )
1145 }
1146 };
1147
1148 // Return
1149 let span = switch.combine_targets_span();
1150 let span = combine_span(&src_span, &span);
1151 let st = tgt::StatementKind::Switch(switch);
1152 tgt::Statement::new(span, st).into_block()
1153 }
1154 }
1155 }
1156
1157 /// Translate just the block statements and terminator.
1158 fn translate_block_itself(&mut self, block_id: BlockId) -> tgt::Block {
1159 let block = &self.body.body[block_id];
1160 // Translate the statements inside the block
1161 let statements = block
1162 .statements
1163 .iter()
1164 .map(|st| self.translate_statement(st))
1165 .collect_vec();
1166 // Translate the terminator.
1167 let terminator = self.translate_terminator(&block.terminator);
1168 // Prepend the statements to the terminator.
1169 if let Some(st) = tgt::Block::from_seq(statements) {
1170 st.merge(terminator)
1171 } else {
1172 terminator
1173 }
1174 }
1175
1176 /// Translate a block including surrounding control-flow like looping.
1177 #[tracing::instrument(skip(self), fields(stack = ?self.special_jump_stack))]
1178 fn translate_block(&mut self, block_id: src::BlockId) -> tgt::Block {
1179 ensure_sufficient_stack(|| self.translate_block_inner(block_id))
1180 }
1181 fn translate_block_inner(&mut self, block_id: src::BlockId) -> tgt::Block {
1182 // Some of the blocks we might jump to inside this tree can't be translated as normal
1183 // blocks: the loop backward edges must become `continue`s and the merge nodes may need
1184 // some care if we're jumping to them from distant locations.
1185 // For this purpose, we push to the `special_jump_stack` the block ids that must be
1186 // translated specially. In `translate_jump` we check the stack. At the end of this
1187 // function we restore the stack to its previous state.
1188 let old_context_depth = self.special_jump_stack.len();
1189 let block_data = &self.cfg.block_data[block_id];
1190 let span = block_data.span;
1191
1192 // Catch jumps to the loop header or loop exit.
1193 if block_data.is_loop_header {
1194 self.break_context_depth += 1;
1195 if let Some(exit_id) = block_data.exit_info.loop_exit {
1196 self.special_jump_stack.push(SpecialJump::new(
1197 exit_id,
1198 SpecialJumpKind::LoopBreak(self.break_context_depth),
1199 ));
1200 }
1201 // Put the next block at the top of the stack.
1202 self.special_jump_stack.push(SpecialJump::new(
1203 block_id,
1204 SpecialJumpKind::LoopContinue(self.break_context_depth),
1205 ));
1206 }
1207
1208 // Catch jumps to a merge node.
1209 let merge_children = &block_data.immediately_dominated_merge_targets;
1210 if let ReconstructMode::ForwardBreak = self.mode {
1211 // We support forward-jumps using `break`
1212 // The child with highest postorder numbering is nested outermost in this scheme.
1213 for &child in merge_children {
1214 self.break_context_depth += 1;
1215 self.special_jump_stack.push(SpecialJump::new(
1216 child,
1217 SpecialJumpKind::ForwardBreak(self.break_context_depth),
1218 ));
1219 }
1220 }
1221
1222 if let Some(bid) = block_data.exit_info.switch_exit
1223 && !block_data.is_loop_header
1224 && !(matches!(self.mode, ReconstructMode::ForwardBreak)
1225 && merge_children.contains(&bid))
1226 {
1227 // Move some code that would be inside one or several switch branches to be after the
1228 // switch intead.
1229 self.special_jump_stack
1230 .push(SpecialJump::new(bid, SpecialJumpKind::NextBlock));
1231 }
1232
1233 // Translate this block. Any jumps to a loop header or a merge node will be replaced with
1234 // `continue`/`break`.
1235 let mut block = self.translate_block_itself(block_id);
1236
1237 // Reset the state to what it was previously, and translate what remains.
1238 let new_statement = move |kind| tgt::Statement::new(block.span, kind);
1239 while self.special_jump_stack.len() > old_context_depth {
1240 let special_jump = self.special_jump_stack.pop().unwrap();
1241 match &special_jump.kind {
1242 SpecialJumpKind::LoopContinue(_) => {
1243 self.break_context_depth -= 1;
1244 if let ReconstructMode::ForwardBreak = self.mode {
1245 // We add `continue` at the end for users that don't know that the default
1246 // behavior at the end of a loop block is `continue`. Not needed for
1247 // `Duplicate` mode because we use explicit `continue`s there. TODO: clean
1248 // that up.
1249 block
1250 .statements
1251 .push(new_statement(tgt::StatementKind::Continue(0)));
1252 }
1253 block = new_statement(tgt::StatementKind::Loop(block)).into_block();
1254 }
1255 SpecialJumpKind::ForwardBreak(_) => {
1256 self.break_context_depth -= 1;
1257 // Remove unneeded `break`s in branches leading up to that final one.
1258 iter_tail_statements(&mut block, &mut |st| {
1259 if matches!(st.kind, tgt::StatementKind::Break(0)) {
1260 st.kind = tgt::StatementKind::Nop;
1261 }
1262 });
1263 // We add a `loop { ...; break }` so that we can use `break` to jump forward.
1264 block
1265 .statements
1266 .push(new_statement(tgt::StatementKind::Break(0)));
1267 block = new_statement(tgt::StatementKind::Loop(block)).into_block();
1268 // We must translate the merge nodes after the block used for forward jumps to
1269 // them.
1270 let next_block = self.translate_jump(span, special_jump.target_block);
1271 block = block.merge(next_block);
1272 }
1273 SpecialJumpKind::NextBlock | SpecialJumpKind::LoopBreak(..) => {
1274 let next_block = self.translate_jump(span, special_jump.target_block);
1275 block = block.merge(next_block);
1276 }
1277 }
1278 }
1279 block
1280 }
1281}
1282
1283fn remove_useless_jump_blocks(body: &mut tgt::ExprBody) {
1284 use tgt::StatementKind;
1285 #[derive(Default)]
1286 struct Count {
1287 continue_count: u32,
1288 break_count: u32,
1289 }
1290 #[derive(Default, Visitor)]
1291 struct CountJumpsVisitor {
1292 counts: HashMap<StatementId, Count>,
1293 loop_stack: Vec<StatementId>,
1294 }
1295 #[derive(Visitor)]
1296 struct RemoveUselessJumpsVisitor {
1297 counts: HashMap<StatementId, Count>,
1298 /// For every loop we encounter, whether we're keeping it or removing it.
1299 loop_stack: Vec<bool>,
1300 }
1301
1302 impl VisitBodyMut for CountJumpsVisitor {
1303 fn visit_llbc_statement(&mut self, st: &mut tgt::Statement) -> ControlFlow<Self::Break> {
1304 if let StatementKind::Loop(_) = &st.kind {
1305 self.loop_stack.push(st.id);
1306 }
1307 match &st.kind {
1308 StatementKind::Break(depth) => {
1309 let loop_id = self.loop_stack[self.loop_stack.len() - 1 - depth];
1310 self.counts.entry(loop_id).or_default().break_count += 1;
1311 }
1312 StatementKind::Continue(depth) => {
1313 let loop_id = self.loop_stack[self.loop_stack.len() - 1 - depth];
1314 self.counts.entry(loop_id).or_default().continue_count += 1;
1315 }
1316 _ => {}
1317 }
1318 self.visit_inner(st)?;
1319 if let StatementKind::Loop(_) = &st.kind {
1320 self.loop_stack.pop();
1321 }
1322 ControlFlow::Continue(())
1323 }
1324 }
1325
1326 impl VisitBodyMut for RemoveUselessJumpsVisitor {
1327 fn visit_llbc_block(&mut self, block: &mut tgt::Block) -> ControlFlow<Self::Break> {
1328 for mut st in mem::take(&mut block.statements) {
1329 if let tgt::StatementKind::Loop(block) = &mut st.kind {
1330 let counts = &self.counts[&st.id];
1331 let remove = counts.continue_count == 0
1332 && counts.break_count == 1
1333 && matches!(
1334 block.statements.last().unwrap().kind,
1335 StatementKind::Break(0)
1336 );
1337 self.loop_stack.push(!remove);
1338 }
1339 self.visit(&mut st)?;
1340 if st.kind.is_loop() && !self.loop_stack.pop().unwrap() {
1341 // Remove the loop.
1342 let StatementKind::Loop(mut inner_block) = st.kind else {
1343 unreachable!()
1344 };
1345 inner_block.statements.last_mut().unwrap().kind = StatementKind::Nop;
1346 block.statements.extend(inner_block.statements);
1347 } else {
1348 block.statements.push(st);
1349 }
1350 }
1351 ControlFlow::Continue(())
1352 }
1353 fn enter_llbc_statement(&mut self, st: &mut tgt::Statement) {
1354 match &st.kind {
1355 StatementKind::Break(depth) => {
1356 let new_depth = self.loop_stack[self.loop_stack.len() - depth..]
1357 .iter()
1358 .filter(|&&keep| keep)
1359 .count();
1360 st.kind = StatementKind::Break(new_depth);
1361 }
1362 StatementKind::Continue(depth) => {
1363 let new_depth = self.loop_stack[self.loop_stack.len() - depth..]
1364 .iter()
1365 .filter(|&&keep| keep)
1366 .count();
1367 st.kind = StatementKind::Continue(new_depth);
1368 }
1369 _ => {}
1370 }
1371 }
1372 }
1373
1374 let mut v = CountJumpsVisitor::default();
1375 body.body.drive_body_mut(&mut v);
1376 let mut v = RemoveUselessJumpsVisitor {
1377 counts: v.counts,
1378 loop_stack: Default::default(),
1379 };
1380 body.body.drive_body_mut(&mut v);
1381}
1382
1383fn translate_body(ctx: &mut TransformCtx, body: &mut Body) {
1384 use Body::{Structured, Unstructured};
1385 let Unstructured(src_body) = body else {
1386 panic!("Called `ullbc_to_llbc` on an already restructured body")
1387 };
1388 trace!("About to translate to ullbc: {:?}", src_body.span);
1389
1390 // Calculate info about the graph and heuristically determine loop and switch exit blocks.
1391 let start_block = BlockId::ZERO;
1392 let mut ctx = match ReconstructCtx::build(ctx, src_body) {
1393 Ok(ctx) => ctx,
1394 Err(Irreducible(bid)) => {
1395 let span = src_body.body[bid].terminator.span;
1396 register_error!(
1397 ctx,
1398 span,
1399 "the control-flow graph of this function is not reducible"
1400 );
1401 panic!("can't reconstruct irreducible control-flow")
1402 }
1403 };
1404 // Translate the blocks using the computed data.
1405 let tgt_body = ctx.translate_block(start_block);
1406
1407 let mut tgt_body = tgt::ExprBody {
1408 span: src_body.span,
1409 locals: src_body.locals.clone(),
1410 bound_body_regions: src_body.bound_body_regions,
1411 body: tgt_body,
1412 comments: src_body.comments.clone(),
1413 };
1414 remove_useless_jump_blocks(&mut tgt_body);
1415
1416 *body = Structured(tgt_body);
1417}
1418
1419pub struct Transform;
1420impl TransformPass for Transform {
1421 fn transform_ctx(&self, ctx: &mut TransformCtx) {
1422 // Translate the bodies one at a time.
1423 ctx.for_each_body(|ctx, body| {
1424 translate_body(ctx, body);
1425 });
1426
1427 if ctx.options.print_built_llbc {
1428 eprintln!("# LLBC resulting from control-flow reconstruction:\n\n{ctx}\n",);
1429 } else {
1430 trace!("# LLBC resulting from control-flow reconstruction:\n\n{ctx}\n",);
1431 }
1432 }
1433}