use std::collections::{HashMap, HashSet};
use std::path::PathBuf;
use crate::types::{
BlockType, CfgBlock, CfgEdge, CfgInfo, DfgInfo, EdgeType, RefType, VarRef,
};
use super::analysis::*;
use super::construct::*;
use super::dominators::*;
use super::format::*;
use super::memory::*;
use super::types::*;
mod fixtures {
use super::*;
pub fn linear_cfg() -> CfgInfo {
CfgInfo {
function: "linear".to_string(),
blocks: vec![
CfgBlock {
id: 0,
block_type: BlockType::Entry,
lines: (1, 2),
calls: Vec::new(),
},
CfgBlock {
id: 1,
block_type: BlockType::Body,
lines: (3, 4),
calls: Vec::new(),
},
CfgBlock {
id: 2,
block_type: BlockType::Exit,
lines: (5, 5),
calls: Vec::new(),
},
],
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(),
}
}
pub fn diamond_cfg() -> CfgInfo {
CfgInfo {
function: "diamond".to_string(),
blocks: vec![
CfgBlock {
id: 0,
block_type: BlockType::Entry,
lines: (1, 2),
calls: Vec::new(),
},
CfgBlock {
id: 1,
block_type: BlockType::Body,
lines: (3, 4),
calls: Vec::new(),
},
CfgBlock {
id: 2,
block_type: BlockType::Body,
lines: (5, 6),
calls: Vec::new(),
},
CfgBlock {
id: 3,
block_type: BlockType::Body,
lines: (7, 8),
calls: Vec::new(),
},
CfgBlock {
id: 4,
block_type: BlockType::Exit,
lines: (9, 9),
calls: Vec::new(),
},
],
edges: vec![
CfgEdge {
from: 0,
to: 1,
edge_type: EdgeType::True,
condition: Some("x > 0".to_string()),
},
CfgEdge {
from: 0,
to: 2,
edge_type: EdgeType::False,
condition: Some("x > 0".to_string()),
},
CfgEdge {
from: 1,
to: 3,
edge_type: EdgeType::Unconditional,
condition: None,
},
CfgEdge {
from: 2,
to: 3,
edge_type: EdgeType::Unconditional,
condition: None,
},
CfgEdge {
from: 3,
to: 4,
edge_type: EdgeType::Unconditional,
condition: None,
},
],
entry_block: 0,
exit_blocks: vec![4],
cyclomatic_complexity: 2,
nested_functions: HashMap::new(),
}
}
pub fn loop_cfg() -> CfgInfo {
CfgInfo {
function: "loop".to_string(),
blocks: vec![
CfgBlock {
id: 0,
block_type: BlockType::Entry,
lines: (1, 2),
calls: Vec::new(),
},
CfgBlock {
id: 1,
block_type: BlockType::LoopHeader,
lines: (3, 4),
calls: Vec::new(),
},
CfgBlock {
id: 2,
block_type: BlockType::Exit,
lines: (8, 8),
calls: Vec::new(),
},
CfgBlock {
id: 3,
block_type: BlockType::Body,
lines: (5, 7),
calls: Vec::new(),
},
],
edges: vec![
CfgEdge {
from: 0,
to: 1,
edge_type: EdgeType::Unconditional,
condition: None,
},
CfgEdge {
from: 1,
to: 2,
edge_type: EdgeType::False,
condition: Some("i < n".to_string()),
},
CfgEdge {
from: 1,
to: 3,
edge_type: EdgeType::True,
condition: Some("i < n".to_string()),
},
CfgEdge {
from: 3,
to: 1,
edge_type: EdgeType::Unconditional,
condition: None,
},
],
entry_block: 0,
exit_blocks: vec![2],
cyclomatic_complexity: 2,
nested_functions: HashMap::new(),
}
}
pub fn complex_cfg() -> CfgInfo {
CfgInfo {
function: "complex".to_string(),
blocks: vec![
CfgBlock {
id: 0,
block_type: BlockType::Entry,
lines: (1, 2),
calls: Vec::new(),
},
CfgBlock {
id: 1,
block_type: BlockType::Body,
lines: (3, 4),
calls: Vec::new(),
},
CfgBlock {
id: 2,
block_type: BlockType::Body,
lines: (5, 6),
calls: Vec::new(),
},
CfgBlock {
id: 3,
block_type: BlockType::Body,
lines: (7, 8),
calls: Vec::new(),
},
CfgBlock {
id: 4,
block_type: BlockType::Body,
lines: (9, 10),
calls: Vec::new(),
},
CfgBlock {
id: 5,
block_type: BlockType::Body,
lines: (11, 12),
calls: Vec::new(),
},
CfgBlock {
id: 6,
block_type: BlockType::Body,
lines: (13, 14),
calls: Vec::new(),
},
CfgBlock {
id: 7,
block_type: BlockType::Exit,
lines: (15, 15),
calls: Vec::new(),
},
],
edges: vec![
CfgEdge {
from: 0,
to: 1,
edge_type: EdgeType::True,
condition: Some("x > 0".to_string()),
},
CfgEdge {
from: 0,
to: 2,
edge_type: EdgeType::False,
condition: Some("x > 0".to_string()),
},
CfgEdge {
from: 1,
to: 3,
edge_type: EdgeType::True,
condition: Some("y > 0".to_string()),
},
CfgEdge {
from: 1,
to: 4,
edge_type: EdgeType::False,
condition: Some("y > 0".to_string()),
},
CfgEdge {
from: 2,
to: 6,
edge_type: EdgeType::Unconditional,
condition: None,
},
CfgEdge {
from: 3,
to: 5,
edge_type: EdgeType::Unconditional,
condition: None,
},
CfgEdge {
from: 4,
to: 5,
edge_type: EdgeType::Unconditional,
condition: None,
},
CfgEdge {
from: 5,
to: 6,
edge_type: EdgeType::Unconditional,
condition: None,
},
CfgEdge {
from: 6,
to: 7,
edge_type: EdgeType::Unconditional,
condition: None,
},
],
entry_block: 0,
exit_blocks: vec![7],
cyclomatic_complexity: 3,
nested_functions: HashMap::new(),
}
}
pub fn diamond_dfg() -> DfgInfo {
DfgInfo {
function: "diamond".to_string(),
refs: vec![
VarRef {
name: "x".to_string(),
ref_type: RefType::Definition,
line: 1,
column: 0,
context: None,
group_id: None,
},
VarRef {
name: "x".to_string(),
ref_type: RefType::Use,
line: 2,
column: 4,
context: None,
group_id: None,
},
VarRef {
name: "y".to_string(),
ref_type: RefType::Definition,
line: 3,
column: 0,
context: None,
group_id: None,
},
VarRef {
name: "y".to_string(),
ref_type: RefType::Definition,
line: 5,
column: 0,
context: None,
group_id: None,
},
VarRef {
name: "y".to_string(),
ref_type: RefType::Use,
line: 9,
column: 7,
context: None,
group_id: None,
},
],
edges: Vec::new(),
variables: Vec::new(),
}
}
pub fn loop_dfg() -> DfgInfo {
DfgInfo {
function: "loop".to_string(),
refs: vec![
VarRef {
name: "total".to_string(),
ref_type: RefType::Definition,
line: 1,
column: 0,
context: None,
group_id: None,
},
VarRef {
name: "i".to_string(),
ref_type: RefType::Definition,
line: 2,
column: 0,
context: None,
group_id: None,
},
VarRef {
name: "i".to_string(),
ref_type: RefType::Use,
line: 3,
column: 6,
context: None,
group_id: None,
},
VarRef {
name: "n".to_string(),
ref_type: RefType::Use,
line: 3,
column: 10,
context: None,
group_id: None,
},
VarRef {
name: "total".to_string(),
ref_type: RefType::Definition,
line: 5,
column: 0,
context: None,
group_id: None,
},
VarRef {
name: "total".to_string(),
ref_type: RefType::Use,
line: 5,
column: 8,
context: None,
group_id: None,
},
VarRef {
name: "i".to_string(),
ref_type: RefType::Use,
line: 5,
column: 16,
context: None,
group_id: None,
},
VarRef {
name: "i".to_string(),
ref_type: RefType::Definition,
line: 6,
column: 0,
context: None,
group_id: None,
},
VarRef {
name: "i".to_string(),
ref_type: RefType::Use,
line: 6,
column: 4,
context: None,
group_id: None,
},
VarRef {
name: "total".to_string(),
ref_type: RefType::Use,
line: 8,
column: 7,
context: None,
group_id: None,
},
],
edges: Vec::new(),
variables: Vec::new(),
}
}
pub fn sample_ssa() -> SsaFunction {
SsaFunction {
function: "sample".to_string(),
file: PathBuf::from("test.py"),
ssa_type: SsaType::Minimal,
blocks: vec![
SsaBlock {
id: 0,
label: Some("entry".to_string()),
lines: (1, 2),
phi_functions: vec![],
instructions: vec![
SsaInstruction {
kind: SsaInstructionKind::Param,
target: Some(SsaNameId(1)),
uses: vec![],
line: 1,
source_text: Some("x = param".to_string()),
},
SsaInstruction {
kind: SsaInstructionKind::Assign,
target: Some(SsaNameId(2)),
uses: vec![],
line: 2,
source_text: Some("y = 0".to_string()),
},
],
successors: vec![1, 2],
predecessors: vec![],
},
SsaBlock {
id: 1,
label: None,
lines: (3, 4),
phi_functions: vec![],
instructions: vec![SsaInstruction {
kind: SsaInstructionKind::Assign,
target: Some(SsaNameId(3)),
uses: vec![SsaNameId(1)],
line: 3,
source_text: Some("y = x + 1".to_string()),
}],
successors: vec![3],
predecessors: vec![0],
},
SsaBlock {
id: 2,
label: None,
lines: (5, 6),
phi_functions: vec![],
instructions: vec![SsaInstruction {
kind: SsaInstructionKind::Assign,
target: Some(SsaNameId(4)),
uses: vec![SsaNameId(1)],
line: 5,
source_text: Some("y = x - 1".to_string()),
}],
successors: vec![3],
predecessors: vec![0],
},
SsaBlock {
id: 3,
label: Some("exit".to_string()),
lines: (7, 8),
phi_functions: vec![PhiFunction {
target: SsaNameId(5),
variable: "y".to_string(),
sources: vec![
PhiSource {
block: 1,
name: SsaNameId(3),
},
PhiSource {
block: 2,
name: SsaNameId(4),
},
],
line: 7,
}],
instructions: vec![SsaInstruction {
kind: SsaInstructionKind::Return,
target: None,
uses: vec![SsaNameId(5)],
line: 8,
source_text: Some("return y".to_string()),
}],
successors: vec![],
predecessors: vec![1, 2],
},
],
ssa_names: vec![
SsaName {
id: SsaNameId(1),
variable: "x".to_string(),
version: 1,
def_block: Some(0),
def_line: 1,
},
SsaName {
id: SsaNameId(2),
variable: "y".to_string(),
version: 1,
def_block: Some(0),
def_line: 2,
},
SsaName {
id: SsaNameId(3),
variable: "y".to_string(),
version: 2,
def_block: Some(1),
def_line: 3,
},
SsaName {
id: SsaNameId(4),
variable: "y".to_string(),
version: 3,
def_block: Some(2),
def_line: 5,
},
SsaName {
id: SsaNameId(5),
variable: "y".to_string(),
version: 4,
def_block: Some(3),
def_line: 7,
},
],
def_use: {
let mut map = HashMap::new();
map.insert(SsaNameId(1), vec![SsaNameId(3), SsaNameId(4)]);
map.insert(SsaNameId(3), vec![SsaNameId(5)]);
map.insert(SsaNameId(4), vec![SsaNameId(5)]);
map
},
stats: SsaStats {
phi_count: 1,
ssa_names: 5,
blocks: 4,
instructions: 5,
dead_phi_count: 0,
},
}
}
pub fn ssa_with_memory_ops() -> SsaFunction {
SsaFunction {
function: "memory_test".to_string(),
file: PathBuf::from("test.py"),
ssa_type: SsaType::Minimal,
blocks: vec![
SsaBlock {
id: 0,
label: Some("entry".to_string()),
lines: (1, 3),
phi_functions: vec![],
instructions: vec![
SsaInstruction {
kind: SsaInstructionKind::Assign,
target: Some(SsaNameId(1)),
uses: vec![],
line: 1,
source_text: Some("obj = MyClass()".to_string()),
},
SsaInstruction {
kind: SsaInstructionKind::Assign,
target: Some(SsaNameId(2)),
uses: vec![SsaNameId(1)],
line: 2,
source_text: Some("obj.field = 10".to_string()),
},
SsaInstruction {
kind: SsaInstructionKind::Assign,
target: Some(SsaNameId(3)),
uses: vec![SsaNameId(1)],
line: 3,
source_text: Some("x = obj.field".to_string()),
},
],
successors: vec![1, 2],
predecessors: vec![],
},
SsaBlock {
id: 1,
label: None,
lines: (4, 5),
phi_functions: vec![],
instructions: vec![
SsaInstruction {
kind: SsaInstructionKind::Assign,
target: Some(SsaNameId(4)),
uses: vec![SsaNameId(1)],
line: 4,
source_text: Some("obj.field = 20".to_string()),
},
],
successors: vec![3],
predecessors: vec![0],
},
SsaBlock {
id: 2,
label: None,
lines: (6, 7),
phi_functions: vec![],
instructions: vec![
SsaInstruction {
kind: SsaInstructionKind::Assign,
target: Some(SsaNameId(5)),
uses: vec![SsaNameId(1)],
line: 6,
source_text: Some("obj.field = 30".to_string()),
},
],
successors: vec![3],
predecessors: vec![0],
},
SsaBlock {
id: 3,
label: Some("merge".to_string()),
lines: (8, 10),
phi_functions: vec![],
instructions: vec![
SsaInstruction {
kind: SsaInstructionKind::Assign,
target: Some(SsaNameId(6)),
uses: vec![SsaNameId(1)],
line: 8,
source_text: Some("y = obj.field".to_string()),
},
SsaInstruction {
kind: SsaInstructionKind::Call,
target: None,
uses: vec![SsaNameId(6)],
line: 9,
source_text: Some("process(y)".to_string()),
},
SsaInstruction {
kind: SsaInstructionKind::Return,
target: None,
uses: vec![SsaNameId(6)],
line: 10,
source_text: Some("return y".to_string()),
},
],
successors: vec![],
predecessors: vec![1, 2],
},
],
ssa_names: vec![
SsaName {
id: SsaNameId(1),
variable: "obj".to_string(),
version: 1,
def_block: Some(0),
def_line: 1,
},
SsaName {
id: SsaNameId(2),
variable: "obj.field".to_string(),
version: 1,
def_block: Some(0),
def_line: 2,
},
SsaName {
id: SsaNameId(3),
variable: "x".to_string(),
version: 1,
def_block: Some(0),
def_line: 3,
},
SsaName {
id: SsaNameId(4),
variable: "obj.field".to_string(),
version: 2,
def_block: Some(1),
def_line: 4,
},
SsaName {
id: SsaNameId(5),
variable: "obj.field".to_string(),
version: 3,
def_block: Some(2),
def_line: 6,
},
SsaName {
id: SsaNameId(6),
variable: "y".to_string(),
version: 1,
def_block: Some(3),
def_line: 8,
},
],
def_use: HashMap::new(),
stats: SsaStats::default(),
}
}
}
#[cfg(test)]
mod dominator_tree_tests {
use super::*;
#[test]
fn test_linear_cfg_dominators() {
let cfg = fixtures::linear_cfg();
let dom_tree = build_dominator_tree(&cfg).expect("should build dominator tree");
assert_eq!(dom_tree.nodes.get(&0).unwrap().idom, None);
assert_eq!(dom_tree.nodes.get(&1).unwrap().idom, Some(0));
assert_eq!(dom_tree.nodes.get(&2).unwrap().idom, Some(1));
assert!(dom_tree.dominates(0, 0));
assert!(dom_tree.dominates(0, 1));
assert!(dom_tree.dominates(0, 2));
assert!(dom_tree.dominates(1, 2));
assert!(!dom_tree.dominates(1, 0));
}
#[test]
fn test_diamond_cfg_dominators() {
let cfg = fixtures::diamond_cfg();
let dom_tree = build_dominator_tree(&cfg).expect("should build dominator tree");
assert_eq!(dom_tree.nodes.get(&0).unwrap().idom, None);
assert_eq!(dom_tree.nodes.get(&1).unwrap().idom, Some(0));
assert_eq!(dom_tree.nodes.get(&2).unwrap().idom, Some(0));
assert_eq!(dom_tree.nodes.get(&3).unwrap().idom, Some(0));
assert_eq!(dom_tree.nodes.get(&4).unwrap().idom, Some(3));
assert!(!dom_tree.dominates(1, 3));
assert!(!dom_tree.dominates(2, 3));
}
#[test]
fn test_loop_cfg_dominators() {
let cfg = fixtures::loop_cfg();
let dom_tree = build_dominator_tree(&cfg).expect("should build dominator tree");
assert_eq!(dom_tree.nodes.get(&0).unwrap().idom, None);
assert_eq!(dom_tree.nodes.get(&1).unwrap().idom, Some(0));
assert_eq!(dom_tree.nodes.get(&3).unwrap().idom, Some(1));
assert_eq!(dom_tree.nodes.get(&2).unwrap().idom, Some(1));
assert!(dom_tree.dominates(1, 3));
assert!(!dom_tree.dominates(3, 1));
}
#[test]
fn test_complex_cfg_dominators() {
let cfg = fixtures::complex_cfg();
let dom_tree = build_dominator_tree(&cfg).expect("should build dominator tree");
for i in 0..8 {
assert!(
dom_tree.dominates(0, i),
"entry should dominate block {}",
i
);
}
assert!(dom_tree.dominates(1, 3));
assert!(dom_tree.dominates(1, 4));
assert!(dom_tree.dominates(1, 5));
assert!(!dom_tree.dominates(1, 6));
assert!(dom_tree.dominates(6, 7));
}
#[test]
fn test_dominator_tree_depth() {
let cfg = fixtures::linear_cfg();
let dom_tree = build_dominator_tree(&cfg).expect("should build dominator tree");
assert_eq!(dom_tree.nodes.get(&0).unwrap().depth, 0);
assert_eq!(dom_tree.nodes.get(&1).unwrap().depth, 1);
assert_eq!(dom_tree.nodes.get(&2).unwrap().depth, 2);
}
#[test]
fn test_dominated_by() {
let cfg = fixtures::diamond_cfg();
let dom_tree = build_dominator_tree(&cfg).expect("should build dominator tree");
let dominated_by_entry = dom_tree.dominated_by(0);
assert_eq!(dominated_by_entry.len(), 5);
let dominated_by_merge = dom_tree.dominated_by(3);
assert!(dominated_by_merge.contains(&3));
assert!(dominated_by_merge.contains(&4));
assert_eq!(dominated_by_merge.len(), 2);
}
#[test]
fn test_strictly_dominates() {
let cfg = fixtures::linear_cfg();
let dom_tree = build_dominator_tree(&cfg).expect("should build dominator tree");
assert!(!dom_tree.strictly_dominates(0, 0));
assert!(dom_tree.strictly_dominates(0, 1));
assert!(dom_tree.strictly_dominates(0, 2));
}
}
#[cfg(test)]
mod dominance_frontier_tests {
use super::*;
#[test]
fn test_entry_block_empty_frontier() {
let cfg = fixtures::diamond_cfg();
let dom_tree = build_dominator_tree(&cfg).expect("should build dominator tree");
let df = compute_dominance_frontier(&cfg, &dom_tree).expect("should compute DF");
assert!(
df.get(0).is_empty(),
"Entry block should have empty dominance frontier"
);
}
#[test]
fn test_diamond_dominance_frontier() {
let cfg = fixtures::diamond_cfg();
let dom_tree = build_dominator_tree(&cfg).expect("should build dominator tree");
let df = compute_dominance_frontier(&cfg, &dom_tree).expect("should compute DF");
assert!(
df.get(1).contains(&3),
"Branch block 1 should have merge (3) in DF"
);
assert!(
df.get(2).contains(&3),
"Branch block 2 should have merge (3) in DF"
);
assert!(df.get(3).is_empty());
}
#[test]
fn test_loop_dominance_frontier() {
let cfg = fixtures::loop_cfg();
let dom_tree = build_dominator_tree(&cfg).expect("should build dominator tree");
let df = compute_dominance_frontier(&cfg, &dom_tree).expect("should compute DF");
assert!(df.get(3).contains(&1), "Loop body should have header in DF");
}
#[test]
fn test_iterated_dominance_frontier() {
let cfg = fixtures::complex_cfg();
let dom_tree = build_dominator_tree(&cfg).expect("should build dominator tree");
let df = compute_dominance_frontier(&cfg, &dom_tree).expect("should compute DF");
let inner_blocks: HashSet<usize> = vec![3, 4].into_iter().collect();
let idf = df.iterated(&inner_blocks);
assert!(idf.contains(&5), "IDF should contain inner merge point");
}
#[test]
fn test_empty_idf() {
let cfg = fixtures::diamond_cfg();
let dom_tree = build_dominator_tree(&cfg).expect("should build dominator tree");
let df = compute_dominance_frontier(&cfg, &dom_tree).expect("should compute DF");
let empty: HashSet<usize> = HashSet::new();
let idf = df.iterated(&empty);
assert!(idf.is_empty());
}
}
#[cfg(test)]
mod minimal_ssa_tests {
use super::*;
#[test]
fn test_linear_no_phi() {
let cfg = fixtures::linear_cfg();
let dfg = DfgInfo {
function: "linear".to_string(),
refs: vec![
VarRef {
name: "x".to_string(),
ref_type: RefType::Definition,
line: 1,
column: 0,
context: None,
group_id: None,
},
VarRef {
name: "x".to_string(),
ref_type: RefType::Use,
line: 5,
column: 7,
context: None,
group_id: None,
},
],
edges: Vec::new(),
variables: Vec::new(),
};
let ssa = construct_minimal_ssa(&cfg, &dfg).expect("should construct SSA");
let total_phis: usize = ssa.blocks.iter().map(|b| b.phi_functions.len()).sum();
assert_eq!(total_phis, 0, "Linear code should have no phi functions");
}
#[test]
fn test_diamond_phi_at_merge() {
let cfg = fixtures::diamond_cfg();
let dfg = fixtures::diamond_dfg();
let ssa = construct_minimal_ssa(&cfg, &dfg).expect("should construct SSA");
let merge_block = ssa.blocks.iter().find(|b| b.id == 3).unwrap();
assert!(
merge_block
.phi_functions
.iter()
.any(|phi| phi.variable == "y"),
"Merge block should have phi for y"
);
let y_phi = merge_block
.phi_functions
.iter()
.find(|phi| phi.variable == "y")
.unwrap();
assert_eq!(y_phi.sources.len(), 2, "Phi should have 2 sources");
}
#[test]
fn test_loop_phi_at_header() {
let cfg = fixtures::loop_cfg();
let dfg = fixtures::loop_dfg();
let ssa = construct_minimal_ssa(&cfg, &dfg).expect("should construct SSA");
let header_block = ssa.blocks.iter().find(|b| b.id == 1).unwrap();
let has_total_phi = header_block
.phi_functions
.iter()
.any(|phi| phi.variable == "total");
let has_i_phi = header_block
.phi_functions
.iter()
.any(|phi| phi.variable == "i");
assert!(has_total_phi, "Header should have phi for total");
assert!(has_i_phi, "Header should have phi for i");
}
#[test]
fn test_multiple_variables_independent_phis() {
let cfg = fixtures::diamond_cfg();
let dfg = DfgInfo {
function: "diamond".to_string(),
refs: vec![
VarRef {
name: "x".to_string(),
ref_type: RefType::Definition,
line: 3,
column: 0,
context: None,
group_id: None,
},
VarRef {
name: "x".to_string(),
ref_type: RefType::Definition,
line: 5,
column: 0,
context: None,
group_id: None,
},
VarRef {
name: "y".to_string(),
ref_type: RefType::Definition,
line: 4,
column: 0,
context: None,
group_id: None,
},
VarRef {
name: "y".to_string(),
ref_type: RefType::Definition,
line: 6,
column: 0,
context: None,
group_id: None,
},
VarRef {
name: "x".to_string(),
ref_type: RefType::Use,
line: 9,
column: 7,
context: None,
group_id: None,
},
VarRef {
name: "y".to_string(),
ref_type: RefType::Use,
line: 9,
column: 11,
context: None,
group_id: None,
},
],
edges: Vec::new(),
variables: Vec::new(),
};
let ssa = construct_minimal_ssa(&cfg, &dfg).expect("should construct SSA");
let merge_block = ssa.blocks.iter().find(|b| b.id == 3).unwrap();
let x_phi = merge_block
.phi_functions
.iter()
.find(|phi| phi.variable == "x");
let y_phi = merge_block
.phi_functions
.iter()
.find(|phi| phi.variable == "y");
assert!(x_phi.is_some(), "Should have phi for x");
assert!(y_phi.is_some(), "Should have phi for y");
assert_eq!(merge_block.phi_functions.len(), 2);
}
#[test]
fn test_ssa_type_minimal() {
let cfg = fixtures::diamond_cfg();
let dfg = fixtures::diamond_dfg();
let ssa = construct_minimal_ssa(&cfg, &dfg).expect("should construct SSA");
assert_eq!(ssa.ssa_type, SsaType::Minimal);
}
}
#[cfg(test)]
mod pruned_ssa_tests {
use super::*;
#[test]
fn test_pruned_removes_dead_phi() {
let cfg = fixtures::diamond_cfg();
let dfg = DfgInfo {
function: "diamond".to_string(),
refs: vec![
VarRef {
name: "z".to_string(),
ref_type: RefType::Definition,
line: 3,
column: 0,
context: None,
group_id: None,
},
VarRef {
name: "z".to_string(),
ref_type: RefType::Definition,
line: 5,
column: 0,
context: None,
group_id: None,
},
],
edges: Vec::new(),
variables: Vec::new(),
};
let live_vars = LiveVariables {
function: "diamond".to_string(),
blocks: {
let mut map = HashMap::new();
map.insert(
3,
LiveSets {
live_in: HashSet::new(),
live_out: HashSet::new(),
},
);
map
},
};
let ssa =
construct_pruned_ssa(&cfg, &dfg, &live_vars).expect("should construct pruned SSA");
let merge_block = ssa.blocks.iter().find(|b| b.id == 3).unwrap();
assert!(
!merge_block
.phi_functions
.iter()
.any(|phi| phi.variable == "z"),
"Pruned SSA should not have phi for dead variable"
);
}
#[test]
fn test_semi_pruned_excludes_local() {
let cfg = fixtures::diamond_cfg();
let dfg = DfgInfo {
function: "diamond".to_string(),
refs: vec![
VarRef {
name: "temp".to_string(),
ref_type: RefType::Definition,
line: 3,
column: 0,
context: None,
group_id: None,
},
VarRef {
name: "temp".to_string(),
ref_type: RefType::Use,
line: 4,
column: 0,
context: None,
group_id: None,
},
],
edges: Vec::new(),
variables: Vec::new(),
};
let ssa = construct_semi_pruned_ssa(&cfg, &dfg).expect("should construct semi-pruned SSA");
for block in &ssa.blocks {
assert!(
!block.phi_functions.iter().any(|phi| phi.variable == "temp"),
"Semi-pruned SSA should not have phi for block-local variable"
);
}
}
#[test]
fn test_semi_pruned_includes_cross_block() {
let cfg = fixtures::diamond_cfg();
let dfg = fixtures::diamond_dfg();
let ssa = construct_semi_pruned_ssa(&cfg, &dfg).expect("should construct semi-pruned SSA");
let merge_block = ssa.blocks.iter().find(|b| b.id == 3).unwrap();
assert!(
merge_block
.phi_functions
.iter()
.any(|phi| phi.variable == "y"),
"Semi-pruned SSA should have phi for cross-block variable"
);
}
#[test]
fn test_ssa_type_pruned() {
let cfg = fixtures::diamond_cfg();
let dfg = fixtures::diamond_dfg();
let live_vars = LiveVariables {
function: "diamond".to_string(),
blocks: HashMap::new(),
};
let ssa =
construct_pruned_ssa(&cfg, &dfg, &live_vars).expect("should construct pruned SSA");
assert_eq!(ssa.ssa_type, SsaType::Pruned);
}
#[test]
fn test_pruned_phi_count_less_than_minimal() {
let cfg = fixtures::diamond_cfg();
let dfg = DfgInfo {
function: "diamond".to_string(),
refs: vec![
VarRef {
name: "y".to_string(),
ref_type: RefType::Definition,
line: 3,
column: 0,
context: None,
group_id: None,
},
VarRef {
name: "y".to_string(),
ref_type: RefType::Definition,
line: 5,
column: 0,
context: None,
group_id: None,
},
VarRef {
name: "y".to_string(),
ref_type: RefType::Use,
line: 9,
column: 0,
context: None,
group_id: None,
},
VarRef {
name: "z".to_string(),
ref_type: RefType::Definition,
line: 4,
column: 0,
context: None,
group_id: None,
},
VarRef {
name: "z".to_string(),
ref_type: RefType::Definition,
line: 6,
column: 0,
context: None,
group_id: None,
},
],
edges: Vec::new(),
variables: Vec::new(),
};
let minimal_ssa = construct_minimal_ssa(&cfg, &dfg).expect("should construct minimal SSA");
let minimal_phi_count: usize = minimal_ssa
.blocks
.iter()
.map(|b| b.phi_functions.len())
.sum();
let live_vars = LiveVariables {
function: "diamond".to_string(),
blocks: {
let mut map = HashMap::new();
map.insert(
3,
LiveSets {
live_in: {
let mut set = HashSet::new();
set.insert("y".to_string());
set
},
live_out: HashSet::new(),
},
);
map
},
};
let pruned_ssa =
construct_pruned_ssa(&cfg, &dfg, &live_vars).expect("should construct pruned SSA");
let pruned_phi_count: usize = pruned_ssa
.blocks
.iter()
.map(|b| b.phi_functions.len())
.sum();
assert!(
pruned_phi_count <= minimal_phi_count,
"Pruned SSA should have <= phi functions than minimal: pruned={}, minimal={}",
pruned_phi_count,
minimal_phi_count
);
}
#[test]
fn test_pruned_phi_for_live_variable() {
let cfg = fixtures::diamond_cfg();
let dfg = DfgInfo {
function: "diamond".to_string(),
refs: vec![
VarRef {
name: "y".to_string(),
ref_type: RefType::Definition,
line: 3,
column: 0,
context: None,
group_id: None,
},
VarRef {
name: "y".to_string(),
ref_type: RefType::Definition,
line: 5,
column: 0,
context: None,
group_id: None,
},
VarRef {
name: "y".to_string(),
ref_type: RefType::Use,
line: 9,
column: 0,
context: None,
group_id: None,
},
],
edges: Vec::new(),
variables: Vec::new(),
};
let live_vars = LiveVariables {
function: "diamond".to_string(),
blocks: {
let mut map = HashMap::new();
map.insert(
3,
LiveSets {
live_in: {
let mut set = HashSet::new();
set.insert("y".to_string());
set
},
live_out: HashSet::new(),
},
);
map
},
};
let ssa =
construct_pruned_ssa(&cfg, &dfg, &live_vars).expect("should construct pruned SSA");
let merge_block = ssa.blocks.iter().find(|b| b.id == 3).unwrap();
assert!(
merge_block
.phi_functions
.iter()
.any(|phi| phi.variable == "y"),
"Pruned SSA should have phi for live variable y"
);
}
#[test]
fn test_semi_pruned_block_local_detection() {
let cfg = fixtures::diamond_cfg();
let dfg = DfgInfo {
function: "diamond".to_string(),
refs: vec![
VarRef {
name: "local_var".to_string(),
ref_type: RefType::Definition,
line: 3,
column: 0,
context: None,
group_id: None,
},
VarRef {
name: "local_var".to_string(),
ref_type: RefType::Use,
line: 4,
column: 0,
context: None,
group_id: None,
},
VarRef {
name: "cross_var".to_string(),
ref_type: RefType::Definition,
line: 3,
column: 0,
context: None,
group_id: None,
},
VarRef {
name: "cross_var".to_string(),
ref_type: RefType::Definition,
line: 5,
column: 0,
context: None,
group_id: None,
},
VarRef {
name: "cross_var".to_string(),
ref_type: RefType::Use,
line: 7,
column: 0,
context: None,
group_id: None,
},
],
edges: Vec::new(),
variables: Vec::new(),
};
let ssa = construct_semi_pruned_ssa(&cfg, &dfg).expect("should construct semi-pruned SSA");
let has_local_phi = ssa.blocks.iter().any(|b| {
b.phi_functions
.iter()
.any(|phi| phi.variable == "local_var")
});
assert!(
!has_local_phi,
"Semi-pruned should not have phi for block-local variable"
);
let merge_block = ssa.blocks.iter().find(|b| b.id == 3).unwrap();
assert!(
merge_block
.phi_functions
.iter()
.any(|phi| phi.variable == "cross_var"),
"Semi-pruned should have phi for cross-block variable"
);
}
}
#[cfg(test)]
mod phase12_live_variables_tests {
use super::*;
#[test]
fn test_live_variables_simple_use() {
let cfg = fixtures::linear_cfg();
let refs = vec![
VarRef {
name: "x".to_string(),
ref_type: RefType::Definition,
line: 1,
column: 0,
context: None,
group_id: None,
},
VarRef {
name: "x".to_string(),
ref_type: RefType::Use,
line: 5,
column: 0,
context: None,
group_id: None,
},
];
let live = compute_live_variables(&cfg, &refs).expect("should compute live variables");
if let Some(block2) = live.blocks.get(&2) {
assert!(
block2.live_in.contains("x"),
"x should be live-in at block 2 (used at line 5)"
);
}
if let Some(block1) = live.blocks.get(&1) {
assert!(
block1.live_out.contains("x"),
"x should be live-out of block 1 (flows to use in block 2)"
);
}
}
#[test]
fn test_live_variables_not_live_after_use() {
let cfg = fixtures::linear_cfg();
let refs = vec![
VarRef {
name: "x".to_string(),
ref_type: RefType::Definition,
line: 1,
column: 0,
context: None,
group_id: None,
},
VarRef {
name: "x".to_string(),
ref_type: RefType::Use,
line: 3,
column: 0,
context: None,
group_id: None,
},
];
let live = compute_live_variables(&cfg, &refs).expect("should compute live variables");
if let Some(block2) = live.blocks.get(&2) {
assert!(
!block2.live_out.contains("x"),
"x should not be live-out of exit block (no more uses)"
);
}
}
#[test]
fn test_live_variables_loop() {
let cfg = fixtures::loop_cfg();
let refs = vec![
VarRef {
name: "i".to_string(),
ref_type: RefType::Definition,
line: 1,
column: 0,
context: None,
group_id: None,
},
VarRef {
name: "i".to_string(),
ref_type: RefType::Use,
line: 3, column: 0,
context: None,
group_id: None,
},
VarRef {
name: "i".to_string(),
ref_type: RefType::Update,
line: 6, column: 0,
context: None,
group_id: None,
},
];
let live = compute_live_variables(&cfg, &refs).expect("should compute live variables");
if let Some(header) = live.blocks.get(&1) {
assert!(
header.live_in.contains("i"),
"i should be live-in at loop header (used in condition)"
);
}
if let Some(body) = live.blocks.get(&3) {
assert!(
body.live_out.contains("i"),
"i should be live-out of loop body (flows back to header)"
);
}
}
#[test]
fn test_live_variables_unused_not_live() {
let cfg = fixtures::diamond_cfg();
let refs = vec![
VarRef {
name: "unused".to_string(),
ref_type: RefType::Definition,
line: 3,
column: 0,
context: None,
group_id: None,
},
];
let live = compute_live_variables(&cfg, &refs).expect("should compute live variables");
let any_live = live
.blocks
.values()
.any(|sets| sets.live_in.contains("unused") || sets.live_out.contains("unused"));
assert!(!any_live, "Unused variable should not be live anywhere");
}
#[test]
fn test_live_variables_multiple_vars() {
let cfg = fixtures::diamond_cfg();
let refs = vec![
VarRef {
name: "x".to_string(),
ref_type: RefType::Definition,
line: 1,
column: 0,
context: None,
group_id: None,
},
VarRef {
name: "x".to_string(),
ref_type: RefType::Use,
line: 9,
column: 0,
context: None,
group_id: None,
},
VarRef {
name: "y".to_string(),
ref_type: RefType::Definition,
line: 3,
column: 0,
context: None,
group_id: None,
},
VarRef {
name: "y".to_string(),
ref_type: RefType::Use,
line: 7,
column: 0,
context: None,
group_id: None,
},
];
let live = compute_live_variables(&cfg, &refs).expect("should compute live variables");
if let Some(merge) = live.blocks.get(&3) {
assert!(
merge.live_in.contains("x"),
"x should be live-in at merge (used at exit)"
);
assert!(
merge.live_in.contains("y"),
"y should be live-in at merge (used there)"
);
}
}
}
#[cfg(test)]
mod variable_versioning_tests {
use super::*;
#[test]
fn test_sequential_assignments_versioned() {
let cfg = fixtures::linear_cfg();
let dfg = DfgInfo {
function: "linear".to_string(),
refs: vec![
VarRef {
name: "x".to_string(),
ref_type: RefType::Definition,
line: 1,
column: 0,
context: None,
group_id: None,
},
VarRef {
name: "x".to_string(),
ref_type: RefType::Definition,
line: 3,
column: 0,
context: None,
group_id: None,
},
],
edges: Vec::new(),
variables: Vec::new(),
};
let ssa = construct_minimal_ssa(&cfg, &dfg).expect("should construct SSA");
let x_names: Vec<&SsaName> = ssa.ssa_names.iter().filter(|n| n.variable == "x").collect();
assert_eq!(x_names.len(), 2, "Should have 2 versions of x");
let versions: HashSet<u32> = x_names.iter().map(|n| n.version).collect();
assert!(versions.contains(&1));
assert!(versions.contains(&2));
}
#[test]
fn test_phi_target_versioning() {
let cfg = fixtures::diamond_cfg();
let dfg = fixtures::diamond_dfg();
let ssa = construct_minimal_ssa(&cfg, &dfg).expect("should construct SSA");
let merge_block = ssa.blocks.iter().find(|b| b.id == 3).unwrap();
let y_phi = merge_block
.phi_functions
.iter()
.find(|phi| phi.variable == "y")
.unwrap();
let target_name = ssa.ssa_names.iter().find(|n| n.id == y_phi.target).unwrap();
for source in &y_phi.sources {
let source_name = ssa.ssa_names.iter().find(|n| n.id == source.name).unwrap();
assert!(
target_name.version > source_name.version,
"Phi target version should be greater than source versions"
);
}
}
#[test]
fn test_definition_lookup() {
let ssa = fixtures::sample_ssa();
for name in &ssa.ssa_names {
let def_block = get_def_block(&ssa, name.id);
assert!(
def_block.is_some() || name.def_block.is_none(),
"Should find definition block for SSA name"
);
}
}
#[test]
fn test_ssa_name_formatting() {
let name = SsaName {
id: SsaNameId(1),
variable: "x".to_string(),
version: 3,
def_block: Some(0),
def_line: 1,
};
assert_eq!(name.format_name(), "x_3");
assert_eq!(format!("{}", name), "x_3");
}
#[test]
fn test_uses_reference_correct_version() {
let ssa = fixtures::sample_ssa();
let exit_block = ssa.blocks.iter().find(|b| b.id == 3).unwrap();
let return_inst = exit_block
.instructions
.iter()
.find(|i| i.kind == SsaInstructionKind::Return)
.unwrap();
assert!(
return_inst.uses.contains(&SsaNameId(5)),
"Return should use phi result"
);
}
}
#[cfg(test)]
mod memory_ssa_tests {
use super::*;
#[test]
fn test_store_creates_memory_version() {
let cfg = fixtures::diamond_cfg();
let ssa = fixtures::ssa_with_memory_ops();
let memory_ssa = build_memory_ssa(&cfg, &ssa).expect("should build memory SSA");
assert!(
memory_ssa.memory_defs.len() >= 3,
"Should have at least 3 memory defs (stores): got {}",
memory_ssa.memory_defs.len()
);
let versions: Vec<u32> = memory_ssa.memory_defs.iter().map(|d| d.version.0).collect();
for i in 1..versions.len() {
assert!(
versions[i] > versions[i - 1] || versions[i] > 0,
"Memory versions should be increasing: {:?}",
versions
);
}
let store_count = memory_ssa
.memory_defs
.iter()
.filter(|d| d.kind == Some(MemoryDefKind::Store))
.count();
assert!(store_count >= 3, "Should have at least 3 Store defs");
}
#[test]
fn test_load_uses_memory_version() {
let cfg = fixtures::diamond_cfg();
let ssa = fixtures::ssa_with_memory_ops();
let memory_ssa = build_memory_ssa(&cfg, &ssa).expect("should build memory SSA");
for use_ in &memory_ssa.memory_uses {
let version_exists = memory_ssa
.memory_defs
.iter()
.any(|d| d.version == use_.version)
|| memory_ssa
.memory_phis
.iter()
.any(|p| p.result == use_.version);
assert!(
version_exists || use_.version.0 == 0,
"Load should use existing memory version"
);
}
}
#[test]
fn test_memory_phi_at_merge() {
let cfg = fixtures::diamond_cfg();
let ssa = fixtures::ssa_with_memory_ops();
let memory_ssa = build_memory_ssa(&cfg, &ssa).expect("should build memory SSA");
let has_merge_phi = memory_ssa.memory_phis.iter().any(|phi| phi.block == 3);
if has_merge_phi {
let merge_phi = memory_ssa
.memory_phis
.iter()
.find(|phi| phi.block == 3)
.unwrap();
assert_eq!(
merge_phi.sources.len(),
2,
"Memory phi should have 2 sources"
);
let pred_blocks: Vec<usize> = merge_phi.sources.iter().map(|s| s.block).collect();
assert!(
pred_blocks.contains(&1) || pred_blocks.contains(&2),
"Phi sources should come from predecessor blocks"
);
}
}
#[test]
fn test_memory_def_use_chains() {
let cfg = fixtures::diamond_cfg();
let ssa = fixtures::ssa_with_memory_ops();
let memory_ssa = build_memory_ssa(&cfg, &ssa).expect("should build memory SSA");
for (def_version, uses) in &memory_ssa.def_use {
for use_version in uses {
assert!(
memory_ssa
.memory_uses
.iter()
.any(|u| &u.version == use_version)
|| memory_ssa
.memory_phis
.iter()
.any(|p| p.sources.iter().any(|s| &s.version == use_version))
|| memory_ssa
.memory_phis
.iter()
.any(|p| p.result == *use_version),
"Def-use chain for {:?} should reference valid use: {:?}",
def_version,
use_version
);
}
}
}
#[test]
fn test_memory_version_formatting() {
let version = MemoryVersion(5);
assert_eq!(format!("{}", version), "mem_5");
}
#[test]
fn test_call_clobbers_memory() {
let cfg = fixtures::diamond_cfg();
let ssa = fixtures::ssa_with_memory_ops();
let memory_ssa = build_memory_ssa(&cfg, &ssa).expect("should build memory SSA");
let call_defs: Vec<_> = memory_ssa
.memory_defs
.iter()
.filter(|d| d.kind == Some(MemoryDefKind::Call))
.collect();
let call_uses: Vec<_> = memory_ssa
.memory_uses
.iter()
.filter(|u| u.kind == Some(MemoryUseKind::Call))
.collect();
assert!(
!call_defs.is_empty(),
"Function calls should create memory defs (clobbers)"
);
assert!(
!call_uses.is_empty(),
"Function calls should create memory uses"
);
}
#[test]
fn test_memory_ssa_stats() {
let cfg = fixtures::diamond_cfg();
let ssa = fixtures::ssa_with_memory_ops();
let memory_ssa = build_memory_ssa(&cfg, &ssa).expect("should build memory SSA");
assert_eq!(
memory_ssa.stats.defs,
memory_ssa.memory_defs.len(),
"Stats defs should match memory_defs length"
);
assert_eq!(
memory_ssa.stats.uses,
memory_ssa.memory_uses.len(),
"Stats uses should match memory_uses length"
);
assert_eq!(
memory_ssa.stats.phis,
memory_ssa.memory_phis.len(),
"Stats phis should match memory_phis length"
);
}
#[test]
fn test_memory_def_clobbers_previous() {
let cfg = fixtures::diamond_cfg();
let ssa = fixtures::ssa_with_memory_ops();
let memory_ssa = build_memory_ssa(&cfg, &ssa).expect("should build memory SSA");
for def in &memory_ssa.memory_defs {
assert!(
def.clobbers.0 < def.version.0,
"Def version {} should clobber a lower version, got {}",
def.version.0,
def.clobbers.0
);
}
}
#[test]
fn test_explicit_def_use_chains() {
let cfg = fixtures::diamond_cfg();
let ssa = fixtures::ssa_with_memory_ops();
let memory_ssa = build_memory_ssa(&cfg, &ssa).expect("should build memory SSA");
let chains = build_explicit_def_use_chains(&memory_ssa);
assert_eq!(
chains.len(),
memory_ssa.memory_defs.len(),
"Should have one chain per memory def"
);
for chain in &chains {
let matching_def = memory_ssa
.memory_defs
.iter()
.find(|d| d.version == chain.def);
assert!(
matching_def.is_some(),
"Chain def should exist in memory_defs"
);
let def = matching_def.unwrap();
assert_eq!(chain.def_line, def.line, "Chain def_line should match");
assert_eq!(chain.def_block, def.block, "Chain def_block should match");
}
}
#[test]
fn test_empty_ssa_produces_empty_memory_ssa() {
let cfg = fixtures::linear_cfg();
let ssa = SsaFunction {
function: "empty".to_string(),
file: PathBuf::from("empty.py"),
ssa_type: SsaType::Minimal,
blocks: vec![SsaBlock {
id: 0,
label: None,
lines: (1, 1),
phi_functions: vec![],
instructions: vec![],
successors: vec![],
predecessors: vec![],
}],
ssa_names: vec![],
def_use: HashMap::new(),
stats: SsaStats::default(),
};
let memory_ssa = build_memory_ssa(&cfg, &ssa).expect("should build memory SSA");
assert!(
memory_ssa.memory_defs.is_empty(),
"Empty SSA should have no memory defs"
);
assert!(
memory_ssa.memory_uses.is_empty(),
"Empty SSA should have no memory uses"
);
assert!(
memory_ssa.memory_phis.is_empty(),
"Empty SSA should have no memory phis"
);
}
}
#[cfg(test)]
mod value_numbering_tests {
use super::*;
#[test]
fn test_same_expression_same_number() {
let ssa = SsaFunction {
function: "test".to_string(),
file: PathBuf::from("test.py"),
ssa_type: SsaType::Minimal,
blocks: vec![SsaBlock {
id: 0,
label: None,
lines: (1, 4),
phi_functions: vec![],
instructions: vec![
SsaInstruction {
kind: SsaInstructionKind::Param,
target: Some(SsaNameId(1)),
uses: vec![],
line: 1,
source_text: Some("a = param".to_string()),
},
SsaInstruction {
kind: SsaInstructionKind::Param,
target: Some(SsaNameId(2)),
uses: vec![],
line: 2,
source_text: Some("b = param".to_string()),
},
SsaInstruction {
kind: SsaInstructionKind::BinaryOp,
target: Some(SsaNameId(3)),
uses: vec![SsaNameId(1), SsaNameId(2)],
line: 3,
source_text: Some("x = a + b".to_string()),
},
SsaInstruction {
kind: SsaInstructionKind::BinaryOp,
target: Some(SsaNameId(4)),
uses: vec![SsaNameId(1), SsaNameId(2)],
line: 4,
source_text: Some("y = a + b".to_string()),
},
],
successors: vec![],
predecessors: vec![],
}],
ssa_names: vec![
SsaName {
id: SsaNameId(1),
variable: "a".to_string(),
version: 1,
def_block: Some(0),
def_line: 1,
},
SsaName {
id: SsaNameId(2),
variable: "b".to_string(),
version: 1,
def_block: Some(0),
def_line: 2,
},
SsaName {
id: SsaNameId(3),
variable: "x".to_string(),
version: 1,
def_block: Some(0),
def_line: 3,
},
SsaName {
id: SsaNameId(4),
variable: "y".to_string(),
version: 1,
def_block: Some(0),
def_line: 4,
},
],
def_use: HashMap::new(),
stats: SsaStats::default(),
};
let vn = compute_value_numbers(&ssa).expect("should compute value numbers");
let x_vn = vn.value_numbers.get(&SsaNameId(3)).unwrap();
let y_vn = vn.value_numbers.get(&SsaNameId(4)).unwrap();
assert_eq!(x_vn, y_vn, "Same expression should have same value number");
}
#[test]
fn test_different_operands_different_number() {
let ssa = SsaFunction {
function: "test".to_string(),
file: PathBuf::from("test.py"),
ssa_type: SsaType::Minimal,
blocks: vec![SsaBlock {
id: 0,
label: None,
lines: (1, 4),
phi_functions: vec![],
instructions: vec![
SsaInstruction {
kind: SsaInstructionKind::Param,
target: Some(SsaNameId(1)),
uses: vec![],
line: 1,
source_text: Some("a = param".to_string()),
},
SsaInstruction {
kind: SsaInstructionKind::Param,
target: Some(SsaNameId(2)),
uses: vec![],
line: 2,
source_text: Some("b = param".to_string()),
},
SsaInstruction {
kind: SsaInstructionKind::BinaryOp,
target: Some(SsaNameId(3)),
uses: vec![SsaNameId(1), SsaNameId(2)], line: 3,
source_text: Some("x = a + b".to_string()),
},
SsaInstruction {
kind: SsaInstructionKind::BinaryOp,
target: Some(SsaNameId(4)),
uses: vec![SsaNameId(1), SsaNameId(1)], line: 4,
source_text: Some("y = a + a".to_string()),
},
],
successors: vec![],
predecessors: vec![],
}],
ssa_names: vec![
SsaName {
id: SsaNameId(1),
variable: "a".to_string(),
version: 1,
def_block: Some(0),
def_line: 1,
},
SsaName {
id: SsaNameId(2),
variable: "b".to_string(),
version: 1,
def_block: Some(0),
def_line: 2,
},
SsaName {
id: SsaNameId(3),
variable: "x".to_string(),
version: 1,
def_block: Some(0),
def_line: 3,
},
SsaName {
id: SsaNameId(4),
variable: "y".to_string(),
version: 1,
def_block: Some(0),
def_line: 4,
},
],
def_use: HashMap::new(),
stats: SsaStats::default(),
};
let vn = compute_value_numbers(&ssa).expect("should compute value numbers");
let x_vn = vn.value_numbers.get(&SsaNameId(3)).unwrap();
let y_vn = vn.value_numbers.get(&SsaNameId(4)).unwrap();
assert_ne!(
x_vn, y_vn,
"Different expressions should have different value numbers"
);
}
#[test]
fn test_equivalences_populated() {
let ssa = fixtures::sample_ssa();
let vn = compute_value_numbers(&ssa).expect("should compute value numbers");
for (vnum, names) in &vn.equivalences {
for name in names {
assert_eq!(
vn.value_numbers.get(name),
Some(vnum),
"Equivalence entry should match value number"
);
}
}
}
}
#[cfg(test)]
mod sccp_tests {
use super::*;
#[test]
fn test_constant_propagation() {
let ssa = SsaFunction {
function: "test".to_string(),
file: PathBuf::from("test.py"),
ssa_type: SsaType::Minimal,
blocks: vec![SsaBlock {
id: 0,
label: None,
lines: (1, 3),
phi_functions: vec![],
instructions: vec![
SsaInstruction {
kind: SsaInstructionKind::Assign,
target: Some(SsaNameId(1)),
uses: vec![],
line: 1,
source_text: Some("x = 1".to_string()),
},
SsaInstruction {
kind: SsaInstructionKind::Assign,
target: Some(SsaNameId(2)),
uses: vec![SsaNameId(1)],
line: 2,
source_text: Some("y = x".to_string()),
},
],
successors: vec![],
predecessors: vec![],
}],
ssa_names: vec![
SsaName {
id: SsaNameId(1),
variable: "x".to_string(),
version: 1,
def_block: Some(0),
def_line: 1,
},
SsaName {
id: SsaNameId(2),
variable: "y".to_string(),
version: 1,
def_block: Some(0),
def_line: 2,
},
],
def_use: HashMap::new(),
stats: SsaStats::default(),
};
let sccp_result = run_sccp(&ssa).expect("should run SCCP");
assert_eq!(
sccp_result.constants.get(&SsaNameId(1)),
Some(&ConstantValue::Int(1))
);
assert_eq!(
sccp_result.constants.get(&SsaNameId(2)),
Some(&ConstantValue::Int(1))
);
}
#[test]
fn test_unreachable_code_detection() {
let ssa = SsaFunction {
function: "test".to_string(),
file: PathBuf::from("test.py"),
ssa_type: SsaType::Minimal,
blocks: vec![
SsaBlock {
id: 0,
label: Some("entry".to_string()),
lines: (1, 2),
phi_functions: vec![],
instructions: vec![
SsaInstruction {
kind: SsaInstructionKind::Assign,
target: Some(SsaNameId(1)),
uses: vec![],
line: 1,
source_text: Some("cond = False".to_string()),
},
SsaInstruction {
kind: SsaInstructionKind::Branch,
target: None,
uses: vec![SsaNameId(1)],
line: 2,
source_text: Some("if cond:".to_string()),
},
],
successors: vec![1, 2],
predecessors: vec![],
},
SsaBlock {
id: 1,
label: Some("unreachable".to_string()),
lines: (3, 4),
phi_functions: vec![],
instructions: vec![SsaInstruction {
kind: SsaInstructionKind::Assign,
target: Some(SsaNameId(2)),
uses: vec![],
line: 3,
source_text: Some("x = 1".to_string()),
}],
successors: vec![2],
predecessors: vec![0],
},
SsaBlock {
id: 2,
label: Some("exit".to_string()),
lines: (5, 5),
phi_functions: vec![],
instructions: vec![],
successors: vec![],
predecessors: vec![0, 1],
},
],
ssa_names: vec![
SsaName {
id: SsaNameId(1),
variable: "cond".to_string(),
version: 1,
def_block: Some(0),
def_line: 1,
},
SsaName {
id: SsaNameId(2),
variable: "x".to_string(),
version: 1,
def_block: Some(1),
def_line: 3,
},
],
def_use: HashMap::new(),
stats: SsaStats::default(),
};
let sccp_result = run_sccp(&ssa).expect("should run SCCP");
assert!(
sccp_result.unreachable_blocks.contains(&1),
"Block 1 should be detected as unreachable"
);
}
#[test]
fn test_lattice_meet() {
let top = LatticeValue::Top;
let const_1 = LatticeValue::Constant(ConstantValue::Int(1));
let const_2 = LatticeValue::Constant(ConstantValue::Int(2));
let bottom = LatticeValue::Bottom;
assert_eq!(top, LatticeValue::Top);
assert_eq!(const_1, LatticeValue::Constant(ConstantValue::Int(1)));
assert_ne!(const_1, const_2);
assert_eq!(bottom, LatticeValue::Bottom);
}
#[test]
fn test_constant_value_display() {
assert_eq!(format!("{}", ConstantValue::Int(42)), "42");
assert_eq!(
format!("{}", ConstantValue::Float("3.14".to_string())),
"3.14"
);
assert_eq!(
format!("{}", ConstantValue::String("hello".to_string())),
"\"hello\""
);
assert_eq!(format!("{}", ConstantValue::Bool(true)), "true");
assert_eq!(format!("{}", ConstantValue::None), "None");
}
}
#[cfg(test)]
mod dead_code_tests {
use super::*;
#[test]
fn test_unused_definition_is_dead() {
let ssa = SsaFunction {
function: "test".to_string(),
file: PathBuf::from("test.py"),
ssa_type: SsaType::Minimal,
blocks: vec![SsaBlock {
id: 0,
label: None,
lines: (1, 3),
phi_functions: vec![],
instructions: vec![
SsaInstruction {
kind: SsaInstructionKind::Assign,
target: Some(SsaNameId(1)),
uses: vec![],
line: 1,
source_text: Some("x = 1".to_string()), },
SsaInstruction {
kind: SsaInstructionKind::Assign,
target: Some(SsaNameId(2)),
uses: vec![],
line: 2,
source_text: Some("y = 2".to_string()),
},
SsaInstruction {
kind: SsaInstructionKind::Return,
target: None,
uses: vec![SsaNameId(2)], line: 3,
source_text: Some("return y".to_string()),
},
],
successors: vec![],
predecessors: vec![],
}],
ssa_names: vec![
SsaName {
id: SsaNameId(1),
variable: "x".to_string(),
version: 1,
def_block: Some(0),
def_line: 1,
},
SsaName {
id: SsaNameId(2),
variable: "y".to_string(),
version: 1,
def_block: Some(0),
def_line: 2,
},
],
def_use: {
let mut map = HashMap::new();
map.insert(SsaNameId(1), vec![]);
map.insert(SsaNameId(2), vec![]);
map
},
stats: SsaStats::default(),
};
let dead = find_dead_code(&ssa).expect("should find dead code");
assert!(
dead.contains(&SsaNameId(1)),
"Unused definition should be marked as dead"
);
assert!(
!dead.contains(&SsaNameId(2)),
"Used definition should not be marked as dead"
);
}
#[test]
fn test_cascading_dead_code() {
let ssa = SsaFunction {
function: "test".to_string(),
file: PathBuf::from("test.py"),
ssa_type: SsaType::Minimal,
blocks: vec![SsaBlock {
id: 0,
label: None,
lines: (1, 4),
phi_functions: vec![],
instructions: vec![
SsaInstruction {
kind: SsaInstructionKind::Assign,
target: Some(SsaNameId(1)),
uses: vec![],
line: 1,
source_text: Some("x = 1".to_string()),
},
SsaInstruction {
kind: SsaInstructionKind::BinaryOp,
target: Some(SsaNameId(2)),
uses: vec![SsaNameId(1)],
line: 2,
source_text: Some("y = x + 1".to_string()),
},
SsaInstruction {
kind: SsaInstructionKind::BinaryOp,
target: Some(SsaNameId(3)),
uses: vec![SsaNameId(2)],
line: 3,
source_text: Some("z = y + 1".to_string()),
},
SsaInstruction {
kind: SsaInstructionKind::Return,
target: None,
uses: vec![],
line: 4,
source_text: Some("return 0".to_string()),
},
],
successors: vec![],
predecessors: vec![],
}],
ssa_names: vec![
SsaName {
id: SsaNameId(1),
variable: "x".to_string(),
version: 1,
def_block: Some(0),
def_line: 1,
},
SsaName {
id: SsaNameId(2),
variable: "y".to_string(),
version: 1,
def_block: Some(0),
def_line: 2,
},
SsaName {
id: SsaNameId(3),
variable: "z".to_string(),
version: 1,
def_block: Some(0),
def_line: 3,
},
],
def_use: HashMap::new(),
stats: SsaStats::default(),
};
let dead = find_dead_code(&ssa).expect("should find dead code");
assert!(dead.contains(&SsaNameId(1)), "x should be dead");
assert!(dead.contains(&SsaNameId(2)), "y should be dead");
assert!(dead.contains(&SsaNameId(3)), "z should be dead");
}
#[test]
fn test_side_effects_prevent_removal() {
assert!(has_side_effects(&SsaInstructionKind::Call));
assert!(has_side_effects(&SsaInstructionKind::Return));
assert!(!has_side_effects(&SsaInstructionKind::Assign));
assert!(!has_side_effects(&SsaInstructionKind::BinaryOp));
}
}
#[cfg(test)]
mod output_format_tests {
use super::*;
#[test]
fn test_json_output_valid() {
let ssa = fixtures::sample_ssa();
let json = serde_json::to_string_pretty(&ssa).expect("should serialize to JSON");
assert!(validate_json(&json), "JSON output should be valid");
assert!(json.contains("\"function\""));
assert!(json.contains("\"ssa_type\""));
assert!(json.contains("\"blocks\""));
assert!(json.contains("\"phi_functions\""));
assert!(json.contains("\"ssa_names\""));
assert!(json.contains("\"stats\""));
}
#[test]
fn test_json_roundtrip() {
let ssa = fixtures::sample_ssa();
let json = serde_json::to_string(&ssa).expect("should serialize");
let parsed: SsaFunction = serde_json::from_str(&json).expect("should deserialize");
assert_eq!(parsed.function, ssa.function);
assert_eq!(parsed.ssa_type, ssa.ssa_type);
assert_eq!(parsed.blocks.len(), ssa.blocks.len());
assert_eq!(parsed.ssa_names.len(), ssa.ssa_names.len());
}
#[test]
fn test_text_format_readable() {
let ssa = fixtures::sample_ssa();
let text = format_ssa_text(&ssa);
assert!(text.contains("sample"));
assert!(text.contains("Minimal"));
assert!(text.contains("Block 0"));
assert!(text.contains("Block 3"));
assert!(text.contains("phi("));
assert!(text.contains("Phi Functions:"));
assert!(text.contains("SSA Names:"));
}
#[test]
fn test_dot_format_valid() {
let ssa = fixtures::sample_ssa();
let dot = format_ssa_dot(&ssa);
assert!(validate_dot(&dot), "DOT output should be valid");
assert!(dot.contains("digraph SSA"));
assert!(dot.contains("block0"));
assert!(dot.contains("block3"));
assert!(dot.contains("->"));
}
#[test]
fn test_memory_ssa_text_format() {
let memory_ssa = MemorySsa {
function: "test".to_string(),
file: Some("test.py".to_string()),
memory_phis: vec![MemoryPhi {
result: MemoryVersion(3),
block: 2,
sources: vec![
MemoryPhiSource {
block: 0,
version: MemoryVersion(1),
},
MemoryPhiSource {
block: 1,
version: MemoryVersion(2),
},
],
}],
memory_defs: vec![MemoryDef {
version: MemoryVersion(1),
clobbers: MemoryVersion(0),
block: 0,
line: 1,
access: "x.field".to_string(),
kind: Some(MemoryDefKind::Store),
}],
memory_uses: vec![MemoryUse {
version: MemoryVersion(1),
block: 1,
line: 3,
access: "x.field".to_string(),
kind: Some(MemoryUseKind::Load),
}],
def_use: HashMap::new(),
stats: MemorySsaStats::default(),
};
let text = format_memory_ssa_text(&memory_ssa);
assert!(text.contains("Memory SSA"));
assert!(text.contains("Memory Phi"));
assert!(text.contains("Memory Definitions"));
assert!(text.contains("Memory Uses"));
}
#[test]
fn test_empty_ssa_serializes() {
let ssa = SsaFunction {
function: "empty".to_string(),
file: PathBuf::from("empty.py"),
ssa_type: SsaType::Minimal,
blocks: vec![],
ssa_names: vec![],
def_use: HashMap::new(),
stats: SsaStats::default(),
};
let json = serde_json::to_string(&ssa).expect("should serialize empty SSA");
assert!(validate_json(&json));
let text = format_ssa_text(&ssa);
assert!(text.contains("empty"));
let dot = format_ssa_dot(&ssa);
assert!(validate_dot(&dot));
}
#[test]
fn test_json_format_function() {
let ssa = fixtures::sample_ssa();
let json_pretty = format_ssa_json(&ssa).expect("should format as JSON");
assert!(validate_json(&json_pretty));
assert!(
json_pretty.contains('\n'),
"Pretty JSON should have newlines"
);
let json_compact = format_ssa_json_compact(&ssa).expect("should format as compact JSON");
assert!(validate_json(&json_compact));
assert!(
!json_compact.contains('\n'),
"Compact JSON should not have newlines"
);
}
#[test]
fn test_json_ssa_name_id_numeric() {
let ssa = fixtures::sample_ssa();
let json = format_ssa_json(&ssa).expect("should serialize");
let parsed: serde_json::Value = serde_json::from_str(&json).expect("valid json");
if let Some(blocks) = parsed.get("blocks").and_then(|b| b.as_array()) {
for block in blocks {
if let Some(instructions) = block.get("instructions").and_then(|i| i.as_array()) {
for instr in instructions {
if let Some(target) = instr.get("target") {
assert!(
target.is_number() || target.is_null(),
"SsaNameId should serialize as number, got: {}",
target
);
}
}
}
}
}
}
#[test]
fn test_text_phi_format() {
let ssa = fixtures::sample_ssa();
let text = format_ssa_text(&ssa);
assert!(text.contains("phi("), "Text should contain phi functions");
assert!(
text.contains("[Block"),
"Phi sources should reference blocks"
);
}
#[test]
fn test_dot_escaping() {
let ssa = SsaFunction {
function: "test_func".to_string(),
file: PathBuf::from("test.py"),
ssa_type: SsaType::Minimal,
blocks: vec![SsaBlock {
id: 0,
label: Some("entry\"with\"quotes".to_string()), lines: (1, 2),
phi_functions: vec![],
instructions: vec![SsaInstruction {
kind: SsaInstructionKind::Assign,
target: Some(SsaNameId(1)),
uses: vec![],
line: 1,
source_text: Some("x = \"string\"".to_string()), }],
successors: vec![],
predecessors: vec![],
}],
ssa_names: vec![SsaName {
id: SsaNameId(1),
variable: "var_with_underscore".to_string(),
version: 1,
def_block: Some(0),
def_line: 1,
}],
def_use: HashMap::new(),
stats: SsaStats::default(),
};
let dot = format_ssa_dot(&ssa);
assert!(validate_dot(&dot), "DOT with special chars should be valid");
assert!(
!dot.contains("\"\""),
"Double quotes should not appear unescaped"
);
}
#[test]
fn test_filter_by_variable() {
let ssa = fixtures::sample_ssa();
let original_ssa_names = ssa.stats.ssa_names;
let filtered = filter_ssa_by_variable(ssa.clone(), "y");
for name in &filtered.ssa_names {
assert_eq!(
name.variable, "y",
"Filtered SSA should only contain 'y' names"
);
}
for block in &filtered.blocks {
for phi in &block.phi_functions {
assert_eq!(phi.variable, "y", "Filtered phi should be for 'y'");
}
}
assert!(filtered.ssa_names.len() <= original_ssa_names);
}
#[test]
fn test_filter_nonexistent_variable() {
let ssa = fixtures::sample_ssa();
let filtered = filter_ssa_by_variable(ssa, "nonexistent");
assert!(
filtered.ssa_names.is_empty(),
"Should have no SSA names for nonexistent var"
);
}
#[test]
fn test_dot_def_use_edges() {
let ssa = fixtures::sample_ssa();
let dot = format_ssa_dot_with_def_use(&ssa);
assert!(
dot.contains("dashed") || dot.contains("style"),
"Should have styled def-use edges"
);
}
#[test]
fn test_text_format_versioned_names() {
let ssa = fixtures::sample_ssa();
let text = format_ssa_text(&ssa);
assert!(
text.contains("_1") || text.contains("$"),
"Should show versioned variable names"
);
}
}
#[cfg(test)]
mod live_variables_tests {
use super::*;
#[test]
fn test_variable_live_before_redef() {
let cfg = fixtures::linear_cfg();
let refs = vec![
VarRef {
name: "x".to_string(),
ref_type: RefType::Definition,
line: 1,
column: 0,
context: None,
group_id: None,
},
VarRef {
name: "x".to_string(),
ref_type: RefType::Use,
line: 3,
column: 4,
context: None,
group_id: None,
},
VarRef {
name: "x".to_string(),
ref_type: RefType::Definition,
line: 4,
column: 0,
context: None,
group_id: None,
},
];
let live = compute_live_variables(&cfg, &refs).expect("should compute live variables");
if let Some(block1_live) = live.blocks.get(&1) {
assert!(
block1_live.live_in.contains("x"),
"x should be live at entry to block 1"
);
}
}
#[test]
fn test_variable_dead_after_last_use() {
let cfg = fixtures::linear_cfg();
let refs = vec![
VarRef {
name: "x".to_string(),
ref_type: RefType::Definition,
line: 1,
column: 0,
context: None,
group_id: None,
},
VarRef {
name: "x".to_string(),
ref_type: RefType::Use,
line: 3,
column: 4,
context: None,
group_id: None,
},
];
let live = compute_live_variables(&cfg, &refs).expect("should compute live variables");
if let Some(last_block_live) = live.blocks.get(&2) {
assert!(
!last_block_live.live_out.contains("x"),
"x should not be live at exit of last block"
);
}
}
#[test]
fn test_multiple_variables_liveness() {
let cfg = fixtures::diamond_cfg();
let refs = vec![
VarRef {
name: "x".to_string(),
ref_type: RefType::Definition,
line: 1,
column: 0,
context: None,
group_id: None,
},
VarRef {
name: "y".to_string(),
ref_type: RefType::Definition,
line: 3,
column: 0,
context: None,
group_id: None,
},
VarRef {
name: "x".to_string(),
ref_type: RefType::Use,
line: 9,
column: 7,
context: None,
group_id: None,
},
];
let live = compute_live_variables(&cfg, &refs).expect("should compute live variables");
let any_y_live = live
.blocks
.values()
.any(|sets| sets.live_in.contains("y") || sets.live_out.contains("y"));
assert!(!any_y_live, "Unused variable y should not be live anywhere");
}
}
#[cfg(test)]
mod available_expressions_tests {
use super::*;
use crate::types::Language;
#[test]
fn test_expression_available_all_paths() {
let cfg = fixtures::linear_cfg();
let source = r#"
x = a + b
y = c + d
z = a + b # a + b should be available here
"#;
let _avail =
compute_available_expressions(&cfg, source, Language::Python).expect("should compute");
}
#[test]
fn test_expression_killed_on_redef() {
let cfg = fixtures::linear_cfg();
let source = r#"
x = a + b
a = 5 # kills "a + b"
z = a + b # a + b NOT available (must recompute)
"#;
let _avail =
compute_available_expressions(&cfg, source, Language::Python).expect("should compute");
}
#[test]
fn test_intersection_at_merge() {
let cfg = fixtures::diamond_cfg();
let source = r#"
if cond:
x = a + b
else:
y = c + d
# At merge: neither a+b nor c+d is available (not on all paths)
z = a + b
"#;
let _avail =
compute_available_expressions(&cfg, source, Language::Python).expect("should compute");
}
}
#[cfg(test)]
mod available_expressions_ast_tests {
use super::*;
use crate::types::Language;
fn single_block_cfg(max_line: u32) -> CfgInfo {
CfgInfo {
function: "test_func".to_string(),
blocks: vec![CfgBlock {
id: 0,
block_type: BlockType::Entry,
lines: (1, max_line),
calls: Vec::new(),
}],
edges: vec![],
entry_block: 0,
exit_blocks: vec![0],
cyclomatic_complexity: 1,
nested_functions: HashMap::new(),
}
}
fn assert_has_expression_with_operands(avail: &AvailableExpressions, left: &str, right: &str) {
let found = avail.expressions.iter().any(|e| {
(e.uses.contains(&left.to_string()) && e.uses.contains(&right.to_string()))
|| e.text.contains(left) && e.text.contains(right)
});
assert!(
found,
"Expected expression with operands '{}' and '{}', but found expressions: {:?}",
left,
right,
avail
.expressions
.iter()
.map(|e| &e.text)
.collect::<Vec<_>>()
);
}
#[test]
fn test_python_ast_available_expressions() {
let source = "x = a + b\ny = a + b\n";
let cfg = single_block_cfg(2);
let avail = compute_available_expressions(&cfg, source, Language::Python)
.expect("should compute for Python");
assert!(
!avail.expressions.is_empty(),
"Python: should find binary expression a + b"
);
assert_has_expression_with_operands(&avail, "a", "b");
}
#[test]
fn test_python_ast_subtraction() {
let source = "x = a - b\ny = a - b\n";
let cfg = single_block_cfg(2);
let avail = compute_available_expressions(&cfg, source, Language::Python)
.expect("should compute for Python subtraction");
assert!(
!avail.expressions.is_empty(),
"Python: should find binary expression a - b"
);
}
#[test]
fn test_python_ast_multiplication() {
let source = "x = a * b\ny = a * b\n";
let cfg = single_block_cfg(2);
let avail = compute_available_expressions(&cfg, source, Language::Python)
.expect("should compute for Python multiplication");
assert!(
!avail.expressions.is_empty(),
"Python: should find binary expression a * b"
);
}
#[test]
fn test_typescript_ast_available_expressions() {
let source = "const x = a + b;\nconst y = a + b;\n";
let cfg = single_block_cfg(2);
let avail = compute_available_expressions(&cfg, source, Language::TypeScript)
.expect("should compute for TypeScript");
assert!(
!avail.expressions.is_empty(),
"TypeScript: should find binary expression a + b"
);
assert_has_expression_with_operands(&avail, "a", "b");
}
#[test]
fn test_typescript_let_assignment() {
let source = "let x = a + b;\nlet y = a + b;\n";
let cfg = single_block_cfg(2);
let avail = compute_available_expressions(&cfg, source, Language::TypeScript)
.expect("should compute for TypeScript let");
assert!(
!avail.expressions.is_empty(),
"TypeScript: should find binary expression in let assignment"
);
}
#[test]
fn test_javascript_ast_available_expressions() {
let source = "var x = a + b;\nvar y = a + b;\n";
let cfg = single_block_cfg(2);
let avail = compute_available_expressions(&cfg, source, Language::JavaScript)
.expect("should compute for JavaScript");
assert!(
!avail.expressions.is_empty(),
"JavaScript: should find binary expression a + b"
);
assert_has_expression_with_operands(&avail, "a", "b");
}
#[test]
fn test_go_ast_available_expressions() {
let source = "x := a + b\ny := a + b\n";
let cfg = single_block_cfg(2);
let avail = compute_available_expressions(&cfg, source, Language::Go)
.expect("should compute for Go");
assert!(
!avail.expressions.is_empty(),
"Go: should find binary expression a + b"
);
assert_has_expression_with_operands(&avail, "a", "b");
}
#[test]
fn test_go_assignment_statement() {
let source = "x = a + b\ny = a + b\n";
let cfg = single_block_cfg(2);
let avail = compute_available_expressions(&cfg, source, Language::Go)
.expect("should compute for Go assignment");
assert!(
!avail.expressions.is_empty(),
"Go: should find binary expression in regular assignment"
);
}
#[test]
fn test_rust_ast_available_expressions() {
let source = "let x = a + b;\nlet y = a + b;\n";
let cfg = single_block_cfg(2);
let avail = compute_available_expressions(&cfg, source, Language::Rust)
.expect("should compute for Rust");
assert!(
!avail.expressions.is_empty(),
"Rust: should find binary expression a + b"
);
assert_has_expression_with_operands(&avail, "a", "b");
}
#[test]
fn test_java_ast_available_expressions() {
let source = "int x = a + b;\nint y = a + b;\n";
let cfg = single_block_cfg(2);
let avail = compute_available_expressions(&cfg, source, Language::Java)
.expect("should compute for Java");
assert!(
!avail.expressions.is_empty(),
"Java: should find binary expression a + b"
);
assert_has_expression_with_operands(&avail, "a", "b");
}
#[test]
fn test_kotlin_ast_available_expressions() {
let source = "val x = a + b\nval y = a + b\n";
let cfg = single_block_cfg(2);
let avail = compute_available_expressions(&cfg, source, Language::Kotlin)
.expect("should compute for Kotlin");
assert!(
!avail.expressions.is_empty(),
"Kotlin: should find binary expression a + b"
);
assert_has_expression_with_operands(&avail, "a", "b");
}
#[test]
fn test_c_ast_available_expressions() {
let source = "int x = a + b;\nint y = a + b;\n";
let cfg = single_block_cfg(2);
let avail =
compute_available_expressions(&cfg, source, Language::C).expect("should compute for C");
assert!(
!avail.expressions.is_empty(),
"C: should find binary expression a + b"
);
assert_has_expression_with_operands(&avail, "a", "b");
}
#[test]
fn test_cpp_ast_available_expressions() {
let source = "int x = a + b;\nint y = a + b;\n";
let cfg = single_block_cfg(2);
let avail = compute_available_expressions(&cfg, source, Language::Cpp)
.expect("should compute for C++");
assert!(
!avail.expressions.is_empty(),
"C++: should find binary expression a + b"
);
assert_has_expression_with_operands(&avail, "a", "b");
}
#[test]
fn test_ruby_ast_available_expressions() {
let source = "x = a + b\ny = a + b\n";
let cfg = single_block_cfg(2);
let avail = compute_available_expressions(&cfg, source, Language::Ruby)
.expect("should compute for Ruby");
assert!(
!avail.expressions.is_empty(),
"Ruby: should find binary expression a + b"
);
assert_has_expression_with_operands(&avail, "a", "b");
}
#[test]
fn test_swift_ast_available_expressions() {
let source = "let x = a + b\nlet y = a + b\n";
let cfg = single_block_cfg(2);
let _avail = compute_available_expressions(&cfg, source, Language::Swift)
.expect("should compute for Swift (regex fallback)");
}
#[test]
fn test_csharp_ast_available_expressions() {
let source = "int x = a + b;\nint y = a + b;\n";
let cfg = single_block_cfg(2);
let avail = compute_available_expressions(&cfg, source, Language::CSharp)
.expect("should compute for C#");
assert!(
!avail.expressions.is_empty(),
"C#: should find binary expression a + b"
);
assert_has_expression_with_operands(&avail, "a", "b");
}
#[test]
fn test_scala_ast_available_expressions() {
let source = "val x = a + b\nval y = a + b\n";
let cfg = single_block_cfg(2);
let avail = compute_available_expressions(&cfg, source, Language::Scala)
.expect("should compute for Scala");
assert!(
!avail.expressions.is_empty(),
"Scala: should find binary expression a + b"
);
assert_has_expression_with_operands(&avail, "a", "b");
}
#[test]
fn test_php_ast_available_expressions() {
let source = "<?php\n$x = $a + $b;\n$y = $a + $b;\n";
let cfg = single_block_cfg(3);
let avail = compute_available_expressions(&cfg, source, Language::Php)
.expect("should compute for PHP");
assert!(
!avail.expressions.is_empty(),
"PHP: should find binary expression $a + $b"
);
}
#[test]
fn test_lua_ast_available_expressions() {
let source = "local x = a + b\nlocal y = a + b\n";
let cfg = single_block_cfg(2);
let avail = compute_available_expressions(&cfg, source, Language::Lua)
.expect("should compute for Lua");
assert!(
!avail.expressions.is_empty(),
"Lua: should find binary expression a + b"
);
assert_has_expression_with_operands(&avail, "a", "b");
}
#[test]
fn test_luau_ast_available_expressions() {
let source = "local x = a + b\nlocal y = a + b\n";
let cfg = single_block_cfg(2);
let avail = compute_available_expressions(&cfg, source, Language::Luau)
.expect("should compute for Luau");
assert!(
!avail.expressions.is_empty(),
"Luau: should find binary expression a + b"
);
assert_has_expression_with_operands(&avail, "a", "b");
}
#[test]
fn test_elixir_ast_available_expressions() {
let source = "x = a + b\ny = a + b\n";
let cfg = single_block_cfg(2);
let avail = compute_available_expressions(&cfg, source, Language::Elixir)
.expect("should compute for Elixir");
assert!(
!avail.expressions.is_empty(),
"Elixir: should find binary expression a + b"
);
assert_has_expression_with_operands(&avail, "a", "b");
}
#[test]
fn test_ocaml_ast_available_expressions() {
let source = "let x = a + b\nlet y = a + b\n";
let cfg = single_block_cfg(2);
let avail = compute_available_expressions(&cfg, source, Language::Ocaml)
.expect("should compute for OCaml");
assert!(
!avail.expressions.is_empty(),
"OCaml: should find binary expression a + b"
);
assert_has_expression_with_operands(&avail, "a", "b");
}
#[test]
fn test_commutative_canonicalization_python() {
let source = "x = a + b\ny = b + a\n";
let cfg = single_block_cfg(2);
let avail =
compute_available_expressions(&cfg, source, Language::Python).expect("should compute");
assert_eq!(
avail.expressions.len(),
1,
"Commutative canonicalization: a + b and b + a should merge. Got: {:?}",
avail
.expressions
.iter()
.map(|e| &e.text)
.collect::<Vec<_>>()
);
}
#[test]
fn test_non_commutative_subtraction_python() {
let source = "x = a - b\ny = b - a\n";
let cfg = single_block_cfg(2);
let avail =
compute_available_expressions(&cfg, source, Language::Python).expect("should compute");
assert_eq!(
avail.expressions.len(),
2,
"Non-commutative: a - b and b - a should be distinct. Got: {:?}",
avail
.expressions
.iter()
.map(|e| &e.text)
.collect::<Vec<_>>()
);
}
#[test]
fn test_kill_semantics_typescript() {
let source = "let x = a + b;\na = 5;\nlet z = a + b;\n";
let cfg = single_block_cfg(3);
let avail = compute_available_expressions(&cfg, source, Language::TypeScript)
.expect("should compute for TypeScript kill test");
assert!(
!avail.expressions.is_empty(),
"TypeScript: should extract expressions even with redefinitions"
);
}
}
#[cfg(test)]
mod integration_tests {
use super::*;
#[test]
fn test_full_ssa_pipeline() {
let source = r#"
def process(x):
if x > 0:
y = 1
else:
y = 2
return y
"#;
let ssa = construct_ssa(
source,
"process",
crate::types::Language::Python,
SsaType::Minimal,
)
.expect("should construct SSA");
assert_eq!(ssa.function, "process");
assert!(!ssa.blocks.is_empty());
let has_y_phi = ssa
.blocks
.iter()
.any(|b| b.phi_functions.iter().any(|phi| phi.variable == "y"));
assert!(has_y_phi, "Should have phi for y");
}
#[test]
fn test_ssa_with_loop() {
let source = r#"
def sum_to_n(n):
total = 0
i = 0
while i < n:
total = total + i
i = i + 1
return total
"#;
let ssa = construct_ssa(
source,
"sum_to_n",
crate::types::Language::Python,
SsaType::Minimal,
)
.expect("should construct SSA");
let loop_header = ssa.blocks.iter().find(|b| b.phi_functions.len() >= 2);
assert!(
loop_header.is_some(),
"Should have loop header with phis for loop variables"
);
}
#[test]
fn test_filter_by_variable() {
let ssa = fixtures::sample_ssa();
let filtered = filter_ssa_by_variable(ssa, "y");
for name in &filtered.ssa_names {
assert_eq!(name.variable, "y", "Filtered SSA should only contain y");
}
for block in &filtered.blocks {
for phi in &block.phi_functions {
assert_eq!(phi.variable, "y");
}
}
}
}
#[cfg(test)]
mod language_specific_tests {
use super::*;
use crate::ssa::construct::{
is_blank_identifier, is_comprehension_scope,
};
use crate::types::VarRefContext;
#[test]
fn test_python_augmented_assignment() {
let cfg = fixtures::linear_cfg();
let refs = vec![
VarRef {
name: "x".to_string(),
ref_type: RefType::Definition,
line: 1,
column: 0,
context: None,
group_id: None,
},
VarRef {
name: "x".to_string(),
ref_type: RefType::Update,
line: 2,
column: 0,
context: Some(VarRefContext::AugmentedAssignment),
group_id: None,
},
];
let dfg = DfgInfo {
function: "test".to_string(),
refs,
edges: Vec::new(),
variables: vec!["x".to_string()],
};
let ssa = construct_minimal_ssa(&cfg, &dfg).expect("should construct SSA");
let x_versions: Vec<_> = ssa.ssa_names.iter().filter(|n| n.variable == "x").collect();
assert!(
x_versions.len() >= 2,
"Should have at least 2 versions of x: original and after +=. Got {:?}",
x_versions
);
let augmented_instr = ssa
.blocks
.iter()
.flat_map(|b| &b.instructions)
.find(|i| i.line == 2);
if let Some(instr) = augmented_instr {
assert!(
instr.target.is_some(),
"Augmented assignment should define a target"
);
}
}
#[test]
fn test_python_multiple_assignment() {
let cfg = fixtures::linear_cfg();
let refs = vec![
VarRef {
name: "a".to_string(),
ref_type: RefType::Definition,
line: 1,
column: 0,
context: None,
group_id: None,
},
VarRef {
name: "b".to_string(),
ref_type: RefType::Definition,
line: 1,
column: 5,
context: None,
group_id: None,
},
VarRef {
name: "b".to_string(),
ref_type: RefType::Use,
line: 2,
column: 9,
context: Some(VarRefContext::MultipleAssignment),
group_id: Some(1),
},
VarRef {
name: "a".to_string(),
ref_type: RefType::Use,
line: 2,
column: 12,
context: Some(VarRefContext::MultipleAssignment),
group_id: Some(1),
},
VarRef {
name: "a".to_string(),
ref_type: RefType::Definition,
line: 2,
column: 0,
context: Some(VarRefContext::MultipleAssignment),
group_id: Some(1),
},
VarRef {
name: "b".to_string(),
ref_type: RefType::Definition,
line: 2,
column: 3,
context: Some(VarRefContext::MultipleAssignment),
group_id: Some(1),
},
];
let dfg = DfgInfo {
function: "swap".to_string(),
refs,
edges: Vec::new(),
variables: vec!["a".to_string(), "b".to_string()],
};
let ssa = construct_minimal_ssa(&cfg, &dfg).expect("should construct SSA");
let a_versions: Vec<_> = ssa.ssa_names.iter().filter(|n| n.variable == "a").collect();
let b_versions: Vec<_> = ssa.ssa_names.iter().filter(|n| n.variable == "b").collect();
assert!(
a_versions.len() >= 2,
"Should have at least 2 versions of a"
);
assert!(
b_versions.len() >= 2,
"Should have at least 2 versions of b"
);
}
#[test]
fn test_python_walrus_operator() {
let cfg = fixtures::linear_cfg();
let refs = vec![
VarRef {
name: "n".to_string(),
ref_type: RefType::Definition,
line: 1,
column: 4,
context: Some(VarRefContext::WalrusOperator),
group_id: None,
},
VarRef {
name: "n".to_string(),
ref_type: RefType::Use,
line: 2,
column: 11,
context: None,
group_id: None,
},
];
let dfg = DfgInfo {
function: "test".to_string(),
refs,
edges: Vec::new(),
variables: vec!["n".to_string()],
};
let ssa = construct_minimal_ssa(&cfg, &dfg).expect("should construct SSA");
let n_defs: Vec<_> = ssa
.ssa_names
.iter()
.filter(|name| name.variable == "n")
.collect();
assert!(!n_defs.is_empty(), "n should be defined by walrus operator");
assert_eq!(
n_defs[0].def_line, 1,
"n should be defined at the walrus operator line"
);
}
#[test]
fn test_python_comprehension_scope() {
let outer_ref = VarRef {
name: "x".to_string(),
ref_type: RefType::Definition,
line: 1,
column: 0,
context: None,
group_id: None,
};
let comp_ref = VarRef {
name: "x".to_string(),
ref_type: RefType::Definition,
line: 2,
column: 5,
context: Some(VarRefContext::ComprehensionScope),
group_id: None,
};
assert!(
!is_comprehension_scope(&outer_ref),
"Outer x is not in comprehension scope"
);
assert!(
is_comprehension_scope(&comp_ref),
"Comprehension x should be marked"
);
}
#[test]
fn test_python_match_bindings() {
let cfg = fixtures::diamond_cfg();
let refs = vec![
VarRef {
name: "point".to_string(),
ref_type: RefType::Use,
line: 1,
column: 6,
context: None,
group_id: None,
},
VarRef {
name: "x".to_string(),
ref_type: RefType::Definition,
line: 2,
column: 10,
context: Some(VarRefContext::MatchBinding),
group_id: None,
},
VarRef {
name: "y".to_string(),
ref_type: RefType::Definition,
line: 2,
column: 13,
context: Some(VarRefContext::MatchBinding),
group_id: None,
},
];
let dfg = DfgInfo {
function: "match_test".to_string(),
refs,
edges: Vec::new(),
variables: vec!["point".to_string(), "x".to_string(), "y".to_string()],
};
let ssa = construct_minimal_ssa(&cfg, &dfg).expect("should construct SSA");
assert!(
ssa.ssa_names.iter().any(|n| n.variable == "x"),
"x should be defined from match binding"
);
assert!(
ssa.ssa_names.iter().any(|n| n.variable == "y"),
"y should be defined from match binding"
);
}
#[test]
fn test_typescript_destructuring() {
let cfg = fixtures::linear_cfg();
let refs = vec![
VarRef {
name: "obj".to_string(),
ref_type: RefType::Use,
line: 1,
column: 18,
context: Some(VarRefContext::Destructuring),
group_id: Some(1),
},
VarRef {
name: "a".to_string(),
ref_type: RefType::Definition,
line: 1,
column: 7,
context: Some(VarRefContext::Destructuring),
group_id: Some(1),
},
VarRef {
name: "c".to_string(), ref_type: RefType::Definition,
line: 1,
column: 10,
context: Some(VarRefContext::Destructuring),
group_id: Some(1),
},
];
let dfg = DfgInfo {
function: "destruct".to_string(),
refs,
edges: Vec::new(),
variables: vec!["obj".to_string(), "a".to_string(), "c".to_string()],
};
let ssa = construct_minimal_ssa(&cfg, &dfg).expect("should construct SSA");
assert!(
ssa.ssa_names.iter().any(|n| n.variable == "a"),
"a should be defined from destructuring"
);
assert!(
ssa.ssa_names.iter().any(|n| n.variable == "c"),
"c should be defined from destructuring"
);
assert!(
!ssa.ssa_names.iter().any(|n| n.variable == "b"),
"b should NOT be defined (c is the binding)"
);
}
#[test]
fn test_typescript_type_guard_no_version() {
let cfg = fixtures::diamond_cfg();
let refs = vec![
VarRef {
name: "x".to_string(),
ref_type: RefType::Definition,
line: 1,
column: 10,
context: None,
group_id: None,
},
VarRef {
name: "x".to_string(),
ref_type: RefType::Use,
line: 2,
column: 8,
context: None, group_id: None,
},
VarRef {
name: "x".to_string(),
ref_type: RefType::Use,
line: 3,
column: 15,
context: None,
group_id: None,
},
];
let dfg = DfgInfo {
function: "guard_test".to_string(),
refs,
edges: Vec::new(),
variables: vec!["x".to_string()],
};
let ssa = construct_minimal_ssa(&cfg, &dfg).expect("should construct SSA");
let x_defs: Vec<_> = ssa.ssa_names.iter().filter(|n| n.variable == "x").collect();
assert_eq!(
x_defs.len(),
1,
"x should have only ONE SSA version (type guards don't create new versions)"
);
}
#[test]
fn test_typescript_for_of_destructuring() {
let cfg = fixtures::loop_cfg();
let refs = vec![
VarRef {
name: "k".to_string(),
ref_type: RefType::Definition,
line: 1,
column: 13,
context: Some(VarRefContext::Destructuring),
group_id: Some(1),
},
VarRef {
name: "v".to_string(),
ref_type: RefType::Definition,
line: 1,
column: 16,
context: Some(VarRefContext::Destructuring),
group_id: Some(1),
},
VarRef {
name: "k".to_string(),
ref_type: RefType::Use,
line: 2,
column: 12,
context: None,
group_id: None,
},
VarRef {
name: "v".to_string(),
ref_type: RefType::Use,
line: 2,
column: 15,
context: None,
group_id: None,
},
];
let dfg = DfgInfo {
function: "forof_test".to_string(),
refs,
edges: Vec::new(),
variables: vec!["k".to_string(), "v".to_string()],
};
let ssa = construct_minimal_ssa(&cfg, &dfg).expect("should construct SSA");
assert!(
ssa.ssa_names.iter().any(|n| n.variable == "k"),
"k should be defined from destructuring"
);
assert!(
ssa.ssa_names.iter().any(|n| n.variable == "v"),
"v should be defined from destructuring"
);
}
#[test]
fn test_go_short_declaration() {
let cfg = fixtures::linear_cfg();
let refs = vec![
VarRef {
name: "x".to_string(),
ref_type: RefType::Definition,
line: 1,
column: 0,
context: Some(VarRefContext::ShortDeclaration),
group_id: None,
},
VarRef {
name: "x".to_string(),
ref_type: RefType::Definition,
line: 2,
column: 0,
context: Some(VarRefContext::ShortDeclaration),
group_id: Some(1),
},
VarRef {
name: "y".to_string(),
ref_type: RefType::Definition,
line: 2,
column: 3,
context: Some(VarRefContext::ShortDeclaration),
group_id: Some(1),
},
];
let dfg = DfgInfo {
function: "short_decl".to_string(),
refs,
edges: Vec::new(),
variables: vec!["x".to_string(), "y".to_string()],
};
let ssa = construct_minimal_ssa(&cfg, &dfg).expect("should construct SSA");
let x_versions: Vec<_> = ssa.ssa_names.iter().filter(|n| n.variable == "x").collect();
assert!(x_versions.len() >= 2, "x should have at least 2 versions");
let y_versions: Vec<_> = ssa.ssa_names.iter().filter(|n| n.variable == "y").collect();
assert_eq!(y_versions.len(), 1, "y should have exactly 1 version");
}
#[test]
fn test_go_multiple_returns() {
let cfg = fixtures::linear_cfg();
let refs = vec![
VarRef {
name: "val".to_string(),
ref_type: RefType::Definition,
line: 1,
column: 0,
context: Some(VarRefContext::MultipleReturn),
group_id: Some(1),
},
VarRef {
name: "err".to_string(),
ref_type: RefType::Definition,
line: 1,
column: 5,
context: Some(VarRefContext::MultipleReturn),
group_id: Some(1),
},
];
let dfg = DfgInfo {
function: "multi_return".to_string(),
refs,
edges: Vec::new(),
variables: vec!["val".to_string(), "err".to_string()],
};
let ssa = construct_minimal_ssa(&cfg, &dfg).expect("should construct SSA");
assert!(
ssa.ssa_names.iter().any(|n| n.variable == "val"),
"val should be defined"
);
assert!(
ssa.ssa_names.iter().any(|n| n.variable == "err"),
"err should be defined"
);
}
#[test]
fn test_go_blank_identifier() {
assert!(is_blank_identifier("_"), "_ should be blank identifier");
assert!(!is_blank_identifier("x"), "x should not be blank");
assert!(!is_blank_identifier("_x"), "_x should not be blank");
}
#[test]
fn test_rust_shadowing() {
let cfg = fixtures::linear_cfg();
let refs = vec![
VarRef {
name: "x".to_string(),
ref_type: RefType::Definition,
line: 1,
column: 4,
context: Some(VarRefContext::Shadowing),
group_id: None,
},
VarRef {
name: "x".to_string(),
ref_type: RefType::Use,
line: 2,
column: 12,
context: None,
group_id: None,
},
VarRef {
name: "x".to_string(),
ref_type: RefType::Definition,
line: 2,
column: 4,
context: Some(VarRefContext::Shadowing),
group_id: None,
},
];
let dfg = DfgInfo {
function: "shadow_test".to_string(),
refs,
edges: Vec::new(),
variables: vec!["x".to_string()],
};
let ssa = construct_minimal_ssa(&cfg, &dfg).expect("should construct SSA");
let x_names: Vec<_> = ssa
.ssa_names
.iter()
.filter(|n| n.variable.starts_with("x"))
.collect();
assert!(
x_names.len() >= 2,
"Should have at least 2 SSA names for x (shadow creates new variable)"
);
}
#[test]
fn test_rust_pattern_binding() {
let cfg = fixtures::linear_cfg();
let refs = vec![
VarRef {
name: "a".to_string(),
ref_type: RefType::Definition,
line: 1,
column: 5,
context: Some(VarRefContext::PatternBinding),
group_id: Some(1),
},
VarRef {
name: "b".to_string(),
ref_type: RefType::Definition,
line: 1,
column: 8,
context: Some(VarRefContext::PatternBinding),
group_id: Some(1),
},
];
let dfg = DfgInfo {
function: "pattern_test".to_string(),
refs,
edges: Vec::new(),
variables: vec!["a".to_string(), "b".to_string()],
};
let ssa = construct_minimal_ssa(&cfg, &dfg).expect("should construct SSA");
assert!(
ssa.ssa_names.iter().any(|n| n.variable == "a"),
"a should be defined from pattern"
);
assert!(
ssa.ssa_names.iter().any(|n| n.variable == "b"),
"b should be defined from pattern"
);
}
#[test]
fn test_rust_match_arm_scope() {
let cfg = fixtures::diamond_cfg();
let refs = vec![
VarRef {
name: "r".to_string(),
ref_type: RefType::Use,
line: 1,
column: 6,
context: None,
group_id: None,
},
VarRef {
name: "n".to_string(),
ref_type: RefType::Definition,
line: 2,
column: 7,
context: Some(VarRefContext::MatchArmBinding),
group_id: None,
},
VarRef {
name: "n".to_string(),
ref_type: RefType::Use,
line: 2,
column: 13,
context: None,
group_id: None,
},
VarRef {
name: "e".to_string(),
ref_type: RefType::Definition,
line: 3,
column: 8,
context: Some(VarRefContext::MatchArmBinding),
group_id: None,
},
];
let dfg = DfgInfo {
function: "match_arm_test".to_string(),
refs,
edges: Vec::new(),
variables: vec!["r".to_string(), "n".to_string(), "e".to_string()],
};
let ssa = construct_minimal_ssa(&cfg, &dfg).expect("should construct SSA");
assert!(
ssa.ssa_names.iter().any(|n| n.variable == "n"),
"n should be defined in Ok arm"
);
assert!(
ssa.ssa_names.iter().any(|n| n.variable == "e"),
"e should be defined in Err arm"
);
}
}
#[cfg(test)]
mod source_text_and_uses_tests {
use super::*;
#[test]
fn test_source_text_populated_via_construct_ssa() {
let source = r#"
def alias_test():
x = [1, 2, 3]
y = x
z = [4, 5, 6]
"#;
let ssa = construct_ssa(
source,
"alias_test",
crate::types::Language::Python,
SsaType::Minimal,
)
.expect("should construct SSA");
let assign_instructions: Vec<&SsaInstruction> = ssa
.blocks
.iter()
.flat_map(|b| &b.instructions)
.filter(|i| i.kind == SsaInstructionKind::Assign)
.collect();
assert!(
!assign_instructions.is_empty(),
"Should have at least one assign instruction"
);
for inst in &assign_instructions {
assert!(
inst.source_text.is_some(),
"Assign instruction at line {} should have source_text populated, got None",
inst.line
);
}
}
#[test]
fn test_source_text_contains_correct_content() {
let source = r#"
def simple():
x = [1, 2, 3]
y = x
"#;
let ssa = construct_ssa(
source,
"simple",
crate::types::Language::Python,
SsaType::Minimal,
)
.expect("should construct SSA");
let x_instr = ssa.blocks.iter().flat_map(|b| &b.instructions).find(|i| {
i.kind == SsaInstructionKind::Assign
&& i.source_text
.as_ref()
.is_some_and(|s| s.contains("[1, 2, 3]"))
});
assert!(
x_instr.is_some(),
"Should find instruction with source_text containing '[1, 2, 3]'"
);
}
#[test]
fn test_uses_populated_for_copy_assignment() {
let source = r#"
def copy_test():
x = [1, 2, 3]
y = x
"#;
let ssa = construct_ssa(
source,
"copy_test",
crate::types::Language::Python,
SsaType::Minimal,
)
.expect("should construct SSA");
let x_name = ssa
.ssa_names
.iter()
.find(|n| n.variable == "x")
.expect("should have SSA name for x");
let y_instr = ssa.blocks.iter().flat_map(|b| &b.instructions).find(|i| {
i.kind == SsaInstructionKind::Assign
&& i.target
.is_some_and(|t| ssa.ssa_names.iter().any(|n| n.id == t && n.variable == "y"))
});
let y_instr = y_instr.expect("should find y's assignment instruction");
assert!(
y_instr.uses.contains(&x_name.id),
"y = x instruction should have x's SSA name ({:?}) in uses, but uses = {:?}",
x_name.id,
y_instr.uses
);
}
#[test]
fn test_uses_populated_for_chained_copies() {
let source = r#"
def chain_test():
x = [1, 2, 3]
y = x
w = y
"#;
let ssa = construct_ssa(
source,
"chain_test",
crate::types::Language::Python,
SsaType::Minimal,
)
.expect("should construct SSA");
let y_name = ssa
.ssa_names
.iter()
.find(|n| n.variable == "y")
.expect("should have SSA name for y");
let w_instr = ssa
.blocks
.iter()
.flat_map(|b| &b.instructions)
.find(|i| {
i.kind == SsaInstructionKind::Assign
&& i.target.is_some_and(|t| {
ssa.ssa_names.iter().any(|n| n.id == t && n.variable == "w")
})
})
.expect("should find w's assignment instruction");
assert!(
w_instr.uses.contains(&y_name.id),
"w = y instruction should have y's SSA name ({:?}) in uses, but uses = {:?}",
y_name.id,
w_instr.uses
);
}
#[test]
fn test_construct_minimal_ssa_backward_compatible() {
let cfg = fixtures::diamond_cfg();
let dfg = fixtures::diamond_dfg();
let ssa = construct_minimal_ssa(&cfg, &dfg).expect("should construct SSA");
let total_instructions: usize = ssa.blocks.iter().map(|b| b.instructions.len()).sum();
assert!(
total_instructions > 0,
"Should have at least one instruction"
);
}
#[test]
fn test_construct_ssa_with_statements_populates_both() {
let source = r#"
def both_test():
x = [1, 2, 3]
y = x
z = [4, 5, 6]
w = y
"#;
let ssa = construct_ssa(
source,
"both_test",
crate::types::Language::Python,
SsaType::Minimal,
)
.expect("should construct SSA");
let assign_instructions: Vec<&SsaInstruction> = ssa
.blocks
.iter()
.flat_map(|b| &b.instructions)
.filter(|i| i.kind == SsaInstructionKind::Assign)
.collect();
let with_source_text = assign_instructions
.iter()
.filter(|i| i.source_text.is_some())
.count();
assert!(
with_source_text > 0,
"At least some instructions should have source_text"
);
let with_uses = assign_instructions
.iter()
.filter(|i| !i.uses.is_empty())
.count();
assert!(
with_uses >= 2,
"At least 2 copy assignments (y=x, w=y) should have uses populated, got {}",
with_uses
);
}
}