Skip to content

Commit 60ea7e4

Browse files
Add a few options to dump the reachability graph (debug only) (#2433)
This is for debugging purpose only. All this logic will be disabled in release builds. For simplicity, I'm just using an environment variable (`KANI_REACH_DEBUG`). Co-authored-by: Zyad Hassan <88045115+zhassan-aws@users.noreply.github.com>
1 parent c3d28e0 commit 60ea7e4

File tree

2 files changed

+163
-15
lines changed

2 files changed

+163
-15
lines changed

docs/src/cheat-sheets.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,22 @@ kani --gen-c file.rs
8181
RUSTFLAGS="--emit mir" kani ${INPUT}.rs
8282
```
8383

84+
The `KANI_REACH_DEBUG` environment variable can be used to debug Kani's reachability analysis.
85+
If defined, Kani will generate a DOT graph `${INPUT}.dot` with the graph traversed during reachability analysis.
86+
If defined and not empty, the graph will be filtered to end at functions that contains the substring
87+
from `KANI_REACH_DEBUG`.
88+
89+
Note that this will only work on debug builds.
90+
91+
```bash
92+
# Generate a DOT graph ${INPUT}.dot with the graph traversed during reachability analysis
93+
KANI_REACH_DEBUG= kani ${INPUT}.rs
94+
95+
# Generate a DOT graph ${INPUT}.dot with the sub-graph traversed during the reachability analysis
96+
# that connect to the given target.
97+
KANI_REACH_DEBUG="${TARGET_ITEM}" kani ${INPUT}.rs
98+
```
99+
84100
## CBMC
85101

86102
```bash

kani-compiler/src/kani_middle/reachability.rs

Lines changed: 147 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -44,10 +44,17 @@ pub fn collect_reachable_items<'tcx>(
4444
) -> Vec<MonoItem<'tcx>> {
4545
// For each harness, collect items using the same collector.
4646
// I.e.: This will return any item that is reachable from one or more of the starting points.
47-
let mut collector = MonoItemsCollector { tcx, collected: FxHashSet::default(), queue: vec![] };
47+
let mut collector = MonoItemsCollector::new(tcx);
4848
for item in starting_points {
4949
collector.collect(*item);
5050
}
51+
52+
#[cfg(debug_assertions)]
53+
collector
54+
.call_graph
55+
.dump_dot(tcx)
56+
.unwrap_or_else(|e| tracing::error!("Failed to dump call graph: {e}"));
57+
5158
tcx.sess.abort_if_errors();
5259

5360
// Sort the result so code generation follows deterministic order.
@@ -140,9 +147,20 @@ struct MonoItemsCollector<'tcx> {
140147
collected: FxHashSet<MonoItem<'tcx>>,
141148
/// Items enqueued for visiting.
142149
queue: Vec<MonoItem<'tcx>>,
150+
#[cfg(debug_assertions)]
151+
call_graph: debug::CallGraph<'tcx>,
143152
}
144153

145154
impl<'tcx> MonoItemsCollector<'tcx> {
155+
pub fn new(tcx: TyCtxt<'tcx>) -> Self {
156+
MonoItemsCollector {
157+
tcx,
158+
collected: FxHashSet::default(),
159+
queue: vec![],
160+
#[cfg(debug_assertions)]
161+
call_graph: debug::CallGraph::default(),
162+
}
163+
}
146164
/// Collects all reachable items starting from the given root.
147165
pub fn collect(&mut self, root: MonoItem<'tcx>) {
148166
debug!(?root, "collect");
@@ -156,52 +174,56 @@ impl<'tcx> MonoItemsCollector<'tcx> {
156174
while let Some(to_visit) = self.queue.pop() {
157175
if !self.collected.contains(&to_visit) {
158176
self.collected.insert(to_visit);
159-
match to_visit {
160-
MonoItem::Fn(instance) => {
161-
self.visit_fn(instance);
162-
}
163-
MonoItem::Static(def_id) => {
164-
self.visit_static(def_id);
165-
}
177+
let next_items = match to_visit {
178+
MonoItem::Fn(instance) => self.visit_fn(instance),
179+
MonoItem::Static(def_id) => self.visit_static(def_id),
166180
MonoItem::GlobalAsm(_) => {
167181
self.visit_asm(to_visit);
182+
vec![]
168183
}
169-
}
184+
};
185+
#[cfg(debug_assertions)]
186+
self.call_graph.add_edges(to_visit, &next_items);
187+
188+
self.queue
189+
.extend(next_items.into_iter().filter(|item| !self.collected.contains(item)));
170190
}
171191
}
172192
}
173193

174194
/// Visit a function and collect all mono-items reachable from its instructions.
175-
fn visit_fn(&mut self, instance: Instance<'tcx>) {
195+
fn visit_fn(&mut self, instance: Instance<'tcx>) -> Vec<MonoItem<'tcx>> {
176196
let _guard = debug_span!("visit_fn", function=?instance).entered();
177197
let body = self.tcx.instance_mir(instance.def);
178198
let mut collector =
179199
MonoItemsFnCollector { tcx: self.tcx, collected: FxHashSet::default(), instance, body };
180200
collector.visit_body(body);
181-
self.queue.extend(collector.collected.iter().filter(|item| !self.collected.contains(item)));
201+
collector.collected.into_iter().collect()
182202
}
183203

184204
/// Visit a static object and collect drop / initialization functions.
185-
fn visit_static(&mut self, def_id: DefId) {
205+
fn visit_static(&mut self, def_id: DefId) -> Vec<MonoItem<'tcx>> {
186206
let _guard = debug_span!("visit_static", ?def_id).entered();
187207
let instance = Instance::mono(self.tcx, def_id);
208+
let mut next_items = vec![];
188209

189210
// Collect drop function.
190211
let static_ty = instance.ty(self.tcx, ParamEnv::reveal_all());
191212
let instance = Instance::resolve_drop_in_place(self.tcx, static_ty);
192-
self.queue.push(MonoItem::Fn(instance.polymorphize(self.tcx)));
213+
next_items.push(MonoItem::Fn(instance.polymorphize(self.tcx)));
193214

194215
// Collect initialization.
195216
let alloc = self.tcx.eval_static_initializer(def_id).unwrap();
196217
for id in alloc.inner().provenance().provenances() {
197-
self.queue.extend(collect_alloc_items(self.tcx, id).iter());
218+
next_items.extend(collect_alloc_items(self.tcx, id).iter());
198219
}
220+
221+
next_items
199222
}
200223

201224
/// Visit global assembly and collect its item.
202225
fn visit_asm(&mut self, item: MonoItem<'tcx>) {
203226
debug!(?item, "visit_asm");
204-
self.collected.insert(item);
205227
}
206228
}
207229

@@ -626,3 +648,113 @@ impl<'a, 'tcx> MirVisitor<'tcx> for ConstMonoItemExtractor<'a, 'tcx> {
626648
self.super_rvalue(rvalue, location);
627649
}
628650
}
651+
652+
#[cfg(debug_assertions)]
653+
mod debug {
654+
#![allow(dead_code)]
655+
656+
use std::{
657+
collections::{HashMap, HashSet},
658+
fs::File,
659+
io::{BufWriter, Write},
660+
};
661+
662+
use rustc_session::config::OutputType;
663+
664+
use super::*;
665+
666+
#[derive(Debug, Default)]
667+
pub struct CallGraph<'tcx> {
668+
// Nodes of the graph.
669+
nodes: HashSet<Node<'tcx>>,
670+
edges: HashMap<Node<'tcx>, Vec<Node<'tcx>>>,
671+
back_edges: HashMap<Node<'tcx>, Vec<Node<'tcx>>>,
672+
}
673+
674+
type Node<'tcx> = MonoItem<'tcx>;
675+
676+
impl<'tcx> CallGraph<'tcx> {
677+
pub fn add_node(&mut self, item: Node<'tcx>) {
678+
self.nodes.insert(item);
679+
self.edges.entry(item).or_default();
680+
self.back_edges.entry(item).or_default();
681+
}
682+
683+
/// Add a new edge "from" -> "to".
684+
pub fn add_edge(&mut self, from: Node<'tcx>, to: Node<'tcx>) {
685+
self.add_node(from);
686+
self.add_node(to);
687+
self.edges.get_mut(&from).unwrap().push(to);
688+
self.back_edges.get_mut(&to).unwrap().push(from);
689+
}
690+
691+
/// Add multiple new edges for the "from" node.
692+
pub fn add_edges(&mut self, from: Node<'tcx>, to: &[Node<'tcx>]) {
693+
self.add_node(from);
694+
for item in to {
695+
self.add_edge(from, *item);
696+
}
697+
}
698+
699+
/// Print the graph in DOT format to a file.
700+
/// See <https://graphviz.org/doc/info/lang.html> for more information.
701+
pub fn dump_dot(&self, tcx: TyCtxt) -> std::io::Result<()> {
702+
if let Ok(target) = std::env::var("KANI_REACH_DEBUG") {
703+
debug!(?target, "dump_dot");
704+
let outputs = tcx.output_filenames(());
705+
let path = outputs.output_path(OutputType::Metadata).with_extension("dot");
706+
let out_file = File::create(&path)?;
707+
let mut writer = BufWriter::new(out_file);
708+
writeln!(writer, "digraph ReachabilityGraph {{")?;
709+
if target.is_empty() {
710+
self.dump_all(&mut writer)?;
711+
} else {
712+
// Only dump nodes that led the reachability analysis to the target node.
713+
self.dump_reason(&mut writer, &target)?;
714+
}
715+
writeln!(writer, "}}")?;
716+
}
717+
718+
Ok(())
719+
}
720+
721+
/// Write all notes to the given writer.
722+
fn dump_all<W: Write>(&self, writer: &mut W) -> std::io::Result<()> {
723+
tracing::info!(nodes=?self.nodes.len(), edges=?self.edges.len(), "dump_all");
724+
for node in &self.nodes {
725+
writeln!(writer, r#""{node}""#)?;
726+
for succ in self.edges.get(node).unwrap() {
727+
writeln!(writer, r#""{node}" -> "{succ}" "#)?;
728+
}
729+
}
730+
Ok(())
731+
}
732+
733+
/// Write all notes that may have led to the discovery of the given target.
734+
fn dump_reason<W: Write>(&self, writer: &mut W, target: &str) -> std::io::Result<()> {
735+
let mut queue = self
736+
.nodes
737+
.iter()
738+
.filter(|item| item.to_string().contains(target))
739+
.collect::<Vec<_>>();
740+
let mut visited: HashSet<&MonoItem> = HashSet::default();
741+
tracing::info!(target=?queue, nodes=?self.nodes.len(), edges=?self.edges.len(), "dump_reason");
742+
while let Some(to_visit) = queue.pop() {
743+
if !visited.contains(to_visit) {
744+
visited.insert(to_visit);
745+
queue.extend(self.back_edges.get(to_visit).unwrap());
746+
}
747+
}
748+
749+
for node in &visited {
750+
writeln!(writer, r#""{node}""#)?;
751+
for succ in
752+
self.edges.get(node).unwrap().iter().filter(|item| visited.contains(item))
753+
{
754+
writeln!(writer, r#""{node}" -> "{succ}" "#)?;
755+
}
756+
}
757+
Ok(())
758+
}
759+
}
760+
}

0 commit comments

Comments
 (0)