rustc_mir_dataflow/framework/
graphviz.rs

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