///|
/// Render complete incoming-closure evidence in original caller-to-callee direction without analysing or mutating it.
/// Top is deliberately absent. Parallel edges remain separate DOT statements.
pub fn render_function_dependents_dot(
report : FunctionDependentsReport,
) -> String {
let out = StringBuilder()
out.write_string("digraph moonsize_dependents {\n rankdir=LR;\n")
let title = [
"Functions with a modeled path to target #\{report.target.index}; nodes \{report.nodes.length()}; edges \{report.edges.length()}",
"Direct: solid; reference: dashed; indirect candidate: dotted. Edge labels are file offsets, not call-site counts.",
"Current reference model, not actual execution, source-change impact or removal savings. No global dead/live classification. Layout cost is not bounded.",
]
if !report.graph_complete {
title.push(
"WARNING: incomplete graph; diagnostics \{report.diagnostics.length()}",
)
}
if !report.graph_warning.is_empty() {
title.push(report.graph_warning)
}
out.write_string(
" graph [labelloc=\"t\", label=" + dependencies_dot_label(title) + "];\n",
)
for
(key, value) in [
("input_sha256", report.sha256),
("file_size", report.file_size.to_string()),
("target_index", report.target.index.to_string()),
("graph_complete", report.graph_complete.to_string()),
("unresolved_count", report.unresolved_count.to_string()),
("modeling_scope", report.modeling_scope),
("graph_warning", report.graph_warning),
(
"diagnostics_json",
diagnostic_records_json(report.diagnostics).stringify(),
),
] {
out.write_string(" " + key + "=" + dependencies_dot_quote(value) + ";\n")
}
let nodes = report.nodes.copy()
nodes.sort_by(fn(a, b) { a.node.index - b.node.index })
for entry in nodes {
let node = entry.node
let label = [
"#\{node.index}: " + node.name,
if node.imported {
"imported; encoded size not measured"
} else {
"local; encoded size " +
(match node.encoded_size {
Some(n) => "\{n} B"
None => "not measured"
})
},
"depth to target \{entry.depth}",
]
let target = node.index == report.target.index
if target {
label.push("query target")
}
out.write_string(
" fn_\{node.index} [shape=" +
(if node.imported { "ellipse" } else { "box" }) +
", label=" +
dependencies_dot_label(label) +
(if target {
", penwidth=2, style=filled, fillcolor=\"lightyellow\""
} else {
""
}) +
"];\n",
)
}
let edges = report.edges.copy()
edges.sort_by(fn(a, b) {
if a.caller != b.caller {
a.caller - b.caller
} else if a.offset != b.offset {
a.offset - b.offset
} else if a.callee != b.callee {
a.callee - b.callee
} else {
path_edge_rank(a.kind) - path_edge_rank(b.kind)
}
})
for edge in edges {
let (kind, style) = match edge.kind {
Direct => ("direct", "solid")
Reference => ("reference", "dashed")
Indirect => ("indirect candidate", "dotted")
}
out.write_string(
" fn_\{edge.caller} -> fn_\{edge.callee} [style=" +
style +
", label=" +
dependencies_dot_quote(kind + "; file offset \{edge.offset}") +
"];\n",
)
}
out.write_string("}\n")
out.to_string()
}