use crate::cfg::{BlockId, Cfg, Terminator};
use petgraph::graph::NodeIndex;
use serde::{Deserialize, Serialize};
use std::collections::{HashMap, HashSet};
#[cfg(test)]
use std::time::Duration;
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct Path {
pub path_id: String,
pub blocks: Vec<BlockId>,
pub kind: PathKind,
pub entry: BlockId,
pub exit: BlockId,
}
impl Path {
pub fn new(blocks: Vec<BlockId>, kind: PathKind) -> Self {
let entry = *blocks.first().unwrap_or(&0);
let exit = *blocks.last().unwrap_or(&0);
let path_id = hash_path(&blocks);
Self {
path_id,
blocks,
kind,
entry,
exit,
}
}
pub fn with_id(path_id: String, blocks: Vec<BlockId>, kind: PathKind) -> Self {
let entry = *blocks.first().unwrap_or(&0);
let exit = *blocks.last().unwrap_or(&0);
Self {
path_id,
blocks,
kind,
entry,
exit,
}
}
pub fn len(&self) -> usize {
self.blocks.len()
}
pub fn is_empty(&self) -> bool {
self.blocks.is_empty()
}
pub fn iter(&self) -> impl Iterator<Item = &BlockId> {
self.blocks.iter()
}
pub fn contains(&self, block_id: BlockId) -> bool {
self.blocks.contains(&block_id)
}
}
impl std::fmt::Display for Path {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.path_id)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum PathKind {
Normal,
Error,
Degenerate,
Unreachable,
}
fn find_node_by_block_id(cfg: &Cfg, block_id: BlockId) -> Option<NodeIndex> {
cfg.node_indices().find(|&idx| cfg[idx].id == block_id)
}
pub fn is_feasible_path(cfg: &Cfg, blocks: &[BlockId]) -> bool {
use crate::cfg::BlockKind;
if blocks.is_empty() {
return false;
}
let first_idx = match find_node_by_block_id(cfg, blocks[0]) {
Some(idx) => idx,
None => return false, };
if cfg[first_idx].kind != BlockKind::Entry {
return false;
}
let last_idx = match find_node_by_block_id(cfg, *blocks.last().unwrap()) {
Some(idx) => idx,
None => return false, };
match &cfg[last_idx].terminator {
Terminator::Return => {} Terminator::Abort(_) => {} Terminator::Call { unwind: None, .. } => {} Terminator::Call {
unwind: Some(_),
target: Some(_),
} => {} Terminator::Unreachable
| Terminator::Goto { .. }
| Terminator::SwitchInt { .. }
| Terminator::Call {
unwind: Some(_),
target: None,
} => {
return false;
}
}
for &block_id in blocks.iter().skip(1).take(blocks.len().saturating_sub(2)) {
if find_node_by_block_id(cfg, block_id).is_none() {
return false;
}
}
true
}
pub fn is_feasible_path_precomputed(
cfg: &Cfg,
blocks: &[BlockId],
reachable_blocks: &HashSet<BlockId>,
) -> bool {
use crate::cfg::BlockKind;
if blocks.is_empty() {
return false;
}
let first_idx = match find_node_by_block_id(cfg, blocks[0]) {
Some(idx) => idx,
None => return false, };
if cfg[first_idx].kind != BlockKind::Entry {
return false;
}
for &block_id in blocks {
if !reachable_blocks.contains(&block_id) {
return false;
}
}
let last_idx = match find_node_by_block_id(cfg, *blocks.last().unwrap()) {
Some(idx) => idx,
None => return false, };
match &cfg[last_idx].terminator {
Terminator::Return => {} Terminator::Abort(_) => {} Terminator::Call { unwind: None, .. } => {} Terminator::Call {
unwind: Some(_),
target: Some(_),
} => {} Terminator::Unreachable
| Terminator::Goto { .. }
| Terminator::SwitchInt { .. }
| Terminator::Call {
unwind: Some(_),
target: None,
} => {
return false;
}
}
for &block_id in blocks.iter().skip(1).take(blocks.len().saturating_sub(2)) {
if find_node_by_block_id(cfg, block_id).is_none() {
return false;
}
}
true
}
pub fn classify_path(cfg: &Cfg, blocks: &[BlockId]) -> PathKind {
use crate::cfg::reachability::is_reachable_from_entry;
if blocks.is_empty() {
return PathKind::Degenerate;
}
for &block_id in blocks {
let node_idx = match find_node_by_block_id(cfg, block_id) {
Some(idx) => idx,
None => return PathKind::Degenerate, };
if !is_reachable_from_entry(cfg, node_idx) {
return PathKind::Unreachable;
}
let terminator = &cfg[node_idx].terminator;
match terminator {
Terminator::Abort(_) => return PathKind::Error,
Terminator::Call {
unwind: Some(_), ..
} => return PathKind::Error,
_ => {}
}
if matches!(terminator, Terminator::Unreachable) {
return PathKind::Degenerate;
}
}
if let Some(&last_block_id) = blocks.last() {
if let Some(node_idx) = find_node_by_block_id(cfg, last_block_id) {
let terminator = &cfg[node_idx].terminator;
if !matches!(terminator, Terminator::Return) {
return PathKind::Degenerate;
}
}
}
PathKind::Normal
}
pub fn classify_path_precomputed(
cfg: &Cfg,
blocks: &[BlockId],
reachable_blocks: &HashSet<BlockId>,
) -> PathKind {
if blocks.is_empty() {
return PathKind::Degenerate;
}
for &block_id in blocks {
if !reachable_blocks.contains(&block_id) {
return PathKind::Unreachable;
}
}
for &block_id in blocks {
let node_idx = match find_node_by_block_id(cfg, block_id) {
Some(idx) => idx,
None => return PathKind::Degenerate, };
let terminator = &cfg[node_idx].terminator;
match terminator {
Terminator::Abort(_) => return PathKind::Error,
Terminator::Call {
unwind: Some(_), ..
} => return PathKind::Error,
_ => {}
}
if matches!(terminator, Terminator::Unreachable) {
return PathKind::Degenerate;
}
}
if !is_feasible_path_precomputed(cfg, blocks, reachable_blocks) {
return PathKind::Degenerate;
}
PathKind::Normal
}
impl PathKind {
pub fn is_normal(&self) -> bool {
matches!(self, Self::Normal)
}
pub fn is_error(&self) -> bool {
matches!(self, Self::Error)
}
pub fn is_degenerate(&self) -> bool {
matches!(self, Self::Degenerate)
}
pub fn is_unreachable(&self) -> bool {
matches!(self, Self::Unreachable)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PathLimits {
pub max_length: usize,
pub max_paths: usize,
pub loop_unroll_limit: usize,
}
impl Default for PathLimits {
fn default() -> Self {
Self {
max_length: 1000,
max_paths: 10000,
loop_unroll_limit: 3,
}
}
}
impl PathLimits {
pub fn new(max_length: usize, max_paths: usize, loop_unroll_limit: usize) -> Self {
Self {
max_length,
max_paths,
loop_unroll_limit,
}
}
pub fn with_max_length(mut self, max_length: usize) -> Self {
self.max_length = max_length;
self
}
pub fn with_max_paths(mut self, max_paths: usize) -> Self {
self.max_paths = max_paths;
self
}
pub fn with_loop_unroll_limit(mut self, loop_unroll_limit: usize) -> Self {
self.loop_unroll_limit = loop_unroll_limit;
self
}
pub fn quick_analysis() -> Self {
Self {
max_length: 100,
max_paths: 1000,
loop_unroll_limit: 2,
}
}
pub fn thorough() -> Self {
Self {
max_length: 10000,
max_paths: 100000,
loop_unroll_limit: 5,
}
}
}
pub fn hash_path(blocks: &[BlockId]) -> String {
let mut hasher = blake3::Hasher::new();
hasher.update(&blocks.len().to_le_bytes());
for &block_id in blocks {
hasher.update(&block_id.to_le_bytes());
}
hasher.finalize().to_hex().to_string()
}
#[derive(Debug, Clone)]
pub struct EnumerationContext {
pub reachable_blocks: HashSet<BlockId>,
pub loop_headers: HashSet<NodeIndex>,
pub exits: HashSet<NodeIndex>,
}
impl EnumerationContext {
pub fn new(cfg: &Cfg) -> Self {
let reachable_nodes = crate::cfg::reachability::find_reachable(cfg);
let reachable_blocks: HashSet<BlockId> =
reachable_nodes.iter().map(|&idx| cfg[idx].id).collect();
let loop_headers = crate::cfg::loops::find_loop_headers(cfg);
let mut exits: HashSet<NodeIndex> =
crate::cfg::analysis::find_exits(cfg).into_iter().collect();
if exits.is_empty() {
for node in cfg.node_indices() {
if cfg.neighbors(node).next().is_none() {
exits.insert(node);
}
}
}
Self {
reachable_blocks,
loop_headers,
exits,
}
}
pub fn reachable_count(&self) -> usize {
self.reachable_blocks.len()
}
pub fn loop_count(&self) -> usize {
self.loop_headers.len()
}
pub fn exit_count(&self) -> usize {
self.exits.len()
}
pub fn is_reachable(&self, block_id: BlockId) -> bool {
self.reachable_blocks.contains(&block_id)
}
pub fn is_loop_header(&self, node: NodeIndex) -> bool {
self.loop_headers.contains(&node)
}
pub fn is_exit(&self, node: NodeIndex) -> bool {
self.exits.contains(&node)
}
}
pub fn enumerate_paths_with_context(
cfg: &Cfg,
limits: &PathLimits,
ctx: &EnumerationContext,
) -> Vec<Path> {
let entry = match crate::cfg::analysis::find_entry(cfg) {
Some(e) => e,
None => return vec![], };
if ctx.exits.is_empty() {
return vec![]; }
let mut paths = Vec::new();
let mut current_path = Vec::new();
let mut visited = HashSet::new();
let mut loop_iterations: HashMap<NodeIndex, usize> = HashMap::new();
dfs_enumerate_with_context(
cfg,
entry,
limits,
&mut paths,
&mut current_path,
&mut visited,
ctx,
&mut loop_iterations,
);
paths
}
fn dfs_enumerate_with_context(
cfg: &Cfg,
current: NodeIndex,
limits: &PathLimits,
paths: &mut Vec<Path>,
current_path: &mut Vec<BlockId>,
visited: &mut HashSet<NodeIndex>,
ctx: &EnumerationContext,
loop_iterations: &mut HashMap<NodeIndex, usize>,
) {
let block_id = match cfg.node_weight(current) {
Some(block) => block.id,
None => return,
};
current_path.push(block_id);
if current_path.len() > limits.max_length {
current_path.pop();
return;
}
if ctx.is_exit(current) {
let kind = classify_path_precomputed(cfg, current_path, &ctx.reachable_blocks);
let path = Path::new(current_path.clone(), kind);
paths.push(path);
current_path.pop();
return;
}
if paths.len() >= limits.max_paths {
current_path.pop();
return;
}
if visited.contains(¤t) && !ctx.is_loop_header(current) {
current_path.pop();
return;
}
visited.insert(current);
let is_loop_header = ctx.is_loop_header(current);
if is_loop_header {
let count = loop_iterations.entry(current).or_insert(0);
if *count >= limits.loop_unroll_limit {
visited.remove(¤t);
current_path.pop();
return;
}
*count += 1;
}
let neighbors: Vec<_> = cfg.neighbors(current).collect();
for next in neighbors {
dfs_enumerate_with_context(
cfg,
next,
limits,
paths,
current_path,
visited,
ctx,
loop_iterations,
);
}
if is_loop_header {
if let Some(count) = loop_iterations.get_mut(¤t) {
*count = count.saturating_sub(1);
}
}
visited.remove(¤t);
current_path.pop();
}
pub fn enumerate_paths(cfg: &Cfg, limits: &PathLimits) -> Vec<Path> {
let entry = match crate::cfg::analysis::find_entry(cfg) {
Some(e) => e,
None => return vec![], };
let mut exits: HashSet<NodeIndex> = crate::cfg::analysis::find_exits(cfg).into_iter().collect();
if exits.is_empty() {
for node in cfg.node_indices() {
if cfg.neighbors(node).next().is_none() {
exits.insert(node);
}
}
}
if exits.is_empty() {
return vec![]; }
let reachable_nodes = crate::cfg::reachability::find_reachable(cfg);
let reachable_blocks: HashSet<BlockId> =
reachable_nodes.iter().map(|&idx| cfg[idx].id).collect();
let mut paths = Vec::new();
let mut current_path = Vec::new();
let mut visited = HashSet::new();
let loop_headers = crate::cfg::loops::find_loop_headers(cfg);
let mut loop_iterations: HashMap<NodeIndex, usize> = HashMap::new();
dfs_enumerate(
cfg,
entry,
&exits,
limits,
&mut paths,
&mut current_path,
&mut visited,
&loop_headers,
&mut loop_iterations,
&reachable_blocks,
);
paths
}
fn dfs_enumerate(
cfg: &Cfg,
current: NodeIndex,
exits: &HashSet<NodeIndex>,
limits: &PathLimits,
paths: &mut Vec<Path>,
current_path: &mut Vec<BlockId>,
visited: &mut HashSet<NodeIndex>,
loop_headers: &HashSet<NodeIndex>,
loop_iterations: &mut HashMap<NodeIndex, usize>,
reachable_blocks: &HashSet<BlockId>,
) {
let block_id = match cfg.node_weight(current) {
Some(block) => block.id,
None => return,
};
current_path.push(block_id);
if current_path.len() > limits.max_length {
current_path.pop();
return;
}
if exits.contains(¤t) {
let kind = classify_path_precomputed(cfg, current_path, reachable_blocks);
let path = Path::new(current_path.clone(), kind);
paths.push(path);
current_path.pop();
return;
}
if paths.len() >= limits.max_paths {
current_path.pop();
return;
}
let is_loop_header = loop_headers.contains(¤t);
if is_loop_header {
let count = loop_iterations.entry(current).or_insert(0);
if *count >= limits.loop_unroll_limit {
current_path.pop();
return;
}
*count += 1;
}
let was_visited = visited.insert(current);
let mut successors: Vec<NodeIndex> = cfg.neighbors(current).collect();
successors.sort_by_key(|n| n.index());
if successors.is_empty() {
let kind = classify_path_precomputed(cfg, current_path, reachable_blocks);
let path = Path::new(current_path.clone(), kind);
paths.push(path);
} else {
for succ in successors {
let is_back_edge = loop_headers.contains(&succ) && loop_iterations.contains_key(&succ);
if visited.contains(&succ) && !is_back_edge {
continue;
}
if is_back_edge {
let count = loop_iterations.get(&succ).copied().unwrap_or(0);
if count >= limits.loop_unroll_limit {
continue; }
}
dfs_enumerate(
cfg,
succ,
exits,
limits,
paths,
current_path,
visited,
loop_headers,
loop_iterations,
reachable_blocks,
);
if paths.len() >= limits.max_paths {
break;
}
}
}
if was_visited {
visited.remove(¤t);
}
if is_loop_header {
loop_iterations.entry(current).and_modify(|c| *c -= 1);
}
current_path.pop();
}
pub fn enumerate_paths_iterative(cfg: &Cfg, limits: &PathLimits) -> Vec<Path> {
use std::collections::BTreeSet;
let entry = match crate::cfg::analysis::find_entry(cfg) {
Some(e) => e,
None => return vec![],
};
let mut exits: HashSet<NodeIndex> = crate::cfg::analysis::find_exits(cfg).into_iter().collect();
if exits.is_empty() {
for node in cfg.node_indices() {
if cfg.neighbors(node).next().is_none() {
exits.insert(node);
}
}
}
if exits.is_empty() {
return vec![]; }
let reachable_nodes = crate::cfg::reachability::find_reachable(cfg);
let reachable_blocks: HashSet<BlockId> =
reachable_nodes.iter().map(|&idx| cfg[idx].id).collect();
let loop_headers = crate::cfg::loops::find_loop_headers(cfg);
let mut seen_paths: BTreeSet<Vec<BlockId>> = BTreeSet::new();
struct StackFrame {
node: NodeIndex,
path: Vec<BlockId>,
visited: HashSet<NodeIndex>,
loop_iterations: HashMap<NodeIndex, usize>,
}
let mut stack = Vec::new();
let mut paths = Vec::new();
let entry_block_id = cfg[entry].id;
let mut initial_visited = HashSet::new();
initial_visited.insert(entry);
stack.push(StackFrame {
node: entry,
path: vec![entry_block_id],
visited: initial_visited,
loop_iterations: HashMap::new(),
});
while let Some(frame) = stack.pop() {
let StackFrame {
node: current,
path: current_path,
visited: current_visited,
mut loop_iterations,
} = frame;
if current_path.len() > limits.max_length {
continue;
}
if exits.contains(¤t) {
if seen_paths.insert(current_path.clone()) {
let kind = classify_path_precomputed(cfg, ¤t_path, &reachable_blocks);
let path = Path::new(current_path, kind);
paths.push(path);
}
continue;
}
if paths.len() >= limits.max_paths {
break;
}
let mut successors: Vec<NodeIndex> = cfg.neighbors(current).collect();
successors.sort_by_key(|n| n.index());
for succ in successors {
let is_back_edge = loop_headers.contains(&succ)
&& loop_iterations.get(&succ).copied().unwrap_or(0) < limits.loop_unroll_limit;
if current_visited.contains(&succ) && !is_back_edge {
continue;
}
if is_back_edge {
let count = loop_iterations.entry(succ).or_insert(0);
if *count >= limits.loop_unroll_limit {
continue;
}
*count += 1;
}
let mut new_path = current_path.clone();
let block_id = cfg[succ].id;
new_path.push(block_id);
let mut new_visited = current_visited.clone();
new_visited.insert(succ);
stack.push(StackFrame {
node: succ,
path: new_path,
visited: new_visited,
loop_iterations: loop_iterations.clone(),
});
}
}
paths
}
#[derive(Debug, Clone)]
pub struct PathEnumerationResult {
pub paths: Vec<Path>,
pub limits_hit: LimitsHit,
pub stats: EnumerationStats,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum LimitsHit {
None,
MaxLength,
MaxPaths,
LoopUnroll,
Multiple,
}
#[derive(Debug, Clone)]
pub struct EnumerationStats {
pub total_paths: usize,
pub normal_paths: usize,
pub error_paths: usize,
pub degenerate_paths: usize,
pub unreachable_paths: usize,
pub avg_path_length: f64,
pub max_path_length: usize,
pub loop_count: usize,
}
pub fn enumerate_paths_with_metadata(cfg: &Cfg, limits: &PathLimits) -> PathEnumerationResult {
let paths = enumerate_paths_iterative(cfg, limits);
let mut stats = EnumerationStats {
total_paths: paths.len(),
normal_paths: 0,
error_paths: 0,
degenerate_paths: 0,
unreachable_paths: 0,
avg_path_length: 0.0,
max_path_length: 0,
loop_count: 0,
};
if paths.is_empty() {
return PathEnumerationResult {
paths,
limits_hit: LimitsHit::None,
stats,
};
}
let mut total_length = 0;
for path in &paths {
match path.kind {
PathKind::Normal => stats.normal_paths += 1,
PathKind::Error => stats.error_paths += 1,
PathKind::Degenerate => stats.degenerate_paths += 1,
PathKind::Unreachable => stats.unreachable_paths += 1,
}
let len = path.len();
total_length += len;
if len > stats.max_path_length {
stats.max_path_length = len;
}
}
stats.avg_path_length = total_length as f64 / paths.len() as f64;
stats.loop_count = crate::cfg::loops::find_loop_headers(cfg).len();
let limits_hit = if paths.len() >= limits.max_paths {
LimitsHit::MaxPaths
} else if stats.max_path_length >= limits.max_length {
LimitsHit::MaxLength
} else {
LimitsHit::None
};
PathEnumerationResult {
paths,
limits_hit,
stats,
}
}
pub fn get_or_enumerate_paths(
cfg: &Cfg,
function_id: i64,
function_hash: &str,
limits: &PathLimits,
db_conn: &mut rusqlite::Connection,
) -> Result<Vec<Path>, String> {
use crate::storage::paths::{get_cached_paths, invalidate_function_paths, store_paths};
let current_hash: Option<String> = db_conn
.query_row(
"SELECT cfg_hash FROM cfg_blocks WHERE function_id = ?1 LIMIT 1",
rusqlite::params![function_id],
|row| row.get(0),
)
.unwrap_or(None);
if let Some(ref hash) = current_hash {
if hash == function_hash {
let paths = get_cached_paths(db_conn, function_id)
.map_err(|e| format!("Failed to retrieve cached paths: {}", e))?;
if !paths.is_empty() {
return Ok(paths);
}
}
}
let paths = enumerate_paths(cfg, limits);
let _ = invalidate_function_paths(db_conn, function_id);
store_paths(db_conn, function_id, &paths)
.map_err(|e| format!("Failed to store enumerated paths: {}", e))?;
Ok(paths)
}
pub fn enumerate_paths_cached(
cfg: &Cfg,
function_id: i64,
function_hash: &str,
limits: &PathLimits,
db_conn: &mut rusqlite::Connection,
) -> Result<Vec<Path>, String> {
use crate::storage::paths::{get_cached_paths, update_function_paths_if_changed};
let current_hash: Option<String> = db_conn
.query_row(
"SELECT cfg_hash FROM cfg_blocks WHERE function_id = ?1 LIMIT 1",
rusqlite::params![function_id],
|row| row.get(0),
)
.unwrap_or(None);
if let Some(ref hash) = current_hash {
if hash == function_hash {
let paths = get_cached_paths(db_conn, function_id)
.map_err(|e| format!("Failed to retrieve cached paths: {}", e))?;
return Ok(paths);
}
}
let ctx = EnumerationContext::new(cfg);
let paths = enumerate_paths_with_context(cfg, limits, &ctx);
update_function_paths_if_changed(db_conn, function_id, function_hash, &paths)
.map_err(|e| format!("Failed to store enumerated paths: {}", e))?;
Ok(paths)
}
pub fn enumerate_paths_cached_with_context(
cfg: &Cfg,
function_id: i64,
function_hash: &str,
limits: &PathLimits,
ctx: &EnumerationContext,
db_conn: &mut rusqlite::Connection,
) -> Result<Vec<Path>, String> {
use crate::storage::paths::{get_cached_paths, update_function_paths_if_changed};
let current_hash: Option<String> = db_conn
.query_row(
"SELECT cfg_hash FROM cfg_blocks WHERE function_id = ?1 LIMIT 1",
rusqlite::params![function_id],
|row| row.get(0),
)
.unwrap_or(None);
if let Some(ref hash) = current_hash {
if hash == function_hash {
return get_cached_paths(db_conn, function_id)
.map_err(|e| format!("Failed to retrieve cached paths: {}", e));
}
}
let paths = enumerate_paths_with_context(cfg, limits, ctx);
update_function_paths_if_changed(db_conn, function_id, function_hash, &paths)
.map_err(|e| format!("Failed to store enumerated paths: {}", e))?;
Ok(paths)
}
pub fn estimate_path_count(cfg: &Cfg, loop_unroll_limit: usize) -> usize {
let loop_headers = crate::cfg::loops::find_loop_headers(cfg);
let loop_count = loop_headers.len();
let mut branch_count = 0;
for node in cfg.node_indices() {
if loop_headers.contains(&node) {
continue; }
let out_degree = cfg
.neighbors_directed(node, petgraph::Direction::Outgoing)
.count();
if out_degree > 1 {
branch_count += out_degree - 1; }
}
if loop_count == 0 && branch_count == 0 {
return 1; }
let unroll_factor = loop_unroll_limit + 1;
let branch_factor = if branch_count < 31 {
2_usize.pow(branch_count as u32)
} else {
usize::MAX / 2 };
let loop_factor = if loop_count < 31 {
unroll_factor.pow(loop_count as u32)
} else {
usize::MAX / 2 };
branch_factor.saturating_mul(loop_factor)
}
pub fn check_path_explosion(cfg: &Cfg, limits: &PathLimits) -> Option<usize> {
let estimate = estimate_path_count(cfg, limits.loop_unroll_limit);
if estimate > limits.max_paths {
Some(estimate)
} else {
None
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::cfg::{BasicBlock, BlockKind, EdgeType, Terminator};
use petgraph::graph::DiGraph;
fn create_linear_cfg() -> Cfg {
let mut g = DiGraph::new();
let b0 = g.add_node(BasicBlock {
id: 0,
db_id: None,
kind: BlockKind::Entry,
statements: vec![],
terminator: Terminator::Goto { target: 1 },
source_location: None,
coord_x: 0,
coord_y: 0,
coord_z: 0,
});
let b1 = g.add_node(BasicBlock {
id: 1,
db_id: None,
kind: BlockKind::Normal,
statements: vec![],
terminator: Terminator::Goto { target: 2 },
source_location: None,
coord_x: 0,
coord_y: 0,
coord_z: 0,
});
let b2 = g.add_node(BasicBlock {
id: 2,
db_id: None,
kind: BlockKind::Exit,
statements: vec![],
terminator: Terminator::Return,
source_location: None,
coord_x: 0,
coord_y: 0,
coord_z: 0,
});
g.add_edge(b0, b1, EdgeType::Fallthrough);
g.add_edge(b1, b2, EdgeType::Fallthrough);
g
}
fn create_diamond_cfg() -> Cfg {
let mut g = DiGraph::new();
let b0 = g.add_node(BasicBlock {
id: 0,
db_id: None,
kind: BlockKind::Entry,
statements: vec![],
terminator: Terminator::SwitchInt {
targets: vec![1],
otherwise: 2,
},
source_location: None,
coord_x: 0,
coord_y: 0,
coord_z: 0,
});
let b1 = g.add_node(BasicBlock {
id: 1,
db_id: None,
kind: BlockKind::Normal,
statements: vec![],
terminator: Terminator::Goto { target: 3 },
source_location: None,
coord_x: 0,
coord_y: 0,
coord_z: 0,
});
let b2 = g.add_node(BasicBlock {
id: 2,
db_id: None,
kind: BlockKind::Normal,
statements: vec![],
terminator: Terminator::Goto { target: 3 },
source_location: None,
coord_x: 0,
coord_y: 0,
coord_z: 0,
});
let b3 = g.add_node(BasicBlock {
id: 3,
db_id: None,
kind: BlockKind::Exit,
statements: vec![],
terminator: Terminator::Return,
source_location: None,
coord_x: 0,
coord_y: 0,
coord_z: 0,
});
g.add_edge(b0, b1, EdgeType::TrueBranch);
g.add_edge(b0, b2, EdgeType::FalseBranch);
g.add_edge(b1, b3, EdgeType::Fallthrough);
g.add_edge(b2, b3, EdgeType::Fallthrough);
g
}
fn create_loop_cfg() -> Cfg {
let mut g = DiGraph::new();
let b0 = g.add_node(BasicBlock {
id: 0,
db_id: None,
kind: BlockKind::Entry,
statements: vec![],
terminator: Terminator::Goto { target: 1 },
source_location: None,
coord_x: 0,
coord_y: 0,
coord_z: 0,
});
let b1 = g.add_node(BasicBlock {
id: 1,
db_id: None,
kind: BlockKind::Normal,
statements: vec![],
terminator: Terminator::SwitchInt {
targets: vec![2],
otherwise: 3,
},
source_location: None,
coord_x: 0,
coord_y: 0,
coord_z: 0,
});
let b2 = g.add_node(BasicBlock {
id: 2,
db_id: None,
kind: BlockKind::Normal,
statements: vec![],
terminator: Terminator::Goto { target: 1 },
source_location: None,
coord_x: 0,
coord_y: 0,
coord_z: 0,
});
let b3 = g.add_node(BasicBlock {
id: 3,
db_id: None,
kind: BlockKind::Exit,
statements: vec![],
terminator: Terminator::Return,
source_location: None,
coord_x: 0,
coord_y: 0,
coord_z: 0,
});
g.add_edge(b0, b1, EdgeType::Fallthrough);
g.add_edge(b1, b2, EdgeType::TrueBranch);
g.add_edge(b1, b3, EdgeType::FalseBranch);
g.add_edge(b2, b1, EdgeType::LoopBack);
g
}
#[test]
fn test_hash_path_deterministic() {
let blocks = vec![0, 1, 2];
let hash1 = hash_path(&blocks);
let hash2 = hash_path(&blocks);
assert_eq!(hash1, hash2, "Same input should produce same hash");
}
#[test]
fn test_hash_path_different_sequences() {
let blocks1 = vec![0, 1, 2];
let blocks2 = vec![0, 2, 1];
assert_ne!(hash_path(&blocks1), hash_path(&blocks2));
}
#[test]
fn test_hash_path_length_collision_protection() {
let blocks1 = vec![1, 2, 3];
let blocks2 = vec![1, 2, 3, 0];
assert_ne!(hash_path(&blocks1), hash_path(&blocks2));
}
#[test]
fn test_path_new() {
let blocks = vec![0, 1, 2];
let path = Path::new(blocks.clone(), PathKind::Normal);
assert_eq!(path.blocks, blocks);
assert_eq!(path.entry, 0);
assert_eq!(path.exit, 2);
assert_eq!(path.kind, PathKind::Normal);
assert!(!path.path_id.is_empty());
}
#[test]
fn test_path_len() {
let blocks = vec![0, 1, 2];
let path = Path::new(blocks, PathKind::Normal);
assert_eq!(path.len(), 3);
assert!(!path.is_empty());
}
#[test]
fn test_path_contains() {
let blocks = vec![0, 1, 2];
let path = Path::new(blocks, PathKind::Normal);
assert!(path.contains(0));
assert!(path.contains(1));
assert!(path.contains(2));
assert!(!path.contains(3));
}
#[test]
fn test_path_limits_default() {
let limits = PathLimits::default();
assert_eq!(limits.max_length, 1000);
assert_eq!(limits.max_paths, 10000);
assert_eq!(limits.loop_unroll_limit, 3);
}
#[test]
fn test_path_limits_custom() {
let limits = PathLimits::new(100, 500, 5);
assert_eq!(limits.max_length, 100);
assert_eq!(limits.max_paths, 500);
assert_eq!(limits.loop_unroll_limit, 5);
}
#[test]
fn test_path_limits_builder() {
let limits = PathLimits::default()
.with_max_length(200)
.with_max_paths(1000)
.with_loop_unroll_limit(10);
assert_eq!(limits.max_length, 200);
assert_eq!(limits.max_paths, 1000);
assert_eq!(limits.loop_unroll_limit, 10);
}
#[test]
fn test_path_kind_is_normal() {
assert!(PathKind::Normal.is_normal());
assert!(!PathKind::Error.is_normal());
assert!(!PathKind::Degenerate.is_normal());
assert!(!PathKind::Unreachable.is_normal());
}
#[test]
fn test_path_kind_is_error() {
assert!(PathKind::Error.is_error());
assert!(!PathKind::Normal.is_error());
}
#[test]
fn test_path_kind_is_degenerate() {
assert!(PathKind::Degenerate.is_degenerate());
assert!(!PathKind::Normal.is_degenerate());
}
#[test]
fn test_path_kind_is_unreachable() {
assert!(PathKind::Unreachable.is_unreachable());
assert!(!PathKind::Normal.is_unreachable());
}
#[test]
fn test_find_node_by_block_id_existing() {
let cfg = create_linear_cfg();
let b0 = find_node_by_block_id(&cfg, 0);
let b1 = find_node_by_block_id(&cfg, 1);
let b2 = find_node_by_block_id(&cfg, 2);
assert!(b0.is_some());
assert!(b1.is_some());
assert!(b2.is_some());
assert_eq!(b0.unwrap().index(), 0);
assert_eq!(b1.unwrap().index(), 1);
assert_eq!(b2.unwrap().index(), 2);
}
#[test]
fn test_find_node_by_block_id_nonexistent() {
let cfg = create_linear_cfg();
let b99 = find_node_by_block_id(&cfg, 99);
assert!(b99.is_none());
}
#[test]
fn test_find_node_by_block_id_empty_cfg() {
let cfg: Cfg = DiGraph::new();
let b0 = find_node_by_block_id(&cfg, 0);
assert!(b0.is_none());
}
fn create_error_cfg() -> Cfg {
let mut g = DiGraph::new();
let b0 = g.add_node(BasicBlock {
id: 0,
db_id: None,
kind: BlockKind::Entry,
statements: vec![],
terminator: Terminator::Goto { target: 1 },
source_location: None,
coord_x: 0,
coord_y: 0,
coord_z: 0,
});
let b1 = g.add_node(BasicBlock {
id: 1,
db_id: None,
kind: BlockKind::Exit,
statements: vec![],
terminator: Terminator::Abort("panic!".to_string()),
source_location: None,
coord_x: 0,
coord_y: 0,
coord_z: 0,
});
g.add_edge(b0, b1, EdgeType::Fallthrough);
g
}
fn create_unreachable_term_cfg() -> Cfg {
let mut g = DiGraph::new();
let b0 = g.add_node(BasicBlock {
id: 0,
db_id: None,
kind: BlockKind::Entry,
statements: vec![],
terminator: Terminator::Goto { target: 1 },
source_location: None,
coord_x: 0,
coord_y: 0,
coord_z: 0,
});
let b1 = g.add_node(BasicBlock {
id: 1,
db_id: None,
kind: BlockKind::Exit,
statements: vec![],
terminator: Terminator::Unreachable,
source_location: None,
coord_x: 0,
coord_y: 0,
coord_z: 0,
});
g.add_edge(b0, b1, EdgeType::Fallthrough);
g
}
fn create_dead_code_cfg() -> Cfg {
let mut g = DiGraph::new();
let _b0 = g.add_node(BasicBlock {
id: 0,
db_id: None,
kind: BlockKind::Entry,
statements: vec![],
terminator: Terminator::Return,
source_location: None,
coord_x: 0,
coord_y: 0,
coord_z: 0,
});
let _b1 = g.add_node(BasicBlock {
id: 1,
db_id: None,
kind: BlockKind::Exit,
statements: vec![],
terminator: Terminator::Return,
source_location: None,
coord_x: 0,
coord_y: 0,
coord_z: 0,
});
g
}
fn create_call_unwind_cfg() -> Cfg {
let mut g = DiGraph::new();
let b0 = g.add_node(BasicBlock {
id: 0,
db_id: None,
kind: BlockKind::Entry,
statements: vec![],
terminator: Terminator::Call {
target: Some(1),
unwind: Some(2), },
source_location: None,
coord_x: 0,
coord_y: 0,
coord_z: 0,
});
let b1 = g.add_node(BasicBlock {
id: 1,
db_id: None,
kind: BlockKind::Exit,
statements: vec![],
terminator: Terminator::Return,
source_location: None,
coord_x: 0,
coord_y: 0,
coord_z: 0,
});
let _b2 = g.add_node(BasicBlock {
id: 2,
db_id: None,
kind: BlockKind::Exit,
statements: vec![],
terminator: Terminator::Return,
source_location: None,
coord_x: 0,
coord_y: 0,
coord_z: 0,
});
g.add_edge(b0, b1, EdgeType::Fallthrough);
g
}
#[test]
fn test_classify_path_normal_return() {
let cfg = create_linear_cfg();
let path = vec![0, 1, 2];
let kind = classify_path(&cfg, &path);
assert_eq!(kind, PathKind::Normal);
}
#[test]
fn test_classify_path_error_abort() {
let cfg = create_error_cfg();
let path = vec![0, 1];
let kind = classify_path(&cfg, &path);
assert_eq!(kind, PathKind::Error);
}
#[test]
fn test_classify_path_degenerate_unreachable_terminator() {
let cfg = create_unreachable_term_cfg();
let path = vec![0, 1];
let kind = classify_path(&cfg, &path);
assert_eq!(kind, PathKind::Degenerate);
}
#[test]
fn test_classify_path_unreachable_block() {
let cfg = create_dead_code_cfg();
let path = vec![1];
let kind = classify_path(&cfg, &path);
assert_eq!(kind, PathKind::Unreachable);
}
#[test]
fn test_classify_path_error_call_unwind() {
let cfg = create_call_unwind_cfg();
let path = vec![0, 1];
let kind = classify_path(&cfg, &path);
assert_eq!(kind, PathKind::Error);
}
#[test]
fn test_classify_path_empty() {
let cfg = create_linear_cfg();
let path: Vec<BlockId> = vec![];
let kind = classify_path(&cfg, &path);
assert_eq!(kind, PathKind::Degenerate);
}
#[test]
fn test_classify_path_single_block() {
let mut g = DiGraph::new();
let _b0 = g.add_node(BasicBlock {
id: 0,
db_id: None,
kind: BlockKind::Entry,
statements: vec![],
terminator: Terminator::Return,
source_location: None,
coord_x: 0,
coord_y: 0,
coord_z: 0,
});
let path = vec![0];
let kind = classify_path(&g, &path);
assert_eq!(kind, PathKind::Normal);
}
#[test]
fn test_classify_path_nonexistent_block() {
let cfg = create_linear_cfg();
let path = vec![0, 99];
let kind = classify_path(&cfg, &path);
assert_eq!(kind, PathKind::Degenerate);
}
#[test]
fn test_classify_path_precomputed_matches_classify_path() {
let cfg = create_diamond_cfg();
use crate::cfg::reachability::find_reachable;
let reachable_nodes = find_reachable(&cfg);
let reachable_blocks: HashSet<BlockId> =
reachable_nodes.iter().map(|&idx| cfg[idx].id).collect();
let test_paths = vec![vec![0, 1, 3], vec![0, 2, 3], vec![0, 1], vec![0]];
for path in test_paths {
let kind1 = classify_path(&cfg, &path);
let kind2 = classify_path_precomputed(&cfg, &path, &reachable_blocks);
assert_eq!(
kind1, kind2,
"classify_path_precomputed should match classify_path for path {:?}",
path
);
}
}
#[test]
fn test_classify_path_precomputed_unreachable() {
let cfg = create_dead_code_cfg();
use crate::cfg::reachability::find_reachable;
let reachable_nodes = find_reachable(&cfg);
let reachable_blocks: HashSet<BlockId> =
reachable_nodes.iter().map(|&idx| cfg[idx].id).collect();
let path = vec![1];
let kind = classify_path_precomputed(&cfg, &path, &reachable_blocks);
assert_eq!(kind, PathKind::Unreachable);
}
#[test]
fn test_classify_path_precomputed_performance() {
use crate::cfg::reachability::find_reachable;
use std::time::Instant;
let cfg = create_diamond_cfg();
let reachable_nodes = find_reachable(&cfg);
let reachable_blocks: HashSet<BlockId> =
reachable_nodes.iter().map(|&idx| cfg[idx].id).collect();
let test_paths: Vec<Vec<BlockId>> = (0..1000).map(|_| vec![0, 1, 3]).collect();
let start = Instant::now();
for path in &test_paths {
let _ = classify_path_precomputed(&cfg, path, &reachable_blocks);
}
let precomputed_duration = start.elapsed();
assert!(
precomputed_duration.as_millis() < 10,
"classify_path_precomputed should classify 1000 paths in <10ms, took {}ms",
precomputed_duration.as_millis()
);
}
#[test]
fn test_classify_path_precomputed_all_kinds() {
use crate::cfg::reachability::find_reachable;
let cfg_normal = create_linear_cfg();
let reachable = find_reachable(&cfg_normal)
.iter()
.map(|&idx| cfg_normal[idx].id)
.collect();
assert_eq!(
classify_path_precomputed(&cfg_normal, &[0, 1, 2], &reachable),
PathKind::Normal
);
let cfg_error = create_error_cfg();
let reachable = find_reachable(&cfg_error)
.iter()
.map(|&idx| cfg_error[idx].id)
.collect();
assert_eq!(
classify_path_precomputed(&cfg_error, &[0, 1], &reachable),
PathKind::Error
);
let cfg_degen = create_unreachable_term_cfg();
let reachable = find_reachable(&cfg_degen)
.iter()
.map(|&idx| cfg_degen[idx].id)
.collect();
assert_eq!(
classify_path_precomputed(&cfg_degen, &[0, 1], &reachable),
PathKind::Degenerate
);
}
#[test]
fn test_enumerate_paths_linear_cfg() {
let cfg = create_linear_cfg();
let paths = enumerate_paths(&cfg, &PathLimits::default());
assert_eq!(paths.len(), 1);
assert_eq!(paths[0].blocks, vec![0, 1, 2]);
assert_eq!(paths[0].entry, 0);
assert_eq!(paths[0].exit, 2);
assert_eq!(paths[0].kind, PathKind::Normal);
}
#[test]
fn test_enumerate_paths_diamond_cfg() {
let cfg = create_diamond_cfg();
let paths = enumerate_paths(&cfg, &PathLimits::default());
assert_eq!(paths.len(), 2);
for path in &paths {
assert_eq!(path.entry, 0);
assert_eq!(path.exit, 3);
assert_eq!(path.kind, PathKind::Normal);
}
let path_blocks: Vec<_> = paths.iter().map(|p| p.blocks.clone()).collect();
assert!(path_blocks.contains(&vec![0, 1, 3]));
assert!(path_blocks.contains(&vec![0, 2, 3]));
}
#[test]
fn test_enumerate_paths_loop_with_unroll_limit() {
let cfg = create_loop_cfg();
let limits = PathLimits::default().with_loop_unroll_limit(3);
let paths = enumerate_paths(&cfg, &limits);
assert!(
paths.len() >= 2,
"Should have at least direct exit and one loop iteration"
);
assert!(paths.len() <= 5, "Should be bounded by loop unroll limit");
for path in &paths {
assert_eq!(path.kind, PathKind::Normal);
assert_eq!(path.entry, 0);
assert_eq!(path.exit, 3);
}
assert!(paths.iter().any(|p| p.blocks == vec![0, 1, 3]));
}
#[test]
fn test_enumerate_paths_empty_cfg() {
let cfg: Cfg = DiGraph::new();
let paths = enumerate_paths(&cfg, &PathLimits::default());
assert_eq!(paths.len(), 0);
}
#[test]
fn test_enumerate_paths_max_paths_limit() {
let cfg = create_diamond_cfg();
let limits = PathLimits::default().with_max_paths(1);
let paths = enumerate_paths(&cfg, &limits);
assert_eq!(paths.len(), 1);
}
#[test]
fn test_enumerate_paths_max_length_limit() {
let cfg = create_diamond_cfg();
let limits = PathLimits::default().with_max_length(2);
let paths = enumerate_paths(&cfg, &limits);
assert_eq!(paths.len(), 0);
}
#[test]
fn test_enumerate_paths_single_block_cfg() {
let mut g = DiGraph::new();
let _b0 = g.add_node(BasicBlock {
id: 0,
db_id: None,
kind: BlockKind::Entry,
statements: vec![],
terminator: Terminator::Return,
source_location: None,
coord_x: 0,
coord_y: 0,
coord_z: 0,
});
let paths = enumerate_paths(&g, &PathLimits::default());
assert_eq!(paths.len(), 1);
assert_eq!(paths[0].blocks, vec![0]);
assert_eq!(paths[0].entry, 0);
assert_eq!(paths[0].exit, 0);
}
#[test]
fn test_enumerate_paths_with_unreachable_exit() {
let mut g = DiGraph::new();
let b0 = g.add_node(BasicBlock {
id: 0,
db_id: None,
kind: BlockKind::Entry,
statements: vec![],
terminator: Terminator::Goto { target: 1 },
source_location: None,
coord_x: 0,
coord_y: 0,
coord_z: 0,
});
let b1 = g.add_node(BasicBlock {
id: 1,
db_id: None,
kind: BlockKind::Exit,
statements: vec![],
terminator: Terminator::Return,
source_location: None,
coord_x: 0,
coord_y: 0,
coord_z: 0,
});
let _b2 = g.add_node(BasicBlock {
id: 2,
db_id: None,
kind: BlockKind::Exit,
statements: vec![],
terminator: Terminator::Unreachable,
source_location: None,
coord_x: 0,
coord_y: 0,
coord_z: 0,
});
g.add_edge(b0, b1, EdgeType::Fallthrough);
let paths = enumerate_paths(&g, &PathLimits::default());
assert_eq!(paths.len(), 1);
assert_eq!(paths[0].blocks, vec![0, 1]);
}
#[test]
fn test_enumerate_paths_classification_diamond() {
let cfg = create_diamond_cfg();
let paths = enumerate_paths(&cfg, &PathLimits::default());
assert_eq!(paths.len(), 2);
for path in &paths {
assert_eq!(
path.kind,
PathKind::Normal,
"Diamond CFG should only have Normal paths, got {:?} for {:?}",
path.kind,
path.blocks
);
}
}
#[test]
fn test_enumerate_paths_classification_with_error() {
let cfg = create_error_cfg();
let paths = enumerate_paths(&cfg, &PathLimits::default());
assert_eq!(paths.len(), 1);
assert_eq!(paths[0].kind, PathKind::Error);
assert_eq!(paths[0].blocks, vec![0, 1]);
}
#[test]
fn test_enumerate_paths_classification_with_unreachable() {
let cfg = create_dead_code_cfg();
let paths = enumerate_paths(&cfg, &PathLimits::default());
assert_eq!(paths.len(), 1);
assert_eq!(paths[0].blocks, vec![0]);
assert_eq!(paths[0].kind, PathKind::Normal);
}
#[test]
fn test_enumerate_paths_classification_mixed() {
let mut g = DiGraph::new();
let b0 = g.add_node(BasicBlock {
id: 0,
db_id: None,
kind: BlockKind::Entry,
statements: vec![],
terminator: Terminator::SwitchInt {
targets: vec![1],
otherwise: 2,
},
source_location: None,
coord_x: 0,
coord_y: 0,
coord_z: 0,
});
let b1 = g.add_node(BasicBlock {
id: 1,
db_id: None,
kind: BlockKind::Exit,
statements: vec![],
terminator: Terminator::Return,
source_location: None,
coord_x: 0,
coord_y: 0,
coord_z: 0,
});
let b2 = g.add_node(BasicBlock {
id: 2,
db_id: None,
kind: BlockKind::Exit,
statements: vec![],
terminator: Terminator::Abort("panic!".to_string()),
source_location: None,
coord_x: 0,
coord_y: 0,
coord_z: 0,
});
g.add_edge(b0, b1, EdgeType::TrueBranch);
g.add_edge(b0, b2, EdgeType::FalseBranch);
let paths = enumerate_paths(&g, &PathLimits::default());
assert_eq!(paths.len(), 2);
let normal_count = paths.iter().filter(|p| p.kind == PathKind::Normal).count();
let error_count = paths.iter().filter(|p| p.kind == PathKind::Error).count();
assert_eq!(normal_count, 1, "Should have 1 Normal path");
assert_eq!(error_count, 1, "Should have 1 Error path");
}
#[test]
fn test_enumerate_paths_classification_correctness() {
let cfg = create_diamond_cfg();
let paths = enumerate_paths(&cfg, &PathLimits::default());
use crate::cfg::reachability::find_reachable;
let reachable_nodes = find_reachable(&cfg);
let reachable_blocks: HashSet<BlockId> =
reachable_nodes.iter().map(|&idx| cfg[idx].id).collect();
for path in &paths {
let expected_kind = classify_path_precomputed(&cfg, &path.blocks, &reachable_blocks);
assert_eq!(
path.kind, expected_kind,
"Path kind mismatch for {:?}: got {:?}, expected {:?}",
path.blocks, path.kind, expected_kind
);
}
}
#[test]
fn test_enumerate_paths_try_operator_self_loop() {
let mut g = DiGraph::new();
let b0 = g.add_node(BasicBlock {
id: 0,
db_id: None,
kind: BlockKind::Entry,
statements: vec![],
terminator: Terminator::Goto { target: 1 },
source_location: None,
coord_x: 0,
coord_y: 0,
coord_z: 0,
});
let b1 = g.add_node(BasicBlock {
id: 1,
db_id: None,
kind: BlockKind::Normal,
statements: vec![],
terminator: Terminator::SwitchInt {
targets: vec![2],
otherwise: 2,
},
source_location: None,
coord_x: 0,
coord_y: 0,
coord_z: 0,
});
let b2 = g.add_node(BasicBlock {
id: 2,
db_id: None,
kind: BlockKind::Normal,
statements: vec![],
terminator: Terminator::Goto { target: 3 },
source_location: None,
coord_x: 0,
coord_y: 0,
coord_z: 0,
});
let b3 = g.add_node(BasicBlock {
id: 3,
db_id: None,
kind: BlockKind::Exit,
statements: vec![],
terminator: Terminator::Return,
source_location: None,
coord_x: 0,
coord_y: 0,
coord_z: 0,
});
g.add_edge(b0, b1, EdgeType::Fallthrough);
g.add_edge(b1, b1, EdgeType::Return); g.add_edge(b1, b2, EdgeType::Fallthrough); g.add_edge(b2, b3, EdgeType::Fallthrough);
let paths = enumerate_paths(&g, &PathLimits::default());
assert!(
!paths.is_empty(),
"Should find at least one path through try operator CFG, found 0"
);
let ok_path: Vec<usize> = vec![0, 1, 2, 3];
let has_ok = paths.iter().any(|p| p.blocks == ok_path);
assert!(has_ok, "Should find Ok path {:?}", ok_path);
}
#[test]
fn test_enumerate_paths_implicit_return_dead_end() {
let mut g = DiGraph::new();
let b0 = g.add_node(BasicBlock {
id: 0,
db_id: None,
kind: BlockKind::Entry,
statements: vec![],
terminator: Terminator::Goto { target: 1 },
source_location: None,
coord_x: 0,
coord_y: 0,
coord_z: 0,
});
let b1 = g.add_node(BasicBlock {
id: 1,
db_id: None,
kind: BlockKind::Normal,
statements: vec![],
terminator: Terminator::Goto { target: 2 },
source_location: None,
coord_x: 0,
coord_y: 0,
coord_z: 0,
});
let b2 = g.add_node(BasicBlock {
id: 2,
db_id: None,
kind: BlockKind::Normal,
statements: vec![],
terminator: Terminator::Goto { target: 0 }, source_location: None,
coord_x: 0,
coord_y: 0,
coord_z: 0,
});
g.add_edge(b0, b1, EdgeType::Fallthrough);
g.add_edge(b1, b2, EdgeType::Fallthrough);
let paths = enumerate_paths(&g, &PathLimits::default());
assert_eq!(
paths.len(),
1,
"Should find 1 path via dead-end fallback, found {}",
paths.len()
);
assert_eq!(paths[0].blocks, vec![0, 1, 2]);
}
#[test]
fn test_path_limits_max_length_long_path() {
let mut g = DiGraph::new();
let b0 = g.add_node(BasicBlock {
id: 0,
db_id: None,
kind: BlockKind::Entry,
statements: vec![],
terminator: Terminator::Goto { target: 1 },
source_location: None,
coord_x: 0,
coord_y: 0,
coord_z: 0,
});
let b1 = g.add_node(BasicBlock {
id: 1,
db_id: None,
kind: BlockKind::Normal,
statements: vec![],
terminator: Terminator::Goto { target: 2 },
source_location: None,
coord_x: 0,
coord_y: 0,
coord_z: 0,
});
let b2 = g.add_node(BasicBlock {
id: 2,
db_id: None,
kind: BlockKind::Normal,
statements: vec![],
terminator: Terminator::Goto { target: 3 },
source_location: None,
coord_x: 0,
coord_y: 0,
coord_z: 0,
});
let b3 = g.add_node(BasicBlock {
id: 3,
db_id: None,
kind: BlockKind::Normal,
statements: vec![],
terminator: Terminator::Goto { target: 4 },
source_location: None,
coord_x: 0,
coord_y: 0,
coord_z: 0,
});
let b4 = g.add_node(BasicBlock {
id: 4,
db_id: None,
kind: BlockKind::Exit,
statements: vec![],
terminator: Terminator::Return,
source_location: None,
coord_x: 0,
coord_y: 0,
coord_z: 0,
});
g.add_edge(b0, b1, EdgeType::Fallthrough);
g.add_edge(b1, b2, EdgeType::Fallthrough);
g.add_edge(b2, b3, EdgeType::Fallthrough);
g.add_edge(b3, b4, EdgeType::Fallthrough);
let limits = PathLimits::default().with_max_length(3);
let paths = enumerate_paths(&g, &limits);
assert_eq!(
paths.len(),
0,
"Path exceeds max_length, should return 0 paths"
);
}
#[test]
fn test_path_limits_max_paths_exact() {
let cfg = create_diamond_cfg();
let limits = PathLimits::default().with_max_paths(1);
let paths = enumerate_paths(&cfg, &limits);
assert_eq!(paths.len(), 1, "Should stop at max_paths=1");
assert_eq!(paths[0].entry, 0);
assert_eq!(paths[0].exit, 3);
}
#[test]
fn test_path_limits_loop_unroll_exact() {
let cfg = create_loop_cfg();
let limits = PathLimits::default().with_loop_unroll_limit(1);
let paths = enumerate_paths(&cfg, &limits);
assert_eq!(
paths.len(),
1,
"Should have exactly 1 path with loop_unroll_limit=1"
);
assert!(
paths.iter().any(|p| p.blocks == vec![0, 1, 3]),
"Direct exit path should exist"
);
}
#[test]
fn test_path_limits_loop_unroll_limit_2() {
let cfg = create_loop_cfg();
let limits = PathLimits::default().with_loop_unroll_limit(2);
let paths = enumerate_paths(&cfg, &limits);
assert_eq!(
paths.len(),
2,
"Should have exactly 2 paths with loop_unroll_limit=2"
);
assert!(
paths.iter().any(|p| p.blocks == vec![0, 1, 3]),
"Direct exit path should exist"
);
assert!(
paths.iter().any(|p| p.blocks == vec![0, 1, 2, 1, 3]),
"One iteration path should exist"
);
}
fn create_self_loop_cfg() -> Cfg {
let mut g = DiGraph::new();
let b0 = g.add_node(BasicBlock {
id: 0,
db_id: None,
kind: BlockKind::Entry,
statements: vec![],
terminator: Terminator::Goto { target: 1 },
source_location: None,
coord_x: 0,
coord_y: 0,
coord_z: 0,
});
let b1 = g.add_node(BasicBlock {
id: 1,
db_id: None,
kind: BlockKind::Normal,
statements: vec![],
terminator: Terminator::Goto { target: 1 },
source_location: None,
coord_x: 0,
coord_y: 0,
coord_z: 0,
});
g.add_edge(b0, b1, EdgeType::Fallthrough);
g
}
#[test]
fn test_self_loop_terminates() {
let cfg = create_self_loop_cfg();
let limits = PathLimits::default();
let paths = enumerate_paths(&cfg, &limits);
assert!(
paths.len() <= limits.loop_unroll_limit + 1,
"Self-loop should be bounded by loop_unroll_limit"
);
}
#[test]
fn test_self_loop_with_low_limit() {
let cfg = create_self_loop_cfg();
let limits = PathLimits::default().with_loop_unroll_limit(1);
let paths = enumerate_paths(&cfg, &limits);
assert!(
paths.len() <= 2,
"Self-loop with low limit should have few paths"
);
}
fn create_nested_loop_cfg() -> Cfg {
let mut g = DiGraph::new();
let b0 = g.add_node(BasicBlock {
id: 0,
db_id: None,
kind: BlockKind::Entry,
statements: vec![],
terminator: Terminator::Goto { target: 1 },
source_location: None,
coord_x: 0,
coord_y: 0,
coord_z: 0,
});
let b1 = g.add_node(BasicBlock {
id: 1,
db_id: None,
kind: BlockKind::Normal,
statements: vec![],
terminator: Terminator::SwitchInt {
targets: vec![2],
otherwise: 4,
},
source_location: None,
coord_x: 0,
coord_y: 0,
coord_z: 0,
});
let b2 = g.add_node(BasicBlock {
id: 2,
db_id: None,
kind: BlockKind::Normal,
statements: vec![],
terminator: Terminator::SwitchInt {
targets: vec![3],
otherwise: 1,
},
source_location: None,
coord_x: 0,
coord_y: 0,
coord_z: 0,
});
let b3 = g.add_node(BasicBlock {
id: 3,
db_id: None,
kind: BlockKind::Normal,
statements: vec![],
terminator: Terminator::Goto { target: 2 },
source_location: None,
coord_x: 0,
coord_y: 0,
coord_z: 0,
});
let b4 = g.add_node(BasicBlock {
id: 4,
db_id: None,
kind: BlockKind::Exit,
statements: vec![],
terminator: Terminator::Return,
source_location: None,
coord_x: 0,
coord_y: 0,
coord_z: 0,
});
g.add_edge(b0, b1, EdgeType::Fallthrough);
g.add_edge(b1, b2, EdgeType::TrueBranch);
g.add_edge(b1, b4, EdgeType::FalseBranch);
g.add_edge(b2, b3, EdgeType::TrueBranch);
g.add_edge(b2, b1, EdgeType::LoopBack); g.add_edge(b3, b2, EdgeType::LoopBack);
g
}
#[test]
fn test_nested_loop_bounding() {
let cfg = create_nested_loop_cfg();
let limits = PathLimits::default().with_loop_unroll_limit(2);
let paths = enumerate_paths(&cfg, &limits);
assert!(
paths.len() <= 9,
"Nested loops should be bounded: got {} paths",
paths.len()
);
assert!(paths.len() > 0, "Should have at least some paths");
}
#[test]
fn test_nested_loop_bounding_three_levels() {
let mut g = DiGraph::new();
let b0 = g.add_node(BasicBlock {
id: 0,
db_id: None,
kind: BlockKind::Entry,
statements: vec![],
terminator: Terminator::Goto { target: 1 },
source_location: None,
coord_x: 0,
coord_y: 0,
coord_z: 0,
});
let b1 = g.add_node(BasicBlock {
id: 1,
db_id: None,
kind: BlockKind::Normal,
statements: vec![],
terminator: Terminator::SwitchInt {
targets: vec![2],
otherwise: 6,
},
source_location: None,
coord_x: 0,
coord_y: 0,
coord_z: 0,
});
let b2 = g.add_node(BasicBlock {
id: 2,
db_id: None,
kind: BlockKind::Normal,
statements: vec![],
terminator: Terminator::SwitchInt {
targets: vec![3],
otherwise: 1,
},
source_location: None,
coord_x: 0,
coord_y: 0,
coord_z: 0,
});
let b3 = g.add_node(BasicBlock {
id: 3,
db_id: None,
kind: BlockKind::Normal,
statements: vec![],
terminator: Terminator::SwitchInt {
targets: vec![4],
otherwise: 2,
},
source_location: None,
coord_x: 0,
coord_y: 0,
coord_z: 0,
});
let b4 = g.add_node(BasicBlock {
id: 4,
db_id: None,
kind: BlockKind::Normal,
statements: vec![],
terminator: Terminator::Goto { target: 3 },
source_location: None,
coord_x: 0,
coord_y: 0,
coord_z: 0,
});
let b6 = g.add_node(BasicBlock {
id: 6,
db_id: None,
kind: BlockKind::Exit,
statements: vec![],
terminator: Terminator::Return,
source_location: None,
coord_x: 0,
coord_y: 0,
coord_z: 0,
});
g.add_edge(b0, b1, EdgeType::Fallthrough);
g.add_edge(b1, b2, EdgeType::TrueBranch);
g.add_edge(b1, b6, EdgeType::FalseBranch);
g.add_edge(b2, b3, EdgeType::TrueBranch);
g.add_edge(b2, b1, EdgeType::LoopBack); g.add_edge(b3, b4, EdgeType::TrueBranch);
g.add_edge(b3, b2, EdgeType::LoopBack); g.add_edge(b4, b3, EdgeType::LoopBack);
let limits = PathLimits::default().with_loop_unroll_limit(2);
let paths = enumerate_paths(&g, &limits);
assert!(
paths.len() <= 27,
"3-level nested loops should be bounded by 27"
);
}
#[test]
fn test_nested_loop_independent_counters() {
let cfg = create_nested_loop_cfg();
let loop_headers = crate::cfg::loops::find_loop_headers(&cfg);
assert_eq!(loop_headers.len(), 2, "Should detect 2 loop headers");
let limits = PathLimits::default().with_loop_unroll_limit(2);
let paths = enumerate_paths(&cfg, &limits);
assert!(paths.len() > 0, "Should have some paths");
assert!(
paths.len() <= 9,
"Should be bounded by (limit+1)^nesting_depth"
);
}
#[test]
fn test_path_limits_quick_analysis() {
let limits = PathLimits::quick_analysis();
assert_eq!(limits.max_length, 100);
assert_eq!(limits.max_paths, 1000);
assert_eq!(limits.loop_unroll_limit, 2);
}
#[test]
fn test_path_limits_thorough() {
let limits = PathLimits::thorough();
assert_eq!(limits.max_length, 10000);
assert_eq!(limits.max_paths, 100000);
assert_eq!(limits.loop_unroll_limit, 5);
}
#[test]
fn test_path_limits_builder_chaining() {
let limits = PathLimits::quick_analysis()
.with_max_length(200)
.with_max_paths(5000)
.with_loop_unroll_limit(3);
assert_eq!(limits.max_length, 200);
assert_eq!(limits.max_paths, 5000);
assert_eq!(limits.loop_unroll_limit, 3);
}
#[test]
fn test_path_limits_presets_differ_from_default() {
let default = PathLimits::default();
let quick = PathLimits::quick_analysis();
let thorough = PathLimits::thorough();
assert!(quick.max_length < default.max_length);
assert!(quick.max_paths < default.max_paths);
assert!(quick.loop_unroll_limit < default.loop_unroll_limit);
assert!(thorough.max_length > default.max_length);
assert!(thorough.max_paths > default.max_paths);
assert!(thorough.loop_unroll_limit > default.loop_unroll_limit);
}
#[test]
fn test_path_limits_quick_vs_thorough_on_loop() {
let cfg = create_loop_cfg();
let quick_paths = enumerate_paths(&cfg, &PathLimits::quick_analysis());
let thorough_paths = enumerate_paths(&cfg, &PathLimits::thorough());
assert!(
thorough_paths.len() >= quick_paths.len(),
"Thorough analysis should find at least as many paths as quick"
);
}
#[test]
fn test_is_feasible_path_empty_path() {
let cfg = create_linear_cfg();
let empty_path: Vec<BlockId> = vec![];
assert!(
!is_feasible_path(&cfg, &empty_path),
"Empty path should be infeasible"
);
}
#[test]
fn test_is_feasible_path_non_entry_first_block() {
let cfg = create_diamond_cfg();
let path = vec![1, 3];
assert!(
!is_feasible_path(&cfg, &path),
"Path starting from non-entry block should be infeasible"
);
}
#[test]
fn test_is_feasible_path_dead_end_goto() {
let cfg = create_linear_cfg();
let path = vec![0, 1];
assert!(
!is_feasible_path(&cfg, &path),
"Path ending in Goto should be infeasible (dead end)"
);
}
#[test]
fn test_is_feasible_path_valid_return() {
let cfg = create_linear_cfg();
let path = vec![0, 1, 2];
assert!(
is_feasible_path(&cfg, &path),
"Complete path ending in Return should be feasible"
);
}
#[test]
fn test_is_feasible_path_abort_is_feasible() {
let cfg = create_error_cfg();
let path = vec![0, 1];
assert!(
is_feasible_path(&cfg, &path),
"Path ending in Abort should be feasible (error path but reachable)"
);
}
#[test]
fn test_is_feasible_path_unreachable_terminator() {
let cfg = create_unreachable_term_cfg();
let path = vec![0, 1];
assert!(
!is_feasible_path(&cfg, &path),
"Path ending in Unreachable should be infeasible"
);
}
#[test]
fn test_is_feasible_path_nonexistent_block() {
let cfg = create_linear_cfg();
let path = vec![0, 99];
assert!(
!is_feasible_path(&cfg, &path),
"Path with nonexistent block should be infeasible"
);
}
#[test]
fn test_is_feasible_path_switch_int_dead_end() {
let cfg = create_diamond_cfg();
let path = vec![0];
assert!(
!is_feasible_path(&cfg, &path),
"Path ending in SwitchInt should be infeasible (dead end)"
);
}
#[test]
fn test_is_feasible_path_complete_diamond() {
let cfg = create_diamond_cfg();
let path1 = vec![0, 1, 3]; let path2 = vec![0, 2, 3];
assert!(
is_feasible_path(&cfg, &path1),
"Complete diamond path 0->1->3 should be feasible"
);
assert!(
is_feasible_path(&cfg, &path2),
"Complete diamond path 0->2->3 should be feasible"
);
}
#[test]
fn test_is_feasible_path_call_no_unwind() {
let mut g = DiGraph::new();
let b0 = g.add_node(BasicBlock {
id: 0,
db_id: None,
kind: BlockKind::Entry,
statements: vec![],
terminator: Terminator::Call {
target: Some(1),
unwind: None,
},
source_location: None,
coord_x: 0,
coord_y: 0,
coord_z: 0,
});
let b1 = g.add_node(BasicBlock {
id: 1,
db_id: None,
kind: BlockKind::Exit,
statements: vec![],
terminator: Terminator::Return,
source_location: None,
coord_x: 0,
coord_y: 0,
coord_z: 0,
});
g.add_edge(b0, b1, EdgeType::Fallthrough);
let path = vec![0];
assert!(
is_feasible_path(&g, &path),
"Path ending in Call with no unwind should be feasible"
);
}
#[test]
fn test_is_feasible_path_call_with_unwind_and_target() {
let cfg = create_call_unwind_cfg();
let path = vec![0];
assert!(
is_feasible_path(&cfg, &path),
"Path ending in Call with both unwind and target should be feasible"
);
}
#[test]
fn test_is_feasible_path_call_always_unwinds() {
let mut g = DiGraph::new();
let b0 = g.add_node(BasicBlock {
id: 0,
db_id: None,
kind: BlockKind::Entry,
statements: vec![],
terminator: Terminator::Call {
target: None, unwind: Some(1),
},
source_location: None,
coord_x: 0,
coord_y: 0,
coord_z: 0,
});
let b1 = g.add_node(BasicBlock {
id: 1,
db_id: None,
kind: BlockKind::Exit,
statements: vec![],
terminator: Terminator::Return,
source_location: None,
coord_x: 0,
coord_y: 0,
coord_z: 0,
});
g.add_edge(b0, b1, EdgeType::Fallthrough);
let path = vec![0];
assert!(
!is_feasible_path(&g, &path),
"Path ending in Call with only unwind (no target) should be infeasible"
);
}
#[test]
fn test_is_feasible_path_precomputed_matches_basic() {
let cfg = create_diamond_cfg();
use crate::cfg::reachability::find_reachable;
let reachable_nodes = find_reachable(&cfg);
let reachable_blocks: HashSet<BlockId> =
reachable_nodes.iter().map(|&idx| cfg[idx].id).collect();
let test_paths = vec![
vec![0, 1, 3], vec![0, 2, 3], vec![0, 1], vec![], ];
for path in test_paths {
let basic = is_feasible_path(&cfg, &path);
let precomputed = is_feasible_path_precomputed(&cfg, &path, &reachable_blocks);
assert_eq!(
basic, precomputed,
"is_feasible_path_precomputed should match is_feasible_path for {:?}",
path
);
}
}
#[test]
fn test_is_feasible_path_precomputed_unreachable_block() {
let cfg = create_dead_code_cfg();
use crate::cfg::reachability::find_reachable;
let reachable_nodes = find_reachable(&cfg);
let reachable_blocks: HashSet<BlockId> =
reachable_nodes.iter().map(|&idx| cfg[idx].id).collect();
let path = vec![1];
assert!(
!is_feasible_path_precomputed(&cfg, &path, &reachable_blocks),
"Path with unreachable block should be infeasible"
);
}
#[test]
fn test_is_feasible_path_precomputed_performance() {
use crate::cfg::reachability::find_reachable;
use std::time::Instant;
let cfg = create_diamond_cfg();
let reachable_nodes = find_reachable(&cfg);
let reachable_blocks: HashSet<BlockId> =
reachable_nodes.iter().map(|&idx| cfg[idx].id).collect();
let test_paths: Vec<Vec<BlockId>> = (0..1000).map(|_| vec![0, 1, 3]).collect();
let start = Instant::now();
for path in &test_paths {
let _ = is_feasible_path_precomputed(&cfg, path, &reachable_blocks);
}
let precomputed_duration = start.elapsed();
assert!(
precomputed_duration.as_millis() < 5,
"is_feasible_path_precomputed should check 1000 paths in <5ms, took {}ms",
precomputed_duration.as_millis()
);
}
#[test]
fn test_is_feasible_path_precomputed_all_criteria() {
use crate::cfg::reachability::find_reachable;
let cfg_normal = create_linear_cfg();
let reachable_normal = find_reachable(&cfg_normal)
.iter()
.map(|&idx| cfg_normal[idx].id)
.collect();
assert!(
is_feasible_path_precomputed(&cfg_normal, &[0, 1, 2], &reachable_normal),
"Complete linear path should be feasible"
);
let cfg_error = create_error_cfg();
let reachable_error = find_reachable(&cfg_error)
.iter()
.map(|&idx| cfg_error[idx].id)
.collect();
assert!(
is_feasible_path_precomputed(&cfg_error, &[0, 1], &reachable_error),
"Path ending in Abort should be feasible (error path but reachable)"
);
let cfg_degen = create_unreachable_term_cfg();
let reachable_degen = find_reachable(&cfg_degen)
.iter()
.map(|&idx| cfg_degen[idx].id)
.collect();
assert!(
!is_feasible_path_precomputed(&cfg_degen, &[0, 1], &reachable_degen),
"Path ending in Unreachable should be infeasible"
);
}
#[test]
fn test_classify_with_feasibility_dead_end() {
use crate::cfg::reachability::find_reachable;
let cfg = create_linear_cfg();
let reachable = find_reachable(&cfg)
.iter()
.map(|&idx| cfg[idx].id)
.collect();
let path = vec![0, 1]; let kind = classify_path_precomputed(&cfg, &path, &reachable);
assert_eq!(
kind,
PathKind::Degenerate,
"Path ending in Goto should be Degenerate"
);
}
#[test]
fn test_classify_with_feasibility_valid_exit() {
use crate::cfg::reachability::find_reachable;
let cfg = create_linear_cfg();
let reachable = find_reachable(&cfg)
.iter()
.map(|&idx| cfg[idx].id)
.collect();
let path = vec![0, 1, 2];
let kind = classify_path_precomputed(&cfg, &path, &reachable);
assert_eq!(
kind,
PathKind::Normal,
"Complete path with Return should be Normal"
);
}
#[test]
fn test_classify_with_feasibility_error_path() {
use crate::cfg::reachability::find_reachable;
let cfg = create_error_cfg();
let reachable = find_reachable(&cfg)
.iter()
.map(|&idx| cfg[idx].id)
.collect();
let path = vec![0, 1];
let kind = classify_path_precomputed(&cfg, &path, &reachable);
assert_eq!(
kind,
PathKind::Error,
"Path ending in Abort should be Error"
);
}
#[test]
fn test_classify_with_feasibility_switch_int_dead_end() {
use crate::cfg::reachability::find_reachable;
let cfg = create_diamond_cfg();
let reachable = find_reachable(&cfg)
.iter()
.map(|&idx| cfg[idx].id)
.collect();
let path = vec![0]; let kind = classify_path_precomputed(&cfg, &path, &reachable);
assert_eq!(
kind,
PathKind::Degenerate,
"Path ending in SwitchInt should be Degenerate"
);
}
#[test]
fn test_classify_with_feasibility_priority_order() {
use crate::cfg::reachability::find_reachable;
let cfg = create_dead_code_cfg();
let reachable = find_reachable(&cfg)
.iter()
.map(|&idx| cfg[idx].id)
.collect();
let path = vec![1];
let kind = classify_path_precomputed(&cfg, &path, &reachable);
assert_eq!(
kind,
PathKind::Unreachable,
"Unreachable should be prioritized over feasibility"
);
}
#[test]
fn test_classify_with_feasibility_complete_paths() {
use crate::cfg::reachability::find_reachable;
let cfg = create_diamond_cfg();
let reachable = find_reachable(&cfg)
.iter()
.map(|&idx| cfg[idx].id)
.collect();
let path1 = vec![0, 1, 3];
let path2 = vec![0, 2, 3];
assert_eq!(
classify_path_precomputed(&cfg, &path1, &reachable),
PathKind::Normal,
"Complete diamond path 0->1->3 should be Normal"
);
assert_eq!(
classify_path_precomputed(&cfg, &path2, &reachable),
PathKind::Normal,
"Complete diamond path 0->2->3 should be Normal"
);
}
#[test]
fn test_classify_with_feasibility_call_terminator() {
use crate::cfg::reachability::find_reachable;
let mut g = DiGraph::new();
let b0 = g.add_node(BasicBlock {
id: 0,
db_id: None,
kind: BlockKind::Entry,
statements: vec![],
terminator: Terminator::Call {
target: Some(1),
unwind: None,
},
source_location: None,
coord_x: 0,
coord_y: 0,
coord_z: 0,
});
let b1 = g.add_node(BasicBlock {
id: 1,
db_id: None,
kind: BlockKind::Exit,
statements: vec![],
terminator: Terminator::Return,
source_location: None,
coord_x: 0,
coord_y: 0,
coord_z: 0,
});
g.add_edge(b0, b1, EdgeType::Fallthrough);
let reachable = find_reachable(&g).iter().map(|&idx| g[idx].id).collect();
let path = vec![0];
let kind = classify_path_precomputed(&g, &path, &reachable);
assert_eq!(
kind,
PathKind::Normal,
"Path ending in Call with target should be Normal"
);
}
fn create_conflicting_conditions_cfg() -> Cfg {
let mut g = DiGraph::new();
let b0 = g.add_node(BasicBlock {
id: 0,
db_id: None,
kind: BlockKind::Entry,
statements: vec![],
terminator: Terminator::SwitchInt {
targets: vec![1],
otherwise: 2,
},
source_location: None,
coord_x: 0,
coord_y: 0,
coord_z: 0,
});
let b1 = g.add_node(BasicBlock {
id: 1,
db_id: None,
kind: BlockKind::Normal,
statements: vec![],
terminator: Terminator::SwitchInt {
targets: vec![3],
otherwise: 3,
},
source_location: None,
coord_x: 0,
coord_y: 0,
coord_z: 0,
});
let b2 = g.add_node(BasicBlock {
id: 2,
db_id: None,
kind: BlockKind::Exit,
statements: vec![],
terminator: Terminator::Return,
source_location: None,
coord_x: 0,
coord_y: 0,
coord_z: 0,
});
let b3 = g.add_node(BasicBlock {
id: 3,
db_id: None,
kind: BlockKind::Exit,
statements: vec![],
terminator: Terminator::Return,
source_location: None,
coord_x: 0,
coord_y: 0,
coord_z: 0,
});
g.add_edge(b0, b1, EdgeType::TrueBranch);
g.add_edge(b0, b2, EdgeType::FalseBranch);
g.add_edge(b1, b3, EdgeType::Fallthrough);
g
}
#[test]
fn test_feasibility_limitation_conflicting_conditions() {
use crate::cfg::reachability::find_reachable;
let cfg = create_conflicting_conditions_cfg();
let reachable = find_reachable(&cfg)
.iter()
.map(|&idx| cfg[idx].id)
.collect();
let path = vec![0, 1, 3];
assert!(
is_feasible_path_precomputed(&cfg, &path, &reachable),
"Static analysis marks conflicting path as feasible (limitation)"
);
assert_eq!(
classify_path_precomputed(&cfg, &path, &reachable),
PathKind::Normal,
"Conflicting path is classified as Normal (static limitation)"
);
}
#[test]
fn test_feasibility_documentation_accuracy() {
use crate::cfg::reachability::find_reachable;
let cfg = create_linear_cfg();
let reachable = find_reachable(&cfg)
.iter()
.map(|&idx| cfg[idx].id)
.collect();
let non_entry_path = vec![1, 2];
assert!(
!is_feasible_path_precomputed(&cfg, &non_entry_path, &reachable),
"Entry check works as documented"
);
let dead_end_path = vec![0, 1];
assert!(
!is_feasible_path_precomputed(&cfg, &dead_end_path, &reachable),
"Dead-end detection works as documented"
);
assert!(
is_feasible_path_precomputed(&cfg, &[0, 1, 2], &reachable),
"Reachable path is feasible as documented"
);
}
#[test]
fn test_get_or_enumerate_paths_cache_miss_enumerates() {
use crate::storage::create_schema;
let mut conn = rusqlite::Connection::open_in_memory().unwrap();
conn.execute(
"CREATE TABLE magellan_meta (
id INTEGER PRIMARY KEY CHECK (id = 1),
magellan_schema_version INTEGER NOT NULL,
sqlitegraph_schema_version INTEGER NOT NULL,
created_at INTEGER NOT NULL
)",
[],
)
.unwrap();
conn.execute(
"CREATE TABLE graph_entities (
id INTEGER PRIMARY KEY AUTOINCREMENT,
kind TEXT NOT NULL,
name TEXT NOT NULL,
file_path TEXT,
data TEXT NOT NULL
)",
[],
)
.unwrap();
conn.execute(
"INSERT INTO magellan_meta (id, magellan_schema_version, sqlitegraph_schema_version, created_at)
VALUES (1, 4, 3, 0)",
[],
).unwrap();
create_schema(&mut conn, crate::storage::TEST_MAGELLAN_SCHEMA_VERSION).unwrap();
conn.execute(
"INSERT INTO graph_entities (kind, name, file_path, data) VALUES (?, ?, ?, ?)",
rusqlite::params!("function", "test_func", "test.rs", "{}"),
)
.unwrap();
let function_id: i64 = 1;
let cfg = create_linear_cfg();
let function_hash = "test_hash_123";
let limits = PathLimits::default();
let paths =
get_or_enumerate_paths(&cfg, function_id, function_hash, &limits, &mut conn).unwrap();
assert_eq!(paths.len(), 1);
assert_eq!(paths[0].blocks, vec![0, 1, 2]);
}
#[test]
fn test_get_or_enumerate_paths_cache_hit_retrieves() {
use crate::storage::create_schema;
let mut conn = rusqlite::Connection::open_in_memory().unwrap();
conn.execute(
"CREATE TABLE magellan_meta (
id INTEGER PRIMARY KEY CHECK (id = 1),
magellan_schema_version INTEGER NOT NULL,
sqlitegraph_schema_version INTEGER NOT NULL,
created_at INTEGER NOT NULL
)",
[],
)
.unwrap();
conn.execute(
"CREATE TABLE graph_entities (
id INTEGER PRIMARY KEY AUTOINCREMENT,
kind TEXT NOT NULL,
name TEXT NOT NULL,
file_path TEXT,
data TEXT NOT NULL
)",
[],
)
.unwrap();
conn.execute(
"INSERT INTO magellan_meta (id, magellan_schema_version, sqlitegraph_schema_version, created_at)
VALUES (1, 4, 3, 0)",
[],
).unwrap();
create_schema(&mut conn, crate::storage::TEST_MAGELLAN_SCHEMA_VERSION).unwrap();
conn.execute(
"INSERT INTO graph_entities (kind, name, file_path, data) VALUES (?, ?, ?, ?)",
rusqlite::params!("function", "test_func", "test.rs", "{}"),
)
.unwrap();
let function_id: i64 = 1;
let cfg = create_linear_cfg();
let function_hash = "test_hash_456";
let limits = PathLimits::default();
let paths1 =
get_or_enumerate_paths(&cfg, function_id, function_hash, &limits, &mut conn).unwrap();
assert_eq!(paths1.len(), 1);
let paths2 =
get_or_enumerate_paths(&cfg, function_id, function_hash, &limits, &mut conn).unwrap();
assert_eq!(paths2.len(), 1);
assert_eq!(paths2[0].blocks, vec![0, 1, 2]);
}
#[test]
fn test_get_or_enumerate_paths_hash_change_invalidates() {
use crate::storage::create_schema;
let mut conn = rusqlite::Connection::open_in_memory().unwrap();
conn.execute(
"CREATE TABLE magellan_meta (
id INTEGER PRIMARY KEY CHECK (id = 1),
magellan_schema_version INTEGER NOT NULL,
sqlitegraph_schema_version INTEGER NOT NULL,
created_at INTEGER NOT NULL
)",
[],
)
.unwrap();
conn.execute(
"CREATE TABLE graph_entities (
id INTEGER PRIMARY KEY AUTOINCREMENT,
kind TEXT NOT NULL,
name TEXT NOT NULL,
file_path TEXT,
data TEXT NOT NULL
)",
[],
)
.unwrap();
conn.execute(
"INSERT INTO magellan_meta (id, magellan_schema_version, sqlitegraph_schema_version, created_at)
VALUES (1, 4, 3, 0)",
[],
).unwrap();
create_schema(&mut conn, crate::storage::TEST_MAGELLAN_SCHEMA_VERSION).unwrap();
conn.execute(
"INSERT INTO graph_entities (kind, name, file_path, data) VALUES (?, ?, ?, ?)",
rusqlite::params!("function", "test_func", "test.rs", "{}"),
)
.unwrap();
let function_id: i64 = 1;
let cfg = create_linear_cfg();
let hash1 = "test_hash_v1";
let hash2 = "test_hash_v3";
let limits = PathLimits::default();
let paths1 = get_or_enumerate_paths(&cfg, function_id, hash1, &limits, &mut conn).unwrap();
assert_eq!(paths1.len(), 1);
let paths2 = get_or_enumerate_paths(&cfg, function_id, hash2, &limits, &mut conn).unwrap();
assert_eq!(paths2.len(), 1);
assert_eq!(paths1[0].blocks, paths2[0].blocks);
}
#[test]
fn test_enumeration_context_new() {
use super::super::EnumerationContext;
let cfg = create_linear_cfg();
let ctx = EnumerationContext::new(&cfg);
assert_eq!(ctx.reachable_count(), 3);
assert_eq!(ctx.loop_count(), 0);
assert_eq!(ctx.exit_count(), 1);
}
#[test]
fn test_enumeration_context_with_loop() {
use super::super::EnumerationContext;
let cfg = create_loop_cfg();
let ctx = EnumerationContext::new(&cfg);
assert_eq!(ctx.loop_count(), 1);
assert!(ctx.reachable_count() > 0);
assert!(ctx.exit_count() > 0);
}
#[test]
fn test_enumeration_context_diamond_cfg() {
use super::super::EnumerationContext;
let cfg = create_diamond_cfg();
let ctx = EnumerationContext::new(&cfg);
assert_eq!(ctx.loop_count(), 0);
assert_eq!(ctx.reachable_count(), 4); assert_eq!(ctx.exit_count(), 1); }
#[test]
fn test_enumeration_context_is_reachable() {
use super::super::EnumerationContext;
let cfg = create_dead_code_cfg();
let ctx = EnumerationContext::new(&cfg);
assert!(ctx.is_reachable(0));
assert!(!ctx.is_reachable(1));
}
#[test]
fn test_enumeration_context_is_loop_header() {
use super::super::EnumerationContext;
use petgraph::graph::NodeIndex;
let cfg = create_loop_cfg();
let ctx = EnumerationContext::new(&cfg);
assert!(ctx.is_loop_header(NodeIndex::new(1)));
assert!(!ctx.is_loop_header(NodeIndex::new(0)));
}
#[test]
fn test_enumeration_context_is_exit() {
use super::super::EnumerationContext;
use petgraph::graph::NodeIndex;
let cfg = create_diamond_cfg();
let ctx = EnumerationContext::new(&cfg);
assert!(ctx.is_exit(NodeIndex::new(3)));
assert!(!ctx.is_exit(NodeIndex::new(0)));
}
#[test]
fn test_enumerate_paths_with_context_matches_basic() {
use super::super::{enumerate_paths, enumerate_paths_with_context, EnumerationContext};
let cfg = create_diamond_cfg();
let limits = PathLimits::default();
let ctx = EnumerationContext::new(&cfg);
let paths_basic = enumerate_paths(&cfg, &limits);
let paths_context = enumerate_paths_with_context(&cfg, &limits, &ctx);
assert_eq!(paths_basic.len(), paths_context.len());
let mut sorted_basic: Vec<_> = paths_basic.iter().collect();
let mut sorted_context: Vec<_> = paths_context.iter().collect();
sorted_basic.sort_by_key(|p| p.blocks.clone());
sorted_context.sort_by_key(|p| p.blocks.clone());
for (basic, context) in sorted_basic.iter().zip(sorted_context.iter()) {
assert_eq!(basic.blocks, context.blocks);
assert_eq!(basic.kind, context.kind);
}
}
#[test]
fn test_enumerate_paths_with_context_linear_cfg() {
use super::super::{enumerate_paths_with_context, EnumerationContext};
let cfg = create_linear_cfg();
let limits = PathLimits::default();
let ctx = EnumerationContext::new(&cfg);
let paths = enumerate_paths_with_context(&cfg, &limits, &ctx);
assert_eq!(paths.len(), 1);
assert_eq!(paths[0].blocks, vec![0, 1, 2]);
}
#[test]
fn test_enumerate_paths_with_context_with_loop() {
use super::super::{enumerate_paths_with_context, EnumerationContext};
let cfg = create_loop_cfg();
let limits = PathLimits::default();
let ctx = EnumerationContext::new(&cfg);
let paths = enumerate_paths_with_context(&cfg, &limits, &ctx);
assert!(paths.len() > 0);
assert!(paths.len() <= 4); }
#[test]
fn test_enumerate_paths_with_context_performance() {
use super::super::{enumerate_paths, enumerate_paths_with_context, EnumerationContext};
use std::time::Instant;
let cfg = create_diamond_cfg();
let limits = PathLimits::default();
let start = Instant::now();
let _paths1 = enumerate_paths(&cfg, &limits);
let basic_time = start.elapsed();
let ctx_start = Instant::now();
let ctx = EnumerationContext::new(&cfg);
let ctx_creation_time = ctx_start.elapsed();
let start = Instant::now();
let _paths2 = enumerate_paths_with_context(&cfg, &limits, &ctx);
let context_time = start.elapsed();
println!(
"Basic: {:?}, Context creation: {:?}, Context enum: {:?}",
basic_time, ctx_creation_time, context_time
);
let start = Instant::now();
let _paths3 = enumerate_paths_with_context(&cfg, &limits, &ctx);
let context_time2 = start.elapsed();
println!("Second context call: {:?}", context_time2);
assert!(context_time2.as_millis() < 100);
}
#[test]
fn test_enumerate_paths_with_context_multiple_calls() {
use super::super::{enumerate_paths_with_context, EnumerationContext};
let cfg = create_diamond_cfg();
let ctx = EnumerationContext::new(&cfg);
let limits1 = PathLimits::default().with_max_paths(10);
let limits2 = PathLimits::default().with_max_paths(100);
let paths1 = enumerate_paths_with_context(&cfg, &limits1, &ctx);
let paths2 = enumerate_paths_with_context(&cfg, &limits2, &ctx);
assert_eq!(paths1.len(), paths2.len());
}
#[test]
fn test_enumerate_paths_cached_cache_miss_enumerates() {
use super::super::enumerate_paths_cached;
use crate::storage::create_schema;
let mut conn = rusqlite::Connection::open_in_memory().unwrap();
conn.execute(
"CREATE TABLE magellan_meta (
id INTEGER PRIMARY KEY CHECK (id = 1),
magellan_schema_version INTEGER NOT NULL,
sqlitegraph_schema_version INTEGER NOT NULL,
created_at INTEGER NOT NULL
)",
[],
)
.unwrap();
conn.execute(
"CREATE TABLE graph_entities (
id INTEGER PRIMARY KEY AUTOINCREMENT,
kind TEXT NOT NULL,
name TEXT NOT NULL,
file_path TEXT,
data TEXT NOT NULL
)",
[],
)
.unwrap();
conn.execute(
"INSERT INTO magellan_meta (id, magellan_schema_version, sqlitegraph_schema_version, created_at)
VALUES (1, 4, 3, 0)",
[],
).unwrap();
create_schema(&mut conn, crate::storage::TEST_MAGELLAN_SCHEMA_VERSION).unwrap();
conn.execute(
"INSERT INTO graph_entities (kind, name, file_path, data) VALUES (?, ?, ?, ?)",
rusqlite::params!("function", "test_func", "test.rs", "{}"),
)
.unwrap();
let function_id: i64 = 1;
let cfg = create_linear_cfg();
let function_hash = "test_hash_123";
let limits = PathLimits::default();
let paths =
enumerate_paths_cached(&cfg, function_id, function_hash, &limits, &mut conn).unwrap();
assert_eq!(paths.len(), 1);
assert_eq!(paths[0].blocks, vec![0, 1, 2]);
}
#[test]
fn test_enumerate_paths_cached_cache_hit_retrieves() {
use super::super::enumerate_paths_cached;
use crate::storage::create_schema;
let mut conn = rusqlite::Connection::open_in_memory().unwrap();
conn.execute(
"CREATE TABLE magellan_meta (
id INTEGER PRIMARY KEY CHECK (id = 1),
magellan_schema_version INTEGER NOT NULL,
sqlitegraph_schema_version INTEGER NOT NULL,
created_at INTEGER NOT NULL
)",
[],
)
.unwrap();
conn.execute(
"CREATE TABLE graph_entities (
id INTEGER PRIMARY KEY AUTOINCREMENT,
kind TEXT NOT NULL,
name TEXT NOT NULL,
file_path TEXT,
data TEXT NOT NULL
)",
[],
)
.unwrap();
conn.execute(
"INSERT INTO magellan_meta (id, magellan_schema_version, sqlitegraph_schema_version, created_at)
VALUES (1, 4, 3, 0)",
[],
).unwrap();
create_schema(&mut conn, crate::storage::TEST_MAGELLAN_SCHEMA_VERSION).unwrap();
conn.execute(
"INSERT INTO graph_entities (kind, name, file_path, data) VALUES (?, ?, ?, ?)",
rusqlite::params!("function", "test_func", "test.rs", "{}"),
)
.unwrap();
let function_id: i64 = 1;
let cfg = create_linear_cfg();
let function_hash = "test_hash_456";
let limits = PathLimits::default();
let paths1 =
enumerate_paths_cached(&cfg, function_id, function_hash, &limits, &mut conn).unwrap();
assert_eq!(paths1.len(), 1);
let paths2 =
enumerate_paths_cached(&cfg, function_id, function_hash, &limits, &mut conn).unwrap();
assert_eq!(paths2.len(), 1);
assert_eq!(paths2[0].blocks, vec![0, 1, 2]);
}
#[test]
fn test_enumerate_paths_cached_hash_change_invalidates() {
use super::super::enumerate_paths_cached;
use crate::storage::create_schema;
let mut conn = rusqlite::Connection::open_in_memory().unwrap();
conn.execute(
"CREATE TABLE magellan_meta (
id INTEGER PRIMARY KEY CHECK (id = 1),
magellan_schema_version INTEGER NOT NULL,
sqlitegraph_schema_version INTEGER NOT NULL,
created_at INTEGER NOT NULL
)",
[],
)
.unwrap();
conn.execute(
"CREATE TABLE graph_entities (
id INTEGER PRIMARY KEY AUTOINCREMENT,
kind TEXT NOT NULL,
name TEXT NOT NULL,
file_path TEXT,
data TEXT NOT NULL
)",
[],
)
.unwrap();
conn.execute(
"INSERT INTO magellan_meta (id, magellan_schema_version, sqlitegraph_schema_version, created_at)
VALUES (1, 4, 3, 0)",
[],
).unwrap();
create_schema(&mut conn, crate::storage::TEST_MAGELLAN_SCHEMA_VERSION).unwrap();
conn.execute(
"INSERT INTO graph_entities (kind, name, file_path, data) VALUES (?, ?, ?, ?)",
rusqlite::params!("function", "test_func", "test.rs", "{}"),
)
.unwrap();
let function_id: i64 = 1;
let cfg = create_linear_cfg();
let hash1 = "test_hash_v1";
let hash2 = "test_hash_v3";
let limits = PathLimits::default();
let paths1 = enumerate_paths_cached(&cfg, function_id, hash1, &limits, &mut conn).unwrap();
assert_eq!(paths1.len(), 1);
let paths2 = enumerate_paths_cached(&cfg, function_id, hash2, &limits, &mut conn).unwrap();
assert_eq!(paths2.len(), 1);
assert_eq!(paths1[0].blocks, paths2[0].blocks);
}
#[test]
fn test_enumerate_paths_cached_with_context() {
use super::super::{enumerate_paths_cached_with_context, EnumerationContext};
use crate::storage::create_schema;
let mut conn = rusqlite::Connection::open_in_memory().unwrap();
conn.execute(
"CREATE TABLE magellan_meta (
id INTEGER PRIMARY KEY CHECK (id = 1),
magellan_schema_version INTEGER NOT NULL,
sqlitegraph_schema_version INTEGER NOT NULL,
created_at INTEGER NOT NULL
)",
[],
)
.unwrap();
conn.execute(
"CREATE TABLE graph_entities (
id INTEGER PRIMARY KEY AUTOINCREMENT,
kind TEXT NOT NULL,
name TEXT NOT NULL,
file_path TEXT,
data TEXT NOT NULL
)",
[],
)
.unwrap();
conn.execute(
"INSERT INTO magellan_meta (id, magellan_schema_version, sqlitegraph_schema_version, created_at)
VALUES (1, 4, 3, 0)",
[],
).unwrap();
create_schema(&mut conn, crate::storage::TEST_MAGELLAN_SCHEMA_VERSION).unwrap();
conn.execute(
"INSERT INTO graph_entities (kind, name, file_path, data) VALUES (?, ?, ?, ?)",
rusqlite::params!("function", "test_func", "test.rs", "{}"),
)
.unwrap();
let function_id: i64 = 1;
let cfg = create_diamond_cfg();
let function_hash = "test_hash_ctx";
let limits = PathLimits::default();
let ctx = EnumerationContext::new(&cfg);
let paths1 = enumerate_paths_cached_with_context(
&cfg,
function_id,
function_hash,
&limits,
&ctx,
&mut conn,
)
.unwrap();
assert_eq!(paths1.len(), 2);
let paths2 = enumerate_paths_cached_with_context(
&cfg,
function_id,
function_hash,
&limits,
&ctx,
&mut conn,
)
.unwrap();
assert_eq!(paths2.len(), 2);
}
#[test]
fn test_estimate_path_count_linear_cfg() {
let cfg = create_linear_cfg();
let estimate = estimate_path_count(&cfg, 3);
assert_eq!(estimate, 1);
}
#[test]
fn test_estimate_path_count_diamond_cfg() {
let cfg = create_diamond_cfg();
let estimate = estimate_path_count(&cfg, 3);
assert_eq!(estimate, 2);
}
#[test]
fn test_estimate_path_count_single_loop() {
let cfg = create_loop_cfg();
let estimate = estimate_path_count(&cfg, 3);
assert_eq!(estimate, 4);
}
#[test]
fn test_estimate_path_count_loop_formula() {
let cfg = create_loop_cfg();
let estimate = estimate_path_count(&cfg, 3);
assert!(estimate >= 4); }
#[test]
fn test_estimate_path_count_increases_with_loop_limit() {
let cfg = create_loop_cfg();
let estimate3 = estimate_path_count(&cfg, 3);
let estimate5 = estimate_path_count(&cfg, 5);
assert!(estimate5 >= estimate3);
}
#[test]
fn test_estimate_path_count_monotonic_with_complexity() {
let linear = create_linear_cfg();
let diamond = create_diamond_cfg();
let loop_cfg = create_loop_cfg();
let linear_estimate = estimate_path_count(&linear, 3);
let diamond_estimate = estimate_path_count(&diamond, 3);
let _loop_estimate = estimate_path_count(&loop_cfg, 3);
assert!(linear_estimate <= diamond_estimate);
}
#[test]
fn test_check_path_explosion_safe() {
let cfg = create_linear_cfg();
let limits = PathLimits::default();
let result = check_path_explosion(&cfg, &limits);
assert!(result.is_none(), "Linear CFG should be safe");
}
#[test]
fn test_check_path_explosion_exceeds_limit() {
let cfg = create_diamond_cfg();
let limits = PathLimits {
max_length: 100,
max_paths: 1, loop_unroll_limit: 3,
};
let result = check_path_explosion(&cfg, &limits);
assert!(result.is_some(), "Should warn about path explosion");
if let Some(estimate) = result {
assert!(estimate > 1);
}
}
#[test]
fn test_estimate_path_count_no_overflow() {
let cfg = create_loop_cfg();
let estimate = estimate_path_count(&cfg, 1000);
assert!(estimate > 0);
assert!(estimate < usize::MAX);
}
fn create_large_linear_cfg(size: usize) -> Cfg {
let mut g = DiGraph::new();
for i in 0..size {
let kind = if i == 0 {
BlockKind::Entry
} else if i == size - 1 {
BlockKind::Exit
} else {
BlockKind::Normal
};
let terminator = if i == size - 1 {
Terminator::Return
} else {
Terminator::Goto { target: i + 1 }
};
let _node = g.add_node(BasicBlock {
id: i,
db_id: None,
kind,
statements: vec![],
terminator,
source_location: None,
coord_x: 0,
coord_y: 0,
coord_z: 0,
});
}
for i in 0..size - 1 {
let from = NodeIndex::new(i);
let to = NodeIndex::new(i + 1);
g.add_edge(from, to, EdgeType::Fallthrough);
}
g
}
fn create_large_diamond_cfg() -> Cfg {
let mut g = DiGraph::new();
let mut nodes = Vec::new();
for i in 0..21 {
let kind = if i == 0 {
BlockKind::Entry
} else if i % 2 == 0 && i > 0 {
BlockKind::Normal
} else if i == 20 {
BlockKind::Exit
} else {
BlockKind::Normal
};
let terminator = if i == 20 {
Terminator::Return
} else if i % 2 == 0 {
let target1 = i + 1;
let target2 = i + 2;
Terminator::SwitchInt {
targets: vec![target1],
otherwise: target2,
}
} else {
let merge = i + 1;
Terminator::Goto { target: merge }
};
let node = g.add_node(BasicBlock {
id: i,
db_id: None,
kind,
statements: vec![],
terminator,
source_location: None,
coord_x: 0,
coord_y: 0,
coord_z: 0,
});
nodes.push(node);
}
for i in (0..20).step_by(2) {
let from = nodes[i];
let to1 = nodes[i + 1];
let to2 = nodes[i + 2];
g.add_edge(from, to1, EdgeType::TrueBranch);
g.add_edge(from, to2, EdgeType::FalseBranch);
}
for i in (1..20).filter(|x| x % 2 == 1) {
let from = nodes[i];
let to = nodes[i + 1];
g.add_edge(from, to, EdgeType::Fallthrough);
}
g
}
#[test]
#[ignore = "benchmark test - run with cargo test -- --ignored"]
fn test_perf_linear_cfg_10_blocks() {
use std::time::Instant;
let cfg = create_large_linear_cfg(10);
let limits = PathLimits::default();
let start = Instant::now();
let paths = enumerate_paths(&cfg, &limits);
let duration = start.elapsed();
assert_eq!(paths.len(), 1);
assert!(
duration < Duration::from_millis(10),
"Linear 10-block CFG should be <10ms, took {:?}",
duration
);
println!("Linear 10 blocks: {:?}", duration);
}
#[test]
#[ignore = "benchmark test - run with cargo test -- --ignored"]
fn test_perf_linear_cfg_100_blocks() {
use std::time::Instant;
let cfg = create_large_linear_cfg(100);
let limits = PathLimits::default();
let start = Instant::now();
let paths = enumerate_paths(&cfg, &limits);
let duration = start.elapsed();
assert_eq!(paths.len(), 1);
assert!(
duration < Duration::from_millis(100),
"Linear 100-block CFG should be <100ms, took {:?}",
duration
);
println!("Linear 100 blocks: {:?}", duration);
}
#[test]
#[ignore = "benchmark test - run with cargo test -- --ignored"]
fn test_perf_diamond_cfg_10_branches() {
use std::time::Instant;
let cfg = create_large_diamond_cfg();
let limits = PathLimits::default();
let start = Instant::now();
let paths = enumerate_paths(&cfg, &limits);
let duration = start.elapsed();
assert!(paths.len() > 0);
assert!(
duration < Duration::from_millis(50),
"Diamond CFG should be <50ms, took {:?}",
duration
);
println!(
"Diamond 10 branches: {} paths in {:?}",
paths.len(),
duration
);
}
#[test]
#[ignore = "benchmark test - run with cargo test -- --ignored"]
fn test_perf_single_loop_unroll_3() {
use std::time::Instant;
let cfg = create_loop_cfg();
let limits = PathLimits::default().with_loop_unroll_limit(3);
let start = Instant::now();
let paths = enumerate_paths(&cfg, &limits);
let duration = start.elapsed();
assert!(paths.len() > 0);
assert!(
duration < Duration::from_millis(100),
"Single loop should be <100ms, took {:?}",
duration
);
println!(
"Single loop (unroll=3): {} paths in {:?}",
paths.len(),
duration
);
}
#[test]
#[ignore = "benchmark test - run with cargo test -- --ignored"]
fn test_perf_nested_loops() {
use crate::cfg::{BasicBlock, BlockKind, EdgeType, Terminator};
use std::time::Instant;
let mut g = DiGraph::new();
let b0 = g.add_node(BasicBlock {
id: 0,
db_id: None,
kind: BlockKind::Entry,
statements: vec![],
terminator: Terminator::Goto { target: 1 },
source_location: None,
coord_x: 0,
coord_y: 0,
coord_z: 0,
});
let b1 = g.add_node(BasicBlock {
id: 1,
db_id: None,
kind: BlockKind::Normal,
statements: vec![],
terminator: Terminator::SwitchInt {
targets: vec![2],
otherwise: 5,
},
source_location: None,
coord_x: 0,
coord_y: 0,
coord_z: 0,
});
let b2 = g.add_node(BasicBlock {
id: 2,
db_id: None,
kind: BlockKind::Normal,
statements: vec![],
terminator: Terminator::SwitchInt {
targets: vec![3],
otherwise: 1,
},
source_location: None,
coord_x: 0,
coord_y: 0,
coord_z: 0,
});
let b3 = g.add_node(BasicBlock {
id: 3,
db_id: None,
kind: BlockKind::Normal,
statements: vec![],
terminator: Terminator::Goto { target: 2 },
source_location: None,
coord_x: 0,
coord_y: 0,
coord_z: 0,
});
let b5 = g.add_node(BasicBlock {
id: 5,
db_id: None,
kind: BlockKind::Exit,
statements: vec![],
terminator: Terminator::Return,
source_location: None,
coord_x: 0,
coord_y: 0,
coord_z: 0,
});
g.add_edge(b0, b1, EdgeType::Fallthrough);
g.add_edge(b1, b2, EdgeType::TrueBranch);
g.add_edge(b1, b5, EdgeType::FalseBranch);
g.add_edge(b2, b3, EdgeType::TrueBranch);
g.add_edge(b2, b1, EdgeType::LoopBack);
g.add_edge(b3, b2, EdgeType::LoopBack);
let limits = PathLimits::default().with_loop_unroll_limit(2);
let start = Instant::now();
let paths = enumerate_paths(&g, &limits);
let duration = start.elapsed();
assert!(paths.len() > 0);
assert!(
duration < Duration::from_millis(500),
"Nested loops should be <500ms, took {:?}",
duration
);
println!(
"Nested 2 loops (unroll=2): {} paths in {:?}",
paths.len(),
duration
);
}
#[test]
#[ignore = "benchmark test - run with cargo test -- --ignored"]
fn test_perf_enumeration_context_reuse() {
use super::super::EnumerationContext;
use std::time::Instant;
let cfg = create_diamond_cfg();
let ctx = EnumerationContext::new(&cfg);
let limits = PathLimits::default();
let start = Instant::now();
for _ in 0..100 {
let _ = enumerate_paths_with_context(&cfg, &limits, &ctx);
}
let duration = start.elapsed();
println!("100 calls with context: {:?}", duration);
assert!(
duration < Duration::from_millis(100),
"100 cached calls should be <100ms, took {:?}",
duration
);
}
#[test]
#[ignore = "benchmark test - run with cargo test -- --ignored"]
fn test_perf_estimation_vs_actual() {
use std::time::Instant;
let cfg = create_diamond_cfg();
let start = Instant::now();
let estimate = estimate_path_count(&cfg, 3);
let est_duration = start.elapsed();
let start = Instant::now();
let limits = PathLimits::default();
let paths = enumerate_paths(&cfg, &limits);
let enum_duration = start.elapsed();
println!("Estimation: {} paths in {:?}", estimate, est_duration);
println!("Enumeration: {} paths in {:?}", paths.len(), enum_duration);
assert!(
est_duration.as_micros() < 1000,
"Estimation should be fast: {:?}",
est_duration
);
assert!(
enum_duration.as_micros() < 1000,
"Enumeration should be fast: {:?}",
enum_duration
);
}
}
#[derive(Debug, Clone)]
pub struct IncrementalPathsResult {
pub analyzed_functions: usize,
pub skipped_functions: usize,
pub paths: Vec<Path>,
}
pub fn enumerate_paths_incremental(
function_id: &str,
backend: &crate::storage::MirageDb,
repo_path: &std::path::Path,
since_revision: &str,
max_length: Option<usize>,
) -> anyhow::Result<IncrementalPathsResult> {
use crate::cfg::{load_cfg_from_db, PathLimits};
if function_id != "all" {
let fid = function_id
.parse::<i64>()
.map_err(|_| anyhow::anyhow!("Invalid function ID: {}", function_id))?;
let cfg = load_cfg_from_db(backend, fid)?;
let limits = PathLimits {
max_length: max_length.unwrap_or(1000),
..Default::default()
};
let paths = enumerate_paths(&cfg, &limits);
return Ok(IncrementalPathsResult {
analyzed_functions: 1,
skipped_functions: 0,
paths,
});
}
let changed_function_ids =
crate::cfg::git_utils::get_changed_functions(backend, repo_path, since_revision)?;
if changed_function_ids.is_empty() {
return Ok(IncrementalPathsResult {
analyzed_functions: 0,
skipped_functions: 0, paths: vec![],
});
}
let mut all_paths = Vec::new();
let mut analyzed = 0;
for fid in changed_function_ids {
match load_cfg_from_db(backend, fid) {
Ok(cfg) => {
let limits = PathLimits {
max_length: max_length.unwrap_or(1000),
..Default::default()
};
let paths = enumerate_paths(&cfg, &limits);
all_paths.extend(paths);
analyzed += 1;
}
Err(e) => {
tracing::warn!("Failed to load CFG for function {}: {}", fid, e);
}
}
}
Ok(IncrementalPathsResult {
analyzed_functions: analyzed,
skipped_functions: 0, paths: all_paths,
})
}
#[cfg(test)]
mod iterative_tests {
use super::*;
use crate::cfg::{BasicBlock, BlockKind, Cfg, EdgeType, Terminator};
use petgraph::graph::DiGraph;
fn create_simple_diamond_cfg() -> Cfg {
let mut g = DiGraph::new();
let b0 = g.add_node(BasicBlock {
id: 0,
db_id: None,
kind: BlockKind::Entry,
statements: vec![],
terminator: Terminator::SwitchInt {
targets: vec![1],
otherwise: 2,
},
source_location: None,
coord_x: 0,
coord_y: 0,
coord_z: 0,
});
let b1 = g.add_node(BasicBlock {
id: 1,
db_id: None,
kind: BlockKind::Normal,
statements: vec![],
terminator: Terminator::Goto { target: 3 },
source_location: None,
coord_x: 0,
coord_y: 0,
coord_z: 0,
});
let b2 = g.add_node(BasicBlock {
id: 2,
db_id: None,
kind: BlockKind::Normal,
statements: vec![],
terminator: Terminator::Goto { target: 3 },
source_location: None,
coord_x: 0,
coord_y: 0,
coord_z: 0,
});
let b3 = g.add_node(BasicBlock {
id: 3,
db_id: None,
kind: BlockKind::Exit,
statements: vec![],
terminator: Terminator::Return,
source_location: None,
coord_x: 0,
coord_y: 0,
coord_z: 0,
});
g.add_edge(b0, b1, EdgeType::TrueBranch);
g.add_edge(b0, b2, EdgeType::FalseBranch);
g.add_edge(b1, b3, EdgeType::Fallthrough);
g.add_edge(b2, b3, EdgeType::Fallthrough);
g
}
#[test]
fn test_enumerate_paths_iterative_diamond_cfg() {
let cfg = create_simple_diamond_cfg();
let limits = PathLimits::default();
let paths = enumerate_paths_iterative(&cfg, &limits);
assert_eq!(paths.len(), 2);
let path1 = &paths[0];
let path2 = &paths[1];
assert_ne!(path1.blocks, path2.blocks);
assert_eq!(path1.entry, 0);
assert_eq!(path1.exit, 3);
assert_eq!(path2.entry, 0);
assert_eq!(path2.exit, 3);
}
#[test]
fn test_enumerate_paths_iterative_produces_same_results_as_recursive() {
let cfg = create_simple_diamond_cfg();
let limits = PathLimits::default();
let recursive_paths = enumerate_paths(&cfg, &limits);
let iterative_paths = enumerate_paths_iterative(&cfg, &limits);
assert_eq!(recursive_paths.len(), iterative_paths.len());
let recursive_set: std::collections::HashSet<Vec<_>> =
recursive_paths.iter().map(|p| p.blocks.clone()).collect();
let iterative_set: std::collections::HashSet<Vec<_>> =
iterative_paths.iter().map(|p| p.blocks.clone()).collect();
assert_eq!(recursive_set, iterative_set);
}
#[test]
fn test_enumerate_paths_with_metadata_diamond() {
let cfg = create_simple_diamond_cfg();
let result = enumerate_paths_with_metadata(&cfg, &PathLimits::default());
assert_eq!(result.stats.total_paths, 2);
assert_eq!(result.stats.normal_paths, 2);
assert_eq!(result.stats.error_paths, 0);
assert_eq!(result.stats.avg_path_length, 3.0);
assert_eq!(result.stats.max_path_length, 3);
assert_eq!(result.limits_hit, LimitsHit::None);
}
#[test]
fn test_enumerate_paths_with_metadata_limits_hit() {
let cfg = create_simple_diamond_cfg();
let limits = PathLimits {
max_paths: 1,
..Default::default()
};
let result = enumerate_paths_with_metadata(&cfg, &limits);
assert_eq!(result.limits_hit, LimitsHit::MaxPaths);
assert_eq!(result.stats.total_paths, 1);
}
#[test]
fn test_enumerate_paths_iterative_with_loop() {
let mut g = DiGraph::new();
let b0 = g.add_node(BasicBlock {
id: 0,
db_id: None,
kind: BlockKind::Entry,
statements: vec![],
terminator: Terminator::SwitchInt {
targets: vec![1],
otherwise: 2,
},
source_location: None,
coord_x: 0,
coord_y: 0,
coord_z: 0,
});
let b1 = g.add_node(BasicBlock {
id: 1,
db_id: None,
kind: BlockKind::Normal,
statements: vec![],
terminator: Terminator::Goto { target: 0 }, source_location: None,
coord_x: 0,
coord_y: 0,
coord_z: 0,
});
let b2 = g.add_node(BasicBlock {
id: 2,
db_id: None,
kind: BlockKind::Exit,
statements: vec![],
terminator: Terminator::Return,
source_location: None,
coord_x: 0,
coord_y: 0,
coord_z: 0,
});
g.add_edge(b0, b1, EdgeType::TrueBranch);
g.add_edge(b0, b2, EdgeType::FalseBranch);
g.add_edge(b1, b0, EdgeType::LoopBack);
let limits = PathLimits {
loop_unroll_limit: 2,
..Default::default()
};
let paths = enumerate_paths_iterative(&g, &limits);
assert!(paths.len() >= 2);
}
#[test]
fn test_enumerate_paths_iterative_empty_cfg() {
let cfg = DiGraph::new();
let paths = enumerate_paths_iterative(&cfg, &PathLimits::default());
assert_eq!(paths.len(), 0);
}
}