rustc_mir_dataflow/framework/
graphviz.rs

1//! A helpful diagram for debugging dataflow problems.
2
3use std::borrow::Cow;
4use std::cell::RefCell;
5use std::ffi::OsString;
6use std::path::PathBuf;
7use std::sync::OnceLock;
8use std::{io, ops, str};
9
10use regex::Regex;
11use rustc_hir::def_id::DefId;
12use rustc_index::bit_set::DenseBitSet;
13use rustc_middle::mir::{
14    self, BasicBlock, Body, Location, create_dump_file, dump_enabled, graphviz_safe_def_name,
15    traversal,
16};
17use rustc_middle::ty::TyCtxt;
18use rustc_middle::ty::print::with_no_trimmed_paths;
19use rustc_span::{Symbol, sym};
20use tracing::debug;
21use {rustc_ast as ast, rustc_graphviz as dot};
22
23use super::fmt::{DebugDiffWithAdapter, DebugWithAdapter, DebugWithContext};
24use super::{
25    Analysis, CallReturnPlaces, Direction, Results, ResultsCursor, ResultsVisitor, visit_results,
26};
27use crate::errors::{
28    DuplicateValuesFor, PathMustEndInFilename, RequiresAnArgument, UnknownFormatter,
29};
30
31/// Writes a DOT file containing the results of a dataflow analysis if the user requested it via
32/// `rustc_mir` attributes and `-Z dump-mir-dataflow`. The `Result` in and the `Results` out are
33/// the same.
34pub(super) fn write_graphviz_results<'tcx, A>(
35    tcx: TyCtxt<'tcx>,
36    body: &Body<'tcx>,
37    analysis: &mut A,
38    results: &Results<A::Domain>,
39    pass_name: Option<&'static str>,
40) -> std::io::Result<()>
41where
42    A: Analysis<'tcx>,
43    A::Domain: DebugWithContext<A>,
44{
45    use std::fs;
46    use std::io::Write;
47
48    let def_id = body.source.def_id();
49    let Ok(attrs) = RustcMirAttrs::parse(tcx, def_id) else {
50        // Invalid `rustc_mir` attrs are reported in `RustcMirAttrs::parse`
51        return Ok(());
52    };
53
54    let file = try {
55        match attrs.output_path(A::NAME) {
56            Some(path) => {
57                debug!("printing dataflow results for {:?} to {}", def_id, path.display());
58                if let Some(parent) = path.parent() {
59                    fs::create_dir_all(parent)?;
60                }
61                fs::File::create_buffered(&path)?
62            }
63
64            None if dump_enabled(tcx, A::NAME, def_id) => {
65                create_dump_file(tcx, "dot", false, A::NAME, &pass_name.unwrap_or("-----"), body)?
66            }
67
68            _ => return Ok(()),
69        }
70    };
71    let mut file = match file {
72        Ok(f) => f,
73        Err(e) => return Err(e),
74    };
75
76    let style = match attrs.formatter {
77        Some(sym::two_phase) => OutputStyle::BeforeAndAfter,
78        _ => OutputStyle::AfterOnly,
79    };
80
81    let mut buf = Vec::new();
82
83    let graphviz = Formatter::new(body, analysis, results, style);
84    let mut render_opts =
85        vec![dot::RenderOption::Fontname(tcx.sess.opts.unstable_opts.graphviz_font.clone())];
86    if tcx.sess.opts.unstable_opts.graphviz_dark_mode {
87        render_opts.push(dot::RenderOption::DarkTheme);
88    }
89    let r = with_no_trimmed_paths!(dot::render_opts(&graphviz, &mut buf, &render_opts));
90
91    let lhs = try {
92        r?;
93        file.write_all(&buf)?;
94    };
95
96    lhs
97}
98
99#[derive(Default)]
100struct RustcMirAttrs {
101    basename_and_suffix: Option<PathBuf>,
102    formatter: Option<Symbol>,
103}
104
105impl RustcMirAttrs {
106    fn parse(tcx: TyCtxt<'_>, def_id: DefId) -> Result<Self, ()> {
107        let mut result = Ok(());
108        let mut ret = RustcMirAttrs::default();
109
110        let rustc_mir_attrs = tcx
111            .get_attrs(def_id, sym::rustc_mir)
112            .flat_map(|attr| attr.meta_item_list().into_iter().flat_map(|v| v.into_iter()));
113
114        for attr in rustc_mir_attrs {
115            let attr_result = match attr.name() {
116                Some(name @ sym::borrowck_graphviz_postflow) => {
117                    Self::set_field(&mut ret.basename_and_suffix, tcx, name, &attr, |s| {
118                        let path = PathBuf::from(s.to_string());
119                        match path.file_name() {
120                            Some(_) => Ok(path),
121                            None => {
122                                tcx.dcx().emit_err(PathMustEndInFilename { span: attr.span() });
123                                Err(())
124                            }
125                        }
126                    })
127                }
128                Some(name @ sym::borrowck_graphviz_format) => {
129                    Self::set_field(&mut ret.formatter, tcx, name, &attr, |s| match s {
130                        sym::two_phase => Ok(s),
131                        _ => {
132                            tcx.dcx().emit_err(UnknownFormatter { span: attr.span() });
133                            Err(())
134                        }
135                    })
136                }
137                _ => Ok(()),
138            };
139
140            result = result.and(attr_result);
141        }
142
143        result.map(|()| ret)
144    }
145
146    fn set_field<T>(
147        field: &mut Option<T>,
148        tcx: TyCtxt<'_>,
149        name: Symbol,
150        attr: &ast::MetaItemInner,
151        mapper: impl FnOnce(Symbol) -> Result<T, ()>,
152    ) -> Result<(), ()> {
153        if field.is_some() {
154            tcx.dcx().emit_err(DuplicateValuesFor { span: attr.span(), name });
155
156            return Err(());
157        }
158
159        if let Some(s) = attr.value_str() {
160            *field = Some(mapper(s)?);
161            Ok(())
162        } else {
163            tcx.dcx()
164                .emit_err(RequiresAnArgument { span: attr.span(), name: attr.name().unwrap() });
165            Err(())
166        }
167    }
168
169    /// Returns the path where dataflow results should be written, or `None`
170    /// `borrowck_graphviz_postflow` was not specified.
171    ///
172    /// This performs the following transformation to the argument of `borrowck_graphviz_postflow`:
173    ///
174    /// "path/suffix.dot" -> "path/analysis_name_suffix.dot"
175    fn output_path(&self, analysis_name: &str) -> Option<PathBuf> {
176        let mut ret = self.basename_and_suffix.as_ref().cloned()?;
177        let suffix = ret.file_name().unwrap(); // Checked when parsing attrs
178
179        let mut file_name: OsString = analysis_name.into();
180        file_name.push("_");
181        file_name.push(suffix);
182        ret.set_file_name(file_name);
183
184        Some(ret)
185    }
186}
187
188#[derive(Clone, Copy, Debug, PartialEq, Eq)]
189enum OutputStyle {
190    AfterOnly,
191    BeforeAndAfter,
192}
193
194impl OutputStyle {
195    fn num_state_columns(&self) -> usize {
196        match self {
197            Self::AfterOnly => 1,
198            Self::BeforeAndAfter => 2,
199        }
200    }
201}
202
203struct Formatter<'mir, 'tcx, A>
204where
205    A: Analysis<'tcx>,
206{
207    body: &'mir Body<'tcx>,
208    // The `RefCell` is used because `<Formatter as Labeller>::node_label`
209    // takes `&self`, but it needs to modify the analysis. This is also the
210    // reason for the `Formatter`/`BlockFormatter` split; `BlockFormatter` has
211    // the operations that involve the mutation, i.e. within the `borrow_mut`.
212    analysis: RefCell<&'mir mut A>,
213    results: &'mir Results<A::Domain>,
214    style: OutputStyle,
215    reachable: DenseBitSet<BasicBlock>,
216}
217
218impl<'mir, 'tcx, A> Formatter<'mir, 'tcx, A>
219where
220    A: Analysis<'tcx>,
221{
222    fn new(
223        body: &'mir Body<'tcx>,
224        analysis: &'mir mut A,
225        results: &'mir Results<A::Domain>,
226        style: OutputStyle,
227    ) -> Self {
228        let reachable = traversal::reachable_as_bitset(body);
229        Formatter { body, analysis: analysis.into(), results, style, reachable }
230    }
231}
232
233/// A pair of a basic block and an index into that basic blocks `successors`.
234#[derive(Copy, Clone, PartialEq, Eq, Debug)]
235struct CfgEdge {
236    source: BasicBlock,
237    index: usize,
238}
239
240fn dataflow_successors(body: &Body<'_>, bb: BasicBlock) -> Vec<CfgEdge> {
241    body[bb]
242        .terminator()
243        .successors()
244        .enumerate()
245        .map(|(index, _)| CfgEdge { source: bb, index })
246        .collect()
247}
248
249impl<'tcx, A> dot::Labeller<'_> for Formatter<'_, 'tcx, A>
250where
251    A: Analysis<'tcx>,
252    A::Domain: DebugWithContext<A>,
253{
254    type Node = BasicBlock;
255    type Edge = CfgEdge;
256
257    fn graph_id(&self) -> dot::Id<'_> {
258        let name = graphviz_safe_def_name(self.body.source.def_id());
259        dot::Id::new(format!("graph_for_def_id_{name}")).unwrap()
260    }
261
262    fn node_id(&self, n: &Self::Node) -> dot::Id<'_> {
263        dot::Id::new(format!("bb_{}", n.index())).unwrap()
264    }
265
266    fn node_label(&self, block: &Self::Node) -> dot::LabelText<'_> {
267        let analysis = &mut **self.analysis.borrow_mut();
268
269        let diffs = StateDiffCollector::run(self.body, *block, analysis, self.results, self.style);
270
271        let mut fmt = BlockFormatter {
272            cursor: ResultsCursor::new_borrowing(self.body, analysis, self.results),
273            style: self.style,
274            bg: Background::Light,
275        };
276        let label = fmt.write_node_label(*block, diffs).unwrap();
277
278        dot::LabelText::html(String::from_utf8(label).unwrap())
279    }
280
281    fn node_shape(&self, _n: &Self::Node) -> Option<dot::LabelText<'_>> {
282        Some(dot::LabelText::label("none"))
283    }
284
285    fn edge_label(&self, e: &Self::Edge) -> dot::LabelText<'_> {
286        let label = &self.body[e.source].terminator().kind.fmt_successor_labels()[e.index];
287        dot::LabelText::label(label.clone())
288    }
289}
290
291impl<'tcx, A> dot::GraphWalk<'_> for Formatter<'_, 'tcx, A>
292where
293    A: Analysis<'tcx>,
294{
295    type Node = BasicBlock;
296    type Edge = CfgEdge;
297
298    fn nodes(&self) -> dot::Nodes<'_, Self::Node> {
299        self.body
300            .basic_blocks
301            .indices()
302            .filter(|&idx| self.reachable.contains(idx))
303            .collect::<Vec<_>>()
304            .into()
305    }
306
307    fn edges(&self) -> dot::Edges<'_, Self::Edge> {
308        self.body
309            .basic_blocks
310            .indices()
311            .flat_map(|bb| dataflow_successors(self.body, bb))
312            .collect::<Vec<_>>()
313            .into()
314    }
315
316    fn source(&self, edge: &Self::Edge) -> Self::Node {
317        edge.source
318    }
319
320    fn target(&self, edge: &Self::Edge) -> Self::Node {
321        self.body[edge.source].terminator().successors().nth(edge.index).unwrap()
322    }
323}
324
325struct BlockFormatter<'mir, 'tcx, A>
326where
327    A: Analysis<'tcx>,
328{
329    cursor: ResultsCursor<'mir, 'tcx, A>,
330    bg: Background,
331    style: OutputStyle,
332}
333
334impl<'tcx, A> BlockFormatter<'_, 'tcx, A>
335where
336    A: Analysis<'tcx>,
337    A::Domain: DebugWithContext<A>,
338{
339    const HEADER_COLOR: &'static str = "#a0a0a0";
340
341    fn toggle_background(&mut self) -> Background {
342        let bg = self.bg;
343        self.bg = !bg;
344        bg
345    }
346
347    fn write_node_label(
348        &mut self,
349        block: BasicBlock,
350        diffs: StateDiffCollector<A::Domain>,
351    ) -> io::Result<Vec<u8>> {
352        use std::io::Write;
353
354        //   Sample output:
355        //   +-+-----------------------------------------------+
356        // A |                      bb4                        |
357        //   +-+----------------------------------+------------+
358        // B |                MIR                 |   STATE    |
359        //   +-+----------------------------------+------------+
360        // C | | (on entry)                       | {_0,_2,_3} |
361        //   +-+----------------------------------+------------+
362        // D |0| StorageLive(_7)                  |            |
363        //   +-+----------------------------------+------------+
364        //   |1| StorageLive(_8)                  |            |
365        //   +-+----------------------------------+------------+
366        //   |2| _8 = &mut _1                     | +_8        |
367        //   +-+----------------------------------+------------+
368        // E |T| _4 = const Foo::twiddle(move _2) | -_2        |
369        //   +-+----------------------------------+------------+
370        // F | | (on unwind)                      | {_0,_3,_8} |
371        //   +-+----------------------------------+------------+
372        //   | | (on successful return)           | +_4        |
373        //   +-+----------------------------------+------------+
374
375        // N.B., Some attributes (`align`, `balign`) are repeated on parent elements and their
376        // children. This is because `xdot` seemed to have a hard time correctly propagating
377        // attributes. Make sure to test the output before trying to remove the redundancy.
378        // Notably, `align` was found to have no effect when applied only to <table>.
379
380        let mut v = vec![];
381        let w = &mut v;
382
383        let table_fmt = concat!(
384            " border=\"1\"",
385            " cellborder=\"1\"",
386            " cellspacing=\"0\"",
387            " cellpadding=\"3\"",
388            " sides=\"rb\"",
389        );
390        write!(w, r#"<table{table_fmt}>"#)?;
391
392        // A + B: Block header
393        match self.style {
394            OutputStyle::AfterOnly => self.write_block_header_simple(w, block)?,
395            OutputStyle::BeforeAndAfter => {
396                self.write_block_header_with_state_columns(w, block, &["BEFORE", "AFTER"])?
397            }
398        }
399
400        // C: State at start of block
401        self.bg = Background::Light;
402        self.cursor.seek_to_block_start(block);
403        let block_start_state = self.cursor.get().clone();
404        self.write_row_with_full_state(w, "", "(on start)")?;
405
406        // D + E: Statement and terminator transfer functions
407        self.write_statements_and_terminator(w, block, diffs)?;
408
409        // F: State at end of block
410
411        let terminator = self.cursor.body()[block].terminator();
412
413        // Write the full dataflow state immediately after the terminator if it differs from the
414        // state at block entry.
415        self.cursor.seek_to_block_end(block);
416        if self.cursor.get() != &block_start_state || A::Direction::IS_BACKWARD {
417            let after_terminator_name = match terminator.kind {
418                mir::TerminatorKind::Call { target: Some(_), .. } => "(on unwind)",
419                _ => "(on end)",
420            };
421
422            self.write_row_with_full_state(w, "", after_terminator_name)?;
423        }
424
425        // Write any changes caused by terminator-specific effects.
426        //
427        // FIXME: These should really be printed as part of each outgoing edge rather than the node
428        // for the basic block itself. That way, we could display terminator-specific effects for
429        // backward dataflow analyses as well as effects for `SwitchInt` terminators.
430        match terminator.kind {
431            mir::TerminatorKind::Call { destination, .. } => {
432                self.write_row(w, "", "(on successful return)", |this, w, fmt| {
433                    let state_on_unwind = this.cursor.get().clone();
434                    this.cursor.apply_custom_effect(|analysis, state| {
435                        analysis.apply_call_return_effect(
436                            state,
437                            block,
438                            CallReturnPlaces::Call(destination),
439                        );
440                    });
441
442                    write!(
443                        w,
444                        r#"<td balign="left" colspan="{colspan}" {fmt} align="left">{diff}</td>"#,
445                        colspan = this.style.num_state_columns(),
446                        fmt = fmt,
447                        diff = diff_pretty(
448                            this.cursor.get(),
449                            &state_on_unwind,
450                            this.cursor.analysis()
451                        ),
452                    )
453                })?;
454            }
455
456            mir::TerminatorKind::Yield { resume, resume_arg, .. } => {
457                self.write_row(w, "", "(on yield resume)", |this, w, fmt| {
458                    let state_on_coroutine_drop = this.cursor.get().clone();
459                    this.cursor.apply_custom_effect(|analysis, state| {
460                        analysis.apply_call_return_effect(
461                            state,
462                            resume,
463                            CallReturnPlaces::Yield(resume_arg),
464                        );
465                    });
466
467                    write!(
468                        w,
469                        r#"<td balign="left" colspan="{colspan}" {fmt} align="left">{diff}</td>"#,
470                        colspan = this.style.num_state_columns(),
471                        fmt = fmt,
472                        diff = diff_pretty(
473                            this.cursor.get(),
474                            &state_on_coroutine_drop,
475                            this.cursor.analysis()
476                        ),
477                    )
478                })?;
479            }
480
481            mir::TerminatorKind::InlineAsm { ref targets, ref operands, .. }
482                if !targets.is_empty() =>
483            {
484                self.write_row(w, "", "(on successful return)", |this, w, fmt| {
485                    let state_on_unwind = this.cursor.get().clone();
486                    this.cursor.apply_custom_effect(|analysis, state| {
487                        analysis.apply_call_return_effect(
488                            state,
489                            block,
490                            CallReturnPlaces::InlineAsm(operands),
491                        );
492                    });
493
494                    write!(
495                        w,
496                        r#"<td balign="left" colspan="{colspan}" {fmt} align="left">{diff}</td>"#,
497                        colspan = this.style.num_state_columns(),
498                        fmt = fmt,
499                        diff = diff_pretty(
500                            this.cursor.get(),
501                            &state_on_unwind,
502                            this.cursor.analysis()
503                        ),
504                    )
505                })?;
506            }
507
508            _ => {}
509        };
510
511        write!(w, "</table>")?;
512
513        Ok(v)
514    }
515
516    fn write_block_header_simple(
517        &mut self,
518        w: &mut impl io::Write,
519        block: BasicBlock,
520    ) -> io::Result<()> {
521        //   +-------------------------------------------------+
522        // A |                      bb4                        |
523        //   +-----------------------------------+-------------+
524        // B |                MIR                |    STATE    |
525        //   +-+---------------------------------+-------------+
526        //   | |              ...                |             |
527
528        // A
529        write!(
530            w,
531            concat!("<tr>", r#"<td colspan="3" sides="tl">bb{block_id}</td>"#, "</tr>",),
532            block_id = block.index(),
533        )?;
534
535        // B
536        write!(
537            w,
538            concat!(
539                "<tr>",
540                r#"<td colspan="2" {fmt}>MIR</td>"#,
541                r#"<td {fmt}>STATE</td>"#,
542                "</tr>",
543            ),
544            fmt = format!("bgcolor=\"{}\" sides=\"tl\"", Self::HEADER_COLOR),
545        )
546    }
547
548    fn write_block_header_with_state_columns(
549        &mut self,
550        w: &mut impl io::Write,
551        block: BasicBlock,
552        state_column_names: &[&str],
553    ) -> io::Result<()> {
554        //   +------------------------------------+-------------+
555        // A |                bb4                 |    STATE    |
556        //   +------------------------------------+------+------+
557        // B |                MIR                 |  GEN | KILL |
558        //   +-+----------------------------------+------+------+
559        //   | |              ...                 |      |      |
560
561        // A
562        write!(
563            w,
564            concat!(
565                "<tr>",
566                r#"<td {fmt} colspan="2">bb{block_id}</td>"#,
567                r#"<td {fmt} colspan="{num_state_cols}">STATE</td>"#,
568                "</tr>",
569            ),
570            fmt = "sides=\"tl\"",
571            num_state_cols = state_column_names.len(),
572            block_id = block.index(),
573        )?;
574
575        // B
576        let fmt = format!("bgcolor=\"{}\" sides=\"tl\"", Self::HEADER_COLOR);
577        write!(w, concat!("<tr>", r#"<td colspan="2" {fmt}>MIR</td>"#,), fmt = fmt,)?;
578
579        for name in state_column_names {
580            write!(w, "<td {fmt}>{name}</td>")?;
581        }
582
583        write!(w, "</tr>")
584    }
585
586    fn write_statements_and_terminator(
587        &mut self,
588        w: &mut impl io::Write,
589        block: BasicBlock,
590        diffs: StateDiffCollector<A::Domain>,
591    ) -> io::Result<()> {
592        let mut diffs_before = diffs.before.map(|v| v.into_iter());
593        let mut diffs_after = diffs.after.into_iter();
594
595        let next_in_dataflow_order = |it: &mut std::vec::IntoIter<_>| {
596            if A::Direction::IS_FORWARD { it.next().unwrap() } else { it.next_back().unwrap() }
597        };
598
599        for (i, statement) in self.cursor.body()[block].statements.iter().enumerate() {
600            let statement_str = format!("{statement:?}");
601            let index_str = format!("{i}");
602
603            let after = next_in_dataflow_order(&mut diffs_after);
604            let before = diffs_before.as_mut().map(next_in_dataflow_order);
605
606            self.write_row(w, &index_str, &statement_str, |_this, w, fmt| {
607                if let Some(before) = before {
608                    write!(w, r#"<td {fmt} align="left">{before}</td>"#)?;
609                }
610
611                write!(w, r#"<td {fmt} align="left">{after}</td>"#)
612            })?;
613        }
614
615        let after = next_in_dataflow_order(&mut diffs_after);
616        let before = diffs_before.as_mut().map(next_in_dataflow_order);
617
618        assert!(diffs_after.is_empty());
619        assert!(diffs_before.as_ref().is_none_or(ExactSizeIterator::is_empty));
620
621        let terminator = self.cursor.body()[block].terminator();
622        let mut terminator_str = String::new();
623        terminator.kind.fmt_head(&mut terminator_str).unwrap();
624
625        self.write_row(w, "T", &terminator_str, |_this, w, fmt| {
626            if let Some(before) = before {
627                write!(w, r#"<td {fmt} align="left">{before}</td>"#)?;
628            }
629
630            write!(w, r#"<td {fmt} align="left">{after}</td>"#)
631        })
632    }
633
634    /// Write a row with the given index and MIR, using the function argument to fill in the
635    /// "STATE" column(s).
636    fn write_row<W: io::Write>(
637        &mut self,
638        w: &mut W,
639        i: &str,
640        mir: &str,
641        f: impl FnOnce(&mut Self, &mut W, &str) -> io::Result<()>,
642    ) -> io::Result<()> {
643        let bg = self.toggle_background();
644        let valign = if mir.starts_with("(on ") && mir != "(on entry)" { "bottom" } else { "top" };
645
646        let fmt = format!("valign=\"{}\" sides=\"tl\" {}", valign, bg.attr());
647
648        write!(
649            w,
650            concat!(
651                "<tr>",
652                r#"<td {fmt} align="right">{i}</td>"#,
653                r#"<td {fmt} align="left">{mir}</td>"#,
654            ),
655            i = i,
656            fmt = fmt,
657            mir = dot::escape_html(mir),
658        )?;
659
660        f(self, w, &fmt)?;
661        write!(w, "</tr>")
662    }
663
664    fn write_row_with_full_state(
665        &mut self,
666        w: &mut impl io::Write,
667        i: &str,
668        mir: &str,
669    ) -> io::Result<()> {
670        self.write_row(w, i, mir, |this, w, fmt| {
671            let state = this.cursor.get();
672            let analysis = this.cursor.analysis();
673
674            // FIXME: The full state vector can be quite long. It would be nice to split on commas
675            // and use some text wrapping algorithm.
676            write!(
677                w,
678                r#"<td colspan="{colspan}" {fmt} align="left">{state}</td>"#,
679                colspan = this.style.num_state_columns(),
680                fmt = fmt,
681                state = dot::escape_html(&format!(
682                    "{:?}",
683                    DebugWithAdapter { this: state, ctxt: analysis }
684                )),
685            )
686        })
687    }
688}
689
690struct StateDiffCollector<D> {
691    prev_state: D,
692    before: Option<Vec<String>>,
693    after: Vec<String>,
694}
695
696impl<D> StateDiffCollector<D> {
697    fn run<'tcx, A>(
698        body: &Body<'tcx>,
699        block: BasicBlock,
700        analysis: &mut A,
701        results: &Results<A::Domain>,
702        style: OutputStyle,
703    ) -> Self
704    where
705        A: Analysis<'tcx, Domain = D>,
706        D: DebugWithContext<A>,
707    {
708        let mut collector = StateDiffCollector {
709            prev_state: analysis.bottom_value(body),
710            after: vec![],
711            before: (style == OutputStyle::BeforeAndAfter).then_some(vec![]),
712        };
713
714        visit_results(body, std::iter::once(block), analysis, results, &mut collector);
715        collector
716    }
717}
718
719impl<'tcx, A> ResultsVisitor<'tcx, A> for StateDiffCollector<A::Domain>
720where
721    A: Analysis<'tcx>,
722    A::Domain: DebugWithContext<A>,
723{
724    fn visit_block_start(&mut self, state: &A::Domain) {
725        if A::Direction::IS_FORWARD {
726            self.prev_state.clone_from(state);
727        }
728    }
729
730    fn visit_block_end(&mut self, state: &A::Domain) {
731        if A::Direction::IS_BACKWARD {
732            self.prev_state.clone_from(state);
733        }
734    }
735
736    fn visit_after_early_statement_effect(
737        &mut self,
738        analysis: &mut A,
739        state: &A::Domain,
740        _statement: &mir::Statement<'tcx>,
741        _location: Location,
742    ) {
743        if let Some(before) = self.before.as_mut() {
744            before.push(diff_pretty(state, &self.prev_state, analysis));
745            self.prev_state.clone_from(state)
746        }
747    }
748
749    fn visit_after_primary_statement_effect(
750        &mut self,
751        analysis: &mut A,
752        state: &A::Domain,
753        _statement: &mir::Statement<'tcx>,
754        _location: Location,
755    ) {
756        self.after.push(diff_pretty(state, &self.prev_state, analysis));
757        self.prev_state.clone_from(state)
758    }
759
760    fn visit_after_early_terminator_effect(
761        &mut self,
762        analysis: &mut A,
763        state: &A::Domain,
764        _terminator: &mir::Terminator<'tcx>,
765        _location: Location,
766    ) {
767        if let Some(before) = self.before.as_mut() {
768            before.push(diff_pretty(state, &self.prev_state, analysis));
769            self.prev_state.clone_from(state)
770        }
771    }
772
773    fn visit_after_primary_terminator_effect(
774        &mut self,
775        analysis: &mut A,
776        state: &A::Domain,
777        _terminator: &mir::Terminator<'tcx>,
778        _location: Location,
779    ) {
780        self.after.push(diff_pretty(state, &self.prev_state, analysis));
781        self.prev_state.clone_from(state)
782    }
783}
784
785macro_rules! regex {
786    ($re:literal $(,)?) => {{
787        static RE: OnceLock<regex::Regex> = OnceLock::new();
788        RE.get_or_init(|| Regex::new($re).unwrap())
789    }};
790}
791
792fn diff_pretty<T, C>(new: T, old: T, ctxt: &C) -> String
793where
794    T: DebugWithContext<C>,
795{
796    if new == old {
797        return String::new();
798    }
799
800    let re = regex!("\t?\u{001f}([+-])");
801
802    let raw_diff = format!("{:#?}", DebugDiffWithAdapter { new, old, ctxt });
803
804    // Replace newlines in the `Debug` output with `<br/>`
805    let raw_diff = raw_diff.replace('\n', r#"<br align="left"/>"#);
806
807    let mut inside_font_tag = false;
808    let html_diff = re.replace_all(&raw_diff, |captures: &regex::Captures<'_>| {
809        let mut ret = String::new();
810        if inside_font_tag {
811            ret.push_str(r#"</font>"#);
812        }
813
814        let tag = match &captures[1] {
815            "+" => r#"<font color="darkgreen">+"#,
816            "-" => r#"<font color="red">-"#,
817            _ => unreachable!(),
818        };
819
820        inside_font_tag = true;
821        ret.push_str(tag);
822        ret
823    });
824
825    let Cow::Owned(mut html_diff) = html_diff else {
826        return raw_diff;
827    };
828
829    if inside_font_tag {
830        html_diff.push_str("</font>");
831    }
832
833    html_diff
834}
835
836/// The background color used for zebra-striping the table.
837#[derive(Clone, Copy)]
838enum Background {
839    Light,
840    Dark,
841}
842
843impl Background {
844    fn attr(self) -> &'static str {
845        match self {
846            Self::Dark => "bgcolor=\"#f0f0f0\"",
847            Self::Light => "",
848        }
849    }
850}
851
852impl ops::Not for Background {
853    type Output = Self;
854
855    fn not(self) -> Self {
856        match self {
857            Self::Light => Self::Dark,
858            Self::Dark => Self::Light,
859        }
860    }
861}