From 042a209ed2a2367b00aa6f6a863b5ab31ef600f5 Mon Sep 17 00:00:00 2001 From: cds-amal Date: Sun, 30 Nov 2025 12:56:38 -0500 Subject: [PATCH 1/5] Add GlobalAlloc mapping and enhanced constant display to dot graphs Restructure the graph generation to build indices upfront rather than resolving relationships at traversal time. This improves maintainability and enables richer constant/allocation information in the output. Add new data structures for graph context: - AllocIndex: maps AllocId to processed AllocEntry with descriptions - AllocEntry: contains alloc metadata and human-readable description - AllocKind: categorizes allocs as Memory/Static/VTable/Function - TypeIndex: maps type IDs to display names - GraphContext: holds all indices and provides rendering methods Add ALLOCS legend node to graphs: - Display all GlobalAlloc entries in a yellow info node - Show allocation ID, type, and decoded value where possible - Decode string literals as escaped ASCII - Decode small integers as numeric values Enhance constant operand labels: - Show provenance references: `const [alloc0: Int(I32) = 42]` - Display inline constant values with types: `const 42_Uint(Usize)` - Show function names for ZeroSized function constants Add context-aware rendering functions: - render_stmt_ctx: renders statements with alloc context - render_rvalue_ctx: renders rvalues with operand context - render_intrinsic_ctx: renders intrinsics with operand context Update to_dot_file to use new architecture: - Build GraphContext before consuming SmirJson - Pass context to statement/operand rendering - Use context for call edge argument labels Add accessor methods to AllocInfo in printer.rs: - alloc_id(): returns the allocation ID - ty(): returns the type - global_alloc(): returns reference to GlobalAlloc --- src/mk_graph.rs | 398 +++++++++++++++++++++++++++++++++++++++++++++--- src/printer.rs | 14 ++ 2 files changed, 395 insertions(+), 17 deletions(-) diff --git a/src/mk_graph.rs b/src/mk_graph.rs index 3b18cf5..eced371 100644 --- a/src/mk_graph.rs +++ b/src/mk_graph.rs @@ -14,7 +14,9 @@ extern crate stable_mir; use rustc_session::config::{OutFileName, OutputType}; extern crate rustc_session; -use stable_mir::ty::{IndexedVal, Ty}; +use stable_mir::mir::alloc::GlobalAlloc; +use stable_mir::ty::{ConstantKind, IndexedVal, MirConst, Ty}; +use stable_mir::CrateDef; use stable_mir::{ mir::{ AggregateKind, BasicBlock, BorrowKind, ConstOperand, Mutability, NonDivergingIntrinsic, @@ -25,10 +27,301 @@ use stable_mir::{ }; use crate::{ - printer::{collect_smir, FnSymType, SmirJson}, + printer::{collect_smir, AllocInfo, FnSymType, SmirJson, TypeMetadata}, MonoItemKind, }; +// ============================================================================= +// Graph Index Structures +// ============================================================================= + +/// Index for looking up allocation information by AllocId +pub struct AllocIndex { + pub by_id: HashMap, +} + +/// Processed allocation entry with human-readable description +pub struct AllocEntry { + pub alloc_id: u64, + pub ty: Ty, + pub kind: AllocKind, + pub description: String, +} + +/// Simplified allocation kind for display +pub enum AllocKind { + Memory { bytes_len: usize, is_str: bool }, + Static { name: String }, + VTable { ty_desc: String }, + Function { name: String }, +} + +/// Index for looking up type information +pub struct TypeIndex { + by_id: HashMap, +} + +/// Context for rendering graph labels with access to indices +pub struct GraphContext { + pub allocs: AllocIndex, + pub types: TypeIndex, + pub functions: HashMap, +} + +// ============================================================================= +// Index Implementation +// ============================================================================= + +impl AllocIndex { + pub fn new() -> Self { + Self { + by_id: HashMap::new(), + } + } + + pub fn from_alloc_infos(allocs: &[AllocInfo], type_index: &TypeIndex) -> Self { + let mut index = Self::new(); + for info in allocs { + let entry = AllocEntry::from_alloc_info(info, type_index); + index.by_id.insert(entry.alloc_id, entry); + } + index + } + + pub fn get(&self, id: u64) -> Option<&AllocEntry> { + self.by_id.get(&id) + } + + pub fn iter(&self) -> impl Iterator { + self.by_id.values() + } + + /// Describe an alloc by its ID for use in labels + pub fn describe(&self, id: u64) -> String { + match self.get(id) { + Some(entry) => entry.short_description(), + None => format!("alloc{}", id), + } + } +} + +impl AllocEntry { + pub fn from_alloc_info(info: &AllocInfo, type_index: &TypeIndex) -> Self { + let alloc_id = info.alloc_id().to_index() as u64; + let ty = info.ty(); + let ty_name = type_index.get_name(ty); + + let (kind, description) = match info.global_alloc() { + GlobalAlloc::Memory(alloc) => { + let bytes = &alloc.bytes; + let is_str = ty_name.contains("str") || ty_name.contains("&str"); + + // Convert Option bytes to actual bytes for display + let concrete_bytes: Vec = bytes.iter().filter_map(|&b| b).collect(); + + let desc = if is_str && concrete_bytes.iter().all(|b| b.is_ascii()) { + let s: String = concrete_bytes + .iter() + .take(20) + .map(|&b| b as char) + .collect::() + .escape_default() + .to_string(); + if concrete_bytes.len() > 20 { + format!("\"{}...\" ({} bytes)", s, concrete_bytes.len()) + } else { + format!("\"{}\"", s) + } + } else if concrete_bytes.len() <= 8 && !concrete_bytes.is_empty() { + // Try to show as integer value + let mut val: u64 = 0; + for (i, &b) in concrete_bytes.iter().enumerate() { + val |= (b as u64) << (i * 8); + } + format!("{} = {}", ty_name, val) + } else { + format!("{} ({} bytes)", ty_name, bytes.len()) + }; + + ( + AllocKind::Memory { + bytes_len: bytes.len(), + is_str, + }, + desc, + ) + } + GlobalAlloc::Static(def) => { + let name = def.name(); + ( + AllocKind::Static { name: name.clone() }, + format!("static {}", name), + ) + } + GlobalAlloc::VTable(vty, _trait_ref) => { + let desc = format!("{}", vty); + ( + AllocKind::VTable { + ty_desc: desc.clone(), + }, + format!("vtable<{}>", desc), + ) + } + GlobalAlloc::Function(instance) => { + let name = instance.name(); + ( + AllocKind::Function { name: name.clone() }, + format!("fn {}", name), + ) + } + }; + + Self { + alloc_id, + ty, + kind, + description, + } + } + + pub fn short_description(&self) -> String { + format!("alloc{}: {}", self.alloc_id, self.description) + } +} + +impl TypeIndex { + pub fn new() -> Self { + Self { + by_id: HashMap::new(), + } + } + + pub fn from_types(types: &[(Ty, TypeMetadata)]) -> Self { + let mut index = Self::new(); + for (ty, metadata) in types { + let name = Self::type_name_from_metadata(metadata, *ty); + index.by_id.insert(ty.to_index() as u64, name); + } + index + } + + fn type_name_from_metadata(metadata: &TypeMetadata, ty: Ty) -> String { + match metadata { + TypeMetadata::PrimitiveType(rigid) => format!("{:?}", rigid), + TypeMetadata::EnumType { name, .. } => name.clone(), + TypeMetadata::StructType { name, .. } => name.clone(), + TypeMetadata::UnionType { name, .. } => name.clone(), + TypeMetadata::ArrayType { .. } => format!("{}", ty), + TypeMetadata::PtrType { .. } => format!("{}", ty), + TypeMetadata::RefType { .. } => format!("{}", ty), + TypeMetadata::TupleType { .. } => format!("{}", ty), + TypeMetadata::FunType(name) => name.clone(), + TypeMetadata::VoidType => "()".to_string(), + } + } + + pub fn get_name(&self, ty: Ty) -> String { + self.by_id + .get(&(ty.to_index() as u64)) + .cloned() + .unwrap_or_else(|| format!("{}", ty)) + } +} + +impl GraphContext { + pub fn from_smir(smir: &SmirJson) -> Self { + let types = TypeIndex::from_types(&smir.types); + let allocs = AllocIndex::from_alloc_infos(&smir.allocs, &types); + let functions: HashMap = smir + .functions + .iter() + .map(|(k, v)| (k.0, function_string(v.clone()))) + .collect(); + + Self { + allocs, + types, + functions, + } + } + + /// Render a constant operand with alloc information + pub fn render_const(&self, const_: &MirConst) -> String { + let ty = const_.ty(); + let ty_name = self.types.get_name(ty); + + match const_.kind() { + ConstantKind::Allocated(alloc) => { + // Check if this constant references any allocs via provenance + if !alloc.provenance.ptrs.is_empty() { + let alloc_refs: Vec = alloc + .provenance + .ptrs + .iter() + .map(|(_offset, prov)| self.allocs.describe(prov.0.to_index() as u64)) + .collect(); + format!("const [{}]", alloc_refs.join(", ")) + } else { + // Inline constant - try to show value + let bytes = &alloc.bytes; + // Convert Option to concrete bytes + let concrete_bytes: Vec = bytes.iter().filter_map(|&b| b).collect(); + if concrete_bytes.len() <= 8 && !concrete_bytes.is_empty() { + let mut val: u64 = 0; + for (i, &b) in concrete_bytes.iter().enumerate() { + val |= (b as u64) << (i * 8); + } + format!("const {}_{}", val, ty_name) + } else { + format!("const {}", ty_name) + } + } + } + ConstantKind::ZeroSized => { + // Function pointers, unit type, etc. + if ty.kind().is_fn() { + if let Some(name) = self.functions.get(&ty) { + format!("const fn {}", short_fn_name(name)) + } else { + format!("const {}", ty_name) + } + } else { + format!("const {}", ty_name) + } + } + ConstantKind::Ty(_) => format!("const {}", ty_name), + ConstantKind::Unevaluated(_) => format!("const unevaluated {}", ty_name), + ConstantKind::Param(_) => format!("const param {}", ty_name), + } + } + + /// Render an operand with context + pub fn render_operand(&self, op: &Operand) -> String { + match op { + Operand::Constant(ConstOperand { const_, .. }) => self.render_const(const_), + Operand::Copy(place) => format!("cp({})", place.label()), + Operand::Move(place) => format!("mv({})", place.label()), + } + } + + /// Generate the allocs legend as lines for display + pub fn allocs_legend_lines(&self) -> Vec { + let mut lines = vec!["ALLOCS".to_string()]; + let mut entries: Vec<_> = self.allocs.iter().collect(); + entries.sort_by_key(|e| e.alloc_id); + for entry in entries { + lines.push(entry.short_description()); + } + lines + } +} + +/// Shorten a function name for display +fn short_fn_name(name: &str) -> String { + // Take last segment after :: + name.rsplit("::").next().unwrap_or(name).to_string() +} + // entry point to write the dot file pub fn emit_dotfile(tcx: TyCtxt<'_>) { let smir_dot = collect_smir(tcx).to_dot_file(); @@ -51,6 +344,9 @@ impl SmirJson<'_> { pub fn to_dot_file(self) -> String { let mut bytes = Vec::new(); + // Build context BEFORE consuming self + let ctx = GraphContext::from_smir(&self); + { let mut writer = DotWriter::from(&mut bytes); @@ -60,17 +356,21 @@ impl SmirJson<'_> { graph.set_label(&self.name[..]); graph.node_attributes().set_shape(Shape::Rectangle); - let func_map: HashMap = self - .functions - .into_iter() - .map(|(k, v)| (k.0, function_string(v))) - .collect(); - let item_names: HashSet = self.items.iter().map(|i| i.symbol_name.clone()).collect(); + // Add allocs legend node if there are any allocs + if !ctx.allocs.by_id.is_empty() { + let mut alloc_node = graph.node_auto(); + let mut lines = ctx.allocs_legend_lines(); + lines.push("".to_string()); + alloc_node.set_label(&lines.join("\\l")); + alloc_node.set_style(Style::Filled); + alloc_node.set("color", "lightyellow", false); + } + // first create all nodes for functions not in the items list - for f in func_map.values() { + for f in ctx.functions.values() { if !item_names.contains(f) { graph .node_named(block_name(f, 0)) @@ -111,7 +411,7 @@ impl SmirJson<'_> { let this_block = block_name(name, node_id); let mut label_strs: Vec = - b.statements.iter().map(render_stmt).collect(); + b.statements.iter().map(|s| render_stmt_ctx(s, &ctx)).collect(); // TODO: render statements and terminator as text label (with line breaks) // switch on terminator kind, add inner and out-edges according to terminator use TerminatorKind::*; @@ -121,7 +421,7 @@ impl SmirJson<'_> { cluster.edge(&this_block, block_name(name, *target)); } SwitchInt { discr, targets } => { - label_strs.push(format!("SwitchInt {}", discr.label())); + label_strs.push(format!("SwitchInt {}", ctx.render_operand(discr))); for (d, t) in targets.clone().branches() { cluster .edge(&this_block, block_name(name, t)) @@ -196,7 +496,7 @@ impl SmirJson<'_> { } => { label_strs.push(format!( "Assert {} == {}", - cond.label(), + ctx.render_operand(cond), expected )); if let UnwindAction::Cleanup(t) = unwind { @@ -260,7 +560,7 @@ impl SmirJson<'_> { Operand::Constant(ConstOperand { const_, .. }) => { - if let Some(callee) = func_map.get(&const_.ty()) + if let Some(callee) = ctx.functions.get(&const_.ty()) { // callee node/body will be added when its body is added, missing ones added before graph.edge( @@ -285,7 +585,7 @@ impl SmirJson<'_> { }; let arg_str = args .iter() - .map(|op| op.label()) + .map(|op| ctx.render_operand(op)) .collect::>() .join(","); e.attributes().set_label(&arg_str); @@ -355,10 +655,11 @@ fn block_name(function_name: &String, id: usize) -> String { format!("X{:x}_{}", h.finish(), id) } -fn render_stmt(s: &Statement) -> String { +/// Render statement with context for alloc/type information +fn render_stmt_ctx(s: &Statement, ctx: &GraphContext) -> String { use StatementKind::*; match &s.kind { - Assign(p, v) => format!("{} <- {}", p.label(), v.label()), + Assign(p, v) => format!("{} <- {}", p.label(), render_rvalue_ctx(v, ctx)), FakeRead(_cause, p) => format!("Fake-Read {}", p.label()), SetDiscriminant { place, @@ -379,12 +680,75 @@ fn render_stmt(s: &Statement) -> String { variance: _, } => format!("Ascribe {}.{}", place.label(), projections.base), Coverage(_) => "Coverage".to_string(), - Intrinsic(intr) => format!("Intr: {}", intr.label()), + Intrinsic(intr) => format!("Intr: {}", render_intrinsic_ctx(intr, ctx)), ConstEvalCounter {} => "ConstEvalCounter".to_string(), Nop {} => "Nop".to_string(), } } +/// Render rvalue with context +fn render_rvalue_ctx(v: &Rvalue, ctx: &GraphContext) -> String { + use Rvalue::*; + match v { + AddressOf(mutability, p) => match mutability { + Mutability::Not => format!("&raw {}", p.label()), + Mutability::Mut => format!("&raw mut {}", p.label()), + }, + Aggregate(kind, operands) => { + let os: Vec = operands.iter().map(|op| ctx.render_operand(op)).collect(); + format!("{} ({})", kind.label(), os.join(", ")) + } + BinaryOp(binop, op1, op2) => format!( + "{:?}({}, {})", + binop, + ctx.render_operand(op1), + ctx.render_operand(op2) + ), + Cast(kind, op, _ty) => format!("Cast-{:?} {}", kind, ctx.render_operand(op)), + CheckedBinaryOp(binop, op1, op2) => { + format!( + "chkd-{:?}({}, {})", + binop, + ctx.render_operand(op1), + ctx.render_operand(op2) + ) + } + CopyForDeref(p) => format!("CopyForDeref({})", p.label()), + Discriminant(p) => format!("Discriminant({})", p.label()), + Len(p) => format!("Len({})", p.label()), + Ref(_region, borrowkind, p) => { + format!( + "&{} {}", + match borrowkind { + BorrowKind::Mut { kind: _ } => "mut", + _other => "", + }, + p.label() + ) + } + Repeat(op, _ty_const) => format!("Repeat {}", ctx.render_operand(op)), + ShallowInitBox(op, _ty) => format!("ShallowInitBox({})", ctx.render_operand(op)), + ThreadLocalRef(_item) => "ThreadLocalRef".to_string(), + NullaryOp(nullop, ty) => format!("{} :: {}", nullop.label(), ty), + UnaryOp(unop, op) => format!("{:?}({})", unop, ctx.render_operand(op)), + Use(op) => format!("Use({})", ctx.render_operand(op)), + } +} + +/// Render intrinsic with context +fn render_intrinsic_ctx(intr: &NonDivergingIntrinsic, ctx: &GraphContext) -> String { + use NonDivergingIntrinsic::*; + match intr { + Assume(op) => format!("Assume {}", ctx.render_operand(op)), + CopyNonOverlapping(c) => format!( + "CopyNonOverlapping: {} <- {}({})", + c.dst.label(), + c.src.label(), + ctx.render_operand(&c.count) + ), + } +} + /// Rendering things as part of graph node labels trait GraphLabelString { fn label(&self) -> String; diff --git a/src/printer.rs b/src/printer.rs index 866f2f3..a503f62 100644 --- a/src/printer.rs +++ b/src/printer.rs @@ -1319,6 +1319,20 @@ pub struct AllocInfo { global_alloc: GlobalAlloc, } +impl AllocInfo { + pub fn alloc_id(&self) -> AllocId { + self.alloc_id + } + + pub fn ty(&self) -> stable_mir::ty::Ty { + self.ty + } + + pub fn global_alloc(&self) -> &GlobalAlloc { + &self.global_alloc + } +} + // Serialization Entrypoint // ======================== From 301b89d97dea9f590bdcef87d611c15e7536689a Mon Sep 17 00:00:00 2001 From: cds-amal Date: Sun, 30 Nov 2025 13:46:58 -0500 Subject: [PATCH 2/5] Add D2 diagram format output Add alternative graph output format using D2 language alongside existing DOT format. D2 offers modern diagramming with better text rendering and can be viewed in browser via d2lang.com playground. Changes: - Add --d2 command-line flag for D2 output - Add to_d2_file() method to SmirJson for D2 rendering - Add emit_d2file() entry point - Add render_terminator_ctx() and terminator_targets() helpers - Add resolve_call_target() method to GraphContext - Add escape_d2() to handle $ and other special characters in labels Output includes: - ALLOCS legend with allocation descriptions - Function containers with basic blocks - Block-to-block control flow edges - Call edges to external functions --- src/main.rs | 6 +- src/mk_graph.rs | 260 +++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 263 insertions(+), 3 deletions(-) diff --git a/src/main.rs b/src/main.rs index 8806738..deaaebb 100644 --- a/src/main.rs +++ b/src/main.rs @@ -4,7 +4,7 @@ pub mod driver; pub mod printer; use driver::stable_mir_driver; use printer::emit_smir; -use stable_mir_json::mk_graph::emit_dotfile; +use stable_mir_json::mk_graph::{emit_d2file, emit_dotfile}; fn main() { let mut args: Vec = env::args().collect(); @@ -19,6 +19,10 @@ fn main() { args.remove(1); stable_mir_driver(&args, emit_dotfile) } + Some(arg) if arg == "--d2" => { + args.remove(1); + stable_mir_driver(&args, emit_d2file) + } Some(_other) => stable_mir_driver(&args, emit_smir), // backward compatibility } } diff --git a/src/mk_graph.rs b/src/mk_graph.rs index eced371..a76798a 100644 --- a/src/mk_graph.rs +++ b/src/mk_graph.rs @@ -20,8 +20,8 @@ use stable_mir::CrateDef; use stable_mir::{ mir::{ AggregateKind, BasicBlock, BorrowKind, ConstOperand, Mutability, NonDivergingIntrinsic, - NullOp, Operand, Place, ProjectionElem, Rvalue, Statement, StatementKind, TerminatorKind, - UnwindAction, + NullOp, Operand, Place, ProjectionElem, Rvalue, Statement, StatementKind, Terminator, + TerminatorKind, UnwindAction, }, ty::RigidTy, }; @@ -314,6 +314,21 @@ impl GraphContext { } lines } + + /// Resolve a call target to a function name if it's a constant function pointer + pub fn resolve_call_target(&self, func: &Operand) -> Option { + match func { + Operand::Constant(ConstOperand { const_, .. }) => { + let ty = const_.ty(); + if ty.kind().is_fn() { + self.functions.get(&ty).cloned() + } else { + None + } + } + _ => None, + } + } } /// Shorten a function name for display @@ -340,6 +355,24 @@ 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(); + + match tcx.output_filenames(()).path(OutputType::Mir) { + OutFileName::Stdout => { + write!(io::stdout(), "{}", smir_d2).expect("Failed to write smir.d2"); + } + OutFileName::Real(path) => { + let mut b = io::BufWriter::new( + File::create(path.with_extension("smir.d2")) + .expect("Failed to create {path}.smir.d2 output file"), + ); + write!(b, "{}", smir_d2).expect("Failed to write smir.d2"); + } + } +} + impl SmirJson<'_> { pub fn to_dot_file(self) -> String { let mut bytes = Vec::new(); @@ -619,6 +652,136 @@ impl SmirJson<'_> { String::from_utf8(bytes).expect("Error converting dot file") } + + /// Convert to D2 diagram format + pub fn to_d2_file(self) -> String { + let ctx = GraphContext::from_smir(&self); + let mut output = String::new(); + + // D2 direction setting + output.push_str("direction: right\n\n"); + + // Add allocs legend as a special container + let legend_lines = ctx.allocs_legend_lines(); + if !legend_lines.is_empty() { + output.push_str("ALLOCS: {\n"); + output.push_str(" style.fill: \"#ffffcc\"\n"); + output.push_str(" style.stroke: \"#999999\"\n"); + let legend_text = legend_lines + .iter() + .map(|s| escape_d2(s)) + .collect::>() + .join("\\n"); + output.push_str(&format!(" label: \"{}\"\n", legend_text)); + output.push_str("}\n\n"); + } + + // Process each item + for item in self.items { + match item.mono_item_kind { + MonoItemKind::MonoItemFn { + name, + id: _, + body, + } => { + let fn_id = short_name(&name); + let display_name = escape_d2(&name_lines(&name)); + + // Create function container + output.push_str(&format!("{}: {{\n", fn_id)); + output.push_str(&format!(" label: \"{}\"\n", display_name)); + output.push_str(" style.fill: \"#e0e0ff\"\n"); + + if let Some(ref body) = body { + // Add blocks as nodes within the function + for (idx, block) in body.blocks.iter().enumerate() { + let block_id = format!("bb{}", idx); + let stmts: Vec = block + .statements + .iter() + .map(|s| escape_d2(&render_stmt_ctx(s, &ctx))) + .collect(); + + let term_str = escape_d2(&render_terminator_ctx(&block.terminator, &ctx)); + + let mut block_label = format!("bb{}:", idx); + for stmt in &stmts { + block_label.push_str(&format!("\\n{}", stmt)); + } + block_label.push_str(&format!("\\n---\\n{}", term_str)); + + output.push_str(&format!(" {}: \"{}\"\n", block_id, block_label)); + } + + // Add edges between blocks + for (idx, block) in body.blocks.iter().enumerate() { + let from_id = format!("bb{}", idx); + let targets = terminator_targets(&block.terminator); + for target in targets { + let to_id = format!("bb{}", target); + output.push_str(&format!(" {} -> {}\n", from_id, to_id)); + } + } + } + + output.push_str("}\n\n"); + + // Add call edges to external functions + if let Some(ref body) = body { + for (idx, block) in body.blocks.iter().enumerate() { + if let TerminatorKind::Call { func, .. } = &block.terminator.kind { + if let Some(fn_name) = ctx.resolve_call_target(func) { + if is_unqualified(&fn_name) { + let target_id = short_name(&fn_name); + // Ensure external function node exists + output.push_str(&format!( + "{}: \"{}\"\n", + target_id, + escape_d2(&fn_name) + )); + output.push_str(&format!( + "{}.style.fill: \"#ffe0e0\"\n", + target_id + )); + output.push_str(&format!( + "{}.bb{} -> {}: call\n", + fn_id, idx, target_id + )); + } + } + } + } + } + } + MonoItemKind::MonoItemGlobalAsm { asm } => { + let asm_id = short_name(&asm); + let asm_text = escape_d2(&asm.lines().collect::()); + output.push_str(&format!("{}: \"{}\" {{\n", asm_id, asm_text)); + output.push_str(" style.fill: \"#ffe0ff\"\n"); + output.push_str("}\n\n"); + } + MonoItemKind::MonoItemStatic { name, .. } => { + let static_id = short_name(&name); + output.push_str(&format!( + "{}: \"{}\" {{\n", + static_id, + escape_d2(&name) + )); + output.push_str(" style.fill: \"#e0ffe0\"\n"); + output.push_str("}\n\n"); + } + } + } + + output + } +} + +/// Escape special characters for D2 string labels +fn escape_d2(s: &str) -> String { + s.replace('\\', "\\\\") + .replace('"', "\\\"") + .replace('$', "\\$") } fn is_unqualified(name: &str) -> bool { @@ -749,6 +912,99 @@ fn render_intrinsic_ctx(intr: &NonDivergingIntrinsic, ctx: &GraphContext) -> Str } } +/// Render terminator with context for alloc/type information +fn render_terminator_ctx(term: &Terminator, ctx: &GraphContext) -> String { + use TerminatorKind::*; + match &term.kind { + Goto { .. } => "Goto".to_string(), + SwitchInt { discr, .. } => format!("SwitchInt {}", ctx.render_operand(discr)), + Resume {} => "Resume".to_string(), + Abort {} => "Abort".to_string(), + Return {} => "Return".to_string(), + Unreachable {} => "Unreachable".to_string(), + Drop { place, .. } => format!("Drop {}", place.label()), + Call { + func, + args, + destination, + .. + } => { + let fn_name = ctx + .resolve_call_target(func) + .map(|n| short_fn_name(&n)) + .unwrap_or_else(|| "?".to_string()); + let arg_str = args + .iter() + .map(|op| ctx.render_operand(op)) + .collect::>() + .join(", "); + format!("{} = {}({})", destination.label(), fn_name, arg_str) + } + Assert { + cond, expected, .. + } => format!("Assert {} == {}", ctx.render_operand(cond), expected), + InlineAsm { .. } => "InlineAsm".to_string(), + } +} + +/// Get target block indices from a terminator +fn terminator_targets(term: &Terminator) -> Vec { + use TerminatorKind::*; + match &term.kind { + Goto { target } => vec![*target], + SwitchInt { targets, .. } => { + let mut result: Vec = targets.branches().map(|(_, t)| t).collect(); + result.push(targets.otherwise()); + result + } + Resume {} | Abort {} | Return {} | Unreachable {} => vec![], + Drop { + target, unwind, .. + } => { + let mut result = vec![*target]; + if let UnwindAction::Cleanup(t) = unwind { + result.push(*t); + } + result + } + Call { + target, unwind, .. + } => { + let mut result = vec![]; + if let Some(t) = target { + result.push(*t); + } + if let UnwindAction::Cleanup(t) = unwind { + result.push(*t); + } + result + } + Assert { + target, unwind, .. + } => { + let mut result = vec![*target]; + if let UnwindAction::Cleanup(t) = unwind { + result.push(*t); + } + result + } + InlineAsm { + destination, + unwind, + .. + } => { + let mut result = vec![]; + if let Some(t) = destination { + result.push(*t); + } + if let UnwindAction::Cleanup(t) = unwind { + result.push(*t); + } + result + } + } +} + /// Rendering things as part of graph node labels trait GraphLabelString { fn label(&self) -> String; From 4e99e5e56fe87eda9f1063b9fe4fd98133ad8a36 Mon Sep 17 00:00:00 2001 From: cds-amal Date: Sun, 30 Nov 2025 14:10:25 -0500 Subject: [PATCH 3/5] refactor: improve code quality - Change &String to &str in short_name() and block_name() signatures - Extract bytes_to_u64_le() helper for repeated byte-to-int conversion - Fix expect() messages to properly interpolate path using unwrap_or_else --- src/mk_graph.rs | 41 ++++++++++++++++++++--------------------- 1 file changed, 20 insertions(+), 21 deletions(-) diff --git a/src/mk_graph.rs b/src/mk_graph.rs index a76798a..0be1046 100644 --- a/src/mk_graph.rs +++ b/src/mk_graph.rs @@ -133,12 +133,7 @@ impl AllocEntry { format!("\"{}\"", s) } } else if concrete_bytes.len() <= 8 && !concrete_bytes.is_empty() { - // Try to show as integer value - let mut val: u64 = 0; - for (i, &b) in concrete_bytes.iter().enumerate() { - val |= (b as u64) << (i * 8); - } - format!("{} = {}", ty_name, val) + format!("{} = {}", ty_name, bytes_to_u64_le(&concrete_bytes)) } else { format!("{} ({} bytes)", ty_name, bytes.len()) }; @@ -267,11 +262,7 @@ impl GraphContext { // Convert Option to concrete bytes let concrete_bytes: Vec = bytes.iter().filter_map(|&b| b).collect(); if concrete_bytes.len() <= 8 && !concrete_bytes.is_empty() { - let mut val: u64 = 0; - for (i, &b) in concrete_bytes.iter().enumerate() { - val |= (b as u64) << (i * 8); - } - format!("const {}_{}", val, ty_name) + format!("const {}_{}", bytes_to_u64_le(&concrete_bytes), ty_name) } else { format!("const {}", ty_name) } @@ -346,10 +337,10 @@ pub fn emit_dotfile(tcx: TyCtxt<'_>) { write!(io::stdout(), "{}", smir_dot).expect("Failed to write smir.dot"); } OutFileName::Real(path) => { - let mut b = io::BufWriter::new( - File::create(path.with_extension("smir.dot")) - .expect("Failed to create {path}.smir.dot output file"), - ); + let out_path = path.with_extension("smir.dot"); + let mut b = io::BufWriter::new(File::create(&out_path).unwrap_or_else(|e| { + panic!("Failed to create {}: {}", out_path.display(), e) + })); write!(b, "{}", smir_dot).expect("Failed to write smir.dot"); } } @@ -364,10 +355,10 @@ pub fn emit_d2file(tcx: TyCtxt<'_>) { write!(io::stdout(), "{}", smir_d2).expect("Failed to write smir.d2"); } OutFileName::Real(path) => { - let mut b = io::BufWriter::new( - File::create(path.with_extension("smir.d2")) - .expect("Failed to create {path}.smir.d2 output file"), - ); + let out_path = path.with_extension("smir.d2"); + let mut b = io::BufWriter::new(File::create(&out_path).unwrap_or_else(|e| { + panic!("Failed to create {}: {}", out_path.display(), e) + })); write!(b, "{}", smir_d2).expect("Failed to write smir.d2"); } } @@ -784,6 +775,14 @@ fn escape_d2(s: &str) -> String { .replace('$', "\\$") } +/// Convert byte slice to u64, little-endian (least significant byte first) +fn bytes_to_u64_le(bytes: &[u8]) -> u64 { + bytes + .iter() + .enumerate() + .fold(0u64, |acc, (i, &b)| acc | ((b as u64) << (i * 8))) +} + fn is_unqualified(name: &str) -> bool { !name.contains("::") } @@ -805,14 +804,14 @@ fn name_lines(name: &str) -> String { } /// consistently naming function clusters -fn short_name(function_name: &String) -> String { +fn short_name(function_name: &str) -> String { let mut h = DefaultHasher::new(); function_name.hash(&mut h); format!("X{:x}", h.finish()) } /// consistently naming block nodes in function clusters -fn block_name(function_name: &String, id: usize) -> String { +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) From d4ef38cd4802e84f748e277bab7ab44d41b7bdcc Mon Sep 17 00:00:00 2001 From: cds-amal Date: Sun, 30 Nov 2025 14:15:58 -0500 Subject: [PATCH 4/5] refactor: flatten nested logic in to_d2_file Extract D2 rendering into focused helper functions: - render_d2_allocs_legend - render_d2_function - render_d2_blocks - render_d2_block_edges - render_d2_call_edges - render_d2_asm - render_d2_static --- src/mk_graph.rs | 223 ++++++++++++++++++++++++++---------------------- 1 file changed, 120 insertions(+), 103 deletions(-) diff --git a/src/mk_graph.rs b/src/mk_graph.rs index 0be1046..af13b24 100644 --- a/src/mk_graph.rs +++ b/src/mk_graph.rs @@ -649,117 +649,19 @@ impl SmirJson<'_> { let ctx = GraphContext::from_smir(&self); let mut output = String::new(); - // D2 direction setting output.push_str("direction: right\n\n"); + render_d2_allocs_legend(&ctx, &mut output); - // Add allocs legend as a special container - let legend_lines = ctx.allocs_legend_lines(); - if !legend_lines.is_empty() { - output.push_str("ALLOCS: {\n"); - output.push_str(" style.fill: \"#ffffcc\"\n"); - output.push_str(" style.stroke: \"#999999\"\n"); - let legend_text = legend_lines - .iter() - .map(|s| escape_d2(s)) - .collect::>() - .join("\\n"); - output.push_str(&format!(" label: \"{}\"\n", legend_text)); - output.push_str("}\n\n"); - } - - // Process each item for item in self.items { match item.mono_item_kind { - MonoItemKind::MonoItemFn { - name, - id: _, - body, - } => { - let fn_id = short_name(&name); - let display_name = escape_d2(&name_lines(&name)); - - // Create function container - output.push_str(&format!("{}: {{\n", fn_id)); - output.push_str(&format!(" label: \"{}\"\n", display_name)); - output.push_str(" style.fill: \"#e0e0ff\"\n"); - - if let Some(ref body) = body { - // Add blocks as nodes within the function - for (idx, block) in body.blocks.iter().enumerate() { - let block_id = format!("bb{}", idx); - let stmts: Vec = block - .statements - .iter() - .map(|s| escape_d2(&render_stmt_ctx(s, &ctx))) - .collect(); - - let term_str = escape_d2(&render_terminator_ctx(&block.terminator, &ctx)); - - let mut block_label = format!("bb{}:", idx); - for stmt in &stmts { - block_label.push_str(&format!("\\n{}", stmt)); - } - block_label.push_str(&format!("\\n---\\n{}", term_str)); - - output.push_str(&format!(" {}: \"{}\"\n", block_id, block_label)); - } - - // Add edges between blocks - for (idx, block) in body.blocks.iter().enumerate() { - let from_id = format!("bb{}", idx); - let targets = terminator_targets(&block.terminator); - for target in targets { - let to_id = format!("bb{}", target); - output.push_str(&format!(" {} -> {}\n", from_id, to_id)); - } - } - } - - output.push_str("}\n\n"); - - // Add call edges to external functions - if let Some(ref body) = body { - for (idx, block) in body.blocks.iter().enumerate() { - if let TerminatorKind::Call { func, .. } = &block.terminator.kind { - if let Some(fn_name) = ctx.resolve_call_target(func) { - if is_unqualified(&fn_name) { - let target_id = short_name(&fn_name); - // Ensure external function node exists - output.push_str(&format!( - "{}: \"{}\"\n", - target_id, - escape_d2(&fn_name) - )); - output.push_str(&format!( - "{}.style.fill: \"#ffe0e0\"\n", - target_id - )); - output.push_str(&format!( - "{}.bb{} -> {}: call\n", - fn_id, idx, target_id - )); - } - } - } - } - } + MonoItemKind::MonoItemFn { name, body, .. } => { + render_d2_function(&name, body.as_ref(), &ctx, &mut output); } MonoItemKind::MonoItemGlobalAsm { asm } => { - let asm_id = short_name(&asm); - let asm_text = escape_d2(&asm.lines().collect::()); - output.push_str(&format!("{}: \"{}\" {{\n", asm_id, asm_text)); - output.push_str(" style.fill: \"#ffe0ff\"\n"); - output.push_str("}\n\n"); + render_d2_asm(&asm, &mut output); } MonoItemKind::MonoItemStatic { name, .. } => { - let static_id = short_name(&name); - output.push_str(&format!( - "{}: \"{}\" {{\n", - static_id, - escape_d2(&name) - )); - output.push_str(" style.fill: \"#e0ffe0\"\n"); - output.push_str("}\n\n"); + render_d2_static(&name, &mut output); } } } @@ -768,6 +670,121 @@ impl SmirJson<'_> { } } +// ============================================================================= +// D2 Rendering Helpers +// ============================================================================= + +fn render_d2_allocs_legend(ctx: &GraphContext, out: &mut String) { + let legend_lines = ctx.allocs_legend_lines(); + if legend_lines.is_empty() { + return; + } + + 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(&render_stmt_ctx(s, ctx))) + .collect(); + let term_str = escape_d2(&render_terminator_ctx(&block.terminator, ctx)); + + 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"); +} + /// Escape special characters for D2 string labels fn escape_d2(s: &str) -> String { s.replace('\\', "\\\\") From aa830d990739706691b9c51b7e0e486dc3c74462 Mon Sep 17 00:00:00 2001 From: cds-amal Date: Sun, 30 Nov 2025 23:04:30 -0500 Subject: [PATCH 5/5] cargo clippy && cargo fmt --- src/mk_graph.rs | 59 +++++++++++++++++++++++++++++++------------------ 1 file changed, 37 insertions(+), 22 deletions(-) diff --git a/src/mk_graph.rs b/src/mk_graph.rs index af13b24..f2aa584 100644 --- a/src/mk_graph.rs +++ b/src/mk_graph.rs @@ -72,6 +72,12 @@ pub struct GraphContext { // Index Implementation // ============================================================================= +impl Default for AllocIndex { + fn default() -> Self { + Self::new() + } +} + impl AllocIndex { pub fn new() -> Self { Self { @@ -184,6 +190,12 @@ impl AllocEntry { } } +impl Default for TypeIndex { + fn default() -> Self { + Self::new() + } +} + impl TypeIndex { pub fn new() -> Self { Self { @@ -338,9 +350,10 @@ pub fn emit_dotfile(tcx: TyCtxt<'_>) { } OutFileName::Real(path) => { let out_path = path.with_extension("smir.dot"); - let mut b = io::BufWriter::new(File::create(&out_path).unwrap_or_else(|e| { - panic!("Failed to create {}: {}", out_path.display(), e) - })); + let mut b = io::BufWriter::new( + File::create(&out_path) + .unwrap_or_else(|e| panic!("Failed to create {}: {}", out_path.display(), e)), + ); write!(b, "{}", smir_dot).expect("Failed to write smir.dot"); } } @@ -356,9 +369,10 @@ pub fn emit_d2file(tcx: TyCtxt<'_>) { } OutFileName::Real(path) => { let out_path = path.with_extension("smir.d2"); - let mut b = io::BufWriter::new(File::create(&out_path).unwrap_or_else(|e| { - panic!("Failed to create {}: {}", out_path.display(), e) - })); + let mut b = io::BufWriter::new( + File::create(&out_path) + .unwrap_or_else(|e| panic!("Failed to create {}: {}", out_path.display(), e)), + ); write!(b, "{}", smir_d2).expect("Failed to write smir.d2"); } } @@ -434,8 +448,11 @@ impl SmirJson<'_> { let name = &item.symbol_name; let this_block = block_name(name, node_id); - let mut label_strs: Vec = - b.statements.iter().map(|s| render_stmt_ctx(s, &ctx)).collect(); + let mut label_strs: Vec = b + .statements + .iter() + .map(|s| render_stmt_ctx(s, &ctx)) + .collect(); // TODO: render statements and terminator as text label (with line breaks) // switch on terminator kind, add inner and out-edges according to terminator use TerminatorKind::*; @@ -445,7 +462,10 @@ impl SmirJson<'_> { cluster.edge(&this_block, block_name(name, *target)); } SwitchInt { discr, targets } => { - label_strs.push(format!("SwitchInt {}", ctx.render_operand(discr))); + label_strs.push(format!( + "SwitchInt {}", + ctx.render_operand(discr) + )); for (d, t) in targets.clone().branches() { cluster .edge(&this_block, block_name(name, t)) @@ -584,7 +604,8 @@ impl SmirJson<'_> { Operand::Constant(ConstOperand { const_, .. }) => { - if let Some(callee) = ctx.functions.get(&const_.ty()) + if let Some(callee) = + ctx.functions.get(&const_.ty()) { // callee node/body will be added when its body is added, missing ones added before graph.edge( @@ -956,9 +977,9 @@ fn render_terminator_ctx(term: &Terminator, ctx: &GraphContext) -> String { .join(", "); format!("{} = {}({})", destination.label(), fn_name, arg_str) } - Assert { - cond, expected, .. - } => format!("Assert {} == {}", ctx.render_operand(cond), expected), + Assert { cond, expected, .. } => { + format!("Assert {} == {}", ctx.render_operand(cond), expected) + } InlineAsm { .. } => "InlineAsm".to_string(), } } @@ -974,18 +995,14 @@ fn terminator_targets(term: &Terminator) -> Vec { result } Resume {} | Abort {} | Return {} | Unreachable {} => vec![], - Drop { - target, unwind, .. - } => { + Drop { target, unwind, .. } => { let mut result = vec![*target]; if let UnwindAction::Cleanup(t) = unwind { result.push(*t); } result } - Call { - target, unwind, .. - } => { + Call { target, unwind, .. } => { let mut result = vec![]; if let Some(t) = target { result.push(*t); @@ -995,9 +1012,7 @@ fn terminator_targets(term: &Terminator) -> Vec { } result } - Assert { - target, unwind, .. - } => { + Assert { target, unwind, .. } => { let mut result = vec![*target]; if let UnwindAction::Cleanup(t) = unwind { result.push(*t);