llmcc_core/
graph_builder.rs

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    /// Compile unit this graph belongs to
24    unit_index: usize,
25    /// Root block ID of this unit
26    root: BlockId,
27    /// Edges of this graph unit
28    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/// ProjectGraph represents a complete compilation project with all units and their inter-dependencies.
71///
72/// # Overview
73/// ProjectGraph maintains a collection of per-unit compilation graphs (UnitGraph) and facilitates
74/// cross-unit dependency resolution. It provides efficient multi-dimensional indexing for block
75/// lookups by name, kind, unit, and ID, enabling quick context retrieval for LLM consumption.
76///
77/// # Architecture
78/// The graph consists of:
79/// - **UnitGraphs**: One per compilation unit (file), containing blocks and intra-unit relations
80/// - **Block Indexes**: Multi-dimensional indexes via BlockIndexMaps for O(1) to O(log n) lookups
81/// - **Cross-unit Links**: Dependencies tracked between blocks across different units
82///
83/// # Primary Use Cases
84/// 1. **Symbol Resolution**: Find blocks by name across the entire project
85/// 2. **Context Gathering**: Collect all related blocks for code analysis
86/// 3. **LLM Serialization**: Export graph as text or JSON for LLM model consumption
87/// 4. **Dependency Analysis**: Traverse dependency graphs to understand block relationships
88///
89#[derive(Debug)]
90pub struct ProjectGraph<'tcx> {
91    /// Reference to the compilation context containing all symbols, HIR nodes, and blocks
92    pub cc: &'tcx CompileCtxt<'tcx>,
93    /// Per-unit graphs containing blocks and intra-unit relations
94    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    /// Configure the number of PageRank-filtered nodes retained when rendering compact graphs.
114    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                    // Get all blocks that this block depends on
397                    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                    // Direct dependents tracked on this unit (covers cross-unit edges too)
416                    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                    // Fallback: scan current unit for reverse DependsOn edges
441                    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                    // Skip unknown relations
456                }
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(&current) {
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(&current) {
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(&current) {
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(&current_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(&current_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                // Extract file_name from HirFile node if available
723                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        // Try to process symbol dependencies for this node
781        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        // Recurse into children
788        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        // Avoid processing the same symbol twice
804        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 target symbol exists and has a block, add the dependency edge
825        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            // Target symbol exists but block not yet known
841            if unresolved.insert(dep_id) {
842                self.unit.add_unresolved_symbol(target_symbol);
843            }
844            return;
845        }
846
847        // Target symbol not found at all
848        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                // Only set the block ID if it hasn't been set before
873                // This prevents impl blocks from overwriting struct block IDs
874                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 self.config.compact {
900        //     if kind == BlockKind::Root {
901        //         self.build_block(node, parent, true);
902        //     } else {
903        //         self.visit_children(node, parent);
904        //     }
905        //     return;
906        // }
907
908        // Non-compact mode: process all defined kinds
909        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        // if self.config.compact {
919        //     // In compact mode, only create blocks for major constructs (Class, Enum, Impl)
920        //     // Skip functions, fields, and scopes to reduce graph size
921        //     match kind {
922        //         BlockKind::Class | BlockKind::Enum => {
923        //             // Build with recursion enabled to capture nested major constructs
924        //             self.build_block(node, parent, true);
925        //         }
926        //         // Skip all other scopes - don't recurse
927        //         _ => {
928        //             // Stop here, do not visit children
929        //             // self.visit_children(node, parent);
930        //         }
931        //     }
932        //     return;
933        // }
934
935        // Non-compact mode: build blocks for all major constructs
936        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(&current) {
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(&current) {
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}