1use std::collections::HashMap;
25use std::ops::Range;
26use std::sync::Arc;
27
28use rayon::prelude::*;
29
30use crate::graph::unified::edge::delta::{DeltaEdge, DeltaOp};
31use crate::graph::unified::edge::kind::{EdgeKind, MqProtocol};
32use crate::graph::unified::file::FileId;
33use crate::graph::unified::node::NodeId;
34use crate::graph::unified::storage::NodeArena;
35use crate::graph::unified::storage::arena::{NodeEntry, Slot};
36use crate::graph::unified::string::StringId;
37
38use super::pass3_intra::PendingEdge;
39use super::staging::{StagingGraph, StagingOp};
40
41#[derive(Debug, Clone, Default)]
46pub struct GlobalOffsets {
47 pub node_offset: u32,
49 pub string_offset: u32,
51}
52
53#[derive(Debug, Clone)]
55pub struct FilePlan {
56 pub parsed_index: usize,
58 pub file_id: FileId,
60 pub node_range: Range<u32>,
62 pub string_range: Range<u32>,
64}
65
66#[derive(Debug, Clone)]
68pub struct ChunkCommitPlan {
69 pub file_plans: Vec<FilePlan>,
71 pub total_nodes: u32,
73 pub total_strings: u32,
75 pub total_edges: u64,
77}
78
79#[must_use]
99pub fn compute_commit_plan(
100 node_counts: &[u32],
101 string_counts: &[u32],
102 edge_counts: &[u32],
103 file_ids: &[FileId],
104 node_offset: u32,
105 string_offset: u32,
106) -> ChunkCommitPlan {
107 debug_assert_eq!(node_counts.len(), string_counts.len());
108 debug_assert_eq!(node_counts.len(), edge_counts.len());
109 debug_assert_eq!(node_counts.len(), file_ids.len());
110
111 let mut plans = Vec::with_capacity(node_counts.len());
112 let mut node_cursor = node_offset;
113 let mut string_cursor = string_offset;
114 let mut total_edges: u64 = 0;
115
116 for i in 0..node_counts.len() {
117 let nc = node_counts[i];
118 let sc = string_counts[i];
119
120 let node_end = node_cursor
121 .checked_add(nc)
122 .expect("node ID space overflow in commit plan");
123 let string_end = string_cursor
124 .checked_add(sc)
125 .expect("string ID space overflow in commit plan");
126
127 plans.push(FilePlan {
128 parsed_index: i,
129 file_id: file_ids[i],
130 node_range: node_cursor..node_end,
131 string_range: string_cursor..string_end,
132 });
133
134 node_cursor = node_end;
135 string_cursor = string_end;
136 total_edges += u64::from(edge_counts[i]);
137 }
138
139 ChunkCommitPlan {
140 file_plans: plans,
141 total_nodes: node_cursor - node_offset,
142 total_strings: string_cursor - string_offset,
143 total_edges,
144 }
145}
146
147#[must_use]
152pub fn phase2_assign_ranges(
153 staging_graphs: &[&StagingGraph],
154 file_ids: &[FileId],
155 offsets: &GlobalOffsets,
156) -> ChunkCommitPlan {
157 let node_counts: Vec<u32> = staging_graphs
158 .iter()
159 .map(|sg| sg.node_count_u32())
160 .collect();
161 let string_counts: Vec<u32> = staging_graphs
162 .iter()
163 .map(|sg| sg.string_count_u32())
164 .collect();
165 let edge_counts: Vec<u32> = staging_graphs
166 .iter()
167 .map(|sg| sg.edge_count_u32())
168 .collect();
169
170 compute_commit_plan(
171 &node_counts,
172 &string_counts,
173 &edge_counts,
174 file_ids,
175 offsets.node_offset,
176 offsets.string_offset,
177 )
178}
179
180pub struct Phase3Result {
183 pub per_file_edges: Vec<Vec<PendingEdge>>,
185 pub per_file_node_ids: Vec<Vec<NodeId>>,
194 pub total_nodes_written: usize,
196 pub total_strings_written: usize,
198 pub total_edges_collected: usize,
200}
201
202#[must_use]
236pub(crate) fn phase3_parallel_commit<
237 G: crate::graph::unified::mutation_target::GraphMutationTarget,
238>(
239 plan: &ChunkCommitPlan,
240 staging_graphs: &[&StagingGraph],
241 graph: &mut G,
242) -> Phase3Result {
243 if plan.file_plans.is_empty() {
244 return Phase3Result {
245 per_file_edges: Vec::new(),
246 per_file_node_ids: Vec::new(),
247 total_nodes_written: 0,
248 total_strings_written: 0,
249 total_edges_collected: 0,
250 };
251 }
252
253 let node_start = plan.file_plans[0].node_range.start;
255 let string_start = plan.file_plans[0].string_range.start;
256
257 let (arena, interner) = graph.nodes_and_strings_mut();
262
263 let node_slice = arena.bulk_slice_mut(node_start, plan.total_nodes);
265 let (str_slice, rc_slice) = interner.bulk_slices_mut(string_start, plan.total_strings);
266
267 let mut node_remaining = &mut *node_slice;
269 let mut str_remaining = &mut *str_slice;
270 let mut rc_remaining = &mut *rc_slice;
271
272 #[allow(clippy::type_complexity)]
273 let mut file_work: Vec<(
274 &mut [Slot<NodeEntry>],
275 &mut [Option<Arc<str>>],
276 &mut [u32],
277 &FilePlan,
278 usize,
279 )> = Vec::with_capacity(plan.file_plans.len());
280
281 for (i, file_plan) in plan.file_plans.iter().enumerate() {
282 let nc = (file_plan.node_range.end - file_plan.node_range.start) as usize;
283 let sc = (file_plan.string_range.end - file_plan.string_range.start) as usize;
284
285 let (n, nr) = node_remaining.split_at_mut(nc);
286 let (s, sr) = str_remaining.split_at_mut(sc);
287 let (r, rr) = rc_remaining.split_at_mut(sc);
288
289 file_work.push((n, s, r, file_plan, i));
290 node_remaining = nr;
291 str_remaining = sr;
292 rc_remaining = rr;
293 }
294
295 let results: Vec<FileCommitResult> = file_work
297 .into_par_iter()
298 .map(|(node_slots, str_slots, rc_slots, file_plan, idx)| {
299 commit_single_file(
300 staging_graphs[idx],
301 file_plan,
302 node_slots,
303 str_slots,
304 rc_slots,
305 )
306 })
307 .collect();
308
309 let total_nodes_written: usize = results.iter().map(|r| r.nodes_written).sum();
310 let total_strings_written: usize = results.iter().map(|r| r.strings_written).sum();
311 let total_edges_collected: usize = results.iter().map(|r| r.edges.len()).sum();
312 let mut per_file_edges = Vec::with_capacity(results.len());
313 let mut per_file_node_ids = Vec::with_capacity(results.len());
314 for r in results {
315 per_file_edges.push(r.edges);
316 per_file_node_ids.push(r.node_ids);
317 }
318
319 Phase3Result {
320 per_file_edges,
321 per_file_node_ids,
322 total_nodes_written,
323 total_strings_written,
324 total_edges_collected,
325 }
326}
327
328struct FileCommitResult {
344 edges: Vec<PendingEdge>,
345 node_ids: Vec<NodeId>,
349 nodes_written: usize,
350 strings_written: usize,
351}
352
353fn commit_single_file(
354 staging: &StagingGraph,
355 plan: &FilePlan,
356 node_slots: &mut [Slot<NodeEntry>],
357 str_slots: &mut [Option<Arc<str>>],
358 rc_slots: &mut [u32],
359) -> FileCommitResult {
360 let ops = staging.operations();
361
362 let (string_remap, strings_written) = write_strings(ops, plan, str_slots, rc_slots);
364
365 let (node_remap, nodes_written, node_ids) = write_nodes(ops, plan, node_slots, &string_remap);
367
368 let edges = collect_edges(ops, plan, &node_remap, &string_remap);
370
371 FileCommitResult {
372 edges,
373 node_ids,
374 nodes_written,
375 strings_written,
376 }
377}
378
379fn write_strings(
386 ops: &[StagingOp],
387 plan: &FilePlan,
388 str_slots: &mut [Option<Arc<str>>],
389 rc_slots: &mut [u32],
390) -> (HashMap<StringId, StringId>, usize) {
391 let mut remap = HashMap::new();
392 let mut string_cursor = 0usize;
393
394 for op in ops {
395 if let StagingOp::InternString { local_id, value } = op {
396 assert!(
398 local_id.is_local(),
399 "non-local StringId {:?} in InternString op for file {:?}",
400 local_id,
401 plan.file_id,
402 );
403 assert!(
405 !remap.contains_key(local_id),
406 "duplicate local StringId {:?} in InternString op for file {:?}",
407 local_id,
408 plan.file_id,
409 );
410
411 if string_cursor >= str_slots.len() {
412 log::warn!(
413 "string slot overflow in file {:?}: cursor={string_cursor}, slots={}, skipping remaining strings",
414 plan.file_id,
415 str_slots.len()
416 );
417 break;
418 }
419
420 #[allow(clippy::cast_possible_truncation)] let global_id = StringId::new(plan.string_range.start + string_cursor as u32);
423
424 str_slots[string_cursor] = Some(Arc::from(value.as_str()));
426 rc_slots[string_cursor] = 1;
427
428 remap.insert(*local_id, global_id);
429 string_cursor += 1;
430 }
431 }
432
433 (remap, string_cursor)
434}
435
436fn remap_node_entry_string_ids(entry: &mut NodeEntry, remap: &HashMap<StringId, StringId>) {
442 remap_required_local(&mut entry.name, remap);
443 remap_option_local(&mut entry.signature, remap);
444 remap_option_local(&mut entry.doc, remap);
445 remap_option_local(&mut entry.qualified_name, remap);
446 remap_option_local(&mut entry.visibility, remap);
447}
448
449#[allow(clippy::match_same_arms)]
454fn remap_edge_kind_local_string_ids(kind: &mut EdgeKind, remap: &HashMap<StringId, StringId>) {
455 match kind {
456 EdgeKind::Imports { alias, .. } => remap_option_local(alias, remap),
457 EdgeKind::Exports { alias, .. } => remap_option_local(alias, remap),
458 EdgeKind::TypeOf { name, .. } => remap_option_local(name, remap),
459 EdgeKind::TraitMethodBinding {
460 trait_name,
461 impl_type,
462 ..
463 } => {
464 remap_required_local(trait_name, remap);
465 remap_required_local(impl_type, remap);
466 }
467 EdgeKind::HttpRequest { url, .. } => remap_option_local(url, remap),
468 EdgeKind::GrpcCall { service, method } => {
469 remap_required_local(service, remap);
470 remap_required_local(method, remap);
471 }
472 EdgeKind::DbQuery { table, .. } => remap_option_local(table, remap),
473 EdgeKind::TableRead { table_name, schema } => {
474 remap_required_local(table_name, remap);
475 remap_option_local(schema, remap);
476 }
477 EdgeKind::TableWrite {
478 table_name, schema, ..
479 } => {
480 remap_required_local(table_name, remap);
481 remap_option_local(schema, remap);
482 }
483 EdgeKind::TriggeredBy {
484 trigger_name,
485 schema,
486 } => {
487 remap_required_local(trigger_name, remap);
488 remap_option_local(schema, remap);
489 }
490 EdgeKind::MessageQueue { protocol, topic } => {
491 if let MqProtocol::Other(s) = protocol {
492 remap_required_local(s, remap);
493 }
494 remap_option_local(topic, remap);
495 }
496 EdgeKind::WebSocket { event } => remap_option_local(event, remap),
497 EdgeKind::GraphQLOperation { operation } => remap_required_local(operation, remap),
498 EdgeKind::ProcessExec { command } => remap_required_local(command, remap),
499 EdgeKind::FileIpc { path_pattern } => remap_option_local(path_pattern, remap),
500 EdgeKind::ProtocolCall { protocol, metadata } => {
501 remap_required_local(protocol, remap);
502 remap_option_local(metadata, remap);
503 }
504 EdgeKind::Defines
506 | EdgeKind::Contains
507 | EdgeKind::Calls { .. }
508 | EdgeKind::References
509 | EdgeKind::Inherits
510 | EdgeKind::Implements
511 | EdgeKind::LifetimeConstraint { .. }
512 | EdgeKind::MacroExpansion { .. }
513 | EdgeKind::FfiCall { .. }
514 | EdgeKind::WebAssemblyCall
515 | EdgeKind::GenericBound
516 | EdgeKind::AnnotatedWith
517 | EdgeKind::AnnotationParam
518 | EdgeKind::LambdaCaptures
519 | EdgeKind::ModuleExports
520 | EdgeKind::ModuleRequires
521 | EdgeKind::ModuleOpens
522 | EdgeKind::ModuleProvides
523 | EdgeKind::TypeArgument
524 | EdgeKind::ExtensionReceiver
525 | EdgeKind::CompanionOf
526 | EdgeKind::SealedPermit => {}
527 }
528}
529
530fn remap_required_local(id: &mut StringId, remap: &HashMap<StringId, StringId>) {
535 if id.is_local() {
536 let global = remap.get(id).unwrap_or_else(|| {
537 panic!("unmapped local StringId {id:?} — missing intern_string op?")
538 });
539 *id = *global;
540 }
541}
542
543fn remap_option_local(opt: &mut Option<StringId>, remap: &HashMap<StringId, StringId>) {
545 if let Some(id) = opt
546 && id.is_local()
547 {
548 let global = remap.get(id).unwrap_or_else(|| {
549 panic!("unmapped local StringId {id:?} — missing intern_string op?")
550 });
551 *id = *global;
552 }
553}
554
555fn write_nodes(
561 ops: &[StagingOp],
562 plan: &FilePlan,
563 node_slots: &mut [Slot<NodeEntry>],
564 string_remap: &HashMap<StringId, StringId>,
565) -> (HashMap<NodeId, NodeId>, usize, Vec<NodeId>) {
566 let mut node_remap = HashMap::new();
567 let mut node_cursor = 0usize;
568 let mut node_ids: Vec<NodeId> = Vec::with_capacity(node_slots.len());
569
570 for op in ops {
571 if let StagingOp::AddNode {
572 entry, expected_id, ..
573 } = op
574 {
575 if node_cursor >= node_slots.len() {
576 log::warn!(
577 "node slot overflow in file {:?}: cursor={node_cursor}, slots={}, skipping remaining nodes",
578 plan.file_id,
579 node_slots.len()
580 );
581 break;
582 }
583
584 let mut entry = entry.clone();
585
586 remap_node_entry_string_ids(&mut entry, string_remap);
588
589 entry.file = plan.file_id;
591
592 #[allow(clippy::cast_possible_truncation)] let actual_index = plan.node_range.start + node_cursor as u32;
595 let actual_id = NodeId::new(actual_index, 1);
596
597 node_slots[node_cursor] = Slot::new_occupied(1, entry);
599
600 if let Some(expected) = expected_id {
601 node_remap.insert(*expected, actual_id);
602 }
603
604 node_ids.push(actual_id);
605 node_cursor += 1;
606 }
607 }
608
609 (node_remap, node_cursor, node_ids)
610}
611
612fn collect_edges(
615 ops: &[StagingOp],
616 plan: &FilePlan,
617 node_remap: &HashMap<NodeId, NodeId>,
618 string_remap: &HashMap<StringId, StringId>,
619) -> Vec<PendingEdge> {
620 let mut edges = Vec::new();
621
622 for op in ops {
623 if let StagingOp::AddEdge {
624 source,
625 target,
626 kind,
627 spans,
628 ..
629 } = op
630 {
631 let actual_source = node_remap.get(source).copied().unwrap_or(*source);
632 let actual_target = node_remap.get(target).copied().unwrap_or(*target);
633
634 let mut remapped_kind = kind.clone();
636 remap_edge_kind_local_string_ids(&mut remapped_kind, string_remap);
637
638 edges.push(PendingEdge {
639 source: actual_source,
640 target: actual_target,
641 kind: remapped_kind,
642 file: plan.file_id,
643 spans: spans.clone(),
644 });
645 }
646 }
647
648 edges
649}
650
651#[allow(clippy::implicit_hasher)]
656pub fn remap_string_id(id: &mut StringId, remap: &HashMap<StringId, StringId>) {
657 if let Some(&canonical) = remap.get(id) {
658 *id = canonical;
659 }
660}
661
662#[allow(clippy::implicit_hasher)]
664pub fn remap_option_string_id(id: &mut Option<StringId>, remap: &HashMap<StringId, StringId>) {
665 if let Some(inner) = id {
666 remap_string_id(inner, remap);
667 }
668}
669
670#[allow(clippy::match_same_arms, clippy::implicit_hasher)] pub fn remap_edge_kind_string_ids(kind: &mut EdgeKind, remap: &HashMap<StringId, StringId>) {
676 match kind {
677 EdgeKind::Imports { alias, .. } => remap_option_string_id(alias, remap),
679 EdgeKind::Exports { alias, .. } => remap_option_string_id(alias, remap),
680 EdgeKind::TypeOf { name, .. } => remap_option_string_id(name, remap),
681 EdgeKind::TraitMethodBinding {
682 trait_name,
683 impl_type,
684 ..
685 } => {
686 remap_string_id(trait_name, remap);
687 remap_string_id(impl_type, remap);
688 }
689 EdgeKind::HttpRequest { url, .. } => remap_option_string_id(url, remap),
690 EdgeKind::GrpcCall { service, method } => {
691 remap_string_id(service, remap);
692 remap_string_id(method, remap);
693 }
694 EdgeKind::DbQuery { table, .. } => remap_option_string_id(table, remap),
695 EdgeKind::TableRead { table_name, schema } => {
696 remap_string_id(table_name, remap);
697 remap_option_string_id(schema, remap);
698 }
699 EdgeKind::TableWrite {
700 table_name, schema, ..
701 } => {
702 remap_string_id(table_name, remap);
703 remap_option_string_id(schema, remap);
704 }
705 EdgeKind::TriggeredBy {
706 trigger_name,
707 schema,
708 } => {
709 remap_string_id(trigger_name, remap);
710 remap_option_string_id(schema, remap);
711 }
712 EdgeKind::MessageQueue { protocol, topic } => {
713 if let MqProtocol::Other(s) = protocol {
714 remap_string_id(s, remap);
715 }
716 remap_option_string_id(topic, remap);
717 }
718 EdgeKind::WebSocket { event } => remap_option_string_id(event, remap),
719 EdgeKind::GraphQLOperation { operation } => remap_string_id(operation, remap),
720 EdgeKind::ProcessExec { command } => remap_string_id(command, remap),
721 EdgeKind::FileIpc { path_pattern } => remap_option_string_id(path_pattern, remap),
722 EdgeKind::ProtocolCall { protocol, metadata } => {
723 remap_string_id(protocol, remap);
724 remap_option_string_id(metadata, remap);
725 }
726 EdgeKind::Defines
728 | EdgeKind::Contains
729 | EdgeKind::Calls { .. }
730 | EdgeKind::References
731 | EdgeKind::Inherits
732 | EdgeKind::Implements
733 | EdgeKind::LifetimeConstraint { .. }
734 | EdgeKind::MacroExpansion { .. }
735 | EdgeKind::FfiCall { .. }
736 | EdgeKind::WebAssemblyCall
737 | EdgeKind::GenericBound
738 | EdgeKind::AnnotatedWith
739 | EdgeKind::AnnotationParam
740 | EdgeKind::LambdaCaptures
741 | EdgeKind::ModuleExports
742 | EdgeKind::ModuleRequires
743 | EdgeKind::ModuleOpens
744 | EdgeKind::ModuleProvides
745 | EdgeKind::TypeArgument
746 | EdgeKind::ExtensionReceiver
747 | EdgeKind::CompanionOf
748 | EdgeKind::SealedPermit => {}
749 }
750}
751
752#[allow(clippy::implicit_hasher)]
759pub fn remap_node_entry_global(entry: &mut NodeEntry, remap: &HashMap<StringId, StringId>) {
760 remap_string_id(&mut entry.name, remap);
761 remap_option_string_id(&mut entry.signature, remap);
762 remap_option_string_id(&mut entry.doc, remap);
763 remap_option_string_id(&mut entry.qualified_name, remap);
764 remap_option_string_id(&mut entry.visibility, remap);
765}
766
767#[allow(clippy::implicit_hasher)]
774pub fn phase4_apply_global_remap(
775 arena: &mut NodeArena,
776 all_edges: &mut [Vec<PendingEdge>],
777 remap: &HashMap<StringId, StringId>,
778) {
779 if remap.is_empty() {
780 return;
781 }
782
783 for (_id, entry) in arena.iter_mut() {
785 remap_node_entry_global(entry, remap);
786 }
787
788 for file_edges in all_edges.iter_mut() {
790 for edge in file_edges.iter_mut() {
791 remap_edge_kind_string_ids(&mut edge.kind, remap);
792 }
793 }
794}
795
796#[derive(Debug, Default)]
798pub struct UnificationStats {
799 pub candidate_pairs_examined: usize,
801 pub nodes_merged: usize,
803 pub edges_rewritten: usize,
805 pub nodes_inert: usize,
807 pub elapsed_ms: u64,
809}
810
811pub(crate) fn phase4c_prime_unify_cross_file_nodes<
842 G: crate::graph::unified::mutation_target::GraphMutationTarget,
843>(
844 graph: &mut G,
845 all_edges: &mut [Vec<PendingEdge>],
846) -> UnificationStats {
847 use crate::graph::unified::mutation_target::GraphMutationTarget;
848
849 use super::helper::CALL_COMPATIBLE_KINDS;
850 use super::unification::{NodeRemapTable, merge_node_into};
851 use std::time::Instant;
852
853 let start = Instant::now();
854 let mut stats = UnificationStats::default();
855
856 let mut qn_groups: HashMap<crate::graph::unified::string::StringId, Vec<NodeId>> =
859 HashMap::new();
860
861 for (node_id, entry) in GraphMutationTarget::nodes(graph).iter() {
862 if !CALL_COMPATIBLE_KINDS.contains(&entry.kind) {
863 continue;
864 }
865 if let Some(qn_id) = entry.qualified_name {
866 qn_groups.entry(qn_id).or_default().push(node_id);
867 }
868 }
869
870 let groups_to_unify: Vec<Vec<NodeId>> = qn_groups
872 .into_values()
873 .filter(|group| {
874 if group.len() >= 2 {
875 stats.candidate_pairs_examined += 1;
876 true
877 } else {
878 false
879 }
880 })
881 .collect();
882
883 let mut remap = NodeRemapTable::with_capacity(groups_to_unify.len());
885
886 let path_keys: HashMap<NodeId, String> = {
905 let arena = GraphMutationTarget::nodes(graph);
906 let files = GraphMutationTarget::files(graph);
907 let mut out: HashMap<NodeId, String> =
908 HashMap::with_capacity(groups_to_unify.iter().map(Vec::len).sum());
909 for group in &groups_to_unify {
910 for &nid in group {
911 if out.contains_key(&nid) {
912 continue;
913 }
914 let key = arena
915 .get(nid)
916 .and_then(|entry| files.resolve(entry.file))
917 .map_or_else(String::new, |path| path.to_string_lossy().into_owned());
918 out.insert(nid, key);
919 }
920 }
921 out
922 };
923 let empty_path_key = String::new();
924
925 for group in &groups_to_unify {
926 let winner_id = *group
930 .iter()
931 .max_by(|&&a, &&b| {
932 let ea = GraphMutationTarget::nodes(graph).get(a);
933 let eb = GraphMutationTarget::nodes(graph).get(b);
934 match (ea, eb) {
935 (Some(ea), Some(eb)) => {
936 let a_real = ea.start_line > 0;
938 let b_real = eb.start_line > 0;
939 match (a_real, b_real) {
940 (true, false) => std::cmp::Ordering::Greater,
941 (false, true) => std::cmp::Ordering::Less,
942 _ => {
943 let a_range = ea.end_line.saturating_sub(ea.start_line);
945 let b_range = eb.end_line.saturating_sub(eb.start_line);
946 a_range
947 .cmp(&b_range)
948 .then_with(|| {
949 let pa = path_keys.get(&a).unwrap_or(&empty_path_key);
954 let pb = path_keys.get(&b).unwrap_or(&empty_path_key);
955 pb.cmp(pa)
956 })
957 .then_with(|| {
958 b.index().cmp(&a.index())
963 })
964 }
965 }
966 }
967 (Some(_), None) => std::cmp::Ordering::Greater,
968 (None, Some(_)) => std::cmp::Ordering::Less,
969 (None, None) => std::cmp::Ordering::Equal,
970 }
971 })
972 .expect("group is non-empty");
973
974 for &node_id in group {
976 if node_id == winner_id {
977 continue;
978 }
979 match merge_node_into(GraphMutationTarget::nodes_mut(graph), node_id, winner_id) {
980 Ok(()) => {
981 remap.insert(node_id, winner_id);
982 stats.nodes_merged += 1;
983 stats.nodes_inert += 1;
984 }
985 Err(e) => {
986 log::debug!("Phase 4c-prime: skipping merge ({e})");
987 }
988 }
989 }
990 }
991
992 if !remap.is_empty() {
1009 let pre_count: usize = all_edges.iter().map(|v| v.len()).sum();
1010 remap.apply_to_edges(all_edges);
1011 remap.apply_to_committed_edges(GraphMutationTarget::edges(graph));
1012 stats.edges_rewritten = pre_count; }
1061
1062 stats.elapsed_ms = start.elapsed().as_millis() as u64;
1063 stats
1064}
1065
1066#[must_use]
1073pub fn pending_edges_to_delta(
1074 per_file_edges: &[Vec<PendingEdge>],
1075 seq_start: u64,
1076) -> (Vec<Vec<DeltaEdge>>, u64) {
1077 let mut seq = seq_start;
1078 let mut result = Vec::with_capacity(per_file_edges.len());
1079
1080 for file_edges in per_file_edges {
1081 let mut delta_vec = Vec::with_capacity(file_edges.len());
1082 for edge in file_edges {
1083 delta_vec.push(DeltaEdge::with_spans(
1084 edge.source,
1085 edge.target,
1086 edge.kind.clone(),
1087 seq,
1088 DeltaOp::Add,
1089 edge.file,
1090 edge.spans.clone(),
1091 ));
1092 seq += 1;
1093 }
1094 result.push(delta_vec);
1095 }
1096
1097 (result, seq)
1098}
1099
1100pub(crate) fn rebuild_indices<G: crate::graph::unified::mutation_target::GraphMutationTarget>(
1118 graph: &mut G,
1119) {
1120 let (nodes, indices) = graph.nodes_and_indices_mut();
1121 indices.build_from_arena(nodes);
1122}
1123
1124pub(crate) fn phase4d_bulk_insert_edges<
1181 G: crate::graph::unified::mutation_target::GraphMutationTarget,
1182>(
1183 graph: &mut G,
1184 per_file_edges: &[Vec<PendingEdge>],
1185) -> u64 {
1186 let edge_seq_start = graph.edges().forward().seq_counter();
1190 let (delta_edge_vecs, final_seq) = pending_edges_to_delta(per_file_edges, edge_seq_start);
1191 let total_edge_count: u64 = delta_edge_vecs.iter().map(|v| v.len() as u64).sum();
1192 if total_edge_count > 0 {
1193 graph
1194 .edges_mut()
1195 .add_edges_bulk_ordered(&delta_edge_vecs, total_edge_count);
1196 }
1197 final_seq
1198}
1199
1200#[cfg(test)]
1201mod tests {
1202 use super::*;
1203
1204 #[test]
1205 fn test_compute_commit_plan_basic() {
1206 let file_ids = vec![FileId::new(0), FileId::new(1), FileId::new(2)];
1207 let node_counts = vec![3, 0, 5];
1208 let string_counts = vec![2, 1, 3];
1209 let edge_counts = vec![4, 0, 6];
1210
1211 let plan = compute_commit_plan(
1212 &node_counts,
1213 &string_counts,
1214 &edge_counts,
1215 &file_ids,
1216 0,
1217 1, );
1219
1220 assert_eq!(plan.total_nodes, 8);
1221 assert_eq!(plan.total_strings, 6);
1222 assert_eq!(plan.total_edges, 10);
1223
1224 assert_eq!(plan.file_plans[0].node_range, 0..3);
1226 assert_eq!(plan.file_plans[0].string_range, 1..3);
1227
1228 assert_eq!(plan.file_plans[1].node_range, 3..3);
1230 assert_eq!(plan.file_plans[1].string_range, 3..4);
1231
1232 assert_eq!(plan.file_plans[2].node_range, 3..8);
1234 assert_eq!(plan.file_plans[2].string_range, 4..7);
1235 }
1236
1237 #[test]
1238 fn test_compute_commit_plan_with_offsets() {
1239 let file_ids = vec![FileId::new(5)];
1240 let plan = compute_commit_plan(&[10], &[5], &[7], &file_ids, 100, 50);
1241 assert_eq!(plan.file_plans[0].node_range, 100..110);
1242 assert_eq!(plan.file_plans[0].string_range, 50..55);
1243 assert_eq!(plan.total_nodes, 10);
1244 assert_eq!(plan.total_strings, 5);
1245 assert_eq!(plan.total_edges, 7);
1246 }
1247
1248 #[test]
1249 fn test_compute_commit_plan_empty() {
1250 let plan = compute_commit_plan(&[], &[], &[], &[], 0, 1);
1251 assert_eq!(plan.total_nodes, 0);
1252 assert_eq!(plan.total_strings, 0);
1253 assert_eq!(plan.total_edges, 0);
1254 assert!(plan.file_plans.is_empty());
1255 }
1256
1257 #[test]
1258 fn test_remap_string_id_basic() {
1259 let mut remap = HashMap::new();
1260 remap.insert(StringId::new(1), StringId::new(100));
1261
1262 let mut id = StringId::new(1);
1263 remap_string_id(&mut id, &remap);
1264 assert_eq!(id, StringId::new(100));
1265 }
1266
1267 #[test]
1268 fn test_remap_string_id_not_in_remap() {
1269 let remap = HashMap::new();
1270 let mut id = StringId::new(42);
1271 remap_string_id(&mut id, &remap);
1272 assert_eq!(id, StringId::new(42)); }
1274
1275 #[test]
1276 fn test_remap_option_string_id() {
1277 let mut remap = HashMap::new();
1278 remap.insert(StringId::new(5), StringId::new(50));
1279
1280 let mut some_id = Some(StringId::new(5));
1281 remap_option_string_id(&mut some_id, &remap);
1282 assert_eq!(some_id, Some(StringId::new(50)));
1283
1284 let mut none_id: Option<StringId> = None;
1285 remap_option_string_id(&mut none_id, &remap);
1286 assert_eq!(none_id, None);
1287 }
1288
1289 #[test]
1290 fn test_remap_edge_kind_imports() {
1291 let mut remap = HashMap::new();
1292 remap.insert(StringId::new(1), StringId::new(100));
1293
1294 let mut kind = EdgeKind::Imports {
1295 alias: Some(StringId::new(1)),
1296 is_wildcard: false,
1297 };
1298 remap_edge_kind_string_ids(&mut kind, &remap);
1299 assert!(
1300 matches!(kind, EdgeKind::Imports { alias: Some(id), .. } if id == StringId::new(100))
1301 );
1302 }
1303
1304 #[test]
1305 fn test_remap_edge_kind_trait_method_binding() {
1306 let mut remap = HashMap::new();
1307 remap.insert(StringId::new(1), StringId::new(100));
1308 remap.insert(StringId::new(2), StringId::new(200));
1309
1310 let mut kind = EdgeKind::TraitMethodBinding {
1311 trait_name: StringId::new(1),
1312 impl_type: StringId::new(2),
1313 is_ambiguous: false,
1314 };
1315 remap_edge_kind_string_ids(&mut kind, &remap);
1316 assert!(
1317 matches!(kind, EdgeKind::TraitMethodBinding { trait_name, impl_type, .. }
1318 if trait_name == StringId::new(100) && impl_type == StringId::new(200))
1319 );
1320 }
1321
1322 #[test]
1323 fn test_remap_edge_kind_no_op_variants() {
1324 let remap = HashMap::new();
1325
1326 let mut kind = EdgeKind::Defines;
1328 remap_edge_kind_string_ids(&mut kind, &remap);
1329 assert!(matches!(kind, EdgeKind::Defines));
1330
1331 let mut kind = EdgeKind::Calls {
1333 argument_count: 3,
1334 is_async: true,
1335 };
1336 remap_edge_kind_string_ids(&mut kind, &remap);
1337 assert!(matches!(
1338 kind,
1339 EdgeKind::Calls {
1340 argument_count: 3,
1341 is_async: true,
1342 }
1343 ));
1344 }
1345
1346 fn placeholder_entry() -> NodeEntry {
1347 use crate::graph::unified::node::NodeKind;
1348 NodeEntry::new(NodeKind::Function, StringId::new(0), FileId::new(0))
1349 }
1350
1351 #[test]
1352 fn test_phase2_assign_ranges_basic() {
1353 use super::super::staging::StagingGraph;
1354
1355 let mut sg0 = StagingGraph::new();
1357 let mut sg1 = StagingGraph::new();
1358
1359 let entry0 = placeholder_entry();
1361 let n0 = sg0.add_node(entry0.clone());
1362 let n1 = sg0.add_node(entry0.clone());
1363 sg0.intern_string(StringId::new_local(0), "hello".into());
1364 sg0.add_edge(
1365 n0,
1366 n1,
1367 EdgeKind::Calls {
1368 argument_count: 0,
1369 is_async: false,
1370 },
1371 FileId::new(0),
1372 );
1373
1374 sg1.add_node(entry0);
1376 sg1.intern_string(StringId::new_local(0), "world".into());
1377 sg1.intern_string(StringId::new_local(1), "foo".into());
1378
1379 let file_ids = vec![FileId::new(10), FileId::new(11)];
1380 let offsets = GlobalOffsets {
1381 node_offset: 5,
1382 string_offset: 3,
1383 };
1384
1385 let plan = phase2_assign_ranges(&[&sg0, &sg1], &file_ids, &offsets);
1386
1387 assert_eq!(plan.file_plans[0].node_range, 5..7);
1389 assert_eq!(plan.file_plans[0].string_range, 3..4);
1390
1391 assert_eq!(plan.file_plans[1].node_range, 7..8);
1393 assert_eq!(plan.file_plans[1].string_range, 4..6);
1394
1395 assert_eq!(plan.total_nodes, 3);
1396 assert_eq!(plan.total_strings, 3);
1397 assert_eq!(plan.total_edges, 1);
1398 }
1399
1400 #[test]
1401 fn test_phase3_parallel_commit_basic() {
1402 use super::super::staging::StagingGraph;
1403 use crate::graph::unified::concurrent::CodeGraph;
1404 use crate::graph::unified::node::NodeKind;
1405 let mut sg = StagingGraph::new();
1414 let local_name = StringId::new_local(0);
1415 sg.intern_string(local_name, "my_func".into());
1416
1417 let entry = NodeEntry::new(NodeKind::Function, local_name, FileId::new(0));
1418 let n0 = sg.add_node(entry.clone());
1419
1420 let entry2 = NodeEntry::new(NodeKind::Variable, local_name, FileId::new(0));
1421 let n1 = sg.add_node(entry2);
1422
1423 sg.add_edge(
1424 n0,
1425 n1,
1426 EdgeKind::Calls {
1427 argument_count: 0,
1428 is_async: false,
1429 },
1430 FileId::new(0),
1431 );
1432
1433 let file_ids = vec![FileId::new(5)];
1434
1435 let mut graph = CodeGraph::new();
1439 graph
1440 .nodes_mut()
1441 .alloc_range(10, &placeholder_entry())
1442 .unwrap();
1443 let string_start = graph.strings_mut().alloc_range(1).unwrap();
1444 assert_eq!(string_start, 1); let offsets = GlobalOffsets {
1447 node_offset: 10, string_offset: string_start,
1449 };
1450 let plan = phase2_assign_ranges(&[&sg], &file_ids, &offsets);
1451 assert_eq!(plan.file_plans[0].node_range, 10..12);
1452
1453 graph
1455 .nodes_mut()
1456 .alloc_range(plan.total_nodes, &placeholder_entry())
1457 .unwrap();
1458 graph.strings_mut().alloc_range(plan.total_strings).unwrap();
1459
1460 let result = phase3_parallel_commit(&plan, &[&sg], &mut graph);
1463
1464 assert_eq!(result.total_nodes_written, 2);
1466 assert_eq!(result.total_strings_written, 1);
1467
1468 let global_name = StringId::new(string_start);
1470 assert_eq!(&*graph.strings().resolve(global_name).unwrap(), "my_func");
1471
1472 assert_eq!(result.per_file_edges.len(), 1);
1474 assert_eq!(result.per_file_edges[0].len(), 1);
1475
1476 let edge = &result.per_file_edges[0][0];
1478 assert_eq!(edge.file, FileId::new(5));
1479 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);
1486 assert_eq!(
1487 result.per_file_node_ids[0],
1488 vec![NodeId::new(10, 1), NodeId::new(11, 1)]
1489 );
1490 }
1491
1492 #[test]
1493 fn test_phase3_parallel_commit_empty() {
1494 use crate::graph::unified::concurrent::CodeGraph;
1495
1496 let mut graph = CodeGraph::new();
1497
1498 let plan = ChunkCommitPlan {
1499 file_plans: vec![],
1500 total_nodes: 0,
1501 total_strings: 0,
1502 total_edges: 0,
1503 };
1504
1505 let result = phase3_parallel_commit(&plan, &[], &mut graph);
1506 assert!(result.per_file_edges.is_empty());
1507 assert!(result.per_file_node_ids.is_empty());
1508 assert_eq!(result.total_nodes_written, 0);
1509 assert_eq!(result.total_strings_written, 0);
1510 }
1511
1512 #[test]
1525 #[cfg(feature = "rebuild-internals")]
1526 fn phase3_parallel_commit_runs_against_rebuild_graph() {
1527 use super::super::staging::StagingGraph;
1528 use crate::graph::unified::concurrent::CodeGraph;
1529 use crate::graph::unified::mutation_target::GraphMutationTarget;
1530 use crate::graph::unified::node::NodeKind;
1531
1532 let mut sg = StagingGraph::new();
1536 let local_name = StringId::new_local(0);
1537 sg.intern_string(local_name, "rebuild_target".into());
1538 let entry = NodeEntry::new(NodeKind::Function, local_name, FileId::new(0));
1539 let n0 = sg.add_node(entry.clone());
1540 let entry2 = NodeEntry::new(NodeKind::Variable, local_name, FileId::new(0));
1541 let n1 = sg.add_node(entry2);
1542 sg.add_edge(
1543 n0,
1544 n1,
1545 EdgeKind::Calls {
1546 argument_count: 0,
1547 is_async: false,
1548 },
1549 FileId::new(0),
1550 );
1551
1552 let mut rebuild = {
1556 let graph = CodeGraph::new();
1557 graph.clone_for_rebuild()
1558 };
1559
1560 rebuild
1566 .nodes_mut()
1567 .alloc_range(10, &placeholder_entry())
1568 .unwrap();
1569 let string_start = rebuild.strings_mut().alloc_range(1).unwrap();
1570 assert_eq!(string_start, 1);
1571
1572 let file_ids = vec![FileId::new(5)];
1573 let offsets = GlobalOffsets {
1574 node_offset: 10,
1575 string_offset: string_start,
1576 };
1577 let plan = phase2_assign_ranges(&[&sg], &file_ids, &offsets);
1578
1579 rebuild
1580 .nodes_mut()
1581 .alloc_range(plan.total_nodes, &placeholder_entry())
1582 .unwrap();
1583 rebuild
1584 .strings_mut()
1585 .alloc_range(plan.total_strings)
1586 .unwrap();
1587
1588 let result = phase3_parallel_commit(&plan, &[&sg], &mut rebuild);
1590
1591 let committed_ids = &result.per_file_node_ids[0];
1605 assert_eq!(
1606 committed_ids,
1607 &vec![NodeId::new(10, 1), NodeId::new(11, 1)],
1608 "Phase 3 must commit into slots 10..12 on the rebuild-local arena"
1609 );
1610
1611 let resolved_name = rebuild
1612 .nodes_mut()
1613 .get(NodeId::new(10, 1))
1614 .map(|entry| entry.name)
1615 .expect("committed node must exist in rebuild arena");
1616 let resolved_str = rebuild
1620 .strings_mut()
1621 .resolve(resolved_name)
1622 .expect("name must resolve in rebuild-local interner");
1623 assert_eq!(&*resolved_str, "rebuild_target");
1624
1625 assert_eq!(result.total_nodes_written, 2);
1627 assert_eq!(result.total_strings_written, 1);
1628 assert_eq!(result.per_file_edges.len(), 1);
1629 assert_eq!(result.per_file_edges[0].len(), 1);
1630 let edge = &result.per_file_edges[0][0];
1631 assert_eq!(edge.file, FileId::new(5));
1632 assert_eq!(edge.source, NodeId::new(10, 1));
1633 assert_eq!(edge.target, NodeId::new(11, 1));
1634 }
1635
1636 #[test]
1637 fn test_commit_single_file_string_remap() {
1638 use super::super::staging::StagingGraph;
1639 use crate::graph::unified::node::NodeKind;
1640
1641 let mut sg = StagingGraph::new();
1642 let local_0 = StringId::new_local(0);
1643 let local_1 = StringId::new_local(1);
1644 sg.intern_string(local_0, "alpha".into());
1645 sg.intern_string(local_1, "beta".into());
1646
1647 let mut entry = NodeEntry::new(NodeKind::Function, local_0, FileId::new(0));
1648 entry.signature = Some(local_1);
1649 sg.add_node(entry);
1650
1651 let plan = FilePlan {
1652 parsed_index: 0,
1653 file_id: FileId::new(42),
1654 node_range: 10..11,
1655 string_range: 20..22,
1656 };
1657
1658 let mut node_slots = vec![Slot::new_occupied(1, placeholder_entry())];
1659 let mut str_slots: Vec<Option<Arc<str>>> = vec![None, None];
1660 let mut rc_slots: Vec<u32> = vec![0, 0];
1661
1662 let result = commit_single_file(&sg, &plan, &mut node_slots, &mut str_slots, &mut rc_slots);
1663
1664 assert_eq!(str_slots[0].as_deref(), Some("alpha"));
1666 assert_eq!(str_slots[1].as_deref(), Some("beta"));
1667 assert_eq!(rc_slots[0], 1);
1668 assert_eq!(rc_slots[1], 1);
1669 assert_eq!(result.strings_written, 2);
1670
1671 if let crate::graph::unified::storage::SlotState::Occupied(entry) = node_slots[0].state() {
1673 assert_eq!(entry.name, StringId::new(20)); assert_eq!(entry.signature, Some(StringId::new(21))); assert_eq!(entry.file, FileId::new(42));
1676 } else {
1677 panic!("Expected occupied slot");
1678 }
1679 assert_eq!(result.nodes_written, 1);
1680
1681 assert_eq!(result.node_ids, vec![NodeId::new(10, 1)]);
1683
1684 assert!(result.edges.is_empty());
1686 }
1687
1688 #[test]
1689 fn test_remap_edge_kind_message_queue_other() {
1690 let mut remap = HashMap::new();
1691 remap.insert(StringId::new(10), StringId::new(110));
1692 remap.insert(StringId::new(20), StringId::new(220));
1693
1694 let mut kind = EdgeKind::MessageQueue {
1695 protocol: MqProtocol::Other(StringId::new(10)),
1696 topic: Some(StringId::new(20)),
1697 };
1698 remap_edge_kind_string_ids(&mut kind, &remap);
1699 assert!(matches!(
1700 kind,
1701 EdgeKind::MessageQueue {
1702 protocol: MqProtocol::Other(proto),
1703 topic: Some(topic),
1704 } if proto == StringId::new(110) && topic == StringId::new(220)
1705 ));
1706 }
1707
1708 #[test]
1711 fn test_phase4_apply_global_remap_basic() {
1712 use crate::graph::unified::node::NodeKind;
1713 use crate::graph::unified::storage::NodeArena;
1714
1715 let mut arena = NodeArena::new();
1716
1717 let entry1 = NodeEntry::new(NodeKind::Function, StringId::new(1), FileId::new(0));
1719 let mut entry2 = NodeEntry::new(NodeKind::Variable, StringId::new(2), FileId::new(0));
1720 entry2.signature = Some(StringId::new(3));
1721
1722 arena.alloc(entry1).unwrap();
1723 arena.alloc(entry2).unwrap();
1724
1725 let mut all_edges = vec![vec![PendingEdge {
1727 source: NodeId::new(0, 1),
1728 target: NodeId::new(1, 1),
1729 kind: EdgeKind::Imports {
1730 alias: Some(StringId::new(3)),
1731 is_wildcard: false,
1732 },
1733 file: FileId::new(0),
1734 spans: vec![],
1735 }]];
1736
1737 let mut remap = HashMap::new();
1739 remap.insert(StringId::new(2), StringId::new(1));
1740 remap.insert(StringId::new(3), StringId::new(1));
1741
1742 phase4_apply_global_remap(&mut arena, &mut all_edges, &remap);
1743
1744 let (_, entry) = arena.iter().nth(1).unwrap();
1746 assert_eq!(entry.name, StringId::new(1));
1747 assert_eq!(entry.signature, Some(StringId::new(1)));
1748
1749 if let EdgeKind::Imports { alias, .. } = &all_edges[0][0].kind {
1751 assert_eq!(*alias, Some(StringId::new(1)));
1752 } else {
1753 panic!("Expected Imports edge");
1754 }
1755 }
1756
1757 #[test]
1758 fn test_phase4_apply_global_remap_empty() {
1759 use crate::graph::unified::storage::NodeArena;
1760
1761 let mut arena = NodeArena::new();
1762 let mut edges: Vec<Vec<PendingEdge>> = vec![];
1763 let remap = HashMap::new();
1764
1765 phase4_apply_global_remap(&mut arena, &mut edges, &remap);
1767 }
1768
1769 #[test]
1770 fn test_pending_edges_to_delta_basic() {
1771 let edges = vec![
1772 vec![
1773 PendingEdge {
1774 source: NodeId::new(0, 1),
1775 target: NodeId::new(1, 1),
1776 kind: EdgeKind::Calls {
1777 argument_count: 0,
1778 is_async: false,
1779 },
1780 file: FileId::new(0),
1781 spans: vec![],
1782 },
1783 PendingEdge {
1784 source: NodeId::new(1, 1),
1785 target: NodeId::new(2, 1),
1786 kind: EdgeKind::References,
1787 file: FileId::new(0),
1788 spans: vec![],
1789 },
1790 ],
1791 vec![PendingEdge {
1792 source: NodeId::new(3, 1),
1793 target: NodeId::new(4, 1),
1794 kind: EdgeKind::Defines,
1795 file: FileId::new(1),
1796 spans: vec![],
1797 }],
1798 ];
1799
1800 let (deltas, final_seq) = pending_edges_to_delta(&edges, 100);
1801
1802 assert_eq!(deltas.len(), 2);
1803 assert_eq!(deltas[0].len(), 2);
1804 assert_eq!(deltas[1].len(), 1);
1805 assert_eq!(final_seq, 103);
1806
1807 assert_eq!(deltas[0][0].seq, 100);
1809 assert_eq!(deltas[0][1].seq, 101);
1810 assert_eq!(deltas[1][0].seq, 102);
1811
1812 assert!(matches!(deltas[0][0].op, DeltaOp::Add));
1814 assert!(matches!(deltas[1][0].op, DeltaOp::Add));
1815 }
1816
1817 #[test]
1818 fn test_pending_edges_to_delta_empty() {
1819 let edges: Vec<Vec<PendingEdge>> = vec![];
1820 let (deltas, final_seq) = pending_edges_to_delta(&edges, 0);
1821 assert!(deltas.is_empty());
1822 assert_eq!(final_seq, 0);
1823 }
1824
1825 #[test]
1844 #[cfg(feature = "rebuild-internals")]
1845 fn phase4c_prime_unify_cross_file_nodes_runs_against_rebuild_graph() {
1846 use crate::graph::unified::concurrent::CodeGraph;
1847 use crate::graph::unified::mutation_target::GraphMutationTarget;
1848 use crate::graph::unified::node::NodeKind;
1849
1850 let mut rebuild = {
1851 let graph = CodeGraph::new();
1852 graph.clone_for_rebuild()
1853 };
1854
1855 let qname_sid = rebuild.strings_mut().intern("my_mod::my_func").unwrap();
1858
1859 let file_a = FileId::new(7);
1861 let file_b = FileId::new(8);
1862
1863 let mut winner_entry = NodeEntry::new(NodeKind::Function, qname_sid, file_a);
1868 winner_entry.qualified_name = Some(qname_sid);
1869 winner_entry.start_line = 10;
1870 winner_entry.end_line = 30;
1871
1872 let mut loser_entry = NodeEntry::new(NodeKind::Function, qname_sid, file_b);
1873 loser_entry.qualified_name = Some(qname_sid);
1874 loser_entry.start_line = 5;
1876 loser_entry.end_line = 6;
1877
1878 let winner_id = rebuild.nodes_mut().alloc(winner_entry).unwrap();
1879 let loser_id = rebuild.nodes_mut().alloc(loser_entry).unwrap();
1880
1881 let mut all_edges = vec![vec![PendingEdge {
1884 source: winner_id, target: loser_id,
1886 kind: EdgeKind::Calls {
1887 argument_count: 0,
1888 is_async: false,
1889 },
1890 file: file_b,
1891 spans: vec![],
1892 }]];
1893
1894 let stats = phase4c_prime_unify_cross_file_nodes(&mut rebuild, &mut all_edges);
1895
1896 assert_eq!(stats.nodes_merged, 1, "exactly one loser was tombstoned");
1898 assert_eq!(stats.candidate_pairs_examined, 1);
1899 assert_eq!(stats.edges_rewritten, 1);
1900
1901 let winner_entry_after = GraphMutationTarget::nodes(&rebuild)
1903 .get(winner_id)
1904 .expect("winner must remain live");
1905 assert_eq!(
1906 winner_entry_after.qualified_name,
1907 Some(qname_sid),
1908 "winner keeps its qualified_name"
1909 );
1910
1911 let loser_entry_after = GraphMutationTarget::nodes(&rebuild)
1914 .get(loser_id)
1915 .expect("loser slot remains live (inert) per §F.1 bijection");
1916 assert_eq!(
1917 loser_entry_after.qualified_name, None,
1918 "loser qualified_name cleared by merge_node_into"
1919 );
1920
1921 assert_eq!(
1923 all_edges[0][0].target, winner_id,
1924 "PendingEdge.target rewritten from loser → winner"
1925 );
1926 }
1927
1928 #[test]
1938 #[cfg(feature = "rebuild-internals")]
1939 fn phase4c_prime_tie_break_prefers_lex_smaller_path_over_node_id() {
1940 use crate::graph::unified::concurrent::CodeGraph;
1941 use crate::graph::unified::node::NodeKind;
1942 use std::path::Path;
1943
1944 let mut graph = CodeGraph::new();
1945 let qname = graph.strings_mut().intern("shared_qname").unwrap();
1946 let high_path_file = graph
1952 .files_mut()
1953 .register(Path::new("zzz_late.rs"))
1954 .unwrap();
1955 let low_path_file = graph
1956 .files_mut()
1957 .register(Path::new("aaa_early.rs"))
1958 .unwrap();
1959
1960 let mut high_entry = NodeEntry::new(NodeKind::Function, qname, high_path_file);
1966 high_entry.qualified_name = Some(qname);
1967 high_entry.start_line = 10;
1968 high_entry.end_line = 20;
1969 let high_node = graph.nodes_mut().alloc(high_entry).unwrap();
1970
1971 let mut low_entry = NodeEntry::new(NodeKind::Function, qname, low_path_file);
1972 low_entry.qualified_name = Some(qname);
1973 low_entry.start_line = 10;
1976 low_entry.end_line = 20;
1977 let low_node = graph.nodes_mut().alloc(low_entry).unwrap();
1978
1979 graph.rebuild_indices();
1980
1981 let mut all_edges: Vec<Vec<PendingEdge>> = Vec::new();
1982 let stats = phase4c_prime_unify_cross_file_nodes(&mut graph, &mut all_edges);
1983
1984 assert_eq!(
1985 stats.nodes_merged, 1,
1986 "one of the duplicate nodes must be merged into the other"
1987 );
1988
1989 let low_after = graph
1992 .nodes()
1993 .get(low_node)
1994 .expect("winner slot remains live");
1995 assert_eq!(
1996 low_after.qualified_name,
1997 Some(qname),
1998 "path-earlier node keeps qualified_name as the unification winner"
1999 );
2000
2001 let high_after = graph
2004 .nodes()
2005 .get(high_node)
2006 .expect("loser slot remains inert (Gate 0d bijection contract)");
2007 assert_eq!(
2008 high_after.qualified_name, None,
2009 "path-later node loses even when its NodeId::index() is smaller"
2010 );
2011 }
2012
2013 #[test]
2020 #[cfg(feature = "rebuild-internals")]
2021 fn phase4c_prime_tie_break_falls_back_to_smaller_node_id_on_identical_path() {
2022 use crate::graph::unified::concurrent::CodeGraph;
2023 use crate::graph::unified::node::NodeKind;
2024 use std::path::Path;
2025
2026 let mut graph = CodeGraph::new();
2027 let qname = graph.strings_mut().intern("shared_qname").unwrap();
2028 let file = graph.files_mut().register(Path::new("shared.rs")).unwrap();
2029
2030 let mut first_entry = NodeEntry::new(NodeKind::Function, qname, file);
2036 first_entry.qualified_name = Some(qname);
2037 first_entry.start_line = 1;
2038 first_entry.end_line = 5;
2039 let first_node = graph.nodes_mut().alloc(first_entry).unwrap();
2040
2041 let mut second_entry = NodeEntry::new(NodeKind::Function, qname, file);
2042 second_entry.qualified_name = Some(qname);
2043 second_entry.start_line = 1;
2044 second_entry.end_line = 5;
2045 let second_node = graph.nodes_mut().alloc(second_entry).unwrap();
2046
2047 assert!(
2048 first_node.index() < second_node.index(),
2049 "precondition: first_node's arena slot precedes second_node's"
2050 );
2051
2052 graph.rebuild_indices();
2053
2054 let mut all_edges: Vec<Vec<PendingEdge>> = Vec::new();
2055 let stats = phase4c_prime_unify_cross_file_nodes(&mut graph, &mut all_edges);
2056
2057 assert_eq!(stats.nodes_merged, 1);
2058
2059 let winner_after = graph.nodes().get(first_node).expect("winner live");
2061 assert_eq!(
2062 winner_after.qualified_name,
2063 Some(qname),
2064 "smaller-index node wins the same-path / same-span tie-break"
2065 );
2066 let loser_after = graph.nodes().get(second_node).expect("loser inert");
2067 assert_eq!(
2068 loser_after.qualified_name, None,
2069 "larger-index node loses the same-path / same-span tie-break"
2070 );
2071 }
2072
2073 #[test]
2078 #[cfg(feature = "rebuild-internals")]
2079 fn rebuild_indices_runs_against_rebuild_graph() {
2080 use crate::graph::unified::concurrent::CodeGraph;
2081 use crate::graph::unified::mutation_target::GraphMutationTarget;
2082 use crate::graph::unified::node::NodeKind;
2083
2084 let mut code_graph = CodeGraph::new();
2086 let alpha_id_code = code_graph.strings_mut().intern("alpha").unwrap();
2087 let mut code_entry = NodeEntry::new(NodeKind::Function, alpha_id_code, FileId::new(1));
2088 code_entry.qualified_name = Some(alpha_id_code);
2089 let code_node_id = code_graph.nodes_mut().alloc(code_entry).unwrap();
2090 rebuild_indices(&mut code_graph);
2091 let code_buckets_function: Vec<NodeId> =
2092 code_graph.indices().by_kind(NodeKind::Function).to_vec();
2093
2094 let mut rebuild = {
2096 let graph = CodeGraph::new();
2097 graph.clone_for_rebuild()
2098 };
2099 let alpha_id_rebuild = rebuild.strings_mut().intern("alpha").unwrap();
2100 let mut rebuild_entry =
2101 NodeEntry::new(NodeKind::Function, alpha_id_rebuild, FileId::new(1));
2102 rebuild_entry.qualified_name = Some(alpha_id_rebuild);
2103 let rebuild_node_id = rebuild.nodes_mut().alloc(rebuild_entry).unwrap();
2104 rebuild_indices(&mut rebuild);
2105
2106 assert_eq!(code_node_id, rebuild_node_id);
2110
2111 let rebuild_buckets_function: Vec<NodeId> = GraphMutationTarget::indices(&rebuild)
2115 .by_kind(NodeKind::Function)
2116 .to_vec();
2117
2118 assert_eq!(
2119 code_buckets_function, rebuild_buckets_function,
2120 "rebuild_indices must produce equivalent Function buckets on both paths"
2121 );
2122 let by_name: Vec<NodeId> = GraphMutationTarget::indices(&rebuild)
2124 .by_name(alpha_id_rebuild)
2125 .to_vec();
2126 assert_eq!(by_name, vec![rebuild_node_id]);
2127 }
2128
2129 #[test]
2134 #[cfg(feature = "rebuild-internals")]
2135 fn phase4d_bulk_insert_edges_runs_against_rebuild_graph() {
2136 use crate::graph::unified::concurrent::CodeGraph;
2137 use crate::graph::unified::mutation_target::GraphMutationTarget;
2138 use crate::graph::unified::node::NodeKind;
2139
2140 let mut rebuild = {
2141 let graph = CodeGraph::new();
2142 graph.clone_for_rebuild()
2143 };
2144
2145 let name_sid = rebuild.strings_mut().intern("edge_target").unwrap();
2146 let file = FileId::new(3);
2147
2148 let n_source = rebuild
2149 .nodes_mut()
2150 .alloc(NodeEntry::new(NodeKind::Function, name_sid, file))
2151 .unwrap();
2152 let n_target = rebuild
2153 .nodes_mut()
2154 .alloc(NodeEntry::new(NodeKind::Variable, name_sid, file))
2155 .unwrap();
2156
2157 let pre_counter = GraphMutationTarget::edges(&rebuild).forward().seq_counter();
2159
2160 let per_file_edges = vec![vec![
2161 PendingEdge {
2162 source: n_source,
2163 target: n_target,
2164 kind: EdgeKind::Calls {
2165 argument_count: 0,
2166 is_async: false,
2167 },
2168 file,
2169 spans: vec![],
2170 },
2171 PendingEdge {
2172 source: n_source,
2173 target: n_target,
2174 kind: EdgeKind::Calls {
2175 argument_count: 1,
2176 is_async: false,
2177 },
2178 file,
2179 spans: vec![],
2180 },
2181 ]];
2182
2183 let final_seq = phase4d_bulk_insert_edges(&mut rebuild, &per_file_edges);
2184
2185 assert_eq!(
2187 final_seq,
2188 pre_counter + 2,
2189 "phase4d_bulk_insert_edges must advance seq by edge count"
2190 );
2191
2192 let forward = GraphMutationTarget::edges(&rebuild).forward();
2194 let after_counter = forward.seq_counter();
2195 assert_eq!(after_counter, pre_counter + 2);
2196 assert!(
2198 forward.delta().iter().filter(|e| e.is_add()).count() >= 2,
2199 "expected at least two Add edges in the rebuild-local forward delta"
2200 );
2201 drop(forward);
2202
2203 let empty_final = phase4d_bulk_insert_edges(&mut rebuild, &[]);
2205 assert_eq!(empty_final, pre_counter + 2, "empty input is a no-op");
2206 }
2207
2208 #[test]
2225 fn phase4d_preserves_property_sourced_typeof_field_edges() {
2226 use crate::graph::unified::edge::kind::TypeOfContext;
2227
2228 let struct_id = NodeId::new(10, 1);
2231 let property_id = NodeId::new(11, 1);
2232 let bool_id = NodeId::new(12, 1);
2233
2234 let typeof_field_kind = EdgeKind::TypeOf {
2235 context: Some(TypeOfContext::Field),
2236 index: Some(0),
2237 name: None,
2238 };
2239
2240 let per_file_edges = vec![vec![
2246 PendingEdge {
2247 source: property_id,
2248 target: bool_id,
2249 kind: typeof_field_kind.clone(),
2250 file: FileId::new(0),
2251 spans: vec![],
2252 },
2253 PendingEdge {
2254 source: struct_id,
2255 target: bool_id,
2256 kind: typeof_field_kind.clone(),
2257 file: FileId::new(0),
2258 spans: vec![],
2259 },
2260 ]];
2261
2262 let (deltas, final_seq) = pending_edges_to_delta(&per_file_edges, 500);
2263
2264 assert_eq!(deltas.len(), 1);
2267 assert_eq!(deltas[0].len(), 2);
2268 assert_eq!(final_seq, 502);
2269
2270 assert_eq!(deltas[0][0].source, property_id);
2271 assert_eq!(deltas[0][0].target, bool_id);
2272 assert_eq!(deltas[0][0].seq, 500);
2273 assert!(matches!(
2274 deltas[0][0].kind,
2275 EdgeKind::TypeOf {
2276 context: Some(TypeOfContext::Field),
2277 ..
2278 }
2279 ));
2280
2281 assert_eq!(deltas[0][1].source, struct_id);
2282 assert_eq!(deltas[0][1].target, bool_id);
2283 assert_eq!(deltas[0][1].seq, 501);
2284
2285 let (deltas_again, final_seq_again) = pending_edges_to_delta(&per_file_edges, 500);
2292 assert_eq!(final_seq_again, final_seq);
2293 assert_eq!(deltas_again.len(), deltas.len());
2294 assert_eq!(deltas_again[0].len(), deltas[0].len());
2295 for (a, b) in deltas[0].iter().zip(deltas_again[0].iter()) {
2296 assert_eq!(a.source, b.source);
2297 assert_eq!(a.target, b.target);
2298 assert_eq!(a.seq, b.seq);
2299 }
2300 }
2301}