use std::collections::HashMap;
use std::ops::Range;
use std::sync::Arc;
use rayon::prelude::*;
use crate::graph::unified::edge::delta::{DeltaEdge, DeltaOp};
use crate::graph::unified::edge::kind::{EdgeKind, MqProtocol};
use crate::graph::unified::file::FileId;
use crate::graph::unified::node::NodeId;
use crate::graph::unified::storage::NodeArena;
use crate::graph::unified::storage::arena::{NodeEntry, Slot};
use crate::graph::unified::string::StringId;
use super::pass3_intra::PendingEdge;
use super::staging::{StagingGraph, StagingOp};
#[derive(Debug, Clone, Default)]
pub struct GlobalOffsets {
pub node_offset: u32,
pub string_offset: u32,
}
#[derive(Debug, Clone)]
pub struct FilePlan {
pub parsed_index: usize,
pub file_id: FileId,
pub node_range: Range<u32>,
pub string_range: Range<u32>,
}
#[derive(Debug, Clone)]
pub struct ChunkCommitPlan {
pub file_plans: Vec<FilePlan>,
pub total_nodes: u32,
pub total_strings: u32,
pub total_edges: u64,
}
#[must_use]
pub fn compute_commit_plan(
node_counts: &[u32],
string_counts: &[u32],
edge_counts: &[u32],
file_ids: &[FileId],
node_offset: u32,
string_offset: u32,
) -> ChunkCommitPlan {
debug_assert_eq!(node_counts.len(), string_counts.len());
debug_assert_eq!(node_counts.len(), edge_counts.len());
debug_assert_eq!(node_counts.len(), file_ids.len());
let mut plans = Vec::with_capacity(node_counts.len());
let mut node_cursor = node_offset;
let mut string_cursor = string_offset;
let mut total_edges: u64 = 0;
for i in 0..node_counts.len() {
let nc = node_counts[i];
let sc = string_counts[i];
let node_end = node_cursor
.checked_add(nc)
.expect("node ID space overflow in commit plan");
let string_end = string_cursor
.checked_add(sc)
.expect("string ID space overflow in commit plan");
plans.push(FilePlan {
parsed_index: i,
file_id: file_ids[i],
node_range: node_cursor..node_end,
string_range: string_cursor..string_end,
});
node_cursor = node_end;
string_cursor = string_end;
total_edges += u64::from(edge_counts[i]);
}
ChunkCommitPlan {
file_plans: plans,
total_nodes: node_cursor - node_offset,
total_strings: string_cursor - string_offset,
total_edges,
}
}
#[must_use]
pub fn phase2_assign_ranges(
staging_graphs: &[&StagingGraph],
file_ids: &[FileId],
offsets: &GlobalOffsets,
) -> ChunkCommitPlan {
let node_counts: Vec<u32> = staging_graphs
.iter()
.map(|sg| sg.node_count_u32())
.collect();
let string_counts: Vec<u32> = staging_graphs
.iter()
.map(|sg| sg.string_count_u32())
.collect();
let edge_counts: Vec<u32> = staging_graphs
.iter()
.map(|sg| sg.edge_count_u32())
.collect();
compute_commit_plan(
&node_counts,
&string_counts,
&edge_counts,
file_ids,
offsets.node_offset,
offsets.string_offset,
)
}
pub struct Phase3Result {
pub per_file_edges: Vec<Vec<PendingEdge>>,
pub per_file_node_ids: Vec<Vec<NodeId>>,
pub total_nodes_written: usize,
pub total_strings_written: usize,
pub total_edges_collected: usize,
}
#[must_use]
pub(crate) fn phase3_parallel_commit<
G: crate::graph::unified::mutation_target::GraphMutationTarget,
>(
plan: &ChunkCommitPlan,
staging_graphs: &[&StagingGraph],
graph: &mut G,
) -> Phase3Result {
if plan.file_plans.is_empty() {
return Phase3Result {
per_file_edges: Vec::new(),
per_file_node_ids: Vec::new(),
total_nodes_written: 0,
total_strings_written: 0,
total_edges_collected: 0,
};
}
let node_start = plan.file_plans[0].node_range.start;
let string_start = plan.file_plans[0].string_range.start;
let (arena, interner) = graph.nodes_and_strings_mut();
let node_slice = arena.bulk_slice_mut(node_start, plan.total_nodes);
let (str_slice, rc_slice) = interner.bulk_slices_mut(string_start, plan.total_strings);
let mut node_remaining = &mut *node_slice;
let mut str_remaining = &mut *str_slice;
let mut rc_remaining = &mut *rc_slice;
#[allow(clippy::type_complexity)]
let mut file_work: Vec<(
&mut [Slot<NodeEntry>],
&mut [Option<Arc<str>>],
&mut [u32],
&FilePlan,
usize,
)> = Vec::with_capacity(plan.file_plans.len());
for (i, file_plan) in plan.file_plans.iter().enumerate() {
let nc = (file_plan.node_range.end - file_plan.node_range.start) as usize;
let sc = (file_plan.string_range.end - file_plan.string_range.start) as usize;
let (n, nr) = node_remaining.split_at_mut(nc);
let (s, sr) = str_remaining.split_at_mut(sc);
let (r, rr) = rc_remaining.split_at_mut(sc);
file_work.push((n, s, r, file_plan, i));
node_remaining = nr;
str_remaining = sr;
rc_remaining = rr;
}
let results: Vec<FileCommitResult> = file_work
.into_par_iter()
.map(|(node_slots, str_slots, rc_slots, file_plan, idx)| {
commit_single_file(
staging_graphs[idx],
file_plan,
node_slots,
str_slots,
rc_slots,
)
})
.collect();
let total_nodes_written: usize = results.iter().map(|r| r.nodes_written).sum();
let total_strings_written: usize = results.iter().map(|r| r.strings_written).sum();
let total_edges_collected: usize = results.iter().map(|r| r.edges.len()).sum();
let mut per_file_edges = Vec::with_capacity(results.len());
let mut per_file_node_ids = Vec::with_capacity(results.len());
for r in results {
per_file_edges.push(r.edges);
per_file_node_ids.push(r.node_ids);
}
Phase3Result {
per_file_edges,
per_file_node_ids,
total_nodes_written,
total_strings_written,
total_edges_collected,
}
}
struct FileCommitResult {
edges: Vec<PendingEdge>,
node_ids: Vec<NodeId>,
nodes_written: usize,
strings_written: usize,
}
fn commit_single_file(
staging: &StagingGraph,
plan: &FilePlan,
node_slots: &mut [Slot<NodeEntry>],
str_slots: &mut [Option<Arc<str>>],
rc_slots: &mut [u32],
) -> FileCommitResult {
let ops = staging.operations();
let (string_remap, strings_written) = write_strings(ops, plan, str_slots, rc_slots);
let (node_remap, nodes_written, node_ids) = write_nodes(ops, plan, node_slots, &string_remap);
let edges = collect_edges(ops, plan, &node_remap, &string_remap);
FileCommitResult {
edges,
node_ids,
nodes_written,
strings_written,
}
}
fn write_strings(
ops: &[StagingOp],
plan: &FilePlan,
str_slots: &mut [Option<Arc<str>>],
rc_slots: &mut [u32],
) -> (HashMap<StringId, StringId>, usize) {
let mut remap = HashMap::new();
let mut string_cursor = 0usize;
for op in ops {
if let StagingOp::InternString { local_id, value } = op {
assert!(
local_id.is_local(),
"non-local StringId {:?} in InternString op for file {:?}",
local_id,
plan.file_id,
);
assert!(
!remap.contains_key(local_id),
"duplicate local StringId {:?} in InternString op for file {:?}",
local_id,
plan.file_id,
);
if string_cursor >= str_slots.len() {
log::warn!(
"string slot overflow in file {:?}: cursor={string_cursor}, slots={}, skipping remaining strings",
plan.file_id,
str_slots.len()
);
break;
}
#[allow(clippy::cast_possible_truncation)] let global_id = StringId::new(plan.string_range.start + string_cursor as u32);
str_slots[string_cursor] = Some(Arc::from(value.as_str()));
rc_slots[string_cursor] = 1;
remap.insert(*local_id, global_id);
string_cursor += 1;
}
}
(remap, string_cursor)
}
fn remap_node_entry_string_ids(entry: &mut NodeEntry, remap: &HashMap<StringId, StringId>) {
remap_required_local(&mut entry.name, remap);
remap_option_local(&mut entry.signature, remap);
remap_option_local(&mut entry.doc, remap);
remap_option_local(&mut entry.qualified_name, remap);
remap_option_local(&mut entry.visibility, remap);
}
#[allow(clippy::match_same_arms)]
fn remap_edge_kind_local_string_ids(kind: &mut EdgeKind, remap: &HashMap<StringId, StringId>) {
match kind {
EdgeKind::Imports { alias, .. } => remap_option_local(alias, remap),
EdgeKind::Exports { alias, .. } => remap_option_local(alias, remap),
EdgeKind::TypeOf { name, .. } => remap_option_local(name, remap),
EdgeKind::TraitMethodBinding {
trait_name,
impl_type,
..
} => {
remap_required_local(trait_name, remap);
remap_required_local(impl_type, remap);
}
EdgeKind::HttpRequest { url, .. } => remap_option_local(url, remap),
EdgeKind::GrpcCall { service, method } => {
remap_required_local(service, remap);
remap_required_local(method, remap);
}
EdgeKind::DbQuery { table, .. } => remap_option_local(table, remap),
EdgeKind::TableRead { table_name, schema } => {
remap_required_local(table_name, remap);
remap_option_local(schema, remap);
}
EdgeKind::TableWrite {
table_name, schema, ..
} => {
remap_required_local(table_name, remap);
remap_option_local(schema, remap);
}
EdgeKind::TriggeredBy {
trigger_name,
schema,
} => {
remap_required_local(trigger_name, remap);
remap_option_local(schema, remap);
}
EdgeKind::MessageQueue { protocol, topic } => {
if let MqProtocol::Other(s) = protocol {
remap_required_local(s, remap);
}
remap_option_local(topic, remap);
}
EdgeKind::WebSocket { event } => remap_option_local(event, remap),
EdgeKind::GraphQLOperation { operation } => remap_required_local(operation, remap),
EdgeKind::ProcessExec { command } => remap_required_local(command, remap),
EdgeKind::FileIpc { path_pattern } => remap_option_local(path_pattern, remap),
EdgeKind::ProtocolCall { protocol, metadata } => {
remap_required_local(protocol, remap);
remap_option_local(metadata, remap);
}
EdgeKind::Defines
| EdgeKind::Contains
| EdgeKind::Calls { .. }
| EdgeKind::References
| EdgeKind::Inherits
| EdgeKind::Implements
| EdgeKind::LifetimeConstraint { .. }
| EdgeKind::MacroExpansion { .. }
| EdgeKind::FfiCall { .. }
| EdgeKind::WebAssemblyCall
| EdgeKind::GenericBound
| EdgeKind::AnnotatedWith
| EdgeKind::AnnotationParam
| EdgeKind::LambdaCaptures
| EdgeKind::ModuleExports
| EdgeKind::ModuleRequires
| EdgeKind::ModuleOpens
| EdgeKind::ModuleProvides
| EdgeKind::TypeArgument
| EdgeKind::ExtensionReceiver
| EdgeKind::CompanionOf
| EdgeKind::SealedPermit => {}
}
}
fn remap_required_local(id: &mut StringId, remap: &HashMap<StringId, StringId>) {
if id.is_local() {
let global = remap.get(id).unwrap_or_else(|| {
panic!("unmapped local StringId {id:?} — missing intern_string op?")
});
*id = *global;
}
}
fn remap_option_local(opt: &mut Option<StringId>, remap: &HashMap<StringId, StringId>) {
if let Some(id) = opt
&& id.is_local()
{
let global = remap.get(id).unwrap_or_else(|| {
panic!("unmapped local StringId {id:?} — missing intern_string op?")
});
*id = *global;
}
}
fn write_nodes(
ops: &[StagingOp],
plan: &FilePlan,
node_slots: &mut [Slot<NodeEntry>],
string_remap: &HashMap<StringId, StringId>,
) -> (HashMap<NodeId, NodeId>, usize, Vec<NodeId>) {
let mut node_remap = HashMap::new();
let mut node_cursor = 0usize;
let mut node_ids: Vec<NodeId> = Vec::with_capacity(node_slots.len());
for op in ops {
if let StagingOp::AddNode {
entry, expected_id, ..
} = op
{
if node_cursor >= node_slots.len() {
log::warn!(
"node slot overflow in file {:?}: cursor={node_cursor}, slots={}, skipping remaining nodes",
plan.file_id,
node_slots.len()
);
break;
}
let mut entry = entry.clone();
remap_node_entry_string_ids(&mut entry, string_remap);
entry.file = plan.file_id;
#[allow(clippy::cast_possible_truncation)] let actual_index = plan.node_range.start + node_cursor as u32;
let actual_id = NodeId::new(actual_index, 1);
node_slots[node_cursor] = Slot::new_occupied(1, entry);
if let Some(expected) = expected_id {
node_remap.insert(*expected, actual_id);
}
node_ids.push(actual_id);
node_cursor += 1;
}
}
(node_remap, node_cursor, node_ids)
}
fn collect_edges(
ops: &[StagingOp],
plan: &FilePlan,
node_remap: &HashMap<NodeId, NodeId>,
string_remap: &HashMap<StringId, StringId>,
) -> Vec<PendingEdge> {
let mut edges = Vec::new();
for op in ops {
if let StagingOp::AddEdge {
source,
target,
kind,
spans,
..
} = op
{
let actual_source = node_remap.get(source).copied().unwrap_or(*source);
let actual_target = node_remap.get(target).copied().unwrap_or(*target);
let mut remapped_kind = kind.clone();
remap_edge_kind_local_string_ids(&mut remapped_kind, string_remap);
edges.push(PendingEdge {
source: actual_source,
target: actual_target,
kind: remapped_kind,
file: plan.file_id,
spans: spans.clone(),
});
}
}
edges
}
#[allow(clippy::implicit_hasher)]
pub fn remap_string_id(id: &mut StringId, remap: &HashMap<StringId, StringId>) {
if let Some(&canonical) = remap.get(id) {
*id = canonical;
}
}
#[allow(clippy::implicit_hasher)]
pub fn remap_option_string_id(id: &mut Option<StringId>, remap: &HashMap<StringId, StringId>) {
if let Some(inner) = id {
remap_string_id(inner, remap);
}
}
#[allow(clippy::match_same_arms, clippy::implicit_hasher)] pub fn remap_edge_kind_string_ids(kind: &mut EdgeKind, remap: &HashMap<StringId, StringId>) {
match kind {
EdgeKind::Imports { alias, .. } => remap_option_string_id(alias, remap),
EdgeKind::Exports { alias, .. } => remap_option_string_id(alias, remap),
EdgeKind::TypeOf { name, .. } => remap_option_string_id(name, remap),
EdgeKind::TraitMethodBinding {
trait_name,
impl_type,
..
} => {
remap_string_id(trait_name, remap);
remap_string_id(impl_type, remap);
}
EdgeKind::HttpRequest { url, .. } => remap_option_string_id(url, remap),
EdgeKind::GrpcCall { service, method } => {
remap_string_id(service, remap);
remap_string_id(method, remap);
}
EdgeKind::DbQuery { table, .. } => remap_option_string_id(table, remap),
EdgeKind::TableRead { table_name, schema } => {
remap_string_id(table_name, remap);
remap_option_string_id(schema, remap);
}
EdgeKind::TableWrite {
table_name, schema, ..
} => {
remap_string_id(table_name, remap);
remap_option_string_id(schema, remap);
}
EdgeKind::TriggeredBy {
trigger_name,
schema,
} => {
remap_string_id(trigger_name, remap);
remap_option_string_id(schema, remap);
}
EdgeKind::MessageQueue { protocol, topic } => {
if let MqProtocol::Other(s) = protocol {
remap_string_id(s, remap);
}
remap_option_string_id(topic, remap);
}
EdgeKind::WebSocket { event } => remap_option_string_id(event, remap),
EdgeKind::GraphQLOperation { operation } => remap_string_id(operation, remap),
EdgeKind::ProcessExec { command } => remap_string_id(command, remap),
EdgeKind::FileIpc { path_pattern } => remap_option_string_id(path_pattern, remap),
EdgeKind::ProtocolCall { protocol, metadata } => {
remap_string_id(protocol, remap);
remap_option_string_id(metadata, remap);
}
EdgeKind::Defines
| EdgeKind::Contains
| EdgeKind::Calls { .. }
| EdgeKind::References
| EdgeKind::Inherits
| EdgeKind::Implements
| EdgeKind::LifetimeConstraint { .. }
| EdgeKind::MacroExpansion { .. }
| EdgeKind::FfiCall { .. }
| EdgeKind::WebAssemblyCall
| EdgeKind::GenericBound
| EdgeKind::AnnotatedWith
| EdgeKind::AnnotationParam
| EdgeKind::LambdaCaptures
| EdgeKind::ModuleExports
| EdgeKind::ModuleRequires
| EdgeKind::ModuleOpens
| EdgeKind::ModuleProvides
| EdgeKind::TypeArgument
| EdgeKind::ExtensionReceiver
| EdgeKind::CompanionOf
| EdgeKind::SealedPermit => {}
}
}
#[allow(clippy::implicit_hasher)]
pub fn remap_node_entry_global(entry: &mut NodeEntry, remap: &HashMap<StringId, StringId>) {
remap_string_id(&mut entry.name, remap);
remap_option_string_id(&mut entry.signature, remap);
remap_option_string_id(&mut entry.doc, remap);
remap_option_string_id(&mut entry.qualified_name, remap);
remap_option_string_id(&mut entry.visibility, remap);
}
#[allow(clippy::implicit_hasher)]
pub fn phase4_apply_global_remap(
arena: &mut NodeArena,
all_edges: &mut [Vec<PendingEdge>],
remap: &HashMap<StringId, StringId>,
) {
if remap.is_empty() {
return;
}
for (_id, entry) in arena.iter_mut() {
remap_node_entry_global(entry, remap);
}
for file_edges in all_edges.iter_mut() {
for edge in file_edges.iter_mut() {
remap_edge_kind_string_ids(&mut edge.kind, remap);
}
}
}
#[derive(Debug, Default)]
pub struct UnificationStats {
pub candidate_pairs_examined: usize,
pub nodes_merged: usize,
pub edges_rewritten: usize,
pub nodes_inert: usize,
pub elapsed_ms: u64,
}
pub(crate) fn phase4c_prime_unify_cross_file_nodes<
G: crate::graph::unified::mutation_target::GraphMutationTarget,
>(
graph: &mut G,
all_edges: &mut [Vec<PendingEdge>],
) -> UnificationStats {
use crate::graph::unified::mutation_target::GraphMutationTarget;
use super::helper::CALL_COMPATIBLE_KINDS;
use super::unification::{NodeRemapTable, merge_node_into};
use std::time::Instant;
let start = Instant::now();
let mut stats = UnificationStats::default();
let mut qn_groups: HashMap<crate::graph::unified::string::StringId, Vec<NodeId>> =
HashMap::new();
for (node_id, entry) in GraphMutationTarget::nodes(graph).iter() {
if !CALL_COMPATIBLE_KINDS.contains(&entry.kind) {
continue;
}
if let Some(qn_id) = entry.qualified_name {
qn_groups.entry(qn_id).or_default().push(node_id);
}
}
let groups_to_unify: Vec<Vec<NodeId>> = qn_groups
.into_values()
.filter(|group| {
if group.len() >= 2 {
stats.candidate_pairs_examined += 1;
true
} else {
false
}
})
.collect();
let mut remap = NodeRemapTable::with_capacity(groups_to_unify.len());
let path_keys: HashMap<NodeId, String> = {
let arena = GraphMutationTarget::nodes(graph);
let files = GraphMutationTarget::files(graph);
let mut out: HashMap<NodeId, String> =
HashMap::with_capacity(groups_to_unify.iter().map(Vec::len).sum());
for group in &groups_to_unify {
for &nid in group {
if out.contains_key(&nid) {
continue;
}
let key = arena
.get(nid)
.and_then(|entry| files.resolve(entry.file))
.map_or_else(String::new, |path| path.to_string_lossy().into_owned());
out.insert(nid, key);
}
}
out
};
let empty_path_key = String::new();
for group in &groups_to_unify {
let winner_id = *group
.iter()
.max_by(|&&a, &&b| {
let ea = GraphMutationTarget::nodes(graph).get(a);
let eb = GraphMutationTarget::nodes(graph).get(b);
match (ea, eb) {
(Some(ea), Some(eb)) => {
let a_real = ea.start_line > 0;
let b_real = eb.start_line > 0;
match (a_real, b_real) {
(true, false) => std::cmp::Ordering::Greater,
(false, true) => std::cmp::Ordering::Less,
_ => {
let a_range = ea.end_line.saturating_sub(ea.start_line);
let b_range = eb.end_line.saturating_sub(eb.start_line);
a_range
.cmp(&b_range)
.then_with(|| {
let pa = path_keys.get(&a).unwrap_or(&empty_path_key);
let pb = path_keys.get(&b).unwrap_or(&empty_path_key);
pb.cmp(pa)
})
.then_with(|| {
b.index().cmp(&a.index())
})
}
}
}
(Some(_), None) => std::cmp::Ordering::Greater,
(None, Some(_)) => std::cmp::Ordering::Less,
(None, None) => std::cmp::Ordering::Equal,
}
})
.expect("group is non-empty");
for &node_id in group {
if node_id == winner_id {
continue;
}
match merge_node_into(GraphMutationTarget::nodes_mut(graph), node_id, winner_id) {
Ok(()) => {
remap.insert(node_id, winner_id);
stats.nodes_merged += 1;
stats.nodes_inert += 1;
}
Err(e) => {
log::debug!("Phase 4c-prime: skipping merge ({e})");
}
}
}
}
if !remap.is_empty() {
let pre_count: usize = all_edges.iter().map(|v| v.len()).sum();
remap.apply_to_edges(all_edges);
remap.apply_to_committed_edges(GraphMutationTarget::edges(graph));
stats.edges_rewritten = pre_count;
}
stats.elapsed_ms = start.elapsed().as_millis() as u64;
stats
}
#[must_use]
pub fn pending_edges_to_delta(
per_file_edges: &[Vec<PendingEdge>],
seq_start: u64,
) -> (Vec<Vec<DeltaEdge>>, u64) {
let mut seq = seq_start;
let mut result = Vec::with_capacity(per_file_edges.len());
for file_edges in per_file_edges {
let mut delta_vec = Vec::with_capacity(file_edges.len());
for edge in file_edges {
delta_vec.push(DeltaEdge::with_spans(
edge.source,
edge.target,
edge.kind.clone(),
seq,
DeltaOp::Add,
edge.file,
edge.spans.clone(),
));
seq += 1;
}
result.push(delta_vec);
}
(result, seq)
}
pub(crate) fn rebuild_indices<G: crate::graph::unified::mutation_target::GraphMutationTarget>(
graph: &mut G,
) {
let (nodes, indices) = graph.nodes_and_indices_mut();
indices.build_from_arena(nodes);
}
pub(crate) fn phase4d_bulk_insert_edges<
G: crate::graph::unified::mutation_target::GraphMutationTarget,
>(
graph: &mut G,
per_file_edges: &[Vec<PendingEdge>],
) -> u64 {
let edge_seq_start = graph.edges().forward().seq_counter();
let (delta_edge_vecs, final_seq) = pending_edges_to_delta(per_file_edges, edge_seq_start);
let total_edge_count: u64 = delta_edge_vecs.iter().map(|v| v.len() as u64).sum();
if total_edge_count > 0 {
graph
.edges_mut()
.add_edges_bulk_ordered(&delta_edge_vecs, total_edge_count);
}
final_seq
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_compute_commit_plan_basic() {
let file_ids = vec![FileId::new(0), FileId::new(1), FileId::new(2)];
let node_counts = vec![3, 0, 5];
let string_counts = vec![2, 1, 3];
let edge_counts = vec![4, 0, 6];
let plan = compute_commit_plan(
&node_counts,
&string_counts,
&edge_counts,
&file_ids,
0,
1, );
assert_eq!(plan.total_nodes, 8);
assert_eq!(plan.total_strings, 6);
assert_eq!(plan.total_edges, 10);
assert_eq!(plan.file_plans[0].node_range, 0..3);
assert_eq!(plan.file_plans[0].string_range, 1..3);
assert_eq!(plan.file_plans[1].node_range, 3..3);
assert_eq!(plan.file_plans[1].string_range, 3..4);
assert_eq!(plan.file_plans[2].node_range, 3..8);
assert_eq!(plan.file_plans[2].string_range, 4..7);
}
#[test]
fn test_compute_commit_plan_with_offsets() {
let file_ids = vec![FileId::new(5)];
let plan = compute_commit_plan(&[10], &[5], &[7], &file_ids, 100, 50);
assert_eq!(plan.file_plans[0].node_range, 100..110);
assert_eq!(plan.file_plans[0].string_range, 50..55);
assert_eq!(plan.total_nodes, 10);
assert_eq!(plan.total_strings, 5);
assert_eq!(plan.total_edges, 7);
}
#[test]
fn test_compute_commit_plan_empty() {
let plan = compute_commit_plan(&[], &[], &[], &[], 0, 1);
assert_eq!(plan.total_nodes, 0);
assert_eq!(plan.total_strings, 0);
assert_eq!(plan.total_edges, 0);
assert!(plan.file_plans.is_empty());
}
#[test]
fn test_remap_string_id_basic() {
let mut remap = HashMap::new();
remap.insert(StringId::new(1), StringId::new(100));
let mut id = StringId::new(1);
remap_string_id(&mut id, &remap);
assert_eq!(id, StringId::new(100));
}
#[test]
fn test_remap_string_id_not_in_remap() {
let remap = HashMap::new();
let mut id = StringId::new(42);
remap_string_id(&mut id, &remap);
assert_eq!(id, StringId::new(42)); }
#[test]
fn test_remap_option_string_id() {
let mut remap = HashMap::new();
remap.insert(StringId::new(5), StringId::new(50));
let mut some_id = Some(StringId::new(5));
remap_option_string_id(&mut some_id, &remap);
assert_eq!(some_id, Some(StringId::new(50)));
let mut none_id: Option<StringId> = None;
remap_option_string_id(&mut none_id, &remap);
assert_eq!(none_id, None);
}
#[test]
fn test_remap_edge_kind_imports() {
let mut remap = HashMap::new();
remap.insert(StringId::new(1), StringId::new(100));
let mut kind = EdgeKind::Imports {
alias: Some(StringId::new(1)),
is_wildcard: false,
};
remap_edge_kind_string_ids(&mut kind, &remap);
assert!(
matches!(kind, EdgeKind::Imports { alias: Some(id), .. } if id == StringId::new(100))
);
}
#[test]
fn test_remap_edge_kind_trait_method_binding() {
let mut remap = HashMap::new();
remap.insert(StringId::new(1), StringId::new(100));
remap.insert(StringId::new(2), StringId::new(200));
let mut kind = EdgeKind::TraitMethodBinding {
trait_name: StringId::new(1),
impl_type: StringId::new(2),
is_ambiguous: false,
};
remap_edge_kind_string_ids(&mut kind, &remap);
assert!(
matches!(kind, EdgeKind::TraitMethodBinding { trait_name, impl_type, .. }
if trait_name == StringId::new(100) && impl_type == StringId::new(200))
);
}
#[test]
fn test_remap_edge_kind_no_op_variants() {
let remap = HashMap::new();
let mut kind = EdgeKind::Defines;
remap_edge_kind_string_ids(&mut kind, &remap);
assert!(matches!(kind, EdgeKind::Defines));
let mut kind = EdgeKind::Calls {
argument_count: 3,
is_async: true,
};
remap_edge_kind_string_ids(&mut kind, &remap);
assert!(matches!(
kind,
EdgeKind::Calls {
argument_count: 3,
is_async: true,
}
));
}
fn placeholder_entry() -> NodeEntry {
use crate::graph::unified::node::NodeKind;
NodeEntry::new(NodeKind::Function, StringId::new(0), FileId::new(0))
}
#[test]
fn test_phase2_assign_ranges_basic() {
use super::super::staging::StagingGraph;
let mut sg0 = StagingGraph::new();
let mut sg1 = StagingGraph::new();
let entry0 = placeholder_entry();
let n0 = sg0.add_node(entry0.clone());
let n1 = sg0.add_node(entry0.clone());
sg0.intern_string(StringId::new_local(0), "hello".into());
sg0.add_edge(
n0,
n1,
EdgeKind::Calls {
argument_count: 0,
is_async: false,
},
FileId::new(0),
);
sg1.add_node(entry0);
sg1.intern_string(StringId::new_local(0), "world".into());
sg1.intern_string(StringId::new_local(1), "foo".into());
let file_ids = vec![FileId::new(10), FileId::new(11)];
let offsets = GlobalOffsets {
node_offset: 5,
string_offset: 3,
};
let plan = phase2_assign_ranges(&[&sg0, &sg1], &file_ids, &offsets);
assert_eq!(plan.file_plans[0].node_range, 5..7);
assert_eq!(plan.file_plans[0].string_range, 3..4);
assert_eq!(plan.file_plans[1].node_range, 7..8);
assert_eq!(plan.file_plans[1].string_range, 4..6);
assert_eq!(plan.total_nodes, 3);
assert_eq!(plan.total_strings, 3);
assert_eq!(plan.total_edges, 1);
}
#[test]
fn test_phase3_parallel_commit_basic() {
use super::super::staging::StagingGraph;
use crate::graph::unified::concurrent::CodeGraph;
use crate::graph::unified::node::NodeKind;
let mut sg = StagingGraph::new();
let local_name = StringId::new_local(0);
sg.intern_string(local_name, "my_func".into());
let entry = NodeEntry::new(NodeKind::Function, local_name, FileId::new(0));
let n0 = sg.add_node(entry.clone());
let entry2 = NodeEntry::new(NodeKind::Variable, local_name, FileId::new(0));
let n1 = sg.add_node(entry2);
sg.add_edge(
n0,
n1,
EdgeKind::Calls {
argument_count: 0,
is_async: false,
},
FileId::new(0),
);
let file_ids = vec![FileId::new(5)];
let mut graph = CodeGraph::new();
graph
.nodes_mut()
.alloc_range(10, &placeholder_entry())
.unwrap();
let string_start = graph.strings_mut().alloc_range(1).unwrap();
assert_eq!(string_start, 1);
let offsets = GlobalOffsets {
node_offset: 10, string_offset: string_start,
};
let plan = phase2_assign_ranges(&[&sg], &file_ids, &offsets);
assert_eq!(plan.file_plans[0].node_range, 10..12);
graph
.nodes_mut()
.alloc_range(plan.total_nodes, &placeholder_entry())
.unwrap();
graph.strings_mut().alloc_range(plan.total_strings).unwrap();
let result = phase3_parallel_commit(&plan, &[&sg], &mut graph);
assert_eq!(result.total_nodes_written, 2);
assert_eq!(result.total_strings_written, 1);
let global_name = StringId::new(string_start);
assert_eq!(&*graph.strings().resolve(global_name).unwrap(), "my_func");
assert_eq!(result.per_file_edges.len(), 1);
assert_eq!(result.per_file_edges[0].len(), 1);
let edge = &result.per_file_edges[0][0];
assert_eq!(edge.file, FileId::new(5));
assert_eq!(edge.source, NodeId::new(10, 1)); assert_eq!(edge.target, NodeId::new(11, 1));
assert_eq!(result.per_file_node_ids.len(), 1);
assert_eq!(
result.per_file_node_ids[0],
vec![NodeId::new(10, 1), NodeId::new(11, 1)]
);
}
#[test]
fn test_phase3_parallel_commit_empty() {
use crate::graph::unified::concurrent::CodeGraph;
let mut graph = CodeGraph::new();
let plan = ChunkCommitPlan {
file_plans: vec![],
total_nodes: 0,
total_strings: 0,
total_edges: 0,
};
let result = phase3_parallel_commit(&plan, &[], &mut graph);
assert!(result.per_file_edges.is_empty());
assert!(result.per_file_node_ids.is_empty());
assert_eq!(result.total_nodes_written, 0);
assert_eq!(result.total_strings_written, 0);
}
#[test]
#[cfg(feature = "rebuild-internals")]
fn phase3_parallel_commit_runs_against_rebuild_graph() {
use super::super::staging::StagingGraph;
use crate::graph::unified::concurrent::CodeGraph;
use crate::graph::unified::mutation_target::GraphMutationTarget;
use crate::graph::unified::node::NodeKind;
let mut sg = StagingGraph::new();
let local_name = StringId::new_local(0);
sg.intern_string(local_name, "rebuild_target".into());
let entry = NodeEntry::new(NodeKind::Function, local_name, FileId::new(0));
let n0 = sg.add_node(entry.clone());
let entry2 = NodeEntry::new(NodeKind::Variable, local_name, FileId::new(0));
let n1 = sg.add_node(entry2);
sg.add_edge(
n0,
n1,
EdgeKind::Calls {
argument_count: 0,
is_async: false,
},
FileId::new(0),
);
let mut rebuild = {
let graph = CodeGraph::new();
graph.clone_for_rebuild()
};
rebuild
.nodes_mut()
.alloc_range(10, &placeholder_entry())
.unwrap();
let string_start = rebuild.strings_mut().alloc_range(1).unwrap();
assert_eq!(string_start, 1);
let file_ids = vec![FileId::new(5)];
let offsets = GlobalOffsets {
node_offset: 10,
string_offset: string_start,
};
let plan = phase2_assign_ranges(&[&sg], &file_ids, &offsets);
rebuild
.nodes_mut()
.alloc_range(plan.total_nodes, &placeholder_entry())
.unwrap();
rebuild
.strings_mut()
.alloc_range(plan.total_strings)
.unwrap();
let result = phase3_parallel_commit(&plan, &[&sg], &mut rebuild);
let committed_ids = &result.per_file_node_ids[0];
assert_eq!(
committed_ids,
&vec![NodeId::new(10, 1), NodeId::new(11, 1)],
"Phase 3 must commit into slots 10..12 on the rebuild-local arena"
);
let resolved_name = rebuild
.nodes_mut()
.get(NodeId::new(10, 1))
.map(|entry| entry.name)
.expect("committed node must exist in rebuild arena");
let resolved_str = rebuild
.strings_mut()
.resolve(resolved_name)
.expect("name must resolve in rebuild-local interner");
assert_eq!(&*resolved_str, "rebuild_target");
assert_eq!(result.total_nodes_written, 2);
assert_eq!(result.total_strings_written, 1);
assert_eq!(result.per_file_edges.len(), 1);
assert_eq!(result.per_file_edges[0].len(), 1);
let edge = &result.per_file_edges[0][0];
assert_eq!(edge.file, FileId::new(5));
assert_eq!(edge.source, NodeId::new(10, 1));
assert_eq!(edge.target, NodeId::new(11, 1));
}
#[test]
fn test_commit_single_file_string_remap() {
use super::super::staging::StagingGraph;
use crate::graph::unified::node::NodeKind;
let mut sg = StagingGraph::new();
let local_0 = StringId::new_local(0);
let local_1 = StringId::new_local(1);
sg.intern_string(local_0, "alpha".into());
sg.intern_string(local_1, "beta".into());
let mut entry = NodeEntry::new(NodeKind::Function, local_0, FileId::new(0));
entry.signature = Some(local_1);
sg.add_node(entry);
let plan = FilePlan {
parsed_index: 0,
file_id: FileId::new(42),
node_range: 10..11,
string_range: 20..22,
};
let mut node_slots = vec![Slot::new_occupied(1, placeholder_entry())];
let mut str_slots: Vec<Option<Arc<str>>> = vec![None, None];
let mut rc_slots: Vec<u32> = vec![0, 0];
let result = commit_single_file(&sg, &plan, &mut node_slots, &mut str_slots, &mut rc_slots);
assert_eq!(str_slots[0].as_deref(), Some("alpha"));
assert_eq!(str_slots[1].as_deref(), Some("beta"));
assert_eq!(rc_slots[0], 1);
assert_eq!(rc_slots[1], 1);
assert_eq!(result.strings_written, 2);
if let crate::graph::unified::storage::SlotState::Occupied(entry) = node_slots[0].state() {
assert_eq!(entry.name, StringId::new(20)); assert_eq!(entry.signature, Some(StringId::new(21))); assert_eq!(entry.file, FileId::new(42));
} else {
panic!("Expected occupied slot");
}
assert_eq!(result.nodes_written, 1);
assert_eq!(result.node_ids, vec![NodeId::new(10, 1)]);
assert!(result.edges.is_empty());
}
#[test]
fn test_remap_edge_kind_message_queue_other() {
let mut remap = HashMap::new();
remap.insert(StringId::new(10), StringId::new(110));
remap.insert(StringId::new(20), StringId::new(220));
let mut kind = EdgeKind::MessageQueue {
protocol: MqProtocol::Other(StringId::new(10)),
topic: Some(StringId::new(20)),
};
remap_edge_kind_string_ids(&mut kind, &remap);
assert!(matches!(
kind,
EdgeKind::MessageQueue {
protocol: MqProtocol::Other(proto),
topic: Some(topic),
} if proto == StringId::new(110) && topic == StringId::new(220)
));
}
#[test]
fn test_phase4_apply_global_remap_basic() {
use crate::graph::unified::node::NodeKind;
use crate::graph::unified::storage::NodeArena;
let mut arena = NodeArena::new();
let entry1 = NodeEntry::new(NodeKind::Function, StringId::new(1), FileId::new(0));
let mut entry2 = NodeEntry::new(NodeKind::Variable, StringId::new(2), FileId::new(0));
entry2.signature = Some(StringId::new(3));
arena.alloc(entry1).unwrap();
arena.alloc(entry2).unwrap();
let mut all_edges = vec![vec![PendingEdge {
source: NodeId::new(0, 1),
target: NodeId::new(1, 1),
kind: EdgeKind::Imports {
alias: Some(StringId::new(3)),
is_wildcard: false,
},
file: FileId::new(0),
spans: vec![],
}]];
let mut remap = HashMap::new();
remap.insert(StringId::new(2), StringId::new(1));
remap.insert(StringId::new(3), StringId::new(1));
phase4_apply_global_remap(&mut arena, &mut all_edges, &remap);
let (_, entry) = arena.iter().nth(1).unwrap();
assert_eq!(entry.name, StringId::new(1));
assert_eq!(entry.signature, Some(StringId::new(1)));
if let EdgeKind::Imports { alias, .. } = &all_edges[0][0].kind {
assert_eq!(*alias, Some(StringId::new(1)));
} else {
panic!("Expected Imports edge");
}
}
#[test]
fn test_phase4_apply_global_remap_empty() {
use crate::graph::unified::storage::NodeArena;
let mut arena = NodeArena::new();
let mut edges: Vec<Vec<PendingEdge>> = vec![];
let remap = HashMap::new();
phase4_apply_global_remap(&mut arena, &mut edges, &remap);
}
#[test]
fn test_pending_edges_to_delta_basic() {
let edges = vec![
vec![
PendingEdge {
source: NodeId::new(0, 1),
target: NodeId::new(1, 1),
kind: EdgeKind::Calls {
argument_count: 0,
is_async: false,
},
file: FileId::new(0),
spans: vec![],
},
PendingEdge {
source: NodeId::new(1, 1),
target: NodeId::new(2, 1),
kind: EdgeKind::References,
file: FileId::new(0),
spans: vec![],
},
],
vec![PendingEdge {
source: NodeId::new(3, 1),
target: NodeId::new(4, 1),
kind: EdgeKind::Defines,
file: FileId::new(1),
spans: vec![],
}],
];
let (deltas, final_seq) = pending_edges_to_delta(&edges, 100);
assert_eq!(deltas.len(), 2);
assert_eq!(deltas[0].len(), 2);
assert_eq!(deltas[1].len(), 1);
assert_eq!(final_seq, 103);
assert_eq!(deltas[0][0].seq, 100);
assert_eq!(deltas[0][1].seq, 101);
assert_eq!(deltas[1][0].seq, 102);
assert!(matches!(deltas[0][0].op, DeltaOp::Add));
assert!(matches!(deltas[1][0].op, DeltaOp::Add));
}
#[test]
fn test_pending_edges_to_delta_empty() {
let edges: Vec<Vec<PendingEdge>> = vec![];
let (deltas, final_seq) = pending_edges_to_delta(&edges, 0);
assert!(deltas.is_empty());
assert_eq!(final_seq, 0);
}
#[test]
#[cfg(feature = "rebuild-internals")]
fn phase4c_prime_unify_cross_file_nodes_runs_against_rebuild_graph() {
use crate::graph::unified::concurrent::CodeGraph;
use crate::graph::unified::mutation_target::GraphMutationTarget;
use crate::graph::unified::node::NodeKind;
let mut rebuild = {
let graph = CodeGraph::new();
graph.clone_for_rebuild()
};
let qname_sid = rebuild.strings_mut().intern("my_mod::my_func").unwrap();
let file_a = FileId::new(7);
let file_b = FileId::new(8);
let mut winner_entry = NodeEntry::new(NodeKind::Function, qname_sid, file_a);
winner_entry.qualified_name = Some(qname_sid);
winner_entry.start_line = 10;
winner_entry.end_line = 30;
let mut loser_entry = NodeEntry::new(NodeKind::Function, qname_sid, file_b);
loser_entry.qualified_name = Some(qname_sid);
loser_entry.start_line = 5;
loser_entry.end_line = 6;
let winner_id = rebuild.nodes_mut().alloc(winner_entry).unwrap();
let loser_id = rebuild.nodes_mut().alloc(loser_entry).unwrap();
let mut all_edges = vec![vec![PendingEdge {
source: winner_id, target: loser_id,
kind: EdgeKind::Calls {
argument_count: 0,
is_async: false,
},
file: file_b,
spans: vec![],
}]];
let stats = phase4c_prime_unify_cross_file_nodes(&mut rebuild, &mut all_edges);
assert_eq!(stats.nodes_merged, 1, "exactly one loser was tombstoned");
assert_eq!(stats.candidate_pairs_examined, 1);
assert_eq!(stats.edges_rewritten, 1);
let winner_entry_after = GraphMutationTarget::nodes(&rebuild)
.get(winner_id)
.expect("winner must remain live");
assert_eq!(
winner_entry_after.qualified_name,
Some(qname_sid),
"winner keeps its qualified_name"
);
let loser_entry_after = GraphMutationTarget::nodes(&rebuild)
.get(loser_id)
.expect("loser slot remains live (inert) per §F.1 bijection");
assert_eq!(
loser_entry_after.qualified_name, None,
"loser qualified_name cleared by merge_node_into"
);
assert_eq!(
all_edges[0][0].target, winner_id,
"PendingEdge.target rewritten from loser → winner"
);
}
#[test]
#[cfg(feature = "rebuild-internals")]
fn phase4c_prime_tie_break_prefers_lex_smaller_path_over_node_id() {
use crate::graph::unified::concurrent::CodeGraph;
use crate::graph::unified::node::NodeKind;
use std::path::Path;
let mut graph = CodeGraph::new();
let qname = graph.strings_mut().intern("shared_qname").unwrap();
let high_path_file = graph
.files_mut()
.register(Path::new("zzz_late.rs"))
.unwrap();
let low_path_file = graph
.files_mut()
.register(Path::new("aaa_early.rs"))
.unwrap();
let mut high_entry = NodeEntry::new(NodeKind::Function, qname, high_path_file);
high_entry.qualified_name = Some(qname);
high_entry.start_line = 10;
high_entry.end_line = 20;
let high_node = graph.nodes_mut().alloc(high_entry).unwrap();
let mut low_entry = NodeEntry::new(NodeKind::Function, qname, low_path_file);
low_entry.qualified_name = Some(qname);
low_entry.start_line = 10;
low_entry.end_line = 20;
let low_node = graph.nodes_mut().alloc(low_entry).unwrap();
graph.rebuild_indices();
let mut all_edges: Vec<Vec<PendingEdge>> = Vec::new();
let stats = phase4c_prime_unify_cross_file_nodes(&mut graph, &mut all_edges);
assert_eq!(
stats.nodes_merged, 1,
"one of the duplicate nodes must be merged into the other"
);
let low_after = graph
.nodes()
.get(low_node)
.expect("winner slot remains live");
assert_eq!(
low_after.qualified_name,
Some(qname),
"path-earlier node keeps qualified_name as the unification winner"
);
let high_after = graph
.nodes()
.get(high_node)
.expect("loser slot remains inert (Gate 0d bijection contract)");
assert_eq!(
high_after.qualified_name, None,
"path-later node loses even when its NodeId::index() is smaller"
);
}
#[test]
#[cfg(feature = "rebuild-internals")]
fn phase4c_prime_tie_break_falls_back_to_smaller_node_id_on_identical_path() {
use crate::graph::unified::concurrent::CodeGraph;
use crate::graph::unified::node::NodeKind;
use std::path::Path;
let mut graph = CodeGraph::new();
let qname = graph.strings_mut().intern("shared_qname").unwrap();
let file = graph.files_mut().register(Path::new("shared.rs")).unwrap();
let mut first_entry = NodeEntry::new(NodeKind::Function, qname, file);
first_entry.qualified_name = Some(qname);
first_entry.start_line = 1;
first_entry.end_line = 5;
let first_node = graph.nodes_mut().alloc(first_entry).unwrap();
let mut second_entry = NodeEntry::new(NodeKind::Function, qname, file);
second_entry.qualified_name = Some(qname);
second_entry.start_line = 1;
second_entry.end_line = 5;
let second_node = graph.nodes_mut().alloc(second_entry).unwrap();
assert!(
first_node.index() < second_node.index(),
"precondition: first_node's arena slot precedes second_node's"
);
graph.rebuild_indices();
let mut all_edges: Vec<Vec<PendingEdge>> = Vec::new();
let stats = phase4c_prime_unify_cross_file_nodes(&mut graph, &mut all_edges);
assert_eq!(stats.nodes_merged, 1);
let winner_after = graph.nodes().get(first_node).expect("winner live");
assert_eq!(
winner_after.qualified_name,
Some(qname),
"smaller-index node wins the same-path / same-span tie-break"
);
let loser_after = graph.nodes().get(second_node).expect("loser inert");
assert_eq!(
loser_after.qualified_name, None,
"larger-index node loses the same-path / same-span tie-break"
);
}
#[test]
#[cfg(feature = "rebuild-internals")]
fn rebuild_indices_runs_against_rebuild_graph() {
use crate::graph::unified::concurrent::CodeGraph;
use crate::graph::unified::mutation_target::GraphMutationTarget;
use crate::graph::unified::node::NodeKind;
let mut code_graph = CodeGraph::new();
let alpha_id_code = code_graph.strings_mut().intern("alpha").unwrap();
let mut code_entry = NodeEntry::new(NodeKind::Function, alpha_id_code, FileId::new(1));
code_entry.qualified_name = Some(alpha_id_code);
let code_node_id = code_graph.nodes_mut().alloc(code_entry).unwrap();
rebuild_indices(&mut code_graph);
let code_buckets_function: Vec<NodeId> =
code_graph.indices().by_kind(NodeKind::Function).to_vec();
let mut rebuild = {
let graph = CodeGraph::new();
graph.clone_for_rebuild()
};
let alpha_id_rebuild = rebuild.strings_mut().intern("alpha").unwrap();
let mut rebuild_entry =
NodeEntry::new(NodeKind::Function, alpha_id_rebuild, FileId::new(1));
rebuild_entry.qualified_name = Some(alpha_id_rebuild);
let rebuild_node_id = rebuild.nodes_mut().alloc(rebuild_entry).unwrap();
rebuild_indices(&mut rebuild);
assert_eq!(code_node_id, rebuild_node_id);
let rebuild_buckets_function: Vec<NodeId> = GraphMutationTarget::indices(&rebuild)
.by_kind(NodeKind::Function)
.to_vec();
assert_eq!(
code_buckets_function, rebuild_buckets_function,
"rebuild_indices must produce equivalent Function buckets on both paths"
);
let by_name: Vec<NodeId> = GraphMutationTarget::indices(&rebuild)
.by_name(alpha_id_rebuild)
.to_vec();
assert_eq!(by_name, vec![rebuild_node_id]);
}
#[test]
#[cfg(feature = "rebuild-internals")]
fn phase4d_bulk_insert_edges_runs_against_rebuild_graph() {
use crate::graph::unified::concurrent::CodeGraph;
use crate::graph::unified::mutation_target::GraphMutationTarget;
use crate::graph::unified::node::NodeKind;
let mut rebuild = {
let graph = CodeGraph::new();
graph.clone_for_rebuild()
};
let name_sid = rebuild.strings_mut().intern("edge_target").unwrap();
let file = FileId::new(3);
let n_source = rebuild
.nodes_mut()
.alloc(NodeEntry::new(NodeKind::Function, name_sid, file))
.unwrap();
let n_target = rebuild
.nodes_mut()
.alloc(NodeEntry::new(NodeKind::Variable, name_sid, file))
.unwrap();
let pre_counter = GraphMutationTarget::edges(&rebuild).forward().seq_counter();
let per_file_edges = vec![vec![
PendingEdge {
source: n_source,
target: n_target,
kind: EdgeKind::Calls {
argument_count: 0,
is_async: false,
},
file,
spans: vec![],
},
PendingEdge {
source: n_source,
target: n_target,
kind: EdgeKind::Calls {
argument_count: 1,
is_async: false,
},
file,
spans: vec![],
},
]];
let final_seq = phase4d_bulk_insert_edges(&mut rebuild, &per_file_edges);
assert_eq!(
final_seq,
pre_counter + 2,
"phase4d_bulk_insert_edges must advance seq by edge count"
);
let forward = GraphMutationTarget::edges(&rebuild).forward();
let after_counter = forward.seq_counter();
assert_eq!(after_counter, pre_counter + 2);
assert!(
forward.delta().iter().filter(|e| e.is_add()).count() >= 2,
"expected at least two Add edges in the rebuild-local forward delta"
);
drop(forward);
let empty_final = phase4d_bulk_insert_edges(&mut rebuild, &[]);
assert_eq!(empty_final, pre_counter + 2, "empty input is a no-op");
}
#[test]
fn phase4d_preserves_property_sourced_typeof_field_edges() {
use crate::graph::unified::edge::kind::TypeOfContext;
let struct_id = NodeId::new(10, 1);
let property_id = NodeId::new(11, 1);
let bool_id = NodeId::new(12, 1);
let typeof_field_kind = EdgeKind::TypeOf {
context: Some(TypeOfContext::Field),
index: Some(0),
name: None,
};
let per_file_edges = vec![vec![
PendingEdge {
source: property_id,
target: bool_id,
kind: typeof_field_kind.clone(),
file: FileId::new(0),
spans: vec![],
},
PendingEdge {
source: struct_id,
target: bool_id,
kind: typeof_field_kind.clone(),
file: FileId::new(0),
spans: vec![],
},
]];
let (deltas, final_seq) = pending_edges_to_delta(&per_file_edges, 500);
assert_eq!(deltas.len(), 1);
assert_eq!(deltas[0].len(), 2);
assert_eq!(final_seq, 502);
assert_eq!(deltas[0][0].source, property_id);
assert_eq!(deltas[0][0].target, bool_id);
assert_eq!(deltas[0][0].seq, 500);
assert!(matches!(
deltas[0][0].kind,
EdgeKind::TypeOf {
context: Some(TypeOfContext::Field),
..
}
));
assert_eq!(deltas[0][1].source, struct_id);
assert_eq!(deltas[0][1].target, bool_id);
assert_eq!(deltas[0][1].seq, 501);
let (deltas_again, final_seq_again) = pending_edges_to_delta(&per_file_edges, 500);
assert_eq!(final_seq_again, final_seq);
assert_eq!(deltas_again.len(), deltas.len());
assert_eq!(deltas_again[0].len(), deltas[0].len());
for (a, b) in deltas[0].iter().zip(deltas_again[0].iter()) {
assert_eq!(a.source, b.source);
assert_eq!(a.target, b.target);
assert_eq!(a.seq, b.seq);
}
}
}