nyx/src/symex/loops.rs
Eli Peter 58f1794a4e
Added Cap::DATA_EXFIL and taint fp and fn fixes on real repos (#59)
* feat: Enhance data exfiltration detection with source sensitivity gating for cookies and headers

* feat: Implement cross-file data exfiltration detection with parameter-specific gate filters

* feat: Add calibration tests and refine DATA_EXFIL severity scoring logic

* feat: Introduce per-detector configuration for data exfiltration suppression

* feat: Enhance DATA_EXFIL findings with destination field tracking in diagnostics and SARIF output

* feat: Add tainted body and URL handling for data exfiltration detection

* feat: Add integration tests and fixtures for DATA_EXFIL and SSRF detection in Go

* feat: Add Java integration tests and fixtures for DATA_EXFIL detection across multiple HTTP clients

* feat: Add synthetic externals handling for closure-captured variables in SSA

* feat: Implement closure-based suppression for resource leak findings

* feat: Add regression guards for shell-injection and taint propagation in for-of destructure patterns

* feat: Implement constructor cap narrowing for data exfiltration detection in HTTP request builders

* feat: Add gated sinks for data exfiltration detection in C and C++ using curl_easy_setopt

* feat: Implement DATA_EXFIL cap parity for backwards analysis and add integration tests

* feat: Add data exfiltration sinks for various languages and enhance documentation

* refactor: Simplify formatting and improve readability in various files

* refactor: Improve readability by simplifying conditional statements and adding clippy linting

* docs: Update CHANGELOG and comments for data exfiltration features and configuration

* docs: Clarify configuration instructions for data exfiltration trusted destinations

* docs: Enhance comments for evidence routing logic in data exfiltration
2026-05-01 10:59:52 -04:00

1041 lines
38 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//! Loop analysis for the symbolic executor.
//!
//! Detects back edges, computes natural loop bodies, identifies induction
//! variables, and determines loop exit successors. All analysis is computed
//! once per `explore_finding()` invocation and shared across all paths.
#![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};
/// Default loop unrolling bound. After this many visits to a loop head,
/// the executor widens and skips to the exit.
pub const MAX_LOOP_UNROLL: u8 = 2;
/// Pre-computed loop information for symex exploration.
///
/// Computed once per `explore_finding()` invocation, shared across all paths.
pub struct LoopInfo {
/// Back edges: (latch block, loop head block).
pub back_edges: HashSet<(BlockId, BlockId)>,
/// Blocks that are loop-head targets of back edges.
pub loop_heads: HashSet<BlockId>,
/// Natural loop body per loop head: head → set of blocks in the loop.
pub loop_bodies: HashMap<BlockId, HashSet<BlockId>>,
/// SSA values that are simple induction variables (loop counters).
pub induction_vars: HashSet<SsaValue>,
/// Dominator tree (retained for exit successor queries).
#[allow(dead_code)]
doms: Dominators<NodeIndex>,
}
// ─────────────────────────────────────────────────────────────────────────────
// Public API
// ─────────────────────────────────────────────────────────────────────────────
/// Analyse loop structure in an SSA body.
///
/// Builds a petgraph from the SSA blocks, computes dominators, detects back
/// edges, natural loop bodies, and induction variables. All results are
/// bundled into a [`LoopInfo`] for use by the executor.
pub fn analyse_loops(ssa: &SsaBody) -> LoopInfo {
let num_blocks = ssa.blocks.len();
// Build petgraph from SSA block successors
let (block_graph, block_nodes, entry_node) = build_block_graph(ssa);
// Compute dominator tree
let doms = simple_fast(&block_graph, entry_node);
// Detect back edges: (src, tgt) where tgt dominates src
let back_edges = detect_back_edges(ssa, &block_nodes, &doms, num_blocks);
// Extract loop heads
let loop_heads: HashSet<BlockId> = back_edges.iter().map(|(_, head)| *head).collect();
// Compute natural loop bodies
let loop_bodies = compute_all_loop_bodies(ssa, &back_edges);
// Detect induction variables
let induction_vars = detect_induction_vars(ssa, &back_edges, &loop_heads);
LoopInfo {
back_edges,
loop_heads,
loop_bodies,
induction_vars,
doms,
}
}
impl LoopInfo {
/// Determine the loop exit successor for a branch at a loop head.
///
/// Uses natural loop body membership: the exit successor is the one
/// whose target is NOT in the loop body. Returns `None` if both
/// successors are inside the loop (nested loop) or the block has no
/// branch terminator.
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), // both exit, deterministic pick
(true, true) => None, // nested: no clear exit
}
}
_ => None, // Goto or Return, no branching exit
}
}
/// Check if this LoopInfo has any loops at all (useful for fast skip).
pub fn has_loops(&self) -> bool {
!self.loop_heads.is_empty()
}
}
// ─────────────────────────────────────────────────────────────────────────────
// Internal helpers
// ─────────────────────────────────────────────────────────────────────────────
/// Build a petgraph from SSA block successors.
///
/// Mirrors the pattern in `src/ssa/lower.rs:build_block_graph`.
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)
}
/// Check if `dominator` dominates `target` in the dominator tree.
///
/// Mirrors the pattern in `src/cfg_analysis/dominators.rs:dominates`.
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; // reached root
}
if idom == dominator {
return true;
}
current = idom;
}
false
}
/// Detect back edges using dominator analysis.
///
/// An edge (src, tgt) is a back edge if tgt dominates src in the
/// dominator tree. This is sound for all CFG shapes, unlike the
/// block-index heuristic used by the taint engine.
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
}
/// Compute the natural loop body for a single back edge (latch → head).
///
/// The natural loop is {head} {blocks that can reach latch without
/// going through head}. Uses reverse BFS from the latch, stopping at head.
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; // single-block loop
}
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
}
/// Compute natural loop bodies for all loop heads.
///
/// When multiple back edges target the same head, their bodies are unioned.
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
}
/// Detect induction variables: phi nodes at loop heads where the back-edge
/// operand is a simple increment/decrement of the phi result.
///
/// Mirrors `detect_induction_phis()` in `src/taint/ssa_transfer.rs`.
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;
}
// Identify which operand comes via back edge
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
}
/// Check if `inc_val` is defined as a simple increment of `phi_val`:
/// `inc_val = phi_val + const` or `inc_val = phi_val - const`.
///
/// Mirrors `is_simple_increment()` in `src/taint/ssa_transfer.rs`.
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
}
// ─────────────────────────────────────────────────────────────────────────────
// Tests
// ─────────────────────────────────────────────────────────────────────────────
#[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),
}
}
// ─── Back-edge detection ─────────────────────────────────────────────
#[test]
fn simple_loop_back_edge() {
// B0 → B1 → B2 → B1 (back edge B2→B1)
// → B3 (exit)
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() {
// B0 → B1 → B2
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() {
// B0 → B1 (outer head) → B2 (inner head) → B3 → B2 (inner back)
// → B4 → B1 (outer back)
// B1 → B5 (outer exit)
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)))); // inner
assert!(info.back_edges.contains(&(BlockId(4), BlockId(1)))); // outer
assert_eq!(info.loop_heads.len(), 2);
assert!(info.loop_heads.contains(&BlockId(1)));
assert!(info.loop_heads.contains(&BlockId(2)));
}
// ─── Natural loop body ───────────────────────────────────────────────
#[test]
fn natural_body_simple_loop() {
// B0 → B1 → B2 → B1, B1 → B3
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))); // head
assert!(body.contains(&BlockId(2))); // body
assert!(!body.contains(&BlockId(0))); // pre-loop
assert!(!body.contains(&BlockId(3))); // post-loop
}
#[test]
fn natural_body_nested_excludes_outer() {
// Reuse the nested_loops SSA
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);
// Inner loop body: {B2, B3}
let inner = info.loop_bodies.get(&BlockId(2)).unwrap();
assert!(inner.contains(&BlockId(2)));
assert!(inner.contains(&BlockId(3)));
assert!(!inner.contains(&BlockId(1))); // outer head not in inner
assert!(!inner.contains(&BlockId(4))); // exit of inner not in inner
// Outer loop body: {B1, B2, B3, B4}
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))); // post-loop not in outer
}
// ─── Exit successor ──────────────────────────────────────────────────
#[test]
fn exit_successor_simple() {
// B1 (loop head): true→B2 (body), false→B3 (exit)
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() {
// Single-block loop: B0 → B1 → B1 (Goto back to self)
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() {
// Nested: outer head B1 branches to B2 (inner head, in outer body) and B3 (also in outer body)
// B3 → B1 (outer 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(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);
// Both B2 and B3 are in the loop body for head B1
assert_eq!(info.loop_exit_successor(&ssa, BlockId(1)), None);
}
// ─── Induction variables ─────────────────────────────────────────────
#[test]
fn induction_var_simple_counter() {
// B0: v0 = Const("0"), v2 = Const("1")
// B1: v1 = Phi((B0, v0), (B2, v3)) ← induction var
// B2: v3 = Assign([v1, v2]) ← v1 + const
// B2 → B1 (back edge)
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)), // v0
make_value_def(BlockId(1)), // v1
make_value_def(BlockId(0)), // v2
make_value_def(BlockId(2)), // v3
],
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() {
// B0: v0 = Source
// B1: v1 = Phi((B0, v0), (B2, v2))
// B2: v2 = Call("f", [v1]) ← NOT a simple increment
// B2 → B1
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)), // v0
make_value_def(BlockId(1)), // v1
make_value_def(BlockId(2)), // v2
],
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());
}
// ─── has_loops ───────────────────────────────────────────────────────
#[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());
}
}