use std::collections::{HashMap, HashSet};
use std::path::PathBuf;
use crate::types::{CfgInfo, RefType, VarRef};
pub use super::chains::{
BlockReachingDefs, DefUseChain, Definition, ReachingDefsReport, ReachingDefsStats,
UninitSeverity, UninitializedUse, Use, UseDefChain,
};
#[derive(Debug, Clone)]
pub struct ReachingDefinitions {
pub reaching_in: HashMap<usize, HashSet<DefId>>,
pub reaching_out: HashMap<usize, HashSet<DefId>>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct DefId {
pub ref_index: usize,
pub line: u32,
}
pub fn compute_reaching_definitions(cfg: &CfgInfo, refs: &[VarRef]) -> ReachingDefinitions {
if cfg.blocks.is_empty() {
return ReachingDefinitions {
reaching_in: HashMap::new(),
reaching_out: HashMap::new(),
};
}
let mut predecessors: HashMap<usize, Vec<usize>> = HashMap::new();
for block in &cfg.blocks {
predecessors.insert(block.id, Vec::new());
}
for edge in &cfg.edges {
predecessors.entry(edge.to).or_default().push(edge.from);
}
let defs: Vec<(DefId, &VarRef)> = refs
.iter()
.enumerate()
.filter(|(_, r)| matches!(r.ref_type, RefType::Definition | RefType::Update))
.map(|(i, r)| {
(
DefId {
ref_index: i,
line: r.line,
},
r,
)
})
.collect();
let mut gen: HashMap<usize, HashSet<DefId>> = HashMap::new();
let mut kill: HashMap<usize, HashSet<DefId>> = HashMap::new();
let def_to_block: HashMap<DefId, usize> = {
let mut map = HashMap::new();
for (def_id, var_ref) in &defs {
let mut best_block: Option<(usize, u32)> = None; for block in &cfg.blocks {
let (start, end) = block.lines;
if var_ref.line >= start && var_ref.line <= end {
let size = end - start + 1;
if best_block.is_none()
|| size > best_block.unwrap().1
|| (size == best_block.unwrap().1 && block.id > best_block.unwrap().0)
{
best_block = Some((block.id, size));
}
}
}
if let Some((block_id, _)) = best_block {
map.insert(*def_id, block_id);
} else {
let entry_id = cfg.blocks.first().map(|b| b.id).unwrap_or(0);
map.insert(*def_id, entry_id);
}
}
map
};
for block in &cfg.blocks {
let mut block_gen = HashSet::new();
let mut block_kill = HashSet::new();
let mut last_def_for_var: HashMap<&str, DefId> = HashMap::new();
for (def_id, var_ref) in &defs {
if def_to_block.get(def_id) == Some(&block.id) {
if let Some(&prev) = last_def_for_var.get(var_ref.name.as_str()) {
block_kill.insert(prev);
}
last_def_for_var.insert(&var_ref.name, *def_id);
}
}
block_gen.extend(last_def_for_var.values().copied());
let vars_defined: HashSet<&str> = last_def_for_var.keys().copied().collect();
for (def_id, var_ref) in &defs {
if vars_defined.contains(var_ref.name.as_str()) {
if def_to_block.get(def_id) != Some(&block.id) {
block_kill.insert(*def_id);
}
}
}
gen.insert(block.id, block_gen);
kill.insert(block.id, block_kill);
}
let mut reaching_in: HashMap<usize, HashSet<DefId>> = HashMap::new();
let mut reaching_out: HashMap<usize, HashSet<DefId>> = HashMap::new();
for block in &cfg.blocks {
reaching_in.insert(block.id, HashSet::new());
reaching_out.insert(block.id, gen.get(&block.id).cloned().unwrap_or_default());
}
let max_iterations = 100; for _ in 0..max_iterations {
let mut changed = false;
for block in &cfg.blocks {
let mut new_in = HashSet::new();
if let Some(preds) = predecessors.get(&block.id) {
for &pred in preds {
if let Some(pred_out) = reaching_out.get(&pred) {
new_in.extend(pred_out.iter().copied());
}
}
}
let block_gen = gen.get(&block.id).cloned().unwrap_or_default();
let block_kill = kill.get(&block.id).cloned().unwrap_or_default();
let in_minus_kill: HashSet<DefId> = new_in
.iter()
.filter(|d| !block_kill.contains(d))
.copied()
.collect();
let new_out: HashSet<DefId> = block_gen.union(&in_minus_kill).copied().collect();
if new_in != *reaching_in.get(&block.id).unwrap_or(&HashSet::new()) {
changed = true;
reaching_in.insert(block.id, new_in);
}
if new_out != *reaching_out.get(&block.id).unwrap_or(&HashSet::new()) {
changed = true;
reaching_out.insert(block.id, new_out);
}
}
if !changed {
break;
}
}
ReachingDefinitions {
reaching_in,
reaching_out,
}
}
pub fn definitions_reaching_line(
reaching: &ReachingDefinitions,
cfg: &CfgInfo,
refs: &[VarRef],
line: u32,
variable: Option<&str>,
) -> Vec<DefId> {
let block_id = cfg
.blocks
.iter()
.find(|b| line >= b.lines.0 && line <= b.lines.1)
.map(|b| b.id);
let Some(block_id) = block_id else {
return Vec::new();
};
let Some(reaching_in) = reaching.reaching_in.get(&block_id) else {
return Vec::new();
};
let result: Vec<DefId> = reaching_in
.iter()
.filter(|def_id| {
if let Some(var_name) = variable {
if let Some(var_ref) = refs.get(def_id.ref_index) {
var_ref.name == var_name
} else {
false
}
} else {
true
}
})
.copied()
.collect();
result
}
pub fn build_def_use_chains(
reaching: &ReachingDefinitions,
cfg: &CfgInfo,
refs: &[VarRef],
) -> Vec<DefUseChain> {
let mut def_chains: HashMap<DefId, DefUseChain> = HashMap::new();
for (index, var_ref) in refs.iter().enumerate() {
if matches!(var_ref.ref_type, RefType::Definition | RefType::Update) {
let def_id = DefId {
ref_index: index,
line: var_ref.line,
};
let block_id = cfg
.blocks
.iter()
.find(|b| var_ref.line >= b.lines.0 && var_ref.line <= b.lines.1)
.map(|b| b.id)
.unwrap_or(0);
def_chains.insert(
def_id,
DefUseChain {
definition: Definition {
var: var_ref.name.clone(),
line: var_ref.line,
column: Some(var_ref.column),
block: block_id,
source_text: None,
},
uses: Vec::new(),
},
);
}
}
let defs_by_var: HashMap<String, Vec<DefId>> = {
let mut map: HashMap<String, Vec<DefId>> = HashMap::new();
for (def_id, chain) in &def_chains {
map.entry(chain.definition.var.clone())
.or_default()
.push(*def_id);
}
map
};
for var_ref in refs.iter() {
if !matches!(var_ref.ref_type, RefType::Use) {
continue;
}
let block_id = cfg
.blocks
.iter()
.find(|b| var_ref.line >= b.lines.0 && var_ref.line <= b.lines.1)
.map(|b| b.id);
let Some(block_id) = block_id else {
continue;
};
let Some(reaching_in) = reaching.reaching_in.get(&block_id) else {
continue;
};
let block_defs_before_use: HashSet<DefId> = refs
.iter()
.enumerate()
.filter(|(_, r)| {
matches!(r.ref_type, RefType::Definition | RefType::Update)
&& r.name == var_ref.name
&& r.line < var_ref.line
})
.filter_map(|(i, r)| {
let in_same_block = cfg
.blocks
.iter()
.find(|b| r.line >= b.lines.0 && r.line <= b.lines.1)
.map(|b| b.id)
== Some(block_id);
if in_same_block {
Some(DefId {
ref_index: i,
line: r.line,
})
} else {
None
}
})
.collect();
let Some(var_defs) = defs_by_var.get(&var_ref.name) else {
continue;
};
let last_local_def = block_defs_before_use.iter().max_by_key(|d| d.line);
let use_site = Use {
line: var_ref.line,
column: Some(var_ref.column),
block: block_id,
context: var_ref.context.as_ref().map(|c| format!("{:?}", c)),
};
for &def_id in var_defs {
let reaches = if let Some(last_local) = last_local_def {
def_id == *last_local
} else {
reaching_in.contains(&def_id)
};
if reaches {
if let Some(chain) = def_chains.get_mut(&def_id) {
chain.uses.push(use_site.clone());
}
}
}
}
def_chains.into_values().collect()
}
pub fn build_use_def_chains(
reaching: &ReachingDefinitions,
cfg: &CfgInfo,
refs: &[VarRef],
) -> Vec<UseDefChain> {
let mut chains = Vec::new();
let defs_by_var: HashMap<&str, Vec<(DefId, &VarRef)>> = {
let mut map: HashMap<&str, Vec<(DefId, &VarRef)>> = HashMap::new();
for (index, var_ref) in refs.iter().enumerate() {
if matches!(var_ref.ref_type, RefType::Definition | RefType::Update) {
let def_id = DefId {
ref_index: index,
line: var_ref.line,
};
map.entry(var_ref.name.as_str())
.or_default()
.push((def_id, var_ref));
}
}
map
};
for var_ref in refs.iter() {
if !matches!(var_ref.ref_type, RefType::Use) {
continue;
}
let block_id = cfg
.blocks
.iter()
.find(|b| var_ref.line >= b.lines.0 && var_ref.line <= b.lines.1)
.map(|b| b.id);
let Some(block_id) = block_id else {
continue;
};
let Some(reaching_in) = reaching.reaching_in.get(&block_id) else {
continue;
};
let Some(var_defs) = defs_by_var.get(var_ref.name.as_str()) else {
chains.push(UseDefChain {
use_site: Use {
line: var_ref.line,
column: Some(var_ref.column),
block: block_id,
context: var_ref.context.as_ref().map(|c| format!("{:?}", c)),
},
var: var_ref.name.clone(),
reaching_defs: Vec::new(),
});
continue;
};
let block_defs_before_use: Vec<&(DefId, &VarRef)> = var_defs
.iter()
.filter(|(_, r)| {
let in_same_block = cfg
.blocks
.iter()
.find(|b| r.line >= b.lines.0 && r.line <= b.lines.1)
.map(|b| b.id)
== Some(block_id);
in_same_block && r.line < var_ref.line
})
.collect();
let last_local_def = block_defs_before_use
.iter()
.max_by_key(|(d, _)| d.line)
.map(|(d, _)| *d);
let mut reaching_defs = Vec::new();
for (def_id, def_ref) in var_defs {
let reaches = if let Some(last_local) = last_local_def {
*def_id == last_local
} else {
reaching_in.contains(def_id)
};
if reaches {
let def_block_id = cfg
.blocks
.iter()
.find(|b| def_ref.line >= b.lines.0 && def_ref.line <= b.lines.1)
.map(|b| b.id)
.unwrap_or(0);
reaching_defs.push(Definition {
var: def_ref.name.clone(),
line: def_ref.line,
column: Some(def_ref.column),
block: def_block_id,
source_text: None,
});
}
}
chains.push(UseDefChain {
use_site: Use {
line: var_ref.line,
column: Some(var_ref.column),
block: block_id,
context: var_ref.context.as_ref().map(|c| format!("{:?}", c)),
},
var: var_ref.name.clone(),
reaching_defs,
});
}
chains
}
pub fn build_reaching_defs_report(
cfg: &CfgInfo,
refs: &[VarRef],
file_path: PathBuf,
) -> ReachingDefsReport {
build_reaching_defs_report_with_params(cfg, refs, file_path, &[])
}
pub fn build_reaching_defs_report_with_params(
cfg: &CfgInfo,
refs: &[VarRef],
file_path: PathBuf,
params: &[String],
) -> ReachingDefsReport {
let reaching = compute_reaching_definitions(cfg, refs);
let detected_params: Vec<String> = if params.is_empty() {
let min_def_line = refs
.iter()
.filter(|r| matches!(r.ref_type, RefType::Definition))
.map(|r| r.line)
.min();
if let Some(param_line) = min_def_line {
refs.iter()
.filter(|r| matches!(r.ref_type, RefType::Definition) && r.line == param_line)
.map(|r| r.name.clone())
.collect()
} else {
Vec::new()
}
} else {
params.to_vec()
};
let def_use_chains = build_def_use_chains(&reaching, cfg, refs);
let use_def_chains = build_use_def_chains(&reaching, cfg, refs);
let blocks: Vec<BlockReachingDefs> = cfg
.blocks
.iter()
.map(|b| {
let in_set = reaching
.reaching_in
.get(&b.id)
.map(|set| def_ids_to_definitions(set, refs, cfg))
.unwrap_or_default();
let out_set = reaching
.reaching_out
.get(&b.id)
.map(|set| def_ids_to_definitions(set, refs, cfg))
.unwrap_or_default();
let gen = compute_block_gen(b, refs, cfg);
let kill = compute_block_kill(b, refs, cfg);
BlockReachingDefs {
id: b.id,
lines: b.lines,
gen,
kill,
in_set,
out: out_set,
}
})
.collect();
let def_count = refs
.iter()
.filter(|r| matches!(r.ref_type, RefType::Definition | RefType::Update))
.count();
let use_count = refs
.iter()
.filter(|r| matches!(r.ref_type, RefType::Use))
.count();
let uninitialized = detect_uninitialized(&reaching, cfg, refs, &detected_params, &[]);
let stats = ReachingDefsStats {
definitions: def_count,
uses: use_count,
blocks: cfg.blocks.len(),
iterations: 0, uninitialized_count: uninitialized.len(),
};
let total_blocks = cfg.blocks.len();
let blocks_with_defs = blocks.iter().filter(|b| !b.gen.is_empty()).count();
let confidence = if total_blocks == 0 {
crate::dfg::chains::Confidence::Low
} else if blocks_with_defs > 0 {
crate::dfg::chains::Confidence::High
} else {
crate::dfg::chains::Confidence::Medium
};
ReachingDefsReport {
function: cfg.function.clone(),
file: file_path,
blocks,
def_use_chains,
use_def_chains,
uninitialized,
stats,
uncertain_defs: Vec::new(),
confidence,
}
}
fn def_ids_to_definitions(
def_ids: &HashSet<DefId>,
refs: &[VarRef],
cfg: &CfgInfo,
) -> Vec<Definition> {
def_ids
.iter()
.filter_map(|def_id| {
let var_ref = refs.get(def_id.ref_index)?;
let block_id = cfg
.blocks
.iter()
.find(|b| var_ref.line >= b.lines.0 && var_ref.line <= b.lines.1)
.map(|b| b.id)
.unwrap_or(0);
Some(Definition {
var: var_ref.name.clone(),
line: var_ref.line,
column: Some(var_ref.column),
block: block_id,
source_text: None,
})
})
.collect()
}
fn compute_block_gen(
block: &crate::types::CfgBlock,
refs: &[VarRef],
cfg: &CfgInfo,
) -> Vec<Definition> {
let (start_line, end_line) = block.lines;
let is_entry = cfg.blocks.first().map(|b| b.id) == Some(block.id);
let mut last_def_for_var: HashMap<&str, (usize, &VarRef)> = HashMap::new();
for (index, var_ref) in refs.iter().enumerate() {
if matches!(var_ref.ref_type, RefType::Definition | RefType::Update) {
let in_block = var_ref.line >= start_line && var_ref.line <= end_line;
let is_orphan = is_entry
&& var_ref.line < start_line
&& !cfg
.blocks
.iter()
.any(|b| var_ref.line >= b.lines.0 && var_ref.line <= b.lines.1);
if in_block || is_orphan {
last_def_for_var.insert(&var_ref.name, (index, var_ref));
}
}
}
last_def_for_var
.values()
.map(|(_, var_ref)| Definition {
var: var_ref.name.clone(),
line: var_ref.line,
column: Some(var_ref.column),
block: block.id,
source_text: None,
})
.collect()
}
fn compute_block_kill(
block: &crate::types::CfgBlock,
refs: &[VarRef],
cfg: &CfgInfo,
) -> Vec<Definition> {
let (start_line, end_line) = block.lines;
let is_entry = cfg.blocks.first().map(|b| b.id) == Some(block.id);
let vars_defined_in_block: HashSet<&str> = refs
.iter()
.filter(|r| {
if !matches!(r.ref_type, RefType::Definition | RefType::Update) {
return false;
}
let in_block = r.line >= start_line && r.line <= end_line;
let is_orphan = is_entry
&& r.line < start_line
&& !cfg
.blocks
.iter()
.any(|b| r.line >= b.lines.0 && r.line <= b.lines.1);
in_block || is_orphan
})
.map(|r| r.name.as_str())
.collect();
refs.iter()
.filter(|r| {
if !matches!(r.ref_type, RefType::Definition | RefType::Update) {
return false;
}
if !vars_defined_in_block.contains(r.name.as_str()) {
return false;
}
let in_block = r.line >= start_line && r.line <= end_line;
let is_orphan = is_entry
&& r.line < start_line
&& !cfg
.blocks
.iter()
.any(|b| r.line >= b.lines.0 && r.line <= b.lines.1);
!(in_block || is_orphan)
})
.map(|var_ref| {
let def_block_id = cfg
.blocks
.iter()
.find(|b| var_ref.line >= b.lines.0 && var_ref.line <= b.lines.1)
.map(|b| b.id)
.unwrap_or(0);
Definition {
var: var_ref.name.clone(),
line: var_ref.line,
column: Some(var_ref.column),
block: def_block_id,
source_text: None,
}
})
.collect()
}
pub fn detect_uninitialized(
reaching: &ReachingDefinitions,
cfg: &CfgInfo,
refs: &[VarRef],
params: &[String],
globals: &[String],
) -> Vec<UninitializedUse> {
let mut uninit = Vec::new();
let pre_initialized: HashSet<&str> = params
.iter()
.chain(globals.iter())
.map(|s| s.as_str())
.collect();
let defs_by_var: HashMap<&str, Vec<DefId>> = {
let mut map: HashMap<&str, Vec<DefId>> = HashMap::new();
for (index, var_ref) in refs.iter().enumerate() {
if matches!(var_ref.ref_type, RefType::Definition | RefType::Update) {
map.entry(var_ref.name.as_str()).or_default().push(DefId {
ref_index: index,
line: var_ref.line,
});
}
}
map
};
let mut predecessors: HashMap<usize, Vec<usize>> = HashMap::new();
for block in &cfg.blocks {
predecessors.insert(block.id, Vec::new());
}
for edge in &cfg.edges {
predecessors.entry(edge.to).or_default().push(edge.from);
}
for var_ref in refs.iter() {
if !matches!(var_ref.ref_type, RefType::Use) {
continue;
}
if pre_initialized.contains(var_ref.name.as_str()) {
continue;
}
let block_id = cfg
.blocks
.iter()
.find(|b| var_ref.line >= b.lines.0 && var_ref.line <= b.lines.1)
.map(|b| b.id);
let Some(block_id) = block_id else {
uninit.push(UninitializedUse {
var: var_ref.name.clone(),
line: var_ref.line,
column: Some(var_ref.column),
block: 0,
reason: "use not within any block".to_string(),
severity: UninitSeverity::Definite,
});
continue;
};
let var_defs = defs_by_var.get(var_ref.name.as_str());
if var_defs.is_none() || var_defs.unwrap().is_empty() {
uninit.push(UninitializedUse {
var: var_ref.name.clone(),
line: var_ref.line,
column: Some(var_ref.column),
block: block_id,
reason: "no definition of this variable exists".to_string(),
severity: UninitSeverity::Definite,
});
continue;
}
let var_defs = var_defs.unwrap();
let local_defs_before_use: Vec<&DefId> = var_defs
.iter()
.filter(|d| {
if let Some(def_ref) = refs.get(d.ref_index) {
let in_same_block = cfg
.blocks
.iter()
.find(|b| def_ref.line >= b.lines.0 && def_ref.line <= b.lines.1)
.map(|b| b.id)
== Some(block_id);
in_same_block && def_ref.line < var_ref.line
} else {
false
}
})
.collect();
if !local_defs_before_use.is_empty() {
continue;
}
let reaching_in = reaching.reaching_in.get(&block_id);
let reaching_this_var: Vec<&DefId> = var_defs
.iter()
.filter(|d| reaching_in.map(|r| r.contains(d)).unwrap_or(false))
.collect();
if reaching_this_var.is_empty() {
uninit.push(UninitializedUse {
var: var_ref.name.clone(),
line: var_ref.line,
column: Some(var_ref.column),
block: block_id,
reason: "definition exists but does not reach this use on any path".to_string(),
severity: UninitSeverity::Possible,
});
continue;
}
let has_undefined_path = {
let mut no_def_reaches: HashSet<usize> = HashSet::new();
no_def_reaches.insert(cfg.entry_block);
let mut successors: HashMap<usize, Vec<usize>> = HashMap::new();
for block in &cfg.blocks {
successors.insert(block.id, Vec::new());
}
for edge in &cfg.edges {
successors.entry(edge.from).or_default().push(edge.to);
}
let blocks_with_def: HashSet<usize> = var_defs
.iter()
.filter_map(|d| {
refs.get(d.ref_index).and_then(|r| {
let mut best_block: Option<(usize, u32)> = None;
for block in &cfg.blocks {
let (start, end) = block.lines;
if r.line >= start && r.line <= end {
let size = end - start + 1;
if best_block.is_none()
|| size > best_block.unwrap().1
|| (size == best_block.unwrap().1
&& block.id > best_block.unwrap().0)
{
best_block = Some((block.id, size));
}
}
}
best_block.map(|(id, _)| id)
})
})
.collect();
let mut changed = true;
let mut iterations = 0;
while changed && iterations < 100 {
changed = false;
iterations += 1;
for block in &cfg.blocks {
if no_def_reaches.contains(&block.id) && !blocks_with_def.contains(&block.id) {
if let Some(succs) = successors.get(&block.id) {
for &succ in succs {
if !no_def_reaches.contains(&succ) {
no_def_reaches.insert(succ);
changed = true;
}
}
}
}
}
}
no_def_reaches.contains(&block_id)
};
if has_undefined_path {
uninit.push(UninitializedUse {
var: var_ref.name.clone(),
line: var_ref.line,
column: Some(var_ref.column),
block: block_id,
reason: "definition does not reach this use on all paths".to_string(),
severity: UninitSeverity::Possible,
});
}
}
uninit
}
pub fn detect_uninitialized_simple(
reaching: &ReachingDefinitions,
cfg: &CfgInfo,
refs: &[VarRef],
) -> Vec<UninitializedUse> {
detect_uninitialized(reaching, cfg, refs, &[], &[])
}
pub fn compute_rpo(cfg: &CfgInfo) -> Vec<usize> {
let mut visited = HashSet::new();
let mut postorder = Vec::new();
fn dfs(
block_id: usize,
successors: &HashMap<usize, Vec<usize>>,
visited: &mut HashSet<usize>,
postorder: &mut Vec<usize>,
) {
if visited.insert(block_id) {
if let Some(succs) = successors.get(&block_id) {
for &succ in succs {
dfs(succ, successors, visited, postorder);
}
}
postorder.push(block_id);
}
}
let mut successors: HashMap<usize, Vec<usize>> = HashMap::new();
for block in &cfg.blocks {
successors.insert(block.id, Vec::new());
}
for edge in &cfg.edges {
successors.entry(edge.from).or_default().push(edge.to);
}
dfs(cfg.entry_block, &successors, &mut visited, &mut postorder);
postorder.reverse();
postorder
}
#[derive(Debug, Clone)]
pub struct ReachingDefinitionsWithStats {
pub reaching: ReachingDefinitions,
pub iterations: usize,
}
pub fn compute_reaching_definitions_rpo(
cfg: &CfgInfo,
refs: &[VarRef],
) -> ReachingDefinitionsWithStats {
if cfg.blocks.is_empty() {
return ReachingDefinitionsWithStats {
reaching: ReachingDefinitions {
reaching_in: HashMap::new(),
reaching_out: HashMap::new(),
},
iterations: 0,
};
}
let mut predecessors: HashMap<usize, Vec<usize>> = HashMap::new();
for block in &cfg.blocks {
predecessors.insert(block.id, Vec::new());
}
for edge in &cfg.edges {
predecessors.entry(edge.to).or_default().push(edge.from);
}
let defs: Vec<(DefId, &VarRef)> = refs
.iter()
.enumerate()
.filter(|(_, r)| matches!(r.ref_type, RefType::Definition | RefType::Update))
.map(|(i, r)| {
(
DefId {
ref_index: i,
line: r.line,
},
r,
)
})
.collect();
let mut gen: HashMap<usize, HashSet<DefId>> = HashMap::new();
let mut kill: HashMap<usize, HashSet<DefId>> = HashMap::new();
for block in &cfg.blocks {
let (start_line, end_line) = block.lines;
let mut block_gen = HashSet::new();
let mut block_kill = HashSet::new();
let mut last_def_for_var: HashMap<&str, DefId> = HashMap::new();
for (def_id, var_ref) in &defs {
if var_ref.line >= start_line && var_ref.line <= end_line {
if let Some(&prev) = last_def_for_var.get(var_ref.name.as_str()) {
block_kill.insert(prev);
}
last_def_for_var.insert(&var_ref.name, *def_id);
}
}
block_gen.extend(last_def_for_var.values().copied());
let vars_defined: HashSet<&str> = last_def_for_var.keys().copied().collect();
for (def_id, var_ref) in &defs {
if vars_defined.contains(var_ref.name.as_str()) {
let (start, end) = block.lines;
if var_ref.line < start || var_ref.line > end {
block_kill.insert(*def_id);
}
}
}
gen.insert(block.id, block_gen);
kill.insert(block.id, block_kill);
}
let mut reaching_in: HashMap<usize, HashSet<DefId>> = HashMap::new();
let mut reaching_out: HashMap<usize, HashSet<DefId>> = HashMap::new();
for block in &cfg.blocks {
reaching_in.insert(block.id, HashSet::new());
reaching_out.insert(block.id, gen.get(&block.id).cloned().unwrap_or_default());
}
let rpo = compute_rpo(cfg);
let max_iterations = 100;
let mut iterations = 0;
for iteration in 0..max_iterations {
let mut changed = false;
iterations = iteration + 1;
for &block_id in &rpo {
let mut new_in = HashSet::new();
if let Some(preds) = predecessors.get(&block_id) {
for &pred in preds {
if let Some(pred_out) = reaching_out.get(&pred) {
new_in.extend(pred_out.iter().copied());
}
}
}
let block_gen = gen.get(&block_id).cloned().unwrap_or_default();
let block_kill = kill.get(&block_id).cloned().unwrap_or_default();
let in_minus_kill: HashSet<DefId> = new_in
.iter()
.filter(|d| !block_kill.contains(d))
.copied()
.collect();
let new_out: HashSet<DefId> = block_gen.union(&in_minus_kill).copied().collect();
if new_in != *reaching_in.get(&block_id).unwrap_or(&HashSet::new()) {
changed = true;
reaching_in.insert(block_id, new_in);
}
if new_out != *reaching_out.get(&block_id).unwrap_or(&HashSet::new()) {
changed = true;
reaching_out.insert(block_id, new_out);
}
}
if !changed {
break;
}
}
ReachingDefinitionsWithStats {
reaching: ReachingDefinitions {
reaching_in,
reaching_out,
},
iterations,
}
}
use bitvec::prelude::*;
#[derive(Debug, Clone)]
pub struct DenseDefMapping {
pub def_to_bit: HashMap<DefId, usize>,
pub bit_to_def: Vec<DefId>,
pub num_defs: usize,
}
pub fn create_dense_def_mapping(refs: &[VarRef]) -> DenseDefMapping {
let mut def_to_bit = HashMap::new();
let mut bit_to_def = Vec::new();
for (index, var_ref) in refs.iter().enumerate() {
if matches!(var_ref.ref_type, RefType::Definition | RefType::Update) {
let def_id = DefId {
ref_index: index,
line: var_ref.line,
};
def_to_bit.entry(def_id).or_insert_with(|| {
let idx = bit_to_def.len();
bit_to_def.push(def_id);
idx
});
}
}
DenseDefMapping {
num_defs: bit_to_def.len(),
def_to_bit,
bit_to_def,
}
}
#[derive(Debug, Clone)]
pub struct BitVectorReachingDefs {
pub mapping: DenseDefMapping,
pub in_sets: HashMap<usize, BitVec>,
pub out_sets: HashMap<usize, BitVec>,
pub iterations: usize,
}
impl BitVectorReachingDefs {
pub fn to_standard(&self) -> ReachingDefinitions {
let mut reaching_in = HashMap::new();
let mut reaching_out = HashMap::new();
for (&block_id, in_bv) in &self.in_sets {
let mut in_set = HashSet::new();
for (bit_idx, bit) in in_bv.iter().enumerate() {
if *bit {
if let Some(&def_id) = self.mapping.bit_to_def.get(bit_idx) {
in_set.insert(def_id);
}
}
}
reaching_in.insert(block_id, in_set);
}
for (&block_id, out_bv) in &self.out_sets {
let mut out_set = HashSet::new();
for (bit_idx, bit) in out_bv.iter().enumerate() {
if *bit {
if let Some(&def_id) = self.mapping.bit_to_def.get(bit_idx) {
out_set.insert(def_id);
}
}
}
reaching_out.insert(block_id, out_set);
}
ReachingDefinitions {
reaching_in,
reaching_out,
}
}
}
pub fn compute_reaching_definitions_bitvec(
cfg: &CfgInfo,
refs: &[VarRef],
) -> BitVectorReachingDefs {
let mapping = create_dense_def_mapping(refs);
let num_defs = mapping.num_defs;
if cfg.blocks.is_empty() || num_defs == 0 {
return BitVectorReachingDefs {
mapping,
in_sets: HashMap::new(),
out_sets: HashMap::new(),
iterations: 0,
};
}
let mut predecessors: HashMap<usize, Vec<usize>> = HashMap::new();
for block in &cfg.blocks {
predecessors.insert(block.id, Vec::new());
}
for edge in &cfg.edges {
predecessors.entry(edge.to).or_default().push(edge.from);
}
let defs: Vec<(DefId, &VarRef, usize)> = refs
.iter()
.enumerate()
.filter(|(_, r)| matches!(r.ref_type, RefType::Definition | RefType::Update))
.map(|(i, r)| {
let def_id = DefId {
ref_index: i,
line: r.line,
};
let bit_pos = *mapping.def_to_bit.get(&def_id).unwrap();
(def_id, r, bit_pos)
})
.collect();
let mut gen_bits: HashMap<usize, BitVec> = HashMap::new();
let mut kill_bits: HashMap<usize, BitVec> = HashMap::new();
for block in &cfg.blocks {
let (start_line, end_line) = block.lines;
let mut block_gen = bitvec![0; num_defs];
let mut block_kill = bitvec![0; num_defs];
let mut last_def_for_var: HashMap<&str, usize> = HashMap::new();
for (_def_id, var_ref, bit_pos) in &defs {
if var_ref.line >= start_line && var_ref.line <= end_line {
if let Some(&prev_bit) = last_def_for_var.get(var_ref.name.as_str()) {
block_kill.set(prev_bit, true);
}
last_def_for_var.insert(&var_ref.name, *bit_pos);
}
}
for &bit_pos in last_def_for_var.values() {
block_gen.set(bit_pos, true);
}
let vars_defined: HashSet<&str> = last_def_for_var.keys().copied().collect();
for (_def_id, var_ref, bit_pos) in &defs {
if vars_defined.contains(var_ref.name.as_str()) {
let (start, end) = block.lines;
if var_ref.line < start || var_ref.line > end {
block_kill.set(*bit_pos, true);
}
}
}
gen_bits.insert(block.id, block_gen);
kill_bits.insert(block.id, block_kill);
}
let mut in_sets: HashMap<usize, BitVec> = HashMap::new();
let mut out_sets: HashMap<usize, BitVec> = HashMap::new();
for block in &cfg.blocks {
in_sets.insert(block.id, bitvec![0; num_defs]);
out_sets.insert(
block.id,
gen_bits
.get(&block.id)
.cloned()
.unwrap_or_else(|| bitvec![0; num_defs]),
);
}
let rpo = compute_rpo(cfg);
let max_iterations = 100;
let mut iterations = 0;
for iteration in 0..max_iterations {
let mut changed = false;
iterations = iteration + 1;
for &block_id in &rpo {
let mut new_in = bitvec![0; num_defs];
if let Some(preds) = predecessors.get(&block_id) {
for &pred in preds {
if let Some(pred_out) = out_sets.get(&pred) {
new_in |= pred_out.clone();
}
}
}
let gen = gen_bits
.get(&block_id)
.cloned()
.unwrap_or_else(|| bitvec![0; num_defs]);
let kill = kill_bits
.get(&block_id)
.cloned()
.unwrap_or_else(|| bitvec![0; num_defs]);
let mut in_minus_kill = new_in.clone();
in_minus_kill &= !kill;
let mut new_out = gen;
new_out |= in_minus_kill;
if new_in != *in_sets.get(&block_id).unwrap() {
changed = true;
in_sets.insert(block_id, new_in);
}
if new_out != *out_sets.get(&block_id).unwrap() {
changed = true;
out_sets.insert(block_id, new_out);
}
}
if !changed {
break;
}
}
BitVectorReachingDefs {
mapping,
in_sets,
out_sets,
iterations,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::types::{BlockType, CfgBlock, CfgEdge, EdgeType};
fn make_block(id: usize, start: u32, end: u32) -> CfgBlock {
CfgBlock {
id,
block_type: BlockType::Body,
lines: (start, end),
calls: Vec::new(),
}
}
fn make_def(name: &str, line: u32) -> VarRef {
VarRef {
name: name.to_string(),
ref_type: RefType::Definition,
line,
column: 0,
context: None,
group_id: None,
}
}
fn make_use(name: &str, line: u32) -> VarRef {
VarRef {
name: name.to_string(),
ref_type: RefType::Use,
line,
column: 0,
context: None,
group_id: None,
}
}
#[test]
fn test_simple_reaching() {
let cfg = CfgInfo {
function: "test".to_string(),
blocks: vec![make_block(0, 1, 1), make_block(1, 2, 2)],
edges: vec![CfgEdge {
from: 0,
to: 1,
edge_type: EdgeType::Unconditional,
condition: None,
}],
entry_block: 0,
exit_blocks: vec![1],
cyclomatic_complexity: 1,
nested_functions: HashMap::new(),
};
let refs = vec![make_def("x", 1), make_use("x", 2)];
let reaching = compute_reaching_definitions(&cfg, &refs);
let block1_in = reaching.reaching_in.get(&1).unwrap();
assert!(
block1_in.iter().any(|d| d.ref_index == 0),
"x's def should reach block 1"
);
}
#[test]
fn test_kill_set() {
let cfg = CfgInfo {
function: "test".to_string(),
blocks: vec![
make_block(0, 1, 1),
make_block(1, 2, 2),
make_block(2, 3, 3),
],
edges: vec![
CfgEdge {
from: 0,
to: 1,
edge_type: EdgeType::Unconditional,
condition: None,
},
CfgEdge {
from: 1,
to: 2,
edge_type: EdgeType::Unconditional,
condition: None,
},
],
entry_block: 0,
exit_blocks: vec![2],
cyclomatic_complexity: 1,
nested_functions: HashMap::new(),
};
let refs = vec![
make_def("x", 1), make_def("x", 2), make_use("x", 3),
];
let reaching = compute_reaching_definitions(&cfg, &refs);
let block2_in = reaching.reaching_in.get(&2).unwrap();
assert!(
!block2_in.iter().any(|d| d.ref_index == 0),
"x=1 should be killed"
);
assert!(
block2_in.iter().any(|d| d.ref_index == 1),
"x=2 should reach block 2"
);
}
#[test]
fn test_multiple_paths() {
let cfg = CfgInfo {
function: "test".to_string(),
blocks: vec![
CfgBlock {
id: 0,
block_type: BlockType::Entry,
lines: (0, 0),
calls: Vec::new(),
},
make_block(1, 1, 1),
make_block(2, 2, 2),
make_block(3, 3, 3),
],
edges: vec![
CfgEdge {
from: 0,
to: 1,
edge_type: EdgeType::True,
condition: None,
},
CfgEdge {
from: 0,
to: 2,
edge_type: EdgeType::False,
condition: None,
},
CfgEdge {
from: 1,
to: 3,
edge_type: EdgeType::Unconditional,
condition: None,
},
CfgEdge {
from: 2,
to: 3,
edge_type: EdgeType::Unconditional,
condition: None,
},
],
entry_block: 0,
exit_blocks: vec![3],
cyclomatic_complexity: 2,
nested_functions: HashMap::new(),
};
let refs = vec![
make_def("x", 1), make_def("x", 2), make_use("x", 3),
];
let reaching = compute_reaching_definitions(&cfg, &refs);
let block3_in = reaching.reaching_in.get(&3).unwrap();
assert!(
block3_in.iter().any(|d| d.ref_index == 0),
"x=1 should reach block 3"
);
assert!(
block3_in.iter().any(|d| d.ref_index == 1),
"x=2 should reach block 3"
);
}
#[test]
fn test_empty_cfg() {
let cfg = CfgInfo {
function: "test".to_string(),
blocks: Vec::new(),
edges: Vec::new(),
entry_block: 0,
exit_blocks: Vec::new(),
cyclomatic_complexity: 0,
nested_functions: HashMap::new(),
};
let refs: Vec<VarRef> = Vec::new();
let reaching = compute_reaching_definitions(&cfg, &refs);
assert!(reaching.reaching_in.is_empty());
assert!(reaching.reaching_out.is_empty());
}
}