use std::borrow::Cow;
use std::cell::RefCell;
use std::ffi::OsString;
use std::path::PathBuf;
use std::sync::OnceLock;
use std::{io, ops, str};
use regex::Regex;
use rustc_hir::def_id::DefId;
use rustc_index::bit_set::BitSet;
use rustc_middle::mir::{
self, BasicBlock, Body, Location, create_dump_file, dump_enabled, graphviz_safe_def_name,
traversal,
};
use rustc_middle::ty::TyCtxt;
use rustc_middle::ty::print::with_no_trimmed_paths;
use rustc_span::symbol::{Symbol, sym};
use tracing::debug;
use {rustc_ast as ast, rustc_graphviz as dot};
use super::fmt::{DebugDiffWithAdapter, DebugWithAdapter, DebugWithContext};
use super::{Analysis, CallReturnPlaces, Direction, Results, ResultsCursor, ResultsVisitor};
use crate::errors::{
DuplicateValuesFor, PathMustEndInFilename, RequiresAnArgument, UnknownFormatter,
};
pub(super) fn write_graphviz_results<'tcx, A>(
tcx: TyCtxt<'tcx>,
body: &Body<'tcx>,
results: &mut Results<'tcx, A>,
pass_name: Option<&'static str>,
) -> std::io::Result<()>
where
A: Analysis<'tcx>,
A::Domain: DebugWithContext<A>,
{
use std::fs;
use std::io::Write;
let def_id = body.source.def_id();
let Ok(attrs) = RustcMirAttrs::parse(tcx, def_id) else {
return Ok(());
};
let file = try {
match attrs.output_path(A::NAME) {
Some(path) => {
debug!("printing dataflow results for {:?} to {}", def_id, path.display());
if let Some(parent) = path.parent() {
fs::create_dir_all(parent)?;
}
fs::File::create_buffered(&path)?
}
None if dump_enabled(tcx, A::NAME, def_id) => {
create_dump_file(tcx, "dot", false, A::NAME, &pass_name.unwrap_or("-----"), body)?
}
_ => return Ok(()),
}
};
let mut file = match file {
Ok(f) => f,
Err(e) => return Err(e),
};
let style = match attrs.formatter {
Some(sym::two_phase) => OutputStyle::BeforeAndAfter,
_ => OutputStyle::AfterOnly,
};
let mut buf = Vec::new();
let graphviz = Formatter::new(body, results, style);
let mut render_opts =
vec![dot::RenderOption::Fontname(tcx.sess.opts.unstable_opts.graphviz_font.clone())];
if tcx.sess.opts.unstable_opts.graphviz_dark_mode {
render_opts.push(dot::RenderOption::DarkTheme);
}
let r = with_no_trimmed_paths!(dot::render_opts(&graphviz, &mut buf, &render_opts));
let lhs = try {
r?;
file.write_all(&buf)?;
};
lhs
}
#[derive(Default)]
struct RustcMirAttrs {
basename_and_suffix: Option<PathBuf>,
formatter: Option<Symbol>,
}
impl RustcMirAttrs {
fn parse(tcx: TyCtxt<'_>, def_id: DefId) -> Result<Self, ()> {
let mut result = Ok(());
let mut ret = RustcMirAttrs::default();
let rustc_mir_attrs = tcx
.get_attrs(def_id, sym::rustc_mir)
.flat_map(|attr| attr.meta_item_list().into_iter().flat_map(|v| v.into_iter()));
for attr in rustc_mir_attrs {
let attr_result = if attr.has_name(sym::borrowck_graphviz_postflow) {
Self::set_field(&mut ret.basename_and_suffix, tcx, &attr, |s| {
let path = PathBuf::from(s.to_string());
match path.file_name() {
Some(_) => Ok(path),
None => {
tcx.dcx().emit_err(PathMustEndInFilename { span: attr.span() });
Err(())
}
}
})
} else if attr.has_name(sym::borrowck_graphviz_format) {
Self::set_field(&mut ret.formatter, tcx, &attr, |s| match s {
sym::gen_kill | sym::two_phase => Ok(s),
_ => {
tcx.dcx().emit_err(UnknownFormatter { span: attr.span() });
Err(())
}
})
} else {
Ok(())
};
result = result.and(attr_result);
}
result.map(|()| ret)
}
fn set_field<T>(
field: &mut Option<T>,
tcx: TyCtxt<'_>,
attr: &ast::MetaItemInner,
mapper: impl FnOnce(Symbol) -> Result<T, ()>,
) -> Result<(), ()> {
if field.is_some() {
tcx.dcx()
.emit_err(DuplicateValuesFor { span: attr.span(), name: attr.name_or_empty() });
return Err(());
}
if let Some(s) = attr.value_str() {
*field = Some(mapper(s)?);
Ok(())
} else {
tcx.dcx()
.emit_err(RequiresAnArgument { span: attr.span(), name: attr.name_or_empty() });
Err(())
}
}
fn output_path(&self, analysis_name: &str) -> Option<PathBuf> {
let mut ret = self.basename_and_suffix.as_ref().cloned()?;
let suffix = ret.file_name().unwrap(); let mut file_name: OsString = analysis_name.into();
file_name.push("_");
file_name.push(suffix);
ret.set_file_name(file_name);
Some(ret)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum OutputStyle {
AfterOnly,
BeforeAndAfter,
}
impl OutputStyle {
fn num_state_columns(&self) -> usize {
match self {
Self::AfterOnly => 1,
Self::BeforeAndAfter => 2,
}
}
}
struct Formatter<'mir, 'tcx, A>
where
A: Analysis<'tcx>,
{
cursor: RefCell<ResultsCursor<'mir, 'tcx, A>>,
style: OutputStyle,
reachable: BitSet<BasicBlock>,
}
impl<'mir, 'tcx, A> Formatter<'mir, 'tcx, A>
where
A: Analysis<'tcx>,
{
fn new(
body: &'mir Body<'tcx>,
results: &'mir mut Results<'tcx, A>,
style: OutputStyle,
) -> Self {
let reachable = traversal::reachable_as_bitset(body);
Formatter { cursor: results.as_results_cursor(body).into(), style, reachable }
}
fn body(&self) -> &'mir Body<'tcx> {
self.cursor.borrow().body()
}
}
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
struct CfgEdge {
source: BasicBlock,
index: usize,
}
fn dataflow_successors(body: &Body<'_>, bb: BasicBlock) -> Vec<CfgEdge> {
body[bb]
.terminator()
.successors()
.enumerate()
.map(|(index, _)| CfgEdge { source: bb, index })
.collect()
}
impl<'tcx, A> dot::Labeller<'_> for Formatter<'_, 'tcx, A>
where
A: Analysis<'tcx>,
A::Domain: DebugWithContext<A>,
{
type Node = BasicBlock;
type Edge = CfgEdge;
fn graph_id(&self) -> dot::Id<'_> {
let name = graphviz_safe_def_name(self.body().source.def_id());
dot::Id::new(format!("graph_for_def_id_{name}")).unwrap()
}
fn node_id(&self, n: &Self::Node) -> dot::Id<'_> {
dot::Id::new(format!("bb_{}", n.index())).unwrap()
}
fn node_label(&self, block: &Self::Node) -> dot::LabelText<'_> {
let mut cursor = self.cursor.borrow_mut();
let mut fmt =
BlockFormatter { cursor: &mut cursor, style: self.style, bg: Background::Light };
let label = fmt.write_node_label(*block).unwrap();
dot::LabelText::html(String::from_utf8(label).unwrap())
}
fn node_shape(&self, _n: &Self::Node) -> Option<dot::LabelText<'_>> {
Some(dot::LabelText::label("none"))
}
fn edge_label(&self, e: &Self::Edge) -> dot::LabelText<'_> {
let label = &self.body()[e.source].terminator().kind.fmt_successor_labels()[e.index];
dot::LabelText::label(label.clone())
}
}
impl<'tcx, A> dot::GraphWalk<'_> for Formatter<'_, 'tcx, A>
where
A: Analysis<'tcx>,
{
type Node = BasicBlock;
type Edge = CfgEdge;
fn nodes(&self) -> dot::Nodes<'_, Self::Node> {
self.body()
.basic_blocks
.indices()
.filter(|&idx| self.reachable.contains(idx))
.collect::<Vec<_>>()
.into()
}
fn edges(&self) -> dot::Edges<'_, Self::Edge> {
let body = self.body();
body.basic_blocks
.indices()
.flat_map(|bb| dataflow_successors(body, bb))
.collect::<Vec<_>>()
.into()
}
fn source(&self, edge: &Self::Edge) -> Self::Node {
edge.source
}
fn target(&self, edge: &Self::Edge) -> Self::Node {
self.body()[edge.source].terminator().successors().nth(edge.index).unwrap()
}
}
struct BlockFormatter<'a, 'mir, 'tcx, A>
where
A: Analysis<'tcx>,
{
cursor: &'a mut ResultsCursor<'mir, 'tcx, A>,
bg: Background,
style: OutputStyle,
}
impl<'tcx, A> BlockFormatter<'_, '_, 'tcx, A>
where
A: Analysis<'tcx>,
A::Domain: DebugWithContext<A>,
{
const HEADER_COLOR: &'static str = "#a0a0a0";
fn toggle_background(&mut self) -> Background {
let bg = self.bg;
self.bg = !bg;
bg
}
fn write_node_label(&mut self, block: BasicBlock) -> io::Result<Vec<u8>> {
use std::io::Write;
let mut v = vec![];
let w = &mut v;
let table_fmt = concat!(
" border=\"1\"",
" cellborder=\"1\"",
" cellspacing=\"0\"",
" cellpadding=\"3\"",
" sides=\"rb\"",
);
write!(w, r#"<table{table_fmt}>"#)?;
match self.style {
OutputStyle::AfterOnly => self.write_block_header_simple(w, block)?,
OutputStyle::BeforeAndAfter => {
self.write_block_header_with_state_columns(w, block, &["BEFORE", "AFTER"])?
}
}
self.bg = Background::Light;
self.cursor.seek_to_block_start(block);
let block_start_state = self.cursor.get().clone();
self.write_row_with_full_state(w, "", "(on start)")?;
self.write_statements_and_terminator(w, block)?;
let terminator = self.cursor.body()[block].terminator();
self.cursor.seek_to_block_end(block);
if self.cursor.get() != &block_start_state || A::Direction::IS_BACKWARD {
let after_terminator_name = match terminator.kind {
mir::TerminatorKind::Call { target: Some(_), .. } => "(on unwind)",
_ => "(on end)",
};
self.write_row_with_full_state(w, "", after_terminator_name)?;
}
match terminator.kind {
mir::TerminatorKind::Call { destination, .. } => {
self.write_row(w, "", "(on successful return)", |this, w, fmt| {
let state_on_unwind = this.cursor.get().clone();
this.cursor.apply_custom_effect(|analysis, state| {
analysis.apply_call_return_effect(
state,
block,
CallReturnPlaces::Call(destination),
);
});
write!(
w,
r#"<td balign="left" colspan="{colspan}" {fmt} align="left">{diff}</td>"#,
colspan = this.style.num_state_columns(),
fmt = fmt,
diff = diff_pretty(
this.cursor.get(),
&state_on_unwind,
this.cursor.analysis()
),
)
})?;
}
mir::TerminatorKind::Yield { resume, resume_arg, .. } => {
self.write_row(w, "", "(on yield resume)", |this, w, fmt| {
let state_on_coroutine_drop = this.cursor.get().clone();
this.cursor.apply_custom_effect(|analysis, state| {
analysis.apply_call_return_effect(
state,
resume,
CallReturnPlaces::Yield(resume_arg),
);
});
write!(
w,
r#"<td balign="left" colspan="{colspan}" {fmt} align="left">{diff}</td>"#,
colspan = this.style.num_state_columns(),
fmt = fmt,
diff = diff_pretty(
this.cursor.get(),
&state_on_coroutine_drop,
this.cursor.analysis()
),
)
})?;
}
mir::TerminatorKind::InlineAsm { ref targets, ref operands, .. }
if !targets.is_empty() =>
{
self.write_row(w, "", "(on successful return)", |this, w, fmt| {
let state_on_unwind = this.cursor.get().clone();
this.cursor.apply_custom_effect(|analysis, state| {
analysis.apply_call_return_effect(
state,
block,
CallReturnPlaces::InlineAsm(operands),
);
});
write!(
w,
r#"<td balign="left" colspan="{colspan}" {fmt} align="left">{diff}</td>"#,
colspan = this.style.num_state_columns(),
fmt = fmt,
diff = diff_pretty(
this.cursor.get(),
&state_on_unwind,
this.cursor.analysis()
),
)
})?;
}
_ => {}
};
write!(w, "</table>")?;
Ok(v)
}
fn write_block_header_simple(
&mut self,
w: &mut impl io::Write,
block: BasicBlock,
) -> io::Result<()> {
write!(
w,
concat!("<tr>", r#"<td colspan="3" sides="tl">bb{block_id}</td>"#, "</tr>",),
block_id = block.index(),
)?;
write!(
w,
concat!(
"<tr>",
r#"<td colspan="2" {fmt}>MIR</td>"#,
r#"<td {fmt}>STATE</td>"#,
"</tr>",
),
fmt = format!("bgcolor=\"{}\" sides=\"tl\"", Self::HEADER_COLOR),
)
}
fn write_block_header_with_state_columns(
&mut self,
w: &mut impl io::Write,
block: BasicBlock,
state_column_names: &[&str],
) -> io::Result<()> {
write!(
w,
concat!(
"<tr>",
r#"<td {fmt} colspan="2">bb{block_id}</td>"#,
r#"<td {fmt} colspan="{num_state_cols}">STATE</td>"#,
"</tr>",
),
fmt = "sides=\"tl\"",
num_state_cols = state_column_names.len(),
block_id = block.index(),
)?;
let fmt = format!("bgcolor=\"{}\" sides=\"tl\"", Self::HEADER_COLOR);
write!(w, concat!("<tr>", r#"<td colspan="2" {fmt}>MIR</td>"#,), fmt = fmt,)?;
for name in state_column_names {
write!(w, "<td {fmt}>{name}</td>")?;
}
write!(w, "</tr>")
}
fn write_statements_and_terminator(
&mut self,
w: &mut impl io::Write,
block: BasicBlock,
) -> io::Result<()> {
let diffs = StateDiffCollector::run(
self.cursor.body(),
block,
self.cursor.mut_results(),
self.style,
);
let mut diffs_before = diffs.before.map(|v| v.into_iter());
let mut diffs_after = diffs.after.into_iter();
let next_in_dataflow_order = |it: &mut std::vec::IntoIter<_>| {
if A::Direction::IS_FORWARD { it.next().unwrap() } else { it.next_back().unwrap() }
};
for (i, statement) in self.cursor.body()[block].statements.iter().enumerate() {
let statement_str = format!("{statement:?}");
let index_str = format!("{i}");
let after = next_in_dataflow_order(&mut diffs_after);
let before = diffs_before.as_mut().map(next_in_dataflow_order);
self.write_row(w, &index_str, &statement_str, |_this, w, fmt| {
if let Some(before) = before {
write!(w, r#"<td {fmt} align="left">{before}</td>"#)?;
}
write!(w, r#"<td {fmt} align="left">{after}</td>"#)
})?;
}
let after = next_in_dataflow_order(&mut diffs_after);
let before = diffs_before.as_mut().map(next_in_dataflow_order);
assert!(diffs_after.is_empty());
assert!(diffs_before.as_ref().map_or(true, ExactSizeIterator::is_empty));
let terminator = self.cursor.body()[block].terminator();
let mut terminator_str = String::new();
terminator.kind.fmt_head(&mut terminator_str).unwrap();
self.write_row(w, "T", &terminator_str, |_this, w, fmt| {
if let Some(before) = before {
write!(w, r#"<td {fmt} align="left">{before}</td>"#)?;
}
write!(w, r#"<td {fmt} align="left">{after}</td>"#)
})
}
fn write_row<W: io::Write>(
&mut self,
w: &mut W,
i: &str,
mir: &str,
f: impl FnOnce(&mut Self, &mut W, &str) -> io::Result<()>,
) -> io::Result<()> {
let bg = self.toggle_background();
let valign = if mir.starts_with("(on ") && mir != "(on entry)" { "bottom" } else { "top" };
let fmt = format!("valign=\"{}\" sides=\"tl\" {}", valign, bg.attr());
write!(
w,
concat!(
"<tr>",
r#"<td {fmt} align="right">{i}</td>"#,
r#"<td {fmt} align="left">{mir}</td>"#,
),
i = i,
fmt = fmt,
mir = dot::escape_html(mir),
)?;
f(self, w, &fmt)?;
write!(w, "</tr>")
}
fn write_row_with_full_state(
&mut self,
w: &mut impl io::Write,
i: &str,
mir: &str,
) -> io::Result<()> {
self.write_row(w, i, mir, |this, w, fmt| {
let state = this.cursor.get();
let analysis = this.cursor.analysis();
write!(
w,
r#"<td colspan="{colspan}" {fmt} align="left">{state}</td>"#,
colspan = this.style.num_state_columns(),
fmt = fmt,
state = dot::escape_html(&format!("{:?}", DebugWithAdapter {
this: state,
ctxt: analysis
})),
)
})
}
}
struct StateDiffCollector<D> {
prev_state: D,
before: Option<Vec<String>>,
after: Vec<String>,
}
impl<D> StateDiffCollector<D> {
fn run<'tcx, A>(
body: &Body<'tcx>,
block: BasicBlock,
results: &mut Results<'tcx, A>,
style: OutputStyle,
) -> Self
where
A: Analysis<'tcx, Domain = D>,
D: DebugWithContext<A>,
{
let mut collector = StateDiffCollector {
prev_state: results.analysis.bottom_value(body),
after: vec![],
before: (style == OutputStyle::BeforeAndAfter).then_some(vec![]),
};
results.visit_with(body, std::iter::once(block), &mut collector);
collector
}
}
impl<'tcx, A> ResultsVisitor<'_, 'tcx, A> for StateDiffCollector<A::Domain>
where
A: Analysis<'tcx>,
A::Domain: DebugWithContext<A>,
{
fn visit_block_start(&mut self, state: &A::Domain) {
if A::Direction::IS_FORWARD {
self.prev_state.clone_from(state);
}
}
fn visit_block_end(&mut self, state: &A::Domain) {
if A::Direction::IS_BACKWARD {
self.prev_state.clone_from(state);
}
}
fn visit_after_early_statement_effect(
&mut self,
results: &mut Results<'tcx, A>,
state: &A::Domain,
_statement: &mir::Statement<'tcx>,
_location: Location,
) {
if let Some(before) = self.before.as_mut() {
before.push(diff_pretty(state, &self.prev_state, &results.analysis));
self.prev_state.clone_from(state)
}
}
fn visit_after_primary_statement_effect(
&mut self,
results: &mut Results<'tcx, A>,
state: &A::Domain,
_statement: &mir::Statement<'tcx>,
_location: Location,
) {
self.after.push(diff_pretty(state, &self.prev_state, &results.analysis));
self.prev_state.clone_from(state)
}
fn visit_after_early_terminator_effect(
&mut self,
results: &mut Results<'tcx, A>,
state: &A::Domain,
_terminator: &mir::Terminator<'tcx>,
_location: Location,
) {
if let Some(before) = self.before.as_mut() {
before.push(diff_pretty(state, &self.prev_state, &results.analysis));
self.prev_state.clone_from(state)
}
}
fn visit_after_primary_terminator_effect(
&mut self,
results: &mut Results<'tcx, A>,
state: &A::Domain,
_terminator: &mir::Terminator<'tcx>,
_location: Location,
) {
self.after.push(diff_pretty(state, &self.prev_state, &results.analysis));
self.prev_state.clone_from(state)
}
}
macro_rules! regex {
($re:literal $(,)?) => {{
static RE: OnceLock<regex::Regex> = OnceLock::new();
RE.get_or_init(|| Regex::new($re).unwrap())
}};
}
fn diff_pretty<T, C>(new: T, old: T, ctxt: &C) -> String
where
T: DebugWithContext<C>,
{
if new == old {
return String::new();
}
let re = regex!("\t?\u{001f}([+-])");
let raw_diff = format!("{:#?}", DebugDiffWithAdapter { new, old, ctxt });
let raw_diff = raw_diff.replace('\n', r#"<br align="left"/>"#);
let mut inside_font_tag = false;
let html_diff = re.replace_all(&raw_diff, |captures: ®ex::Captures<'_>| {
let mut ret = String::new();
if inside_font_tag {
ret.push_str(r#"</font>"#);
}
let tag = match &captures[1] {
"+" => r#"<font color="darkgreen">+"#,
"-" => r#"<font color="red">-"#,
_ => unreachable!(),
};
inside_font_tag = true;
ret.push_str(tag);
ret
});
let Cow::Owned(mut html_diff) = html_diff else {
return raw_diff;
};
if inside_font_tag {
html_diff.push_str("</font>");
}
html_diff
}
#[derive(Clone, Copy)]
enum Background {
Light,
Dark,
}
impl Background {
fn attr(self) -> &'static str {
match self {
Self::Dark => "bgcolor=\"#f0f0f0\"",
Self::Light => "",
}
}
}
impl ops::Not for Background {
type Output = Self;
fn not(self) -> Self {
match self {
Self::Light => Self::Dark,
Self::Dark => Self::Light,
}
}
}