use std::collections::HashMap;
use std::path::PathBuf;
use anyhow::Result;
use clap::Args;
use serde::Serialize;
use tldr_core::ssa::{build_dominator_tree, compute_dominance_frontier};
use tldr_core::{get_cfg_context, Language};
use crate::output::OutputFormat;
#[derive(Debug, Args)]
pub struct DominatorsArgs {
pub file: PathBuf,
pub function: String,
#[arg(long, short = 'l')]
pub lang: Option<Language>,
#[arg(long)]
pub idom_only: bool,
#[arg(long)]
pub frontier_only: bool,
}
#[derive(Debug, Serialize)]
struct DominatorsOutput {
function: String,
idom: HashMap<usize, usize>,
dom_tree: HashMap<usize, Vec<usize>>,
dom_frontier: HashMap<usize, Vec<usize>>,
entry_block: usize,
}
impl DominatorsArgs {
pub fn run(&self, format: OutputFormat, quiet: bool) -> Result<()> {
use crate::output::OutputWriter;
let writer = OutputWriter::new(format, quiet);
let language = self.lang.unwrap_or_else(|| {
Language::from_path(&self.file).unwrap_or(Language::Python)
});
writer.progress(&format!(
"Computing dominators for {} in {}...",
self.function,
self.file.display()
));
if !self.file.exists() {
return Err(anyhow::anyhow!(
"File not found: {}",
self.file.display()
));
}
let cfg = get_cfg_context(
self.file.to_str().unwrap_or_default(),
&self.function,
language,
)?;
let dom_tree = build_dominator_tree(&cfg)?;
let dom_frontier = compute_dominance_frontier(&cfg, &dom_tree)?;
let mut idom_map: HashMap<usize, usize> = HashMap::new();
let mut tree_map: HashMap<usize, Vec<usize>> = HashMap::new();
for (block_id, node) in &dom_tree.nodes {
if let Some(idom) = node.idom {
idom_map.insert(*block_id, idom);
}
tree_map.insert(*block_id, node.children.clone());
}
let frontier_map: HashMap<usize, Vec<usize>> = dom_frontier
.frontier
.iter()
.map(|(k, v)| {
let mut sorted: Vec<usize> = v.iter().copied().collect();
sorted.sort();
(*k, sorted)
})
.collect();
let output = DominatorsOutput {
function: dom_tree.function.clone(),
idom: idom_map,
dom_tree: tree_map,
dom_frontier: frontier_map,
entry_block: dom_tree.entry,
};
match format {
OutputFormat::Text => {
let text = format_dominators_text(&output, self.idom_only, self.frontier_only);
writer.write_text(&text)?;
}
OutputFormat::Json | OutputFormat::Compact => {
if self.idom_only {
#[derive(Serialize)]
struct IdomOnly {
function: String,
idom: HashMap<usize, usize>,
entry_block: usize,
}
writer.write(&IdomOnly {
function: output.function,
idom: output.idom,
entry_block: output.entry_block,
})?;
} else if self.frontier_only {
#[derive(Serialize)]
struct FrontierOnly {
function: String,
dom_frontier: HashMap<usize, Vec<usize>>,
}
writer.write(&FrontierOnly {
function: output.function,
dom_frontier: output.dom_frontier,
})?;
} else {
writer.write(&output)?;
}
}
OutputFormat::Dot => {
let dot = format_dominators_dot(&output);
writer.write_text(&dot)?;
}
OutputFormat::Sarif => {
writer.write(&output)?;
}
}
Ok(())
}
}
fn format_dominators_text(output: &DominatorsOutput, idom_only: bool, frontier_only: bool) -> String {
let mut lines = Vec::new();
lines.push(format!("Dominator Analysis: {}", output.function));
lines.push(format!("Entry Block: {}", output.entry_block));
lines.push(String::new());
if !frontier_only {
lines.push("Immediate Dominators (idom):".to_string());
let mut idom_items: Vec<_> = output.idom.iter().collect();
idom_items.sort_by_key(|(k, _)| *k);
for (block, idom) in idom_items {
lines.push(format!(" Block {} -> idom {}", block, idom));
}
lines.push(String::new());
}
if !idom_only && !frontier_only {
lines.push("Dominator Tree:".to_string());
let mut tree_items: Vec<_> = output.dom_tree.iter().collect();
tree_items.sort_by_key(|(k, _)| *k);
for (block, children) in tree_items {
if children.is_empty() {
lines.push(format!(" Block {} (leaf)", block));
} else {
lines.push(format!(" Block {} -> children {:?}", block, children));
}
}
lines.push(String::new());
}
if !idom_only {
lines.push("Dominance Frontier:".to_string());
let mut frontier_items: Vec<_> = output.dom_frontier.iter().collect();
frontier_items.sort_by_key(|(k, _)| *k);
for (block, frontier) in frontier_items {
if frontier.is_empty() {
lines.push(format!(" DF[{}] = {{}}", block));
} else {
lines.push(format!(" DF[{}] = {:?}", block, frontier));
}
}
}
lines.join("\n")
}
fn format_dominators_dot(output: &DominatorsOutput) -> String {
let mut lines = Vec::new();
lines.push(format!("digraph dominator_tree_{} {{", output.function));
lines.push(" rankdir=TB;".to_string());
lines.push(" node [shape=box];".to_string());
lines.push(String::new());
lines.push(format!(
" block_{} [label=\"Block {} (entry)\" style=filled fillcolor=lightblue];",
output.entry_block, output.entry_block
));
for block_id in output.dom_tree.keys() {
if *block_id != output.entry_block {
lines.push(format!(" block_{} [label=\"Block {}\"];", block_id, block_id));
}
}
lines.push(String::new());
lines.push(" // Dominator tree edges (solid)".to_string());
for (block, children) in &output.dom_tree {
for child in children {
lines.push(format!(" block_{} -> block_{};", block, child));
}
}
lines.push(String::new());
lines.push(" // Dominance frontier edges (dashed)".to_string());
for (block, frontier) in &output.dom_frontier {
for f in frontier {
lines.push(format!(
" block_{} -> block_{} [style=dashed color=red];",
block, f
));
}
}
lines.push("}".to_string());
lines.join("\n")
}