• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 //! A helpful diagram for debugging dataflow problems.
2 
3 use std::borrow::Cow;
4 use std::cell::RefCell;
5 use std::sync::OnceLock;
6 use std::{io, ops, str};
7 
8 use regex::Regex;
9 use rustc_graphviz as dot;
10 use rustc_index::bit_set::BitSet;
11 use rustc_middle::mir::graphviz_safe_def_name;
12 use rustc_middle::mir::{self, BasicBlock, Body, Location};
13 
14 use super::fmt::{DebugDiffWithAdapter, DebugWithAdapter, DebugWithContext};
15 use super::{Analysis, CallReturnPlaces, Direction, Results, ResultsRefCursor, ResultsVisitor};
16 
17 #[derive(Clone, Copy, Debug, PartialEq, Eq)]
18 pub enum OutputStyle {
19     AfterOnly,
20     BeforeAndAfter,
21 }
22 
23 impl OutputStyle {
num_state_columns(&self) -> usize24     fn num_state_columns(&self) -> usize {
25         match self {
26             Self::AfterOnly => 1,
27             Self::BeforeAndAfter => 2,
28         }
29     }
30 }
31 
32 pub struct Formatter<'res, 'mir, 'tcx, A>
33 where
34     A: Analysis<'tcx>,
35 {
36     body: &'mir Body<'tcx>,
37     results: RefCell<&'res mut Results<'tcx, A>>,
38     style: OutputStyle,
39     reachable: BitSet<BasicBlock>,
40 }
41 
42 impl<'res, 'mir, 'tcx, A> Formatter<'res, 'mir, 'tcx, A>
43 where
44     A: Analysis<'tcx>,
45 {
new( body: &'mir Body<'tcx>, results: &'res mut Results<'tcx, A>, style: OutputStyle, ) -> Self46     pub fn new(
47         body: &'mir Body<'tcx>,
48         results: &'res mut Results<'tcx, A>,
49         style: OutputStyle,
50     ) -> Self {
51         let reachable = mir::traversal::reachable_as_bitset(body);
52         Formatter { body, results: results.into(), style, reachable }
53     }
54 }
55 
56 /// A pair of a basic block and an index into that basic blocks `successors`.
57 #[derive(Copy, Clone, PartialEq, Eq, Debug)]
58 pub struct CfgEdge {
59     source: BasicBlock,
60     index: usize,
61 }
62 
dataflow_successors(body: &Body<'_>, bb: BasicBlock) -> Vec<CfgEdge>63 fn dataflow_successors(body: &Body<'_>, bb: BasicBlock) -> Vec<CfgEdge> {
64     body[bb]
65         .terminator()
66         .successors()
67         .enumerate()
68         .map(|(index, _)| CfgEdge { source: bb, index })
69         .collect()
70 }
71 
72 impl<'tcx, A> dot::Labeller<'_> for Formatter<'_, '_, 'tcx, A>
73 where
74     A: Analysis<'tcx>,
75     A::Domain: DebugWithContext<A>,
76 {
77     type Node = BasicBlock;
78     type Edge = CfgEdge;
79 
graph_id(&self) -> dot::Id<'_>80     fn graph_id(&self) -> dot::Id<'_> {
81         let name = graphviz_safe_def_name(self.body.source.def_id());
82         dot::Id::new(format!("graph_for_def_id_{name}")).unwrap()
83     }
84 
node_id(&self, n: &Self::Node) -> dot::Id<'_>85     fn node_id(&self, n: &Self::Node) -> dot::Id<'_> {
86         dot::Id::new(format!("bb_{}", n.index())).unwrap()
87     }
88 
node_label(&self, block: &Self::Node) -> dot::LabelText<'_>89     fn node_label(&self, block: &Self::Node) -> dot::LabelText<'_> {
90         let mut label = Vec::new();
91         let mut results = self.results.borrow_mut();
92         let mut fmt = BlockFormatter {
93             results: results.as_results_cursor(self.body),
94             style: self.style,
95             bg: Background::Light,
96         };
97 
98         fmt.write_node_label(&mut label, *block).unwrap();
99         dot::LabelText::html(String::from_utf8(label).unwrap())
100     }
101 
node_shape(&self, _n: &Self::Node) -> Option<dot::LabelText<'_>>102     fn node_shape(&self, _n: &Self::Node) -> Option<dot::LabelText<'_>> {
103         Some(dot::LabelText::label("none"))
104     }
105 
edge_label(&self, e: &Self::Edge) -> dot::LabelText<'_>106     fn edge_label(&self, e: &Self::Edge) -> dot::LabelText<'_> {
107         let label = &self.body[e.source].terminator().kind.fmt_successor_labels()[e.index];
108         dot::LabelText::label(label.clone())
109     }
110 }
111 
112 impl<'mir, 'tcx, A> dot::GraphWalk<'mir> for Formatter<'_, 'mir, 'tcx, A>
113 where
114     A: Analysis<'tcx>,
115 {
116     type Node = BasicBlock;
117     type Edge = CfgEdge;
118 
nodes(&self) -> dot::Nodes<'_, Self::Node>119     fn nodes(&self) -> dot::Nodes<'_, Self::Node> {
120         self.body
121             .basic_blocks
122             .indices()
123             .filter(|&idx| self.reachable.contains(idx))
124             .collect::<Vec<_>>()
125             .into()
126     }
127 
edges(&self) -> dot::Edges<'_, Self::Edge>128     fn edges(&self) -> dot::Edges<'_, Self::Edge> {
129         self.body
130             .basic_blocks
131             .indices()
132             .flat_map(|bb| dataflow_successors(self.body, bb))
133             .collect::<Vec<_>>()
134             .into()
135     }
136 
source(&self, edge: &Self::Edge) -> Self::Node137     fn source(&self, edge: &Self::Edge) -> Self::Node {
138         edge.source
139     }
140 
target(&self, edge: &Self::Edge) -> Self::Node141     fn target(&self, edge: &Self::Edge) -> Self::Node {
142         self.body[edge.source].terminator().successors().nth(edge.index).unwrap()
143     }
144 }
145 
146 struct BlockFormatter<'res, 'mir, 'tcx, A>
147 where
148     A: Analysis<'tcx>,
149 {
150     results: ResultsRefCursor<'res, 'mir, 'tcx, A>,
151     bg: Background,
152     style: OutputStyle,
153 }
154 
155 impl<'res, 'mir, 'tcx, A> BlockFormatter<'res, 'mir, 'tcx, A>
156 where
157     A: Analysis<'tcx>,
158     A::Domain: DebugWithContext<A>,
159 {
160     const HEADER_COLOR: &'static str = "#a0a0a0";
161 
toggle_background(&mut self) -> Background162     fn toggle_background(&mut self) -> Background {
163         let bg = self.bg;
164         self.bg = !bg;
165         bg
166     }
167 
write_node_label(&mut self, w: &mut impl io::Write, block: BasicBlock) -> io::Result<()>168     fn write_node_label(&mut self, w: &mut impl io::Write, block: BasicBlock) -> io::Result<()> {
169         //   Sample output:
170         //   +-+-----------------------------------------------+
171         // A |                      bb4                        |
172         //   +-+----------------------------------+------------+
173         // B |                MIR                 |   STATE    |
174         //   +-+----------------------------------+------------+
175         // C | | (on entry)                       | {_0,_2,_3} |
176         //   +-+----------------------------------+------------+
177         // D |0| StorageLive(_7)                  |            |
178         //   +-+----------------------------------+------------+
179         //   |1| StorageLive(_8)                  |            |
180         //   +-+----------------------------------+------------+
181         //   |2| _8 = &mut _1                     | +_8        |
182         //   +-+----------------------------------+------------+
183         // E |T| _4 = const Foo::twiddle(move _2) | -_2        |
184         //   +-+----------------------------------+------------+
185         // F | | (on unwind)                      | {_0,_3,_8} |
186         //   +-+----------------------------------+------------+
187         //   | | (on successful return)           | +_4        |
188         //   +-+----------------------------------+------------+
189 
190         // N.B., Some attributes (`align`, `balign`) are repeated on parent elements and their
191         // children. This is because `xdot` seemed to have a hard time correctly propagating
192         // attributes. Make sure to test the output before trying to remove the redundancy.
193         // Notably, `align` was found to have no effect when applied only to <table>.
194 
195         let table_fmt = concat!(
196             " border=\"1\"",
197             " cellborder=\"1\"",
198             " cellspacing=\"0\"",
199             " cellpadding=\"3\"",
200             " sides=\"rb\"",
201         );
202         write!(w, r#"<table{table_fmt}>"#)?;
203 
204         // A + B: Block header
205         match self.style {
206             OutputStyle::AfterOnly => self.write_block_header_simple(w, block)?,
207             OutputStyle::BeforeAndAfter => {
208                 self.write_block_header_with_state_columns(w, block, &["BEFORE", "AFTER"])?
209             }
210         }
211 
212         // C: State at start of block
213         self.bg = Background::Light;
214         self.results.seek_to_block_start(block);
215         let block_start_state = self.results.get().clone();
216         self.write_row_with_full_state(w, "", "(on start)")?;
217 
218         // D + E: Statement and terminator transfer functions
219         self.write_statements_and_terminator(w, block)?;
220 
221         // F: State at end of block
222 
223         let terminator = self.results.body()[block].terminator();
224 
225         // Write the full dataflow state immediately after the terminator if it differs from the
226         // state at block entry.
227         self.results.seek_to_block_end(block);
228         if self.results.get() != &block_start_state || A::Direction::IS_BACKWARD {
229             let after_terminator_name = match terminator.kind {
230                 mir::TerminatorKind::Call { target: Some(_), .. } => "(on unwind)",
231                 _ => "(on end)",
232             };
233 
234             self.write_row_with_full_state(w, "", after_terminator_name)?;
235         }
236 
237         // Write any changes caused by terminator-specific effects.
238         //
239         // FIXME: These should really be printed as part of each outgoing edge rather than the node
240         // for the basic block itself. That way, we could display terminator-specific effects for
241         // backward dataflow analyses as well as effects for `SwitchInt` terminators.
242         match terminator.kind {
243             mir::TerminatorKind::Call { destination, .. } => {
244                 self.write_row(w, "", "(on successful return)", |this, w, fmt| {
245                     let state_on_unwind = this.results.get().clone();
246                     this.results.apply_custom_effect(|analysis, state| {
247                         analysis.apply_call_return_effect(
248                             state,
249                             block,
250                             CallReturnPlaces::Call(destination),
251                         );
252                     });
253 
254                     write!(
255                         w,
256                         r#"<td balign="left" colspan="{colspan}" {fmt} align="left">{diff}</td>"#,
257                         colspan = this.style.num_state_columns(),
258                         fmt = fmt,
259                         diff = diff_pretty(
260                             this.results.get(),
261                             &state_on_unwind,
262                             this.results.analysis()
263                         ),
264                     )
265                 })?;
266             }
267 
268             mir::TerminatorKind::Yield { resume, resume_arg, .. } => {
269                 self.write_row(w, "", "(on yield resume)", |this, w, fmt| {
270                     let state_on_generator_drop = this.results.get().clone();
271                     this.results.apply_custom_effect(|analysis, state| {
272                         analysis.apply_yield_resume_effect(state, resume, resume_arg);
273                     });
274 
275                     write!(
276                         w,
277                         r#"<td balign="left" colspan="{colspan}" {fmt} align="left">{diff}</td>"#,
278                         colspan = this.style.num_state_columns(),
279                         fmt = fmt,
280                         diff = diff_pretty(
281                             this.results.get(),
282                             &state_on_generator_drop,
283                             this.results.analysis()
284                         ),
285                     )
286                 })?;
287             }
288 
289             mir::TerminatorKind::InlineAsm { destination: Some(_), ref operands, .. } => {
290                 self.write_row(w, "", "(on successful return)", |this, w, fmt| {
291                     let state_on_unwind = this.results.get().clone();
292                     this.results.apply_custom_effect(|analysis, state| {
293                         analysis.apply_call_return_effect(
294                             state,
295                             block,
296                             CallReturnPlaces::InlineAsm(operands),
297                         );
298                     });
299 
300                     write!(
301                         w,
302                         r#"<td balign="left" colspan="{colspan}" {fmt} align="left">{diff}</td>"#,
303                         colspan = this.style.num_state_columns(),
304                         fmt = fmt,
305                         diff = diff_pretty(
306                             this.results.get(),
307                             &state_on_unwind,
308                             this.results.analysis()
309                         ),
310                     )
311                 })?;
312             }
313 
314             _ => {}
315         };
316 
317         write!(w, "</table>")
318     }
319 
write_block_header_simple( &mut self, w: &mut impl io::Write, block: BasicBlock, ) -> io::Result<()>320     fn write_block_header_simple(
321         &mut self,
322         w: &mut impl io::Write,
323         block: BasicBlock,
324     ) -> io::Result<()> {
325         //   +-------------------------------------------------+
326         // A |                      bb4                        |
327         //   +-----------------------------------+-------------+
328         // B |                MIR                |    STATE    |
329         //   +-+---------------------------------+-------------+
330         //   | |              ...                |             |
331 
332         // A
333         write!(
334             w,
335             concat!("<tr>", r#"<td colspan="3" sides="tl">bb{block_id}</td>"#, "</tr>",),
336             block_id = block.index(),
337         )?;
338 
339         // B
340         write!(
341             w,
342             concat!(
343                 "<tr>",
344                 r#"<td colspan="2" {fmt}>MIR</td>"#,
345                 r#"<td {fmt}>STATE</td>"#,
346                 "</tr>",
347             ),
348             fmt = format!("bgcolor=\"{}\" sides=\"tl\"", Self::HEADER_COLOR),
349         )
350     }
351 
write_block_header_with_state_columns( &mut self, w: &mut impl io::Write, block: BasicBlock, state_column_names: &[&str], ) -> io::Result<()>352     fn write_block_header_with_state_columns(
353         &mut self,
354         w: &mut impl io::Write,
355         block: BasicBlock,
356         state_column_names: &[&str],
357     ) -> io::Result<()> {
358         //   +------------------------------------+-------------+
359         // A |                bb4                 |    STATE    |
360         //   +------------------------------------+------+------+
361         // B |                MIR                 |  GEN | KILL |
362         //   +-+----------------------------------+------+------+
363         //   | |              ...                 |      |      |
364 
365         // A
366         write!(
367             w,
368             concat!(
369                 "<tr>",
370                 r#"<td {fmt} colspan="2">bb{block_id}</td>"#,
371                 r#"<td {fmt} colspan="{num_state_cols}">STATE</td>"#,
372                 "</tr>",
373             ),
374             fmt = "sides=\"tl\"",
375             num_state_cols = state_column_names.len(),
376             block_id = block.index(),
377         )?;
378 
379         // B
380         let fmt = format!("bgcolor=\"{}\" sides=\"tl\"", Self::HEADER_COLOR);
381         write!(w, concat!("<tr>", r#"<td colspan="2" {fmt}>MIR</td>"#,), fmt = fmt,)?;
382 
383         for name in state_column_names {
384             write!(w, "<td {fmt}>{name}</td>")?;
385         }
386 
387         write!(w, "</tr>")
388     }
389 
write_statements_and_terminator( &mut self, w: &mut impl io::Write, block: BasicBlock, ) -> io::Result<()>390     fn write_statements_and_terminator(
391         &mut self,
392         w: &mut impl io::Write,
393         block: BasicBlock,
394     ) -> io::Result<()> {
395         let diffs = StateDiffCollector::run(
396             self.results.body(),
397             block,
398             self.results.mut_results(),
399             self.style,
400         );
401 
402         let mut diffs_before = diffs.before.map(|v| v.into_iter());
403         let mut diffs_after = diffs.after.into_iter();
404 
405         let next_in_dataflow_order = |it: &mut std::vec::IntoIter<_>| {
406             if A::Direction::IS_FORWARD { it.next().unwrap() } else { it.next_back().unwrap() }
407         };
408 
409         for (i, statement) in self.results.body()[block].statements.iter().enumerate() {
410             let statement_str = format!("{statement:?}");
411             let index_str = format!("{i}");
412 
413             let after = next_in_dataflow_order(&mut diffs_after);
414             let before = diffs_before.as_mut().map(next_in_dataflow_order);
415 
416             self.write_row(w, &index_str, &statement_str, |_this, w, fmt| {
417                 if let Some(before) = before {
418                     write!(w, r#"<td {fmt} align="left">{before}</td>"#)?;
419                 }
420 
421                 write!(w, r#"<td {fmt} align="left">{after}</td>"#)
422             })?;
423         }
424 
425         let after = next_in_dataflow_order(&mut diffs_after);
426         let before = diffs_before.as_mut().map(next_in_dataflow_order);
427 
428         assert!(diffs_after.is_empty());
429         assert!(diffs_before.as_ref().map_or(true, ExactSizeIterator::is_empty));
430 
431         let terminator = self.results.body()[block].terminator();
432         let mut terminator_str = String::new();
433         terminator.kind.fmt_head(&mut terminator_str).unwrap();
434 
435         self.write_row(w, "T", &terminator_str, |_this, w, fmt| {
436             if let Some(before) = before {
437                 write!(w, r#"<td {fmt} align="left">{before}</td>"#)?;
438             }
439 
440             write!(w, r#"<td {fmt} align="left">{after}</td>"#)
441         })
442     }
443 
444     /// Write a row with the given index and MIR, using the function argument to fill in the
445     /// "STATE" column(s).
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<()>446     fn write_row<W: io::Write>(
447         &mut self,
448         w: &mut W,
449         i: &str,
450         mir: &str,
451         f: impl FnOnce(&mut Self, &mut W, &str) -> io::Result<()>,
452     ) -> io::Result<()> {
453         let bg = self.toggle_background();
454         let valign = if mir.starts_with("(on ") && mir != "(on entry)" { "bottom" } else { "top" };
455 
456         let fmt = format!("valign=\"{}\" sides=\"tl\" {}", valign, bg.attr());
457 
458         write!(
459             w,
460             concat!(
461                 "<tr>",
462                 r#"<td {fmt} align="right">{i}</td>"#,
463                 r#"<td {fmt} align="left">{mir}</td>"#,
464             ),
465             i = i,
466             fmt = fmt,
467             mir = dot::escape_html(mir),
468         )?;
469 
470         f(self, w, &fmt)?;
471         write!(w, "</tr>")
472     }
473 
write_row_with_full_state( &mut self, w: &mut impl io::Write, i: &str, mir: &str, ) -> io::Result<()>474     fn write_row_with_full_state(
475         &mut self,
476         w: &mut impl io::Write,
477         i: &str,
478         mir: &str,
479     ) -> io::Result<()> {
480         self.write_row(w, i, mir, |this, w, fmt| {
481             let state = this.results.get();
482             let analysis = this.results.analysis();
483 
484             // FIXME: The full state vector can be quite long. It would be nice to split on commas
485             // and use some text wrapping algorithm.
486             write!(
487                 w,
488                 r#"<td colspan="{colspan}" {fmt} align="left">{state}</td>"#,
489                 colspan = this.style.num_state_columns(),
490                 fmt = fmt,
491                 state = dot::escape_html(&format!(
492                     "{:?}",
493                     DebugWithAdapter { this: state, ctxt: analysis }
494                 )),
495             )
496         })
497     }
498 }
499 
500 struct StateDiffCollector<D> {
501     prev_state: D,
502     before: Option<Vec<String>>,
503     after: Vec<String>,
504 }
505 
506 impl<D> StateDiffCollector<D> {
run<'tcx, A>( body: &mir::Body<'tcx>, block: BasicBlock, results: &mut Results<'tcx, A>, style: OutputStyle, ) -> Self where A: Analysis<'tcx, Domain = D>, D: DebugWithContext<A>,507     fn run<'tcx, A>(
508         body: &mir::Body<'tcx>,
509         block: BasicBlock,
510         results: &mut Results<'tcx, A>,
511         style: OutputStyle,
512     ) -> Self
513     where
514         A: Analysis<'tcx, Domain = D>,
515         D: DebugWithContext<A>,
516     {
517         let mut collector = StateDiffCollector {
518             prev_state: results.analysis.bottom_value(body),
519             after: vec![],
520             before: (style == OutputStyle::BeforeAndAfter).then_some(vec![]),
521         };
522 
523         results.visit_with(body, std::iter::once(block), &mut collector);
524         collector
525     }
526 }
527 
528 impl<'tcx, A> ResultsVisitor<'_, 'tcx, Results<'tcx, A>> for StateDiffCollector<A::Domain>
529 where
530     A: Analysis<'tcx>,
531     A::Domain: DebugWithContext<A>,
532 {
533     type FlowState = A::Domain;
534 
visit_block_start( &mut self, _results: &Results<'tcx, A>, state: &Self::FlowState, _block_data: &mir::BasicBlockData<'tcx>, _block: BasicBlock, )535     fn visit_block_start(
536         &mut self,
537         _results: &Results<'tcx, A>,
538         state: &Self::FlowState,
539         _block_data: &mir::BasicBlockData<'tcx>,
540         _block: BasicBlock,
541     ) {
542         if A::Direction::IS_FORWARD {
543             self.prev_state.clone_from(state);
544         }
545     }
546 
visit_block_end( &mut self, _results: &Results<'tcx, A>, state: &Self::FlowState, _block_data: &mir::BasicBlockData<'tcx>, _block: BasicBlock, )547     fn visit_block_end(
548         &mut self,
549         _results: &Results<'tcx, A>,
550         state: &Self::FlowState,
551         _block_data: &mir::BasicBlockData<'tcx>,
552         _block: BasicBlock,
553     ) {
554         if A::Direction::IS_BACKWARD {
555             self.prev_state.clone_from(state);
556         }
557     }
558 
visit_statement_before_primary_effect( &mut self, results: &Results<'tcx, A>, state: &Self::FlowState, _statement: &mir::Statement<'tcx>, _location: Location, )559     fn visit_statement_before_primary_effect(
560         &mut self,
561         results: &Results<'tcx, A>,
562         state: &Self::FlowState,
563         _statement: &mir::Statement<'tcx>,
564         _location: Location,
565     ) {
566         if let Some(before) = self.before.as_mut() {
567             before.push(diff_pretty(state, &self.prev_state, &results.analysis));
568             self.prev_state.clone_from(state)
569         }
570     }
571 
visit_statement_after_primary_effect( &mut self, results: &Results<'tcx, A>, state: &Self::FlowState, _statement: &mir::Statement<'tcx>, _location: Location, )572     fn visit_statement_after_primary_effect(
573         &mut self,
574         results: &Results<'tcx, A>,
575         state: &Self::FlowState,
576         _statement: &mir::Statement<'tcx>,
577         _location: Location,
578     ) {
579         self.after.push(diff_pretty(state, &self.prev_state, &results.analysis));
580         self.prev_state.clone_from(state)
581     }
582 
visit_terminator_before_primary_effect( &mut self, results: &Results<'tcx, A>, state: &Self::FlowState, _terminator: &mir::Terminator<'tcx>, _location: Location, )583     fn visit_terminator_before_primary_effect(
584         &mut self,
585         results: &Results<'tcx, A>,
586         state: &Self::FlowState,
587         _terminator: &mir::Terminator<'tcx>,
588         _location: Location,
589     ) {
590         if let Some(before) = self.before.as_mut() {
591             before.push(diff_pretty(state, &self.prev_state, &results.analysis));
592             self.prev_state.clone_from(state)
593         }
594     }
595 
visit_terminator_after_primary_effect( &mut self, results: &Results<'tcx, A>, state: &Self::FlowState, _terminator: &mir::Terminator<'tcx>, _location: Location, )596     fn visit_terminator_after_primary_effect(
597         &mut self,
598         results: &Results<'tcx, A>,
599         state: &Self::FlowState,
600         _terminator: &mir::Terminator<'tcx>,
601         _location: Location,
602     ) {
603         self.after.push(diff_pretty(state, &self.prev_state, &results.analysis));
604         self.prev_state.clone_from(state)
605     }
606 }
607 
608 macro_rules! regex {
609     ($re:literal $(,)?) => {{
610         static RE: OnceLock<regex::Regex> = OnceLock::new();
611         RE.get_or_init(|| Regex::new($re).unwrap())
612     }};
613 }
614 
diff_pretty<T, C>(new: T, old: T, ctxt: &C) -> String where T: DebugWithContext<C>,615 fn diff_pretty<T, C>(new: T, old: T, ctxt: &C) -> String
616 where
617     T: DebugWithContext<C>,
618 {
619     if new == old {
620         return String::new();
621     }
622 
623     let re = regex!("\t?\u{001f}([+-])");
624 
625     let raw_diff = format!("{:#?}", DebugDiffWithAdapter { new, old, ctxt });
626 
627     // Replace newlines in the `Debug` output with `<br/>`
628     let raw_diff = raw_diff.replace('\n', r#"<br align="left"/>"#);
629 
630     let mut inside_font_tag = false;
631     let html_diff = re.replace_all(&raw_diff, |captures: &regex::Captures<'_>| {
632         let mut ret = String::new();
633         if inside_font_tag {
634             ret.push_str(r#"</font>"#);
635         }
636 
637         let tag = match &captures[1] {
638             "+" => r#"<font color="darkgreen">+"#,
639             "-" => r#"<font color="red">-"#,
640             _ => unreachable!(),
641         };
642 
643         inside_font_tag = true;
644         ret.push_str(tag);
645         ret
646     });
647 
648     let Cow::Owned(mut html_diff) = html_diff else {
649         return raw_diff;
650     };
651 
652     if inside_font_tag {
653         html_diff.push_str("</font>");
654     }
655 
656     html_diff
657 }
658 
659 /// The background color used for zebra-striping the table.
660 #[derive(Clone, Copy)]
661 enum Background {
662     Light,
663     Dark,
664 }
665 
666 impl Background {
attr(self) -> &'static str667     fn attr(self) -> &'static str {
668         match self {
669             Self::Dark => "bgcolor=\"#f0f0f0\"",
670             Self::Light => "",
671         }
672     }
673 }
674 
675 impl ops::Not for Background {
676     type Output = Self;
677 
not(self) -> Self678     fn not(self) -> Self {
679         match self {
680             Self::Light => Self::Dark,
681             Self::Dark => Self::Light,
682         }
683     }
684 }
685