use std::collections::HashMap;
use crate::confidence::ConfidenceMetadata;
use crate::graph::unified::bind::alias::AliasTable;
use crate::graph::unified::bind::scope::ScopeArena;
use crate::graph::unified::bind::scope::provenance::ScopeProvenanceStore;
use crate::graph::unified::bind::shadow::ShadowTable;
use crate::graph::unified::build::staging::GoHints;
use crate::graph::unified::edge::EdgeId;
use crate::graph::unified::edge::bidirectional::BidirectionalEdgeStore;
use crate::graph::unified::node::NodeId;
use crate::graph::unified::storage::arena::NodeArena;
use crate::graph::unified::storage::edge_provenance::EdgeProvenanceStore;
use crate::graph::unified::storage::indices::AuxiliaryIndices;
use crate::graph::unified::storage::interner::StringInterner;
use crate::graph::unified::storage::metadata::NodeMetadataStore;
use crate::graph::unified::storage::node_provenance::NodeProvenanceStore;
use crate::graph::unified::storage::registry::FileRegistry;
use crate::graph::unified::storage::segment::FileSegmentTable;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Receiver {
Value,
Pointer,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct CallsEdgeMeta {
pub argument_count: u8,
pub is_async: bool,
}
#[expect(
dead_code,
reason = "Phase 2 migration surface: `macro_metadata_mut`, the three \
provenance `_mut`s, every binding-plane `_mut`, scalar \
`_mut`s, and `strings_mut` are Phase 3 (real incremental \
rebuild body) coverage. Remove this `expect` in the Phase 3 \
landing PR once they are wired up."
)]
pub(crate) trait GraphMutationTarget {
fn nodes(&self) -> &NodeArena;
fn edges(&self) -> &BidirectionalEdgeStore;
fn strings(&self) -> &StringInterner;
fn files(&self) -> &FileRegistry;
fn indices(&self) -> &AuxiliaryIndices;
fn scope_arena(&self) -> &ScopeArena;
fn fact_epoch(&self) -> u64;
fn nodes_mut(&mut self) -> &mut NodeArena;
fn edges_mut(&mut self) -> &mut BidirectionalEdgeStore;
fn strings_mut(&mut self) -> &mut StringInterner;
fn files_mut(&mut self) -> &mut FileRegistry;
fn indices_mut(&mut self) -> &mut AuxiliaryIndices;
fn macro_metadata_mut(&mut self) -> &mut NodeMetadataStore;
fn node_provenance_mut(&mut self) -> &mut NodeProvenanceStore;
fn edge_provenance_mut(&mut self) -> &mut EdgeProvenanceStore;
fn file_segments_mut(&mut self) -> &mut FileSegmentTable;
fn scope_arena_mut(&mut self) -> &mut ScopeArena;
fn alias_table_mut(&mut self) -> &mut AliasTable;
fn shadow_table_mut(&mut self) -> &mut ShadowTable;
fn scope_provenance_store_mut(&mut self) -> &mut ScopeProvenanceStore;
fn set_scope_arena(&mut self, arena: ScopeArena);
fn set_alias_table(&mut self, table: AliasTable);
fn set_shadow_table(&mut self, table: ShadowTable);
fn set_scope_provenance_store(&mut self, store: ScopeProvenanceStore);
fn fact_epoch_mut(&mut self) -> &mut u64;
fn epoch_mut(&mut self) -> &mut u64;
fn confidence_mut(&mut self) -> &mut HashMap<String, ConfidenceMetadata>;
fn nodes_and_strings_mut(&mut self) -> (&mut NodeArena, &mut StringInterner);
fn nodes_and_indices_mut(&mut self) -> (&NodeArena, &mut AuxiliaryIndices);
fn rebuild_qualified_name_index_for_new_nodes(&mut self, new_nodes: &[NodeId]);
fn calls_into(&self, callee: NodeId) -> Vec<(NodeId, EdgeId, CallsEdgeMeta)>;
fn go_hints(&self) -> &GoHints;
fn go_hints_mut(&mut self) -> &mut GoHints;
}
use crate::graph::unified::concurrent::CodeGraph;
use std::sync::Arc;
impl GraphMutationTarget for CodeGraph {
fn nodes(&self) -> &NodeArena {
CodeGraph::nodes(self)
}
fn edges(&self) -> &BidirectionalEdgeStore {
CodeGraph::edges(self)
}
fn strings(&self) -> &StringInterner {
CodeGraph::strings(self)
}
fn files(&self) -> &FileRegistry {
CodeGraph::files(self)
}
fn indices(&self) -> &AuxiliaryIndices {
CodeGraph::indices(self)
}
fn scope_arena(&self) -> &ScopeArena {
CodeGraph::scope_arena(self)
}
fn fact_epoch(&self) -> u64 {
CodeGraph::fact_epoch(self)
}
fn nodes_mut(&mut self) -> &mut NodeArena {
CodeGraph::nodes_mut(self)
}
fn edges_mut(&mut self) -> &mut BidirectionalEdgeStore {
CodeGraph::edges_mut(self)
}
fn strings_mut(&mut self) -> &mut StringInterner {
CodeGraph::strings_mut(self)
}
fn files_mut(&mut self) -> &mut FileRegistry {
CodeGraph::files_mut(self)
}
fn indices_mut(&mut self) -> &mut AuxiliaryIndices {
CodeGraph::indices_mut(self)
}
fn macro_metadata_mut(&mut self) -> &mut NodeMetadataStore {
CodeGraph::macro_metadata_mut(self)
}
fn node_provenance_mut(&mut self) -> &mut NodeProvenanceStore {
Arc::make_mut(&mut self.node_provenance)
}
fn edge_provenance_mut(&mut self) -> &mut EdgeProvenanceStore {
Arc::make_mut(&mut self.edge_provenance)
}
fn file_segments_mut(&mut self) -> &mut FileSegmentTable {
CodeGraph::file_segments_mut(self)
}
fn scope_arena_mut(&mut self) -> &mut ScopeArena {
Arc::make_mut(&mut self.scope_arena)
}
fn alias_table_mut(&mut self) -> &mut AliasTable {
Arc::make_mut(&mut self.alias_table)
}
fn shadow_table_mut(&mut self) -> &mut ShadowTable {
Arc::make_mut(&mut self.shadow_table)
}
fn scope_provenance_store_mut(&mut self) -> &mut ScopeProvenanceStore {
Arc::make_mut(&mut self.scope_provenance_store)
}
fn set_scope_arena(&mut self, arena: ScopeArena) {
CodeGraph::set_scope_arena(self, arena);
}
fn set_alias_table(&mut self, table: AliasTable) {
CodeGraph::set_alias_table(self, table);
}
fn set_shadow_table(&mut self, table: ShadowTable) {
CodeGraph::set_shadow_table(self, table);
}
fn set_scope_provenance_store(&mut self, store: ScopeProvenanceStore) {
CodeGraph::set_scope_provenance_store(self, store);
}
fn fact_epoch_mut(&mut self) -> &mut u64 {
&mut self.fact_epoch
}
fn epoch_mut(&mut self) -> &mut u64 {
&mut self.epoch
}
fn confidence_mut(&mut self) -> &mut HashMap<String, ConfidenceMetadata> {
&mut self.confidence
}
fn nodes_and_strings_mut(&mut self) -> (&mut NodeArena, &mut StringInterner) {
CodeGraph::nodes_and_strings_mut(self)
}
fn nodes_and_indices_mut(&mut self) -> (&NodeArena, &mut AuxiliaryIndices) {
let Self { nodes, indices, .. } = self;
(&**nodes, Arc::make_mut(indices))
}
fn rebuild_qualified_name_index_for_new_nodes(&mut self, new_nodes: &[NodeId]) {
if new_nodes.is_empty() {
return;
}
let mut tuples: Vec<(NodeId, _, _, Option<_>, _)> = Vec::with_capacity(new_nodes.len());
{
let arena: &NodeArena = self.nodes();
for nid in new_nodes {
if let Some(entry) = arena.get(*nid) {
tuples.push((
*nid,
entry.kind,
entry.name,
entry.qualified_name,
entry.file,
));
}
}
}
let indices = self.indices_mut();
for (nid, kind, name, qualified_name, file) in tuples {
indices.add(nid, kind, name, qualified_name, file);
}
}
fn calls_into(&self, callee: NodeId) -> Vec<(NodeId, EdgeId, CallsEdgeMeta)> {
use crate::graph::unified::edge::EdgeKind;
let edges_view = self.edges();
edges_view
.edges_to(callee)
.into_iter()
.filter_map(|edge_ref| {
if let EdgeKind::Calls {
argument_count,
is_async,
..
} = edge_ref.kind
{
let edge_id = EdgeId::new(u32::try_from(edge_ref.seq).unwrap_or(u32::MAX));
Some((
edge_ref.source,
edge_id,
CallsEdgeMeta {
argument_count,
is_async,
},
))
} else {
None
}
})
.collect()
}
fn go_hints(&self) -> &GoHints {
&self.go_hints
}
fn go_hints_mut(&mut self) -> &mut GoHints {
&mut self.go_hints
}
}
use crate::graph::unified::rebuild::rebuild_graph::RebuildGraph;
impl GraphMutationTarget for RebuildGraph {
fn nodes(&self) -> &NodeArena {
&self.nodes
}
fn edges(&self) -> &BidirectionalEdgeStore {
&self.edges
}
fn strings(&self) -> &StringInterner {
&self.strings
}
fn files(&self) -> &FileRegistry {
&self.files
}
fn indices(&self) -> &AuxiliaryIndices {
&self.indices
}
fn scope_arena(&self) -> &ScopeArena {
&self.scope_arena
}
fn fact_epoch(&self) -> u64 {
self.fact_epoch
}
fn nodes_mut(&mut self) -> &mut NodeArena {
&mut self.nodes
}
fn edges_mut(&mut self) -> &mut BidirectionalEdgeStore {
&mut self.edges
}
fn strings_mut(&mut self) -> &mut StringInterner {
&mut self.strings
}
fn files_mut(&mut self) -> &mut FileRegistry {
&mut self.files
}
fn indices_mut(&mut self) -> &mut AuxiliaryIndices {
&mut self.indices
}
fn macro_metadata_mut(&mut self) -> &mut NodeMetadataStore {
&mut self.macro_metadata
}
fn node_provenance_mut(&mut self) -> &mut NodeProvenanceStore {
&mut self.node_provenance
}
fn edge_provenance_mut(&mut self) -> &mut EdgeProvenanceStore {
&mut self.edge_provenance
}
fn file_segments_mut(&mut self) -> &mut FileSegmentTable {
&mut self.file_segments
}
fn scope_arena_mut(&mut self) -> &mut ScopeArena {
&mut self.scope_arena
}
fn alias_table_mut(&mut self) -> &mut AliasTable {
&mut self.alias_table
}
fn shadow_table_mut(&mut self) -> &mut ShadowTable {
&mut self.shadow_table
}
fn scope_provenance_store_mut(&mut self) -> &mut ScopeProvenanceStore {
&mut self.scope_provenance_store
}
fn set_scope_arena(&mut self, arena: ScopeArena) {
self.scope_arena = arena;
}
fn set_alias_table(&mut self, table: AliasTable) {
self.alias_table = table;
}
fn set_shadow_table(&mut self, table: ShadowTable) {
self.shadow_table = table;
}
fn set_scope_provenance_store(&mut self, store: ScopeProvenanceStore) {
self.scope_provenance_store = store;
}
fn fact_epoch_mut(&mut self) -> &mut u64 {
&mut self.fact_epoch
}
fn epoch_mut(&mut self) -> &mut u64 {
&mut self.epoch
}
fn confidence_mut(&mut self) -> &mut HashMap<String, ConfidenceMetadata> {
&mut self.confidence
}
fn nodes_and_strings_mut(&mut self) -> (&mut NodeArena, &mut StringInterner) {
let Self { nodes, strings, .. } = self;
(nodes, strings)
}
fn nodes_and_indices_mut(&mut self) -> (&NodeArena, &mut AuxiliaryIndices) {
let Self { nodes, indices, .. } = self;
(nodes, indices)
}
fn rebuild_qualified_name_index_for_new_nodes(&mut self, new_nodes: &[NodeId]) {
if new_nodes.is_empty() {
return;
}
let mut tuples: Vec<(NodeId, _, _, Option<_>, _)> = Vec::with_capacity(new_nodes.len());
for nid in new_nodes {
if let Some(entry) = self.nodes.get(*nid) {
tuples.push((
*nid,
entry.kind,
entry.name,
entry.qualified_name,
entry.file,
));
}
}
for (nid, kind, name, qualified_name, file) in tuples {
self.indices.add(nid, kind, name, qualified_name, file);
}
}
fn calls_into(&self, callee: NodeId) -> Vec<(NodeId, EdgeId, CallsEdgeMeta)> {
use crate::graph::unified::edge::EdgeKind;
self.edges
.edges_to(callee)
.into_iter()
.filter_map(|edge_ref| {
if let EdgeKind::Calls {
argument_count,
is_async,
..
} = edge_ref.kind
{
let edge_id = EdgeId::new(u32::try_from(edge_ref.seq).unwrap_or(u32::MAX));
Some((
edge_ref.source,
edge_id,
CallsEdgeMeta {
argument_count,
is_async,
},
))
} else {
None
}
})
.collect()
}
fn go_hints(&self) -> &GoHints {
&self.go_hints
}
fn go_hints_mut(&mut self) -> &mut GoHints {
&mut self.go_hints
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::graph::unified::concurrent::CodeGraph;
use crate::graph::unified::edge::{EdgeKind, ResolvedVia};
use crate::graph::unified::file::FileId;
use crate::graph::unified::node::NodeId;
use crate::graph::unified::node::kind::NodeKind;
use crate::graph::unified::storage::NodeEntry;
fn make_node(graph: &mut CodeGraph, kind: NodeKind, name: &str, qn: &str) -> NodeId {
let name_id = graph.strings_mut().intern(name).expect("intern name");
let qn_id = graph
.strings_mut()
.intern(qn)
.expect("intern qualified name");
let file = FileId::new(0);
let entry = NodeEntry {
kind,
name: name_id,
file,
start_byte: 0,
end_byte: 0,
start_line: 0,
start_column: 0,
end_line: 0,
end_column: 0,
signature: None,
doc: None,
qualified_name: Some(qn_id),
visibility: None,
is_async: false,
is_static: false,
body_hash: None,
is_unsafe: false,
};
graph.nodes_mut().alloc(entry).expect("alloc node")
}
#[test]
fn rebuild_qualified_name_index_for_new_nodes_inserts_into_live_index() {
let mut graph = CodeGraph::new();
let nid = make_node(&mut graph, NodeKind::Method, "M", "pkg.S.M");
let qn_id = graph.strings().get("pkg.S.M").expect("qn must be interned");
assert!(
graph.indices().by_qualified_name(qn_id).is_empty(),
"qualified-name bucket must be empty before the targeted update",
);
<CodeGraph as GraphMutationTarget>::rebuild_qualified_name_index_for_new_nodes(
&mut graph,
&[nid],
);
let bucket = graph.indices().by_qualified_name(qn_id);
assert_eq!(
bucket,
&[nid],
"the inserted node must resolve through the by-qualified-name index",
);
}
#[test]
fn calls_into_returns_calls_edges_with_metadata() {
let mut graph = CodeGraph::new();
let callee = make_node(&mut graph, NodeKind::Function, "callee", "pkg.callee");
let caller_a = make_node(&mut graph, NodeKind::Function, "caller_a", "pkg.caller_a");
let caller_b = make_node(&mut graph, NodeKind::Function, "caller_b", "pkg.caller_b");
let unrelated = make_node(&mut graph, NodeKind::Function, "noise", "pkg.noise");
let file = FileId::new(0);
graph.edges().add_edge(
caller_a,
callee,
EdgeKind::Calls {
argument_count: 2,
is_async: false,
resolved_via: ResolvedVia::Direct,
},
file,
);
graph.edges().add_edge(
caller_b,
callee,
EdgeKind::Calls {
argument_count: 0,
is_async: true,
resolved_via: ResolvedVia::Direct,
},
file,
);
graph
.edges()
.add_edge(unrelated, callee, EdgeKind::References, file);
let mut got = <CodeGraph as GraphMutationTarget>::calls_into(&graph, callee);
got.sort_by_key(|(src, _, _)| src.index());
let calls_only: Vec<(NodeId, CallsEdgeMeta)> =
got.iter().map(|(s, _, m)| (*s, *m)).collect();
let mut expected = vec![
(
caller_a,
CallsEdgeMeta {
argument_count: 2,
is_async: false,
},
),
(
caller_b,
CallsEdgeMeta {
argument_count: 0,
is_async: true,
},
),
];
expected.sort_by_key(|(src, _)| src.index());
assert_eq!(calls_only, expected);
assert_ne!(
got[0].1, got[1].1,
"EdgeId projection must be injective within the returned set"
);
assert!(
!got.iter().any(|(src, _, _)| *src == unrelated),
"`calls_into` must filter out non-Calls edges (got References from {unrelated:?})",
);
}
#[test]
fn go_hints_accessor_pair_round_trips() {
let mut graph = CodeGraph::new();
assert!(
<CodeGraph as GraphMutationTarget>::go_hints(&graph)
.embeddings
.is_empty(),
"fresh CodeGraph must start with empty GoHints",
);
let hints = <CodeGraph as GraphMutationTarget>::go_hints_mut(&mut graph);
hints
.embeddings
.push(crate::graph::unified::build::staging::GoEmbeddingHint {
outer: NodeId::new(0, 1),
inner_qualified_name: crate::graph::unified::StringId::new(0),
pointerness: Receiver::Value,
file: FileId::new(0),
});
assert_eq!(
<CodeGraph as GraphMutationTarget>::go_hints(&graph)
.embeddings
.len(),
1,
);
}
}