From 9388e5607a41b4276739a75a2a96999d3308a4f7 Mon Sep 17 00:00:00 2001 From: 0xh4ty <0xh4ty@proton.me> Date: Tue, 24 Feb 2026 09:36:00 +0530 Subject: [PATCH 01/23] mk_graph: introduce GraphBuilder trait --- src/mk_graph/traverse.rs | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 src/mk_graph/traverse.rs diff --git a/src/mk_graph/traverse.rs b/src/mk_graph/traverse.rs new file mode 100644 index 00000000..ce2ba2c8 --- /dev/null +++ b/src/mk_graph/traverse.rs @@ -0,0 +1,31 @@ +//! Generic MIR graph traversal. +//! +//! This module owns the traversal order and graph semantics. + +/// Format agnostic graph sink. +/// Implemented by all renderers. +pub trait GraphBuilder { + type Output; + + fn begin_graph(&mut self, name: &str); + + fn alloc_legend(&mut self, lines: &[String]); + + fn type_legend(&mut self, lines: &[String]); + + fn begin_function(&mut self, id: &str, label: &str, is_local: bool); + + fn block(&mut self, fn_id: &str, idx: usize, stmts: &[String], terminator: &str); + + fn block_edge(&mut self, fn_id: &str, from: usize, to: usize, label: Option<&str>); + + fn call_edge(&mut self, fn_id: &str, block: usize, callee_id: &str, callee_name: &str); + + fn end_function(&mut self, id: &str); + + fn static_item(&mut self, id: &str, name: &str); + + fn asm_item(&mut self, id: &str, content: &str); + + fn finish(self) -> Self::Output; +} From 4c3b0960572575fd7e365f75bd28d4d5a279e6aa Mon Sep 17 00:00:00 2001 From: 0xh4ty <0xh4ty@proton.me> Date: Tue, 24 Feb 2026 11:22:28 +0530 Subject: [PATCH 02/23] d2: extract item traversal helper --- src/mk_graph/output/d2.rs | 30 +++++++++++++++++------------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/src/mk_graph/output/d2.rs b/src/mk_graph/output/d2.rs index c3ccc491..895bbeca 100644 --- a/src/mk_graph/output/d2.rs +++ b/src/mk_graph/output/d2.rs @@ -20,19 +20,7 @@ impl SmirJson { output.push_str("direction: right\n\n"); render_d2_allocs_legend(&ctx, &mut output); - for item in self.items { - match item.mono_item_kind { - MonoItemKind::MonoItemFn { name, body, .. } => { - render_d2_function(&name, body.as_ref(), &ctx, &mut output); - } - MonoItemKind::MonoItemGlobalAsm { asm } => { - render_d2_asm(&asm, &mut output); - } - MonoItemKind::MonoItemStatic { name, .. } => { - render_d2_static(&name, &mut output); - } - } - } + render_d2_items(&self.items, &ctx, &mut output); output } @@ -42,6 +30,22 @@ impl SmirJson { // D2 Rendering Helpers // ============================================================================= +fn render_d2_items(items: &[crate::printer::Item], ctx: &GraphContext, out: &mut String) { + for item in items { + match &item.mono_item_kind { + MonoItemKind::MonoItemFn { name, body, .. } => { + render_d2_function(name, body.as_ref(), ctx, out); + } + MonoItemKind::MonoItemGlobalAsm { asm } => { + render_d2_asm(asm, out); + } + MonoItemKind::MonoItemStatic { name, .. } => { + render_d2_static(name, out); + } + } + } +} + fn render_d2_allocs_legend(ctx: &GraphContext, out: &mut String) { let legend_lines = ctx.allocs_legend_lines(); From 6ecfc1ef150b0260688b9a5fd6add9d88b825e99 Mon Sep 17 00:00:00 2001 From: 0xh4ty <0xh4ty@proton.me> Date: Tue, 24 Feb 2026 11:30:53 +0530 Subject: [PATCH 03/23] d2: introduce D2Builder struct --- src/mk_graph/output/d2.rs | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/src/mk_graph/output/d2.rs b/src/mk_graph/output/d2.rs index 895bbeca..6f3b9e49 100644 --- a/src/mk_graph/output/d2.rs +++ b/src/mk_graph/output/d2.rs @@ -11,7 +11,25 @@ use crate::mk_graph::util::{ escape_d2, is_unqualified, name_lines, short_name, terminator_targets, }; -impl SmirJson { +// ============================================================================= +// D2 Builder +// ============================================================================= + +struct D2Builder { + buf: String, +} + +impl D2Builder { + fn new() -> Self { + Self { buf: String::new() } + } +} + +// ============================================================================= +// Public entry point +// ============================================================================= + +impl SmirJson<'_> { /// Convert the MIR to D2 diagram format pub fn to_d2_file(self) -> String { let ctx = GraphContext::from_smir(&self); From ba097dd2f022eebc19104b01c58d7ea2bc19f4f5 Mon Sep 17 00:00:00 2001 From: 0xh4ty <0xh4ty@proton.me> Date: Tue, 24 Feb 2026 12:08:11 +0530 Subject: [PATCH 04/23] d2: implement GraphBuilder for D2Builder --- src/mk_graph/output/d2.rs | 74 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 74 insertions(+) diff --git a/src/mk_graph/output/d2.rs b/src/mk_graph/output/d2.rs index 6f3b9e49..5e3795fb 100644 --- a/src/mk_graph/output/d2.rs +++ b/src/mk_graph/output/d2.rs @@ -11,6 +11,8 @@ use crate::mk_graph::util::{ escape_d2, is_unqualified, name_lines, short_name, terminator_targets, }; +use crate::mk_graph::traverse::GraphBuilder; + // ============================================================================= // D2 Builder // ============================================================================= @@ -25,6 +27,78 @@ impl D2Builder { } } +impl GraphBuilder for D2Builder { + type Output = String; + + fn begin_graph(&mut self, _name: &str) { + self.buf.push_str("direction: right\n\n"); + } + + fn alloc_legend(&mut self, lines: &[String]) { + self.buf.push_str("ALLOCS: {\n"); + self.buf.push_str(" style.fill: \"#ffffcc\"\n"); + self.buf.push_str(" style.stroke: \"#999999\"\n"); + let legend_text = lines + .iter() + .map(|s| escape_d2(s)) + .collect::>() + .join("\\n"); + self.buf.push_str(&format!(" label: \"{}\"\n", legend_text)); + self.buf.push_str("}\n\n"); + } + + fn type_legend(&mut self, _: &[String]) {} + + fn begin_function(&mut self, id: &str, label: &str, _is_local: bool) { + self.buf.push_str(&format!("{}: {{\n", id)); + self.buf.push_str(&format!(" label: \"{}\"\n", label)); + self.buf.push_str(" style.fill: \"#e0e0ff\"\n"); + } + + fn block( &mut self, _fn_id: &str, idx: usize, stmts: &[String], terminator: &str) { + let mut label = format!("bb{}:", idx); + for stmt in stmts { + label.push_str(&format!("\\n{}", stmt)); + } + label.push_str(&format!("\\n---\\n{}", terminator)); + + self.buf.push_str(&format!(" bb{}: \"{}\"\n", idx, label)); + } + + fn block_edge(&mut self, _fn_id: &str, from: usize, to: usize, _label: Option<&str>) { + self.buf.push_str(&format!(" bb{} -> bb{}\n", from, to)); + } + + fn call_edge(&mut self, fn_id: &str, block: usize, callee_id: &str, callee_name: &str) { + self.buf.push_str(&format!("{}: \"{}\"\n", callee_id, escape_d2(callee_name))); + self.buf.push_str(&format!("{}.style.fill: \"#ffe0e0\"\n", callee_id)); + self.buf.push_str(&format!("{}.bb{} -> {}: call\n", fn_id, block, callee_id + )); + } + + fn end_function(&mut self, _id: &str) { + self.buf.push_str("}\n\n"); + } + + fn static_item(&mut self, id: &str, name: &str) { + self.buf + .push_str(&format!("{}: \"{}\" {{\n", id, escape_d2(name))); + self.buf.push_str(" style.fill: \"#e0ffe0\"\n"); + self.buf.push_str("}\n\n"); + } + + fn asm_item(&mut self, id: &str, content: &str) { + let asm_text = escape_d2(&content.lines().collect::()); + self.buf.push_str(&format!("{}: \"{}\" {{\n", id, asm_text)); + self.buf.push_str(" style.fill: \"#ffe0ff\"\n"); + self.buf.push_str("}\n\n"); + } + + fn finish(self) -> String { + self.buf + } +} + // ============================================================================= // Public entry point // ============================================================================= From dc6668266a927fa63638f9b8d15ebf1420e524a6 Mon Sep 17 00:00:00 2001 From: 0xh4ty <0xh4ty@proton.me> Date: Sun, 1 Mar 2026 10:13:14 +0530 Subject: [PATCH 05/23] mk_graph: add generic graph traversal --- src/mk_graph/mod.rs | 1 + src/mk_graph/traverse.rs | 97 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 98 insertions(+) diff --git a/src/mk_graph/mod.rs b/src/mk_graph/mod.rs index 480ee518..58fbe17d 100644 --- a/src/mk_graph/mod.rs +++ b/src/mk_graph/mod.rs @@ -15,6 +15,7 @@ pub mod context; pub mod index; pub mod output; pub mod util; +pub mod traverse; // Re-exports for convenience pub use context::GraphContext; diff --git a/src/mk_graph/traverse.rs b/src/mk_graph/traverse.rs index ce2ba2c8..3f865405 100644 --- a/src/mk_graph/traverse.rs +++ b/src/mk_graph/traverse.rs @@ -1,6 +1,16 @@ //! Generic MIR graph traversal. //! //! This module owns the traversal order and graph semantics. +extern crate stable_mir; +use stable_mir::mir::TerminatorKind; + +use crate::printer::SmirJson; +use crate::MonoItemKind; + +use crate::mk_graph::context::GraphContext; +use crate::mk_graph::util::{ + escape_d2, is_unqualified, name_lines, short_name, terminator_targets, +}; /// Format agnostic graph sink. /// Implemented by all renderers. @@ -29,3 +39,90 @@ pub trait GraphBuilder { fn finish(self) -> Self::Output; } + +pub fn render_graph( + smir: &SmirJson, + mut builder: B, +) -> B::Output { + let ctx = GraphContext::from_smir(smir); + + builder.begin_graph(&smir.name); + + builder.alloc_legend(&ctx.allocs_legend_lines()); + builder.type_legend(&ctx.types_legend_lines()); + + for item in &smir.items { + match &item.mono_item_kind { + MonoItemKind::MonoItemFn { name, body, .. } => { + render_function(&ctx, &mut builder, name, body.as_ref()); + } + MonoItemKind::MonoItemStatic { name, .. } => { + let id = short_name(name); + builder.static_item(&id, name); + } + MonoItemKind::MonoItemGlobalAsm { asm } => { + let id = short_name(asm); + builder.asm_item(&id, asm); + } + } + } + + builder.finish() +} + +fn render_function( + ctx: &GraphContext, + builder: &mut B, + name: &str, + body: Option<&stable_mir::mir::Body>, +) { + let fn_id = short_name(name); + let label = escape_d2(&name_lines(name)); + let is_local = true; // preserve current behavior + + builder.begin_function(&fn_id, &label, is_local); + + if let Some(body) = body { + // blocks + for (idx, block) in body.blocks.iter().enumerate() { + let stmts = block + .statements + .iter() + .map(|s| escape_d2(&ctx.render_stmt(s))) + .collect::>(); + + let term = escape_d2(&ctx.render_terminator(&block.terminator)); + + builder.block(&fn_id, idx, &stmts, &term); + } + + // CFG edges + for (idx, block) in body.blocks.iter().enumerate() { + for target in terminator_targets(&block.terminator) { + builder.block_edge(&fn_id, idx, target, None); + } + } + } + + builder.end_function(&fn_id); + + // Call edges (outside container) + if let Some(body) = body { + for (idx, block) in body.blocks.iter().enumerate() { + let TerminatorKind::Call { func, .. } = &block.terminator.kind else { + continue; + }; + + let Some(callee_name) = ctx.resolve_call_target(func) else { + continue; + }; + + if !is_unqualified(&callee_name) { + continue; + } + + let callee_id = short_name(&callee_name); + builder.call_edge(&fn_id, idx, &callee_id, &callee_name); + } + } +} From d063ffeca740b505d0e5acecbaa94d13e119f987 Mon Sep 17 00:00:00 2001 From: 0xh4ty <0xh4ty@proton.me> Date: Sun, 1 Mar 2026 10:39:47 +0530 Subject: [PATCH 06/23] d2: wire GraphBuilder traversal behind flag --- src/mk_graph/mod.rs | 8 +++++++- src/mk_graph/output/d2.rs | 6 ++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/src/mk_graph/mod.rs b/src/mk_graph/mod.rs index 58fbe17d..e96028d7 100644 --- a/src/mk_graph/mod.rs +++ b/src/mk_graph/mod.rs @@ -46,7 +46,13 @@ pub fn emit_dotfile(tcx: TyCtxt<'_>) { /// Entry point to write the D2 file pub fn emit_d2file(tcx: TyCtxt<'_>) { - let smir_d2 = collect_smir(tcx).to_d2_file(); + let smir = collect_smir(tcx); + + let smir_d2 = if std::env::var("SMIR_D2_NEW").is_ok() { + smir.to_d2_file_new() + } else { + smir.to_d2_file() + }; match mir_output_path(tcx, "smir.d2") { OutputDest::Stdout => { diff --git a/src/mk_graph/output/d2.rs b/src/mk_graph/output/d2.rs index 5e3795fb..b5380dc4 100644 --- a/src/mk_graph/output/d2.rs +++ b/src/mk_graph/output/d2.rs @@ -12,6 +12,7 @@ use crate::mk_graph::util::{ }; use crate::mk_graph::traverse::GraphBuilder; +use crate::mk_graph::traverse::render_graph; // ============================================================================= // D2 Builder @@ -116,6 +117,11 @@ impl SmirJson<'_> { output } + + /// Convert the MIR to D2 using GraphBuilder traversal (experimental) + pub fn to_d2_file_new(&self) -> String { + render_graph(self, D2Builder::new()) + } } // ============================================================================= From 64bfeb1d0f53337f806b427f2f25ba619a114b18 Mon Sep 17 00:00:00 2001 From: 0xh4ty <0xh4ty@proton.me> Date: Sun, 1 Mar 2026 17:37:54 +0530 Subject: [PATCH 07/23] mk_graph: make traversal independent of D2 formatting --- src/mk_graph/output/d2.rs | 6 +++--- src/mk_graph/traverse.rs | 10 +++++----- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/mk_graph/output/d2.rs b/src/mk_graph/output/d2.rs index b5380dc4..81c2d26b 100644 --- a/src/mk_graph/output/d2.rs +++ b/src/mk_graph/output/d2.rs @@ -52,16 +52,16 @@ impl GraphBuilder for D2Builder { fn begin_function(&mut self, id: &str, label: &str, _is_local: bool) { self.buf.push_str(&format!("{}: {{\n", id)); - self.buf.push_str(&format!(" label: \"{}\"\n", label)); + self.buf.push_str(&format!(" label: \"{}\"\n", escape_d2(label))); self.buf.push_str(" style.fill: \"#e0e0ff\"\n"); } fn block( &mut self, _fn_id: &str, idx: usize, stmts: &[String], terminator: &str) { let mut label = format!("bb{}:", idx); for stmt in stmts { - label.push_str(&format!("\\n{}", stmt)); + label.push_str(&format!("\\n{}", escape_d2(stmt))); } - label.push_str(&format!("\\n---\\n{}", terminator)); + label.push_str(&format!("\\n---\\n{}", escape_d2(terminator))); self.buf.push_str(&format!(" bb{}: \"{}\"\n", idx, label)); } diff --git a/src/mk_graph/traverse.rs b/src/mk_graph/traverse.rs index 3f865405..302088b4 100644 --- a/src/mk_graph/traverse.rs +++ b/src/mk_graph/traverse.rs @@ -9,7 +9,7 @@ use crate::MonoItemKind; use crate::mk_graph::context::GraphContext; use crate::mk_graph::util::{ - escape_d2, is_unqualified, name_lines, short_name, terminator_targets, + is_unqualified, name_lines, short_name, terminator_targets, }; /// Format agnostic graph sink. @@ -77,8 +77,8 @@ fn render_function( body: Option<&stable_mir::mir::Body>, ) { let fn_id = short_name(name); - let label = escape_d2(&name_lines(name)); - let is_local = true; // preserve current behavior + let label = name_lines(name); + let is_local = true; builder.begin_function(&fn_id, &label, is_local); @@ -88,10 +88,10 @@ fn render_function( let stmts = block .statements .iter() - .map(|s| escape_d2(&ctx.render_stmt(s))) + .map(|s| ctx.render_stmt(s)) .collect::>(); - let term = escape_d2(&ctx.render_terminator(&block.terminator)); + let term = ctx.render_terminator(&block.terminator); builder.block(&fn_id, idx, &stmts, &term); } From cbbbe4a4547030cd73d0ff1efc92060e89f2e118 Mon Sep 17 00:00:00 2001 From: 0xh4ty <0xh4ty@proton.me> Date: Sun, 1 Mar 2026 17:59:04 +0530 Subject: [PATCH 08/23] mk_graph: add comments clarifying traversal responsibilities --- src/mk_graph/traverse.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/mk_graph/traverse.rs b/src/mk_graph/traverse.rs index 302088b4..9bdd0ee9 100644 --- a/src/mk_graph/traverse.rs +++ b/src/mk_graph/traverse.rs @@ -40,6 +40,8 @@ pub trait GraphBuilder { fn finish(self) -> Self::Output; } +/// Format-agnostic MIR graph traversal. +/// Owns traversal order and graph semantics, delegates rendering to `GraphBuilder`. pub fn render_graph( smir: &SmirJson, mut builder: B, @@ -70,6 +72,8 @@ pub fn render_graph( builder.finish() } +/// Emit graph events for a single function body. +/// Traverses blocks, CFG edges, and call edges without renderer-specific logic. fn render_function( ctx: &GraphContext, builder: &mut B, From 795084c0f6492ca6089aeaabc6ae3f1ec50d0c05 Mon Sep 17 00:00:00 2001 From: 0xh4ty <0xh4ty@proton.me> Date: Mon, 2 Mar 2026 14:47:58 +0530 Subject: [PATCH 09/23] mk_graph: extend GraphBuilder to pass statements, terminators, and call args --- src/mk_graph/mod.rs | 2 +- src/mk_graph/output/d2.rs | 54 ++++++++++++++++++++++++++------------- src/mk_graph/traverse.rs | 37 ++++++++++++--------------- 3 files changed, 53 insertions(+), 40 deletions(-) diff --git a/src/mk_graph/mod.rs b/src/mk_graph/mod.rs index e96028d7..17c7cfdc 100644 --- a/src/mk_graph/mod.rs +++ b/src/mk_graph/mod.rs @@ -14,8 +14,8 @@ use crate::printer::collect_smir; pub mod context; pub mod index; pub mod output; -pub mod util; pub mod traverse; +pub mod util; // Re-exports for convenience pub use context::GraphContext; diff --git a/src/mk_graph/output/d2.rs b/src/mk_graph/output/d2.rs index 81c2d26b..c19cbccf 100644 --- a/src/mk_graph/output/d2.rs +++ b/src/mk_graph/output/d2.rs @@ -1,7 +1,7 @@ //! D2 diagram format output for MIR graphs. use crate::compat::stable_mir; -use stable_mir::mir::TerminatorKind; +use stable_mir::mir::{Operand, Statement, Terminator, TerminatorKind}; use crate::printer::SmirJson; use crate::MonoItemKind; @@ -11,24 +11,28 @@ use crate::mk_graph::util::{ escape_d2, is_unqualified, name_lines, short_name, terminator_targets, }; -use crate::mk_graph::traverse::GraphBuilder; use crate::mk_graph::traverse::render_graph; +use crate::mk_graph::traverse::GraphBuilder; // ============================================================================= // D2 Builder // ============================================================================= -struct D2Builder { +pub struct D2Builder<'a> { + ctx: &'a GraphContext, buf: String, } -impl D2Builder { - fn new() -> Self { - Self { buf: String::new() } +impl<'a> D2Builder<'a> { + pub fn new(ctx: &'a GraphContext) -> Self { + Self { + ctx, + buf: String::new(), + } } } -impl GraphBuilder for D2Builder { +impl<'a> GraphBuilder for D2Builder<'a> { type Output = String; fn begin_graph(&mut self, _name: &str) { @@ -44,7 +48,8 @@ impl GraphBuilder for D2Builder { .map(|s| escape_d2(s)) .collect::>() .join("\\n"); - self.buf.push_str(&format!(" label: \"{}\"\n", legend_text)); + self.buf + .push_str(&format!(" label: \"{}\"\n", legend_text)); self.buf.push_str("}\n\n"); } @@ -52,16 +57,19 @@ impl GraphBuilder for D2Builder { fn begin_function(&mut self, id: &str, label: &str, _is_local: bool) { self.buf.push_str(&format!("{}: {{\n", id)); - self.buf.push_str(&format!(" label: \"{}\"\n", escape_d2(label))); + self.buf + .push_str(&format!(" label: \"{}\"\n", escape_d2(label))); self.buf.push_str(" style.fill: \"#e0e0ff\"\n"); } - fn block( &mut self, _fn_id: &str, idx: usize, stmts: &[String], terminator: &str) { + fn block(&mut self, _fn_id: &str, idx: usize, stmts: &[Statement], terminator: &Terminator) { let mut label = format!("bb{}:", idx); for stmt in stmts { - label.push_str(&format!("\\n{}", escape_d2(stmt))); + let s = self.ctx.render_stmt(stmt); + label.push_str(&format!("\\n{}", escape_d2(&s))); } - label.push_str(&format!("\\n---\\n{}", escape_d2(terminator))); + let term_str = self.ctx.render_terminator(terminator); + label.push_str(&format!("\\n---\\n{}", escape_d2(&term_str))); self.buf.push_str(&format!(" bb{}: \"{}\"\n", idx, label)); } @@ -70,11 +78,20 @@ impl GraphBuilder for D2Builder { self.buf.push_str(&format!(" bb{} -> bb{}\n", from, to)); } - fn call_edge(&mut self, fn_id: &str, block: usize, callee_id: &str, callee_name: &str) { - self.buf.push_str(&format!("{}: \"{}\"\n", callee_id, escape_d2(callee_name))); - self.buf.push_str(&format!("{}.style.fill: \"#ffe0e0\"\n", callee_id)); - self.buf.push_str(&format!("{}.bb{} -> {}: call\n", fn_id, block, callee_id - )); + fn call_edge( + &mut self, + fn_id: &str, + block: usize, + callee_id: &str, + callee_name: &str, + _args: &[Operand], + ) { + self.buf + .push_str(&format!("{}: \"{}\"\n", callee_id, escape_d2(callee_name))); + self.buf + .push_str(&format!("{}.style.fill: \"#ffe0e0\"\n", callee_id)); + self.buf + .push_str(&format!("{}.bb{} -> {}: call\n", fn_id, block, callee_id)); } fn end_function(&mut self, _id: &str) { @@ -120,7 +137,8 @@ impl SmirJson<'_> { /// Convert the MIR to D2 using GraphBuilder traversal (experimental) pub fn to_d2_file_new(&self) -> String { - render_graph(self, D2Builder::new()) + let ctx = GraphContext::from_smir(self); + render_graph(self, D2Builder::new(&ctx)) } } diff --git a/src/mk_graph/traverse.rs b/src/mk_graph/traverse.rs index 9bdd0ee9..162bb5da 100644 --- a/src/mk_graph/traverse.rs +++ b/src/mk_graph/traverse.rs @@ -2,15 +2,13 @@ //! //! This module owns the traversal order and graph semantics. extern crate stable_mir; -use stable_mir::mir::TerminatorKind; +use stable_mir::mir::{Operand, Statement, Terminator, TerminatorKind}; use crate::printer::SmirJson; use crate::MonoItemKind; use crate::mk_graph::context::GraphContext; -use crate::mk_graph::util::{ - is_unqualified, name_lines, short_name, terminator_targets, -}; +use crate::mk_graph::util::{is_unqualified, name_lines, short_name, terminator_targets}; /// Format agnostic graph sink. /// Implemented by all renderers. @@ -25,11 +23,18 @@ pub trait GraphBuilder { fn begin_function(&mut self, id: &str, label: &str, is_local: bool); - fn block(&mut self, fn_id: &str, idx: usize, stmts: &[String], terminator: &str); + fn block(&mut self, fn_id: &str, idx: usize, stmts: &[Statement], terminator: &Terminator); fn block_edge(&mut self, fn_id: &str, from: usize, to: usize, label: Option<&str>); - fn call_edge(&mut self, fn_id: &str, block: usize, callee_id: &str, callee_name: &str); + fn call_edge( + &mut self, + fn_id: &str, + block: usize, + callee_id: &str, + callee_name: &str, + args: &[Operand], + ); fn end_function(&mut self, id: &str); @@ -42,10 +47,7 @@ pub trait GraphBuilder { /// Format-agnostic MIR graph traversal. /// Owns traversal order and graph semantics, delegates rendering to `GraphBuilder`. -pub fn render_graph( - smir: &SmirJson, - mut builder: B, -) -> B::Output { +pub fn render_graph(smir: &SmirJson, mut builder: B) -> B::Output { let ctx = GraphContext::from_smir(smir); builder.begin_graph(&smir.name); @@ -89,15 +91,7 @@ fn render_function( if let Some(body) = body { // blocks for (idx, block) in body.blocks.iter().enumerate() { - let stmts = block - .statements - .iter() - .map(|s| ctx.render_stmt(s)) - .collect::>(); - - let term = ctx.render_terminator(&block.terminator); - - builder.block(&fn_id, idx, &stmts, &term); + builder.block(&fn_id, idx, &block.statements, &block.terminator); } // CFG edges @@ -113,7 +107,7 @@ fn render_function( // Call edges (outside container) if let Some(body) = body { for (idx, block) in body.blocks.iter().enumerate() { - let TerminatorKind::Call { func, .. } = &block.terminator.kind else { + let TerminatorKind::Call { func, args, .. } = &block.terminator.kind else { continue; }; @@ -126,7 +120,8 @@ fn render_function( } let callee_id = short_name(&callee_name); - builder.call_edge(&fn_id, idx, &callee_id, &callee_name); + + builder.call_edge(&fn_id, idx, &callee_id, &callee_name, args); } } } From 238958365d4ce019cdb98560e25566e7c03f1132 Mon Sep 17 00:00:00 2001 From: 0xh4ty <0xh4ty@proton.me> Date: Mon, 2 Mar 2026 15:08:35 +0530 Subject: [PATCH 10/23] d2: elide explicit lifetime in GraphBuilder impl --- src/mk_graph/output/d2.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/mk_graph/output/d2.rs b/src/mk_graph/output/d2.rs index c19cbccf..4190861e 100644 --- a/src/mk_graph/output/d2.rs +++ b/src/mk_graph/output/d2.rs @@ -32,7 +32,7 @@ impl<'a> D2Builder<'a> { } } -impl<'a> GraphBuilder for D2Builder<'a> { +impl GraphBuilder for D2Builder<'_> { type Output = String; fn begin_graph(&mut self, _name: &str) { From da51b89e44ecca32f1518f4de5b65d5743cfe5d1 Mon Sep 17 00:00:00 2001 From: 0xh4ty <0xh4ty@proton.me> Date: Thu, 5 Mar 2026 07:23:21 +0530 Subject: [PATCH 11/23] mk_graph: introduce RenderedFunction, RenderedBlock and CallEdge structures --- src/mk_graph/traverse.rs | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/src/mk_graph/traverse.rs b/src/mk_graph/traverse.rs index 162bb5da..051aa103 100644 --- a/src/mk_graph/traverse.rs +++ b/src/mk_graph/traverse.rs @@ -10,6 +10,33 @@ use crate::MonoItemKind; use crate::mk_graph::context::GraphContext; use crate::mk_graph::util::{is_unqualified, name_lines, short_name, terminator_targets}; +/// A single call edge discovered during traversal. +pub struct CallEdge { + pub block_idx: usize, + pub callee_id: String, + pub callee_name: String, + pub rendered_args: String, +} + +/// A single basic block with pre-rendered content. +pub struct RenderedBlock<'a> { + pub idx: usize, + pub stmts: Vec, + pub terminator: String, + pub raw_terminator: &'a Terminator, + pub cfg_edges: Vec<(usize, Option)>, +} + +/// A fully rendered function ready for format-specific builders. +pub struct RenderedFunction<'a> { + pub id: String, + pub display_name: String, + pub is_local: bool, + pub locals: Vec<(usize, String)>, + pub blocks: Vec>, + pub call_edges: Vec, +} + /// Format agnostic graph sink. /// Implemented by all renderers. pub trait GraphBuilder { From 096d49cc5ccc6e2186a4256687596de9595a9415 Mon Sep 17 00:00:00 2001 From: 0xh4ty <0xh4ty@proton.me> Date: Thu, 5 Mar 2026 10:56:55 +0530 Subject: [PATCH 12/23] mk_graph: refactor traversal and builders to use RenderedFunction model --- src/mk_graph/output/d2.rs | 121 +++++++++++++++++++++----------------- src/mk_graph/traverse.rs | 118 +++++++++++++++++++++---------------- 2 files changed, 134 insertions(+), 105 deletions(-) diff --git a/src/mk_graph/output/d2.rs b/src/mk_graph/output/d2.rs index 4190861e..e1b90144 100644 --- a/src/mk_graph/output/d2.rs +++ b/src/mk_graph/output/d2.rs @@ -1,7 +1,7 @@ //! D2 diagram format output for MIR graphs. use crate::compat::stable_mir; -use stable_mir::mir::{Operand, Statement, Terminator, TerminatorKind}; +use stable_mir::mir::{TerminatorKind}; use crate::printer::SmirJson; use crate::MonoItemKind; @@ -12,27 +12,23 @@ use crate::mk_graph::util::{ }; use crate::mk_graph::traverse::render_graph; -use crate::mk_graph::traverse::GraphBuilder; +use crate::mk_graph::traverse::{GraphBuilder, RenderedFunction}; // ============================================================================= // D2 Builder // ============================================================================= -pub struct D2Builder<'a> { - ctx: &'a GraphContext, +pub struct D2Builder { buf: String, } -impl<'a> D2Builder<'a> { - pub fn new(ctx: &'a GraphContext) -> Self { - Self { - ctx, - buf: String::new(), - } +impl D2Builder { + pub fn new() -> Self { + Self { buf: String::new() } } } -impl GraphBuilder for D2Builder<'_> { +impl GraphBuilder for D2Builder { type Output = String; fn begin_graph(&mut self, _name: &str) { @@ -40,65 +36,83 @@ impl GraphBuilder for D2Builder<'_> { } fn alloc_legend(&mut self, lines: &[String]) { + self.buf.push_str("ALLOCS: {\n"); self.buf.push_str(" style.fill: \"#ffffcc\"\n"); self.buf.push_str(" style.stroke: \"#999999\"\n"); - let legend_text = lines + + let text = lines .iter() - .map(|s| escape_d2(s)) + .map(|l| escape_d2(l)) .collect::>() .join("\\n"); - self.buf - .push_str(&format!(" label: \"{}\"\n", legend_text)); + + self.buf.push_str(&format!(" label: \"{}\"\n", text)); self.buf.push_str("}\n\n"); } - fn type_legend(&mut self, _: &[String]) {} + fn type_legend(&mut self, _lines: &[String]) {} + + fn external_function(&mut self, id: &str, name: &str) { - fn begin_function(&mut self, id: &str, label: &str, _is_local: bool) { - self.buf.push_str(&format!("{}: {{\n", id)); self.buf - .push_str(&format!(" label: \"{}\"\n", escape_d2(label))); - self.buf.push_str(" style.fill: \"#e0e0ff\"\n"); + .push_str(&format!("{}: \"{}\"\n", id, escape_d2(name))); } - fn block(&mut self, _fn_id: &str, idx: usize, stmts: &[Statement], terminator: &Terminator) { - let mut label = format!("bb{}:", idx); - for stmt in stmts { - let s = self.ctx.render_stmt(stmt); - label.push_str(&format!("\\n{}", escape_d2(&s))); - } - let term_str = self.ctx.render_terminator(terminator); - label.push_str(&format!("\\n---\\n{}", escape_d2(&term_str))); + fn render_function(&mut self, func: &RenderedFunction) { - self.buf.push_str(&format!(" bb{}: \"{}\"\n", idx, label)); - } + self.buf.push_str(&format!("{}: {{\n", func.id)); + self.buf + .push_str(&format!(" label: \"{}\"\n", escape_d2(&func.display_name))); + self.buf.push_str(" style.fill: \"#e0e0ff\"\n"); - fn block_edge(&mut self, _fn_id: &str, from: usize, to: usize, _label: Option<&str>) { - self.buf.push_str(&format!(" bb{} -> bb{}\n", from, to)); - } + for block in &func.blocks { - fn call_edge( - &mut self, - fn_id: &str, - block: usize, - callee_id: &str, - callee_name: &str, - _args: &[Operand], - ) { - self.buf - .push_str(&format!("{}: \"{}\"\n", callee_id, escape_d2(callee_name))); - self.buf - .push_str(&format!("{}.style.fill: \"#ffe0e0\"\n", callee_id)); - self.buf - .push_str(&format!("{}.bb{} -> {}: call\n", fn_id, block, callee_id)); - } + let mut label = format!("bb{}:", block.idx); + + for stmt in &block.stmts { + label.push_str(&format!("\\n{}", escape_d2(stmt))); + } + + label.push_str(&format!("\\n---\\n{}", escape_d2(&block.terminator))); + + self.buf + .push_str(&format!(" bb{}: \"{}\"\n", block.idx, label)); + } + + for block in &func.blocks { + for (target, _) in &block.cfg_edges { + self.buf + .push_str(&format!(" bb{} -> bb{}\n", block.idx, target)); + } + } - fn end_function(&mut self, _id: &str) { self.buf.push_str("}\n\n"); + + for edge in &func.call_edges { + + self.buf.push_str(&format!( + "{}: \"{}\"\n", + edge.callee_id, + escape_d2(&edge.callee_name) + )); + + self.buf.push_str(&format!( + "{}.style.fill: \"#ffe0e0\"\n", + edge.callee_id + )); + + self.buf.push_str(&format!( + "{}.bb{} -> {}: call\n", + func.id, + edge.block_idx, + edge.callee_id + )); + } } fn static_item(&mut self, id: &str, name: &str) { + self.buf .push_str(&format!("{}: \"{}\" {{\n", id, escape_d2(name))); self.buf.push_str(" style.fill: \"#e0ffe0\"\n"); @@ -106,8 +120,10 @@ impl GraphBuilder for D2Builder<'_> { } fn asm_item(&mut self, id: &str, content: &str) { - let asm_text = escape_d2(&content.lines().collect::()); - self.buf.push_str(&format!("{}: \"{}\" {{\n", id, asm_text)); + + let text = escape_d2(&content.lines().collect::()); + + self.buf.push_str(&format!("{}: \"{}\" {{\n", id, text)); self.buf.push_str(" style.fill: \"#ffe0ff\"\n"); self.buf.push_str("}\n\n"); } @@ -137,8 +153,7 @@ impl SmirJson<'_> { /// Convert the MIR to D2 using GraphBuilder traversal (experimental) pub fn to_d2_file_new(&self) -> String { - let ctx = GraphContext::from_smir(self); - render_graph(self, D2Builder::new(&ctx)) + render_graph(self, D2Builder::new()) } } diff --git a/src/mk_graph/traverse.rs b/src/mk_graph/traverse.rs index 051aa103..40fdf59f 100644 --- a/src/mk_graph/traverse.rs +++ b/src/mk_graph/traverse.rs @@ -2,7 +2,7 @@ //! //! This module owns the traversal order and graph semantics. extern crate stable_mir; -use stable_mir::mir::{Operand, Statement, Terminator, TerminatorKind}; +use stable_mir::mir::{Body, Terminator, TerminatorKind}; use crate::printer::SmirJson; use crate::MonoItemKind; @@ -48,22 +48,9 @@ pub trait GraphBuilder { fn type_legend(&mut self, lines: &[String]); - fn begin_function(&mut self, id: &str, label: &str, is_local: bool); + fn external_function(&mut self, id: &str, name: &str); - fn block(&mut self, fn_id: &str, idx: usize, stmts: &[Statement], terminator: &Terminator); - - fn block_edge(&mut self, fn_id: &str, from: usize, to: usize, label: Option<&str>); - - fn call_edge( - &mut self, - fn_id: &str, - block: usize, - callee_id: &str, - callee_name: &str, - args: &[Operand], - ); - - fn end_function(&mut self, id: &str); + fn render_function(&mut self, func: &RenderedFunction); fn static_item(&mut self, id: &str, name: &str); @@ -85,7 +72,8 @@ pub fn render_graph(smir: &SmirJson, mut builder: B) -> B::Outp for item in &smir.items { match &item.mono_item_kind { MonoItemKind::MonoItemFn { name, body, .. } => { - render_function(&ctx, &mut builder, name, body.as_ref()); + let func = render_function(&ctx, name, body.as_ref()); + builder.render_function(&func); } MonoItemKind::MonoItemStatic { name, .. } => { let id = short_name(name); @@ -103,52 +91,78 @@ pub fn render_graph(smir: &SmirJson, mut builder: B) -> B::Outp /// Emit graph events for a single function body. /// Traverses blocks, CFG edges, and call edges without renderer-specific logic. -fn render_function( +fn render_function<'a>( ctx: &GraphContext, - builder: &mut B, name: &str, - body: Option<&stable_mir::mir::Body>, -) { - let fn_id = short_name(name); - let label = name_lines(name); - let is_local = true; + body: Option<&'a Body>, +) -> RenderedFunction<'a> { + let id = short_name(name); + let display_name = name_lines(name); + let is_local = body.is_some(); - builder.begin_function(&fn_id, &label, is_local); + let mut blocks = Vec::new(); + let mut call_edges = Vec::new(); + let mut locals = Vec::new(); if let Some(body) = body { - // blocks - for (idx, block) in body.blocks.iter().enumerate() { - builder.block(&fn_id, idx, &block.statements, &block.terminator); - } - // CFG edges - for (idx, block) in body.blocks.iter().enumerate() { - for target in terminator_targets(&block.terminator) { - builder.block_edge(&fn_id, idx, target, None); - } + for (idx, decl) in body.local_decls() { + locals.push((idx, ctx.render_type_with_layout(decl.ty))); } - } - builder.end_function(&fn_id); - - // Call edges (outside container) - if let Some(body) = body { for (idx, block) in body.blocks.iter().enumerate() { - let TerminatorKind::Call { func, args, .. } = &block.terminator.kind else { - continue; - }; - let Some(callee_name) = ctx.resolve_call_target(func) else { - continue; - }; - - if !is_unqualified(&callee_name) { - continue; + let stmts = block + .statements + .iter() + .map(|s| ctx.render_stmt(s)) + .collect(); + + let terminator = ctx.render_terminator(&block.terminator); + + let cfg_edges = terminator_targets(&block.terminator) + .into_iter() + .map(|t| (t, None)) + .collect(); + + blocks.push(RenderedBlock { + idx, + stmts, + terminator, + raw_terminator: &block.terminator, + cfg_edges, + }); + + if let TerminatorKind::Call { func, args, .. } = &block.terminator.kind { + + if let Some(callee) = ctx.resolve_call_target(func) { + + if is_unqualified(&callee) { + + let rendered_args = args + .iter() + .map(|a| ctx.render_operand(a)) + .collect::>() + .join(", "); + + call_edges.push(CallEdge { + block_idx: idx, + callee_id: short_name(&callee), + callee_name: callee, + rendered_args, + }); + } + } } - - let callee_id = short_name(&callee_name); - - builder.call_edge(&fn_id, idx, &callee_id, &callee_name, args); } } + + RenderedFunction { + id, + display_name, + is_local, + locals, + blocks, + call_edges, + } } From d617db43fcd8074acb83b9058f853457d2035a0f Mon Sep 17 00:00:00 2001 From: 0xh4ty <0xh4ty@proton.me> Date: Thu, 5 Mar 2026 12:03:33 +0530 Subject: [PATCH 13/23] mk_graph: add body hash to function IDs to avoid collisions --- src/mk_graph/traverse.rs | 8 ++++++-- src/mk_graph/util.rs | 28 +++++++++++++++++++++++++++- 2 files changed, 33 insertions(+), 3 deletions(-) diff --git a/src/mk_graph/traverse.rs b/src/mk_graph/traverse.rs index 40fdf59f..4dec2a16 100644 --- a/src/mk_graph/traverse.rs +++ b/src/mk_graph/traverse.rs @@ -8,7 +8,7 @@ use crate::printer::SmirJson; use crate::MonoItemKind; use crate::mk_graph::context::GraphContext; -use crate::mk_graph::util::{is_unqualified, name_lines, short_name, terminator_targets}; +use crate::mk_graph::util::{is_unqualified, name_lines, short_name, terminator_targets, hash_body}; /// A single call edge discovered during traversal. pub struct CallEdge { @@ -96,7 +96,11 @@ fn render_function<'a>( name: &str, body: Option<&'a Body>, ) -> RenderedFunction<'a> { - let id = short_name(name); + let id = match body { + Some(b) => format!("fn_{}_{}", short_name(name), hash_body(b)), + None => format!("fn_{}_no_body", short_name(name)), + }; + let display_name = name_lines(name); let is_local = body.is_some(); diff --git a/src/mk_graph/util.rs b/src/mk_graph/util.rs index ac21cbb1..db7a8efa 100644 --- a/src/mk_graph/util.rs +++ b/src/mk_graph/util.rs @@ -5,7 +5,7 @@ use std::hash::{DefaultHasher, Hash, Hasher}; use crate::compat::stable_mir; use stable_mir::mir::{ AggregateKind, BorrowKind, ConstOperand, Mutability, NonDivergingIntrinsic, NullOp, Operand, - Place, ProjectionElem, Rvalue, Terminator, TerminatorKind, UnwindAction, + Place, ProjectionElem, Rvalue, Terminator, TerminatorKind, UnwindAction, Body, }; use stable_mir::ty::{IndexedVal, RigidTy}; @@ -280,3 +280,29 @@ pub fn terminator_targets(term: &Terminator) -> Vec { } } } + +/// Generate a consistent short hash for a MIR body. +/// Used to avoid fn_id collisions between monomorphizations. +pub fn hash_body(body: &Body) -> u64 { + let mut h = DefaultHasher::new(); + + // Hash number of blocks + body.blocks.len().hash(&mut h); + + for (idx, block) in body.blocks.iter().enumerate() { + idx.hash(&mut h); + + // Hash terminator kind + std::mem::discriminant(&block.terminator.kind).hash(&mut h); + + // Hash control-flow edges + for target in terminator_targets(&block.terminator) { + target.hash(&mut h); + } + + // Statement count for entropy + block.statements.len().hash(&mut h); + } + + h.finish() +} From ff84ec782c6cc08b3edd5e07e02659142add6d20 Mon Sep 17 00:00:00 2001 From: 0xh4ty <0xh4ty@proton.me> Date: Thu, 5 Mar 2026 12:11:55 +0530 Subject: [PATCH 14/23] d2: remove SMIR_D2_NEW flag and use new traversal by default --- src/mk_graph/mod.rs | 6 +- src/mk_graph/output/d2.rs | 152 +------------------------------------- 2 files changed, 4 insertions(+), 154 deletions(-) diff --git a/src/mk_graph/mod.rs b/src/mk_graph/mod.rs index 17c7cfdc..e93c4d00 100644 --- a/src/mk_graph/mod.rs +++ b/src/mk_graph/mod.rs @@ -48,11 +48,7 @@ pub fn emit_dotfile(tcx: TyCtxt<'_>) { pub fn emit_d2file(tcx: TyCtxt<'_>) { let smir = collect_smir(tcx); - let smir_d2 = if std::env::var("SMIR_D2_NEW").is_ok() { - smir.to_d2_file_new() - } else { - smir.to_d2_file() - }; + let smir_d2 = smir.to_d2_file(); match mir_output_path(tcx, "smir.d2") { OutputDest::Stdout => { diff --git a/src/mk_graph/output/d2.rs b/src/mk_graph/output/d2.rs index e1b90144..f3ab22a7 100644 --- a/src/mk_graph/output/d2.rs +++ b/src/mk_graph/output/d2.rs @@ -1,15 +1,10 @@ //! D2 diagram format output for MIR graphs. use crate::compat::stable_mir; -use stable_mir::mir::{TerminatorKind}; use crate::printer::SmirJson; -use crate::MonoItemKind; -use crate::mk_graph::context::GraphContext; -use crate::mk_graph::util::{ - escape_d2, is_unqualified, name_lines, short_name, terminator_targets, -}; +use crate::mk_graph::util::escape_d2; use crate::mk_graph::traverse::render_graph; use crate::mk_graph::traverse::{GraphBuilder, RenderedFunction}; @@ -138,149 +133,8 @@ impl GraphBuilder for D2Builder { // ============================================================================= impl SmirJson<'_> { - /// Convert the MIR to D2 diagram format - pub fn to_d2_file(self) -> String { - let ctx = GraphContext::from_smir(&self); - let mut output = String::new(); - - output.push_str("direction: right\n\n"); - render_d2_allocs_legend(&ctx, &mut output); - - render_d2_items(&self.items, &ctx, &mut output); - - output - } - - /// Convert the MIR to D2 using GraphBuilder traversal (experimental) - pub fn to_d2_file_new(&self) -> String { + /// Convert the MIR to D2 using GraphBuilder traversal + pub fn to_d2_file(&self) -> String { render_graph(self, D2Builder::new()) } } - -// ============================================================================= -// D2 Rendering Helpers -// ============================================================================= - -fn render_d2_items(items: &[crate::printer::Item], ctx: &GraphContext, out: &mut String) { - for item in items { - match &item.mono_item_kind { - MonoItemKind::MonoItemFn { name, body, .. } => { - render_d2_function(name, body.as_ref(), ctx, out); - } - MonoItemKind::MonoItemGlobalAsm { asm } => { - render_d2_asm(asm, out); - } - MonoItemKind::MonoItemStatic { name, .. } => { - render_d2_static(name, out); - } - } - } -} - -fn render_d2_allocs_legend(ctx: &GraphContext, out: &mut String) { - let legend_lines = ctx.allocs_legend_lines(); - - out.push_str("ALLOCS: {\n"); - out.push_str(" style.fill: \"#ffffcc\"\n"); - out.push_str(" style.stroke: \"#999999\"\n"); - let legend_text = legend_lines - .iter() - .map(|s| escape_d2(s)) - .collect::>() - .join("\\n"); - out.push_str(&format!(" label: \"{}\"\n", legend_text)); - out.push_str("}\n\n"); -} - -fn render_d2_function( - name: &str, - body: Option<&stable_mir::mir::Body>, - ctx: &GraphContext, - out: &mut String, -) { - let fn_id = short_name(name); - let display_name = escape_d2(&name_lines(name)); - - // Function container - out.push_str(&format!("{}: {{\n", fn_id)); - out.push_str(&format!(" label: \"{}\"\n", display_name)); - out.push_str(" style.fill: \"#e0e0ff\"\n"); - - if let Some(body) = body { - render_d2_blocks(body, ctx, out); - render_d2_block_edges(body, out); - } - - out.push_str("}\n\n"); - - // Call edges (must be outside the container) - if let Some(body) = body { - render_d2_call_edges(&fn_id, body, ctx, out); - } -} - -fn render_d2_blocks(body: &stable_mir::mir::Body, ctx: &GraphContext, out: &mut String) { - for (idx, block) in body.blocks.iter().enumerate() { - let stmts: Vec = block - .statements - .iter() - .map(|s| escape_d2(&ctx.render_stmt(s))) - .collect(); - let term_str = escape_d2(&ctx.render_terminator(&block.terminator)); - - let mut label = format!("bb{}:", idx); - for stmt in &stmts { - label.push_str(&format!("\\n{}", stmt)); - } - label.push_str(&format!("\\n---\\n{}", term_str)); - - out.push_str(&format!(" bb{}: \"{}\"\n", idx, label)); - } -} - -fn render_d2_block_edges(body: &stable_mir::mir::Body, out: &mut String) { - for (idx, block) in body.blocks.iter().enumerate() { - for target in terminator_targets(&block.terminator) { - out.push_str(&format!(" bb{} -> bb{}\n", idx, target)); - } - } -} - -fn render_d2_call_edges( - fn_id: &str, - body: &stable_mir::mir::Body, - ctx: &GraphContext, - out: &mut String, -) { - for (idx, block) in body.blocks.iter().enumerate() { - let TerminatorKind::Call { func, .. } = &block.terminator.kind else { - continue; - }; - let Some(callee_name) = ctx.resolve_call_target(func) else { - continue; - }; - if !is_unqualified(&callee_name) { - continue; - } - - let target_id = short_name(&callee_name); - out.push_str(&format!("{}: \"{}\"\n", target_id, escape_d2(&callee_name))); - out.push_str(&format!("{}.style.fill: \"#ffe0e0\"\n", target_id)); - out.push_str(&format!("{}.bb{} -> {}: call\n", fn_id, idx, target_id)); - } -} - -fn render_d2_asm(asm: &str, out: &mut String) { - let asm_id = short_name(asm); - let asm_text = escape_d2(&asm.lines().collect::()); - out.push_str(&format!("{}: \"{}\" {{\n", asm_id, asm_text)); - out.push_str(" style.fill: \"#ffe0ff\"\n"); - out.push_str("}\n\n"); -} - -fn render_d2_static(name: &str, out: &mut String) { - let static_id = short_name(name); - out.push_str(&format!("{}: \"{}\" {{\n", static_id, escape_d2(name))); - out.push_str(" style.fill: \"#e0ffe0\"\n"); - out.push_str("}\n\n"); -} From 32c6a5541f32c1e77126b0810d26d9a617ba8b5e Mon Sep 17 00:00:00 2001 From: 0xh4ty <0xh4ty@proton.me> Date: Thu, 5 Mar 2026 12:23:50 +0530 Subject: [PATCH 15/23] mk_graph: expand documentation for traversal and rendering structures Add detailed Rustdoc comments for GraphBuilder, RenderedFunction, RenderedBlock, CallEdge, and traversal entry points. The documentation explains the separation between MIR traversal and format-specific rendering, and clarifies the responsibilities of each structure. This improves discoverability via `cargo doc` and makes the graph rendering architecture easier to understand for future contributors. --- src/mk_graph/traverse.rs | 40 +++++++++++++++++++++++++++++++++------- 1 file changed, 33 insertions(+), 7 deletions(-) diff --git a/src/mk_graph/traverse.rs b/src/mk_graph/traverse.rs index 4dec2a16..ac564385 100644 --- a/src/mk_graph/traverse.rs +++ b/src/mk_graph/traverse.rs @@ -10,7 +10,10 @@ use crate::MonoItemKind; use crate::mk_graph::context::GraphContext; use crate::mk_graph::util::{is_unqualified, name_lines, short_name, terminator_targets, hash_body}; -/// A single call edge discovered during traversal. +/// Represents a call from a block to another function. +/// +/// The callee is resolved during traversal and arguments are already +/// rendered as a string. Builders may choose how to visualize this edge. pub struct CallEdge { pub block_idx: usize, pub callee_id: String, @@ -18,7 +21,14 @@ pub struct CallEdge { pub rendered_args: String, } -/// A single basic block with pre-rendered content. +/// A basic block with pre-rendered textual content and structural edges. +/// +/// `stmts` and `terminator` are pre-rendered strings produced using +/// `GraphContext`. Builders are free to format or escape them according +/// to their output format. +/// +/// `raw_terminator` is provided as an escape hatch for renderers that +/// need to inspect the underlying MIR structure. pub struct RenderedBlock<'a> { pub idx: usize, pub stmts: Vec, @@ -27,7 +37,12 @@ pub struct RenderedBlock<'a> { pub cfg_edges: Vec<(usize, Option)>, } -/// A fully rendered function ready for format-specific builders. +/// A fully analyzed MIR function ready for rendering. +/// +/// The traversal layer resolves call targets, renders statements and +/// terminators, and computes the control-flow edges. Builders receive +/// this structure and are responsible only for formatting it into a +/// specific graph representation. pub struct RenderedFunction<'a> { pub id: String, pub display_name: String, @@ -37,8 +52,16 @@ pub struct RenderedFunction<'a> { pub call_edges: Vec, } -/// Format agnostic graph sink. -/// Implemented by all renderers. +/// Trait implemented by graph renderers. +/// +/// The traversal layer walks the MIR graph and constructs a +/// `RenderedFunction` representation. Implementations of this trait +/// consume those structures and emit format-specific output such as +/// D2, DOT, or other diagram formats. +/// +/// The trait intentionally separates graph structure from formatting. +/// Traversal decides *what* the graph contains while the builder +/// decides *how* it is rendered. pub trait GraphBuilder { type Output; @@ -59,8 +82,11 @@ pub trait GraphBuilder { fn finish(self) -> Self::Output; } -/// Format-agnostic MIR graph traversal. -/// Owns traversal order and graph semantics, delegates rendering to `GraphBuilder`. +/// Traverse the SMIR representation and produce rendered graph data. +/// +/// This function performs MIR traversal, resolves call targets, and +/// constructs `RenderedFunction` structures which are then passed to +/// the provided `GraphBuilder`. pub fn render_graph(smir: &SmirJson, mut builder: B) -> B::Output { let ctx = GraphContext::from_smir(smir); From abe0acd56ec816fcf6545bf8483524b327ac2b5b Mon Sep 17 00:00:00 2001 From: 0xh4ty <0xh4ty@proton.me> Date: Thu, 5 Mar 2026 12:55:52 +0530 Subject: [PATCH 16/23] d2: fix build issues after rebase and address clippy warnings Resolve compilation issues introduced after rebasing onto upstream master. Remove an unused import, update the SmirJson impl to match the new upstream definition without lifetime parameters, and implement Default for D2Builder to satisfy Clippy's new_without_default lint. Run cargo fmt to normalize formatting. --- src/mk_graph/output/d2.rs | 27 ++++++++++----------------- src/mk_graph/traverse.rs | 9 +++------ src/mk_graph/util.rs | 4 ++-- 3 files changed, 15 insertions(+), 25 deletions(-) diff --git a/src/mk_graph/output/d2.rs b/src/mk_graph/output/d2.rs index f3ab22a7..843a1223 100644 --- a/src/mk_graph/output/d2.rs +++ b/src/mk_graph/output/d2.rs @@ -1,7 +1,5 @@ //! D2 diagram format output for MIR graphs. -use crate::compat::stable_mir; - use crate::printer::SmirJson; use crate::mk_graph::util::escape_d2; @@ -23,6 +21,12 @@ impl D2Builder { } } +impl Default for D2Builder { + fn default() -> Self { + Self::new() + } +} + impl GraphBuilder for D2Builder { type Output = String; @@ -31,7 +35,6 @@ impl GraphBuilder for D2Builder { } fn alloc_legend(&mut self, lines: &[String]) { - self.buf.push_str("ALLOCS: {\n"); self.buf.push_str(" style.fill: \"#ffffcc\"\n"); self.buf.push_str(" style.stroke: \"#999999\"\n"); @@ -49,20 +52,17 @@ impl GraphBuilder for D2Builder { fn type_legend(&mut self, _lines: &[String]) {} fn external_function(&mut self, id: &str, name: &str) { - self.buf .push_str(&format!("{}: \"{}\"\n", id, escape_d2(name))); } fn render_function(&mut self, func: &RenderedFunction) { - self.buf.push_str(&format!("{}: {{\n", func.id)); self.buf .push_str(&format!(" label: \"{}\"\n", escape_d2(&func.display_name))); self.buf.push_str(" style.fill: \"#e0e0ff\"\n"); for block in &func.blocks { - let mut label = format!("bb{}:", block.idx); for stmt in &block.stmts { @@ -85,29 +85,23 @@ impl GraphBuilder for D2Builder { self.buf.push_str("}\n\n"); for edge in &func.call_edges { - self.buf.push_str(&format!( "{}: \"{}\"\n", edge.callee_id, escape_d2(&edge.callee_name) )); - self.buf.push_str(&format!( - "{}.style.fill: \"#ffe0e0\"\n", - edge.callee_id - )); + self.buf + .push_str(&format!("{}.style.fill: \"#ffe0e0\"\n", edge.callee_id)); self.buf.push_str(&format!( "{}.bb{} -> {}: call\n", - func.id, - edge.block_idx, - edge.callee_id + func.id, edge.block_idx, edge.callee_id )); } } fn static_item(&mut self, id: &str, name: &str) { - self.buf .push_str(&format!("{}: \"{}\" {{\n", id, escape_d2(name))); self.buf.push_str(" style.fill: \"#e0ffe0\"\n"); @@ -115,7 +109,6 @@ impl GraphBuilder for D2Builder { } fn asm_item(&mut self, id: &str, content: &str) { - let text = escape_d2(&content.lines().collect::()); self.buf.push_str(&format!("{}: \"{}\" {{\n", id, text)); @@ -132,7 +125,7 @@ impl GraphBuilder for D2Builder { // Public entry point // ============================================================================= -impl SmirJson<'_> { +impl SmirJson { /// Convert the MIR to D2 using GraphBuilder traversal pub fn to_d2_file(&self) -> String { render_graph(self, D2Builder::new()) diff --git a/src/mk_graph/traverse.rs b/src/mk_graph/traverse.rs index ac564385..7067bd22 100644 --- a/src/mk_graph/traverse.rs +++ b/src/mk_graph/traverse.rs @@ -8,7 +8,9 @@ use crate::printer::SmirJson; use crate::MonoItemKind; use crate::mk_graph::context::GraphContext; -use crate::mk_graph::util::{is_unqualified, name_lines, short_name, terminator_targets, hash_body}; +use crate::mk_graph::util::{ + hash_body, is_unqualified, name_lines, short_name, terminator_targets, +}; /// Represents a call from a block to another function. /// @@ -135,13 +137,11 @@ fn render_function<'a>( let mut locals = Vec::new(); if let Some(body) = body { - for (idx, decl) in body.local_decls() { locals.push((idx, ctx.render_type_with_layout(decl.ty))); } for (idx, block) in body.blocks.iter().enumerate() { - let stmts = block .statements .iter() @@ -164,11 +164,8 @@ fn render_function<'a>( }); if let TerminatorKind::Call { func, args, .. } = &block.terminator.kind { - if let Some(callee) = ctx.resolve_call_target(func) { - if is_unqualified(&callee) { - let rendered_args = args .iter() .map(|a| ctx.render_operand(a)) diff --git a/src/mk_graph/util.rs b/src/mk_graph/util.rs index db7a8efa..d7708c74 100644 --- a/src/mk_graph/util.rs +++ b/src/mk_graph/util.rs @@ -4,8 +4,8 @@ use std::hash::{DefaultHasher, Hash, Hasher}; use crate::compat::stable_mir; use stable_mir::mir::{ - AggregateKind, BorrowKind, ConstOperand, Mutability, NonDivergingIntrinsic, NullOp, Operand, - Place, ProjectionElem, Rvalue, Terminator, TerminatorKind, UnwindAction, Body, + AggregateKind, Body, BorrowKind, ConstOperand, Mutability, NonDivergingIntrinsic, NullOp, + Operand, Place, ProjectionElem, Rvalue, Terminator, TerminatorKind, UnwindAction, }; use stable_mir::ty::{IndexedVal, RigidTy}; From 6392101fde9ca4cb992bf164176134718376c93f Mon Sep 17 00:00:00 2001 From: 0xh4ty <0xh4ty@proton.me> Date: Thu, 5 Mar 2026 13:10:20 +0530 Subject: [PATCH 17/23] mk_graph: remove is_local and add raw_stmts to RenderedBlock Remove the unused is_local field from RenderedFunction since the local/external distinction is already represented structurally in the GraphBuilder API. Add raw_stmts to RenderedBlock as an escape hatch for renderers that need access to the underlying MIR statements in addition to the pre-rendered strings. --- src/mk_graph/traverse.rs | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/mk_graph/traverse.rs b/src/mk_graph/traverse.rs index 7067bd22..1d9bd9b1 100644 --- a/src/mk_graph/traverse.rs +++ b/src/mk_graph/traverse.rs @@ -2,7 +2,7 @@ //! //! This module owns the traversal order and graph semantics. extern crate stable_mir; -use stable_mir::mir::{Body, Terminator, TerminatorKind}; +use stable_mir::mir::{Body, Statement, Terminator, TerminatorKind}; use crate::printer::SmirJson; use crate::MonoItemKind; @@ -29,11 +29,12 @@ pub struct CallEdge { /// `GraphContext`. Builders are free to format or escape them according /// to their output format. /// -/// `raw_terminator` is provided as an escape hatch for renderers that -/// need to inspect the underlying MIR structure. +/// `raw_stmts` and `raw_terminator` are escape hatches for renderers +/// that need to inspect the underlying MIR structure. pub struct RenderedBlock<'a> { pub idx: usize, pub stmts: Vec, + pub raw_stmts: &'a [Statement], pub terminator: String, pub raw_terminator: &'a Terminator, pub cfg_edges: Vec<(usize, Option)>, @@ -48,7 +49,6 @@ pub struct RenderedBlock<'a> { pub struct RenderedFunction<'a> { pub id: String, pub display_name: String, - pub is_local: bool, pub locals: Vec<(usize, String)>, pub blocks: Vec>, pub call_edges: Vec, @@ -130,7 +130,6 @@ fn render_function<'a>( }; let display_name = name_lines(name); - let is_local = body.is_some(); let mut blocks = Vec::new(); let mut call_edges = Vec::new(); @@ -158,6 +157,7 @@ fn render_function<'a>( blocks.push(RenderedBlock { idx, stmts, + raw_stmts: &block.statements, terminator, raw_terminator: &block.terminator, cfg_edges, @@ -187,7 +187,6 @@ fn render_function<'a>( RenderedFunction { id, display_name, - is_local, locals, blocks, call_edges, From 4d383f0abbd6d2a81a2ff53c3fe48fa217c2405d Mon Sep 17 00:00:00 2001 From: 0xh4ty <0xh4ty@proton.me> Date: Mon, 23 Mar 2026 10:02:26 +0530 Subject: [PATCH 18/23] mk_graph: remove raw MIR fields and drop lifetimes from rendered structs Remove raw_stmts and raw_terminator from RenderedBlock since they are not required for rendering and unnecessarily expose MIR internals to the builder layer. Eliminate lifetime parameters from RenderedBlock and RenderedFunction by ensuring all data is fully owned. This simplifies the API, reduces coupling to MIR structures, and enforces a clean separation between traversal (data extraction) and rendering (string-based formatting). --- src/mk_graph/traverse.rs | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/src/mk_graph/traverse.rs b/src/mk_graph/traverse.rs index 1d9bd9b1..73918053 100644 --- a/src/mk_graph/traverse.rs +++ b/src/mk_graph/traverse.rs @@ -2,7 +2,7 @@ //! //! This module owns the traversal order and graph semantics. extern crate stable_mir; -use stable_mir::mir::{Body, Statement, Terminator, TerminatorKind}; +use stable_mir::mir::{Body, TerminatorKind}; use crate::printer::SmirJson; use crate::MonoItemKind; @@ -28,15 +28,10 @@ pub struct CallEdge { /// `stmts` and `terminator` are pre-rendered strings produced using /// `GraphContext`. Builders are free to format or escape them according /// to their output format. -/// -/// `raw_stmts` and `raw_terminator` are escape hatches for renderers -/// that need to inspect the underlying MIR structure. -pub struct RenderedBlock<'a> { +pub struct RenderedBlock { pub idx: usize, pub stmts: Vec, - pub raw_stmts: &'a [Statement], pub terminator: String, - pub raw_terminator: &'a Terminator, pub cfg_edges: Vec<(usize, Option)>, } @@ -46,11 +41,11 @@ pub struct RenderedBlock<'a> { /// terminators, and computes the control-flow edges. Builders receive /// this structure and are responsible only for formatting it into a /// specific graph representation. -pub struct RenderedFunction<'a> { +pub struct RenderedFunction { pub id: String, pub display_name: String, pub locals: Vec<(usize, String)>, - pub blocks: Vec>, + pub blocks: Vec, pub call_edges: Vec, } @@ -123,7 +118,7 @@ fn render_function<'a>( ctx: &GraphContext, name: &str, body: Option<&'a Body>, -) -> RenderedFunction<'a> { +) -> RenderedFunction { let id = match body { Some(b) => format!("fn_{}_{}", short_name(name), hash_body(b)), None => format!("fn_{}_no_body", short_name(name)), @@ -157,9 +152,7 @@ fn render_function<'a>( blocks.push(RenderedBlock { idx, stmts, - raw_stmts: &block.statements, terminator, - raw_terminator: &block.terminator, cfg_edges, }); From d99317e8cb8444a6062954674886cc34437b5364 Mon Sep 17 00:00:00 2001 From: 0xh4ty <0xh4ty@proton.me> Date: Mon, 23 Mar 2026 17:38:11 +0530 Subject: [PATCH 19/23] mk_graph: fix call edge collection and compute external functions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fix call edge collection during traversal to reliably capture all callees with their ids and names. Collect defined functions from items and called functions from call edges, then compute external functions as (called ∖ defined). Invoke `builder.external_function` for each external symbol, matching DOT behavior and ensuring traversal owns global call graph semantics instead of individual renderers. --- src/mk_graph/traverse.rs | 103 ++++++++++++++++++++++++++++++++++++--- 1 file changed, 97 insertions(+), 6 deletions(-) diff --git a/src/mk_graph/traverse.rs b/src/mk_graph/traverse.rs index 73918053..49dc88d4 100644 --- a/src/mk_graph/traverse.rs +++ b/src/mk_graph/traverse.rs @@ -2,16 +2,18 @@ //! //! This module owns the traversal order and graph semantics. extern crate stable_mir; -use stable_mir::mir::{Body, TerminatorKind}; +use stable_mir::mir::{Body, TerminatorKind, UnwindAction}; use crate::printer::SmirJson; use crate::MonoItemKind; use crate::mk_graph::context::GraphContext; use crate::mk_graph::util::{ - hash_body, is_unqualified, name_lines, short_name, terminator_targets, + hash_body, is_unqualified, name_lines, short_name, GraphLabelString, }; +use std::collections::{HashMap, HashSet}; + /// Represents a call from a block to another function. /// /// The callee is resolved during traversal and arguments are already @@ -92,10 +94,27 @@ pub fn render_graph(smir: &SmirJson, mut builder: B) -> B::Outp builder.alloc_legend(&ctx.allocs_legend_lines()); builder.type_legend(&ctx.types_legend_lines()); + let mut defined: HashSet = HashSet::new(); + for item in &smir.items { + if let MonoItemKind::MonoItemFn { name, .. } = &item.mono_item_kind { + defined.insert(short_name(name)); + } + } + + let mut called: HashMap = HashMap::new(); + for item in &smir.items { match &item.mono_item_kind { MonoItemKind::MonoItemFn { name, body, .. } => { let func = render_function(&ctx, name, body.as_ref()); + + // collect callees + for edge in &func.call_edges { + called + .entry(edge.callee_id.clone()) + .or_insert(edge.callee_name.clone()); + } + builder.render_function(&func); } MonoItemKind::MonoItemStatic { name, .. } => { @@ -109,6 +128,12 @@ pub fn render_graph(smir: &SmirJson, mut builder: B) -> B::Outp } } + for (id, name) in called { + if !defined.contains(&id) { + builder.external_function(&id, &name); + } + } + builder.finish() } @@ -144,10 +169,76 @@ fn render_function<'a>( let terminator = ctx.render_terminator(&block.terminator); - let cfg_edges = terminator_targets(&block.terminator) - .into_iter() - .map(|t| (t, None)) - .collect(); + let mut cfg_edges = Vec::new(); + + match &block.terminator.kind { + TerminatorKind::Goto { target } => { + cfg_edges.push((*target, None)); + } + + TerminatorKind::SwitchInt { targets, .. } => { + for (value, target) in targets.branches() { + cfg_edges.push((target, Some(value.to_string()))); + } + cfg_edges.push((targets.otherwise(), Some("other".into()))); + } + + TerminatorKind::Return + | TerminatorKind::Abort + | TerminatorKind::Resume + | TerminatorKind::Unreachable => { + // no outgoing edges + } + + TerminatorKind::Drop { target, unwind, .. } => { + cfg_edges.push((*target, None)); + + if let UnwindAction::Cleanup(t) = unwind { + cfg_edges.push((*t, Some("cleanup".into()))); + } + } + + TerminatorKind::Call { + destination, + target, + unwind, + .. + } => { + if let Some(t) = target { + cfg_edges.push((*t, Some(destination.label()))); + } + + if let UnwindAction::Cleanup(t) = unwind { + cfg_edges.push((*t, Some("cleanup".into()))); + } + } + + TerminatorKind::Assert { + target, + unwind, + .. + } => { + cfg_edges.push((*target, None)); + + if let UnwindAction::Cleanup(t) = unwind { + cfg_edges.push((*t, Some("cleanup".into()))); + } + } + + TerminatorKind::InlineAsm { + destination, + unwind, + .. + } => { + if let Some(t) = destination { + cfg_edges.push((*t, None)); + } + + if let UnwindAction::Cleanup(t) = unwind { + cfg_edges.push((*t, Some("cleanup".into()))); + } + } + } blocks.push(RenderedBlock { idx, From da9eabcaef0d40a73f21dd9a24aa7b6431796f96 Mon Sep 17 00:00:00 2001 From: 0xh4ty <0xh4ty@proton.me> Date: Mon, 23 Mar 2026 18:02:38 +0530 Subject: [PATCH 20/23] mk_graph: add new DOT backend behind env gate with GraphBuilder stubs Introduce a new DOT implementation wired through the GraphBuilder abstraction and gate it behind `SMIR_DOT_NEW`. Add `to_dot_file_new` entry point and a `DOTBuilder` with stubbed trait method implementations to establish the integration path. This enables incremental migration to the new traversal-based architecture while preserving the existing DOT backend as default. --- src/mk_graph/mod.rs | 8 +++++- src/mk_graph/output/dot.rs | 56 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 63 insertions(+), 1 deletion(-) diff --git a/src/mk_graph/mod.rs b/src/mk_graph/mod.rs index e93c4d00..d0e6059f 100644 --- a/src/mk_graph/mod.rs +++ b/src/mk_graph/mod.rs @@ -28,7 +28,13 @@ pub use util::GraphLabelString; /// Entry point to write the DOT file pub fn emit_dotfile(tcx: TyCtxt<'_>) { - let smir_dot = collect_smir(tcx).to_dot_file(); + let smir = collect_smir(tcx); + + let smir_dot = if std::env::var("SMIR_DOT_NEW").is_ok() { + smir.to_dot_file_new() + } else { + smir.to_dot_file() + }; match mir_output_path(tcx, "smir.dot") { OutputDest::Stdout => { diff --git a/src/mk_graph/output/dot.rs b/src/mk_graph/output/dot.rs index 78f03478..5554ddb6 100644 --- a/src/mk_graph/output/dot.rs +++ b/src/mk_graph/output/dot.rs @@ -13,6 +13,9 @@ use crate::MonoItemKind; use crate::mk_graph::context::GraphContext; use crate::mk_graph::util::{block_name, is_unqualified, name_lines, short_name, GraphLabelString}; +use crate::mk_graph::traverse::render_graph; +use crate::mk_graph::traverse::{GraphBuilder, RenderedFunction}; + impl SmirJson { /// Convert the MIR to DOT (Graphviz) format pub fn to_dot_file(self) -> String { @@ -306,3 +309,56 @@ impl SmirJson { String::from_utf8(bytes).expect("Error converting dot file") } } + +// ============================================================================= +// DOT Builder +// ============================================================================= + +pub struct DOTBuilder { + buf: String, +} + +impl DOTBuilder { + pub fn new() -> Self { + Self { buf: String::new() } + } +} + +impl Default for DOTBuilder { + fn default() -> Self { + Self::new() + } +} + +impl GraphBuilder for DOTBuilder { + type Output = String; + + fn begin_graph(&mut self, _name: &str) {} + + fn alloc_legend(&mut self, _lines: &[String]) {} + + fn type_legend(&mut self, _lines: &[String]) {} + + fn external_function(&mut self, _id: &str, _name: &str) {} + + fn render_function(&mut self, _func: &RenderedFunction) {} + + fn static_item(&mut self, _id: &str, _name: &str) {} + + fn asm_item(&mut self, _id: &str, _content: &str) {} + + fn finish(self) -> Self::Output { + self.buf + } +} + +// ============================================================================= +// Public entry point (new) +// ============================================================================= + +impl SmirJson { + /// Convert the MIR to DOT using GraphBuilder traversal + pub fn to_dot_file_new(&self) -> String { + render_graph(self, DOTBuilder::new()) + } +} From bbb45022568005154d9b00d3ec549f7f9caf6509 Mon Sep 17 00:00:00 2001 From: 0xh4ty <0xh4ty@proton.me> Date: Mon, 4 May 2026 10:50:05 +0530 Subject: [PATCH 21/23] 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. --- src/mk_graph/output/d2.rs | 12 +-- src/mk_graph/output/dot.rs | 174 ++++++++++++++++++++++++++++++++++--- src/mk_graph/traverse.rs | 84 ++++++++---------- src/mk_graph/util.rs | 7 ++ 4 files changed, 212 insertions(+), 65 deletions(-) diff --git a/src/mk_graph/output/d2.rs b/src/mk_graph/output/d2.rs index 843a1223..c6b8f9e8 100644 --- a/src/mk_graph/output/d2.rs +++ b/src/mk_graph/output/d2.rs @@ -2,7 +2,7 @@ use crate::printer::SmirJson; -use crate::mk_graph::util::escape_d2; +use crate::mk_graph::util::{escape_d2, short_name}; use crate::mk_graph::traverse::render_graph; use crate::mk_graph::traverse::{GraphBuilder, RenderedFunction}; @@ -51,7 +51,8 @@ impl GraphBuilder for D2Builder { fn type_legend(&mut self, _lines: &[String]) {} - fn external_function(&mut self, id: &str, name: &str) { + fn external_function(&mut self, name: &str) { + let id = short_name(name); self.buf .push_str(&format!("{}: \"{}\"\n", id, escape_d2(name))); } @@ -85,18 +86,19 @@ impl GraphBuilder for D2Builder { self.buf.push_str("}\n\n"); for edge in &func.call_edges { + let callee_id = short_name(&edge.callee_name); self.buf.push_str(&format!( "{}: \"{}\"\n", - edge.callee_id, + callee_id, escape_d2(&edge.callee_name) )); self.buf - .push_str(&format!("{}.style.fill: \"#ffe0e0\"\n", edge.callee_id)); + .push_str(&format!("{}.style.fill: \"#ffe0e0\"\n", callee_id)); self.buf.push_str(&format!( "{}.bb{} -> {}: call\n", - func.id, edge.block_idx, edge.callee_id + func.id, edge.block_idx, callee_id )); } } diff --git a/src/mk_graph/output/dot.rs b/src/mk_graph/output/dot.rs index 5554ddb6..c9870e6c 100644 --- a/src/mk_graph/output/dot.rs +++ b/src/mk_graph/output/dot.rs @@ -11,7 +11,9 @@ use crate::printer::SmirJson; use crate::MonoItemKind; use crate::mk_graph::context::GraphContext; -use crate::mk_graph::util::{block_name, is_unqualified, name_lines, short_name, GraphLabelString}; +use crate::mk_graph::util::{ + block_name, escape_dot, is_unqualified, name_lines, short_name, GraphLabelString, +}; use crate::mk_graph::traverse::render_graph; use crate::mk_graph::traverse::{GraphBuilder, RenderedFunction}; @@ -311,16 +313,27 @@ impl SmirJson { } // ============================================================================= -// DOT Builder +// DOT Builder (new) // ============================================================================= pub struct DOTBuilder { buf: String, + /// Cross-cluster call edges deferred until finish(): + /// (from_node_id, to_node_id, rendered_args) + deferred_call_edges: Vec<(String, String, String)>, } impl DOTBuilder { pub fn new() -> Self { - Self { buf: String::new() } + Self { + buf: String::new(), + deferred_call_edges: Vec::new(), + } + } + + fn line(&mut self, s: &str) { + self.buf.push_str(s); + self.buf.push('\n'); } } @@ -333,27 +346,164 @@ impl Default for DOTBuilder { impl GraphBuilder for DOTBuilder { type Output = String; - fn begin_graph(&mut self, _name: &str) {} + fn begin_graph(&mut self, name: &str) { + self.line("digraph {"); + self.line(&format!(" label=\"{}\";", escape_dot(name))); + self.line(" node [shape=rectangle];"); + } + + fn alloc_legend(&mut self, lines: &[String]) { + if lines.is_empty() { + return; + } + let mut all_lines = lines.to_vec(); + all_lines.push(String::new()); + let label = all_lines + .iter() + .map(|l| escape_dot(l)) + .collect::>() + .join("\\l"); + self.line(&format!( + " node_allocs [label=\"{}\", style=\"filled\", color=lightyellow];", + label + )); + } + + fn type_legend(&mut self, lines: &[String]) { + // Only emit when there are entries beyond the header line. + if lines.len() <= 1 { + return; + } + let mut all_lines = lines.to_vec(); + all_lines.push(String::new()); + let label = all_lines + .iter() + .map(|l| escape_dot(l)) + .collect::>() + .join("\\l"); + self.line(&format!( + " node_types [label=\"{}\", style=\"filled\", color=lavender];", + label + )); + } + + /// Emit a red external node for a callee that has no defined body. + /// `name` is the full resolved symbol name; block_name(name, 0) gives + /// the node ID, matching the target IDs used in deferred call edges. + fn external_function(&mut self, name: &str) { + let node_id = block_name(name, 0); + let label = escape_dot(&name_lines(name)); + self.line(&format!(" {} [label=\"{}\", color=red];", node_id, label)); + } - fn alloc_legend(&mut self, _lines: &[String]) {} + fn render_function(&mut self, func: &RenderedFunction) { + let cluster_color = if func.is_unqualified { + "palegreen" + } else { + "lightgray" + }; + + self.line(&format!( + " subgraph cluster_{} {{", + short_name(&func.symbol_name) + )); + self.line(&format!( + " label=\"{}\";", + escape_dot(&func.display_name) + )); + self.line(" style=\"filled\";"); + self.line(&format!(" color={};", cluster_color)); + + // LOCALS node + { + let mut lines = vec!["LOCALS".to_string()]; + for (idx, ty) in &func.locals { + lines.push(format!("{} = {}", idx, ty)); + } + lines.push(String::new()); + let label = lines + .iter() + .map(|l| escape_dot(l)) + .collect::>() + .join("\\l"); + self.line(&format!( + " {}_locals [label=\"{}\", style=\"filled\", color=palegreen3];", + short_name(&func.symbol_name), + label + )); + } - fn type_legend(&mut self, _lines: &[String]) {} + // Block nodes + for block in &func.blocks { + let node_id = block_name(&func.symbol_name, block.idx); + let mut parts: Vec = block.stmts.clone(); + parts.push(block.terminator.clone()); + parts.push(String::new()); + let label = parts + .iter() + .map(|l| escape_dot(l)) + .collect::>() + .join("\\l"); + self.line(&format!(" {} [label=\"{}\"];", node_id, label)); + } - fn external_function(&mut self, _id: &str, _name: &str) {} + // Intra-cluster CFG edges + for block in &func.blocks { + let from = block_name(&func.symbol_name, block.idx); + for (target, label_opt) in &block.cfg_edges { + let to = block_name(&func.symbol_name, *target); + match label_opt { + Some(lbl) => self.line(&format!( + " {} -> {} [label=\"{}\"];", + from, + to, + escape_dot(lbl) + )), + None => self.line(&format!(" {} -> {};", from, to)), + } + } + } - fn render_function(&mut self, _func: &RenderedFunction) {} + self.line(" }"); + + // Defer cross-cluster call edges - emitted after all clusters in finish(). + // callee_name is the full symbol name so block_name(callee_name, 0) + // matches the node ID emitted by external_function or by another + // cluster's block 0. + for edge in &func.call_edges { + let from = block_name(&func.symbol_name, edge.block_idx); + let to = block_name(&edge.callee_name, 0); + self.deferred_call_edges + .push((from, to, edge.rendered_args.clone())); + } + } - fn static_item(&mut self, _id: &str, _name: &str) {} + fn static_item(&mut self, id: &str, name: &str) { + self.line(&format!(" {} [label=\"{}\"];", id, escape_dot(name))); + } - fn asm_item(&mut self, _id: &str, _content: &str) {} + fn asm_item(&mut self, id: &str, content: &str) { + let text = content.lines().collect::(); + self.line(&format!(" {} [label=\"{}\"];", id, escape_dot(&text))); + } - fn finish(self) -> Self::Output { + fn finish(mut self) -> String { + // Emit all cross-cluster call edges after all subgraph clusters. + for (from, to, args) in &self.deferred_call_edges { + self.buf.push_str(&format!( + " {} -> {} [label=\"{}\"];\n", + from, + to, + escape_dot(args) + )); + } + self.buf.push_str("}\n"); self.buf } } // ============================================================================= -// Public entry point (new) +// Public entry points (new) // ============================================================================= impl SmirJson { diff --git a/src/mk_graph/traverse.rs b/src/mk_graph/traverse.rs index 49dc88d4..a6c99a35 100644 --- a/src/mk_graph/traverse.rs +++ b/src/mk_graph/traverse.rs @@ -8,9 +8,7 @@ use crate::printer::SmirJson; use crate::MonoItemKind; use crate::mk_graph::context::GraphContext; -use crate::mk_graph::util::{ - hash_body, is_unqualified, name_lines, short_name, GraphLabelString, -}; +use crate::mk_graph::util::{hash_body, is_unqualified, name_lines, short_name, GraphLabelString}; use std::collections::{HashMap, HashSet}; @@ -20,7 +18,6 @@ use std::collections::{HashMap, HashSet}; /// rendered as a string. Builders may choose how to visualize this edge. pub struct CallEdge { pub block_idx: usize, - pub callee_id: String, pub callee_name: String, pub rendered_args: String, } @@ -45,6 +42,8 @@ pub struct RenderedBlock { /// specific graph representation. pub struct RenderedFunction { pub id: String, + pub symbol_name: String, + pub is_unqualified: bool, pub display_name: String, pub locals: Vec<(usize, String)>, pub blocks: Vec, @@ -70,7 +69,7 @@ pub trait GraphBuilder { fn type_legend(&mut self, lines: &[String]); - fn external_function(&mut self, id: &str, name: &str); + fn external_function(&mut self, name: &str); fn render_function(&mut self, func: &RenderedFunction); @@ -90,47 +89,43 @@ pub fn render_graph(smir: &SmirJson, mut builder: B) -> B::Outp let ctx = GraphContext::from_smir(smir); builder.begin_graph(&smir.name); - builder.alloc_legend(&ctx.allocs_legend_lines()); builder.type_legend(&ctx.types_legend_lines()); - let mut defined: HashSet = HashSet::new(); - for item in &smir.items { - if let MonoItemKind::MonoItemFn { name, .. } = &item.mono_item_kind { - defined.insert(short_name(name)); - } - } + // Full symbol names of all defined mono items. Used to suppress + // external_function calls for callees that have a defined body. + let defined_symbol_names: HashSet = + smir.items.iter().map(|i| i.symbol_name.clone()).collect(); + // Accumulates all reachable callees: full symbol name -> full symbol name. let mut called: HashMap = HashMap::new(); for item in &smir.items { match &item.mono_item_kind { MonoItemKind::MonoItemFn { name, body, .. } => { - let func = render_function(&ctx, name, body.as_ref()); + let func = render_function(&ctx, name, &item.symbol_name, body.as_ref()); - // collect callees for edge in &func.call_edges { called - .entry(edge.callee_id.clone()) + .entry(edge.callee_name.clone()) .or_insert(edge.callee_name.clone()); } builder.render_function(&func); } MonoItemKind::MonoItemStatic { name, .. } => { - let id = short_name(name); - builder.static_item(&id, name); + builder.static_item(&short_name(name), name); } MonoItemKind::MonoItemGlobalAsm { asm } => { - let id = short_name(asm); - builder.asm_item(&id, asm); + builder.asm_item(&short_name(asm), asm); } } } - for (id, name) in called { - if !defined.contains(&id) { - builder.external_function(&id, &name); + // Emit external nodes only for callees with no defined body. + for (name, _) in called { + if !defined_symbol_names.contains(&name) { + builder.external_function(&name); } } @@ -142,6 +137,7 @@ pub fn render_graph(smir: &SmirJson, mut builder: B) -> B::Outp fn render_function<'a>( ctx: &GraphContext, name: &str, + symbol_name: &str, body: Option<&'a Body>, ) -> RenderedFunction { let id = match body { @@ -150,6 +146,7 @@ fn render_function<'a>( }; let display_name = name_lines(name); + let unqualified = is_unqualified(name); let mut blocks = Vec::new(); let mut call_edges = Vec::new(); @@ -186,13 +183,10 @@ fn render_function<'a>( TerminatorKind::Return | TerminatorKind::Abort | TerminatorKind::Resume - | TerminatorKind::Unreachable => { - // no outgoing edges - } + | TerminatorKind::Unreachable => {} TerminatorKind::Drop { target, unwind, .. } => { cfg_edges.push((*target, None)); - if let UnwindAction::Cleanup(t) = unwind { cfg_edges.push((*t, Some("cleanup".into()))); } @@ -207,19 +201,13 @@ fn render_function<'a>( if let Some(t) = target { cfg_edges.push((*t, Some(destination.label()))); } - if let UnwindAction::Cleanup(t) = unwind { cfg_edges.push((*t, Some("cleanup".into()))); } } - TerminatorKind::Assert { - target, - unwind, - .. - } => { + TerminatorKind::Assert { target, unwind, .. } => { cfg_edges.push((*target, None)); - if let UnwindAction::Cleanup(t) = unwind { cfg_edges.push((*t, Some("cleanup".into()))); } @@ -233,7 +221,6 @@ fn render_function<'a>( if let Some(t) = destination { cfg_edges.push((*t, None)); } - if let UnwindAction::Cleanup(t) = unwind { cfg_edges.push((*t, Some("cleanup".into()))); } @@ -247,22 +234,21 @@ fn render_function<'a>( cfg_edges, }); + // Collect call edges for all resolvable call targets. + // No is_unqualified guard here if let TerminatorKind::Call { func, args, .. } = &block.terminator.kind { if let Some(callee) = ctx.resolve_call_target(func) { - if is_unqualified(&callee) { - let rendered_args = args - .iter() - .map(|a| ctx.render_operand(a)) - .collect::>() - .join(", "); - - call_edges.push(CallEdge { - block_idx: idx, - callee_id: short_name(&callee), - callee_name: callee, - rendered_args, - }); - } + let rendered_args = args + .iter() + .map(|a| ctx.render_operand(a)) + .collect::>() + .join(", "); + + call_edges.push(CallEdge { + block_idx: idx, + callee_name: callee, + rendered_args, + }); } } } @@ -270,6 +256,8 @@ fn render_function<'a>( RenderedFunction { id, + symbol_name: symbol_name.to_string(), + is_unqualified: unqualified, display_name, locals, blocks, diff --git a/src/mk_graph/util.rs b/src/mk_graph/util.rs index d7708c74..485e32c3 100644 --- a/src/mk_graph/util.rs +++ b/src/mk_graph/util.rs @@ -213,6 +213,13 @@ pub fn escape_d2(s: &str) -> String { .replace('$', "\\$") } +/// Escape special characters for DOT string labels +pub fn escape_dot(s: &str) -> String { + s.replace('\\', "\\\\") + .replace('"', "\\\"") + .replace('\n', "\\n") +} + // ============================================================================= // Byte Helpers // ============================================================================= From 633d48d5c18ffd2a7d15acce820d524c821f42aa Mon Sep 17 00:00:00 2001 From: 0xh4ty <0xh4ty@proton.me> Date: Fri, 8 May 2026 02:07:18 +0530 Subject: [PATCH 22/23] mk_graph: elide unnecessary lifetime in render_function Remove the explicit lifetime parameter from `render_function` after rendered graph structures became fully owned and no longer borrow MIR data. This addresses Clippy's `needless_lifetimes` warning and simplifies the traversal API. --- src/mk_graph/traverse.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/mk_graph/traverse.rs b/src/mk_graph/traverse.rs index a6c99a35..1892a4f2 100644 --- a/src/mk_graph/traverse.rs +++ b/src/mk_graph/traverse.rs @@ -134,11 +134,11 @@ pub fn render_graph(smir: &SmirJson, mut builder: B) -> B::Outp /// Emit graph events for a single function body. /// Traverses blocks, CFG edges, and call edges without renderer-specific logic. -fn render_function<'a>( +fn render_function( ctx: &GraphContext, name: &str, symbol_name: &str, - body: Option<&'a Body>, + body: Option<&Body>, ) -> RenderedFunction { let id = match body { Some(b) => format!("fn_{}_{}", short_name(name), hash_body(b)), From 3f9d36e462e0844177b1699d99a420daa4af77ba Mon Sep 17 00:00:00 2001 From: 0xh4ty <0xh4ty@proton.me> Date: Fri, 8 May 2026 05:11:40 +0530 Subject: [PATCH 23/23] mk_graph: centralize graph identity resolution in traversal Move function and call target ID resolution fully into the traversal layer instead of reconstructing IDs inside renderers. Add a global `function_ids` map in `render_graph` and extend `CallEdge` to carry resolved `callee_id` values for both internal and external call targets. Update `GraphBuilder::external_function` to accept an explicit `id: &str` parameter so traversal owns external node identity consistently across renderers. Simplify `block_name` into pure ID composition and update the DOT backend to consume traversal-provided IDs consistently for nodes and edges. This fixes mismatches between internal and external call edges and further strengthens the separation between traversal semantics and renderer formatting. --- src/mk_graph/output/d2.rs | 3 +-- src/mk_graph/output/dot.rs | 22 ++++++++------------ src/mk_graph/traverse.rs | 42 +++++++++++++++++++++++++++++++------- src/mk_graph/util.rs | 6 ++---- 4 files changed, 47 insertions(+), 26 deletions(-) diff --git a/src/mk_graph/output/d2.rs b/src/mk_graph/output/d2.rs index c6b8f9e8..d93b36f0 100644 --- a/src/mk_graph/output/d2.rs +++ b/src/mk_graph/output/d2.rs @@ -51,8 +51,7 @@ impl GraphBuilder for D2Builder { fn type_legend(&mut self, _lines: &[String]) {} - fn external_function(&mut self, name: &str) { - let id = short_name(name); + fn external_function(&mut self, id: &str, name: &str) { self.buf .push_str(&format!("{}: \"{}\"\n", id, escape_d2(name))); } diff --git a/src/mk_graph/output/dot.rs b/src/mk_graph/output/dot.rs index c9870e6c..fa380a76 100644 --- a/src/mk_graph/output/dot.rs +++ b/src/mk_graph/output/dot.rs @@ -390,8 +390,8 @@ impl GraphBuilder for DOTBuilder { /// Emit a red external node for a callee that has no defined body. /// `name` is the full resolved symbol name; block_name(name, 0) gives /// the node ID, matching the target IDs used in deferred call edges. - fn external_function(&mut self, name: &str) { - let node_id = block_name(name, 0); + fn external_function(&mut self, id: &str, name: &str) { + let node_id = block_name(id, 0); let label = escape_dot(&name_lines(name)); self.line(&format!(" {} [label=\"{}\", color=red];", node_id, label)); } @@ -403,10 +403,7 @@ impl GraphBuilder for DOTBuilder { "lightgray" }; - self.line(&format!( - " subgraph cluster_{} {{", - short_name(&func.symbol_name) - )); + self.line(&format!(" subgraph cluster_{} {{", &func.id)); self.line(&format!( " label=\"{}\";", escape_dot(&func.display_name) @@ -428,14 +425,13 @@ impl GraphBuilder for DOTBuilder { .join("\\l"); self.line(&format!( " {}_locals [label=\"{}\", style=\"filled\", color=palegreen3];", - short_name(&func.symbol_name), - label + &func.id, label )); } // Block nodes for block in &func.blocks { - let node_id = block_name(&func.symbol_name, block.idx); + let node_id = block_name(&func.id, block.idx); let mut parts: Vec = block.stmts.clone(); parts.push(block.terminator.clone()); parts.push(String::new()); @@ -449,9 +445,9 @@ impl GraphBuilder for DOTBuilder { // Intra-cluster CFG edges for block in &func.blocks { - let from = block_name(&func.symbol_name, block.idx); + let from = block_name(&func.id, block.idx); for (target, label_opt) in &block.cfg_edges { - let to = block_name(&func.symbol_name, *target); + let to = block_name(&func.id, *target); match label_opt { Some(lbl) => self.line(&format!( " {} -> {} [label=\"{}\"];", @@ -471,8 +467,8 @@ impl GraphBuilder for DOTBuilder { // matches the node ID emitted by external_function or by another // cluster's block 0. for edge in &func.call_edges { - let from = block_name(&func.symbol_name, edge.block_idx); - let to = block_name(&edge.callee_name, 0); + let from = block_name(&func.id, edge.block_idx); + let to = block_name(&edge.callee_id, 0); self.deferred_call_edges .push((from, to, edge.rendered_args.clone())); } diff --git a/src/mk_graph/traverse.rs b/src/mk_graph/traverse.rs index 1892a4f2..2b866ce1 100644 --- a/src/mk_graph/traverse.rs +++ b/src/mk_graph/traverse.rs @@ -18,6 +18,7 @@ use std::collections::{HashMap, HashSet}; /// rendered as a string. Builders may choose how to visualize this edge. pub struct CallEdge { pub block_idx: usize, + pub callee_id: String, pub callee_name: String, pub rendered_args: String, } @@ -69,7 +70,7 @@ pub trait GraphBuilder { fn type_legend(&mut self, lines: &[String]); - fn external_function(&mut self, name: &str); + fn external_function(&mut self, id: &str, name: &str); fn render_function(&mut self, func: &RenderedFunction); @@ -97,13 +98,32 @@ pub fn render_graph(smir: &SmirJson, mut builder: B) -> B::Outp let defined_symbol_names: HashSet = smir.items.iter().map(|i| i.symbol_name.clone()).collect(); + // Full symbol name -> graph function ID. + let mut function_ids: HashMap = HashMap::new(); + + for item in &smir.items { + if let MonoItemKind::MonoItemFn { name, body, .. } = &item.mono_item_kind { + let id = match body { + Some(body) => { + format!("fn_{}_{}", short_name(name), hash_body(body)) + } + None => { + format!("fn_{}_no_body", short_name(name)) + } + }; + + function_ids.insert(item.symbol_name.clone(), id); + } + } + // Accumulates all reachable callees: full symbol name -> full symbol name. let mut called: HashMap = HashMap::new(); for item in &smir.items { match &item.mono_item_kind { MonoItemKind::MonoItemFn { name, body, .. } => { - let func = render_function(&ctx, name, &item.symbol_name, body.as_ref()); + let func = + render_function(&ctx, name, &item.symbol_name, body.as_ref(), &function_ids); for edge in &func.call_edges { called @@ -125,7 +145,8 @@ pub fn render_graph(smir: &SmirJson, mut builder: B) -> B::Outp // Emit external nodes only for callees with no defined body. for (name, _) in called { if !defined_symbol_names.contains(&name) { - builder.external_function(&name); + let id = format!("fn_{}_external", short_name(&name)); + builder.external_function(&id, &name); } } @@ -139,11 +160,12 @@ fn render_function( name: &str, symbol_name: &str, body: Option<&Body>, + function_ids: &HashMap, ) -> RenderedFunction { - let id = match body { - Some(b) => format!("fn_{}_{}", short_name(name), hash_body(b)), - None => format!("fn_{}_no_body", short_name(name)), - }; + let id = function_ids + .get(symbol_name) + .expect("missing function id") + .clone(); let display_name = name_lines(name); let unqualified = is_unqualified(name); @@ -244,8 +266,14 @@ fn render_function( .collect::>() .join(", "); + let callee_id = function_ids + .get(&callee) + .cloned() + .unwrap_or_else(|| format!("fn_{}_external", short_name(&callee))); + call_edges.push(CallEdge { block_idx: idx, + callee_id, callee_name: callee, rendered_args, }); diff --git a/src/mk_graph/util.rs b/src/mk_graph/util.rs index 485e32c3..effdd932 100644 --- a/src/mk_graph/util.rs +++ b/src/mk_graph/util.rs @@ -196,10 +196,8 @@ pub fn short_name(function_name: &str) -> String { } /// Generate a consistent block name within a function -pub fn block_name(function_name: &str, id: usize) -> String { - let mut h = DefaultHasher::new(); - function_name.hash(&mut h); - format!("X{:x}_{}", h.finish(), id) +pub fn block_name(function_id: &str, block_idx: usize) -> String { + format!("{}_{}", function_id, block_idx) } // =============================================================================