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