use std::collections::hash_map::DefaultHasher;
use std::collections::{HashMap, HashSet};
use std::f64::consts::PI;
use std::hash::{Hash, Hasher};
#[derive(Debug, Clone)]
pub struct MockExpression {
pub text: String,
pub operands: Vec<String>,
pub line: u32,
}
impl PartialEq for MockExpression {
fn eq(&self, other: &Self) -> bool {
self.text == other.text
}
}
impl Eq for MockExpression {}
impl Hash for MockExpression {
fn hash<H: Hasher>(&self, state: &mut H) {
self.text.hash(state);
}
}
impl MockExpression {
pub fn new(text: &str, operands: Vec<&str>, line: u32) -> Self {
let mut ops: Vec<String> = operands.iter().map(|s| s.to_string()).collect();
ops.sort();
Self {
text: text.to_string(),
operands: ops,
line,
}
}
pub fn is_killed_by(&self, var: &str) -> bool {
self.operands.iter().any(|op| op == var)
}
pub fn to_json_value(&self) -> serde_json::Value {
serde_json::json!({
"text": self.text,
"operands": self.operands,
"line": self.line,
})
}
}
pub const COMMUTATIVE_OPS: &[&str] = &["+", "*", "==", "!=", "and", "or", "&", "|", "^"];
pub fn normalize_expression(op: &str, left: &str, right: &str) -> String {
if COMMUTATIVE_OPS.contains(&op) {
let mut operands = [left.trim(), right.trim()];
operands.sort();
format!("{} {} {}", operands[0], op, operands[1])
} else {
format!("{} {} {}", left.trim(), op, right.trim())
}
}
#[derive(Debug, Clone, Default)]
pub struct MockAvailableExprsInfo {
pub avail_in: HashMap<u32, HashSet<MockExpression>>,
pub avail_out: HashMap<u32, HashSet<MockExpression>>,
pub all_exprs: HashSet<MockExpression>,
pub entry_block: u32,
pub expr_instances: Vec<MockExpression>,
}
impl MockAvailableExprsInfo {
pub fn new(entry_block: u32) -> Self {
Self {
entry_block,
..Default::default()
}
}
pub fn is_available(&self, block: u32, expr: &MockExpression) -> bool {
self.avail_in
.get(&block)
.is_some_and(|set| set.contains(expr))
}
pub fn is_available_at_exit(&self, block: u32, expr: &MockExpression) -> bool {
self.avail_out
.get(&block)
.is_some_and(|set| set.contains(expr))
}
pub fn redundant_computations(&self) -> Vec<(String, u32, u32)> {
let mut redundant = Vec::new();
let mut seen: HashMap<String, u32> = HashMap::new();
for expr in &self.expr_instances {
if let Some(&first_line) = seen.get(&expr.text) {
redundant.push((expr.text.clone(), first_line, expr.line));
} else {
seen.insert(expr.text.clone(), expr.line);
}
}
redundant.sort_by_key(|(_, _, line)| *line);
redundant
}
pub fn to_json_value(&self) -> serde_json::Value {
let avail_in: HashMap<String, Vec<serde_json::Value>> = self
.avail_in
.iter()
.map(|(k, v)| {
let exprs: Vec<_> = v.iter().map(|e| e.to_json_value()).collect();
(k.to_string(), exprs)
})
.collect();
let avail_out: HashMap<String, Vec<serde_json::Value>> = self
.avail_out
.iter()
.map(|(k, v)| {
let exprs: Vec<_> = v.iter().map(|e| e.to_json_value()).collect();
(k.to_string(), exprs)
})
.collect();
let all_exprs: Vec<_> = self.all_exprs.iter().map(|e| e.to_json_value()).collect();
let redundant: Vec<_> = self
.redundant_computations()
.iter()
.map(|(expr, first, redundant)| {
serde_json::json!({
"expr": expr,
"first_at": first,
"redundant_at": redundant,
})
})
.collect();
serde_json::json!({
"avail_in": avail_in,
"avail_out": avail_out,
"all_expressions": all_exprs,
"entry_block": self.entry_block,
"redundant_computations": redundant,
})
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub enum MockNullability {
Never,
#[default]
Maybe,
Always,
}
impl MockNullability {
pub fn as_str(&self) -> &'static str {
match self {
MockNullability::Never => "never",
MockNullability::Maybe => "maybe",
MockNullability::Always => "always",
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum MockConstantValue {
Int(i64),
Float(f64),
String(String),
Bool(bool),
Null,
}
impl MockConstantValue {
pub fn to_json_value(&self) -> serde_json::Value {
match self {
MockConstantValue::Int(v) => serde_json::json!(v),
MockConstantValue::Float(v) => serde_json::json!(v),
MockConstantValue::String(v) => serde_json::json!(v),
MockConstantValue::Bool(v) => serde_json::json!(v),
MockConstantValue::Null => serde_json::Value::Null,
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct MockAbstractValue {
pub type_: Option<String>,
pub range_: Option<(Option<i64>, Option<i64>)>,
pub nullable: MockNullability,
pub constant: Option<MockConstantValue>,
}
impl Eq for MockAbstractValue {}
impl Hash for MockAbstractValue {
fn hash<H: Hasher>(&self, state: &mut H) {
self.type_.hash(state);
self.range_.hash(state);
self.nullable.hash(state);
}
}
impl MockAbstractValue {
pub fn top() -> Self {
MockAbstractValue {
type_: None,
range_: None,
nullable: MockNullability::Maybe,
constant: None,
}
}
pub fn bottom() -> Self {
MockAbstractValue {
type_: Some("<bottom>".to_string()),
range_: Some((None, None)),
nullable: MockNullability::Never,
constant: None,
}
}
pub fn from_constant(value: MockConstantValue) -> Self {
match value {
MockConstantValue::Int(v) => MockAbstractValue {
type_: Some("int".to_string()),
range_: Some((Some(v), Some(v))),
nullable: MockNullability::Never,
constant: Some(MockConstantValue::Int(v)),
},
MockConstantValue::Float(v) => MockAbstractValue {
type_: Some("float".to_string()),
range_: None,
nullable: MockNullability::Never,
constant: Some(MockConstantValue::Float(v)),
},
MockConstantValue::String(ref s) => MockAbstractValue {
type_: Some("str".to_string()),
range_: Some((Some(s.len() as i64), Some(s.len() as i64))),
nullable: MockNullability::Never,
constant: Some(value),
},
MockConstantValue::Bool(v) => MockAbstractValue {
type_: Some("bool".to_string()),
range_: Some((Some(v as i64), Some(v as i64))),
nullable: MockNullability::Never,
constant: Some(MockConstantValue::Bool(v)),
},
MockConstantValue::Null => MockAbstractValue {
type_: Some("NoneType".to_string()),
range_: None,
nullable: MockNullability::Always,
constant: None,
},
}
}
pub fn may_be_zero(&self) -> bool {
match &self.range_ {
None => true, Some((low, high)) => {
let low = low.unwrap_or(i64::MIN);
let high = high.unwrap_or(i64::MAX);
low <= 0 && 0 <= high
}
}
}
pub fn may_be_null(&self) -> bool {
self.nullable != MockNullability::Never
}
pub fn is_constant(&self) -> bool {
self.constant.is_some()
}
pub fn to_json_value(&self) -> serde_json::Value {
let mut obj = serde_json::Map::new();
if let Some(ref t) = self.type_ {
obj.insert("type".to_string(), serde_json::json!(t));
}
if let Some((low, high)) = &self.range_ {
let range = serde_json::json!([low, high]);
obj.insert("range".to_string(), range);
}
obj.insert(
"nullable".to_string(),
serde_json::json!(self.nullable.as_str()),
);
if let Some(ref c) = self.constant {
obj.insert("constant".to_string(), c.to_json_value());
}
serde_json::Value::Object(obj)
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct MockAbstractState {
pub values: HashMap<String, MockAbstractValue>,
}
impl MockAbstractState {
pub fn new() -> Self {
Self::default()
}
pub fn get(&self, var: &str) -> MockAbstractValue {
self.values
.get(var)
.cloned()
.unwrap_or_else(MockAbstractValue::top)
}
pub fn set(&self, var: &str, value: MockAbstractValue) -> Self {
let mut new_values = self.values.clone();
new_values.insert(var.to_string(), value);
MockAbstractState { values: new_values }
}
pub fn copy(&self) -> Self {
self.clone()
}
}
#[derive(Debug, Clone, Default)]
pub struct MockAbstractInterpInfo {
pub state_in: HashMap<u32, MockAbstractState>,
pub state_out: HashMap<u32, MockAbstractState>,
pub potential_div_zero: Vec<(u32, String)>,
pub potential_null_deref: Vec<(u32, String)>,
pub function_name: String,
}
impl MockAbstractInterpInfo {
pub fn new(function_name: &str) -> Self {
Self {
function_name: function_name.to_string(),
..Default::default()
}
}
pub fn value_at(&self, block: u32, var: &str) -> MockAbstractValue {
self.state_in
.get(&block)
.map(|s| s.get(var))
.unwrap_or_else(MockAbstractValue::top)
}
pub fn value_at_exit(&self, block: u32, var: &str) -> MockAbstractValue {
self.state_out
.get(&block)
.map(|s| s.get(var))
.unwrap_or_else(MockAbstractValue::top)
}
pub fn range_at(&self, block: u32, var: &str) -> Option<(Option<i64>, Option<i64>)> {
self.value_at(block, var).range_
}
pub fn type_at(&self, block: u32, var: &str) -> Option<String> {
self.value_at(block, var).type_
}
pub fn is_definitely_not_null(&self, block: u32, var: &str) -> bool {
self.value_at(block, var).nullable == MockNullability::Never
}
pub fn get_constants(&self) -> HashMap<String, MockConstantValue> {
let mut constants = HashMap::new();
for state in self.state_out.values() {
for (var, val) in &state.values {
if let Some(c) = &val.constant {
constants.insert(var.clone(), c.clone());
}
}
}
constants
}
pub fn to_json_value(&self) -> serde_json::Value {
let state_in: HashMap<String, serde_json::Value> = self
.state_in
.iter()
.map(|(k, state)| {
let vars: HashMap<String, serde_json::Value> = state
.values
.iter()
.map(|(var, val)| (var.clone(), val.to_json_value()))
.collect();
(k.to_string(), serde_json::json!(vars))
})
.collect();
let state_out: HashMap<String, serde_json::Value> = self
.state_out
.iter()
.map(|(k, state)| {
let vars: HashMap<String, serde_json::Value> = state
.values
.iter()
.map(|(var, val)| (var.clone(), val.to_json_value()))
.collect();
(k.to_string(), serde_json::json!(vars))
})
.collect();
let div_zero: Vec<_> = self
.potential_div_zero
.iter()
.map(|(line, var)| serde_json::json!({"line": line, "var": var}))
.collect();
let null_deref: Vec<_> = self
.potential_null_deref
.iter()
.map(|(line, var)| serde_json::json!({"line": line, "var": var}))
.collect();
serde_json::json!({
"function": self.function_name,
"state_in": state_in,
"state_out": state_out,
"potential_div_zero": div_zero,
"potential_null_deref": null_deref,
})
}
}
pub fn join_values(values: &[MockAbstractValue]) -> MockAbstractValue {
if values.is_empty() {
return MockAbstractValue::top();
}
if values.len() == 1 {
return values[0].clone();
}
let ranges: Vec<_> = values.iter().filter_map(|v| v.range_).collect();
let joined_range = if ranges.is_empty() {
None
} else {
let lows: Vec<_> = ranges.iter().filter_map(|r| r.0).collect();
let highs: Vec<_> = ranges.iter().filter_map(|r| r.1).collect();
Some((lows.iter().min().copied(), highs.iter().max().copied()))
};
let types: Vec<_> = values.iter().filter_map(|v| v.type_.clone()).collect();
let joined_type = if !types.is_empty() && types.windows(2).all(|w| w[0] == w[1]) {
Some(types[0].clone())
} else {
None
};
let nulls: Vec<_> = values.iter().map(|v| v.nullable).collect();
let joined_null = if nulls.contains(&MockNullability::Maybe) {
MockNullability::Maybe
} else if nulls.windows(2).all(|w| w[0] == w[1]) {
nulls[0]
} else {
MockNullability::Maybe
};
let constants: Vec<_> = values.iter().filter_map(|v| v.constant.clone()).collect();
let joined_const =
if constants.len() == values.len() && constants.windows(2).all(|w| w[0] == w[1]) {
constants.into_iter().next()
} else {
None
};
MockAbstractValue {
type_: joined_type,
range_: joined_range,
nullable: joined_null,
constant: joined_const,
}
}
pub fn widen_value(old: &MockAbstractValue, new: &MockAbstractValue) -> MockAbstractValue {
let widened_range = match (&old.range_, &new.range_) {
(None, None) => None,
(None, r) => *r,
(_, None) => None,
(Some((old_low, old_high)), Some((new_low, new_high))) => {
let widened_low = match (old_low, new_low) {
(None, _) => None,
(_, None) => None,
(Some(o), Some(n)) if *n < *o => None,
(_, n) => *n,
};
let widened_high = match (old_high, new_high) {
(None, _) => None,
(_, None) => None,
(Some(o), Some(n)) if *n > *o => None,
(_, n) => *n,
};
Some((widened_low, widened_high))
}
};
MockAbstractValue {
type_: new.type_.clone(),
range_: widened_range,
nullable: new.nullable,
constant: None, }
}
pub fn apply_arithmetic(operand: &MockAbstractValue, op: char, constant: i64) -> MockAbstractValue {
let new_range = operand.range_.map(|(low, high)| match op {
'+' => (low.map(|l| l + constant), high.map(|h| h + constant)),
'-' => (low.map(|l| l - constant), high.map(|h| h - constant)),
'*' => {
let vals = [low.map(|l| l * constant), high.map(|h| h * constant)];
(
vals.iter().filter_map(|&v| v).min(),
vals.iter().filter_map(|&v| v).max(),
)
}
_ => (None, None),
});
let new_constant = if operand.is_constant() {
if let Some((Some(l), Some(h))) = new_range {
if l == h {
Some(MockConstantValue::Int(l))
} else {
None
}
} else {
None
}
} else {
None
};
MockAbstractValue {
type_: operand.type_.clone(),
range_: new_range,
nullable: operand.nullable,
constant: new_constant,
}
}
pub fn get_null_keywords(language: &str) -> &'static [&'static str] {
match language {
"python" => &["None"],
"typescript" | "javascript" => &["null", "undefined"],
"go" => &["nil"],
"rust" => &[], "java" | "kotlin" | "csharp" => &["null"],
"swift" => &["nil"],
_ => &["null", "nil", "None"],
}
}
pub fn get_boolean_keywords(language: &str) -> HashMap<&'static str, bool> {
match language {
"python" => [("True", true), ("False", false)].into_iter().collect(),
"typescript" | "javascript" | "go" | "rust" => {
[("true", true), ("false", false)].into_iter().collect()
}
_ => [
("True", true),
("False", false),
("true", true),
("false", false),
]
.into_iter()
.collect(),
}
}
pub fn get_comment_pattern(language: &str) -> &'static str {
match language {
"python" => "#",
"typescript" | "javascript" | "go" | "rust" | "java" | "csharp" | "kotlin" | "swift" => {
"//"
}
_ => "#",
}
}
#[test]
fn test_expression_is_frozen_hashable() {
let expr1 = MockExpression::new("a + b", vec!["a", "b"], 1);
let expr2 = MockExpression::new("a + b", vec!["a", "b"], 5);
let mut set: HashSet<MockExpression> = HashSet::new();
set.insert(expr1.clone());
set.insert(expr2.clone());
assert_eq!(
set.len(),
1,
"Expressions with same text should hash to same value"
);
}
#[test]
fn test_expression_equality_based_on_text_only() {
let expr1 = MockExpression::new("a + b", vec!["a", "b"], 1);
let expr2 = MockExpression::new("a + b", vec!["a", "b"], 100);
assert_eq!(
expr1, expr2,
"Equality should be based on text only, not line"
);
}
#[test]
fn test_expression_hash_based_on_text_only() {
let expr1 = MockExpression::new("x * y", vec!["x", "y"], 1);
let expr2 = MockExpression::new("x * y", vec!["x", "y"], 999);
let mut hasher1 = DefaultHasher::new();
let mut hasher2 = DefaultHasher::new();
expr1.hash(&mut hasher1);
expr2.hash(&mut hasher2);
assert_eq!(
hasher1.finish(),
hasher2.finish(),
"Hash should be based on text only"
);
}
#[test]
fn test_expression_different_text_not_equal() {
let expr1 = MockExpression::new("a + b", vec!["a", "b"], 1);
let expr2 = MockExpression::new("a - b", vec!["a", "b"], 1);
assert_ne!(
expr1, expr2,
"Different text should mean different expressions"
);
}
#[test]
fn test_expression_is_killed_by_operand() {
let expr = MockExpression::new("a + b", vec!["a", "b"], 1);
assert!(
expr.is_killed_by("a"),
"Expression should be killed by redefining 'a'"
);
assert!(
expr.is_killed_by("b"),
"Expression should be killed by redefining 'b'"
);
}
#[test]
fn test_expression_not_killed_by_non_operand() {
let expr = MockExpression::new("a + b", vec!["a", "b"], 1);
assert!(
!expr.is_killed_by("c"),
"Expression should NOT be killed by unrelated variable"
);
assert!(
!expr.is_killed_by("x"),
"Expression should NOT be killed by unrelated variable"
);
}
#[test]
fn test_commutative_addition_normalized() {
let norm1 = normalize_expression("+", "a", "b");
let norm2 = normalize_expression("+", "b", "a");
assert_eq!(
norm1, norm2,
"Commutative addition should normalize identically"
);
}
#[test]
fn test_commutative_multiplication_normalized() {
let norm1 = normalize_expression("*", "x", "y");
let norm2 = normalize_expression("*", "y", "x");
assert_eq!(
norm1, norm2,
"Commutative multiplication should normalize identically"
);
}
#[test]
fn test_commutative_equality_normalized() {
let norm1 = normalize_expression("==", "foo", "bar");
let norm2 = normalize_expression("==", "bar", "foo");
assert_eq!(
norm1, norm2,
"Commutative equality should normalize identically"
);
}
#[test]
fn test_non_commutative_subtraction_preserves_order() {
let norm1 = normalize_expression("-", "a", "b");
let norm2 = normalize_expression("-", "b", "a");
assert_ne!(
norm1, norm2,
"Non-commutative subtraction should preserve order"
);
assert_eq!(norm1, "a - b");
assert_eq!(norm2, "b - a");
}
#[test]
fn test_non_commutative_division_preserves_order() {
let norm1 = normalize_expression("/", "x", "y");
let norm2 = normalize_expression("/", "y", "x");
assert_ne!(
norm1, norm2,
"Non-commutative division should preserve order"
);
}
#[test]
fn test_available_exprs_info_has_required_fields() {
let info = MockAvailableExprsInfo::new(0);
assert!(info.avail_in.is_empty());
assert!(info.avail_out.is_empty());
assert!(info.all_exprs.is_empty());
assert_eq!(info.entry_block, 0);
assert!(info.expr_instances.is_empty());
}
#[test]
fn test_is_available_returns_true_when_in_avail_in() {
let mut info = MockAvailableExprsInfo::new(0);
let expr = MockExpression::new("a + b", vec!["a", "b"], 1);
let mut block_exprs = HashSet::new();
block_exprs.insert(expr.clone());
info.avail_in.insert(1, block_exprs);
assert!(
info.is_available(1, &expr),
"is_available should return true when expr in avail_in"
);
}
#[test]
fn test_is_available_returns_false_when_not_in_avail_in() {
let info = MockAvailableExprsInfo::new(0);
let expr = MockExpression::new("a + b", vec!["a", "b"], 1);
assert!(
!info.is_available(1, &expr),
"is_available should return false when expr not in avail_in"
);
}
#[test]
fn test_is_available_returns_false_for_unknown_block() {
let info = MockAvailableExprsInfo::new(0);
let expr = MockExpression::new("a + b", vec!["a", "b"], 1);
assert!(
!info.is_available(999, &expr),
"is_available should return false for unknown block"
);
}
#[test]
fn test_is_available_at_exit_returns_true_when_in_avail_out() {
let mut info = MockAvailableExprsInfo::new(0);
let expr = MockExpression::new("x * y", vec!["x", "y"], 5);
let mut block_exprs = HashSet::new();
block_exprs.insert(expr.clone());
info.avail_out.insert(0, block_exprs);
assert!(
info.is_available_at_exit(0, &expr),
"is_available_at_exit should check avail_out"
);
}
#[test]
fn test_avail_exprs_to_json_serializable() {
let mut info = MockAvailableExprsInfo::new(0);
let expr = MockExpression::new("a + b", vec!["a", "b"], 2);
let mut block_exprs = HashSet::new();
block_exprs.insert(expr.clone());
info.avail_in.insert(0, HashSet::new());
info.avail_out.insert(0, block_exprs.clone());
info.all_exprs.insert(expr);
let json = info.to_json_value();
assert!(json.get("avail_in").is_some());
assert!(json.get("avail_out").is_some());
assert!(json.get("all_expressions").is_some());
assert!(json.get("entry_block").is_some());
assert!(json.get("redundant_computations").is_some());
let serialized = serde_json::to_string(&json);
assert!(serialized.is_ok(), "JSON should serialize without error");
}
use crate::dataflow::available::{
compute_available_exprs, AvailableExprsInfo, Expression,
};
use crate::types::{BlockType, CfgBlock, CfgEdge, CfgInfo, DfgInfo, EdgeType, RefType, VarRef};
fn make_test_cfg_for_phase3(
blocks: Vec<(usize, BlockType, (u32, u32))>,
edges: Vec<(usize, usize)>,
entry: usize,
) -> CfgInfo {
CfgInfo {
function: "test".to_string(),
blocks: blocks
.into_iter()
.map(|(id, block_type, lines)| CfgBlock {
id,
block_type,
lines,
calls: vec![],
})
.collect(),
edges: edges
.into_iter()
.map(|(from, to)| CfgEdge {
from,
to,
edge_type: EdgeType::Unconditional,
condition: None,
})
.collect(),
entry_block: entry,
exit_blocks: vec![],
cyclomatic_complexity: 1,
nested_functions: std::collections::HashMap::new(),
}
}
fn make_empty_dfg_for_phase3() -> DfgInfo {
DfgInfo {
function: "test".to_string(),
refs: vec![],
edges: vec![],
variables: vec![],
}
}
fn make_var_ref_for_phase3(name: &str, line: u32, ref_type: RefType) -> VarRef {
VarRef {
name: name.to_string(),
ref_type,
line,
column: 0,
context: None,
group_id: None,
}
}
fn make_dfg_with_refs_for_phase3(refs: Vec<VarRef>) -> DfgInfo {
let variables: Vec<String> = refs.iter().map(|r| r.name.clone()).collect();
DfgInfo {
function: "test".to_string(),
refs,
edges: vec![],
variables,
}
}
#[test]
fn test_must_analysis_diamond_single_branch_not_available() {
let cfg = make_test_cfg_for_phase3(
vec![
(0, BlockType::Entry, (1, 1)),
(1, BlockType::Body, (2, 2)),
(2, BlockType::Body, (3, 3)),
(3, BlockType::Exit, (4, 4)),
],
vec![(0, 1), (0, 2), (1, 3), (2, 3)],
0,
);
let dfg = make_dfg_with_refs_for_phase3(vec![
make_var_ref_for_phase3("x", 2, RefType::Definition),
make_var_ref_for_phase3("a", 2, RefType::Use),
make_var_ref_for_phase3("b", 2, RefType::Use),
]);
let result = compute_available_exprs(&cfg, &dfg).unwrap();
let avail_at_merge = result.avail_in.get(&3).unwrap();
assert!(
avail_at_merge.is_empty() || !result.all_exprs.is_empty(),
"MUST analysis: single-branch expression should not be available at merge"
);
}
#[test]
fn test_must_analysis_diamond_both_branches_is_available() {
let cfg = make_test_cfg_for_phase3(
vec![
(0, BlockType::Entry, (1, 1)),
(1, BlockType::Body, (2, 2)),
(2, BlockType::Body, (3, 3)),
(3, BlockType::Exit, (4, 4)),
],
vec![(0, 1), (0, 2), (1, 3), (2, 3)],
0,
);
let dfg = make_dfg_with_refs_for_phase3(vec![
make_var_ref_for_phase3("x", 2, RefType::Definition),
make_var_ref_for_phase3("a", 2, RefType::Use),
make_var_ref_for_phase3("b", 2, RefType::Use),
make_var_ref_for_phase3("y", 3, RefType::Definition),
make_var_ref_for_phase3("a", 3, RefType::Use),
make_var_ref_for_phase3("b", 3, RefType::Use),
]);
let result = compute_available_exprs(&cfg, &dfg).unwrap();
if !result.all_exprs.is_empty() {
let avail_out_1 = result.avail_out.get(&1).unwrap();
let avail_out_2 = result.avail_out.get(&2).unwrap();
assert!(
!avail_out_1.is_empty() || !avail_out_2.is_empty(),
"Both branches should have expressions at their exits"
);
}
}
#[test]
fn test_entry_block_has_nothing_available() {
let cfg = make_test_cfg_for_phase3(
vec![(0, BlockType::Entry, (1, 5)), (1, BlockType::Exit, (6, 10))],
vec![(0, 1)],
0,
);
let dfg = make_empty_dfg_for_phase3();
let result = compute_available_exprs(&cfg, &dfg).unwrap();
let entry = result.entry_block;
assert!(
result.avail_in.get(&entry).unwrap().is_empty(),
"Entry block should have nothing available at its entry"
);
}
#[test]
fn test_expression_killed_by_operand_redefinition() {
let expr = Expression::new("a + b", vec!["a", "b"], 1);
assert!(
expr.is_killed_by("a"),
"Expression should be killed by redefining 'a'"
);
assert!(
expr.is_killed_by("b"),
"Expression should be killed by redefining 'b'"
);
}
#[test]
fn test_expression_not_killed_by_unrelated_redefinition() {
let expr = Expression::new("a + b", vec!["a", "b"], 1);
assert!(
!expr.is_killed_by("c"),
"Expression should NOT be killed by unrelated variable 'c'"
);
assert!(
!expr.is_killed_by("x"),
"Expression should NOT be killed by unrelated variable 'x'"
);
}
#[test]
fn test_redundant_computations_detects_simple_cse() {
let mut info = AvailableExprsInfo::new(0);
info.expr_instances
.push(Expression::new("a + b", vec!["a", "b"], 2));
info.expr_instances
.push(Expression::new("a + b", vec!["a", "b"], 5));
let redundant = info.redundant_computations();
assert_eq!(redundant.len(), 1);
assert_eq!(redundant[0].0, "a + b");
assert_eq!(redundant[0].1, 2); assert_eq!(redundant[0].2, 5); }
#[test]
fn test_redundant_computations_no_false_positive_after_kill() {
let expr = Expression::new("a + b", vec!["a", "b"], 2);
assert!(expr.is_killed_by("a"));
}
#[test]
fn test_redundant_computations_returns_sorted_list() {
let mut info = AvailableExprsInfo::new(0);
info.expr_instances
.push(Expression::new("a + b", vec!["a", "b"], 2));
info.expr_instances
.push(Expression::new("x * y", vec!["x", "y"], 3));
info.expr_instances
.push(Expression::new("a + b", vec!["a", "b"], 10));
info.expr_instances
.push(Expression::new("x * y", vec!["x", "y"], 8));
let redundant = info.redundant_computations();
for window in redundant.windows(2) {
assert!(
window[0].2 <= window[1].2,
"redundant_computations should be sorted by line"
);
}
}
#[test]
fn test_to_dict_includes_redundant_computations_field() {
let info = MockAvailableExprsInfo::new(0);
let json = info.to_json_value();
assert!(json.get("redundant_computations").is_some());
assert!(json["redundant_computations"].is_array());
}
#[test]
fn test_phase4_intra_block_kill_prevents_false_positive() {
use crate::dataflow::{AvailableExprsInfo, ExprInstance, Expression};
let mut info = AvailableExprsInfo::new(0);
let expr1 = Expression::new("a + b", vec!["a", "b"], 2);
let expr2 = Expression::new("a + b", vec!["a", "b"], 5);
info.all_exprs.insert(expr1.clone());
info.avail_in
.insert(0, [expr1.clone()].into_iter().collect());
info.expr_instances_with_blocks
.push(ExprInstance::new(expr1.clone(), 0));
info.expr_instances_with_blocks
.push(ExprInstance::new(expr2.clone(), 0));
info.defs_per_line
.insert(3, ["a".to_string()].into_iter().collect());
let redundant = info.redundant_computations();
assert!(
redundant.is_empty(),
"Should not flag as redundant when operand is killed between computations. Got: {:?}",
redundant
);
}
#[test]
fn test_phase4_intra_block_same_expr_is_redundant() {
use crate::dataflow::{AvailableExprsInfo, ExprInstance, Expression};
let mut info = AvailableExprsInfo::new(0);
let expr1 = Expression::new("a + b", vec!["a", "b"], 2);
let expr2 = Expression::new("a + b", vec!["a", "b"], 5);
info.all_exprs.insert(expr1.clone());
info.avail_in
.insert(0, [expr1.clone()].into_iter().collect());
info.expr_instances_with_blocks
.push(ExprInstance::new(expr1.clone(), 0));
info.expr_instances_with_blocks
.push(ExprInstance::new(expr2.clone(), 0));
let redundant = info.redundant_computations();
assert_eq!(
redundant.len(),
1,
"Should detect one redundant computation"
);
assert_eq!(redundant[0].0, "a + b");
assert_eq!(redundant[0].1, 2); assert_eq!(redundant[0].2, 5); }
#[test]
fn test_phase4_get_available_at_line_with_kill() {
use crate::dataflow::{AvailableExprsInfo, Expression};
use crate::types::{BlockType, CfgBlock, CfgInfo};
use std::collections::HashMap;
let mut info = AvailableExprsInfo::new(0);
let expr = Expression::new("a + b", vec!["a", "b"], 1);
info.all_exprs.insert(expr.clone());
info.avail_in
.insert(0, [expr.clone()].into_iter().collect());
info.defs_per_line
.insert(3, ["a".to_string()].into_iter().collect());
let cfg = CfgInfo {
function: "test".to_string(),
blocks: vec![CfgBlock {
id: 0,
block_type: BlockType::Entry,
lines: (1, 5),
calls: vec![],
}],
edges: vec![],
entry_block: 0,
exit_blocks: vec![],
cyclomatic_complexity: 1,
nested_functions: HashMap::new(),
};
let avail_at_2 = info.get_available_at_line(2, &cfg);
assert!(
avail_at_2.contains(&expr),
"a+b should be available at line 2 (before kill)"
);
let avail_at_4 = info.get_available_at_line(4, &cfg);
assert!(
avail_at_4.is_empty(),
"a+b should NOT be available at line 4 (after kill at line 3)"
);
}
#[test]
fn test_linear_cfg_expression_available_downstream() {
let cfg = make_test_cfg_for_phase3(
vec![
(0, BlockType::Entry, (1, 2)),
(1, BlockType::Body, (3, 4)),
(2, BlockType::Exit, (5, 6)),
],
vec![(0, 1), (1, 2)],
0,
);
let dfg = make_dfg_with_refs_for_phase3(vec![
make_var_ref_for_phase3("x", 2, RefType::Definition),
make_var_ref_for_phase3("a", 2, RefType::Use),
make_var_ref_for_phase3("b", 2, RefType::Use),
]);
let result = compute_available_exprs(&cfg, &dfg).unwrap();
if !result.all_exprs.is_empty() {
let expr = result.all_exprs.iter().next().unwrap();
assert!(
result.is_available_at_exit(0, expr),
"Expression should be available at exit of generating block"
);
assert!(
result.is_available(1, expr),
"Expression should propagate to downstream block 1"
);
assert!(
result.is_available(2, expr),
"Expression should propagate to downstream block 2"
);
}
}
#[test]
fn test_loop_cfg_expression_available_in_body() {
let cfg = make_test_cfg_for_phase3(
vec![
(0, BlockType::Entry, (1, 1)),
(1, BlockType::LoopHeader, (2, 2)),
(2, BlockType::LoopBody, (3, 3)),
(3, BlockType::Exit, (4, 4)),
],
vec![(0, 1), (1, 2), (2, 1), (1, 3)],
0,
);
let dfg = make_empty_dfg_for_phase3();
let result = compute_available_exprs(&cfg, &dfg);
assert!(result.is_ok(), "Loop CFG should be handled without crash");
}
#[test]
fn test_unreachable_block_handled() {
let cfg = make_test_cfg_for_phase3(
vec![
(0, BlockType::Entry, (1, 1)),
(1, BlockType::Exit, (2, 2)),
(2, BlockType::Body, (3, 3)), ],
vec![(0, 1)],
0,
);
let dfg = make_empty_dfg_for_phase3();
let result = compute_available_exprs(&cfg, &dfg);
assert!(
result.is_ok(),
"Unreachable block should be handled without crash"
);
let info = result.unwrap();
assert!(
info.avail_in.get(&2).unwrap().is_empty(),
"Unreachable block should have nothing available"
);
}
#[test]
fn test_self_loop_handled() {
let cfg = make_test_cfg_for_phase3(
vec![
(0, BlockType::Entry, (1, 1)),
(1, BlockType::LoopHeader, (2, 3)),
],
vec![(0, 1), (1, 1)],
0,
);
let dfg = make_empty_dfg_for_phase3();
let result = compute_available_exprs(&cfg, &dfg);
assert!(result.is_ok(), "Self-loop CFG should terminate at fixpoint");
}
#[test]
fn test_avail_exprs_empty_function_no_crash() {
let cfg = make_test_cfg_for_phase3(vec![(0, BlockType::Entry, (1, 1))], vec![], 0);
let dfg = make_empty_dfg_for_phase3();
let result = compute_available_exprs(&cfg, &dfg).unwrap();
assert!(
result.all_exprs.is_empty(),
"Empty function should have no expressions"
);
assert!(
result.redundant_computations().is_empty(),
"Empty function should have no redundant computations"
);
}
#[test]
fn test_multiple_expressions_tracked_independently() {
let expr_ab = Expression::new("a + b", vec!["a", "b"], 1);
let expr_xy = Expression::new("x * y", vec!["x", "y"], 2);
let expr_cd = Expression::new("c - d", vec!["c", "d"], 3);
assert!(expr_ab.is_killed_by("a"));
assert!(!expr_xy.is_killed_by("a"));
assert!(!expr_cd.is_killed_by("a"));
assert_ne!(expr_ab, expr_xy);
assert_ne!(expr_xy, expr_cd);
assert_ne!(expr_ab, expr_cd);
}
#[test]
fn test_function_calls_excluded_from_cse() {
use crate::dataflow::available::{is_function_call, parse_expression_from_line};
assert!(is_function_call("foo()"));
assert!(is_function_call("bar(x, y)"));
assert!(is_function_call("obj.method()"));
assert!(!is_function_call("a + b"));
assert!(!is_function_call("x * y"));
assert!(parse_expression_from_line("x = foo()").is_none());
assert!(parse_expression_from_line("y = bar.baz()").is_none());
assert!(parse_expression_from_line("z = process(data)").is_none());
let result = parse_expression_from_line("x = a + b");
assert!(result.is_some());
let (left, op, right) = result.unwrap();
assert_eq!(left, "a");
assert_eq!(op, "+");
assert_eq!(right, "b");
}
#[test]
fn test_nullability_enum_has_three_values() {
let never = MockNullability::Never;
let maybe = MockNullability::Maybe;
let always = MockNullability::Always;
assert_eq!(never.as_str(), "never");
assert_eq!(maybe.as_str(), "maybe");
assert_eq!(always.as_str(), "always");
}
#[test]
fn test_nullability_default_is_maybe() {
let default: MockNullability = Default::default();
assert_eq!(default, MockNullability::Maybe);
}
#[test]
fn test_abstract_value_has_required_fields() {
let val = MockAbstractValue::top();
let _ = val.type_;
let _ = val.range_;
let _ = val.nullable;
let _ = val.constant;
}
#[test]
fn test_abstract_value_is_hashable() {
let val1 = MockAbstractValue::from_constant(MockConstantValue::Int(5));
let val2 = MockAbstractValue::from_constant(MockConstantValue::Int(5));
let mut set: HashSet<MockAbstractValue> = HashSet::new();
set.insert(val1);
set.insert(val2);
assert_eq!(set.len(), 1);
}
#[test]
fn test_abstract_value_top_creates_unknown() {
let top = MockAbstractValue::top();
assert!(top.type_.is_none(), "top() should have None type");
assert!(top.range_.is_none(), "top() should have None range");
assert_eq!(
top.nullable,
MockNullability::Maybe,
"top() should have MAYBE nullable"
);
assert!(top.constant.is_none(), "top() should have None constant");
}
#[test]
fn test_abstract_value_bottom_creates_contradiction() {
let bottom = MockAbstractValue::bottom();
assert_eq!(bottom.type_, Some("<bottom>".to_string()));
}
#[test]
fn test_abstract_value_from_constant_int() {
let val = MockAbstractValue::from_constant(MockConstantValue::Int(42));
assert_eq!(val.type_, Some("int".to_string()));
assert_eq!(val.range_, Some((Some(42), Some(42))));
assert_eq!(val.nullable, MockNullability::Never);
assert!(val.is_constant());
}
#[test]
fn test_abstract_value_from_constant_negative_int() {
let val = MockAbstractValue::from_constant(MockConstantValue::Int(-5));
assert_eq!(val.type_, Some("int".to_string()));
assert_eq!(val.range_, Some((Some(-5), Some(-5))));
}
#[test]
fn test_abstract_value_from_constant_string() {
let val = MockAbstractValue::from_constant(MockConstantValue::String("hello".to_string()));
assert_eq!(val.type_, Some("str".to_string()));
assert_eq!(val.nullable, MockNullability::Never);
assert!(val.is_constant());
}
#[test]
fn test_abstract_value_string_tracks_length() {
let val = MockAbstractValue::from_constant(MockConstantValue::String("hello".to_string()));
assert_eq!(
val.range_,
Some((Some(5), Some(5))),
"String length should be tracked"
);
}
#[test]
fn test_abstract_value_from_constant_none() {
let val = MockAbstractValue::from_constant(MockConstantValue::Null);
assert_eq!(val.type_, Some("NoneType".to_string()));
assert_eq!(val.nullable, MockNullability::Always);
assert!(val.range_.is_none());
}
#[test]
fn test_abstract_value_from_constant_bool() {
let val_true = MockAbstractValue::from_constant(MockConstantValue::Bool(true));
let val_false = MockAbstractValue::from_constant(MockConstantValue::Bool(false));
assert_eq!(val_true.type_, Some("bool".to_string()));
assert_eq!(val_false.type_, Some("bool".to_string()));
assert_eq!(val_true.range_, Some((Some(1), Some(1))));
assert_eq!(val_false.range_, Some((Some(0), Some(0))));
}
#[test]
fn test_abstract_value_from_constant_float() {
let val = MockAbstractValue::from_constant(MockConstantValue::Float(PI));
assert_eq!(val.type_, Some("float".to_string()));
assert!(val.range_.is_none(), "Float ranges not tracked");
assert_eq!(val.nullable, MockNullability::Never);
}
#[test]
fn test_may_be_zero_returns_true_when_range_includes_zero() {
let val = MockAbstractValue {
type_: Some("int".to_string()),
range_: Some((Some(-5), Some(5))),
nullable: MockNullability::Never,
constant: None,
};
assert!(val.may_be_zero(), "Range [-5, 5] should may_be_zero");
}
#[test]
fn test_may_be_zero_returns_false_when_range_excludes_zero() {
let val = MockAbstractValue {
type_: Some("int".to_string()),
range_: Some((Some(5), Some(10))),
nullable: MockNullability::Never,
constant: None,
};
assert!(!val.may_be_zero(), "Range [5, 10] should not may_be_zero");
}
#[test]
fn test_may_be_zero_returns_true_for_unknown_range() {
let val = MockAbstractValue::top();
assert!(
val.may_be_zero(),
"Unknown range should conservatively may_be_zero"
);
}
#[test]
fn test_may_be_null_for_maybe() {
let val = MockAbstractValue {
type_: None,
range_: None,
nullable: MockNullability::Maybe,
constant: None,
};
assert!(val.may_be_null(), "MAYBE should may_be_null");
}
#[test]
fn test_may_be_null_for_never() {
let val = MockAbstractValue::from_constant(MockConstantValue::Int(5));
assert!(!val.may_be_null(), "NEVER should not may_be_null");
}
#[test]
fn test_may_be_null_for_always() {
let val = MockAbstractValue::from_constant(MockConstantValue::Null);
assert!(val.may_be_null(), "ALWAYS should may_be_null");
}
#[test]
fn test_is_constant_true_when_constant_set() {
let val = MockAbstractValue::from_constant(MockConstantValue::Int(42));
assert!(val.is_constant());
}
#[test]
fn test_is_constant_false_when_constant_none() {
let val = MockAbstractValue::top();
assert!(!val.is_constant());
}
#[test]
fn test_abstract_state_empty_initialization() {
let state = MockAbstractState::new();
assert!(state.values.is_empty());
}
#[test]
fn test_abstract_state_get_returns_value_for_existing_var() {
let mut state = MockAbstractState::new();
let val = MockAbstractValue::from_constant(MockConstantValue::Int(10));
state.values.insert("x".to_string(), val.clone());
let retrieved = state.get("x");
assert_eq!(retrieved, val);
}
#[test]
fn test_abstract_state_get_returns_top_for_missing_var() {
let state = MockAbstractState::new();
let retrieved = state.get("unknown");
assert_eq!(retrieved, MockAbstractValue::top());
}
#[test]
fn test_abstract_state_set_returns_new_state() {
let state = MockAbstractState::new();
let val = MockAbstractValue::from_constant(MockConstantValue::Int(5));
let new_state = state.set("x", val);
assert!(
state.values.is_empty(),
"Original state should be unchanged"
);
assert!(
new_state.values.contains_key("x"),
"New state should have x"
);
}
#[test]
fn test_abstract_state_copy_creates_independent_copy() {
let mut state = MockAbstractState::new();
state.values.insert(
"x".to_string(),
MockAbstractValue::from_constant(MockConstantValue::Int(1)),
);
let copied = state.copy();
state.values.insert(
"y".to_string(),
MockAbstractValue::from_constant(MockConstantValue::Int(2)),
);
assert!(
!copied.values.contains_key("y"),
"Copy should be independent"
);
}
#[test]
fn test_abstract_state_equality() {
let state1 = MockAbstractState::new().set(
"x",
MockAbstractValue::from_constant(MockConstantValue::Int(5)),
);
let state2 = MockAbstractState::new().set(
"x",
MockAbstractValue::from_constant(MockConstantValue::Int(5)),
);
assert_eq!(state1, state2);
}
#[test]
fn test_abstract_interp_info_has_required_fields() {
let info = MockAbstractInterpInfo::new("test_func");
assert_eq!(info.function_name, "test_func");
assert!(info.state_in.is_empty());
assert!(info.state_out.is_empty());
assert!(info.potential_div_zero.is_empty());
assert!(info.potential_null_deref.is_empty());
}
#[test]
fn test_value_at_returns_abstract_value_at_block_entry() {
let mut info = MockAbstractInterpInfo::new("test");
let val = MockAbstractValue::from_constant(MockConstantValue::Int(42));
let state = MockAbstractState::new().set("x", val.clone());
info.state_in.insert(0, state);
let retrieved = info.value_at(0, "x");
assert_eq!(retrieved, val);
}
#[test]
fn test_value_at_returns_top_for_missing_block() {
let info = MockAbstractInterpInfo::new("test");
let retrieved = info.value_at(999, "x");
assert_eq!(retrieved, MockAbstractValue::top());
}
#[test]
fn test_value_at_exit_returns_value_at_block_exit() {
let mut info = MockAbstractInterpInfo::new("test");
let val = MockAbstractValue::from_constant(MockConstantValue::Int(100));
let state = MockAbstractState::new().set("result", val.clone());
info.state_out.insert(0, state);
let retrieved = info.value_at_exit(0, "result");
assert_eq!(retrieved, val);
}
#[test]
fn test_range_at_returns_range_tuple() {
let mut info = MockAbstractInterpInfo::new("test");
let val = MockAbstractValue {
type_: Some("int".to_string()),
range_: Some((Some(1), Some(10))),
nullable: MockNullability::Never,
constant: None,
};
let state = MockAbstractState::new().set("x", val);
info.state_in.insert(0, state);
let range = info.range_at(0, "x");
assert_eq!(range, Some((Some(1), Some(10))));
}
#[test]
fn test_type_at_returns_inferred_type() {
let mut info = MockAbstractInterpInfo::new("test");
let val = MockAbstractValue::from_constant(MockConstantValue::String("hello".to_string()));
let state = MockAbstractState::new().set("s", val);
info.state_in.insert(0, state);
let typ = info.type_at(0, "s");
assert_eq!(typ, Some("str".to_string()));
}
#[test]
fn test_is_definitely_not_null_for_never_nullable() {
let mut info = MockAbstractInterpInfo::new("test");
let val = MockAbstractValue::from_constant(MockConstantValue::Int(5));
let state = MockAbstractState::new().set("x", val);
info.state_in.insert(0, state);
assert!(info.is_definitely_not_null(0, "x"));
}
#[test]
fn test_is_definitely_not_null_for_maybe_nullable() {
let mut info = MockAbstractInterpInfo::new("test");
let val = MockAbstractValue::top(); let state = MockAbstractState::new().set("x", val);
info.state_in.insert(0, state);
assert!(!info.is_definitely_not_null(0, "x"));
}
#[test]
fn test_get_constants_returns_known_constant_values() {
let mut info = MockAbstractInterpInfo::new("test");
let val = MockAbstractValue::from_constant(MockConstantValue::Int(42));
let state = MockAbstractState::new().set("x", val);
info.state_out.insert(0, state);
let constants = info.get_constants();
assert!(constants.contains_key("x"));
assert_eq!(constants.get("x"), Some(&MockConstantValue::Int(42)));
}
#[test]
fn test_abstract_interp_to_json_serializable() {
let mut info = MockAbstractInterpInfo::new("example");
let val = MockAbstractValue::from_constant(MockConstantValue::Int(5));
let state = MockAbstractState::new().set("x", val);
info.state_in.insert(0, MockAbstractState::new());
info.state_out.insert(0, state);
info.potential_div_zero.push((10, "divisor".to_string()));
let json = info.to_json_value();
assert!(json.get("function").is_some());
assert!(json.get("state_in").is_some());
assert!(json.get("state_out").is_some());
assert!(json.get("potential_div_zero").is_some());
assert!(json.get("potential_null_deref").is_some());
let serialized = serde_json::to_string(&json);
assert!(serialized.is_ok());
}
#[test]
fn test_join_ranges_union() {
let val1 = MockAbstractValue {
type_: Some("int".to_string()),
range_: Some((Some(1), Some(1))),
nullable: MockNullability::Never,
constant: Some(MockConstantValue::Int(1)),
};
let val2 = MockAbstractValue {
type_: Some("int".to_string()),
range_: Some((Some(10), Some(10))),
nullable: MockNullability::Never,
constant: Some(MockConstantValue::Int(10)),
};
let joined = join_values(&[val1, val2]);
assert_eq!(joined.range_, Some((Some(1), Some(10))));
}
#[test]
fn test_join_loses_constant_on_disagreement() {
let val1 = MockAbstractValue::from_constant(MockConstantValue::Int(1));
let val2 = MockAbstractValue::from_constant(MockConstantValue::Int(10));
let joined = join_values(&[val1, val2]);
assert!(
joined.constant.is_none(),
"Constant should be lost on disagreement"
);
}
#[test]
fn test_join_preserves_constant_on_agreement() {
let val1 = MockAbstractValue::from_constant(MockConstantValue::Int(5));
let val2 = MockAbstractValue::from_constant(MockConstantValue::Int(5));
let joined = join_values(&[val1, val2]);
assert_eq!(joined.constant, Some(MockConstantValue::Int(5)));
}
#[test]
fn test_join_nullable_maybe_if_any_maybe() {
let val1 = MockAbstractValue {
type_: None,
range_: None,
nullable: MockNullability::Never,
constant: None,
};
let val2 = MockAbstractValue {
type_: None,
range_: None,
nullable: MockNullability::Maybe,
constant: None,
};
let joined = join_values(&[val1, val2]);
assert_eq!(joined.nullable, MockNullability::Maybe);
}
#[test]
fn test_widening_upper_bound_to_infinity() {
let old = MockAbstractValue {
type_: Some("int".to_string()),
range_: Some((Some(0), Some(5))),
nullable: MockNullability::Never,
constant: None,
};
let new = MockAbstractValue {
type_: Some("int".to_string()),
range_: Some((Some(0), Some(10))), nullable: MockNullability::Never,
constant: None,
};
let widened = widen_value(&old, &new);
assert_eq!(widened.range_, Some((Some(0), None)));
}
#[test]
fn test_widening_lower_bound_to_infinity() {
let old = MockAbstractValue {
type_: Some("int".to_string()),
range_: Some((Some(-5), Some(10))),
nullable: MockNullability::Never,
constant: None,
};
let new = MockAbstractValue {
type_: Some("int".to_string()),
range_: Some((Some(-10), Some(10))), nullable: MockNullability::Never,
constant: None,
};
let widened = widen_value(&old, &new);
assert_eq!(widened.range_, Some((None, Some(10))));
}
#[test]
fn test_widening_loses_constant() {
let old = MockAbstractValue::from_constant(MockConstantValue::Int(5));
let new = MockAbstractValue::from_constant(MockConstantValue::Int(6));
let widened = widen_value(&old, &new);
assert!(widened.constant.is_none(), "Widening should lose constant");
}
#[test]
fn test_arithmetic_add() {
let operand = MockAbstractValue::from_constant(MockConstantValue::Int(5));
let result = apply_arithmetic(&operand, '+', 3);
assert_eq!(result.range_, Some((Some(8), Some(8))));
}
#[test]
fn test_arithmetic_subtract() {
let operand = MockAbstractValue::from_constant(MockConstantValue::Int(10));
let result = apply_arithmetic(&operand, '-', 3);
assert_eq!(result.range_, Some((Some(7), Some(7))));
}
#[test]
fn test_arithmetic_multiply() {
let operand = MockAbstractValue::from_constant(MockConstantValue::Int(4));
let result = apply_arithmetic(&operand, '*', 2);
assert_eq!(result.range_, Some((Some(8), Some(8))));
}
#[test]
fn test_arithmetic_on_range() {
let operand = MockAbstractValue {
type_: Some("int".to_string()),
range_: Some((Some(1), Some(5))),
nullable: MockNullability::Never,
constant: None,
};
let result = apply_arithmetic(&operand, '+', 10);
assert_eq!(result.range_, Some((Some(11), Some(15))));
}
#[test]
#[ignore = "Abstract interpretation not yet implemented"]
fn test_div_zero_detected_for_constant_zero() {
todo!("Implement division-by-zero detection");
}
#[test]
#[ignore = "Abstract interpretation not yet implemented"]
fn test_div_zero_detected_for_range_including_zero() {
todo!("Implement range-based div-zero detection");
}
#[test]
#[ignore = "Abstract interpretation not yet implemented"]
fn test_div_safe_no_warning_for_constant_nonzero() {
todo!("Implement safe division detection");
}
#[test]
#[ignore = "Abstract interpretation not yet implemented"]
fn test_div_safe_no_warning_for_positive_range() {
todo!("Implement positive range detection");
}
#[test]
#[ignore = "Abstract interpretation not yet implemented"]
fn test_div_zero_intra_block_accuracy() {
todo!("Implement intra-block state accuracy");
}
#[test]
#[ignore = "Abstract interpretation not yet implemented"]
fn test_null_deref_detected_at_attribute_access() {
todo!("Implement null dereference detection");
}
#[test]
#[ignore = "Abstract interpretation not yet implemented"]
fn test_null_deref_safe_for_non_null_constant() {
todo!("Implement safe null detection");
}
#[test]
#[ignore = "Abstract interpretation not yet implemented"]
fn test_compute_abstract_interp_returns_info() {
todo!("Implement compute_abstract_interp");
}
#[test]
#[ignore = "Abstract interpretation not yet implemented"]
fn test_compute_tracks_constant_assignment() {
todo!("Implement constant tracking");
}
#[test]
#[ignore = "Abstract interpretation not yet implemented"]
fn test_compute_tracks_variable_copy() {
todo!("Implement variable copy tracking");
}
#[test]
#[ignore = "Abstract interpretation not yet implemented"]
fn test_compute_tracks_none_assignment() {
todo!("Implement None tracking");
}
#[test]
#[ignore = "Abstract interpretation not yet implemented"]
fn test_abstract_interp_empty_function_no_crash() {
todo!("Implement empty function handling");
}
#[test]
#[ignore = "Abstract interpretation not yet implemented"]
fn test_unknown_rhs_defaults_to_top() {
todo!("Implement unknown RHS handling");
}
#[test]
#[ignore = "Abstract interpretation not yet implemented"]
fn test_parameter_starts_as_top() {
todo!("Implement parameter initialization");
}
#[test]
#[ignore = "Abstract interpretation not yet implemented"]
fn test_nested_loops_terminate() {
todo!("Implement nested loop termination");
}
#[test]
fn test_python_none_keyword_recognized() {
let keywords = get_null_keywords("python");
assert!(keywords.contains(&"None"));
}
#[test]
fn test_typescript_null_keyword_recognized() {
let keywords = get_null_keywords("typescript");
assert!(keywords.contains(&"null"));
}
#[test]
fn test_typescript_undefined_keyword_recognized() {
let keywords = get_null_keywords("typescript");
assert!(keywords.contains(&"undefined"));
}
#[test]
fn test_go_nil_keyword_recognized() {
let keywords = get_null_keywords("go");
assert!(keywords.contains(&"nil"));
}
#[test]
fn test_rust_has_no_null_keyword() {
let keywords = get_null_keywords("rust");
assert!(keywords.is_empty(), "Rust should have no null keywords");
}
#[test]
fn test_python_boolean_capitalized() {
let bools = get_boolean_keywords("python");
assert_eq!(bools.get("True"), Some(&true));
assert_eq!(bools.get("False"), Some(&false));
}
#[test]
fn test_typescript_boolean_lowercase() {
let bools = get_boolean_keywords("typescript");
assert_eq!(bools.get("true"), Some(&true));
assert_eq!(bools.get("false"), Some(&false));
}
#[test]
fn test_python_comment_pattern() {
let pattern = get_comment_pattern("python");
assert_eq!(pattern, "#");
}
#[test]
fn test_typescript_comment_pattern() {
let pattern = get_comment_pattern("typescript");
assert_eq!(pattern, "//");
}
#[test]
#[ignore = "Abstract interpretation not yet implemented"]
fn test_compute_accepts_language_parameter() {
todo!("Implement language parameter support");
}
#[test]
#[ignore = "Abstract interpretation not yet implemented"]
fn test_compute_with_typescript_null() {
todo!("Implement TypeScript null handling");
}
#[test]
#[ignore = "Abstract interpretation not yet implemented"]
fn test_compute_with_go_nil() {
todo!("Implement Go nil handling");
}
#[test]
fn test_integration_compute_available_exprs_from_cfg_dfg() {
use super::compute_available_exprs;
use crate::cfg::get_cfg_context;
use crate::dfg::get_dfg_context;
use crate::types::Language;
let source = r#"
def test_func(a, b):
x = a + b
y = a + b
return x + y
"#;
let cfg = get_cfg_context(source, "test_func", Language::Python);
assert!(
cfg.is_ok(),
"CFG extraction should succeed: {:?}",
cfg.err()
);
let cfg = cfg.unwrap();
let dfg = get_dfg_context(source, "test_func", Language::Python);
assert!(
dfg.is_ok(),
"DFG extraction should succeed: {:?}",
dfg.err()
);
let dfg = dfg.unwrap();
let result = compute_available_exprs(&cfg, &dfg);
assert!(
result.is_ok(),
"compute_available_exprs should succeed: {:?}",
result.err()
);
let info = result.unwrap();
assert!(!info.avail_in.is_empty(), "avail_in should not be empty");
assert!(!info.avail_out.is_empty(), "avail_out should not be empty");
let json = info.to_json();
assert!(json.is_object(), "to_json should return an object");
assert!(
json.get("avail_in").is_some(),
"JSON should have avail_in field"
);
assert!(
json.get("avail_out").is_some(),
"JSON should have avail_out field"
);
assert!(
json.get("all_expressions").is_some(),
"JSON should have all_expressions field"
);
}
#[test]
fn test_integration_compute_abstract_interp_from_cfg() {
use super::compute_abstract_interp;
use crate::cfg::get_cfg_context;
use crate::dfg::get_dfg_context;
use crate::types::Language;
let source = r#"
def test_func():
x = 5
y = None
z = x + 1
return z
"#;
let cfg = get_cfg_context(source, "test_func", Language::Python);
assert!(
cfg.is_ok(),
"CFG extraction should succeed: {:?}",
cfg.err()
);
let cfg = cfg.unwrap();
let dfg = get_dfg_context(source, "test_func", Language::Python);
assert!(
dfg.is_ok(),
"DFG extraction should succeed: {:?}",
dfg.err()
);
let dfg = dfg.unwrap();
let source_lines: Vec<&str> = source.lines().collect();
let result = compute_abstract_interp(&cfg, &dfg, Some(&source_lines), "python");
assert!(
result.is_ok(),
"compute_abstract_interp should succeed: {:?}",
result.err()
);
let info = result.unwrap();
assert!(!info.state_in.is_empty(), "state_in should not be empty");
assert!(!info.state_out.is_empty(), "state_out should not be empty");
let json = info.to_json();
assert!(json.is_object(), "to_json should return an object");
assert!(
json.get("state_in").is_some(),
"JSON should have state_in field"
);
assert!(
json.get("state_out").is_some(),
"JSON should have state_out field"
);
assert!(
json.get("potential_div_zero").is_some(),
"JSON should have potential_div_zero field"
);
assert!(
json.get("potential_null_deref").is_some(),
"JSON should have potential_null_deref field"
);
}
#[test]
fn test_switch_five_arms_available_expr() {
use super::compute_available_exprs;
use crate::types::{BlockType, RefType};
let cfg = make_test_cfg_for_phase3(
vec![
(0, BlockType::Entry, (1, 1)),
(1, BlockType::Body, (2, 2)),
(2, BlockType::Body, (3, 3)),
(3, BlockType::Body, (4, 4)),
(4, BlockType::Body, (5, 5)),
(5, BlockType::Body, (6, 6)),
(6, BlockType::Exit, (7, 7)),
],
vec![
(0, 1),
(0, 2),
(0, 3),
(0, 4),
(0, 5),
(1, 6),
(2, 6),
(3, 6),
(4, 6),
(5, 6),
],
0,
);
let dfg = make_dfg_with_refs_for_phase3(vec![
make_var_ref_for_phase3("a", 2, RefType::Use),
make_var_ref_for_phase3("b", 2, RefType::Use),
make_var_ref_for_phase3("x", 2, RefType::Definition),
]);
let result = compute_available_exprs(&cfg, &dfg);
assert!(
result.is_ok(),
"compute_available_exprs should succeed for 5-arm switch: {:?}",
result.err()
);
let info = result.unwrap();
assert!(
info.avail_out.contains_key(&1),
"Block 1 should have avail_out"
);
let avail_at_merge = info.avail_in.get(&6).cloned().unwrap_or_default();
for expr in &avail_at_merge {
let has_both =
expr.operands.contains(&"a".to_string()) && expr.operands.contains(&"b".to_string());
assert!(
!has_both,
"Expression 'a + b' should NOT be available at merge block (only in 1 of 5 arms)"
);
}
}
#[test]
fn test_try_catch_returns_error_or_handles() {
use super::compute_available_exprs;
use crate::types::{BlockType, RefType};
let cfg = make_test_cfg_for_phase3(
vec![
(0, BlockType::Entry, (1, 1)),
(1, BlockType::Body, (2, 3)), (2, BlockType::Body, (4, 5)), (3, BlockType::Body, (6, 6)), (4, BlockType::Exit, (7, 7)), ],
vec![(0, 1), (1, 2), (1, 3), (2, 4), (3, 4)],
0,
);
let dfg = make_dfg_with_refs_for_phase3(vec![
make_var_ref_for_phase3("a", 2, RefType::Use),
make_var_ref_for_phase3("b", 2, RefType::Use),
make_var_ref_for_phase3("x", 2, RefType::Definition),
]);
let result = compute_available_exprs(&cfg, &dfg);
match result {
Ok(info) => {
assert!(
info.avail_in.contains_key(&0),
"Entry block should have avail_in"
);
}
Err(super::DataflowError::UnsupportedCfgPattern { .. }) => {
}
Err(other) => {
panic!("Unexpected error for try-catch CFG: {:?}", other);
}
}
}
#[test]
fn test_early_return_intra_block_handling() {
use super::compute_available_exprs;
use crate::types::{BlockType, RefType};
let cfg = make_test_cfg_for_phase3(
vec![(0, BlockType::Entry, (1, 2)), (1, BlockType::Exit, (3, 3))],
vec![(0, 1)],
0,
);
let dfg = make_dfg_with_refs_for_phase3(vec![
make_var_ref_for_phase3("a", 1, RefType::Use),
make_var_ref_for_phase3("b", 1, RefType::Use),
make_var_ref_for_phase3("x", 1, RefType::Definition),
]);
let result = compute_available_exprs(&cfg, &dfg);
assert!(
result.is_ok(),
"Analysis should handle simple early return pattern: {:?}",
result.err()
);
let info = result.unwrap();
assert!(
info.avail_out.contains_key(&0),
"Block 0 should have an avail_out entry"
);
}
#[test]
fn test_deeply_nested_expression_limited() {
use super::{normalize_expression, Expression};
let mut result = "a".to_string();
for i in 0..100 {
let var = format!("v{}", i);
result = normalize_expression(&result, "+", &var);
}
assert!(
result.contains('+'),
"Deeply nested normalization should complete"
);
assert!(
result.len() > 100,
"Result should be built from many operations"
);
let many_operands: Vec<String> = (0..100).map(|i| format!("var{}", i)).collect();
let expr = Expression::new(
"complex expression text",
many_operands.iter().map(|s| s.as_str()).collect(),
1,
);
assert!(
expr.is_killed_by("var0"),
"Should detect kill for first operand"
);
assert!(
expr.is_killed_by("var99"),
"Should detect kill for last operand"
);
assert!(
!expr.is_killed_by("not_present"),
"Should not false-positive"
);
}
#[test]
fn test_pathological_cfg_10k_blocks_limited() {
use super::{compute_available_exprs, DataflowError, MAX_BLOCKS};
use crate::types::{BlockType, CfgBlock, CfgEdge, CfgInfo, DfgInfo, EdgeType};
use std::collections::HashMap;
let block_count = MAX_BLOCKS + 1;
let blocks: Vec<CfgBlock> = (0..block_count)
.map(|id| CfgBlock {
id,
block_type: if id == 0 {
BlockType::Entry
} else {
BlockType::Body
},
lines: (id as u32, id as u32),
calls: vec![],
})
.collect();
let edges: Vec<CfgEdge> = (0..block_count - 1)
.map(|i| CfgEdge {
from: i,
to: i + 1,
edge_type: EdgeType::Unconditional,
condition: None,
})
.collect();
let cfg = CfgInfo {
function: "pathological".to_string(),
blocks,
edges,
entry_block: 0,
exit_blocks: vec![block_count - 1],
cyclomatic_complexity: 1,
nested_functions: HashMap::new(),
};
let dfg = DfgInfo {
function: "pathological".to_string(),
refs: vec![],
edges: vec![],
variables: vec![],
};
let result = compute_available_exprs(&cfg, &dfg);
match result {
Err(DataflowError::TooManyBlocks { count }) => {
assert_eq!(count, block_count, "Error should report actual block count");
}
Ok(_) => {
panic!(
"Analysis should reject CFG with {} blocks (exceeds MAX_BLOCKS={})",
block_count, MAX_BLOCKS
);
}
Err(other) => {
panic!("Expected TooManyBlocks error, got: {:?}", other);
}
}
}
#[test]
fn test_range_overflow_saturates() {
use super::abstract_interp::apply_arithmetic;
use super::{AbstractValue, ConstantValue};
let max_val = AbstractValue::from_constant(ConstantValue::Int(i64::MAX));
let result = apply_arithmetic(&max_val, '+', 1);
if let Some((low, high)) = result.range_ {
if let Some(h) = high {
assert_eq!(h, i64::MAX, "Saturated to MAX");
}
if let Some(l) = low {
assert!(l >= i64::MAX - 1, "Low bound reasonable after saturation");
}
}
let min_val = AbstractValue::from_constant(ConstantValue::Int(i64::MIN));
let result = apply_arithmetic(&min_val, '-', 1);
if let Some((Some(l), _high)) = result.range_ {
assert_eq!(l, i64::MIN, "Saturated to MIN");
}
let large_val = AbstractValue::from_constant(ConstantValue::Int(i64::MAX / 2 + 1));
let result = apply_arithmetic(&large_val, '*', 3);
assert!(
result.range_.is_some() || result.range_.is_none(),
"Multiplication overflow should be handled gracefully"
);
let normal_val = AbstractValue::from_constant(ConstantValue::Int(100));
let result = apply_arithmetic(&normal_val, '+', 50);
match result.range_ {
Some((Some(low), Some(high))) => {
assert_eq!(low, 150, "Normal addition should work");
assert_eq!(high, 150, "Normal addition should work");
}
_ => panic!("Normal arithmetic should produce bounded range"),
}
let pos_val = AbstractValue::from_constant(ConstantValue::Int(10));
let result = apply_arithmetic(&pos_val, '*', -2);
match result.range_ {
Some((Some(low), Some(high))) => {
assert_eq!(low, -20, "Negative mult should work");
assert_eq!(high, -20, "Negative mult should work");
}
_ => panic!("Negative multiplication should produce bounded range"),
}
}
#[test]
fn test_confidence_enum_values() {
use super::available::Confidence;
let low = Confidence::Low;
let med = Confidence::Medium;
let high = Confidence::High;
assert_ne!(format!("{:?}", low), format!("{:?}", high));
assert_ne!(format!("{:?}", med), format!("{:?}", low));
}
#[test]
fn test_confidence_default_is_low() {
use super::available::Confidence;
let c: Confidence = Default::default();
assert_eq!(c, Confidence::Low);
}
#[test]
fn test_confidence_serialization() {
use super::available::Confidence;
let json = serde_json::to_string(&Confidence::High).unwrap();
assert_eq!(json, "\"high\"");
let json = serde_json::to_string(&Confidence::Medium).unwrap();
assert_eq!(json, "\"medium\"");
let json = serde_json::to_string(&Confidence::Low).unwrap();
assert_eq!(json, "\"low\"");
}
#[test]
fn test_uncertain_finding_construction() {
use super::available::UncertainFinding;
let uf = UncertainFinding {
expr: "foo() + x".to_string(),
line: 42,
reason: "contains function call - purity unknown".to_string(),
};
assert_eq!(uf.expr, "foo() + x");
assert_eq!(uf.line, 42);
assert_eq!(uf.reason, "contains function call - purity unknown");
}
#[test]
fn test_uncertain_finding_serialization() {
use super::available::UncertainFinding;
let uf = UncertainFinding {
expr: "obj.method() + y".to_string(),
line: 15,
reason: "method access - purity unknown".to_string(),
};
let json = serde_json::to_string(&uf).unwrap();
assert!(json.contains("\"expr\""));
assert!(json.contains("\"line\""));
assert!(json.contains("\"reason\""));
let deserialized: UncertainFinding = serde_json::from_str(&json).unwrap();
assert_eq!(deserialized.expr, uf.expr);
assert_eq!(deserialized.line, uf.line);
}
#[test]
fn test_available_exprs_info_has_uncertain_fields() {
use super::available::{AvailableExprsInfo, Confidence};
let info = AvailableExprsInfo::empty(0);
assert!(info.uncertain_exprs.is_empty());
assert_eq!(info.confidence, Confidence::Low);
}
#[test]
fn test_available_exprs_info_uncertain_in_to_json() {
use super::available::{AvailableExprsInfo, Confidence, UncertainFinding};
let mut info = AvailableExprsInfo::empty(0);
info.uncertain_exprs.push(UncertainFinding {
expr: "foo() + bar()".to_string(),
line: 23,
reason: "function calls may have side effects".to_string(),
});
info.confidence = Confidence::Medium;
let json = info.to_json();
let obj = json
.as_object()
.unwrap_or_else(|| panic!("expected JSON object"));
assert!(
obj.contains_key("uncertain_exprs"),
"JSON should have uncertain_exprs key"
);
assert!(
obj.contains_key("confidence"),
"JSON should have confidence key"
);
let uncertain = obj["uncertain_exprs"].as_array().unwrap();
assert_eq!(uncertain.len(), 1);
assert_eq!(uncertain[0]["expr"], "foo() + bar()");
assert_eq!(uncertain[0]["line"], 23);
assert_eq!(obj["confidence"], "medium");
}
#[test]
fn test_available_exprs_function_call_becomes_uncertain() {
use super::available::is_function_call;
assert!(is_function_call("foo(x)"));
assert!(is_function_call("bar.baz(1, 2)"));
assert!(!is_function_call("a + b"));
}