pub mod builder;
pub mod types;
pub use builder::{build_pdg, build_pdg_from_graphs, build_pdg_with_language, PDGBuilder};
pub use types::{BranchType, ControlDependence, PDGInfo};
use std::collections::{HashMap, HashSet, VecDeque};
use crate::error::{Result, BrrrError};
#[derive(Debug, Clone)]
pub struct SliceCriteria {
pub line: usize,
pub variable: Option<String>,
}
impl SliceCriteria {
#[inline]
pub fn at_line(line: usize) -> Self {
Self {
line,
variable: None,
}
}
#[inline]
pub fn at_line_variable(line: usize, variable: impl Into<String>) -> Self {
Self {
line,
variable: Some(variable.into()),
}
}
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct SliceResult {
pub function_name: String,
pub target_line: usize,
pub direction: String,
pub lines: Vec<usize>,
pub variables: Vec<String>,
pub variable_filter: Option<String>,
pub metrics: SliceMetrics,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct SliceMetrics {
pub slice_size: usize,
pub data_edges_traversed: usize,
pub control_deps_traversed: usize,
pub slice_ratio: f64,
pub variable_count: usize,
}
impl SliceResult {
#[inline]
pub fn contains_line(&self, line: usize) -> bool {
self.lines.binary_search(&line).is_ok()
}
pub fn line_range(&self) -> Option<(usize, usize)> {
if self.lines.is_empty() {
None
} else {
Some((self.lines[0], *self.lines.last().unwrap()))
}
}
#[inline]
pub fn is_empty(&self) -> bool {
self.lines.is_empty()
}
}
pub fn backward_slice(pdg: &PDGInfo, criteria: &SliceCriteria) -> SliceResult {
let (lines, data_edges, control_deps) = if let Some(ref var) = criteria.variable {
backward_slice_variable_impl(pdg, criteria.line, var)
} else {
backward_slice_all_impl(pdg, criteria.line)
};
let variables = collect_slice_variables(pdg, &lines);
let total_lines = compute_function_line_range(pdg).map(|(min, max)| max.saturating_sub(min).saturating_add(1)).unwrap_or(1);
let metrics = SliceMetrics {
slice_size: lines.len(),
data_edges_traversed: data_edges,
control_deps_traversed: control_deps,
slice_ratio: lines.len() as f64 / total_lines as f64,
variable_count: variables.len(),
};
SliceResult {
function_name: pdg.function_name.clone(),
target_line: criteria.line,
direction: "backward".to_string(),
lines,
variables,
variable_filter: criteria.variable.clone(),
metrics,
}
}
pub fn forward_slice(pdg: &PDGInfo, criteria: &SliceCriteria) -> SliceResult {
let (lines, data_edges, control_deps) = if let Some(ref var) = criteria.variable {
forward_slice_variable_impl(pdg, criteria.line, var)
} else {
forward_slice_all_impl(pdg, criteria.line)
};
let variables = collect_slice_variables(pdg, &lines);
let total_lines = compute_function_line_range(pdg).map(|(min, max)| max.saturating_sub(min).saturating_add(1)).unwrap_or(1);
let metrics = SliceMetrics {
slice_size: lines.len(),
data_edges_traversed: data_edges,
control_deps_traversed: control_deps,
slice_ratio: lines.len() as f64 / total_lines as f64,
variable_count: variables.len(),
};
SliceResult {
function_name: pdg.function_name.clone(),
target_line: criteria.line,
direction: "forward".to_string(),
lines,
variables,
variable_filter: criteria.variable.clone(),
metrics,
}
}
pub fn bidirectional_slice(pdg: &PDGInfo, criteria: &SliceCriteria) -> (SliceResult, SliceResult) {
let backward = backward_slice(pdg, criteria);
let forward = forward_slice(pdg, criteria);
(backward, forward)
}
pub fn chop(pdg: &PDGInfo, source_line: usize, target_line: usize) -> Vec<usize> {
let (forward_lines, _, _) = forward_slice_all_impl(pdg, source_line);
let forward_set: HashSet<usize> = forward_lines.into_iter().collect();
let (backward_lines, _, _) = backward_slice_all_impl(pdg, target_line);
let backward_set: HashSet<usize> = backward_lines.into_iter().collect();
let mut result: Vec<usize> = forward_set.intersection(&backward_set).copied().collect();
result.sort_unstable();
result
}
fn backward_slice_all_impl(pdg: &PDGInfo, target_line: usize) -> (Vec<usize>, usize, usize) {
let mut data_incoming: HashMap<usize, Vec<usize>> = HashMap::new();
for edge in &pdg.dfg.edges {
data_incoming
.entry(edge.to_line)
.or_default()
.push(edge.from_line);
}
let mut control_incoming: HashMap<usize, Vec<usize>> = HashMap::new();
for dep in &pdg.control_deps {
control_incoming
.entry(dep.dependent_line)
.or_default()
.push(dep.condition_line);
}
let mut result = HashSet::new();
let mut frontier = VecDeque::new();
frontier.push_back(target_line);
let mut data_edges_traversed = 0;
let mut control_deps_traversed = 0;
while let Some(line) = frontier.pop_front() {
if result.insert(line) {
if let Some(sources) = data_incoming.get(&line) {
for &source in sources {
if !result.contains(&source) {
frontier.push_back(source);
data_edges_traversed += 1;
}
}
}
if let Some(conditions) = control_incoming.get(&line) {
for &condition in conditions {
if !result.contains(&condition) {
frontier.push_back(condition);
control_deps_traversed += 1;
}
}
}
}
}
let mut lines: Vec<_> = result.into_iter().collect();
lines.sort_unstable();
(lines, data_edges_traversed, control_deps_traversed)
}
fn backward_slice_variable_impl(
pdg: &PDGInfo,
target_line: usize,
variable: &str,
) -> (Vec<usize>, usize, usize) {
let mut data_incoming: HashMap<usize, Vec<usize>> = HashMap::new();
for edge in &pdg.dfg.edges {
if edge.variable == variable {
data_incoming
.entry(edge.to_line)
.or_default()
.push(edge.from_line);
}
}
let mut control_incoming: HashMap<usize, Vec<usize>> = HashMap::new();
for dep in &pdg.control_deps {
control_incoming
.entry(dep.dependent_line)
.or_default()
.push(dep.condition_line);
}
let mut result = HashSet::new();
let mut frontier = VecDeque::new();
frontier.push_back(target_line);
let mut data_edges_traversed = 0;
let mut control_deps_traversed = 0;
while let Some(line) = frontier.pop_front() {
if result.insert(line) {
if let Some(sources) = data_incoming.get(&line) {
for &source in sources {
if !result.contains(&source) {
frontier.push_back(source);
data_edges_traversed += 1;
}
}
}
if let Some(conditions) = control_incoming.get(&line) {
for &condition in conditions {
if !result.contains(&condition) {
frontier.push_back(condition);
control_deps_traversed += 1;
}
}
}
}
}
let mut lines: Vec<_> = result.into_iter().collect();
lines.sort_unstable();
(lines, data_edges_traversed, control_deps_traversed)
}
fn forward_slice_all_impl(pdg: &PDGInfo, source_line: usize) -> (Vec<usize>, usize, usize) {
let mut data_outgoing: HashMap<usize, Vec<usize>> = HashMap::new();
for edge in &pdg.dfg.edges {
data_outgoing
.entry(edge.from_line)
.or_default()
.push(edge.to_line);
}
let mut control_outgoing: HashMap<usize, Vec<usize>> = HashMap::new();
for dep in &pdg.control_deps {
control_outgoing
.entry(dep.condition_line)
.or_default()
.push(dep.dependent_line);
}
let mut result = HashSet::new();
let mut frontier = VecDeque::new();
frontier.push_back(source_line);
let mut data_edges_traversed = 0;
let mut control_deps_traversed = 0;
while let Some(line) = frontier.pop_front() {
if result.insert(line) {
if let Some(targets) = data_outgoing.get(&line) {
for &target in targets {
if !result.contains(&target) {
frontier.push_back(target);
data_edges_traversed += 1;
}
}
}
if let Some(dependents) = control_outgoing.get(&line) {
for &dependent in dependents {
if !result.contains(&dependent) {
frontier.push_back(dependent);
control_deps_traversed += 1;
}
}
}
}
}
let mut lines: Vec<_> = result.into_iter().collect();
lines.sort_unstable();
(lines, data_edges_traversed, control_deps_traversed)
}
fn forward_slice_variable_impl(
pdg: &PDGInfo,
source_line: usize,
variable: &str,
) -> (Vec<usize>, usize, usize) {
let mut data_outgoing: HashMap<usize, Vec<usize>> = HashMap::new();
for edge in &pdg.dfg.edges {
if edge.variable == variable {
data_outgoing
.entry(edge.from_line)
.or_default()
.push(edge.to_line);
}
}
let mut control_outgoing: HashMap<usize, Vec<usize>> = HashMap::new();
for dep in &pdg.control_deps {
control_outgoing
.entry(dep.condition_line)
.or_default()
.push(dep.dependent_line);
}
let mut result = HashSet::new();
let mut frontier = VecDeque::new();
frontier.push_back(source_line);
let mut data_edges_traversed = 0;
let mut control_deps_traversed = 0;
while let Some(line) = frontier.pop_front() {
if result.insert(line) {
if let Some(targets) = data_outgoing.get(&line) {
for &target in targets {
if !result.contains(&target) {
frontier.push_back(target);
data_edges_traversed += 1;
}
}
}
if let Some(dependents) = control_outgoing.get(&line) {
for &dependent in dependents {
if !result.contains(&dependent) {
frontier.push_back(dependent);
control_deps_traversed += 1;
}
}
}
}
}
let mut lines: Vec<_> = result.into_iter().collect();
lines.sort_unstable();
(lines, data_edges_traversed, control_deps_traversed)
}
fn collect_slice_variables(pdg: &PDGInfo, lines: &[usize]) -> Vec<String> {
let line_set: HashSet<usize> = lines.iter().copied().collect();
let mut variables: HashSet<&str> = HashSet::new();
for edge in &pdg.dfg.edges {
if line_set.contains(&edge.from_line) || line_set.contains(&edge.to_line) {
variables.insert(&edge.variable);
}
}
let mut result: Vec<String> = variables.into_iter().map(String::from).collect();
result.sort_unstable();
result
}
fn compute_function_line_range(pdg: &PDGInfo) -> Option<(usize, usize)> {
let mut min_line = usize::MAX;
let mut max_line = 0;
for block in pdg.cfg.blocks.values() {
min_line = min_line.min(block.start_line);
max_line = max_line.max(block.end_line);
}
for edge in &pdg.dfg.edges {
min_line = min_line.min(edge.from_line).min(edge.to_line);
max_line = max_line.max(edge.from_line).max(edge.to_line);
}
if min_line <= max_line {
Some((min_line, max_line))
} else {
None
}
}
pub fn get_slice(file: &str, function: &str, line: usize) -> Result<Vec<usize>> {
if line == 0 {
return Err(BrrrError::InvalidArgument(
"Line numbers are 1-indexed, got 0".to_string(),
));
}
get_slice_with_language(file, function, line, None)
}
pub fn get_slice_with_language(
file: &str,
function: &str,
line: usize,
language: Option<&str>,
) -> Result<Vec<usize>> {
if line == 0 {
return Err(BrrrError::InvalidArgument(
"Line numbers are 1-indexed, got 0".to_string(),
));
}
let pdg = build_pdg_with_language(file, function, language)?;
let (lines, _, _) = backward_slice_all_impl(&pdg, line);
Ok(lines)
}
pub fn get_slice_result(file: &str, function: &str, line: usize) -> Result<SliceResult> {
if line == 0 {
return Err(BrrrError::InvalidArgument(
"Line numbers are 1-indexed, got 0".to_string(),
));
}
let pdg = build_pdg(file, function)?;
let criteria = SliceCriteria::at_line(line);
Ok(backward_slice(&pdg, &criteria))
}
pub fn get_forward_slice(file: &str, function: &str, line: usize) -> Result<Vec<usize>> {
if line == 0 {
return Err(BrrrError::InvalidArgument(
"Line numbers are 1-indexed, got 0".to_string(),
));
}
get_forward_slice_with_language(file, function, line, None)
}
pub fn get_forward_slice_with_language(
file: &str,
function: &str,
line: usize,
language: Option<&str>,
) -> Result<Vec<usize>> {
if line == 0 {
return Err(BrrrError::InvalidArgument(
"Line numbers are 1-indexed, got 0".to_string(),
));
}
let pdg = build_pdg_with_language(file, function, language)?;
let (lines, _, _) = forward_slice_all_impl(&pdg, line);
Ok(lines)
}
pub fn get_forward_slice_result(file: &str, function: &str, line: usize) -> Result<SliceResult> {
if line == 0 {
return Err(BrrrError::InvalidArgument(
"Line numbers are 1-indexed, got 0".to_string(),
));
}
let pdg = build_pdg(file, function)?;
let criteria = SliceCriteria::at_line(line);
Ok(forward_slice(&pdg, &criteria))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::cfg::types::{BlockId, BlockType, CFGBlock, CFGEdge, CFGInfo, EdgeType};
use crate::dfg::types::{DataflowEdge, DataflowKind, DFGInfo};
use std::collections::HashMap;
fn create_test_pdg() -> PDGInfo {
let mut blocks = HashMap::new();
blocks.insert(BlockId(0), CFGBlock {
id: BlockId(0),
label: "entry".to_string(),
block_type: BlockType::Entry,
statements: vec!["def example(x):".to_string()],
func_calls: vec![],
start_line: 1,
end_line: 1,
});
blocks.insert(BlockId(1), CFGBlock {
id: BlockId(1),
label: "if x > 0".to_string(),
block_type: BlockType::Branch,
statements: vec!["if x > 0:".to_string()],
func_calls: vec![],
start_line: 2,
end_line: 2,
});
blocks.insert(BlockId(2), CFGBlock {
id: BlockId(2),
label: "true branch".to_string(),
block_type: BlockType::Body,
statements: vec!["result = x * 2".to_string()],
func_calls: vec![],
start_line: 3,
end_line: 3,
});
blocks.insert(BlockId(3), CFGBlock {
id: BlockId(3),
label: "false branch".to_string(),
block_type: BlockType::Body,
statements: vec!["result = 0".to_string()],
func_calls: vec![],
start_line: 5,
end_line: 5,
});
blocks.insert(BlockId(4), CFGBlock {
id: BlockId(4),
label: "return".to_string(),
block_type: BlockType::Return,
statements: vec!["return result".to_string()],
func_calls: vec![],
start_line: 6,
end_line: 6,
});
let cfg = CFGInfo {
function_name: "example".to_string(),
blocks,
edges: vec![
CFGEdge::unconditional(BlockId(0), BlockId(1)),
CFGEdge::new(BlockId(1), BlockId(2), EdgeType::True),
CFGEdge::new(BlockId(1), BlockId(3), EdgeType::False),
CFGEdge::unconditional(BlockId(2), BlockId(4)),
CFGEdge::unconditional(BlockId(3), BlockId(4)),
],
entry: BlockId(0),
exits: vec![BlockId(4)],
decision_points: 1,
comprehension_decision_points: 0,
nested_cfgs: HashMap::new(),
is_async: false,
await_points: 0,
blocking_calls: Vec::new(),
..Default::default()
};
let dfg = DFGInfo::new(
"example".to_string(),
vec![
DataflowEdge {
variable: "x".to_string(),
from_line: 1,
to_line: 2,
kind: DataflowKind::Use,
},
DataflowEdge {
variable: "x".to_string(),
from_line: 1,
to_line: 3,
kind: DataflowKind::Use,
},
DataflowEdge {
variable: "result".to_string(),
from_line: 3,
to_line: 6,
kind: DataflowKind::Definition,
},
DataflowEdge {
variable: "result".to_string(),
from_line: 5,
to_line: 6,
kind: DataflowKind::Definition,
},
],
[
("x".to_string(), vec![1]),
("result".to_string(), vec![3, 5]),
]
.into_iter()
.collect(),
[
("x".to_string(), vec![2, 3]),
("result".to_string(), vec![6]),
]
.into_iter()
.collect(),
);
build_pdg_from_graphs(cfg, dfg)
}
#[test]
fn test_backward_slice_includes_condition() {
let pdg = create_test_pdg();
let criteria = SliceCriteria::at_line(6);
let result = backward_slice(&pdg, &criteria);
assert!(
result.lines.contains(&6),
"Slice should contain target line 6"
);
assert!(
result.lines.contains(&3),
"Slice should contain data dep line 3"
);
assert!(
result.lines.contains(&5),
"Slice should contain data dep line 5"
);
assert!(
result.lines.contains(&2),
"BUG FIX: Slice MUST contain condition line 2 (control dependency)!"
);
assert!(
result.lines.contains(&1),
"Slice should contain param line 1"
);
}
#[test]
fn test_backward_slice_from_true_branch() {
let pdg = create_test_pdg();
let criteria = SliceCriteria::at_line(3);
let result = backward_slice(&pdg, &criteria);
assert!(result.lines.contains(&3));
assert!(
result.lines.contains(&2),
"Slice from true branch should include condition line"
);
assert!(result.lines.contains(&1));
}
#[test]
fn test_forward_slice_from_condition() {
let pdg = create_test_pdg();
let criteria = SliceCriteria::at_line(2);
let result = forward_slice(&pdg, &criteria);
assert!(result.lines.contains(&2));
assert!(
result.lines.contains(&3),
"Forward slice should include true branch"
);
assert!(
result.lines.contains(&5),
"Forward slice should include false branch"
);
}
#[test]
fn test_control_deps_computed() {
let pdg = create_test_pdg();
assert!(
pdg.is_control_dependent(3, 2),
"Line 3 should be control-dependent on line 2"
);
assert!(
pdg.is_control_dependent(5, 2),
"Line 5 should be control-dependent on line 2"
);
}
#[test]
fn test_slice_metrics_track_both_types() {
let pdg = create_test_pdg();
let criteria = SliceCriteria::at_line(6);
let result = backward_slice(&pdg, &criteria);
assert!(
result.metrics.data_edges_traversed > 0 || result.metrics.control_deps_traversed > 0,
"Slice should have traversed some edges"
);
}
#[test]
fn test_chop_includes_control_deps() {
let pdg = create_test_pdg();
let result = chop(&pdg, 1, 6);
assert!(result.contains(&2), "Chop should include condition line");
}
#[test]
fn test_dfg_only_slice_would_miss_condition() {
let pdg = create_test_pdg();
let mut data_incoming: HashMap<usize, Vec<usize>> = HashMap::new();
for edge in &pdg.dfg.edges {
data_incoming
.entry(edge.to_line)
.or_default()
.push(edge.from_line);
}
let mut dfg_only_result = HashSet::new();
let mut frontier = VecDeque::new();
frontier.push_back(6usize);
while let Some(line) = frontier.pop_front() {
if dfg_only_result.insert(line) {
if let Some(sources) = data_incoming.get(&line) {
for &source in sources {
if !dfg_only_result.contains(&source) {
frontier.push_back(source);
}
}
}
}
}
assert!(
!dfg_only_result.contains(&2),
"DFG-only slice correctly does NOT include condition (demonstrating the bug)"
);
let criteria = SliceCriteria::at_line(6);
let pdg_result = backward_slice(&pdg, &criteria);
assert!(
pdg_result.lines.contains(&2),
"PDG slice correctly DOES include condition (the fix)"
);
}
}