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
6 changes: 6 additions & 0 deletions crates/mirth-lab/src/artifacts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,12 @@ use sha2::{Digest, Sha256};
/// `.<7 chars>.rcgu.o`: the per-session suffix of codegen-unit objects.
static SESSION: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"\.[0-9a-z]{7}(\.rcgu\.(?:o|dwo))").unwrap());

/// `data` with the per-session suffixes of codegen-unit object names removed: an incremental
/// session names its objects afresh, so two clean incremental builds differ there only.
pub fn without_session_suffixes(data: &[u8]) -> Vec<u8> {
SESSION.replace_all(data, &b"$1"[..]).into_owned()
}

pub fn sha256(data: &[u8]) -> String {
let mut h = Sha256::new();
h.update(data);
Expand Down
124 changes: 124 additions & 0 deletions crates/mirth-lab/src/compiler_checks.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
//! The patched compiler's own checks (docs/shadow-mode.md, docs/untracked-reads.md), turned on
//! for any compile: `RUSTC_VERIFY_REUSE` recomputes reused (with `all`, every cached) query
//! values at the end of the session and compares them, and `RUSTC_REPORT_UNTRACKED` reports
//! reads of untracked state inside reusable tasks. Both hook the dependency graph, so they act
//! only in incremental sessions: a compile with the checks on gets `-Cincremental`.
//!
//! An untracked read is known when its (what, file) pair is in rustc/untracked-known.tsv: every
//! pair the fuzzer, the replays and the flag walks have reported, each with a verdict in
//! docs/untracked-reads.md, and options known anywhere (file `*`). A new site of "source text"
//! is a note (the class has a verdict: a position or a wording computed from raw text). Anything
//! else is new.

use std::collections::BTreeSet;
use std::path::Path;
use std::sync::LazyLock;

use regex::Regex;

use crate::cargo;

/// The environment that turns the checks on; `all` also recomputes values computed this session.
pub fn env(all: bool) -> [(&'static str, &'static str); 2] {
[("RUSTC_VERIFY_REUSE", if all { "all" } else { "1" }), ("RUSTC_REPORT_UNTRACKED", "1")]
}

/// The (what, file) pairs already reported and triaged, and the reuse reports
/// (rustc/reuse-known.txt).
pub struct Known(BTreeSet<(String, String)>, BTreeSet<String>);

impl Known {
fn contains(&self, what: &str, file: &str) -> bool {
self.0.contains(&(what.to_owned(), file.to_owned())) || self.0.contains(&(what.to_owned(), "*".to_owned()))
}

pub fn load() -> Known {
let path = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../rustc/untracked-known.tsv");
let text = std::fs::read_to_string(path).unwrap_or_default();
let reuse = std::fs::read_to_string(Path::new(env!("CARGO_MANIFEST_DIR")).join("../../rustc/reuse-known.txt")).unwrap_or_default();
let lines = |t: &str| t.lines().filter(|l| !l.starts_with('#') && !l.trim().is_empty()).map(str::to_owned).collect::<Vec<_>>();
Known(
lines(&text).iter().filter_map(|l| l.split_once('\t')).map(|(a, b)| (a.to_owned(), b.to_owned())).collect(),
lines(&reuse).into_iter().collect(),
)
}
}

static READ: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"^rustc-untracked-read: (.*?), read at ([^ ,]+?)(?::\d+:\d+)?, while computing `([^`]+)`").unwrap());
static MODE: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"^rustc-untracked-read: (printing mode `[^`]+`), inherited").unwrap());

/// What the checks reported in one compile's stderr.
#[derive(Default, Debug)]
pub struct Report {
/// Stale reuse (`query <name>`, `metadata`, `codegen unit`, `allocation sharing ...`).
pub reuse: Vec<String>,
/// Untracked reads not in the known list: `<what> at <file> while computing <task>`.
pub untracked: Vec<String>,
/// New sites of a known class (source text).
pub notes: Vec<String>,
}

impl Report {
pub fn findings(&self) -> Vec<String> {
self.reuse.iter().map(|r| format!("verify-reuse: {r}")).chain(self.untracked.iter().map(|u| format!("untracked: {u}"))).collect()
}
}

pub fn read(stderr: &str, known: &Known) -> Report {
let (mut untracked, mut notes) = (BTreeSet::new(), BTreeSet::new());
for line in stderr.lines().filter(|l| l.starts_with("rustc-untracked-read:")) {
if let Some(c) = READ.captures(line) {
if !known.contains(&c[1], &c[2]) {
let what = format!("{} at {} while computing {}", &c[1], &c[2], &c[3]);
if &c[1] == "source text" { notes.insert(what) } else { untracked.insert(what) };
}
} else if let Some(c) = MODE.captures(line) {
if !known.0.iter().any(|(w, _)| w == &c[1]) {
untracked.insert(line.trim_start_matches("rustc-untracked-read: ").to_owned());
}
} else {
untracked.insert(line.to_owned());
}
}
let (known_reuse, reuse): (Vec<String>, Vec<String>) = cargo::reuse_checks(stderr).into_iter().partition(|r| known.1.contains(r));
notes.extend(known_reuse.into_iter().map(|r| format!("known reuse report: {r}")));
Report { reuse, untracked: untracked.into_iter().collect(), notes: notes.into_iter().collect() }
}

/// The stderr without the checks' lines (a reuse report's indented continuation lines too),
/// for comparing diagnostics.
pub fn strip(stderr: &str) -> String {
let mut out = String::new();
let mut in_report = false;
for l in stderr.lines() {
if l.starts_with("rustc-untracked-read:") || l.starts_with("rustc-verify-reuse") {
in_report = true;
continue;
}
if in_report && l.starts_with(" ") {
continue;
}
in_report = false;
out.push_str(l);
out.push('\n');
}
out
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn known_and_new_reads() {
let known = Known([("the untracked option jobs".to_owned(), "compiler/rustc_query_impl/src/execution.rs".to_owned())].into(), Default::default());
let log = "rustc-untracked-read: the untracked option jobs, read at compiler/rustc_query_impl/src/execution.rs:295:24, while computing `crate_name`, whose result incremental compilation may reuse\n\
rustc-untracked-read: the untracked option no_leak_check, read at compiler/rustc_infer/src/infer/relate/higher_ranked.rs:95:47, while computing `typeck_root`, whose result incremental compilation may reuse\n\
rustc-verify-reuse: metadata differs at byte 5\n allocation: memory, 0 bytes\nerror: x\n";
let r = read(log, &known);
assert_eq!(r.untracked, vec!["the untracked option no_leak_check at compiler/rustc_infer/src/infer/relate/higher_ranked.rs while computing typeck_root"]);
assert_eq!(r.reuse, vec!["metadata"]);
assert_eq!(strip(log), "error: x\n");
}
}
4 changes: 4 additions & 0 deletions crates/mirth-lab/src/driver.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,10 @@ pub struct Sweep {
/// Run only the tests that have findings under <work>/findings.
#[arg(long)]
pub recheck: bool,
/// Compile with the patched compiler's own checks on (RUSTC_VERIFY_REUSE=all,
/// RUSTC_REPORT_UNTRACKED; an incremental session), where the check supports it.
#[arg(long)]
pub compiler_checks: bool,
}

impl Sweep {
Expand Down
1 change: 1 addition & 0 deletions crates/mirth-lab/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

pub mod artifacts;
pub mod cargo;
pub mod compiler_checks;
pub mod coverage;
pub mod driver;
pub mod miri;
Expand Down
4 changes: 4 additions & 0 deletions crates/mirth-lab/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ mod tools {
pub mod survey;
pub mod ui_coverage;
pub mod ui_fuzz;
pub mod ui_incr;
pub mod verify_closed;
pub mod xlink;
}
Expand Down Expand Up @@ -141,6 +142,8 @@ 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),
/// P6 over the UI corpus: incremental rebuilds (unchanged, spans moved, an unused fn added) equal clean builds, with the compiler checking its reuse.
UiIncr(tools::ui_incr::Args),
}

fn main() -> ExitCode {
Expand All @@ -164,6 +167,7 @@ fn main() -> ExitCode {
Check::ScaleCheck(a) => tools::scale_check::run(a),
Check::AbiDiff(a) => tools::abi_diff::run(a),
Check::UiFuzz(a) => tools::ui_fuzz::run(a),
Check::UiIncr(a) => tools::ui_incr::run(a),
Check::Callgraph(a) => tools::callgraph::run(a),
Check::FlagUniverse(a) => tools::flag_universe::run(a),
Check::FlagModel(a) => tools::flag_model::run(a),
Expand Down
28 changes: 27 additions & 1 deletion crates/mirth-lab/src/rustc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,11 @@ pub struct Compile<'a> {
/// Name the output `<out_dir>/prog` with `-o` (the default); off when the extra options say
/// where outputs go (`--out-dir`).
pub name_output: bool,
/// More environment for rustc (the patched compiler's checks).
pub env: Vec<(String, String)>,
/// `-Cincremental=<dir>`, passed before the test's flags (which may end with an option that
/// takes a value, such as `--cap-lints`).
pub incremental: Option<PathBuf>,
}

impl<'a> Compile<'a> {
Expand All @@ -144,9 +149,26 @@ impl<'a> Compile<'a> {
timeout: Duration::from_secs(300),
bootstrap: true,
name_output: true,
env: Vec::new(),
incremental: None,
}
}

pub fn env<K: Into<String>, V: Into<String>>(mut self, k: K, v: V) -> Self {
self.env.push((k.into(), v.into()));
self
}

/// Turn the patched compiler's checks on (docs/shadow-mode.md), in an incremental session
/// under `incr`: `all` recomputes every cached value, not only reused ones.
pub fn compiler_checks(mut self, incr: &Path, all: bool) -> Self {
self.incremental = Some(incr.to_path_buf());
for (k, v) in crate::compiler_checks::env(all) {
self = self.env(k, v);
}
self
}

pub fn extra<I: IntoIterator<Item = S>, S: Into<String>>(mut self, extra: I) -> Self {
self.extra.extend(extra.into_iter().map(Into::into));
self
Expand Down Expand Up @@ -190,12 +212,16 @@ impl<'a> Compile<'a> {
if self.bootstrap {
cmd.args(["-Zunstable-options", "-Ainternal_features", "-Aincomplete_features"]);
}
if let Some(incr) = &self.incremental {
cmd.arg(format!("-Cincremental={}", incr.display()));
}
cmd
.arg(if self.json { "--error-format=json" } else { "--error-format=short" })
.args(self.flags)
.args(&self.extra)
.current_dir(self.out_dir)
.env("RUST_BACKTRACE", "0");
.env("RUST_BACKTRACE", "0")
.envs(self.env.iter().map(|(k, v)| (k.as_str(), v.as_str())));
if self.bootstrap {
cmd.env("RUSTC_BOOTSTRAP", "1");
} else {
Expand Down
25 changes: 19 additions & 6 deletions crates/mirth-lab/src/tools/diag_check.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,16 @@
//!
//! Errors without any span and exact duplicates are notes. Tests that ask for compiler internals
//! on purpose (verbose printing, dump attributes) are left out.
//!
//! With --compiler-checks the compile is an incremental session with the patched compiler's
//! checks on: stale or order-dependent query values and new untracked reads are findings too.

use std::collections::{BTreeMap, BTreeSet};
use std::path::{Path, PathBuf};
use std::process::ExitCode;
use std::sync::LazyLock;

use mirth_lab::compiler_checks::{self, Known};
use mirth_lab::driver::{self, Record, Sweep};
use mirth_lab::rustc::{self, Compile, Status};
use mirth_lab::uitest::{self, Test};
Expand All @@ -39,6 +43,7 @@ static DEBUG_TEST: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(r"#!?\[rustc_(dump|effective_visibility|regions|variance|outlives|layout|abi|def_path|symbol_name|object_lifetime_default|evaluate_where_clauses|then_this_would_need|if_this_changed|clean|partition)").unwrap()
});
static DEBUG_FLAG: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"verbose|-Zdump|unpretty|print-").unwrap());
static KNOWN: LazyLock<Known> = LazyLock::new(Known::load);
static SUMMARY: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"^(aborting due to|could not compile|\d+ (previous )?errors?)").unwrap());

Expand Down Expand Up @@ -77,11 +82,11 @@ fn file_info(cache: &mut BTreeMap<String, Option<(usize, usize)>>, test_dir: &Pa

fn check(args: &Args, test: &Test) -> Rec {
let dir = driver::scratch_dir(&args.sweep);
let c = Compile::new(&args.rustc, &test.path, dir.path(), &test.flags, test.edition())
.emit("metadata")
.json()
.timeout(120)
.run();
let mut compile = Compile::new(&args.rustc, &test.path, dir.path(), &test.flags, test.edition()).emit("metadata").json().timeout(120);
if args.sweep.compiler_checks {
compile = compile.compiler_checks(&dir.path().join("incr"), true);
}
let c = compile.run();
let mut rec = Rec { test: test.rel.clone(), skip: None, found: Vec::new(), notes: Vec::new() };
if matches!(c.status, Status::Ice | Status::Timeout) {
rec.skip = Some(format!("{:?}", c.status).to_lowercase());
Expand Down Expand Up @@ -127,7 +132,15 @@ fn check(args: &Args, test: &Test) -> Rec {
rec.notes.push(format!("duplicate x{n}: {}", message.chars().take(100).collect::<String>()));
}
}
rec.found = found.iter().map(|f| format!("{}: {}", f.what, f.detail.chars().take(120).collect::<String>())).collect();
if args.sweep.compiler_checks {
let report = compiler_checks::read(&c.stderr, &KNOWN);
rec.notes.extend(report.notes.iter().map(|n| if n.starts_with("known reuse") { n.clone() } else { format!("untracked site: {n}") }));
for f in report.findings() {
let (what, detail) = f.split_once(": ").unwrap_or(("compiler-check", &f));
found.insert(Finding { what: what.into(), detail: detail.into(), code: String::new() });
}
}
rec.found = found.iter().map(|f| format!("{}: {}", f.what, f.detail.chars().take(200).collect::<String>())).collect();
if !found.is_empty() {
driver::write_finding(&args.sweep.work, test, &[], &serde_json::json!({ "found": found, "notes": rec.notes }));
}
Expand Down
36 changes: 33 additions & 3 deletions crates/mirth-lab/src/tools/gate_mutate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ use std::process::{Command, ExitCode};
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
use std::sync::{LazyLock, Mutex};

use mirth_lab::compiler_checks::{self, Known};
use mirth_lab::mutations;
use mirth_lab::rustc::{Compile, Status};
use mirth_lab::uitest::{self, Test};
Expand Down Expand Up @@ -71,6 +72,11 @@ pub struct Args {
/// Stop at the first new signature (exit 3).
#[arg(long)]
pause_on_finding: bool,
/// Compile each mutant in an incremental session with the patched compiler's own checks on
/// (RUSTC_VERIFY_REUSE=all, RUSTC_REPORT_UNTRACKED); what they report goes to
/// <work>/compiler-checks.jsonl, with the mutant under <work>/compiler-checks/.
#[arg(long)]
compiler_checks: bool,
/// Only features whose name contains this.
#[arg(long)]
only: Option<String>,
Expand All @@ -82,6 +88,7 @@ pub struct Args {
rereduce: bool,
}

static KNOWN: LazyLock<Known> = LazyLock::new(Known::load);
static FEATURE: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"#!\[feature\(([^)\]]*)\)\]").unwrap());
static INCOMPLETE: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"(?m)^\s*\(incomplete, (\w+),").unwrap());
/// Tests that are meant to crash, or whose output is already an ICE.
Expand Down Expand Up @@ -390,11 +397,25 @@ struct Env<'a> {
/// Status and signature (for an ICE, or "hang" for a confirmed timeout), and whether the
/// compiler complained of a missing feature gate.
fn compile(env: &Env, text: &str, flags: &[String], edition: &str) -> (Status, Option<Signature>, bool) {
let (status, sig, ungated, _) = compile_checked(env, text, flags, edition, false);
(status, sig, ungated)
}

/// `compile`, and with `checks` what the patched compiler's own checks reported.
fn compile_checked(env: &Env, text: &str, flags: &[String], edition: &str, checks: bool) -> (Status, Option<Signature>, bool, Vec<String>) {
let d = tempfile::tempdir_in(&env.scratch).expect("scratch");
let src = d.path().join("m.rs");
let _ = std::fs::write(&src, text);
let run = |secs| Compile::new(&env.args.rustc, &src, d.path(), flags, edition).emit("link").timeout(secs).run();
let c = run(env.args.timeout);
let first = Compile::new(&env.args.rustc, &src, d.path(), flags, edition).emit("link").timeout(env.args.timeout);
let first = if checks { first.compiler_checks(&d.path().join("incr"), true) } else { first };
let c = first.run();
let reported = if checks { compiler_checks::read(&c.stderr, &KNOWN).findings() } else { Vec::new() };
let (status, sig, ungated) = classify(env, &src, d.path(), flags, edition, c);
(status, sig, ungated, reported)
}

fn classify(env: &Env, src: &Path, dir: &Path, flags: &[String], edition: &str, c: mirth_lab::rustc::Compiled) -> (Status, Option<Signature>, bool) {
let run = |secs| Compile::new(&env.args.rustc, src, dir, flags, edition).emit("link").timeout(secs).run();
let ungated = c.stderr.contains("E0658");
match c.status {
Status::Ice => (Status::Ice, Some(signature(&c.stderr)), ungated),
Expand Down Expand Up @@ -630,7 +651,16 @@ pub fn run(args: Args) -> anyhow::Result<ExitCode> {
let _ = writeln!(l, "{}", serde_json::json!({"i": i, "strategy": "none"}));
return;
};
let (status, sig, _) = compile(&env, &text, &flags, &edition);
let (status, sig, _, reported) = compile_checked(&env, &text, &flags, &edition, args.compiler_checks);
if !reported.is_empty() {
let dir = args.work.join("compiler-checks");
let _ = std::fs::create_dir_all(&dir);
let _ = std::fs::write(dir.join(format!("{i}.rs")), &text);
let line = serde_json::json!({ "i": i, "strategy": mutant.strategy, "sources": mutant.sources, "config": mutant.config, "flags": flags, "edition": edition, "found": reported });
if let Ok(mut f) = std::fs::OpenOptions::new().create(true).append(true).open(args.work.join("compiler-checks.jsonl")) {
let _ = writeln!(f, "{line}");
}
}
let mut key = None;
if let Some(sig) = sig {
// Not a finding when the unmutated test already gives it.
Expand Down
Loading