mod cache;
mod call_graph_adapter;
mod known_pure_functions;
pub use cache::PurityCache;
pub use call_graph_adapter::PurityCallGraphAdapter;
pub use known_pure_functions::{
aggregate_callee_purity, resolve_callee_purity, CalleeEvidence, CalleePurity,
};
use crate::analysis::purity_analysis::{PurityAnalysis, PurityAnalyzer, PurityLevel};
use crate::core::FunctionMetrics;
use crate::priority::call_graph::FunctionId;
use anyhow::{anyhow, Result};
use dashmap::DashMap;
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct PurityResult {
pub level: PurityLevel,
pub confidence: f64,
pub reason: PurityReason,
}
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
pub enum PurityReason {
Intrinsic,
PropagatedFromDeps { depth: usize },
SideEffects { effects: Vec<String> },
RecursiveWithSideEffects,
RecursivePure,
UnknownDeps { count: usize },
}
impl PurityResult {
pub fn from_analysis(analysis: PurityAnalysis) -> Self {
let reason = if !analysis.violations.is_empty() {
PurityReason::SideEffects {
effects: analysis
.violations
.iter()
.map(|v| v.description())
.collect(),
}
} else {
PurityReason::Intrinsic
};
Self {
level: analysis.purity,
confidence: 1.0,
reason,
}
}
}
pub struct PurityPropagator {
cache: DashMap<FunctionId, PurityResult>,
call_graph: PurityCallGraphAdapter,
#[allow(dead_code)]
purity_analyzer: PurityAnalyzer,
}
impl PurityPropagator {
pub fn new(call_graph: PurityCallGraphAdapter, purity_analyzer: PurityAnalyzer) -> Self {
Self {
cache: DashMap::new(),
call_graph,
purity_analyzer,
}
}
pub fn propagate(&mut self, functions: &[FunctionMetrics]) -> Result<()> {
for func in functions {
let initial = self.analyze_intrinsic_purity(func)?;
let func_id = FunctionId::new(func.file.clone(), func.name.clone(), func.line);
self.cache.insert(func_id, initial);
}
let sorted = self.call_graph.topological_sort()?;
for func_id in sorted {
self.propagate_for_function(&func_id)?;
}
Ok(())
}
fn analyze_intrinsic_purity(&self, func: &FunctionMetrics) -> Result<PurityResult> {
if let (Some(is_pure), Some(confidence)) = (func.is_pure, func.purity_confidence) {
let level = if is_pure {
PurityLevel::StrictlyPure
} else {
PurityLevel::Impure
};
return Ok(PurityResult {
level,
confidence: confidence as f64,
reason: PurityReason::Intrinsic,
});
}
Ok(PurityResult {
level: PurityLevel::Impure,
confidence: 0.3,
reason: PurityReason::UnknownDeps { count: 0 },
})
}
fn propagate_for_function(&mut self, func_id: &FunctionId) -> Result<()> {
let mut result = self
.cache
.get(func_id)
.ok_or_else(|| anyhow!("Function not in cache"))?
.clone();
let deps = self.call_graph.get_dependencies(func_id);
if self.call_graph.is_in_cycle(func_id) {
if result.level == PurityLevel::StrictlyPure || result.level == PurityLevel::LocallyPure
{
result.reason = PurityReason::RecursivePure;
result.confidence *= 0.7; } else {
result.level = PurityLevel::Impure;
result.reason = PurityReason::RecursiveWithSideEffects;
result.confidence = 0.95;
}
self.cache.insert(func_id.clone(), result);
return Ok(());
}
let mut callee_evidence = Vec::new();
for dep_id in &deps {
let cached_purity = self.cache.get(dep_id).map(|r| {
let is_pure = r.level == PurityLevel::StrictlyPure;
(is_pure, r.confidence)
});
let callee_purity = resolve_callee_purity(&dep_id.name, None, cached_purity);
callee_evidence.push(CalleeEvidence {
callee_name: dep_id.name.clone(),
callee_purity,
});
}
let (all_deps_pure, aggregated_confidence, impure_reasons) =
aggregate_callee_purity(&callee_evidence);
let max_depth = deps
.iter()
.filter_map(|dep_id| self.cache.get(dep_id))
.filter_map(|r| {
if let PurityReason::PropagatedFromDeps { depth } = r.reason {
Some(depth)
} else {
None
}
})
.max()
.unwrap_or(0);
let unknown_count = callee_evidence
.iter()
.filter(|e| matches!(e.callee_purity, CalleePurity::Unknown))
.count();
if all_deps_pure && result.level != PurityLevel::Impure && impure_reasons.is_empty() {
result.level = PurityLevel::StrictlyPure;
result.reason = PurityReason::PropagatedFromDeps {
depth: max_depth + 1,
};
let depth_confidence = 0.9_f64.powi((max_depth + 1) as i32);
result.confidence =
(result.confidence * depth_confidence * aggregated_confidence).clamp(0.5, 1.0);
} else if !impure_reasons.is_empty() {
result.level = PurityLevel::Impure;
result.reason = PurityReason::SideEffects {
effects: impure_reasons,
};
result.confidence = aggregated_confidence;
} else if unknown_count > 0 {
result.reason = PurityReason::UnknownDeps {
count: unknown_count,
};
result.confidence = (result.confidence * aggregated_confidence).clamp(0.3, 1.0);
}
self.cache.insert(func_id.clone(), result);
Ok(())
}
pub fn get_result(&self, func_id: &FunctionId) -> Option<PurityResult> {
self.cache.get(func_id).map(|r| r.clone())
}
}