From 6b5869a0ae3488ee19c2c4cb30cd589f68a3d2a8 Mon Sep 17 00:00:00 2001 From: Rich Kadel Date: Sat, 29 Aug 2020 10:55:46 -0700 Subject: [PATCH] Add new `-Z dump-mir-spanview` option Similar to `-Z dump-mir-graphviz`, this adds the option to write HTML+CSS files that allow users to analyze the spans associated with MIR elements (by individual statement, just terminator, or overall basic block). This PR was split out from PR #76004, and exposes an API for spanview HTML+CSS files that is also used to analyze code regions chosen for coverage instrumentation (in a follow-on PR). Rust compiler MCP rust-lang/compiler-team#278 Relevant issue: #34701 - Implement support for LLVMs code coverage instrumentation --- compiler/rustc_mir/src/util/mod.rs | 1 + compiler/rustc_mir/src/util/pretty.rs | 11 + compiler/rustc_mir/src/util/spanview.rs | 461 ++++++++++++++++++ compiler/rustc_session/src/config.rs | 15 + compiler/rustc_session/src/options.rs | 36 ++ src/test/mir-opt/spanview-block.rs | 5 + src/test/mir-opt/spanview-statement.rs | 5 + src/test/mir-opt/spanview-terminator.rs | 5 + .../spanview_block.main.mir_map.0.html | 67 +++ .../spanview_statement.main.mir_map.0.html | 67 +++ .../spanview_terminator.main.mir_map.0.html | 66 +++ 11 files changed, 739 insertions(+) create mode 100644 compiler/rustc_mir/src/util/spanview.rs create mode 100644 src/test/mir-opt/spanview-block.rs create mode 100644 src/test/mir-opt/spanview-statement.rs create mode 100644 src/test/mir-opt/spanview-terminator.rs create mode 100644 src/test/mir-opt/spanview_block.main.mir_map.0.html create mode 100644 src/test/mir-opt/spanview_statement.main.mir_map.0.html create mode 100644 src/test/mir-opt/spanview_terminator.main.mir_map.0.html diff --git a/compiler/rustc_mir/src/util/mod.rs b/compiler/rustc_mir/src/util/mod.rs index 8bbe207c077ee..ed0fafb1aac16 100644 --- a/compiler/rustc_mir/src/util/mod.rs +++ b/compiler/rustc_mir/src/util/mod.rs @@ -9,6 +9,7 @@ mod alignment; pub mod collect_writes; mod graphviz; pub(crate) mod pretty; +pub(crate) mod spanview; pub use self::aggregate::expand_aggregate; pub use self::alignment::is_disaligned; diff --git a/compiler/rustc_mir/src/util/pretty.rs b/compiler/rustc_mir/src/util/pretty.rs index 2a9cbc7fc0e37..db57766620e81 100644 --- a/compiler/rustc_mir/src/util/pretty.rs +++ b/compiler/rustc_mir/src/util/pretty.rs @@ -6,6 +6,7 @@ use std::io::{self, Write}; use std::path::{Path, PathBuf}; use super::graphviz::write_mir_fn_graphviz; +use super::spanview::write_mir_fn_spanview; use crate::transform::MirSource; use either::Either; use rustc_data_structures::fx::FxHashMap; @@ -147,6 +148,16 @@ fn dump_matched_mir_node<'tcx, F>( write_mir_fn_graphviz(tcx, source.def_id(), body, false, &mut file)?; }; } + + if let Some(spanview) = tcx.sess.opts.debugging_opts.dump_mir_spanview { + let _: io::Result<()> = try { + let mut file = + create_dump_file(tcx, "html", pass_num, pass_name, disambiguator, source)?; + if source.def_id().is_local() { + write_mir_fn_spanview(tcx, source.def_id(), body, spanview, &mut file)?; + } + }; + } } /// Returns the path to the filename where we should dump a given MIR. diff --git a/compiler/rustc_mir/src/util/spanview.rs b/compiler/rustc_mir/src/util/spanview.rs new file mode 100644 index 0000000000000..b2f2b5fc1e6f4 --- /dev/null +++ b/compiler/rustc_mir/src/util/spanview.rs @@ -0,0 +1,461 @@ +use rustc_hir::def_id::DefId; +use rustc_middle::hir; +use rustc_middle::mir::*; +use rustc_middle::ty::TyCtxt; +use rustc_session::config::MirSpanview; +use rustc_span::{BytePos, Pos, Span}; + +use std::io::{self, Write}; +use std::iter::Peekable; + +pub const TOOLTIP_INDENT: &str = " "; + +const NEW_LINE_SPAN: &str = "\n"; +const HEADER: &str = r#" + + + coverage_of_if_else - Code Regions + + +"#; + +const FOOTER: &str = r#" + +"#; + +/// Metadata to highlight the span of a MIR BasicBlock, Statement, or Terminator. +pub struct SpanViewable { + pub span: Span, + pub title: String, + pub tooltip: String, +} + +/// Write a spanview HTML+CSS file to analyze MIR element spans. +pub fn write_mir_fn_spanview<'tcx, W>( + tcx: TyCtxt<'tcx>, + def_id: DefId, + body: &Body<'tcx>, + spanview: MirSpanview, + w: &mut W, +) -> io::Result<()> +where + W: Write, +{ + let body_span = hir_body(tcx, def_id).value.span; + let mut span_viewables = Vec::new(); + for (bb, data) in body.basic_blocks().iter_enumerated() { + match spanview { + MirSpanview::Statement => { + for (i, statement) in data.statements.iter().enumerate() { + if let Some(span_viewable) = + statement_span_viewable(tcx, body_span, bb, i, statement) + { + span_viewables.push(span_viewable); + } + } + if let Some(span_viewable) = terminator_span_viewable(tcx, body_span, bb, data) { + span_viewables.push(span_viewable); + } + } + MirSpanview::Terminator => { + if let Some(span_viewable) = terminator_span_viewable(tcx, body_span, bb, data) { + span_viewables.push(span_viewable); + } + } + MirSpanview::Block => { + if let Some(span_viewable) = block_span_viewable(tcx, body_span, bb, data) { + span_viewables.push(span_viewable); + } + } + } + } + write_spanview_document(tcx, def_id, span_viewables, w)?; + Ok(()) +} + +/// Generate a spanview HTML+CSS document for the given local function `def_id`, and a pre-generated +/// list `SpanViewable`s. +pub fn write_spanview_document<'tcx, W>( + tcx: TyCtxt<'tcx>, + def_id: DefId, + mut span_viewables: Vec, + w: &mut W, +) -> io::Result<()> +where + W: Write, +{ + let fn_span = fn_span(tcx, def_id); + writeln!(w, "{}", HEADER)?; + let mut next_pos = fn_span.lo(); + let end_pos = fn_span.hi(); + let source_map = tcx.sess.source_map(); + let start = source_map.lookup_char_pos(next_pos); + write!( + w, + r#"
{}"#, + start.line - 1, + " ".repeat(start.col.to_usize()) + )?; + span_viewables.sort_unstable_by(|a, b| { + let a = a.span; + let b = b.span; + if a.lo() == b.lo() { + // Sort hi() in reverse order so shorter spans are attempted after longer spans. + // This should give shorter spans a higher "layer", so they are not covered by + // the longer spans. + b.hi().partial_cmp(&a.hi()) + } else { + a.lo().partial_cmp(&b.lo()) + } + .unwrap() + }); + let mut ordered_span_viewables = span_viewables.iter().peekable(); + let mut alt = false; + while ordered_span_viewables.peek().is_some() { + next_pos = write_span_viewables(tcx, next_pos, &mut ordered_span_viewables, false, 1, w)?; + alt = !alt; + } + if next_pos < end_pos { + write_coverage_gap(tcx, next_pos, end_pos, w)?; + } + write!(w, r#"
"#)?; + writeln!(w, "{}", FOOTER)?; + Ok(()) +} + +/// Format a string showing the start line and column, and end line and column within a file. +pub fn source_range_no_file<'tcx>(tcx: TyCtxt<'tcx>, span: &Span) -> String { + let source_map = tcx.sess.source_map(); + let start = source_map.lookup_char_pos(span.lo()); + let end = source_map.lookup_char_pos(span.hi()); + format!("{}:{}-{}:{}", start.line, start.col.to_usize() + 1, end.line, end.col.to_usize() + 1) +} + +pub fn statement_kind_name(statement: &Statement<'_>) -> &'static str { + use StatementKind::*; + match statement.kind { + Assign(..) => "Assign", + FakeRead(..) => "FakeRead", + SetDiscriminant { .. } => "SetDiscriminant", + StorageLive(..) => "StorageLive", + StorageDead(..) => "StorageDead", + LlvmInlineAsm(..) => "LlvmInlineAsm", + Retag(..) => "Retag", + AscribeUserType(..) => "AscribeUserType", + Coverage(..) => "Coverage", + Nop => "Nop", + } +} + +pub fn terminator_kind_name(term: &Terminator<'_>) -> &'static str { + use TerminatorKind::*; + match term.kind { + Goto { .. } => "Goto", + SwitchInt { .. } => "SwitchInt", + Resume => "Resume", + Abort => "Abort", + Return => "Return", + Unreachable => "Unreachable", + Drop { .. } => "Drop", + DropAndReplace { .. } => "DropAndReplace", + Call { .. } => "Call", + Assert { .. } => "Assert", + Yield { .. } => "Yield", + GeneratorDrop => "GeneratorDrop", + FalseEdge { .. } => "FalseEdge", + FalseUnwind { .. } => "FalseUnwind", + InlineAsm { .. } => "InlineAsm", + } +} + +fn statement_span_viewable<'tcx>( + tcx: TyCtxt<'tcx>, + body_span: Span, + bb: BasicBlock, + i: usize, + statement: &Statement<'tcx>, +) -> Option { + let span = statement.source_info.span; + if !body_span.contains(span) { + return None; + } + let title = format!("bb{}[{}]", bb.index(), i); + let tooltip = tooltip(tcx, &title, span, vec![statement.clone()], &None); + Some(SpanViewable { span, title, tooltip }) +} + +fn terminator_span_viewable<'tcx>( + tcx: TyCtxt<'tcx>, + body_span: Span, + bb: BasicBlock, + data: &BasicBlockData<'tcx>, +) -> Option { + let term = data.terminator(); + let span = term.source_info.span; + if !body_span.contains(span) { + return None; + } + let title = format!("bb{}`{}`", bb.index(), terminator_kind_name(term)); + let tooltip = tooltip(tcx, &title, span, vec![], &data.terminator); + Some(SpanViewable { span, title, tooltip }) +} + +fn block_span_viewable<'tcx>( + tcx: TyCtxt<'tcx>, + body_span: Span, + bb: BasicBlock, + data: &BasicBlockData<'tcx>, +) -> Option { + let span = compute_block_span(data, body_span); + if !body_span.contains(span) { + return None; + } + let title = format!("bb{}", bb.index()); + let tooltip = tooltip(tcx, &title, span, data.statements.clone(), &data.terminator); + Some(SpanViewable { span, title, tooltip }) +} + +fn compute_block_span<'tcx>(data: &BasicBlockData<'tcx>, body_span: Span) -> Span { + let mut span = data.terminator().source_info.span; + for statement_span in data.statements.iter().map(|statement| statement.source_info.span) { + // Only combine Spans from the function's body_span. + if body_span.contains(statement_span) { + span = span.to(statement_span); + } + } + span +} + +/// Recursively process each ordered span. Spans that overlap will have progressively varying +/// styles, such as increased padding for each overlap. Non-overlapping adjacent spans will +/// have alternating style choices, to help distinguish between them if, visually adjacent. +/// The `layer` is incremented for each overlap, and the `alt` bool alternates between true +/// and false, for each adjacent non-overlapping span. Source code between the spans (code +/// that is not in any coverage region) has neutral styling. +fn write_span_viewables<'tcx, 'b, W>( + tcx: TyCtxt<'tcx>, + next_pos: BytePos, + ordered_span_viewables: &mut Peekable>, + alt: bool, + layer: usize, + w: &mut W, +) -> io::Result +where + W: Write, +{ + let span_viewable = + ordered_span_viewables.next().expect("ordered_span_viewables should have some"); + if next_pos < span_viewable.span.lo() { + write_coverage_gap(tcx, next_pos, span_viewable.span.lo(), w)?; + } + let mut remaining_span = span_viewable.span; + let mut subalt = false; + loop { + let next_span_viewable = match ordered_span_viewables.peek() { + None => break, + Some(span_viewable) => *span_viewable, + }; + if !next_span_viewable.span.overlaps(remaining_span) { + break; + } + write_span( + tcx, + remaining_span.until(next_span_viewable.span), + Some(span_viewable), + alt, + layer, + w, + )?; + let next_pos = write_span_viewables( + tcx, + next_span_viewable.span.lo(), + ordered_span_viewables, + subalt, + layer + 1, + w, + )?; + subalt = !subalt; + if next_pos < remaining_span.hi() { + remaining_span = remaining_span.with_lo(next_pos); + } else { + return Ok(next_pos); + } + } + write_span(tcx, remaining_span, Some(span_viewable), alt, layer, w) +} + +fn write_coverage_gap<'tcx, W>( + tcx: TyCtxt<'tcx>, + lo: BytePos, + hi: BytePos, + w: &mut W, +) -> io::Result +where + W: Write, +{ + write_span(tcx, Span::with_root_ctxt(lo, hi), None, false, 0, w) +} + +fn write_span<'tcx, W>( + tcx: TyCtxt<'tcx>, + span: Span, + span_viewable: Option<&SpanViewable>, + alt: bool, + layer: usize, + w: &mut W, +) -> io::Result +where + W: Write, +{ + let source_map = tcx.sess.source_map(); + let snippet = source_map + .span_to_snippet(span) + .unwrap_or_else(|err| bug!("span_to_snippet error for span {:?}: {:?}", span, err)); + let labeled_snippet = if let Some(SpanViewable { title, .. }) = span_viewable { + if span.is_empty() { + format!(r#"@{}"#, title) + } else { + format!(r#"@{}: {}"#, title, escape_html(&snippet)) + } + } else { + snippet + }; + let maybe_alt = if layer > 0 { + if alt { " odd" } else { " even" } + } else { + "" + }; + let maybe_tooltip = if let Some(SpanViewable { tooltip, .. }) = span_viewable { + format!(" title=\"{}\"", escape_attr(tooltip)) + } else { + "".to_owned() + }; + if layer == 1 { + write!(w, "")?; + } + for (i, line) in labeled_snippet.lines().enumerate() { + if i > 0 { + write!(w, "{}", NEW_LINE_SPAN)?; + } + write!( + w, + r#"{}"#, + maybe_alt, layer, maybe_tooltip, line + )?; + } + if layer == 1 { + write!(w, "")?; + } + Ok(span.hi()) +} + +fn tooltip<'tcx>( + tcx: TyCtxt<'tcx>, + title: &str, + span: Span, + statements: Vec>, + terminator: &Option>, +) -> String { + let source_map = tcx.sess.source_map(); + let mut text = Vec::new(); + text.push(format!("{}: {}:", title, &source_map.span_to_string(span))); + for statement in statements { + let source_range = source_range_no_file(tcx, &statement.source_info.span); + text.push(format!( + "\n{}{}: {}: {}", + TOOLTIP_INDENT, + source_range, + statement_kind_name(&statement), + format!("{:?}", statement) + )); + } + if let Some(term) = terminator { + let source_range = source_range_no_file(tcx, &term.source_info.span); + text.push(format!( + "\n{}{}: {}: {:?}", + TOOLTIP_INDENT, + source_range, + terminator_kind_name(term), + term.kind + )); + } + text.join("") +} + +fn fn_span<'tcx>(tcx: TyCtxt<'tcx>, def_id: DefId) -> Span { + let hir_id = + tcx.hir().local_def_id_to_hir_id(def_id.as_local().expect("expected DefId is local")); + tcx.hir().span(hir_id) +} + +fn hir_body<'tcx>(tcx: TyCtxt<'tcx>, def_id: DefId) -> &'tcx rustc_hir::Body<'tcx> { + let hir_node = tcx.hir().get_if_local(def_id).expect("expected DefId is local"); + let fn_body_id = hir::map::associated_body(hir_node).expect("HIR node is a function with body"); + tcx.hir().body(fn_body_id) +} + +fn escape_html(s: &str) -> String { + s.replace("&", "&").replace("<", "<").replace(">", ">") +} + +fn escape_attr(s: &str) -> String { + s.replace("&", "&") + .replace("\"", """) + .replace("'", "'") + .replace("<", "<") + .replace(">", ">") +} diff --git a/compiler/rustc_session/src/config.rs b/compiler/rustc_session/src/config.rs index 0a2a535598a2f..6861314a88f6f 100644 --- a/compiler/rustc_session/src/config.rs +++ b/compiler/rustc_session/src/config.rs @@ -163,6 +163,21 @@ pub enum LtoCli { Unspecified, } +/// The different settings that the `-Z dump_mir_spanview` flag can have. `Statement` generates a +/// document highlighting each span of every statement (including terminators). `Terminator` and +/// `Block` highlight a single span per `BasicBlock`: the span of the block's `Terminator`, or a +/// computed span for the block, representing the entire range, covering the block's terminator and +/// all of its statements. +#[derive(Clone, Copy, PartialEq, Hash, Debug)] +pub enum MirSpanview { + /// Default `-Z dump_mir_spanview` or `-Z dump_mir_spanview=statement` + Statement, + /// `-Z dump_mir_spanview=terminator` + Terminator, + /// `-Z dump_mir_spanview=block` + Block, +} + #[derive(Clone, PartialEq, Hash)] pub enum LinkerPluginLto { LinkerPlugin(PathBuf), diff --git a/compiler/rustc_session/src/options.rs b/compiler/rustc_session/src/options.rs index ee30c16108a0c..bcf65a1c4d2b0 100644 --- a/compiler/rustc_session/src/options.rs +++ b/compiler/rustc_session/src/options.rs @@ -255,6 +255,7 @@ macro_rules! options { pub const parse_strip: &str = "either `none`, `debuginfo`, or `symbols`"; pub const parse_linker_flavor: &str = ::rustc_target::spec::LinkerFlavor::one_of(); pub const parse_optimization_fuel: &str = "crate=integer"; + pub const parse_mir_spanview: &str = "`statement` (default), `terminator`, or `block`"; pub const parse_unpretty: &str = "`string` or `string=string`"; pub const parse_treat_err_as_bug: &str = "either no value or a number bigger than 0"; pub const parse_lto: &str = @@ -551,6 +552,36 @@ macro_rules! options { } } + fn parse_mir_spanview(slot: &mut Option, v: Option<&str>) -> bool { + if v.is_some() { + let mut bool_arg = None; + if parse_opt_bool(&mut bool_arg, v) { + *slot = if bool_arg.unwrap() { + Some(MirSpanview::Statement) + } else { + None + }; + return true + } + } + + let v = match v { + None => { + *slot = Some(MirSpanview::Statement); + return true; + } + Some(v) => v, + }; + + *slot = Some(match v.trim_end_matches("s") { + "statement" | "stmt" => MirSpanview::Statement, + "terminator" | "term" => MirSpanview::Terminator, + "block" | "basicblock" => MirSpanview::Block, + _ => return false, + }); + true + } + fn parse_treat_err_as_bug(slot: &mut Option, v: Option<&str>) -> bool { match v { Some(s) => { *slot = s.parse().ok().filter(|&x| x != 0); slot.unwrap_or(0) != 0 } @@ -849,6 +880,11 @@ options! {DebuggingOptions, DebuggingSetter, basic_debugging_options, "exclude the pass number when dumping MIR (used in tests) (default: no)"), dump_mir_graphviz: bool = (false, parse_bool, [UNTRACKED], "in addition to `.mir` files, create graphviz `.dot` files (default: no)"), + dump_mir_spanview: Option = (None, parse_mir_spanview, [UNTRACKED], + "in addition to `.mir` files, create `.html` files to view spans for \ + all `statement`s (including terminators), only `terminator` spans, or \ + computed `block` spans (one span encompassing a block's terminator and \ + all statements)."), emit_stack_sizes: bool = (false, parse_bool, [UNTRACKED], "emit a section containing stack size metadata (default: no)"), fewer_names: bool = (false, parse_bool, [TRACKED], diff --git a/src/test/mir-opt/spanview-block.rs b/src/test/mir-opt/spanview-block.rs new file mode 100644 index 0000000000000..fc1d6e0ede66e --- /dev/null +++ b/src/test/mir-opt/spanview-block.rs @@ -0,0 +1,5 @@ +// Test spanview block output +// compile-flags: -Z dump-mir-spanview=block + +// EMIT_MIR spanview_block.main.mir_map.0.html +fn main() {} diff --git a/src/test/mir-opt/spanview-statement.rs b/src/test/mir-opt/spanview-statement.rs new file mode 100644 index 0000000000000..a43ad5e71a393 --- /dev/null +++ b/src/test/mir-opt/spanview-statement.rs @@ -0,0 +1,5 @@ +// Test spanview output (the default value for `-Z dump-mir-spanview` is "statement") +// compile-flags: -Z dump-mir-spanview + +// EMIT_MIR spanview_statement.main.mir_map.0.html +fn main() {} diff --git a/src/test/mir-opt/spanview-terminator.rs b/src/test/mir-opt/spanview-terminator.rs new file mode 100644 index 0000000000000..92e1411eadbda --- /dev/null +++ b/src/test/mir-opt/spanview-terminator.rs @@ -0,0 +1,5 @@ +// Test spanview terminator output +// compile-flags: -Z dump-mir-spanview=terminator + +// EMIT_MIR spanview_terminator.main.mir_map.0.html +fn main() {} diff --git a/src/test/mir-opt/spanview_block.main.mir_map.0.html b/src/test/mir-opt/spanview_block.main.mir_map.0.html new file mode 100644 index 0000000000000..7c1b7bc3b84b0 --- /dev/null +++ b/src/test/mir-opt/spanview_block.main.mir_map.0.html @@ -0,0 +1,67 @@ + + + + coverage_of_if_else - Code Regions + + + +
fn main() @bb0: {}@bb2
+ + diff --git a/src/test/mir-opt/spanview_statement.main.mir_map.0.html b/src/test/mir-opt/spanview_statement.main.mir_map.0.html new file mode 100644 index 0000000000000..f8662a3277a05 --- /dev/null +++ b/src/test/mir-opt/spanview_statement.main.mir_map.0.html @@ -0,0 +1,67 @@ + + + + coverage_of_if_else - Code Regions + + + +
fn main() @bb0[0]: {}@bb0`Goto`@bb2`Return`
+ + diff --git a/src/test/mir-opt/spanview_terminator.main.mir_map.0.html b/src/test/mir-opt/spanview_terminator.main.mir_map.0.html new file mode 100644 index 0000000000000..d0a11a8d2629d --- /dev/null +++ b/src/test/mir-opt/spanview_terminator.main.mir_map.0.html @@ -0,0 +1,66 @@ + + + + coverage_of_if_else - Code Regions + + + +
fn main() {}@bb0`Goto`@bb2`Return`
+ +