use std::any::TypeId;
use std::collections::BTreeMap;
use std::fmt::Write as _;
use std::sync::Arc;
use crate::container::{Container, GroupElementKey, ProviderKey};
use crate::invoke::ScopedInvoker;
use crate::lifecycle::Lifecycle;
use crate::scope::ScopeId;
use crate::shutdown::Shutdowner;
use crate::trace::TARGET;
pub(crate) fn render_dot(container: &Container, invokers: &[ScopedInvoker]) -> String {
let renderer = DotRenderer::new(container, invokers);
renderer.render()
}
struct DotRenderer<'a> {
container: &'a Container,
invokers: &'a [ScopedInvoker],
nodes: Vec<NodeInfo>,
keys: BTreeMap<String, usize>,
edges: Vec<Edge>,
}
struct NodeInfo {
scope: ScopeId,
attrs: String,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum EdgeStyle {
Dependency,
GroupMember,
}
struct Edge {
from: usize,
to: usize,
style: EdgeStyle,
}
impl<'a> DotRenderer<'a> {
fn new(container: &'a Container, invokers: &'a [ScopedInvoker]) -> Self {
Self {
container,
invokers,
nodes: Vec::new(),
keys: BTreeMap::new(),
edges: Vec::new(),
}
}
fn render(mut self) -> String {
self.collect_nodes();
self.collect_dependency_edges();
self.collect_group_edges();
self.format_dot()
}
fn collect_nodes(&mut self) {
for &key in &self.container.provider_order {
let Some(provider) = self.container.provider_at(key) else {
continue;
};
let node_key = self.provider_node_key(key);
let label = if key.is_group_member() {
format!(
"{}\nctor={}\n(group member)",
label_type_name(provider.result_name()),
short_name(provider.constructor_name()),
)
} else if self.container.group_virtual_to_element.contains_key(&key) {
format!(
"{}\nctor={}\n(group aggregate)",
display_type_name(provider.result_name()),
provider.constructor_name(),
)
} else {
let mut label = format!(
"{}\nctor={}",
label_type_name(provider.result_name()),
short_name(provider.constructor_name()),
);
if key.private {
label.push_str("\n(private)");
}
label
};
self.ensure_node(&node_key, key.scope, &label);
}
for node in &self.container.value_nodes {
if is_framework_dep(node.type_id) {
continue;
}
let key = self.value_node_key(node.type_id, node.scope, node.private);
let label = format!("{}\n(supplied)", label_type_name(node.type_name));
self.ensure_node(&key, node.scope, &label);
}
for (index, scoped) in self.invokers.iter().enumerate() {
let function = scoped.invoker.name();
let key = self.invoker_node_key(index, function);
let label = format!("{}\n(invoker)", short_name(function));
self.ensure_node(&key, scoped.scope, &label);
}
}
fn collect_dependency_edges(&mut self) {
for &key in &self.container.provider_order {
let Some(provider) = self.container.provider_at(key) else {
continue;
};
let node = self.node_index(&self.provider_node_key(key));
for &(dep_id, dep_name) in provider.dep_types() {
if is_framework_dep(dep_id) {
continue;
}
if let Some(target) = self.resolve_dep(dep_id, dep_name, key.scope) {
self.edges.push(Edge {
from: target,
to: node,
style: EdgeStyle::Dependency,
});
}
}
}
for (index, scoped) in self.invokers.iter().enumerate() {
let node = self.node_index(&self.invoker_node_key(index, scoped.invoker.name()));
for &(dep_id, dep_name) in scoped.invoker.dep_types() {
if is_framework_dep(dep_id) {
continue;
}
if let Some(target) = self.resolve_dep(dep_id, dep_name, scoped.scope) {
self.edges.push(Edge {
from: target,
to: node,
style: EdgeStyle::Dependency,
});
}
}
}
}
fn collect_group_edges(&mut self) {
for reg in self.container.group_registrations.values() {
let group = self.node_index(&self.provider_node_key(reg.virtual_key));
let members = self
.container
.group_members
.get(&GroupElementKey {
element: reg.element,
})
.map(Vec::as_slice)
.unwrap_or(&[]);
for &member_key in members {
let member = self.node_index(&self.provider_node_key(member_key));
self.edges.push(Edge {
from: member,
to: group,
style: EdgeStyle::GroupMember,
});
}
}
}
fn ensure_node(&mut self, key: &str, scope: ScopeId, label: &str) -> usize {
if let Some(&existing) = self.keys.get(key) {
if self.nodes[existing].scope != scope {
tracing::warn!(
target: TARGET,
key,
"modrun graph node key reused across scopes: {key}"
);
}
return existing;
}
let cluster = self.container.scopes().name(scope);
let attrs = format!(
"label=\"{}\" tooltip=\"{}\"",
dot_escape(label),
dot_escape(&format!("{cluster}::{key}"))
);
let id = self.nodes.len();
self.nodes.push(NodeInfo { scope, attrs });
self.keys.insert(key.to_owned(), id);
id
}
fn node_index(&self, key: &str) -> usize {
*self
.keys
.get(key)
.unwrap_or_else(|| panic!("graph node missing for key: {key}"))
}
fn resolve_dep(&self, dep_id: TypeId, dep_name: &'static str, from: ScopeId) -> Option<usize> {
let key = if let Some((scope, private)) = self.container.resolve_value_binding(dep_id, from)
{
self.value_node_key(dep_id, scope, private)
} else if let Some((dep_key, _)) = self.container.resolve_provider(dep_id, from) {
self.provider_node_key(dep_key)
} else {
tracing::warn!(
target: TARGET,
dependency = dep_name,
module = self.container.scopes().name(from),
"modrun graph could not resolve dependency edge for {dep_name}"
);
return None;
};
if self.keys.contains_key(&key) {
return Some(self.node_index(&key));
}
tracing::warn!(
target: TARGET,
dependency = dep_name,
module = self.container.scopes().name(from),
"modrun graph dependency resolved but node is missing: {dep_name}"
);
None
}
fn provider_node_key(&self, key: ProviderKey) -> String {
let provider = self
.container
.provider_at(key)
.expect("provider node missing provider");
let module = self.container.scopes().name(key.scope);
if key.is_group_member() {
return format!(
"provider:{module}:{}#{}",
short_name(provider.constructor_name()),
key.ordinal
);
}
let type_name = if self.container.group_virtual_to_element.contains_key(&key) {
display_type_name(provider.result_name())
} else {
label_type_name(provider.result_name())
};
if key.private {
format!("provider:{module}:{type_name}:private")
} else {
format!("provider:{module}:{type_name}")
}
}
fn value_node_key(&self, type_id: TypeId, scope: ScopeId, private: bool) -> String {
let module = self.container.scopes().name(scope);
let privacy = if private { "private" } else { "public" };
format!("value:{module}:{type_id:?}:{privacy}")
}
fn invoker_node_key(&self, index: usize, function: &'static str) -> String {
format!("invoker:{index}:{}", short_name(function))
}
fn format_dot(&self) -> String {
let mut out = String::from("digraph modrun {\n rankdir=LR;\n node [shape=box];\n");
let mut clusters: BTreeMap<ScopeId, Vec<usize>> = BTreeMap::new();
for (index, node) in self.nodes.iter().enumerate() {
clusters.entry(node.scope).or_default().push(index);
}
for (scope_id, node_indices) in clusters {
let scope_name = self.container.scopes().name(scope_id);
if scope_id == ScopeId::ROOT {
for index in node_indices {
writeln!(out, " n{index} [{}];", self.nodes[index].attrs).unwrap();
}
continue;
}
writeln!(out, " subgraph cluster_{} {{", scope_id.discriminant()).unwrap();
writeln!(out, " label=\"{}\";", dot_escape(scope_name)).unwrap();
for index in node_indices {
writeln!(out, " n{index} [{}];", self.nodes[index].attrs).unwrap();
}
writeln!(out, " }}").unwrap();
}
for edge in &self.edges {
let style = match edge.style {
EdgeStyle::Dependency => "",
EdgeStyle::GroupMember => " [style=dotted, arrowhead=none]",
};
writeln!(out, " n{} -> n{}{style};", edge.from, edge.to).unwrap();
}
out.push_str("}\n");
out
}
}
fn display_type_name(full: &str) -> String {
full.strip_prefix("modrun::").unwrap_or(full).to_owned()
}
fn label_type_name(full: &str) -> String {
let trimmed = full
.strip_prefix("alloc::sync::")
.or_else(|| full.strip_prefix("std::sync::"))
.unwrap_or(full);
let trimmed = trimmed.strip_prefix("modrun::").unwrap_or(trimmed);
if trimmed.contains('<') {
trimmed.to_owned()
} else {
trimmed.rsplit("::").next().unwrap_or(trimmed).to_owned()
}
}
fn short_name(full: &str) -> &str {
full.rsplit("::").next().unwrap_or(full)
}
fn dot_escape(value: &str) -> String {
value
.replace('\\', "\\\\")
.replace('"', "\\\"")
.replace('\n', "\\n")
.replace('\r', "\\r")
}
fn is_framework_dep(id: TypeId) -> bool {
id == TypeId::of::<Lifecycle>()
|| id == TypeId::of::<Shutdowner>()
|| id == TypeId::of::<Arc<Lifecycle>>()
|| id == TypeId::of::<Arc<Shutdowner>>()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn dot_escape_handles_special_characters() {
let escaped = dot_escape("a\"b\\c\nd");
assert_eq!(escaped, "a\\\"b\\\\c\\nd");
}
#[test]
fn label_type_name_handles_arc_generics() {
assert_eq!(
label_type_name("alloc::sync::Arc<graph::Config>"),
"Arc<graph::Config>"
);
assert_eq!(label_type_name("graph::Config"), "Config");
assert_eq!(
label_type_name("my::Wrapper<my::Config>"),
"my::Wrapper<my::Config>"
);
}
#[test]
fn is_framework_dep_matches_lifecycle_and_shutdowner() {
assert!(is_framework_dep(TypeId::of::<Lifecycle>()));
assert!(is_framework_dep(TypeId::of::<Shutdowner>()));
assert!(is_framework_dep(TypeId::of::<Arc<Lifecycle>>()));
assert!(is_framework_dep(TypeId::of::<Arc<Shutdowner>>()));
assert!(!is_framework_dep(TypeId::of::<u32>()));
}
}