use std::collections::HashMap;
use std::path::{Path, PathBuf};
use anyhow::Result;
use clap::Args;
use tldr_core::cfg::get_cfg_context;
use tldr_core::dfg::get_dfg_context;
use tldr_core::ssa::{SsaFunction, SsaNameId};
use tldr_core::types::RefType;
use tldr_core::Language;
use crate::output::{OutputFormat, OutputWriter};
use super::error::{ContractsError, ContractsResult};
use super::types::{DeadStore, DeadStoresReport, OutputFormat as ContractsOutputFormat};
use super::validation::{
check_ssa_node_limit, read_file_safe, validate_file_path, validate_function_name,
};
#[derive(Debug, Args)]
pub struct DeadStoresArgs {
#[arg(value_name = "file")]
pub file: PathBuf,
#[arg(value_name = "function")]
pub function: String,
#[arg(
long = "output-format",
short = 'o',
hide = true,
default_value = "json"
)]
pub output_format: ContractsOutputFormat,
#[arg(long, short = 'l')]
pub lang: Option<Language>,
#[arg(long)]
pub compare: bool,
}
impl DeadStoresArgs {
pub fn run(&self, format: OutputFormat, quiet: bool) -> Result<()> {
let writer = OutputWriter::new(format, quiet);
let canonical_path = validate_file_path(&self.file)?;
validate_function_name(&self.function)?;
writer.progress(&format!(
"Analyzing dead stores for {}::{}...",
self.file.display(),
self.function
));
let language = self
.lang
.unwrap_or_else(|| Language::from_path(&self.file).unwrap_or(Language::Python));
let report = run_dead_stores(&canonical_path, &self.function, language, self.compare)?;
let use_text = matches!(self.output_format, ContractsOutputFormat::Text)
|| matches!(format, OutputFormat::Text);
if use_text {
let text = format_dead_stores_text(&report);
writer.write_text(&text)?;
} else {
writer.write(&report)?;
}
Ok(())
}
}
pub fn run_dead_stores(
file: &Path,
function: &str,
language: Language,
compare: bool,
) -> ContractsResult<DeadStoresReport> {
let source = read_file_safe(file)?;
let cfg = get_cfg_context(&source, function, language).map_err(|e| {
let msg = e.to_string();
if msg.contains("not found") || msg.contains("Not found") {
ContractsError::FunctionNotFound {
function: function.to_string(),
file: file.to_path_buf(),
}
} else {
ContractsError::SsaError(format!("CFG extraction failed: {}", e))
}
})?;
let dfg = get_dfg_context(&source, function, language).map_err(|e| {
let msg = e.to_string();
if msg.contains("not found") || msg.contains("Not found") {
ContractsError::FunctionNotFound {
function: function.to_string(),
file: file.to_path_buf(),
}
} else {
ContractsError::SsaError(format!("DFG extraction failed: {}", e))
}
})?;
let def_count = dfg
.refs
.iter()
.filter(|r| matches!(r.ref_type, RefType::Definition | RefType::Update))
.count();
check_ssa_node_limit(def_count)?;
let dead_stores_ssa = find_dead_stores_dfg(&dfg.refs, &cfg)?;
let (dead_stores_live_vars, live_vars_count) = if compare {
let live_vars_dead = find_dead_stores_live_vars(&source, function, language)?;
let count = live_vars_dead.len() as u32;
(Some(live_vars_dead), Some(count))
} else {
(None, None)
};
Ok(DeadStoresReport {
function: function.to_string(),
file: file.to_path_buf(),
dead_stores_ssa: dead_stores_ssa.clone(),
count: dead_stores_ssa.len() as u32,
dead_stores_live_vars,
live_vars_count,
})
}
pub fn find_dead_stores_dfg(
refs: &[tldr_core::types::VarRef],
cfg: &tldr_core::types::CfgInfo,
) -> ContractsResult<Vec<DeadStore>> {
let mut dead_stores = Vec::new();
let mut line_to_block: HashMap<u32, usize> = HashMap::new();
for block in &cfg.blocks {
for line in block.lines.0..=block.lines.1 {
line_to_block.insert(line, block.id);
}
}
let first_def_line = refs
.iter()
.filter(|r| matches!(r.ref_type, RefType::Definition))
.map(|r| r.line)
.min();
let mut refs_by_var: HashMap<String, Vec<&tldr_core::types::VarRef>> = HashMap::new();
for var_ref in refs {
refs_by_var
.entry(var_ref.name.clone())
.or_default()
.push(var_ref);
}
let mut version_counters: HashMap<String, u32> = HashMap::new();
for (var_name, mut var_refs) in refs_by_var {
var_refs.sort_by_key(|r| r.line);
let is_parameter = first_def_line
.map(|line| {
var_refs
.first()
.map(|r| r.line == line && matches!(r.ref_type, RefType::Definition))
.unwrap_or(false)
})
.unwrap_or(false);
let mut definitions: Vec<(u32, u32, u32)> = Vec::new(); let uses: Vec<u32> = var_refs
.iter()
.filter(|r| matches!(r.ref_type, RefType::Use))
.map(|r| r.line)
.collect();
for var_ref in &var_refs {
match var_ref.ref_type {
RefType::Definition | RefType::Update => {
let version = version_counters.entry(var_name.clone()).or_insert(0);
*version += 1;
let block_id = line_to_block.get(&var_ref.line).copied().unwrap_or(0);
definitions.push((var_ref.line, block_id as u32, *version));
}
RefType::Use => {}
}
}
if uses.is_empty() && !definitions.is_empty() && !is_parameter {
for (def_line, block_id, version) in &definitions {
dead_stores.push(DeadStore {
variable: var_name.clone(),
ssa_name: format!("{}_{}", var_name, version),
line: *def_line,
block_id: *block_id,
is_phi: false,
});
}
continue;
}
for (i, &(def_line, def_block_id, version)) in definitions.iter().enumerate() {
if is_parameter && i == 0 {
continue;
}
let next_def_in_block = definitions
.iter()
.skip(i + 1)
.find(|(_, block_id, _)| *block_id == def_block_id);
if let Some(&(next_def_line, _, _)) = next_def_in_block {
let has_use_between = uses
.iter()
.any(|&use_line| use_line > def_line && use_line < next_def_line);
if !has_use_between {
dead_stores.push(DeadStore {
variable: var_name.clone(),
ssa_name: format!("{}_{}", var_name, version),
line: def_line,
block_id: def_block_id,
is_phi: false,
});
}
}
}
}
dead_stores.sort_by_key(|d| (d.line, d.variable.clone()));
Ok(dead_stores)
}
pub fn find_dead_stores_ssa(ssa: &SsaFunction) -> Vec<DeadStore> {
use std::collections::HashSet;
let mut dead_stores = Vec::new();
let mut used_names: HashSet<SsaNameId> = HashSet::new();
for uses in ssa.def_use.values() {
for &use_id in uses {
used_names.insert(use_id);
}
}
for block in &ssa.blocks {
for inst in &block.instructions {
for &use_id in &inst.uses {
used_names.insert(use_id);
}
}
for phi in &block.phi_functions {
for source in &phi.sources {
used_names.insert(source.name);
}
}
}
for ssa_name in &ssa.ssa_names {
let is_used_in_def_use = ssa
.def_use
.get(&ssa_name.id)
.is_some_and(|uses| !uses.is_empty());
let is_used_anywhere = used_names.contains(&ssa_name.id);
if !is_used_in_def_use && !is_used_anywhere {
let is_phi = is_phi_definition(ssa, ssa_name.id);
let (line, block_id) = get_def_location(ssa, ssa_name.id);
dead_stores.push(DeadStore {
variable: ssa_name.variable.clone(),
ssa_name: format!("{}_{}", ssa_name.variable, ssa_name.version),
line,
block_id,
is_phi,
});
}
}
dead_stores
}
fn is_phi_definition(ssa: &SsaFunction, name_id: SsaNameId) -> bool {
for block in &ssa.blocks {
for phi in &block.phi_functions {
if phi.target == name_id {
return true;
}
}
}
false
}
fn get_def_location(ssa: &SsaFunction, name_id: SsaNameId) -> (u32, u32) {
for block in &ssa.blocks {
for phi in &block.phi_functions {
if phi.target == name_id {
return (phi.line, block.id as u32);
}
}
}
for block in &ssa.blocks {
for inst in &block.instructions {
if inst.target == Some(name_id) {
return (inst.line, block.id as u32);
}
}
}
if let Some(ssa_name) = ssa.ssa_names.iter().find(|n| n.id == name_id) {
return (ssa_name.def_line, ssa_name.def_block.unwrap_or(0) as u32);
}
(0, 0)
}
fn find_dead_stores_live_vars(
source: &str,
function: &str,
language: Language,
) -> ContractsResult<Vec<DeadStore>> {
use tldr_core::cfg::get_cfg_context;
use tldr_core::dfg::get_dfg_context;
use tldr_core::ssa::analysis::compute_live_variables;
use tldr_core::types::RefType;
let cfg = get_cfg_context(source, function, language)
.map_err(|e| ContractsError::SsaError(format!("CFG extraction failed: {}", e)))?;
let dfg = get_dfg_context(source, function, language)
.map_err(|e| ContractsError::SsaError(format!("DFG extraction failed: {}", e)))?;
let live_vars = compute_live_variables(&cfg, &dfg.refs)
.map_err(|e| ContractsError::SsaError(format!("Live variables analysis failed: {}", e)))?;
let mut line_to_block = std::collections::HashMap::new();
for block in &cfg.blocks {
for line in block.lines.0..=block.lines.1 {
line_to_block.insert(line, block.id);
}
}
let mut dead_stores = Vec::new();
for var_ref in &dfg.refs {
if !matches!(var_ref.ref_type, RefType::Definition | RefType::Update) {
continue;
}
if let Some(&block_id) = line_to_block.get(&var_ref.line) {
let is_live_out = live_vars
.blocks
.get(&block_id)
.map(|block_info| block_info.live_out.contains(&var_ref.name))
.unwrap_or(false);
if !is_live_out {
dead_stores.push(DeadStore {
variable: var_ref.name.clone(),
ssa_name: format!("{}_lv", var_ref.name),
line: var_ref.line,
block_id: block_id as u32,
is_phi: false,
});
}
}
}
Ok(dead_stores)
}
fn format_dead_stores_text(report: &DeadStoresReport) -> String {
let mut output = String::new();
output.push_str(&format!(
"Dead Stores: {} ({})\n",
report.function,
report.file.display()
));
output.push_str(&"=".repeat(60));
output.push('\n');
if report.dead_stores_ssa.is_empty() {
output.push_str("No dead stores detected.\n");
} else {
output.push_str(&format!(
"Found {} dead store(s):\n\n",
report.dead_stores_ssa.len()
));
for store in &report.dead_stores_ssa {
let phi_marker = if store.is_phi { " [phi]" } else { "" };
output.push_str(&format!(
" Line {}: {} ({}){}'\n",
store.line, store.variable, store.ssa_name, phi_marker
));
}
}
if let Some(live_vars_dead) = &report.dead_stores_live_vars {
output.push('\n');
output.push_str("Live-Variables Comparison:\n");
output.push_str(&"-".repeat(40));
output.push('\n');
output.push_str(&format!(" SSA-based: {} dead stores\n", report.count));
output.push_str(&format!(
" Live-vars: {} dead stores\n",
report.live_vars_count.unwrap_or(0)
));
if !live_vars_dead.is_empty() {
output.push_str("\n Live-vars dead stores:\n");
for store in live_vars_dead {
output.push_str(&format!(" Line {}: {}\n", store.line, store.variable));
}
}
}
output
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_find_dead_stores_ssa_simple() {
}
#[test]
fn test_format_dead_stores_text_empty() {
let report = DeadStoresReport {
function: "test_func".to_string(),
file: PathBuf::from("test.py"),
dead_stores_ssa: vec![],
count: 0,
dead_stores_live_vars: None,
live_vars_count: None,
};
let text = format_dead_stores_text(&report);
assert!(text.contains("No dead stores detected"));
}
#[test]
fn test_format_dead_stores_text_with_stores() {
let report = DeadStoresReport {
function: "test_func".to_string(),
file: PathBuf::from("test.py"),
dead_stores_ssa: vec![
DeadStore {
variable: "x".to_string(),
ssa_name: "x_1".to_string(),
line: 5,
block_id: 1,
is_phi: false,
},
DeadStore {
variable: "y".to_string(),
ssa_name: "y_2".to_string(),
line: 10,
block_id: 2,
is_phi: true,
},
],
count: 2,
dead_stores_live_vars: None,
live_vars_count: None,
};
let text = format_dead_stores_text(&report);
assert!(text.contains("Found 2 dead store(s)"));
assert!(text.contains("Line 5: x"));
assert!(text.contains("Line 10: y"));
assert!(text.contains("[phi]"));
}
#[test]
fn test_format_dead_stores_text_with_comparison() {
let report = DeadStoresReport {
function: "test_func".to_string(),
file: PathBuf::from("test.py"),
dead_stores_ssa: vec![DeadStore {
variable: "a".to_string(),
ssa_name: "a_1".to_string(),
line: 3,
block_id: 0,
is_phi: false,
}],
count: 1,
dead_stores_live_vars: Some(vec![DeadStore {
variable: "a".to_string(),
ssa_name: "a_lv".to_string(),
line: 3,
block_id: 0,
is_phi: false,
}]),
live_vars_count: Some(1),
};
let text = format_dead_stores_text(&report);
assert!(text.contains("Live-Variables Comparison"));
assert!(text.contains("SSA-based: 1"));
assert!(text.contains("Live-vars: 1"));
}
}