1use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet};
2use std::marker::PhantomData;
3use std::path::Path;
4
5pub use crate::block::{BasicBlock, BlockId, BlockKind, BlockRelation};
6use crate::block::{
7 BlockCall, BlockClass, BlockConst, BlockEnum, BlockField, BlockFunc, BlockImpl, BlockRoot,
8 BlockStmt,
9};
10use crate::block_rel::BlockRelationMap;
11use crate::context::{CompileCtxt, CompileUnit};
12use crate::ir::HirNode;
13use crate::lang_def::LanguageTrait;
14use crate::pagerank::PageRanker;
15use crate::symbol::{SymId, Symbol};
16use crate::visit::HirVisitor;
17use crate::DynError;
18
19const COMPACT_INTERESTING_KINDS: [BlockKind; 2] = [BlockKind::Class, BlockKind::Enum];
20
21#[derive(Debug, Clone)]
22pub struct UnitGraph {
23 unit_index: usize,
25 root: BlockId,
27 edges: BlockRelationMap,
29}
30
31impl UnitGraph {
32 pub fn new(unit_index: usize, root: BlockId, edges: BlockRelationMap) -> Self {
33 Self {
34 unit_index,
35 root,
36 edges,
37 }
38 }
39
40 pub fn unit_index(&self) -> usize {
41 self.unit_index
42 }
43
44 pub fn root(&self) -> BlockId {
45 self.root
46 }
47
48 pub fn edges(&self) -> &BlockRelationMap {
49 &self.edges
50 }
51}
52
53#[derive(Debug, Clone, Copy, Default)]
54pub struct GraphBuildConfig {
55 pub compact: bool,
56}
57
58impl GraphBuildConfig {
59 pub fn compact() -> Self {
60 Self { compact: true }
61 }
62}
63
64#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
65pub struct GraphNode {
66 pub unit_index: usize,
67 pub block_id: BlockId,
68}
69
70#[derive(Debug)]
90pub struct ProjectGraph<'tcx> {
91 pub cc: &'tcx CompileCtxt<'tcx>,
93 units: Vec<UnitGraph>,
95 compact_rank_limit: Option<usize>,
96 pagerank_enabled: bool,
97}
98
99impl<'tcx> ProjectGraph<'tcx> {
100 pub fn new(cc: &'tcx CompileCtxt<'tcx>) -> Self {
101 Self {
102 cc,
103 units: Vec::new(),
104 compact_rank_limit: None,
105 pagerank_enabled: false,
106 }
107 }
108
109 pub fn add_child(&mut self, graph: UnitGraph) {
110 self.units.push(graph);
111 }
112
113 pub fn set_compact_rank_limit(&mut self, limit: Option<usize>) {
115 self.compact_rank_limit = match limit {
116 Some(0) => None,
117 other => other,
118 };
119 self.pagerank_enabled = self.compact_rank_limit.is_some();
120 }
121
122 pub fn link_units(&mut self) {
123 if self.units.is_empty() {
124 return;
125 }
126
127 let mut unresolved = self.cc.unresolve_symbols.write().unwrap();
128
129 unresolved.retain(|symbol_ref| {
130 let target = *symbol_ref;
131 let Some(target_block) = target.block_id() else {
132 return false;
133 };
134
135 let dependents: Vec<SymId> = target.depended.read().unwrap().clone();
136 for dependent_id in dependents {
137 let Some(source_symbol) = self.cc.opt_get_symbol(dependent_id) else {
138 continue;
139 };
140 let Some(from_block) = source_symbol.block_id() else {
141 continue;
142 };
143 self.add_cross_edge(
144 source_symbol.unit_index().unwrap(),
145 target.unit_index().unwrap(),
146 from_block,
147 target_block,
148 );
149 }
150
151 false
152 });
153 }
154
155 pub fn units(&self) -> &[UnitGraph] {
156 &self.units
157 }
158
159 pub fn unit_graph(&self, unit_index: usize) -> Option<&UnitGraph> {
160 self.units
161 .iter()
162 .find(|unit| unit.unit_index() == unit_index)
163 }
164
165 pub fn block_by_name(&self, name: &str) -> Option<GraphNode> {
166 let block_indexes = self.cc.block_indexes.read().unwrap();
167 let matches = block_indexes.find_by_name(name);
168
169 matches.first().map(|(unit_index, _, block_id)| GraphNode {
170 unit_index: *unit_index,
171 block_id: *block_id,
172 })
173 }
174
175 pub fn blocks_by_name(&self, name: &str) -> Vec<GraphNode> {
176 let block_indexes = self.cc.block_indexes.read().unwrap();
177 let matches = block_indexes.find_by_name(name);
178
179 matches
180 .into_iter()
181 .map(|(unit_index, _, block_id)| GraphNode {
182 unit_index,
183 block_id,
184 })
185 .collect()
186 }
187
188 pub fn render_compact_graph(&self) -> String {
189 self.render_compact_graph_inner(self.compact_rank_limit)
190 }
191
192 fn ranked_block_filter(
193 &self,
194 top_k: Option<usize>,
195 interesting_kinds: &[BlockKind],
196 ) -> Option<HashSet<BlockId>> {
197 let ranked_order = top_k.and_then(|limit| {
198 let ranker = PageRanker::new(self);
199 let mut collected = Vec::new();
200
201 for ranked in ranker.rank() {
202 if interesting_kinds.contains(&ranked.kind) {
203 collected.push(ranked.node.block_id);
204 }
205 if collected.len() >= limit {
206 break;
207 }
208 }
209
210 if collected.is_empty() {
211 None
212 } else {
213 Some(collected)
214 }
215 });
216
217 ranked_order.map(|ordered| ordered.into_iter().collect())
218 }
219
220 fn collect_compact_nodes(
221 &self,
222 interesting_kinds: &[BlockKind],
223 ranked_filter: Option<&HashSet<BlockId>>,
224 ) -> Vec<CompactNode> {
225 let block_indexes = self.cc.block_indexes.read().unwrap();
226 block_indexes
227 .block_id_index
228 .iter()
229 .filter_map(|(&block_id, (unit_index, name_opt, kind))| {
230 if !interesting_kinds.contains(kind) {
231 return None;
232 }
233
234 if let Some(ids) = ranked_filter {
235 if !ids.contains(&block_id) {
236 return None;
237 }
238 }
239
240 let unit = self.cc.compile_unit(*unit_index);
241 let block = unit.bb(block_id);
242 let display_name = name_opt
243 .clone()
244 .or_else(|| {
245 block
246 .base()
247 .and_then(|base| base.opt_get_name().map(|s| s.to_string()))
248 })
249 .unwrap_or_else(|| format!("{}:{}", kind, block_id.as_u32()));
250
251 let raw_path = unit
252 .file_path()
253 .or_else(|| unit.file().path())
254 .unwrap_or("<unknown>");
255
256 let path = std::fs::canonicalize(raw_path)
257 .map(|p| p.to_string_lossy().to_string())
258 .unwrap_or_else(|_| raw_path.to_string());
259
260 let file_bytes = unit.file().content();
261 let location = block
262 .opt_node()
263 .map(|node| {
264 let line = compact_byte_to_line(file_bytes, node.start_byte());
265 format!("{path}:{line}")
266 })
267 .or(Some(path.clone()));
268
269 let group = location
270 .as_ref()
271 .map(|loc| extract_group_path(loc))
272 .unwrap_or_else(|| extract_group_path(&path));
273
274 Some(CompactNode {
275 block_id,
276 unit_index: *unit_index,
277 name: display_name,
278 location,
279 group,
280 })
281 })
282 .collect()
283 }
284
285 fn collect_sorted_compact_nodes(&self, top_k: Option<usize>) -> Vec<CompactNode> {
286 let ranked_filter = if self.pagerank_enabled {
287 self.ranked_block_filter(top_k, &COMPACT_INTERESTING_KINDS)
288 } else {
289 None
290 };
291 let mut nodes =
292 self.collect_compact_nodes(&COMPACT_INTERESTING_KINDS, ranked_filter.as_ref());
293 nodes.sort_by(|a, b| a.name.cmp(&b.name));
294 nodes
295 }
296
297 fn collect_compact_edges(
298 &self,
299 nodes: &[CompactNode],
300 node_index: &HashMap<BlockId, usize>,
301 ) -> BTreeSet<(usize, usize)> {
302 let mut edges = BTreeSet::new();
303
304 for node in nodes {
305 let Some(unit_graph) = self.unit_graph(node.unit_index) else {
306 continue;
307 };
308 let from_idx = node_index[&node.block_id];
309
310 let dependencies = unit_graph
311 .edges()
312 .get_related(node.block_id, BlockRelation::DependsOn);
313
314 for dep_block_id in dependencies {
315 if let Some(&to_idx) = node_index.get(&dep_block_id) {
316 edges.insert((from_idx, to_idx));
317 }
318 }
319 }
320
321 edges
322 }
323
324 pub fn block_by_name_in(&self, unit_index: usize, name: &str) -> Option<GraphNode> {
325 let block_indexes = self.cc.block_indexes.read().unwrap();
326 let matches = block_indexes.find_by_name(name);
327
328 matches
329 .iter()
330 .find(|(u, _, _)| *u == unit_index)
331 .map(|(_, _, block_id)| GraphNode {
332 unit_index,
333 block_id: *block_id,
334 })
335 }
336
337 pub fn blocks_by_kind(&self, block_kind: BlockKind) -> Vec<GraphNode> {
338 let block_indexes = self.cc.block_indexes.read().unwrap();
339 let matches = block_indexes.find_by_kind(block_kind);
340
341 matches
342 .into_iter()
343 .map(|(unit_index, _, block_id)| GraphNode {
344 unit_index,
345 block_id,
346 })
347 .collect()
348 }
349
350 pub fn blocks_by_kind_in(&self, block_kind: BlockKind, unit_index: usize) -> Vec<GraphNode> {
351 let block_indexes = self.cc.block_indexes.read().unwrap();
352 let block_ids = block_indexes.find_by_kind_and_unit(block_kind, unit_index);
353
354 block_ids
355 .into_iter()
356 .map(|block_id| GraphNode {
357 unit_index,
358 block_id,
359 })
360 .collect()
361 }
362
363 pub fn blocks_in(&self, unit_index: usize) -> Vec<GraphNode> {
364 let block_indexes = self.cc.block_indexes.read().unwrap();
365 let matches = block_indexes.find_by_unit(unit_index);
366
367 matches
368 .into_iter()
369 .map(|(_, _, block_id)| GraphNode {
370 unit_index,
371 block_id,
372 })
373 .collect()
374 }
375
376 pub fn block_info(&self, block_id: BlockId) -> Option<(usize, Option<String>, BlockKind)> {
377 let block_indexes = self.cc.block_indexes.read().unwrap();
378 block_indexes.get_block_info(block_id)
379 }
380
381 pub fn find_related_blocks(
382 &self,
383 node: GraphNode,
384 relations: Vec<BlockRelation>,
385 ) -> Vec<GraphNode> {
386 if node.unit_index >= self.units.len() {
387 return Vec::new();
388 }
389
390 let unit = &self.units[node.unit_index];
391 let mut result = Vec::new();
392
393 for relation in relations {
394 match relation {
395 BlockRelation::DependsOn => {
396 let dependencies = unit
398 .edges
399 .get_related(node.block_id, BlockRelation::DependsOn);
400 let block_indexes = self.cc.block_indexes.read().unwrap();
401 for dep_block_id in dependencies {
402 let dep_unit_index = block_indexes
403 .get_block_info(dep_block_id)
404 .map(|(idx, _, _)| idx)
405 .unwrap_or(node.unit_index);
406 result.push(GraphNode {
407 unit_index: dep_unit_index,
408 block_id: dep_block_id,
409 });
410 }
411 }
412 BlockRelation::DependedBy => {
413 let mut seen = HashSet::new();
414
415 let dependents = unit
417 .edges
418 .get_related(node.block_id, BlockRelation::DependedBy);
419 if !dependents.is_empty() {
420 let indexes = self.cc.block_indexes.read().unwrap();
421 for dep_block_id in dependents {
422 if !seen.insert(dep_block_id) {
423 continue;
424 }
425 if let Some((dep_unit_idx, _, _)) = indexes.get_block_info(dep_block_id)
426 {
427 result.push(GraphNode {
428 unit_index: dep_unit_idx,
429 block_id: dep_block_id,
430 });
431 } else {
432 result.push(GraphNode {
433 unit_index: node.unit_index,
434 block_id: dep_block_id,
435 });
436 }
437 }
438 }
439
440 let local_dependents = unit
442 .edges
443 .find_reverse_relations(node.block_id, BlockRelation::DependsOn);
444 for dep_block_id in local_dependents {
445 if !seen.insert(dep_block_id) {
446 continue;
447 }
448 result.push(GraphNode {
449 unit_index: node.unit_index,
450 block_id: dep_block_id,
451 });
452 }
453 }
454 BlockRelation::Unknown => {
455 }
457 }
458 }
459
460 result
461 }
462
463 pub fn find_dpends_blocks_recursive(&self, node: GraphNode) -> HashSet<GraphNode> {
464 let mut visited = HashSet::new();
465 let mut stack = vec![node];
466 let relations = vec![BlockRelation::DependsOn];
467
468 while let Some(current) = stack.pop() {
469 if visited.contains(¤t) {
470 continue;
471 }
472 visited.insert(current);
473
474 for related in self.find_related_blocks(current, relations.clone()) {
475 if !visited.contains(&related) {
476 stack.push(related);
477 }
478 }
479 }
480
481 visited.remove(&node);
482 visited
483 }
484
485 pub fn find_depended_blocks_recursive(&self, node: GraphNode) -> HashSet<GraphNode> {
486 let mut visited = HashSet::new();
487 let mut stack = vec![node];
488 let relations = vec![BlockRelation::DependedBy];
489
490 while let Some(current) = stack.pop() {
491 if visited.contains(¤t) {
492 continue;
493 }
494 visited.insert(current);
495
496 for related in self.find_related_blocks(current, relations.clone()) {
497 if !visited.contains(&related) {
498 stack.push(related);
499 }
500 }
501 }
502
503 visited.remove(&node);
504 visited
505 }
506
507 pub fn traverse_bfs<F>(&self, start: GraphNode, mut callback: F)
508 where
509 F: FnMut(GraphNode),
510 {
511 let mut visited = HashSet::new();
512 let mut queue = vec![start];
513 let relations = vec![BlockRelation::DependsOn, BlockRelation::DependedBy];
514
515 while !queue.is_empty() {
516 let current = queue.remove(0);
517 if visited.contains(¤t) {
518 continue;
519 }
520 visited.insert(current);
521 callback(current);
522
523 for related in self.find_related_blocks(current, relations.clone()) {
524 if !visited.contains(&related) {
525 queue.push(related);
526 }
527 }
528 }
529 }
530
531 pub fn traverse_dfs<F>(&self, start: GraphNode, mut callback: F)
532 where
533 F: FnMut(GraphNode),
534 {
535 let mut visited = HashSet::new();
536 self.traverse_dfs_impl(start, &mut visited, &mut callback);
537 }
538
539 fn traverse_dfs_impl<F>(
540 &self,
541 node: GraphNode,
542 visited: &mut HashSet<GraphNode>,
543 callback: &mut F,
544 ) where
545 F: FnMut(GraphNode),
546 {
547 if visited.contains(&node) {
548 return;
549 }
550 visited.insert(node);
551 callback(node);
552
553 let relations = vec![BlockRelation::DependsOn, BlockRelation::DependedBy];
554 for related in self.find_related_blocks(node, relations) {
555 if !visited.contains(&related) {
556 self.traverse_dfs_impl(related, visited, callback);
557 }
558 }
559 }
560
561 pub fn get_block_depends(&self, node: GraphNode) -> HashSet<GraphNode> {
562 if node.unit_index >= self.units.len() {
563 return HashSet::new();
564 }
565
566 let unit = &self.units[node.unit_index];
567 let mut result = HashSet::new();
568 let mut visited = HashSet::new();
569 let mut stack = vec![node.block_id];
570 let block_indexes = self.cc.block_indexes.read().unwrap();
571
572 while let Some(current_block) = stack.pop() {
573 if visited.contains(¤t_block) {
574 continue;
575 }
576 visited.insert(current_block);
577
578 let dependencies = unit
579 .edges
580 .get_related(current_block, BlockRelation::DependsOn);
581 for dep_block_id in dependencies {
582 if dep_block_id != node.block_id {
583 let dep_unit_index = block_indexes
584 .get_block_info(dep_block_id)
585 .map(|(idx, _, _)| idx)
586 .unwrap_or(node.unit_index);
587 result.insert(GraphNode {
588 unit_index: dep_unit_index,
589 block_id: dep_block_id,
590 });
591 stack.push(dep_block_id);
592 }
593 }
594 }
595
596 result
597 }
598
599 pub fn get_block_depended(&self, node: GraphNode) -> HashSet<GraphNode> {
600 if node.unit_index >= self.units.len() {
601 return HashSet::new();
602 }
603
604 let unit = &self.units[node.unit_index];
605 let mut result = HashSet::new();
606 let mut visited = HashSet::new();
607 let mut stack = vec![node.block_id];
608 let block_indexes = self.cc.block_indexes.read().unwrap();
609
610 while let Some(current_block) = stack.pop() {
611 if visited.contains(¤t_block) {
612 continue;
613 }
614 visited.insert(current_block);
615
616 let dependencies = unit
617 .edges
618 .get_related(current_block, BlockRelation::DependedBy);
619 for dep_block_id in dependencies {
620 if dep_block_id != node.block_id {
621 let dep_unit_index = block_indexes
622 .get_block_info(dep_block_id)
623 .map(|(idx, _, _)| idx)
624 .unwrap_or(node.unit_index);
625 result.insert(GraphNode {
626 unit_index: dep_unit_index,
627 block_id: dep_block_id,
628 });
629 stack.push(dep_block_id);
630 }
631 }
632 }
633
634 result
635 }
636
637 fn render_compact_graph_inner(&self, top_k: Option<usize>) -> String {
638 let nodes = self.collect_sorted_compact_nodes(top_k);
639
640 if nodes.is_empty() {
641 return "digraph DesignGraph {\n}\n".to_string();
642 }
643
644 let node_index = build_compact_node_index(&nodes);
645 let edges = self.collect_compact_edges(&nodes, &node_index);
646
647 let pruned = prune_compact_components(&nodes, &edges);
648 if pruned.nodes.is_empty() {
649 return "digraph DesignGraph {\n}\n".to_string();
650 }
651
652 let reduced_edges = reduce_transitive_edges(&pruned.nodes, &pruned.edges);
653
654 render_compact_dot(&pruned.nodes, &reduced_edges)
655 }
656
657 fn add_cross_edge(
658 &self,
659 from_idx: usize,
660 to_idx: usize,
661 from_block: BlockId,
662 to_block: BlockId,
663 ) {
664 if from_idx == to_idx {
665 let unit = &self.units[from_idx];
666 if !unit
667 .edges
668 .has_relation(from_block, BlockRelation::DependsOn, to_block)
669 {
670 unit.edges.add_relation(from_block, to_block);
671 }
672 return;
673 }
674
675 let from_unit = &self.units[from_idx];
676 from_unit
677 .edges
678 .add_relation_if_not_exists(from_block, BlockRelation::DependsOn, to_block);
679
680 let to_unit = &self.units[to_idx];
681 to_unit
682 .edges
683 .add_relation_if_not_exists(to_block, BlockRelation::DependedBy, from_block);
684 }
685}
686
687#[derive(Debug)]
688struct GraphBuilder<'tcx, Language> {
689 unit: CompileUnit<'tcx>,
690 root: Option<BlockId>,
691 children_stack: Vec<Vec<BlockId>>,
692 _config: GraphBuildConfig,
693 _marker: PhantomData<Language>,
694}
695
696impl<'tcx, Language: LanguageTrait> GraphBuilder<'tcx, Language> {
697 fn new(unit: CompileUnit<'tcx>, _config: GraphBuildConfig) -> Self {
698 Self {
699 unit,
700 root: None,
701 children_stack: Vec::new(),
702 _config,
703 _marker: PhantomData,
704 }
705 }
706
707 fn next_id(&self) -> BlockId {
708 self.unit.reserve_block_id()
709 }
710
711 fn create_block(
712 &self,
713 id: BlockId,
714 node: HirNode<'tcx>,
715 kind: BlockKind,
716 parent: Option<BlockId>,
717 children: Vec<BlockId>,
718 ) -> BasicBlock<'tcx> {
719 let arena = &self.unit.cc.block_arena;
720 match kind {
721 BlockKind::Root => {
722 let file_name = node.as_file().map(|file| file.file_path.clone());
724 let block = BlockRoot::from_hir(id, node, parent, children, file_name);
725 BasicBlock::Root(arena.alloc(block))
726 }
727 BlockKind::Func => {
728 let block = BlockFunc::from_hir(id, node, parent, children);
729 BasicBlock::Func(arena.alloc(block))
730 }
731 BlockKind::Class => {
732 let block = BlockClass::from_hir(id, node, parent, children);
733 BasicBlock::Class(arena.alloc(block))
734 }
735 BlockKind::Stmt => {
736 let stmt = BlockStmt::from_hir(id, node, parent, children);
737 BasicBlock::Stmt(arena.alloc(stmt))
738 }
739 BlockKind::Call => {
740 let stmt = BlockCall::from_hir(id, node, parent, children);
741 BasicBlock::Call(arena.alloc(stmt))
742 }
743 BlockKind::Enum => {
744 let enum_ty = BlockEnum::from_hir(id, node, parent, children);
745 BasicBlock::Enum(arena.alloc(enum_ty))
746 }
747 BlockKind::Const => {
748 let stmt = BlockConst::from_hir(id, node, parent, children);
749 BasicBlock::Const(arena.alloc(stmt))
750 }
751 BlockKind::Impl => {
752 let block = BlockImpl::from_hir(id, node, parent, children);
753 BasicBlock::Impl(arena.alloc(block))
754 }
755 BlockKind::Field => {
756 let block = BlockField::from_hir(id, node, parent, children);
757 BasicBlock::Field(arena.alloc(block))
758 }
759 _ => {
760 panic!("unknown block kind: {}", kind)
761 }
762 }
763 }
764
765 fn build_edges(&self, node: HirNode<'tcx>) -> BlockRelationMap {
766 let edges = BlockRelationMap::default();
767 let mut visited = HashSet::new();
768 let mut unresolved = HashSet::new();
769 self.collect_edges(node, &edges, &mut visited, &mut unresolved);
770 edges
771 }
772
773 fn collect_edges(
774 &self,
775 node: HirNode<'tcx>,
776 edges: &BlockRelationMap,
777 visited: &mut HashSet<SymId>,
778 unresolved: &mut HashSet<SymId>,
779 ) {
780 if let Some(scope) = self.unit.opt_get_scope(node.hir_id()) {
782 if let Some(symbol) = scope.symbol() {
783 self.process_symbol(symbol, edges, visited, unresolved);
784 }
785 }
786
787 for &child_id in node.children() {
789 let child = self.unit.hir_node(child_id);
790 self.collect_edges(child, edges, visited, unresolved);
791 }
792 }
793
794 fn process_symbol(
795 &self,
796 symbol: &'tcx Symbol,
797 edges: &BlockRelationMap,
798 visited: &mut HashSet<SymId>,
799 unresolved: &mut HashSet<SymId>,
800 ) {
801 let symbol_id = symbol.id;
802
803 if !visited.insert(symbol_id) {
805 return;
806 }
807
808 let Some(from_block) = symbol.block_id() else {
809 return;
810 };
811
812 let dependencies = symbol.depends.read().unwrap().clone();
813 for dep_id in dependencies {
814 self.link_dependency(dep_id, from_block, edges, unresolved);
815 }
816 }
817 fn link_dependency(
818 &self,
819 dep_id: SymId,
820 from_block: BlockId,
821 edges: &BlockRelationMap,
822 unresolved: &mut HashSet<SymId>,
823 ) {
824 if let Some(target_symbol) = self.unit.opt_get_symbol(dep_id) {
826 if let Some(to_block) = target_symbol.block_id() {
827 if !edges.has_relation(from_block, BlockRelation::DependsOn, to_block) {
828 edges.add_relation(from_block, to_block);
829 }
830 let target_unit = target_symbol.unit_index();
831 if target_unit.is_some()
832 && target_unit != Some(self.unit.index)
833 && unresolved.insert(dep_id)
834 {
835 self.unit.add_unresolved_symbol(target_symbol);
836 }
837 return;
838 }
839
840 if unresolved.insert(dep_id) {
842 self.unit.add_unresolved_symbol(target_symbol);
843 }
844 return;
845 }
846
847 unresolved.insert(dep_id);
849 }
850
851 fn build_block(&mut self, node: HirNode<'tcx>, parent: BlockId, recursive: bool) {
852 let id = self.next_id();
853 let block_kind = Language::block_kind(node.kind_id());
854 assert_ne!(block_kind, BlockKind::Undefined);
855
856 if self.root.is_none() {
857 self.root = Some(id);
858 }
859
860 let children = if recursive {
861 self.children_stack.push(Vec::new());
862 self.visit_children(node, id);
863
864 self.children_stack.pop().unwrap()
865 } else {
866 Vec::new()
867 };
868
869 let block = self.create_block(id, node, block_kind, Some(parent), children);
870 if let Some(scope) = self.unit.opt_get_scope(node.hir_id()) {
871 if let Some(symbol) = scope.symbol() {
872 if symbol.block_id().is_none() {
875 symbol.set_block_id(Some(id));
876 }
877 }
878 }
879 self.unit.insert_block(id, block, parent);
880
881 if let Some(children) = self.children_stack.last_mut() {
882 children.push(id);
883 }
884 }
885}
886
887impl<'tcx, Language: LanguageTrait> HirVisitor<'tcx> for GraphBuilder<'tcx, Language> {
888 fn unit(&self) -> CompileUnit<'tcx> {
889 self.unit
890 }
891
892 fn visit_file(&mut self, node: HirNode<'tcx>, parent: BlockId) {
893 self.children_stack.push(Vec::new());
894 self.build_block(node, parent, true);
895 }
896
897 fn visit_internal(&mut self, node: HirNode<'tcx>, parent: BlockId) {
898 let kind = Language::block_kind(node.kind_id());
899 if kind != BlockKind::Undefined {
910 self.build_block(node, parent, false);
911 } else {
912 self.visit_children(node, parent);
913 }
914 }
915
916 fn visit_scope(&mut self, node: HirNode<'tcx>, parent: BlockId) {
917 let kind = Language::block_kind(node.kind_id());
918 match kind {
937 BlockKind::Func
938 | BlockKind::Class
939 | BlockKind::Enum
940 | BlockKind::Const
941 | BlockKind::Impl
942 | BlockKind::Field => self.build_block(node, parent, true),
943 _ => self.visit_children(node, parent),
944 }
945 }
946}
947
948pub fn build_llmcc_graph_with_config<'tcx, L: LanguageTrait>(
949 unit: CompileUnit<'tcx>,
950 unit_index: usize,
951 config: GraphBuildConfig,
952) -> Result<UnitGraph, DynError> {
953 let root_hir = unit
954 .file_start_hir_id()
955 .ok_or("missing file start HIR id")?;
956 let mut builder = GraphBuilder::<L>::new(unit, config);
957 let root_node = unit.hir_node(root_hir);
958 builder.visit_node(root_node, BlockId::ROOT_PARENT);
959
960 let root_block = builder.root;
961 let root_block = root_block.ok_or("graph builder produced no root")?;
962 let edges = builder.build_edges(root_node);
963 Ok(UnitGraph::new(unit_index, root_block, edges))
964}
965
966pub fn build_llmcc_graph<'tcx, L: LanguageTrait>(
967 unit: CompileUnit<'tcx>,
968 unit_index: usize,
969) -> Result<UnitGraph, DynError> {
970 build_llmcc_graph_with_config::<L>(unit, unit_index, GraphBuildConfig::default())
971}
972
973#[derive(Clone)]
974struct CompactNode {
975 block_id: BlockId,
976 unit_index: usize,
977 name: String,
978 location: Option<String>,
979 group: String,
980}
981
982fn compact_byte_to_line(content: &[u8], byte_pos: usize) -> usize {
983 let clamped = byte_pos.min(content.len());
984 content[..clamped].iter().filter(|&&ch| ch == b'\n').count() + 1
985}
986
987fn escape_dot_label(input: &str) -> String {
988 input
989 .replace('\\', "\\\\")
990 .replace('"', "\\\"")
991 .replace('\n', "\\n")
992}
993
994fn escape_dot_attr(input: &str) -> String {
995 input.replace('\\', "\\\\").replace('"', "\\\"")
996}
997
998fn summarize_location(location: &str) -> (String, String) {
999 let (path_part, line_part) = location
1000 .rsplit_once(':')
1001 .map(|(path, line)| (path, Some(line)))
1002 .unwrap_or((location, None));
1003
1004 let path = Path::new(path_part);
1005 let components: Vec<_> = path
1006 .components()
1007 .filter_map(|comp| comp.as_os_str().to_str())
1008 .collect();
1009
1010 let start = components.len().saturating_sub(3);
1011 let mut shortened = components[start..].join("/");
1012 if shortened.is_empty() {
1013 shortened = path
1014 .file_name()
1015 .and_then(|name| name.to_str())
1016 .unwrap_or(path_part)
1017 .to_string();
1018 }
1019
1020 let display = if let Some(line) = line_part {
1021 format!("{shortened}:{line}")
1022 } else {
1023 shortened
1024 };
1025
1026 (display, location.to_string())
1027}
1028
1029fn extract_crate_path(location: &str) -> String {
1030 let path = location.split(':').next().unwrap_or(location);
1031 let parts: Vec<&str> = path.split(['/', '\\']).collect();
1032
1033 if let Some(src_idx) = parts.iter().position(|&p| p == "src") {
1034 if src_idx > 0 {
1035 return parts[src_idx - 1].to_string();
1036 }
1037 }
1038
1039 if let Some(filename) = parts.last() {
1040 if !filename.is_empty() {
1041 return filename.split('.').next().unwrap_or("unknown").to_string();
1042 }
1043 }
1044
1045 "unknown".to_string()
1046}
1047
1048fn extract_python_module_path(location: &str) -> String {
1049 const MAX_MODULE_DEPTH: usize = 2;
1050
1051 let path_str = location.split(':').next().unwrap_or(location);
1052 let path = Path::new(path_str);
1053
1054 if path.extension().and_then(|ext| ext.to_str()) != Some("py") {
1055 return extract_crate_path(location);
1056 }
1057
1058 let file_stem = path
1059 .file_stem()
1060 .and_then(|s| s.to_str())
1061 .map(|s| s.to_string());
1062
1063 let mut packages: Vec<String> = Vec::new();
1064 let mut current = path.parent();
1065
1066 while let Some(dir) = current {
1067 let dir_name = match dir.file_name().and_then(|n| n.to_str()) {
1068 Some(name) if !name.is_empty() => name.to_string(),
1069 _ => break,
1070 };
1071
1072 let has_init = dir.join("__init__.py").exists() || dir.join("__init__.pyi").exists();
1073
1074 if has_init {
1075 packages.push(dir_name);
1076 }
1077
1078 current = dir.parent();
1079 }
1080
1081 if packages.is_empty() {
1082 if let Some(stem) = file_stem
1083 .as_ref()
1084 .filter(|stem| stem.as_str() != "__init__")
1085 {
1086 return stem.clone();
1087 }
1088
1089 if let Some(parent_name) = path
1090 .parent()
1091 .and_then(|dir| dir.file_name().and_then(|n| n.to_str()))
1092 .map(|s| s.to_string())
1093 {
1094 return parent_name;
1095 }
1096
1097 return "unknown".to_string();
1098 }
1099
1100 packages.reverse();
1101 if packages.len() > MAX_MODULE_DEPTH {
1102 packages.truncate(MAX_MODULE_DEPTH);
1103 }
1104
1105 packages.join(".")
1106}
1107
1108fn extract_group_path(location: &str) -> String {
1109 let path = location.split(':').next().unwrap_or(location);
1110 if path.ends_with(".py") {
1111 extract_python_module_path(location)
1112 } else {
1113 extract_crate_path(location)
1114 }
1115}
1116
1117fn render_compact_dot(nodes: &[CompactNode], edges: &BTreeSet<(usize, usize)>) -> String {
1118 let mut crate_groups: BTreeMap<String, Vec<usize>> = BTreeMap::new();
1119 for (idx, node) in nodes.iter().enumerate() {
1120 crate_groups
1121 .entry(node.group.clone())
1122 .or_default()
1123 .push(idx);
1124 }
1125
1126 let mut output = String::from("digraph DesignGraph {\n");
1127
1128 for (subgraph_counter, (crate_path, node_indices)) in crate_groups.iter().enumerate() {
1129 output.push_str(&format!(" subgraph cluster_{} {{\n", subgraph_counter));
1130 output.push_str(&format!(
1131 " label=\"{}\";\n",
1132 escape_dot_label(crate_path)
1133 ));
1134 output.push_str(" style=filled;\n");
1135 output.push_str(" color=lightgrey;\n");
1136
1137 for &idx in node_indices {
1138 let node = &nodes[idx];
1139 let label = escape_dot_label(&node.name);
1140 let mut attrs = vec![format!("label=\"{}\"", label)];
1141
1142 if let Some(location) = &node.location {
1143 let (_display, full) = summarize_location(location);
1144 let escaped_full = escape_dot_attr(&full);
1145 attrs.push(format!("full_path=\"{}\"", escaped_full));
1146 }
1147
1148 output.push_str(&format!(" n{} [{}];\n", idx, attrs.join(", ")));
1149 }
1150
1151 output.push_str(" }\n");
1152 }
1153
1154 for &(from, to) in edges {
1155 output.push_str(&format!(" n{} -> n{};\n", from, to));
1156 }
1157
1158 output.push_str("}\n");
1159 output
1160}
1161
1162fn build_compact_node_index(nodes: &[CompactNode]) -> HashMap<BlockId, usize> {
1163 let mut node_index = HashMap::with_capacity(nodes.len());
1164 for (idx, node) in nodes.iter().enumerate() {
1165 node_index.insert(node.block_id, idx);
1166 }
1167 node_index
1168}
1169
1170struct PrunedGraph {
1171 nodes: Vec<CompactNode>,
1172 edges: BTreeSet<(usize, usize)>,
1173}
1174
1175fn prune_compact_components(
1176 nodes: &[CompactNode],
1177 edges: &BTreeSet<(usize, usize)>,
1178) -> PrunedGraph {
1179 if nodes.is_empty() {
1180 return PrunedGraph {
1181 nodes: Vec::new(),
1182 edges: BTreeSet::new(),
1183 };
1184 }
1185
1186 let components = find_connected_components(nodes.len(), edges);
1187 if components.is_empty() {
1188 return PrunedGraph {
1189 nodes: nodes.to_vec(),
1190 edges: edges.clone(),
1191 };
1192 }
1193
1194 let mut retained_indices = HashSet::new();
1195 for component in components {
1196 if component.len() == 1 {
1197 let idx = component[0];
1198 let has_edges = edges.iter().any(|&(from, to)| from == idx || to == idx);
1199 if !has_edges {
1200 continue;
1201 }
1202 }
1203 retained_indices.extend(component);
1204 }
1205
1206 if retained_indices.is_empty() {
1207 return PrunedGraph {
1208 nodes: Vec::new(),
1209 edges: BTreeSet::new(),
1210 };
1211 }
1212
1213 let mut retained_nodes = Vec::new();
1214 let mut old_to_new = HashMap::new();
1215 for (new_idx, old_idx) in retained_indices.iter().enumerate() {
1216 retained_nodes.push(nodes[*old_idx].clone());
1217 old_to_new.insert(*old_idx, new_idx);
1218 }
1219
1220 let mut retained_edges = BTreeSet::new();
1221 for &(from, to) in edges {
1222 if let (Some(&new_from), Some(&new_to)) = (old_to_new.get(&from), old_to_new.get(&to)) {
1223 retained_edges.insert((new_from, new_to));
1224 }
1225 }
1226
1227 PrunedGraph {
1228 nodes: retained_nodes,
1229 edges: retained_edges,
1230 }
1231}
1232
1233fn find_connected_components(
1234 node_count: usize,
1235 edges: &BTreeSet<(usize, usize)>,
1236) -> Vec<Vec<usize>> {
1237 if node_count == 0 {
1238 return Vec::new();
1239 }
1240
1241 let mut graph: HashMap<usize, Vec<usize>> = HashMap::new();
1242 for &(from, to) in edges.iter() {
1243 graph.entry(from).or_default().push(to);
1244 graph.entry(to).or_default().push(from);
1245 }
1246
1247 let mut visited = HashSet::new();
1248 let mut components = Vec::new();
1249
1250 for node in 0..node_count {
1251 if visited.contains(&node) {
1252 continue;
1253 }
1254
1255 let mut component = Vec::new();
1256 let mut stack = vec![node];
1257
1258 while let Some(current) = stack.pop() {
1259 if !visited.insert(current) {
1260 continue;
1261 }
1262
1263 component.push(current);
1264
1265 if let Some(neighbors) = graph.get(¤t) {
1266 for &neighbor in neighbors {
1267 if !visited.contains(&neighbor) {
1268 stack.push(neighbor);
1269 }
1270 }
1271 }
1272 }
1273
1274 components.push(component);
1275 }
1276
1277 components
1278}
1279
1280fn reduce_transitive_edges(
1281 nodes: &[CompactNode],
1282 edges: &BTreeSet<(usize, usize)>,
1283) -> BTreeSet<(usize, usize)> {
1284 if nodes.is_empty() {
1285 return BTreeSet::new();
1286 }
1287
1288 let mut adjacency: HashMap<usize, Vec<usize>> = HashMap::new();
1289 for &(from, to) in edges.iter() {
1290 adjacency.entry(from).or_default().push(to);
1291 }
1292
1293 let mut minimal_edges = BTreeSet::new();
1294
1295 for &(from, to) in edges.iter() {
1296 if !has_alternative_path(from, to, &adjacency, (from, to)) {
1297 minimal_edges.insert((from, to));
1298 }
1299 }
1300
1301 minimal_edges
1302}
1303
1304fn has_alternative_path(
1305 start: usize,
1306 target: usize,
1307 adjacency: &HashMap<usize, Vec<usize>>,
1308 edge_to_skip: (usize, usize),
1309) -> bool {
1310 let mut visited = HashSet::new();
1311 let mut stack: Vec<usize> = adjacency
1312 .get(&start)
1313 .into_iter()
1314 .flat_map(|neighbors| neighbors.iter())
1315 .filter_map(|&neighbor| {
1316 if (start, neighbor) == edge_to_skip {
1317 None
1318 } else {
1319 Some(neighbor)
1320 }
1321 })
1322 .collect();
1323
1324 while let Some(current) = stack.pop() {
1325 if !visited.insert(current) {
1326 continue;
1327 }
1328
1329 if current == target {
1330 return true;
1331 }
1332
1333 if let Some(neighbors) = adjacency.get(¤t) {
1334 for &neighbor in neighbors {
1335 if (current, neighbor) == edge_to_skip {
1336 continue;
1337 }
1338 if !visited.contains(&neighbor) {
1339 stack.push(neighbor);
1340 }
1341 }
1342 }
1343 }
1344
1345 false
1346}