#![allow(missing_docs)]
use crate::ast::AstFunction;
use crate::errors::{AnalysisError, AnalysisResult};
use petgraph::graph::{DiGraph, NodeIndex};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use tracing::{debug, info};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BasicBlock {
pub id: usize,
pub statements: Vec<Statement>,
pub dominates: Vec<usize>,
pub dominated_by: Vec<usize>,
pub is_loop_header: bool,
pub is_exit: bool,
}
impl BasicBlock {
pub fn new(id: usize) -> Self {
Self {
id,
statements: Vec::new(),
dominates: Vec::new(),
dominated_by: Vec::new(),
is_loop_header: false,
is_exit: false,
}
}
pub fn add_statement(&mut self, stmt: Statement) {
self.statements.push(stmt);
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum Statement {
Assignment { target: String, value: String },
Branch { condition: String },
StateMutation { variable: String, value: String },
FunctionCall { function: String, args: Vec<String> },
Return { value: Option<String> },
Generic { code: String },
}
#[derive(Debug, Clone)]
pub struct ControlFlowGraph {
graph: DiGraph<BasicBlock, String>,
block_map: HashMap<usize, NodeIndex>,
entry_block: usize,
exit_blocks: Vec<usize>,
next_id: usize,
}
impl ControlFlowGraph {
pub fn new() -> Self {
let mut cfg = Self {
graph: DiGraph::new(),
block_map: HashMap::new(),
entry_block: 0,
exit_blocks: Vec::new(),
next_id: 0,
};
let entry = cfg.new_block();
cfg.entry_block = entry;
cfg
}
pub fn new_block(&mut self) -> usize {
let id = self.next_id;
self.next_id += 1;
let block = BasicBlock::new(id);
let node_idx = self.graph.add_node(block);
self.block_map.insert(id, node_idx);
id
}
pub fn add_edge(
&mut self,
from: usize,
to: usize,
label: impl Into<String>,
) -> AnalysisResult<()> {
let from_idx = self
.block_map
.get(&from)
.copied()
.ok_or_else(|| AnalysisError::cfg("Block not found"))?;
let to_idx = self
.block_map
.get(&to)
.copied()
.ok_or_else(|| AnalysisError::cfg("Target block not found"))?;
self.graph.add_edge(from_idx, to_idx, label.into());
Ok(())
}
pub fn add_statement(&mut self, block_id: usize, stmt: Statement) -> AnalysisResult<()> {
let node_idx = self
.block_map
.get(&block_id)
.copied()
.ok_or_else(|| AnalysisError::cfg("Block not found"))?;
if let Some(block) = self.graph.node_weight_mut(node_idx) {
block.add_statement(stmt);
Ok(())
} else {
Err(AnalysisError::cfg("Failed to get block"))
}
}
pub fn mark_exit(&mut self, block_id: usize) -> AnalysisResult<()> {
let node_idx = self
.block_map
.get(&block_id)
.copied()
.ok_or_else(|| AnalysisError::cfg("Block not found"))?;
if let Some(block) = self.graph.node_weight_mut(node_idx) {
block.is_exit = true;
self.exit_blocks.push(block_id);
Ok(())
} else {
Err(AnalysisError::cfg("Failed to get block"))
}
}
pub fn compute_dominance(&mut self) -> AnalysisResult<()> {
info!("Computing dominance relationships for CFG");
self.compute_dominators()?;
let dominators: HashMap<usize, Vec<usize>> = self
.block_map
.iter()
.map(|(&id, &idx)| {
let dominates = self
.graph
.node_weight(idx)
.map(|b| b.dominates.clone())
.unwrap_or_default();
(id, dominates)
})
.collect();
for (dominator_id, dominated_blocks) in dominators {
for dominated_id in dominated_blocks {
let node_idx = self
.block_map
.get(&dominated_id)
.copied()
.ok_or_else(|| AnalysisError::cfg("Block not found"))?;
if let Some(block) = self.graph.node_weight_mut(node_idx) {
block.dominated_by.push(dominator_id);
}
}
}
Ok(())
}
fn compute_dominators(&mut self) -> AnalysisResult<()> {
let blocks: Vec<usize> = self.block_map.keys().copied().collect();
if blocks.is_empty() {
return Ok(());
}
let mut dominators: HashMap<usize, Vec<usize>> =
blocks.iter().map(|&id| (id, blocks.clone())).collect();
if let Some(entry_doms) = dominators.get_mut(&self.entry_block) {
entry_doms.clear();
entry_doms.push(self.entry_block);
}
let mut changed = true;
let mut iterations = 0;
const MAX_ITERATIONS: usize = 100;
while changed && iterations < MAX_ITERATIONS {
changed = false;
iterations += 1;
for &block_id in &blocks {
if block_id == self.entry_block {
continue;
}
let node_idx = self
.block_map
.get(&block_id)
.copied()
.ok_or_else(|| AnalysisError::cfg("Block not found"))?;
let mut pred_dominators: Option<Vec<usize>> = None;
for pred_idx in self
.graph
.neighbors_directed(node_idx, petgraph::Direction::Incoming)
{
let pred_id = self
.graph
.node_weight(pred_idx)
.map(|b| b.id)
.ok_or_else(|| AnalysisError::cfg("Predecessor not found"))?;
let pred_doms = dominators
.get(&pred_id)
.cloned()
.ok_or_else(|| AnalysisError::cfg("Predecessor dominators not found"))?;
pred_dominators = match pred_dominators {
None => Some(pred_doms),
Some(current) => {
let intersection: Vec<_> = current
.iter()
.filter(|d| pred_doms.contains(d))
.copied()
.collect();
Some(intersection)
}
};
}
if let Some(mut pred_doms) = pred_dominators {
pred_doms.push(block_id);
pred_doms.sort();
pred_doms.dedup();
if let Some(old_doms) = dominators.get_mut(&block_id) {
if *old_doms != pred_doms {
*old_doms = pred_doms;
changed = true;
}
}
}
}
}
debug!(
"Dominance computation converged after {} iterations",
iterations
);
for (block_id, doms) in dominators {
if let Some(node_idx) = self.block_map.get(&block_id).copied() {
if let Some(block) = self.graph.node_weight_mut(node_idx) {
block.dominates = doms.into_iter().filter(|&id| id != block_id).collect();
}
}
}
Ok(())
}
pub fn dominates(&self, a: usize, b: usize) -> bool {
self.block_map
.get(&a)
.and_then(|&idx| self.graph.node_weight(idx))
.map(|block| block.dominates.contains(&b))
.unwrap_or(false)
}
pub fn detect_loops(&mut self) -> AnalysisResult<Vec<Loop>> {
info!("Detecting loops in CFG");
let mut loops = Vec::new();
let blocks: Vec<usize> = self.block_map.keys().copied().collect();
for &block_id in &blocks {
let node_idx = self
.block_map
.get(&block_id)
.copied()
.ok_or_else(|| AnalysisError::cfg("Block not found"))?;
let successors: Vec<_> = self.graph.neighbors(node_idx).collect();
for succ_idx in successors {
let succ_id = self
.graph
.node_weight(succ_idx)
.map(|b| b.id)
.ok_or_else(|| AnalysisError::cfg("Successor not found"))?;
if self.dominates(succ_id, block_id) {
loops.push(Loop {
header: succ_id,
back_edges: vec![(block_id, succ_id)],
body_blocks: vec![succ_id],
});
if let Some(block) = self.graph.node_weight_mut(succ_idx) {
block.is_loop_header = true;
}
}
}
}
info!("Found {} loops", loops.len());
Ok(loops)
}
pub fn reachable(&self, from: usize) -> AnalysisResult<Vec<usize>> {
let start_idx = self
.block_map
.get(&from)
.copied()
.ok_or_else(|| AnalysisError::cfg("Block not found"))?;
let mut reachable = Vec::new();
let mut visited = std::collections::HashSet::new();
let mut queue = vec![start_idx];
while let Some(node_idx) = queue.pop() {
if !visited.insert(node_idx) {
continue;
}
if let Some(block) = self.graph.node_weight(node_idx) {
reachable.push(block.id);
}
for succ_idx in self.graph.neighbors(node_idx) {
queue.push(succ_idx);
}
}
Ok(reachable)
}
pub fn entry(&self) -> usize {
self.entry_block
}
pub fn exits(&self) -> Vec<usize> {
self.exit_blocks.clone()
}
pub fn block_count(&self) -> usize {
self.graph.node_count()
}
}
impl Default for ControlFlowGraph {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone)]
pub struct Loop {
pub header: usize,
pub back_edges: Vec<(usize, usize)>,
pub body_blocks: Vec<usize>,
}
pub struct CfgBuilder;
impl CfgBuilder {
pub fn build(function: &AstFunction) -> AnalysisResult<ControlFlowGraph> {
debug!("Building CFG for function '{}'", function.name);
let mut cfg = ControlFlowGraph::new();
let entry = cfg.entry();
cfg.mark_exit(entry)?;
cfg.compute_dominance()?;
cfg.detect_loops()?;
Ok(cfg)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_cfg_creation() {
let mut cfg = ControlFlowGraph::new();
let block1 = cfg.entry();
let block2 = cfg.new_block();
let block3 = cfg.new_block();
assert_eq!(cfg.block_count(), 3);
assert_eq!(block1, 0);
assert_eq!(block2, 1);
assert_eq!(block3, 2);
}
#[test]
fn test_cfg_edges() {
let mut cfg = ControlFlowGraph::new();
let b1 = cfg.entry();
let b2 = cfg.new_block();
let b3 = cfg.new_block();
cfg.add_edge(b1, b2, "true").ok();
cfg.add_edge(b1, b3, "false").ok();
assert_eq!(cfg.block_count(), 3);
}
#[test]
fn test_cfg_exit_marking() {
let mut cfg = ControlFlowGraph::new();
let entry = cfg.entry();
cfg.mark_exit(entry).ok();
assert!(cfg.exits().contains(&entry));
}
}