use std::collections::BTreeMap;
use petgraph::graph::{DiGraph, NodeIndex};
use petgraph::visit::EdgeRef;
use tracing::Span;
use nichlink_run_method::{CallSite, CallTrace, EvidenceKind};
pub fn span_for(call: &CallSite) -> Span {
tracing::span!(
tracing::Level::TRACE,
"nichlink.call",
node = %call.node,
function = call.function,
frame_id = call.frame_id,
)
}
#[derive(Debug, Default)]
pub struct CallGraph {
graph: DiGraph<String, EvidenceKind>,
nodes: BTreeMap<String, NodeIndex>,
}
impl CallGraph {
pub fn from_trace(trace: &CallTrace) -> Self {
let mut graph = Self::default();
for edge in trace.logical_call_edges() {
let caller = format!("{}::{}", edge.caller.node, edge.caller.function);
let callee = format!("{}::{}", edge.callee.node, edge.callee.function);
let caller_index = graph.node(caller);
let callee_index = graph.node(callee);
if let Some(existing) = graph.graph.find_edge(caller_index, callee_index) {
graph.graph[existing] = edge.evidence;
} else {
graph
.graph
.add_edge(caller_index, callee_index, edge.evidence);
}
}
graph
}
pub fn from_relations(relations: &[crate::CallRelation]) -> Self {
let mut graph = Self::default();
for relation in relations {
let caller = graph.node(relation.caller.clone());
let callee = graph.node(relation.callee.clone());
let evidence = relation.evidence;
if let Some(existing) = graph.graph.find_edge(caller, callee) {
graph.graph[existing] = evidence;
} else {
graph.graph.add_edge(caller, callee, evidence);
}
}
graph
}
fn node(&mut self, name: String) -> NodeIndex {
if let Some(index) = self.nodes.get(&name) {
return *index;
}
let index = self.graph.add_node(name.clone());
self.nodes.insert(name, index);
index
}
pub fn node_count(&self) -> usize {
self.graph.node_count()
}
pub fn edge_count(&self) -> usize {
self.graph.edge_count()
}
pub fn to_dot(&self) -> String {
let mut output = String::from("digraph nichlink {\n");
for index in self.graph.node_indices() {
let name = &self.graph[index];
output.push_str(" ");
output.push_str("e_dot(name));
output.push_str(";\n");
}
for edge in self.graph.edge_references() {
output.push_str(" ");
output.push_str("e_dot(&self.graph[edge.source()]));
output.push_str(" -> ");
output.push_str("e_dot(&self.graph[edge.target()]));
output.push_str(" [label=\"");
output.push_str(&evidence_label(*edge.weight()));
output.push_str("\"];\n");
}
output.push_str("}\n");
output
}
}
fn evidence_label(evidence: EvidenceKind) -> String {
format!("{} {}", evidence.marker(), evidence.label())
}
fn quote_dot(value: &str) -> String {
format!("\"{}\"", value.replace('\\', "\\\\").replace('"', "\\\""))
}
#[cfg(test)]
mod tests {
use super::{CallGraph, span_for};
use crate::{CallEvidence, CallRelation};
use crate::{CallTrace, NodeId, SourceLocation};
#[test]
fn graph_keeps_logical_edges_without_duplicate_invocations() {
let mut trace = CallTrace::full();
let source = SourceLocation {
file: "test.rs",
line: 1,
column: 1,
function: "root",
};
trace.with_at(
NodeId::from_path("root.rs", "Root"),
"root",
source,
|trace| {
trace.with_at(
NodeId::from_path("child.rs", "Child"),
"child",
source,
|_| {},
);
trace.with_at(
NodeId::from_path("child.rs", "Child"),
"child",
source,
|_| {},
);
},
);
let graph = CallGraph::from_trace(&trace);
assert_eq!(graph.node_count(), 2);
assert_eq!(graph.edge_count(), 1);
assert!(graph.to_dot().contains("nichlink"));
}
#[test]
fn span_contains_nichlink_fields() {
let call = crate::CallSite {
node: NodeId::from_path("test.rs", "Test"),
function: "test",
frame_id: 7,
source: None,
};
let _span = span_for(&call);
}
#[test]
fn all_evidence_kinds_share_marker_and_label_rendering() {
let relations = [
CallRelation {
caller: "a".to_owned(),
callee: "b".to_owned(),
evidence: CallEvidence::Live,
source: None,
mir_line: None,
caller_frame: None,
callee_frame: None,
},
CallRelation {
caller: "b".to_owned(),
callee: "c".to_owned(),
evidence: CallEvidence::Mir,
source: None,
mir_line: Some(4),
caller_frame: None,
callee_frame: None,
},
CallRelation {
caller: "c".to_owned(),
callee: "d".to_owned(),
evidence: CallEvidence::Source,
source: None,
mir_line: None,
caller_frame: None,
callee_frame: None,
},
];
let dot = CallGraph::from_relations(&relations).to_dot();
assert!(dot.contains("+ live"));
assert!(dot.contains("? mir"));
assert!(dot.contains("~ source"));
assert!(CallEvidence::Live.confirmed());
assert!(!CallEvidence::Mir.confirmed());
}
}