nichlink_debug_method/
adapters.rs1use 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
12pub 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#[derive(Debug, Default)]
27pub struct CallGraph {
28 graph: DiGraph<String, EvidenceKind>,
29 nodes: BTreeMap<String, NodeIndex>,
30}
31
32impl CallGraph {
33 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 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 pub fn node_count(&self) -> usize {
83 self.graph.node_count()
84 }
85
86 pub fn edge_count(&self) -> usize {
90 self.graph.edge_count()
91 }
92
93 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("e_dot(name));
101 output.push_str(";\n");
102 }
103 for edge in self.graph.edge_references() {
104 output.push_str(" ");
105 output.push_str("e_dot(&self.graph[edge.source()]));
106 output.push_str(" -> ");
107 output.push_str("e_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}