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