Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions crates/mirth-lab/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,5 @@ pub mod miri;
pub mod mutations;
pub mod normalize;
pub mod rustc;
pub mod sitemap;
pub mod uitest;
16 changes: 16 additions & 0 deletions crates/mirth-lab/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,16 @@ mod tools {
pub mod apply_suggestions;
pub mod audit_options;
pub mod callgraph;
pub mod codegen_coverage;
pub mod coverage;
pub mod coverage_compact;
pub mod coverage_flags;
pub mod coverage_generators;
pub mod coverage_static;
pub mod crash_diff;
pub mod debug_check;
pub mod diag_check;
pub mod diag_coverage;
pub mod fuzz;
pub mod fuzz_replay;
pub mod flag_fuzz;
Expand All @@ -37,6 +40,7 @@ mod tools {
pub mod replay;
pub mod repro_diff;
pub mod rewrite_diff;
pub mod rmeta_coverage;
pub mod rustdoc_diff;
pub mod scale_check;
pub mod solver_diff;
Expand Down Expand Up @@ -141,6 +145,14 @@ enum Check {
DebugCheck(tools::debug_check::Args),
/// Mutants of the UI tests that use unstable features (splices, moved items, extra gates, edits): ICEs and hangs.
GateMutate(tools::gate_mutate::Args),
/// Coverage denominators from the compiler's source (arms, configuration branches, gates, delayed bugs, queries, tables).
CoverageStatic(tools::coverage_static::Args),
/// Error codes, lints, diagnostic structs and suggestion applicabilities a corpus makes rustc emit.
DiagCoverage(tools::diag_coverage::Args),
/// Metadata tables encoded and decoded per crate type, from the blessed metadata records.
RmetaCoverage(tools::rmeta_coverage::Args),
/// LLVM intrinsics, Rust intrinsic lowering and calling conventions the corpus reaches.
CodegenCoverage(tools::codegen_coverage::Args),
}

fn main() -> ExitCode {
Expand Down Expand Up @@ -173,6 +185,10 @@ fn main() -> ExitCode {
Check::FlagFuzz(a) => tools::flag_fuzz::run(a),
Check::AuditOptions(a) => tools::audit_options::run(a),
Check::Coverage(a) => tools::coverage::run(a),
Check::CoverageStatic(a) => tools::coverage_static::run(a),
Check::DiagCoverage(a) => tools::diag_coverage::run(a),
Check::RmetaCoverage(a) => tools::rmeta_coverage::run(a),
Check::CodegenCoverage(a) => tools::codegen_coverage::run(a),
Check::CoverageCompact(a) => tools::coverage_compact::run(a),
Check::CoverageGenerators(a) => tools::coverage_generators::run(a),
Check::GrammarCoverage(a) => tools::grammar_coverage::run(a),
Expand Down
181 changes: 181 additions & 0 deletions crates/mirth-lab/src/sitemap.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
//! The coverage build's sites by source position, and what the measured suites reached: enough to
//! ask, for any span of the compiler's source, whether code in it ran, without a rebuild.
//!
//! A block site's span is a position inside the statement or terminator the block starts at, so a
//! span (a match arm's body, a call) "ran" when a site starting inside it was reached. Spans with no
//! site inside take the nearest site before them in the same function (`probe`).

use std::collections::{HashMap, HashSet};
use std::path::{Path, PathBuf};

use crate::coverage::files_with;

/// A position: (line, column), both 1-based as the site tables write them.
pub type Pos = (u32, u32);

pub struct Site {
pub id: String,
pub path: String,
pub pos: Pos,
/// Only reached on a panic path (tag `panics`), or only in a logging macro (`log`).
pub panics: bool,
pub log: bool,
pub snippet: String,
}

pub struct SiteMap {
pub sites: Vec<Site>,
/// File (`compiler/...`) -> indices into `sites`, sorted by position.
by_file: HashMap<String, Vec<usize>>,
}

fn parse_pos(span: &str) -> Option<(String, Pos)> {
let mut it = span.rsplitn(3, ':');
let col = it.next()?.parse().ok()?;
let line = it.next()?.parse().ok()?;
Some((it.next()?.to_owned(), (line, col)))
}

impl SiteMap {
/// The `cover` and `block` sites of the tables in `dir`.
pub fn load(dir: &Path) -> SiteMap {
let mut sites = Vec::new();
let mut by_file: HashMap<String, Vec<usize>> = HashMap::new();
for table in files_with(dir, "sites") {
let text = String::from_utf8_lossy(&std::fs::read(&table).unwrap_or_default()).into_owned();
for line in text.lines() {
let f: Vec<&str> = line.split('\t').collect();
if f.len() < 7 || (f[1] != "cover" && f[1] != "block") {
continue;
}
let Some((file, pos)) = parse_pos(f[6]) else { continue };
let tags: Vec<&str> = if f[1] == "block" { f[5].split(' ').skip(1).collect() } else { vec![] };
by_file.entry(file).or_default().push(sites.len());
sites.push(Site {
id: f[0].into(),
path: f[4].into(),
pos,
panics: tags.contains(&"panics"),
log: tags.contains(&"log"),
snippet: f.get(7).unwrap_or(&"").to_string(),
});
}
}
for v in by_file.values_mut() {
v.sort_by_key(|&i| sites[i].pos);
}
SiteMap { sites, by_file }
}

pub fn files(&self) -> impl Iterator<Item = &String> {
self.by_file.keys()
}

/// The sites starting in [start, end] of `file`; when there are none, the nearest site before
/// `start` that is not before `floor` (the enclosing function's start), if any.
pub fn probe(&self, file: &str, start: Pos, end: Pos, floor: Pos) -> Vec<&Site> {
let Some(v) = self.by_file.get(file) else { return Vec::new() };
let lo = v.partition_point(|&i| self.sites[i].pos < start);
let hi = v.partition_point(|&i| self.sites[i].pos <= end);
if lo < hi {
return v[lo..hi].iter().map(|&i| &self.sites[i]).collect();
}
match lo.checked_sub(1).map(|j| &self.sites[v[j]]) {
Some(s) if s.pos >= floor => vec![s],
_ => Vec::new(),
}
}

/// For a file, the share of its sites whose snippet matches the source at their position:
/// below 0.9 the source has moved since the coverage build and spans cannot be trusted.
pub fn alignment(&self, file: &str, source: &str) -> f64 {
let Some(v) = self.by_file.get(file) else { return 1.0 };
let lines: Vec<&str> = source.lines().collect();
let (mut checked, mut ok) = (0u32, 0u32);
for &i in v.iter().step_by((v.len() / 40).max(1)) {
let s = &self.sites[i];
let want: String = s.snippet.chars().filter(|c| !c.is_whitespace()).take(12).collect();
if want.is_empty() {
continue;
}
let Some(line) = lines.get(s.pos.0 as usize - 1) else { continue };
// Columns count characters; take the rest of the line and the next few lines.
let rest: String = line.chars().skip(s.pos.1 as usize - 1).collect::<String>()
+ &lines.iter().skip(s.pos.0 as usize).take(3).copied().collect::<String>();
let have: String = rest.chars().filter(|c| !c.is_whitespace()).take(want.chars().count()).collect();
checked += 1;
ok += (have == want) as u32;
}
if checked == 0 { 1.0 } else { ok as f64 / checked as f64 }
}
}

/// Every site the measured runs reached: the `union.txt` of each suite under the `suites`
/// directories, and the `V <site>` lines of the raw logs under `logs` (recursively).
pub fn reached(inputs: &Inputs) -> HashSet<String> {
let mut out = HashSet::new();
for suites in &inputs.suites {
let Ok(entries) = std::fs::read_dir(suites) else { continue };
for e in entries.flatten() {
if let Ok(text) = std::fs::read_to_string(e.path().join("union.txt")) {
out.extend(text.split_whitespace().map(str::to_owned));
}
}
}
for dir in &inputs.logs {
for e in walkdir::WalkDir::new(dir).into_iter().filter_map(Result::ok) {
if e.path().extension().is_some_and(|x| x == "log") {
let text = String::from_utf8_lossy(&std::fs::read(e.path()).unwrap_or_default()).into_owned();
out.extend(crate::coverage::log_hits(&text).map(str::to_owned));
}
}
}
out
}

/// The functions `mirth-lab callgraph --json` found unreachable.
pub fn unreachable(gaps_json: &Path) -> HashSet<String> {
let text = std::fs::read_to_string(gaps_json).unwrap_or_default();
let v: serde_json::Value = serde_json::from_str(&text).unwrap_or_default();
v["unreachable"].as_array().into_iter().flatten().filter_map(|x| x.as_str().map(str::to_owned)).collect()
}

/// The Rust files under `root` (`<rust>/compiler`), with their key as the site tables write it.
pub fn compiler_files(rust: &Path) -> Vec<(String, PathBuf)> {
walkdir::WalkDir::new(rust.join("compiler"))
.into_iter()
.filter_map(Result::ok)
.filter(|e| e.path().extension().is_some_and(|x| x == "rs"))
.filter_map(|e| {
let rel = e.path().strip_prefix(rust).ok()?.to_string_lossy().into_owned();
Some((rel, e.path().to_owned()))
})
.collect()
}

/// Defaults shared by the reports.
#[derive(clap::Args, Debug, Clone)]
pub struct Inputs {
/// The rust checkout whose `compiler/` the coverage build was built from (a snapshot works).
#[arg(long, default_value_os_t = home("mirth-work/rust"))]
pub rust: PathBuf,
/// The coverage build's site tables.
#[arg(long, default_value_os_t = home("mirth-work/build-blk/mirth-sites"))]
pub sites: PathBuf,
/// Directories of measured suites (`<suite>/union.txt`) of the block build: by default the
/// block corpus (`cov-blk/suites`) and the suites folded since (`cov-suites`, where the checks'
/// block runs go). A function-only run adds no block sites, so mixing it in only adds
/// function entries.
#[arg(long, default_values_os_t = [home("mirth-work/cov-blk/suites"), home("mirth-work/cov-suites")])]
pub suites: Vec<PathBuf>,
/// Directories of raw logs of the block build (searched recursively).
#[arg(long, default_values_os_t = [home("mirth-work/cov-blk/sink"), home("mirth-work/cov-blk/flags")])]
pub logs: Vec<PathBuf>,
/// `mirth-lab callgraph --json` output for the block build (its `unreachable` list).
#[arg(long, default_value_os_t = home("mirth-work/cov-blk/gaps.json"))]
pub gaps: PathBuf,
}

pub fn home(rel: &str) -> PathBuf {
PathBuf::from(std::env::var_os("HOME").unwrap_or_default()).join(rel)
}
Loading
Loading