use crate::fast_hash::{FastMap as HashMap, FastSet as HashSet};
use bincode::Options;
use petgraph::stable_graph::StableGraph;
use petgraph::visit::EdgeRef;
use serde::de::DeserializeOwned;
use serde::{Deserialize, Serialize};
use std::collections::VecDeque;
use std::sync::atomic::{AtomicU64, Ordering as AtomicOrdering};
use std::sync::{Arc, Mutex};
use crate::graph::trigram::TrigramIndex;
mod lookup;
mod edges_traversal_serde;
pub type NodeId = petgraph::stable_graph::NodeIndex;
pub type EdgeId = petgraph::stable_graph::EdgeIndex;
pub const EXTERNAL_NODE_FILE_PATH: &str = "<external>";
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Node {
pub id: String,
pub node_type: NodeType,
pub name: String,
pub file_path: Arc<str>,
pub byte_range: (usize, usize),
pub complexity: u32,
pub language: String,
}
impl Node {
pub fn new_file_summary(file_path: &str, language: &str) -> Self {
let stem = std::path::Path::new(file_path)
.file_stem()
.and_then(|s| s.to_str())
.unwrap_or("file")
.to_string();
Self {
id: format!("{}::file_summary", file_path),
node_type: NodeType::FileSummary,
name: stem,
file_path: std::sync::Arc::from(file_path),
byte_range: (0, 0),
complexity: 0,
language: language.to_string(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
pub enum NodeType {
Function,
Class,
Method,
Variable,
Module,
External,
DocSection,
FileSummary,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
pub enum EdgeType {
Call,
DataDependency,
Inheritance,
Import,
Containment,
StateTransition,
CommandArgument,
Environment,
Stdin,
TypeOf,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Edge {
pub edge_type: EdgeType,
pub metadata: EdgeMetadata,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EdgeMetadata {
pub call_count: Option<usize>,
pub variable_name: Option<String>,
pub confidence: Option<f32>,
#[serde(default)]
pub channel: Option<String>,
#[serde(default)]
pub position: Option<usize>,
}
impl EdgeMetadata {
pub fn empty() -> Self {
Self {
call_count: None,
variable_name: None,
confidence: None,
channel: None,
position: None,
}
}
pub fn with_confidence(confidence: f32) -> Self {
Self {
call_count: None,
variable_name: None,
confidence: Some(confidence),
channel: None,
position: None,
}
}
pub fn with_variable(name: String) -> Self {
Self {
call_count: None,
variable_name: Some(name),
confidence: None,
channel: None,
position: None,
}
}
}
#[derive(Debug, Clone)]
pub struct TraversalConfig {
pub max_depth: Option<usize>,
pub max_nodes: Option<usize>,
pub allowed_edge_types: Option<&'static [EdgeType]>,
pub excluded_node_types: Option<Vec<NodeType>>,
pub min_complexity: Option<u32>,
pub min_edge_confidence: f32,
}
impl TraversalConfig {
pub fn for_llm_context() -> Self {
Self {
max_depth: Some(3),
max_nodes: Some(50),
allowed_edge_types: Some(&[
EdgeType::Call,
EdgeType::DataDependency,
EdgeType::StateTransition,
EdgeType::CommandArgument,
EdgeType::Environment,
EdgeType::Stdin,
]),
excluded_node_types: Some(vec![NodeType::Module]),
min_complexity: None,
min_edge_confidence: 0.5,
}
}
pub fn for_semantic_analysis() -> Self {
Self {
max_depth: Some(5),
max_nodes: Some(150),
allowed_edge_types: Some(&[
EdgeType::Call,
EdgeType::DataDependency,
EdgeType::Inheritance,
EdgeType::TypeOf,
EdgeType::StateTransition,
EdgeType::CommandArgument,
EdgeType::Environment,
EdgeType::Stdin,
]),
excluded_node_types: None,
min_complexity: None,
min_edge_confidence: 0.4,
}
}
pub fn for_impact_analysis() -> Self {
Self {
max_depth: None,
max_nodes: Some(500),
allowed_edge_types: Some(&[
EdgeType::Call,
EdgeType::DataDependency,
EdgeType::Inheritance,
EdgeType::TypeOf,
EdgeType::StateTransition,
EdgeType::CommandArgument,
EdgeType::Environment,
EdgeType::Stdin,
]),
excluded_node_types: None,
min_complexity: None,
min_edge_confidence: 0.0,
}
}
pub fn for_import_graph() -> Self {
Self {
max_depth: Some(10),
max_nodes: Some(1000),
allowed_edge_types: Some(&[EdgeType::Import]),
excluded_node_types: None,
min_complexity: None,
min_edge_confidence: 0.0,
}
}
fn edge_allowed(&self, edge: &Edge) -> bool {
let type_ok = self
.allowed_edge_types
.as_ref()
.map(|types| types.contains(&edge.edge_type))
.unwrap_or(true);
let confidence_ok = edge
.metadata
.confidence
.map(|c| c >= self.min_edge_confidence)
.unwrap_or(true);
type_ok && confidence_ok
}
fn node_should_collect(&self, node: &Node) -> bool {
let type_ok = self
.excluded_node_types
.as_ref()
.map(|excluded| !excluded.contains(&node.node_type))
.unwrap_or(true);
let complexity_ok = self
.min_complexity
.map(|min| node.complexity >= min)
.unwrap_or(true);
type_ok && complexity_ok
}
}
#[derive(Debug, Default, Clone)]
pub struct EmbeddingStore {
pub(crate) embeddings: HashMap<String, Vec<f32>>, }
impl EmbeddingStore {
pub fn new() -> Self {
Self::default()
}
pub fn insert(&mut self, node_id: &str, embedding: Vec<f32>) {
self.embeddings.insert(node_id.to_string(), embedding);
}
pub fn get(&self, node_id: &str) -> Option<&Vec<f32>> {
self.embeddings.get(node_id)
}
pub fn remove(&mut self, node_id: &str) {
self.embeddings.remove(node_id);
}
pub fn len(&self) -> usize {
self.embeddings.len()
}
pub fn is_empty(&self) -> bool {
self.embeddings.is_empty()
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
struct SerializableNode {
index: u32,
node: Node,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
struct SerializableEdge {
source: u32,
target: u32,
edge: Edge,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
struct SerializablePDG {
nodes: Vec<SerializableNode>,
edges: Vec<SerializableEdge>,
symbol_index: HashMap<String, u32>,
file_index: HashMap<String, Vec<u32>>,
#[serde(default)]
name_index: HashMap<String, Vec<u32>>,
#[serde(default)]
name_lower_index: HashMap<String, Vec<u32>>,
#[serde(default)]
embeddings: HashMap<String, Vec<f32>>,
#[serde(default)]
precision_symbols: HashSet<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
struct SerializablePDGWithoutPrecision {
nodes: Vec<SerializableNode>,
edges: Vec<SerializableEdge>,
symbol_index: HashMap<String, u32>,
file_index: HashMap<String, Vec<u32>>,
#[serde(default)]
name_index: HashMap<String, Vec<u32>>,
#[serde(default)]
name_lower_index: HashMap<String, Vec<u32>>,
#[serde(default)]
embeddings: HashMap<String, Vec<f32>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
struct SerializablePDGWithoutEmbeddings {
nodes: Vec<SerializableNode>,
edges: Vec<SerializableEdge>,
symbol_index: HashMap<String, u32>,
file_index: HashMap<String, Vec<u32>>,
#[serde(default)]
name_index: HashMap<String, Vec<u32>>,
#[serde(default)]
name_lower_index: HashMap<String, Vec<u32>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
struct SerializablePDGWithoutEmbeddingsAndNameLower {
nodes: Vec<SerializableNode>,
edges: Vec<SerializableEdge>,
symbol_index: HashMap<String, u32>,
file_index: HashMap<String, Vec<u32>>,
#[serde(default)]
name_index: HashMap<String, Vec<u32>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
struct LegacyEdgeMetadata {
call_count: Option<usize>,
variable_name: Option<String>,
confidence: Option<f32>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
enum LegacyEdgeType {
Call,
DataDependency,
Inheritance,
Import,
Containment,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
struct LegacyEdge {
edge_type: LegacyEdgeType,
metadata: LegacyEdgeMetadata,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
struct LegacyNode {
id: String,
node_type: NodeType,
name: String,
file_path: String,
byte_range: (usize, usize),
complexity: u32,
language: String,
embedding: Option<Vec<f32>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
struct LegacyNodeWithoutEmbedding {
id: String,
node_type: NodeType,
name: String,
file_path: String,
byte_range: (usize, usize),
complexity: u32,
language: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
struct LegacySerializableNode<N> {
index: u32,
node: N,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
struct LegacySerializableEdge {
source: u32,
target: u32,
edge: LegacyEdge,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
struct SerializablePDGWithInlineEmbeddings {
nodes: Vec<LegacySerializableNode<LegacyNode>>,
edges: Vec<LegacySerializableEdge>,
symbol_index: HashMap<String, u32>,
file_index: HashMap<String, Vec<u32>>,
#[serde(default)]
name_index: HashMap<String, Vec<u32>>,
#[serde(default)]
name_lower_index: HashMap<String, Vec<u32>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
struct SerializablePDGWithoutInlineEmbeddings {
nodes: Vec<LegacySerializableNode<LegacyNodeWithoutEmbedding>>,
edges: Vec<LegacySerializableEdge>,
symbol_index: HashMap<String, u32>,
file_index: HashMap<String, Vec<u32>>,
#[serde(default)]
name_index: HashMap<String, Vec<u32>>,
#[serde(default)]
name_lower_index: HashMap<String, Vec<u32>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
struct SerializablePDGWithInlineEmbeddingsWithoutNameLower {
nodes: Vec<LegacySerializableNode<LegacyNode>>,
edges: Vec<LegacySerializableEdge>,
symbol_index: HashMap<String, u32>,
file_index: HashMap<String, Vec<u32>>,
#[serde(default)]
name_index: HashMap<String, Vec<u32>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
struct SerializablePDGWithoutInlineEmbeddingsAndNameLower {
nodes: Vec<LegacySerializableNode<LegacyNodeWithoutEmbedding>>,
edges: Vec<LegacySerializableEdge>,
symbol_index: HashMap<String, u32>,
file_index: HashMap<String, Vec<u32>>,
#[serde(default)]
name_index: HashMap<String, Vec<u32>>,
}
fn convert_legacy_edge(edge: &LegacyEdge) -> Edge {
Edge {
edge_type: match edge.edge_type {
LegacyEdgeType::Call => EdgeType::Call,
LegacyEdgeType::DataDependency => EdgeType::DataDependency,
LegacyEdgeType::Inheritance => EdgeType::Inheritance,
LegacyEdgeType::Import => EdgeType::Import,
LegacyEdgeType::Containment => EdgeType::Containment,
},
metadata: EdgeMetadata {
call_count: edge.metadata.call_count,
variable_name: edge.metadata.variable_name.clone(),
confidence: edge.metadata.confidence,
channel: None,
position: None,
},
}
}
fn convert_legacy_node(node: &LegacyNode) -> Node {
Node {
id: node.id.clone(),
node_type: node.node_type.clone(),
name: node.name.clone(),
file_path: Arc::from(node.file_path.as_str()),
byte_range: node.byte_range,
complexity: node.complexity,
language: node.language.clone(),
}
}
fn convert_legacy_node_without_embedding(node: &LegacyNodeWithoutEmbedding) -> Node {
Node {
id: node.id.clone(),
node_type: node.node_type.clone(),
name: node.name.clone(),
file_path: Arc::from(node.file_path.as_str()),
byte_range: node.byte_range,
complexity: node.complexity,
language: node.language.clone(),
}
}
fn convert_legacy_edges(edges: &[LegacySerializableEdge]) -> Vec<SerializableEdge> {
edges
.iter()
.map(|serialized| SerializableEdge {
source: serialized.source,
target: serialized.target,
edge: convert_legacy_edge(&serialized.edge),
})
.collect()
}
impl SerializablePDGWithInlineEmbeddings {
fn to_pdg(&self) -> Result<ProgramDependenceGraph, String> {
let nodes = self
.nodes
.iter()
.map(|serialized| SerializableNode {
index: serialized.index,
node: convert_legacy_node(&serialized.node),
})
.collect();
let embeddings = self
.nodes
.iter()
.filter_map(|serialized| {
serialized
.node
.embedding
.clone()
.map(|embedding| (serialized.node.id.clone(), embedding))
})
.collect();
SerializablePDG {
nodes,
edges: convert_legacy_edges(&self.edges),
symbol_index: self.symbol_index.clone(),
file_index: self.file_index.clone(),
name_index: self.name_index.clone(),
name_lower_index: self.name_lower_index.clone(),
embeddings,
precision_symbols: HashSet::default(),
}
.to_pdg()
}
}
impl SerializablePDGWithoutInlineEmbeddings {
fn to_pdg(&self) -> Result<ProgramDependenceGraph, String> {
SerializablePDG {
nodes: self
.nodes
.iter()
.map(|serialized| SerializableNode {
index: serialized.index,
node: convert_legacy_node_without_embedding(&serialized.node),
})
.collect(),
edges: convert_legacy_edges(&self.edges),
symbol_index: self.symbol_index.clone(),
file_index: self.file_index.clone(),
name_index: self.name_index.clone(),
name_lower_index: self.name_lower_index.clone(),
embeddings: HashMap::default(),
precision_symbols: HashSet::default(),
}
.to_pdg()
}
}
impl SerializablePDGWithInlineEmbeddingsWithoutNameLower {
fn to_pdg(&self) -> Result<ProgramDependenceGraph, String> {
let nodes = self
.nodes
.iter()
.map(|serialized| SerializableNode {
index: serialized.index,
node: convert_legacy_node(&serialized.node),
})
.collect();
let embeddings = self
.nodes
.iter()
.filter_map(|serialized| {
serialized
.node
.embedding
.clone()
.map(|embedding| (serialized.node.id.clone(), embedding))
})
.collect();
SerializablePDG {
nodes,
edges: convert_legacy_edges(&self.edges),
symbol_index: self.symbol_index.clone(),
file_index: self.file_index.clone(),
name_index: self.name_index.clone(),
name_lower_index: HashMap::default(),
embeddings,
precision_symbols: HashSet::default(),
}
.to_pdg()
}
}
impl SerializablePDGWithoutInlineEmbeddingsAndNameLower {
fn to_pdg(&self) -> Result<ProgramDependenceGraph, String> {
SerializablePDG {
nodes: self
.nodes
.iter()
.map(|serialized| SerializableNode {
index: serialized.index,
node: convert_legacy_node_without_embedding(&serialized.node),
})
.collect(),
edges: convert_legacy_edges(&self.edges),
symbol_index: self.symbol_index.clone(),
file_index: self.file_index.clone(),
name_index: self.name_index.clone(),
name_lower_index: HashMap::default(),
embeddings: HashMap::default(),
precision_symbols: HashSet::default(),
}
.to_pdg()
}
}
#[derive(Serialize)]
struct SerializableNodeRef<'a> {
index: u32,
node: &'a Node,
}
#[derive(Serialize)]
struct SerializableEdgeRef<'a> {
source: u32,
target: u32,
edge: &'a Edge,
}
#[derive(Serialize)]
struct SerializablePDGRef<'a> {
nodes: Vec<SerializableNodeRef<'a>>,
edges: Vec<SerializableEdgeRef<'a>>,
symbol_index: HashMap<&'a str, u32>,
file_index: HashMap<&'a str, Vec<u32>>,
name_index: HashMap<&'a str, Vec<u32>>,
name_lower_index: HashMap<&'a str, Vec<u32>>,
embeddings: HashMap<&'a str, &'a Vec<f32>>,
precision_symbols: &'a HashSet<String>,
}
impl SerializablePDGRef<'_> {
fn from_pdg(pdg: &ProgramDependenceGraph) -> SerializablePDGRef<'_> {
let nodes = pdg
.graph
.node_indices()
.map(|idx| SerializableNodeRef {
index: idx.index() as u32,
node: &pdg.graph[idx],
})
.collect();
let edges = pdg
.graph
.edge_indices()
.map(|eidx| {
let (source, target) = pdg
.graph
.edge_endpoints(eidx)
.expect("Edge endpoints must exist");
SerializableEdgeRef {
source: source.index() as u32,
target: target.index() as u32,
edge: &pdg.graph[eidx],
}
})
.collect();
let symbol_index = pdg
.symbol_index
.iter()
.map(|(k, v)| (k.as_str(), v.index() as u32))
.collect();
let file_index = pdg
.file_index
.iter()
.map(|(k, v)| (k.as_str(), v.iter().map(|id| id.index() as u32).collect()))
.collect();
let name_index = pdg
.name_index
.iter()
.map(|(k, v)| (k.as_str(), v.iter().map(|id| id.index() as u32).collect()))
.collect();
let name_lower_index = pdg
.name_lower_index
.iter()
.map(|(k, v)| (k.as_str(), v.iter().map(|id| id.index() as u32).collect()))
.collect();
let embeddings = pdg
.embedding_store
.embeddings
.iter()
.map(|(k, v)| (k.as_str(), v))
.collect();
SerializablePDGRef {
nodes,
edges,
symbol_index,
file_index,
name_index,
name_lower_index,
embeddings,
precision_symbols: &pdg.precision_symbols,
}
}
}
impl SerializablePDGWithoutPrecision {
fn to_pdg(&self) -> Result<ProgramDependenceGraph, String> {
SerializablePDG {
nodes: self.nodes.clone(),
edges: self.edges.clone(),
symbol_index: self.symbol_index.clone(),
file_index: self.file_index.clone(),
name_index: self.name_index.clone(),
name_lower_index: self.name_lower_index.clone(),
embeddings: self.embeddings.clone(),
precision_symbols: HashSet::default(),
}
.to_pdg()
}
}
impl SerializablePDGWithoutEmbeddings {
fn to_pdg(&self) -> Result<ProgramDependenceGraph, String> {
SerializablePDG {
nodes: self.nodes.clone(),
edges: self.edges.clone(),
symbol_index: self.symbol_index.clone(),
file_index: self.file_index.clone(),
name_index: self.name_index.clone(),
name_lower_index: self.name_lower_index.clone(),
embeddings: HashMap::default(),
precision_symbols: HashSet::default(),
}
.to_pdg()
}
}
impl SerializablePDGWithoutEmbeddingsAndNameLower {
fn to_pdg(&self) -> Result<ProgramDependenceGraph, String> {
SerializablePDG {
nodes: self.nodes.clone(),
edges: self.edges.clone(),
symbol_index: self.symbol_index.clone(),
file_index: self.file_index.clone(),
name_index: self.name_index.clone(),
name_lower_index: HashMap::default(),
embeddings: HashMap::default(),
precision_symbols: HashSet::default(),
}
.to_pdg()
}
}
impl SerializablePDG {
fn to_pdg(&self) -> Result<ProgramDependenceGraph, String> {
let mut pdg = ProgramDependenceGraph::new();
let index_map = self.restore_nodes(&mut pdg);
self.restore_indexes(&mut pdg, &index_map);
self.restore_edges(&mut pdg, &index_map)?;
self.restore_embeddings(&mut pdg);
self.restore_precision_symbols(&mut pdg);
Self::rebuild_name_file_index(&mut pdg);
if pdg.trigram_index.is_empty() {
pdg.rebuild_trigram_index();
}
Ok(pdg)
}
fn restore_nodes(&self, pdg: &mut ProgramDependenceGraph) -> HashMap<u32, NodeId> {
self.nodes
.iter()
.map(|serialized| {
let node_id = pdg.graph.add_node(serialized.node.clone());
(serialized.index, node_id)
})
.collect()
}
fn restore_indexes(&self, pdg: &mut ProgramDependenceGraph, index_map: &HashMap<u32, NodeId>) {
Self::restore_symbol_index(&mut pdg.symbol_index, &self.symbol_index, index_map);
Self::restore_node_index(&mut pdg.file_index, &self.file_index, index_map);
Self::restore_node_index(&mut pdg.name_index, &self.name_index, index_map);
Self::restore_node_index(&mut pdg.name_lower_index, &self.name_lower_index, index_map);
if pdg.name_index.is_empty() {
Self::rebuild_name_indexes(pdg);
}
}
fn restore_symbol_index(
destination: &mut HashMap<String, NodeId>,
source: &HashMap<String, u32>,
index_map: &HashMap<u32, NodeId>,
) {
destination.reserve(source.len());
for (symbol, old_index) in source {
if let Some(&node_id) = index_map.get(old_index) {
destination.insert(symbol.clone(), node_id);
}
}
}
fn restore_node_index(
destination: &mut HashMap<String, Vec<NodeId>>,
source: &HashMap<String, Vec<u32>>,
index_map: &HashMap<u32, NodeId>,
) {
destination.reserve(source.len());
for (name, old_indices) in source {
let node_ids: Vec<NodeId> = old_indices
.iter()
.filter_map(|index| index_map.get(index).copied())
.collect();
if !node_ids.is_empty() {
destination.insert(name.clone(), node_ids);
}
}
}
fn rebuild_name_indexes(pdg: &mut ProgramDependenceGraph) {
let node_count = pdg.graph.node_count();
pdg.name_index.reserve(node_count);
pdg.name_lower_index.reserve(node_count);
for node_id in pdg.graph.node_indices() {
if let Some(node) = pdg.graph.node_weight(node_id) {
pdg.name_index
.entry(node.name.clone())
.or_default()
.push(node_id);
pdg.name_lower_index
.entry(node.name.to_lowercase())
.or_default()
.push(node_id);
}
}
}
fn restore_edges(
&self,
pdg: &mut ProgramDependenceGraph,
index_map: &HashMap<u32, NodeId>,
) -> Result<(), String> {
for serialized in &self.edges {
let source = index_map
.get(&serialized.source)
.ok_or_else(|| format!("Missing source {}", serialized.source))?;
let target = index_map
.get(&serialized.target)
.ok_or_else(|| format!("Missing target {}", serialized.target))?;
pdg.graph
.add_edge(*source, *target, serialized.edge.clone());
}
Ok(())
}
fn restore_embeddings(&self, pdg: &mut ProgramDependenceGraph) {
for (node_id, embedding) in &self.embeddings {
pdg.embedding_store.insert(node_id, embedding.clone());
}
}
fn restore_precision_symbols(&self, pdg: &mut ProgramDependenceGraph) {
let restored = self
.precision_symbols
.iter()
.filter(|symbol| pdg.find_by_symbol(symbol).is_some())
.cloned()
.collect::<Vec<_>>();
pdg.precision_symbols.extend(restored);
}
fn rebuild_name_file_index(pdg: &mut ProgramDependenceGraph) {
for node_id in pdg.graph.node_indices() {
if let Some(node) = pdg.graph.node_weight(node_id) {
pdg.name_file_index
.insert((node.name.clone(), node.file_path.to_string()), node_id);
}
}
}
}
pub struct ProgramDependenceGraph {
pub(crate) graph: StableGraph<Node, Edge>,
pub(crate) symbol_index: HashMap<String, NodeId>,
pub(crate) file_index: HashMap<String, Vec<NodeId>>,
pub(crate) name_index: HashMap<String, Vec<NodeId>>,
pub(crate) name_lower_index: HashMap<String, Vec<NodeId>>,
pub embedding_store: EmbeddingStore,
#[cfg(feature = "community")]
pub communities: HashMap<NodeId, u32>,
precision_symbols: HashSet<String>,
name_file_index: HashMap<(String, String), NodeId>,
trigram_index: TrigramIndex,
revision: u64,
name_corpus: Mutex<Option<(u64, Arc<NameCorpus>)>>,
}
static NEXT_PDG_REVISION: AtomicU64 = AtomicU64::new(1);
fn next_revision() -> u64 {
NEXT_PDG_REVISION.fetch_add(1, AtomicOrdering::Relaxed)
}
#[derive(Debug, Default)]
pub struct NameCorpus {
pub names: Vec<String>,
pub files: Vec<String>,
}
impl Clone for ProgramDependenceGraph {
fn clone(&self) -> Self {
Self {
graph: self.graph.clone(),
symbol_index: self.symbol_index.clone(),
file_index: self.file_index.clone(),
name_index: self.name_index.clone(),
name_lower_index: self.name_lower_index.clone(),
embedding_store: self.embedding_store.clone(),
#[cfg(feature = "community")]
communities: self.communities.clone(),
precision_symbols: self.precision_symbols.clone(),
name_file_index: self.name_file_index.clone(),
trigram_index: self.trigram_index.clone(),
revision: self.revision,
name_corpus: Mutex::new(
self.name_corpus
.lock()
.ok()
.and_then(|cached| cached.clone()),
),
}
}
}
impl ProgramDependenceGraph {
pub fn new() -> Self {
Self {
graph: StableGraph::new(),
symbol_index: HashMap::default(),
file_index: HashMap::default(),
name_index: HashMap::default(),
name_lower_index: HashMap::default(),
embedding_store: EmbeddingStore::new(),
#[cfg(feature = "community")]
communities: HashMap::default(),
precision_symbols: HashSet::default(),
name_file_index: HashMap::default(),
trigram_index: TrigramIndex::new(),
revision: next_revision(),
name_corpus: Mutex::new(None),
}
}
pub fn revision(&self) -> u64 {
self.revision
}
fn touch(&mut self) {
self.revision = next_revision();
}
pub fn name_corpus(&self) -> Arc<NameCorpus> {
let mut cached = match self.name_corpus.lock() {
Ok(guard) => guard,
Err(poisoned) => poisoned.into_inner(),
};
if let Some((revision, corpus)) = cached.as_ref() {
if *revision == self.revision {
return Arc::clone(corpus);
}
}
let mut names: HashSet<String> = HashSet::default();
let mut files: HashSet<String> = HashSet::default();
for node in self.graph.node_weights() {
names.insert(node.name.to_lowercase());
files.insert(node.file_path.to_lowercase());
}
let corpus = Arc::new(NameCorpus {
names: names.into_iter().collect(),
files: files.into_iter().collect(),
});
*cached = Some((self.revision, Arc::clone(&corpus)));
corpus
}
pub fn ensure_file_summary_nodes(&mut self) {
use std::collections::{HashMap, HashSet};
let mut file_lang: HashMap<String, String> = HashMap::default();
let mut have_summary: HashSet<String> = HashSet::default();
for ni in self.node_indices() {
if let Some(n) = self.get_node(ni) {
let fp = n.file_path.to_string();
if matches!(n.node_type, NodeType::FileSummary) {
have_summary.insert(fp);
} else {
file_lang.entry(fp).or_insert_with(|| n.language.clone());
}
}
}
for (fp, lang) in file_lang {
if !have_summary.contains(&fp) {
self.add_node(Node::new_file_summary(&fp, &lang));
}
}
}
pub fn add_node(&mut self, node: Node) -> NodeId {
let name = node.name.clone();
let node_id_str = node.id.clone();
let file_path = Arc::clone(&node.file_path);
let id = self.add_node_without_trigrams(node);
self.trigram_index
.add_node(id, &name, &node_id_str, &file_path);
id
}
pub fn reserve_nodes(&mut self, additional: usize) {
self.graph.reserve_nodes(additional);
self.symbol_index.reserve(additional);
self.name_index.reserve(additional);
self.name_lower_index.reserve(additional);
self.name_file_index.reserve(additional);
}
pub fn add_node_without_trigrams(&mut self, node: Node) -> NodeId {
let symbol = node.id.clone();
let name = node.name.clone();
let lower = name.to_lowercase();
let file_path = Arc::clone(&node.file_path);
let id = self.graph.add_node(node);
self.touch();
self.symbol_index.insert(symbol, id);
match self.file_index.get_mut(&*file_path) {
Some(nodes) => nodes.push(id),
None => {
self.file_index.insert(file_path.to_string(), vec![id]);
}
}
match self.name_index.get_mut(name.as_str()) {
Some(nodes) => nodes.push(id),
None => {
self.name_index.insert(name.clone(), vec![id]);
}
}
match self.name_lower_index.get_mut(lower.as_str()) {
Some(nodes) => nodes.push(id),
None => {
self.name_lower_index.insert(lower, vec![id]);
}
}
self.name_file_index
.insert((name, file_path.to_string()), id);
id
}
pub fn add_edge(&mut self, from: NodeId, to: NodeId, edge: Edge) -> EdgeId {
debug_assert!(
self.graph.contains_node(from) && self.graph.contains_node(to),
"add_edge called with invalid NodeId(s): from={:?} to={:?}",
from,
to
);
self.graph.add_edge(from, to, edge)
}
pub fn remove_node(&mut self, node_id: NodeId) -> Option<Node> {
if let Some(node) = self.graph.remove_node(node_id) {
self.touch();
self.symbol_index.remove(&node.id);
self.precision_symbols.remove(&node.id);
self.embedding_store.remove(&node.id);
let remove_file_entry = if let Some(v) = self.file_index.get_mut(&*node.file_path) {
v.retain(|&id| id != node_id);
v.is_empty()
} else {
false
};
if remove_file_entry {
self.file_index.remove(&*node.file_path);
}
let remove_name_entry = if let Some(v) = self.name_index.get_mut(&node.name) {
v.retain(|&id| id != node_id);
v.is_empty()
} else {
false
};
if remove_name_entry {
self.name_index.remove(&node.name);
}
let lower_name = node.name.to_lowercase();
let remove_lower_name_entry =
if let Some(v) = self.name_lower_index.get_mut(&lower_name) {
v.retain(|&id| id != node_id);
v.is_empty()
} else {
false
};
if remove_lower_name_entry {
self.name_lower_index.remove(&lower_name);
}
self.name_file_index
.remove(&(node.name.clone(), node.file_path.to_string()));
self.trigram_index
.remove_node(node_id, &node.name, &node.id, &node.file_path);
Some(node)
} else {
None
}
}
pub fn remove_edge(&mut self, id: EdgeId) -> Option<Edge> {
self.graph.remove_edge(id)
}
pub fn remove_file(&mut self, file_path: &str) {
let ids = self.nodes_in_file(file_path);
for id in ids {
self.remove_node(id);
}
self.file_index.remove(file_path);
}
pub fn get_node(&self, id: NodeId) -> Option<&Node> {
self.graph.node_weight(id)
}
pub fn get_node_mut(&mut self, id: NodeId) -> Option<&mut Node> {
self.touch();
self.graph.node_weight_mut(id)
}
pub fn repath_node(&mut self, node_id: NodeId, new_path: &str) -> bool {
self.repath_nodes_to_file_path(&[node_id], new_path) > 0
}
pub fn repath_nodes_to_file_path(&mut self, node_ids: &[NodeId], new_path: &str) -> usize {
let mut moves: Vec<(NodeId, String, String, String)> = Vec::with_capacity(node_ids.len()); for &node_id in node_ids {
let already_there = self
.graph
.node_weight(node_id)
.is_some_and(|node| node.file_path.as_ref() == new_path);
if already_there {
Self::repair_forward_file_entry(&mut self.file_index, node_id, new_path);
continue;
}
let Some(node) = self.graph.node_weight_mut(node_id) else {
continue;
};
let name = node.name.clone();
let node_id_str = node.id.clone();
let old_path = std::sync::Arc::clone(&node.file_path);
node.file_path = std::sync::Arc::from(new_path);
moves.push((node_id, name, node_id_str, old_path.to_string()));
}
if moves.is_empty() {
return 0;
}
self.touch();
for (node_id, name, _node_id_str, old_path) in &moves {
if let Some(ids) = self.file_index.get_mut(old_path.as_str()) {
ids.retain(|&id| id != *node_id);
if ids.is_empty() {
self.file_index.remove(old_path.as_str());
}
}
Self::repair_forward_file_entry(&mut self.file_index, *node_id, new_path);
let old_key = (name.clone(), old_path.clone());
if self.name_file_index.get(&old_key) == Some(node_id) {
self.name_file_index.remove(&old_key);
}
self.name_file_index
.insert((name.clone(), new_path.to_string()), *node_id);
}
self.trigram_index.remove_nodes(&moves);
for (node_id, name, node_id_str, old_path) in &moves {
let _ = old_path;
self.trigram_index
.add_node(*node_id, name, node_id_str, new_path);
}
moves.len()
}
fn repair_forward_file_entry(
file_index: &mut HashMap<String, Vec<NodeId>>,
node_id: NodeId,
new_path: &str,
) {
match file_index.get_mut(new_path) {
Some(ids) => {
if !ids.contains(&node_id) {
ids.push(node_id);
}
}
None => {
file_index.insert(new_path.to_string(), vec![node_id]);
}
}
}
pub fn node_weights_mut(&mut self) -> impl Iterator<Item = &mut Node> {
self.touch();
self.graph.node_weights_mut()
}
pub fn get_edge(&self, id: EdgeId) -> Option<&Edge> {
self.graph.edge_weight(id)
}
pub fn get_edge_mut(&mut self, id: EdgeId) -> Option<&mut Edge> {
self.graph.edge_weight_mut(id)
}
pub fn mark_precision_symbol(&mut self, node_id: impl Into<String>) {
self.precision_symbols.insert(node_id.into());
}
pub fn is_precision_symbol(&self, node_id: &str) -> bool {
self.precision_symbols.contains(node_id)
}
pub fn precision_symbols(&self) -> &HashSet<String> {
&self.precision_symbols
}
pub fn retain_precision_symbols<F: FnMut(&String) -> bool>(&mut self, keep: F) {
self.precision_symbols.retain(keep);
}
pub fn node_count(&self) -> usize {
self.graph.node_count()
}
pub fn edge_count(&self) -> usize {
self.graph.edge_count()
}
pub fn file_count(&self) -> usize {
self.file_index
.values()
.filter(|node_ids| !node_ids.is_empty())
.count()
}
pub fn node_indices(&self) -> impl Iterator<Item = NodeId> + '_ {
self.graph.node_indices()
}
pub fn edge_indices(&self) -> impl Iterator<Item = EdgeId> + '_ {
self.graph.edge_indices()
}
pub fn edge_endpoints(&self, edge_id: EdgeId) -> Option<(NodeId, NodeId)> {
self.graph.edge_endpoints(edge_id)
}
pub fn neighbors(&self, node_id: NodeId) -> Vec<NodeId> {
self.graph.neighbors(node_id).collect()
}
pub fn predecessors(&self, node_id: NodeId) -> Vec<NodeId> {
use petgraph::Direction;
self.graph
.neighbors_directed(node_id, Direction::Incoming)
.collect()
}
pub fn neighbors_by_edge_type(
&self,
node_id: NodeId,
edge_type: EdgeType,
direction: petgraph::Direction,
) -> Vec<NodeId> {
use petgraph::visit::EdgeRef;
let mut seen = HashSet::default();
self.graph
.edges_directed(node_id, direction)
.filter(|edge| edge.weight().edge_type == edge_type)
.map(|edge| match direction {
petgraph::Direction::Outgoing => edge.target(),
petgraph::Direction::Incoming => edge.source(),
})
.filter(|id| seen.insert(*id))
.collect()
}
pub fn predecessor_count(&self, node_id: NodeId) -> usize {
use petgraph::Direction;
self.graph
.neighbors_directed(node_id, Direction::Incoming)
.count()
}
pub fn trigram_index(&self) -> &TrigramIndex {
&self.trigram_index
}
pub fn rebuild_trigram_index(&mut self) {
self.trigram_index = TrigramIndex::build_from_pdg(self);
}
pub fn set_trigram_index(&mut self, index: TrigramIndex) {
self.trigram_index = index;
}
}
fn deserialize_schema<T>(data: &[u8], errors: &mut Vec<String>) -> Option<ProgramDependenceGraph>
where
T: DeserializeOwned,
T: IntoPdg,
{
match bincode::DefaultOptions::new()
.with_fixint_encoding()
.reject_trailing_bytes()
.deserialize::<T>(data)
{
Ok(serialized) => match serialized.to_pdg() {
Ok(pdg) => Some(pdg),
Err(error) => {
errors.push(error);
None
}
},
Err(error) => {
errors.push(error.to_string());
None
}
}
}
trait IntoPdg {
fn to_pdg(self) -> Result<ProgramDependenceGraph, String>;
}
impl IntoPdg for SerializablePDG {
fn to_pdg(self) -> Result<ProgramDependenceGraph, String> {
SerializablePDG::to_pdg(&self)
}
}
impl IntoPdg for SerializablePDGWithoutPrecision {
fn to_pdg(self) -> Result<ProgramDependenceGraph, String> {
SerializablePDGWithoutPrecision::to_pdg(&self)
}
}
impl IntoPdg for SerializablePDGWithoutEmbeddings {
fn to_pdg(self) -> Result<ProgramDependenceGraph, String> {
SerializablePDGWithoutEmbeddings::to_pdg(&self)
}
}
impl IntoPdg for SerializablePDGWithoutEmbeddingsAndNameLower {
fn to_pdg(self) -> Result<ProgramDependenceGraph, String> {
SerializablePDGWithoutEmbeddingsAndNameLower::to_pdg(&self)
}
}
impl IntoPdg for SerializablePDGWithInlineEmbeddings {
fn to_pdg(self) -> Result<ProgramDependenceGraph, String> {
SerializablePDGWithInlineEmbeddings::to_pdg(&self)
}
}
impl IntoPdg for SerializablePDGWithoutInlineEmbeddings {
fn to_pdg(self) -> Result<ProgramDependenceGraph, String> {
SerializablePDGWithoutInlineEmbeddings::to_pdg(&self)
}
}
impl IntoPdg for SerializablePDGWithInlineEmbeddingsWithoutNameLower {
fn to_pdg(self) -> Result<ProgramDependenceGraph, String> {
SerializablePDGWithInlineEmbeddingsWithoutNameLower::to_pdg(&self)
}
}
impl IntoPdg for SerializablePDGWithoutInlineEmbeddingsAndNameLower {
fn to_pdg(self) -> Result<ProgramDependenceGraph, String> {
SerializablePDGWithoutInlineEmbeddingsAndNameLower::to_pdg(&self)
}
}
impl Default for ProgramDependenceGraph {
fn default() -> Self {
Self::new()
}
}
enum Direction {
Forward,
Backward,
}
#[cfg(test)]
#[path = "pdg_test.rs"]
mod tests;