Skip to main content

nichlink_debug_method/
adapters.rs

1//! Adapters for mature tracing and graph backends.
2//! 成熟 tracing 与图后端的适配层。
3
4use std::collections::BTreeMap;
5
6use petgraph::graph::{DiGraph, NodeIndex};
7use petgraph::visit::EdgeRef;
8use tracing::Span;
9
10use nichlink_run_method::{CallSite, CallTrace, EvidenceKind};
11
12/// Create a structured tracing span for one NichLink call site.
13/// 为一个 NichLink 调用点创建结构化 tracing span。
14pub fn span_for(call: &CallSite) -> Span {
15    tracing::span!(
16        tracing::Level::TRACE,
17        "nichlink.call",
18        node = %call.node,
19        function = call.function,
20        frame_id = call.frame_id,
21    )
22}
23
24/// A graph view built from logical runtime edges.
25/// 从运行时逻辑边构建的图视图。
26#[derive(Debug, Default)]
27pub struct CallGraph {
28    graph: DiGraph<String, EvidenceKind>,
29    nodes: BTreeMap<String, NodeIndex>,
30}
31
32impl CallGraph {
33    /// Build a graph without copying full call paths or local values.
34    /// 构建图时不复制完整调用路径或局部值。
35    pub fn from_trace(trace: &CallTrace) -> Self {
36        let mut graph = Self::default();
37        for edge in trace.logical_call_edges() {
38            let caller = format!("{}::{}", edge.caller.node, edge.caller.function);
39            let callee = format!("{}::{}", edge.callee.node, edge.callee.function);
40            let caller_index = graph.node(caller);
41            let callee_index = graph.node(callee);
42            if let Some(existing) = graph.graph.find_edge(caller_index, callee_index) {
43                graph.graph[existing] = edge.evidence;
44            } else {
45                graph
46                    .graph
47                    .add_edge(caller_index, callee_index, edge.evidence);
48            }
49        }
50        graph
51    }
52
53    /// Build the same graph from merged MIR/live relations.
54    /// 从合并后的 MIR/实时关系构建同一图后端。
55    pub fn from_relations(relations: &[crate::CallRelation]) -> Self {
56        let mut graph = Self::default();
57        for relation in relations {
58            let caller = graph.node(relation.caller.clone());
59            let callee = graph.node(relation.callee.clone());
60            let evidence = relation.evidence;
61            if let Some(existing) = graph.graph.find_edge(caller, callee) {
62                graph.graph[existing] = evidence;
63            } else {
64                graph.graph.add_edge(caller, callee, evidence);
65            }
66        }
67        graph
68    }
69
70    fn node(&mut self, name: String) -> NodeIndex {
71        if let Some(index) = self.nodes.get(&name) {
72            return *index;
73        }
74        let index = self.graph.add_node(name.clone());
75        self.nodes.insert(name, index);
76        index
77    }
78
79    /// Number of distinct logical functions currently in the graph; parallel
80    /// invocations never add a second node.
81    /// 图中当前不同逻辑函数的数量;同一次调用的多次执行不会新增第二个节点。
82    pub fn node_count(&self) -> usize {
83        self.graph.node_count()
84    }
85
86    /// Number of distinct logical edges in the graph; repeated invocations
87    /// between the same pair collapse into one edge.
88    /// 图中不同逻辑边的数量;同一对函数间的多次调用会合并为一条边。
89    pub fn edge_count(&self) -> usize {
90        self.graph.edge_count()
91    }
92
93    /// Export a compact DOT representation for TUI or external graph tools.
94    /// 导出紧凑 DOT 表示,供 TUI 或外部图工具使用。
95    pub fn to_dot(&self) -> String {
96        let mut output = String::from("digraph nichlink {\n");
97        for index in self.graph.node_indices() {
98            let name = &self.graph[index];
99            output.push_str("  ");
100            output.push_str(&quote_dot(name));
101            output.push_str(";\n");
102        }
103        for edge in self.graph.edge_references() {
104            output.push_str("  ");
105            output.push_str(&quote_dot(&self.graph[edge.source()]));
106            output.push_str(" -> ");
107            output.push_str(&quote_dot(&self.graph[edge.target()]));
108            output.push_str(" [label=\"");
109            output.push_str(&evidence_label(*edge.weight()));
110            output.push_str("\"];\n");
111        }
112        output.push_str("}\n");
113        output
114    }
115}
116
117fn evidence_label(evidence: EvidenceKind) -> String {
118    format!("{} {}", evidence.marker(), evidence.label())
119}
120
121fn quote_dot(value: &str) -> String {
122    format!("\"{}\"", value.replace('\\', "\\\\").replace('"', "\\\""))
123}
124
125#[cfg(test)]
126mod tests {
127    use super::{CallGraph, span_for};
128    use crate::{CallEvidence, CallRelation};
129    use crate::{CallTrace, NodeId, SourceLocation};
130
131    #[test]
132    fn graph_keeps_logical_edges_without_duplicate_invocations() {
133        let mut trace = CallTrace::full();
134        let source = SourceLocation {
135            file: "test.rs",
136            line: 1,
137            column: 1,
138            function: "root",
139        };
140        trace.with_at(
141            NodeId::from_path("root.rs", "Root"),
142            "root",
143            source,
144            |trace| {
145                trace.with_at(
146                    NodeId::from_path("child.rs", "Child"),
147                    "child",
148                    source,
149                    |_| {},
150                );
151                trace.with_at(
152                    NodeId::from_path("child.rs", "Child"),
153                    "child",
154                    source,
155                    |_| {},
156                );
157            },
158        );
159        let graph = CallGraph::from_trace(&trace);
160        assert_eq!(graph.node_count(), 2);
161        assert_eq!(graph.edge_count(), 1);
162        assert!(graph.to_dot().contains("nichlink"));
163    }
164
165    #[test]
166    fn span_contains_nichlink_fields() {
167        let call = crate::CallSite {
168            node: NodeId::from_path("test.rs", "Test"),
169            function: "test",
170            frame_id: 7,
171            source: None,
172        };
173        let _span = span_for(&call);
174    }
175
176    #[test]
177    fn all_evidence_kinds_share_marker_and_label_rendering() {
178        let relations = [
179            CallRelation {
180                caller: "a".to_owned(),
181                callee: "b".to_owned(),
182                evidence: CallEvidence::Live,
183                source: None,
184                mir_line: None,
185                caller_frame: None,
186                callee_frame: None,
187            },
188            CallRelation {
189                caller: "b".to_owned(),
190                callee: "c".to_owned(),
191                evidence: CallEvidence::Mir,
192                source: None,
193                mir_line: Some(4),
194                caller_frame: None,
195                callee_frame: None,
196            },
197            CallRelation {
198                caller: "c".to_owned(),
199                callee: "d".to_owned(),
200                evidence: CallEvidence::Source,
201                source: None,
202                mir_line: None,
203                caller_frame: None,
204                callee_frame: None,
205            },
206        ];
207        let dot = CallGraph::from_relations(&relations).to_dot();
208        assert!(dot.contains("+ live"));
209        assert!(dot.contains("? mir"));
210        assert!(dot.contains("~ source"));
211        assert!(CallEvidence::Live.confirmed());
212        assert!(!CallEvidence::Mir.confirmed());
213    }
214}