Skip to content

Commit bbb4502

Browse files
committed
mk_graph: implement DOT renderer using GraphBuilder and refine traversal semantics
Implement a new DOT backend on top of the GraphBuilder abstraction, replacing the stub with a full renderer that consumes the traversal graph IR and emits DOT output with clusters, CFG edges, and deferred cross-function call edges. Refactor traversal to operate on full symbol names instead of precomputed IDs, simplifying `CallEdge` to carry only semantic information. Update external function handling to use symbol names and adjust `GraphBuilder::external_function` accordingly. Add function metadata (`symbol_name`, `is_unqualified`) to `RenderedFunction` so builders no longer need to recompute these properties. Replace generic CFG edge extraction with explicit terminator handling, producing labeled edges for control flow and unwind paths.
1 parent da9eabc commit bbb4502

4 files changed

Lines changed: 212 additions & 65 deletions

File tree

src/mk_graph/output/d2.rs

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
33
use crate::printer::SmirJson;
44

5-
use crate::mk_graph::util::escape_d2;
5+
use crate::mk_graph::util::{escape_d2, short_name};
66

77
use crate::mk_graph::traverse::render_graph;
88
use crate::mk_graph::traverse::{GraphBuilder, RenderedFunction};
@@ -51,7 +51,8 @@ impl GraphBuilder for D2Builder {
5151

5252
fn type_legend(&mut self, _lines: &[String]) {}
5353

54-
fn external_function(&mut self, id: &str, name: &str) {
54+
fn external_function(&mut self, name: &str) {
55+
let id = short_name(name);
5556
self.buf
5657
.push_str(&format!("{}: \"{}\"\n", id, escape_d2(name)));
5758
}
@@ -85,18 +86,19 @@ impl GraphBuilder for D2Builder {
8586
self.buf.push_str("}\n\n");
8687

8788
for edge in &func.call_edges {
89+
let callee_id = short_name(&edge.callee_name);
8890
self.buf.push_str(&format!(
8991
"{}: \"{}\"\n",
90-
edge.callee_id,
92+
callee_id,
9193
escape_d2(&edge.callee_name)
9294
));
9395

9496
self.buf
95-
.push_str(&format!("{}.style.fill: \"#ffe0e0\"\n", edge.callee_id));
97+
.push_str(&format!("{}.style.fill: \"#ffe0e0\"\n", callee_id));
9698

9799
self.buf.push_str(&format!(
98100
"{}.bb{} -> {}: call\n",
99-
func.id, edge.block_idx, edge.callee_id
101+
func.id, edge.block_idx, callee_id
100102
));
101103
}
102104
}

src/mk_graph/output/dot.rs

Lines changed: 162 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,9 @@ use crate::printer::SmirJson;
1111
use crate::MonoItemKind;
1212

1313
use crate::mk_graph::context::GraphContext;
14-
use crate::mk_graph::util::{block_name, is_unqualified, name_lines, short_name, GraphLabelString};
14+
use crate::mk_graph::util::{
15+
block_name, escape_dot, is_unqualified, name_lines, short_name, GraphLabelString,
16+
};
1517

1618
use crate::mk_graph::traverse::render_graph;
1719
use crate::mk_graph::traverse::{GraphBuilder, RenderedFunction};
@@ -311,16 +313,27 @@ impl SmirJson {
311313
}
312314

313315
// =============================================================================
314-
// DOT Builder
316+
// DOT Builder (new)
315317
// =============================================================================
316318

317319
pub struct DOTBuilder {
318320
buf: String,
321+
/// Cross-cluster call edges deferred until finish():
322+
/// (from_node_id, to_node_id, rendered_args)
323+
deferred_call_edges: Vec<(String, String, String)>,
319324
}
320325

321326
impl DOTBuilder {
322327
pub fn new() -> Self {
323-
Self { buf: String::new() }
328+
Self {
329+
buf: String::new(),
330+
deferred_call_edges: Vec::new(),
331+
}
332+
}
333+
334+
fn line(&mut self, s: &str) {
335+
self.buf.push_str(s);
336+
self.buf.push('\n');
324337
}
325338
}
326339

@@ -333,27 +346,164 @@ impl Default for DOTBuilder {
333346
impl GraphBuilder for DOTBuilder {
334347
type Output = String;
335348

336-
fn begin_graph(&mut self, _name: &str) {}
349+
fn begin_graph(&mut self, name: &str) {
350+
self.line("digraph {");
351+
self.line(&format!(" label=\"{}\";", escape_dot(name)));
352+
self.line(" node [shape=rectangle];");
353+
}
354+
355+
fn alloc_legend(&mut self, lines: &[String]) {
356+
if lines.is_empty() {
357+
return;
358+
}
359+
let mut all_lines = lines.to_vec();
360+
all_lines.push(String::new());
361+
let label = all_lines
362+
.iter()
363+
.map(|l| escape_dot(l))
364+
.collect::<Vec<_>>()
365+
.join("\\l");
366+
self.line(&format!(
367+
" node_allocs [label=\"{}\", style=\"filled\", color=lightyellow];",
368+
label
369+
));
370+
}
371+
372+
fn type_legend(&mut self, lines: &[String]) {
373+
// Only emit when there are entries beyond the header line.
374+
if lines.len() <= 1 {
375+
return;
376+
}
377+
let mut all_lines = lines.to_vec();
378+
all_lines.push(String::new());
379+
let label = all_lines
380+
.iter()
381+
.map(|l| escape_dot(l))
382+
.collect::<Vec<_>>()
383+
.join("\\l");
384+
self.line(&format!(
385+
" node_types [label=\"{}\", style=\"filled\", color=lavender];",
386+
label
387+
));
388+
}
389+
390+
/// Emit a red external node for a callee that has no defined body.
391+
/// `name` is the full resolved symbol name; block_name(name, 0) gives
392+
/// the node ID, matching the target IDs used in deferred call edges.
393+
fn external_function(&mut self, name: &str) {
394+
let node_id = block_name(name, 0);
395+
let label = escape_dot(&name_lines(name));
396+
self.line(&format!(" {} [label=\"{}\", color=red];", node_id, label));
397+
}
337398

338-
fn alloc_legend(&mut self, _lines: &[String]) {}
399+
fn render_function(&mut self, func: &RenderedFunction) {
400+
let cluster_color = if func.is_unqualified {
401+
"palegreen"
402+
} else {
403+
"lightgray"
404+
};
405+
406+
self.line(&format!(
407+
" subgraph cluster_{} {{",
408+
short_name(&func.symbol_name)
409+
));
410+
self.line(&format!(
411+
" label=\"{}\";",
412+
escape_dot(&func.display_name)
413+
));
414+
self.line(" style=\"filled\";");
415+
self.line(&format!(" color={};", cluster_color));
416+
417+
// LOCALS node
418+
{
419+
let mut lines = vec!["LOCALS".to_string()];
420+
for (idx, ty) in &func.locals {
421+
lines.push(format!("{} = {}", idx, ty));
422+
}
423+
lines.push(String::new());
424+
let label = lines
425+
.iter()
426+
.map(|l| escape_dot(l))
427+
.collect::<Vec<_>>()
428+
.join("\\l");
429+
self.line(&format!(
430+
" {}_locals [label=\"{}\", style=\"filled\", color=palegreen3];",
431+
short_name(&func.symbol_name),
432+
label
433+
));
434+
}
339435

340-
fn type_legend(&mut self, _lines: &[String]) {}
436+
// Block nodes
437+
for block in &func.blocks {
438+
let node_id = block_name(&func.symbol_name, block.idx);
439+
let mut parts: Vec<String> = block.stmts.clone();
440+
parts.push(block.terminator.clone());
441+
parts.push(String::new());
442+
let label = parts
443+
.iter()
444+
.map(|l| escape_dot(l))
445+
.collect::<Vec<_>>()
446+
.join("\\l");
447+
self.line(&format!(" {} [label=\"{}\"];", node_id, label));
448+
}
341449

342-
fn external_function(&mut self, _id: &str, _name: &str) {}
450+
// Intra-cluster CFG edges
451+
for block in &func.blocks {
452+
let from = block_name(&func.symbol_name, block.idx);
453+
for (target, label_opt) in &block.cfg_edges {
454+
let to = block_name(&func.symbol_name, *target);
455+
match label_opt {
456+
Some(lbl) => self.line(&format!(
457+
" {} -> {} [label=\"{}\"];",
458+
from,
459+
to,
460+
escape_dot(lbl)
461+
)),
462+
None => self.line(&format!(" {} -> {};", from, to)),
463+
}
464+
}
465+
}
343466

344-
fn render_function(&mut self, _func: &RenderedFunction) {}
467+
self.line(" }");
468+
469+
// Defer cross-cluster call edges - emitted after all clusters in finish().
470+
// callee_name is the full symbol name so block_name(callee_name, 0)
471+
// matches the node ID emitted by external_function or by another
472+
// cluster's block 0.
473+
for edge in &func.call_edges {
474+
let from = block_name(&func.symbol_name, edge.block_idx);
475+
let to = block_name(&edge.callee_name, 0);
476+
self.deferred_call_edges
477+
.push((from, to, edge.rendered_args.clone()));
478+
}
479+
}
345480

346-
fn static_item(&mut self, _id: &str, _name: &str) {}
481+
fn static_item(&mut self, id: &str, name: &str) {
482+
self.line(&format!(" {} [label=\"{}\"];", id, escape_dot(name)));
483+
}
347484

348-
fn asm_item(&mut self, _id: &str, _content: &str) {}
485+
fn asm_item(&mut self, id: &str, content: &str) {
486+
let text = content.lines().collect::<String>();
487+
self.line(&format!(" {} [label=\"{}\"];", id, escape_dot(&text)));
488+
}
349489

350-
fn finish(self) -> Self::Output {
490+
fn finish(mut self) -> String {
491+
// Emit all cross-cluster call edges after all subgraph clusters.
492+
for (from, to, args) in &self.deferred_call_edges {
493+
self.buf.push_str(&format!(
494+
" {} -> {} [label=\"{}\"];\n",
495+
from,
496+
to,
497+
escape_dot(args)
498+
));
499+
}
500+
self.buf.push_str("}\n");
351501
self.buf
352502
}
353503
}
354504

355505
// =============================================================================
356-
// Public entry point (new)
506+
// Public entry points (new)
357507
// =============================================================================
358508

359509
impl SmirJson {

0 commit comments

Comments
 (0)