#![allow(clippy::collapsible_if)]
use std::collections::{HashMap, HashSet};
use petgraph::Graph;
use petgraph::algo::dominators::{Dominators, simple_fast};
use petgraph::graph::NodeIndex;
use crate::ssa::ir::{BlockId, SsaBody, SsaOp, SsaValue, Terminator};
pub const MAX_LOOP_UNROLL: u8 = 2;
pub struct LoopInfo {
pub back_edges: HashSet<(BlockId, BlockId)>,
pub loop_heads: HashSet<BlockId>,
pub loop_bodies: HashMap<BlockId, HashSet<BlockId>>,
pub induction_vars: HashSet<SsaValue>,
#[allow(dead_code)]
doms: Dominators<NodeIndex>,
}
pub fn analyse_loops(ssa: &SsaBody) -> LoopInfo {
let num_blocks = ssa.blocks.len();
let (block_graph, block_nodes, entry_node) = build_block_graph(ssa);
let doms = simple_fast(&block_graph, entry_node);
let back_edges = detect_back_edges(ssa, &block_nodes, &doms, num_blocks);
let loop_heads: HashSet<BlockId> = back_edges.iter().map(|(_, head)| *head).collect();
let loop_bodies = compute_all_loop_bodies(ssa, &back_edges);
let induction_vars = detect_induction_vars(ssa, &back_edges, &loop_heads);
LoopInfo {
back_edges,
loop_heads,
loop_bodies,
induction_vars,
doms,
}
}
impl LoopInfo {
pub fn loop_exit_successor(&self, ssa: &SsaBody, head: BlockId) -> Option<BlockId> {
let body = self.loop_bodies.get(&head)?;
let block = ssa.blocks.get(head.0 as usize)?;
match &block.terminator {
Terminator::Branch {
true_blk,
false_blk,
..
} => {
let true_in = body.contains(true_blk);
let false_in = body.contains(false_blk);
match (true_in, false_in) {
(true, false) => Some(*false_blk),
(false, true) => Some(*true_blk),
(false, false) => Some(*true_blk), (true, true) => None, }
}
_ => None, }
}
pub fn has_loops(&self) -> bool {
!self.loop_heads.is_empty()
}
}
fn build_block_graph(ssa: &SsaBody) -> (Graph<BlockId, ()>, Vec<NodeIndex>, NodeIndex) {
let num_blocks = ssa.blocks.len();
let mut g: Graph<BlockId, ()> = Graph::with_capacity(num_blocks, num_blocks * 2);
let mut block_nodes: Vec<NodeIndex> = Vec::with_capacity(num_blocks);
for i in 0..num_blocks {
block_nodes.push(g.add_node(BlockId(i as u32)));
}
for block in &ssa.blocks {
let src = block_nodes[block.id.0 as usize];
for &succ in &block.succs {
if (succ.0 as usize) < num_blocks {
g.add_edge(src, block_nodes[succ.0 as usize], ());
}
}
}
let entry_node = block_nodes[ssa.entry.0 as usize];
(g, block_nodes, entry_node)
}
fn dominates_block(doms: &Dominators<NodeIndex>, dominator: NodeIndex, target: NodeIndex) -> bool {
if dominator == target {
return true;
}
let mut current = target;
while let Some(idom) = doms.immediate_dominator(current) {
if idom == current {
break; }
if idom == dominator {
return true;
}
current = idom;
}
false
}
fn detect_back_edges(
ssa: &SsaBody,
block_nodes: &[NodeIndex],
doms: &Dominators<NodeIndex>,
num_blocks: usize,
) -> HashSet<(BlockId, BlockId)> {
let mut back_edges = HashSet::new();
for block in &ssa.blocks {
let src_idx = block.id.0 as usize;
if src_idx >= num_blocks {
continue;
}
let src_node = block_nodes[src_idx];
for &succ in &block.succs {
let tgt_idx = succ.0 as usize;
if tgt_idx >= num_blocks {
continue;
}
let tgt_node = block_nodes[tgt_idx];
if dominates_block(doms, tgt_node, src_node) {
back_edges.insert((block.id, succ));
}
}
}
back_edges
}
fn compute_natural_loop_body(ssa: &SsaBody, head: BlockId, latch: BlockId) -> HashSet<BlockId> {
let mut body = HashSet::new();
body.insert(head);
if head == latch {
return body; }
body.insert(latch);
let mut worklist = vec![latch];
while let Some(bid) = worklist.pop() {
if let Some(block) = ssa.blocks.get(bid.0 as usize) {
for &pred in &block.preds {
if pred != head && body.insert(pred) {
worklist.push(pred);
}
}
}
}
body
}
fn compute_all_loop_bodies(
ssa: &SsaBody,
back_edges: &HashSet<(BlockId, BlockId)>,
) -> HashMap<BlockId, HashSet<BlockId>> {
let mut bodies: HashMap<BlockId, HashSet<BlockId>> = HashMap::new();
for &(latch, head) in back_edges {
let body = compute_natural_loop_body(ssa, head, latch);
bodies
.entry(head)
.and_modify(|existing| {
existing.extend(body.iter());
})
.or_insert(body);
}
bodies
}
fn detect_induction_vars(
ssa: &SsaBody,
back_edges: &HashSet<(BlockId, BlockId)>,
loop_heads: &HashSet<BlockId>,
) -> HashSet<SsaValue> {
let mut induction_vars = HashSet::new();
for block in &ssa.blocks {
if !loop_heads.contains(&block.id) {
continue;
}
for phi in &block.phis {
if let SsaOp::Phi(ref operands) = phi.op {
if operands.len() != 2 {
continue;
}
let mut back_edge_op = None;
let mut init_op = None;
for &(pred_blk, operand_val) in operands {
if back_edges.contains(&(pred_blk, block.id)) {
back_edge_op = Some(operand_val);
} else {
init_op = Some(operand_val);
}
}
if let (Some(back_val), Some(_init_val)) = (back_edge_op, init_op) {
if is_simple_increment(ssa, back_val, phi.value) {
induction_vars.insert(phi.value);
}
}
}
}
}
induction_vars
}
fn is_simple_increment(ssa: &SsaBody, inc_val: SsaValue, phi_val: SsaValue) -> bool {
let def = ssa.def_of(inc_val);
let block = ssa.block(def.block);
for inst in &block.body {
if inst.value == inc_val {
if let SsaOp::Assign(ref uses) = inst.op {
if uses.len() == 2 && uses.contains(&phi_val) {
let other = if uses[0] == phi_val { uses[1] } else { uses[0] };
let other_def = ssa.def_of(other);
let other_block = ssa.block(other_def.block);
for other_inst in other_block.phis.iter().chain(other_block.body.iter()) {
if other_inst.value == other && matches!(other_inst.op, SsaOp::Const(_)) {
return true;
}
}
}
}
break;
}
}
false
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ssa::ir::{SsaBlock, SsaInst, ValueDef};
use petgraph::graph::NodeIndex as CfgNodeIndex;
use smallvec::smallvec;
fn dummy_cfg_node() -> CfgNodeIndex {
CfgNodeIndex::new(0)
}
fn make_value_def(block: BlockId) -> ValueDef {
ValueDef {
var_name: None,
cfg_node: dummy_cfg_node(),
block,
}
}
fn make_inst(val: u32, op: SsaOp, _block: BlockId) -> SsaInst {
SsaInst {
value: SsaValue(val),
op,
cfg_node: dummy_cfg_node(),
var_name: None,
span: (0, 0),
}
}
#[test]
fn simple_loop_back_edge() {
let ssa = SsaBody {
blocks: vec![
SsaBlock {
id: BlockId(0),
phis: vec![],
body: vec![],
terminator: Terminator::Goto(BlockId(1)),
preds: smallvec![],
succs: smallvec![BlockId(1)],
},
SsaBlock {
id: BlockId(1),
phis: vec![],
body: vec![],
terminator: Terminator::Branch {
cond: dummy_cfg_node(),
true_blk: BlockId(2),
false_blk: BlockId(3),
condition: None,
},
preds: smallvec![BlockId(0), BlockId(2)],
succs: smallvec![BlockId(2), BlockId(3)],
},
SsaBlock {
id: BlockId(2),
phis: vec![],
body: vec![],
terminator: Terminator::Goto(BlockId(1)),
preds: smallvec![BlockId(1)],
succs: smallvec![BlockId(1)],
},
SsaBlock {
id: BlockId(3),
phis: vec![],
body: vec![],
terminator: Terminator::Return(None),
preds: smallvec![BlockId(1)],
succs: smallvec![],
},
],
entry: BlockId(0),
value_defs: vec![],
cfg_node_map: HashMap::new(),
exception_edges: vec![],
field_interner: crate::ssa::ir::FieldInterner::default(),
field_writes: std::collections::HashMap::new(),
synthetic_externals: std::collections::HashSet::new(),
};
let info = analyse_loops(&ssa);
assert_eq!(info.back_edges.len(), 1);
assert!(info.back_edges.contains(&(BlockId(2), BlockId(1))));
assert_eq!(info.loop_heads.len(), 1);
assert!(info.loop_heads.contains(&BlockId(1)));
}
#[test]
fn no_loop_linear() {
let ssa = SsaBody {
blocks: vec![
SsaBlock {
id: BlockId(0),
phis: vec![],
body: vec![],
terminator: Terminator::Goto(BlockId(1)),
preds: smallvec![],
succs: smallvec![BlockId(1)],
},
SsaBlock {
id: BlockId(1),
phis: vec![],
body: vec![],
terminator: Terminator::Goto(BlockId(2)),
preds: smallvec![BlockId(0)],
succs: smallvec![BlockId(2)],
},
SsaBlock {
id: BlockId(2),
phis: vec![],
body: vec![],
terminator: Terminator::Return(None),
preds: smallvec![BlockId(1)],
succs: smallvec![],
},
],
entry: BlockId(0),
value_defs: vec![],
cfg_node_map: HashMap::new(),
exception_edges: vec![],
field_interner: crate::ssa::ir::FieldInterner::default(),
field_writes: std::collections::HashMap::new(),
synthetic_externals: std::collections::HashSet::new(),
};
let info = analyse_loops(&ssa);
assert!(info.back_edges.is_empty());
assert!(info.loop_heads.is_empty());
assert!(info.loop_bodies.is_empty());
assert!(!info.has_loops());
}
#[test]
fn nested_loops() {
let ssa = SsaBody {
blocks: vec![
SsaBlock {
id: BlockId(0),
phis: vec![],
body: vec![],
terminator: Terminator::Goto(BlockId(1)),
preds: smallvec![],
succs: smallvec![BlockId(1)],
},
SsaBlock {
id: BlockId(1),
phis: vec![],
body: vec![],
terminator: Terminator::Branch {
cond: dummy_cfg_node(),
true_blk: BlockId(2),
false_blk: BlockId(5),
condition: None,
},
preds: smallvec![BlockId(0), BlockId(4)],
succs: smallvec![BlockId(2), BlockId(5)],
},
SsaBlock {
id: BlockId(2),
phis: vec![],
body: vec![],
terminator: Terminator::Branch {
cond: dummy_cfg_node(),
true_blk: BlockId(3),
false_blk: BlockId(4),
condition: None,
},
preds: smallvec![BlockId(1), BlockId(3)],
succs: smallvec![BlockId(3), BlockId(4)],
},
SsaBlock {
id: BlockId(3),
phis: vec![],
body: vec![],
terminator: Terminator::Goto(BlockId(2)),
preds: smallvec![BlockId(2)],
succs: smallvec![BlockId(2)],
},
SsaBlock {
id: BlockId(4),
phis: vec![],
body: vec![],
terminator: Terminator::Goto(BlockId(1)),
preds: smallvec![BlockId(2)],
succs: smallvec![BlockId(1)],
},
SsaBlock {
id: BlockId(5),
phis: vec![],
body: vec![],
terminator: Terminator::Return(None),
preds: smallvec![BlockId(1)],
succs: smallvec![],
},
],
entry: BlockId(0),
value_defs: vec![],
cfg_node_map: HashMap::new(),
exception_edges: vec![],
field_interner: crate::ssa::ir::FieldInterner::default(),
field_writes: std::collections::HashMap::new(),
synthetic_externals: std::collections::HashSet::new(),
};
let info = analyse_loops(&ssa);
assert_eq!(info.back_edges.len(), 2);
assert!(info.back_edges.contains(&(BlockId(3), BlockId(2)))); assert!(info.back_edges.contains(&(BlockId(4), BlockId(1)))); assert_eq!(info.loop_heads.len(), 2);
assert!(info.loop_heads.contains(&BlockId(1)));
assert!(info.loop_heads.contains(&BlockId(2)));
}
#[test]
fn natural_body_simple_loop() {
let ssa = SsaBody {
blocks: vec![
SsaBlock {
id: BlockId(0),
phis: vec![],
body: vec![],
terminator: Terminator::Goto(BlockId(1)),
preds: smallvec![],
succs: smallvec![BlockId(1)],
},
SsaBlock {
id: BlockId(1),
phis: vec![],
body: vec![],
terminator: Terminator::Branch {
cond: dummy_cfg_node(),
true_blk: BlockId(2),
false_blk: BlockId(3),
condition: None,
},
preds: smallvec![BlockId(0), BlockId(2)],
succs: smallvec![BlockId(2), BlockId(3)],
},
SsaBlock {
id: BlockId(2),
phis: vec![],
body: vec![],
terminator: Terminator::Goto(BlockId(1)),
preds: smallvec![BlockId(1)],
succs: smallvec![BlockId(1)],
},
SsaBlock {
id: BlockId(3),
phis: vec![],
body: vec![],
terminator: Terminator::Return(None),
preds: smallvec![BlockId(1)],
succs: smallvec![],
},
],
entry: BlockId(0),
value_defs: vec![],
cfg_node_map: HashMap::new(),
exception_edges: vec![],
field_interner: crate::ssa::ir::FieldInterner::default(),
field_writes: std::collections::HashMap::new(),
synthetic_externals: std::collections::HashSet::new(),
};
let info = analyse_loops(&ssa);
let body = info.loop_bodies.get(&BlockId(1)).unwrap();
assert!(body.contains(&BlockId(1))); assert!(body.contains(&BlockId(2))); assert!(!body.contains(&BlockId(0))); assert!(!body.contains(&BlockId(3))); }
#[test]
fn natural_body_nested_excludes_outer() {
let ssa = SsaBody {
blocks: vec![
SsaBlock {
id: BlockId(0),
phis: vec![],
body: vec![],
terminator: Terminator::Goto(BlockId(1)),
preds: smallvec![],
succs: smallvec![BlockId(1)],
},
SsaBlock {
id: BlockId(1),
phis: vec![],
body: vec![],
terminator: Terminator::Branch {
cond: dummy_cfg_node(),
true_blk: BlockId(2),
false_blk: BlockId(5),
condition: None,
},
preds: smallvec![BlockId(0), BlockId(4)],
succs: smallvec![BlockId(2), BlockId(5)],
},
SsaBlock {
id: BlockId(2),
phis: vec![],
body: vec![],
terminator: Terminator::Branch {
cond: dummy_cfg_node(),
true_blk: BlockId(3),
false_blk: BlockId(4),
condition: None,
},
preds: smallvec![BlockId(1), BlockId(3)],
succs: smallvec![BlockId(3), BlockId(4)],
},
SsaBlock {
id: BlockId(3),
phis: vec![],
body: vec![],
terminator: Terminator::Goto(BlockId(2)),
preds: smallvec![BlockId(2)],
succs: smallvec![BlockId(2)],
},
SsaBlock {
id: BlockId(4),
phis: vec![],
body: vec![],
terminator: Terminator::Goto(BlockId(1)),
preds: smallvec![BlockId(2)],
succs: smallvec![BlockId(1)],
},
SsaBlock {
id: BlockId(5),
phis: vec![],
body: vec![],
terminator: Terminator::Return(None),
preds: smallvec![BlockId(1)],
succs: smallvec![],
},
],
entry: BlockId(0),
value_defs: vec![],
cfg_node_map: HashMap::new(),
exception_edges: vec![],
field_interner: crate::ssa::ir::FieldInterner::default(),
field_writes: std::collections::HashMap::new(),
synthetic_externals: std::collections::HashSet::new(),
};
let info = analyse_loops(&ssa);
let inner = info.loop_bodies.get(&BlockId(2)).unwrap();
assert!(inner.contains(&BlockId(2)));
assert!(inner.contains(&BlockId(3)));
assert!(!inner.contains(&BlockId(1))); assert!(!inner.contains(&BlockId(4)));
let outer = info.loop_bodies.get(&BlockId(1)).unwrap();
assert!(outer.contains(&BlockId(1)));
assert!(outer.contains(&BlockId(2)));
assert!(outer.contains(&BlockId(3)));
assert!(outer.contains(&BlockId(4)));
assert!(!outer.contains(&BlockId(5))); }
#[test]
fn exit_successor_simple() {
let ssa = SsaBody {
blocks: vec![
SsaBlock {
id: BlockId(0),
phis: vec![],
body: vec![],
terminator: Terminator::Goto(BlockId(1)),
preds: smallvec![],
succs: smallvec![BlockId(1)],
},
SsaBlock {
id: BlockId(1),
phis: vec![],
body: vec![],
terminator: Terminator::Branch {
cond: dummy_cfg_node(),
true_blk: BlockId(2),
false_blk: BlockId(3),
condition: None,
},
preds: smallvec![BlockId(0), BlockId(2)],
succs: smallvec![BlockId(2), BlockId(3)],
},
SsaBlock {
id: BlockId(2),
phis: vec![],
body: vec![],
terminator: Terminator::Goto(BlockId(1)),
preds: smallvec![BlockId(1)],
succs: smallvec![BlockId(1)],
},
SsaBlock {
id: BlockId(3),
phis: vec![],
body: vec![],
terminator: Terminator::Return(None),
preds: smallvec![BlockId(1)],
succs: smallvec![],
},
],
entry: BlockId(0),
value_defs: vec![],
cfg_node_map: HashMap::new(),
exception_edges: vec![],
field_interner: crate::ssa::ir::FieldInterner::default(),
field_writes: std::collections::HashMap::new(),
synthetic_externals: std::collections::HashSet::new(),
};
let info = analyse_loops(&ssa);
assert_eq!(info.loop_exit_successor(&ssa, BlockId(1)), Some(BlockId(3)));
}
#[test]
fn exit_successor_goto_returns_none() {
let ssa = SsaBody {
blocks: vec![
SsaBlock {
id: BlockId(0),
phis: vec![],
body: vec![],
terminator: Terminator::Goto(BlockId(1)),
preds: smallvec![],
succs: smallvec![BlockId(1)],
},
SsaBlock {
id: BlockId(1),
phis: vec![],
body: vec![],
terminator: Terminator::Goto(BlockId(1)),
preds: smallvec![BlockId(0), BlockId(1)],
succs: smallvec![BlockId(1)],
},
],
entry: BlockId(0),
value_defs: vec![],
cfg_node_map: HashMap::new(),
exception_edges: vec![],
field_interner: crate::ssa::ir::FieldInterner::default(),
field_writes: std::collections::HashMap::new(),
synthetic_externals: std::collections::HashSet::new(),
};
let info = analyse_loops(&ssa);
assert_eq!(info.loop_exit_successor(&ssa, BlockId(1)), None);
}
#[test]
fn exit_successor_both_in_body_returns_none() {
let ssa = SsaBody {
blocks: vec![
SsaBlock {
id: BlockId(0),
phis: vec![],
body: vec![],
terminator: Terminator::Goto(BlockId(1)),
preds: smallvec![],
succs: smallvec![BlockId(1)],
},
SsaBlock {
id: BlockId(1),
phis: vec![],
body: vec![],
terminator: Terminator::Branch {
cond: dummy_cfg_node(),
true_blk: BlockId(2),
false_blk: BlockId(3),
condition: None,
},
preds: smallvec![BlockId(0), BlockId(3)],
succs: smallvec![BlockId(2), BlockId(3)],
},
SsaBlock {
id: BlockId(2),
phis: vec![],
body: vec![],
terminator: Terminator::Goto(BlockId(3)),
preds: smallvec![BlockId(1)],
succs: smallvec![BlockId(3)],
},
SsaBlock {
id: BlockId(3),
phis: vec![],
body: vec![],
terminator: Terminator::Goto(BlockId(1)),
preds: smallvec![BlockId(1), BlockId(2)],
succs: smallvec![BlockId(1)],
},
],
entry: BlockId(0),
value_defs: vec![],
cfg_node_map: HashMap::new(),
exception_edges: vec![],
field_interner: crate::ssa::ir::FieldInterner::default(),
field_writes: std::collections::HashMap::new(),
synthetic_externals: std::collections::HashSet::new(),
};
let info = analyse_loops(&ssa);
assert_eq!(info.loop_exit_successor(&ssa, BlockId(1)), None);
}
#[test]
fn induction_var_simple_counter() {
let ssa = SsaBody {
blocks: vec![
SsaBlock {
id: BlockId(0),
phis: vec![],
body: vec![
make_inst(0, SsaOp::Const(Some("0".into())), BlockId(0)),
make_inst(2, SsaOp::Const(Some("1".into())), BlockId(0)),
],
terminator: Terminator::Goto(BlockId(1)),
preds: smallvec![],
succs: smallvec![BlockId(1)],
},
SsaBlock {
id: BlockId(1),
phis: vec![make_inst(
1,
SsaOp::Phi(smallvec![
(BlockId(0), SsaValue(0)),
(BlockId(2), SsaValue(3))
]),
BlockId(1),
)],
body: vec![],
terminator: Terminator::Branch {
cond: dummy_cfg_node(),
true_blk: BlockId(2),
false_blk: BlockId(3),
condition: None,
},
preds: smallvec![BlockId(0), BlockId(2)],
succs: smallvec![BlockId(2), BlockId(3)],
},
SsaBlock {
id: BlockId(2),
phis: vec![],
body: vec![make_inst(
3,
SsaOp::Assign(smallvec![SsaValue(1), SsaValue(2)]),
BlockId(2),
)],
terminator: Terminator::Goto(BlockId(1)),
preds: smallvec![BlockId(1)],
succs: smallvec![BlockId(1)],
},
SsaBlock {
id: BlockId(3),
phis: vec![],
body: vec![],
terminator: Terminator::Return(None),
preds: smallvec![BlockId(1)],
succs: smallvec![],
},
],
entry: BlockId(0),
value_defs: vec![
make_value_def(BlockId(0)), make_value_def(BlockId(1)), make_value_def(BlockId(0)), make_value_def(BlockId(2)), ],
cfg_node_map: HashMap::new(),
exception_edges: vec![],
field_interner: crate::ssa::ir::FieldInterner::default(),
field_writes: std::collections::HashMap::new(),
synthetic_externals: std::collections::HashSet::new(),
};
let info = analyse_loops(&ssa);
assert!(info.induction_vars.contains(&SsaValue(1)));
}
#[test]
fn non_induction_phi_not_detected() {
let ssa = SsaBody {
blocks: vec![
SsaBlock {
id: BlockId(0),
phis: vec![],
body: vec![make_inst(0, SsaOp::Source, BlockId(0))],
terminator: Terminator::Goto(BlockId(1)),
preds: smallvec![],
succs: smallvec![BlockId(1)],
},
SsaBlock {
id: BlockId(1),
phis: vec![make_inst(
1,
SsaOp::Phi(smallvec![
(BlockId(0), SsaValue(0)),
(BlockId(2), SsaValue(2))
]),
BlockId(1),
)],
body: vec![],
terminator: Terminator::Branch {
cond: dummy_cfg_node(),
true_blk: BlockId(2),
false_blk: BlockId(3),
condition: None,
},
preds: smallvec![BlockId(0), BlockId(2)],
succs: smallvec![BlockId(2), BlockId(3)],
},
SsaBlock {
id: BlockId(2),
phis: vec![],
body: vec![make_inst(
2,
SsaOp::Call {
callee: "f".into(),
callee_text: None,
args: vec![smallvec![SsaValue(1)]],
receiver: None,
},
BlockId(2),
)],
terminator: Terminator::Goto(BlockId(1)),
preds: smallvec![BlockId(1)],
succs: smallvec![BlockId(1)],
},
SsaBlock {
id: BlockId(3),
phis: vec![],
body: vec![],
terminator: Terminator::Return(None),
preds: smallvec![BlockId(1)],
succs: smallvec![],
},
],
entry: BlockId(0),
value_defs: vec![
make_value_def(BlockId(0)), make_value_def(BlockId(1)), make_value_def(BlockId(2)), ],
cfg_node_map: HashMap::new(),
exception_edges: vec![],
field_interner: crate::ssa::ir::FieldInterner::default(),
field_writes: std::collections::HashMap::new(),
synthetic_externals: std::collections::HashSet::new(),
};
let info = analyse_loops(&ssa);
assert!(info.induction_vars.is_empty());
}
#[test]
fn has_loops_with_loop() {
let ssa = SsaBody {
blocks: vec![
SsaBlock {
id: BlockId(0),
phis: vec![],
body: vec![],
terminator: Terminator::Goto(BlockId(1)),
preds: smallvec![],
succs: smallvec![BlockId(1)],
},
SsaBlock {
id: BlockId(1),
phis: vec![],
body: vec![],
terminator: Terminator::Goto(BlockId(0)),
preds: smallvec![BlockId(0)],
succs: smallvec![BlockId(0)],
},
],
entry: BlockId(0),
value_defs: vec![],
cfg_node_map: HashMap::new(),
exception_edges: vec![],
field_interner: crate::ssa::ir::FieldInterner::default(),
field_writes: std::collections::HashMap::new(),
synthetic_externals: std::collections::HashSet::new(),
};
let info = analyse_loops(&ssa);
assert!(info.has_loops());
}
}