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: ®ex::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