From 0b56a6439aef8bc4e02b4ffd91e51d1fa35ce3a9 Mon Sep 17 00:00:00 2001 From: Zack Maril Date: Sat, 10 Oct 2026 12:50:30 +0000 Subject: [PATCH 1/2] Turn the patched compiler's own checks on in the corpus sweeps: --compiler-checks (diag-check, gate-mutate), and ui-incr (P6 over the UI corpus with the reuse check); known untracked reads and reuse reports in rustc/ Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01QXiEXbESemwqMLYKaWLDbT --- crates/mirth-lab/src/compiler_checks.rs | 124 +++++++++++ crates/mirth-lab/src/driver.rs | 4 + crates/mirth-lab/src/lib.rs | 1 + crates/mirth-lab/src/main.rs | 4 + crates/mirth-lab/src/rustc.rs | 21 +- crates/mirth-lab/src/tools/diag_check.rs | 25 ++- crates/mirth-lab/src/tools/gate_mutate.rs | 36 +++- crates/mirth-lab/src/tools/ui_incr.rs | 249 ++++++++++++++++++++++ rustc/big-run.sh | 2 + rustc/reuse-known.txt | 7 + rustc/untracked-known.tsv | 95 +++++++++ 11 files changed, 558 insertions(+), 10 deletions(-) create mode 100644 crates/mirth-lab/src/compiler_checks.rs create mode 100644 crates/mirth-lab/src/tools/ui_incr.rs create mode 100644 rustc/reuse-known.txt create mode 100644 rustc/untracked-known.tsv diff --git a/crates/mirth-lab/src/compiler_checks.rs b/crates/mirth-lab/src/compiler_checks.rs new file mode 100644 index 0000000..fb92d1a --- /dev/null +++ b/crates/mirth-lab/src/compiler_checks.rs @@ -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); + +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::>(); + 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 = + LazyLock::new(|| Regex::new(r"^rustc-untracked-read: (.*?), read at ([^ ,]+?)(?::\d+:\d+)?, while computing `([^`]+)`").unwrap()); +static MODE: LazyLock = 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 `, `metadata`, `codegen unit`, `allocation sharing ...`). + pub reuse: Vec, + /// Untracked reads not in the known list: ` at while computing `. + pub untracked: Vec, + /// New sites of a known class (source text). + pub notes: Vec, +} + +impl Report { + pub fn findings(&self) -> Vec { + 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, Vec) = 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"); + } +} diff --git a/crates/mirth-lab/src/driver.rs b/crates/mirth-lab/src/driver.rs index 673f6bf..169ff6e 100644 --- a/crates/mirth-lab/src/driver.rs +++ b/crates/mirth-lab/src/driver.rs @@ -39,6 +39,10 @@ pub struct Sweep { /// Run only the tests that have findings under /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 { diff --git a/crates/mirth-lab/src/lib.rs b/crates/mirth-lab/src/lib.rs index ca3d0e3..ea19746 100644 --- a/crates/mirth-lab/src/lib.rs +++ b/crates/mirth-lab/src/lib.rs @@ -3,6 +3,7 @@ pub mod artifacts; pub mod cargo; +pub mod compiler_checks; pub mod coverage; pub mod driver; pub mod miri; diff --git a/crates/mirth-lab/src/main.rs b/crates/mirth-lab/src/main.rs index d15eceb..5c50a68 100644 --- a/crates/mirth-lab/src/main.rs +++ b/crates/mirth-lab/src/main.rs @@ -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; } @@ -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 { @@ -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), diff --git a/crates/mirth-lab/src/rustc.rs b/crates/mirth-lab/src/rustc.rs index 8952f3c..3c8cfcf 100644 --- a/crates/mirth-lab/src/rustc.rs +++ b/crates/mirth-lab/src/rustc.rs @@ -128,6 +128,8 @@ pub struct Compile<'a> { /// Name the output `/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)>, } impl<'a> Compile<'a> { @@ -144,9 +146,25 @@ impl<'a> Compile<'a> { timeout: Duration::from_secs(300), bootstrap: true, name_output: true, + env: Vec::new(), } } + pub fn env, V: Into>(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.extra.push(format!("-Cincremental={}", incr.display())); + for (k, v) in crate::compiler_checks::env(all) { + self = self.env(k, v); + } + self + } + pub fn extra, S: Into>(mut self, extra: I) -> Self { self.extra.extend(extra.into_iter().map(Into::into)); self @@ -195,7 +213,8 @@ impl<'a> Compile<'a> { .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 { diff --git a/crates/mirth-lab/src/tools/diag_check.rs b/crates/mirth-lab/src/tools/diag_check.rs index cafbbc7..8e73804 100644 --- a/crates/mirth-lab/src/tools/diag_check.rs +++ b/crates/mirth-lab/src/tools/diag_check.rs @@ -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}; @@ -39,6 +43,7 @@ static DEBUG_TEST: LazyLock = 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 = LazyLock::new(|| Regex::new(r"verbose|-Zdump|unpretty|print-").unwrap()); +static KNOWN: LazyLock = LazyLock::new(Known::load); static SUMMARY: LazyLock = LazyLock::new(|| Regex::new(r"^(aborting due to|could not compile|\d+ (previous )?errors?)").unwrap()); @@ -77,11 +82,11 @@ fn file_info(cache: &mut BTreeMap>, 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()); @@ -127,7 +132,15 @@ fn check(args: &Args, test: &Test) -> Rec { rec.notes.push(format!("duplicate x{n}: {}", message.chars().take(100).collect::())); } } - rec.found = found.iter().map(|f| format!("{}: {}", f.what, f.detail.chars().take(120).collect::())).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::())).collect(); if !found.is_empty() { driver::write_finding(&args.sweep.work, test, &[], &serde_json::json!({ "found": found, "notes": rec.notes })); } diff --git a/crates/mirth-lab/src/tools/gate_mutate.rs b/crates/mirth-lab/src/tools/gate_mutate.rs index 94065dc..bc324f6 100644 --- a/crates/mirth-lab/src/tools/gate_mutate.rs +++ b/crates/mirth-lab/src/tools/gate_mutate.rs @@ -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}; @@ -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 + /// /compiler-checks.jsonl, with the mutant under /compiler-checks/. + #[arg(long)] + compiler_checks: bool, /// Only features whose name contains this. #[arg(long)] only: Option, @@ -82,6 +88,7 @@ pub struct Args { rereduce: bool, } +static KNOWN: LazyLock = LazyLock::new(Known::load); static FEATURE: LazyLock = LazyLock::new(|| Regex::new(r"#!\[feature\(([^)\]]*)\)\]").unwrap()); static INCOMPLETE: LazyLock = LazyLock::new(|| Regex::new(r"(?m)^\s*\(incomplete, (\w+),").unwrap()); /// Tests that are meant to crash, or whose output is already an ICE. @@ -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, 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, bool, Vec) { 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, 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), @@ -630,7 +651,16 @@ pub fn run(args: Args) -> anyhow::Result { 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. diff --git a/crates/mirth-lab/src/tools/ui_incr.rs b/crates/mirth-lab/src/tools/ui_incr.rs new file mode 100644 index 0000000..62f703b --- /dev/null +++ b/crates/mirth-lab/src/tools/ui_incr.rs @@ -0,0 +1,249 @@ +//! P6 over the UI corpus: an incremental rebuild must give what a clean build of the same source +//! gives, for every standalone UI test, with the patched compiler checking its own reuse. +//! +//! Per test, in one incremental directory: a clean session (RUSTC_VERIFY_REUSE=all, so every +//! cached value is also recomputed and compared), then three rebuilds, each with +//! RUSTC_VERIFY_REUSE=1 and RUSTC_REPORT_UNTRACKED=1: +//! +//! - unchanged: nothing edited; +//! - shift: a blank line first, which moves every span by a line; +//! - unused: an unused private fn appended. +//! +//! Each rebuild is compared with a clean build of the same source in a fresh incremental +//! directory, at the same paths: the diagnostics (as a multiset; a different order is a note), +//! the output bytes (metadata for check tests, the program otherwise), and for run tests the +//! program's output when the bytes differ. A byte difference between two clean builds makes the +//! test nondeterministic, and it is skipped. Stale reuse and new untracked reads the compiler +//! reports in any session are findings too. +//! +//! The reuse check recomputes green values with their providers, and a provider that emits a +//! lint emits it again, printed without trimmed paths (the check runs under +//! `with_no_trimmed_paths`). An incremental diagnostic at the location of a clean one but worded +//! differently is that re-emission, a note; an identical extra one is a duplicate, a finding. +//! Summary lines ("N warnings emitted") are left out of the comparison. + +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::normalize; +use mirth_lab::rustc::{Compile, Compiled, Status, observe}; +use mirth_lab::uitest::{self, Kind, Test}; +use regex::Regex; +use serde::Serialize; + +#[derive(clap::Args, Debug)] +pub struct Args { + #[arg(long)] + rustc: PathBuf, + #[command(flatten)] + sweep: Sweep, +} + +static KNOWN: LazyLock = LazyLock::new(Known::load); +/// Tests that set up incremental compilation or code generation themselves, or ask for output +/// that names the session. +static OWN: LazyLock = LazyLock::new(|| Regex::new(r"incremental|-Ccodegen-units|save-temps|-Zthreads|print-|dump-|unpretty|emit").unwrap()); +/// Run tests whose output depends on more than the program (threads, time, addresses). +static SUMMARY: LazyLock = LazyLock::new(|| Regex::new(r"^(warning|error): (\d+ warnings? emitted|aborting due to)").unwrap()); +static LOCATION: LazyLock = LazyLock::new(|| Regex::new(r"^(\S+:\d+:\d+: \w+)").unwrap()); + +/// The incremental diagnostics that are not the reuse check's re-emissions (see the module +/// comment), and how many were. +fn without_reemissions(inc: &[String], clean: &[String]) -> (Vec, usize) { + let mut rest: Vec = clean.to_vec(); + let mut kept = Vec::new(); + let mut extra = Vec::new(); + for l in inc { + if let Some(i) = rest.iter().position(|c| c == l) { + rest.remove(i); + kept.push(l.clone()); + } else { + extra.push(l.clone()); + } + } + let loc = |l: &str| LOCATION.captures(l).map(|c| c[1].to_owned()); + let mut reemitted = 0; + for l in extra { + if loc(&l).is_some_and(|k| clean.iter().any(|c| loc(c).as_deref() == Some(k.as_str()) && c != &l)) { + reemitted += 1; + } else { + kept.push(l); + } + } + kept.sort(); + (kept, reemitted) +} + +static RACY: LazyLock = LazyLock::new(|| Regex::new(r"thread::spawn|std::thread|Instant::now|SystemTime|RandomState|HashMap|HashSet|\{:p\}|as \*const").unwrap()); + +#[cfg(test)] +mod tests { + #[test] + fn reemission_is_not_a_finding() { + let clean = vec!["a.rs:20:13: warning: trivial cast: `&dyn Any`".to_owned()]; + let inc = vec![clean[0].clone(), "a.rs:20:13: warning: trivial cast: `&dyn std::any::Any`".to_owned()]; + assert_eq!(super::without_reemissions(&inc, &clean), (clean.clone(), 1)); + let dup = vec![clean[0].clone(), clean[0].clone()]; + assert_eq!(super::without_reemissions(&dup, &clean).0.len(), 2); + } +} + +#[derive(Serialize, Default)] +struct Rec { + test: String, + #[serde(skip_serializing_if = "Option::is_none")] + skip: Option, + status: String, + found: Vec, + notes: Vec, +} + +impl Record for Rec { + fn findings(&self) -> Vec { + self.found.clone() + } + fn test(&self) -> &str { + &self.test + } +} + +const STEPS: [&str; 3] = ["unchanged", "shift", "unused"]; + +fn edit(step: &str, text: &str) -> Option { + match step { + "unchanged" => Some(text.to_owned()), + // A shebang must stay on the first line; `#![` is an inner attribute, not a shebang. + "shift" if text.starts_with("#!") && !text.starts_with("#![") => None, + "shift" => Some(format!("\n{text}")), + "unused" => Some(format!("{text}\n#[allow(dead_code, unused)]\nfn __mirth_unused_fn() {{}}\n")), + _ => None, + } +} + +struct Build { + status: Status, + diags: Vec, + bytes: Option>, + stderr: String, +} + +/// One session: compile `src` with incremental state in `incr`, outputs to `out/prog`. +fn build(args: &Args, test: &Test, src: &Path, out: &Path, incr: &Path, all: bool, emit: &str) -> Build { + let _ = std::fs::remove_file(out.join("prog")); + let c: Compiled = Compile::new(&args.rustc, src, out, &test.flags, test.edition()) + .emit(emit) + .timeout(120) + .compiler_checks(incr, all) + .run(); + let stderr = compiler_checks::strip(&c.stderr); + let mut diags: Vec = stderr.lines().filter(|l| !l.trim().is_empty() && !SUMMARY.is_match(l)).map(str::to_owned).collect(); + diags.sort(); + Build { status: c.status, diags, bytes: std::fs::read(out.join("prog")).ok(), stderr: c.stderr } +} + +fn check(args: &Args, test: &Test) -> Rec { + let mut rec = Rec { test: test.rel.clone(), ..Default::default() }; + let Ok(text) = std::fs::read_to_string(&test.path) else { + rec.skip = Some("unreadable".into()); + return rec; + }; + let d = driver::scratch_dir(&args.sweep); + let srcdir = d.path().join("src"); + let out = d.path().join("out"); + let _ = std::fs::create_dir_all(&srcdir); + let _ = std::fs::create_dir_all(&out); + let src = srcdir.join(test.file_name()); + let runs = test.kind == Some(Kind::RunPass); + let emit = if Kind::is_check(test.kind) { "metadata" } else { "link" }; + let incr = d.path().join("incr"); + let _ = std::fs::write(&src, &text); + let first = build(args, test, &src, &out, &incr, true, emit); + if matches!(first.status, Status::Ice | Status::Timeout) { + rec.skip = Some(format!("clean session: {:?}", first.status).to_lowercase()); + return rec; + } + rec.status = format!("{:?}", first.status).to_lowercase(); + let mut found: Vec = Vec::new(); + let mut checks = compiler_checks::read(&first.stderr, &KNOWN); + let mut files: Vec<(String, Vec)> = Vec::new(); + for step in STEPS { + let Some(edited) = edit(step, &text) else { continue }; + let _ = std::fs::write(&src, &edited); + let inc = build(args, test, &src, &out, &incr, false, emit); + let inc_prog = inc.bytes.clone(); + let more = compiler_checks::read(&inc.stderr, &KNOWN); + checks.reuse.extend(more.reuse.iter().map(|r| format!("{step}: {r}"))); + checks.untracked.extend(more.untracked); + checks.notes.extend(more.notes); + let clean = |n: u32| build(args, test, &src, &out, &d.path().join(format!("clean-{step}-{n}")), false, emit); + let c1 = clean(1); + if inc.status != c1.status { + found.push(format!("{step}: status: incremental {:?}, clean {:?}", inc.status, c1.status)); + files.push((format!("{step}.incremental.stderr"), inc.stderr.clone().into_bytes())); + files.push((format!("{step}.clean.stderr"), c1.stderr.clone().into_bytes())); + continue; + } + let (inc_diags, reemitted) = without_reemissions(&inc.diags, &c1.diags); + if reemitted > 0 { + rec.notes.push(format!("{step}: {reemitted} diagnostics re-emitted by the reuse check")); + } + if inc_diags != c1.diags { + found.push(format!("{step}: diagnostics differ ({} incremental lines, {} clean)", inc_diags.len(), c1.diags.len())); + files.push((format!("{step}.incremental.stderr"), compiler_checks::strip(&inc.stderr).into_bytes())); + files.push((format!("{step}.clean.stderr"), compiler_checks::strip(&c1.stderr).into_bytes())); + } else if compiler_checks::strip(&inc.stderr) != compiler_checks::strip(&c1.stderr) { + rec.notes.push(format!("{step}: diagnostics in another order")); + } + if inc_prog != c1.bytes { + // Two clean builds that differ make the comparison meaningless. + let c2 = clean(2); + if c2.bytes != c1.bytes { + rec.skip = Some(format!("{step}: two clean builds differ")); + continue; + } + let mut what = format!("{step}: {emit} bytes differ"); + if runs + && !RACY.is_match(&text) + && let (Some(a), Some(b)) = (&inc_prog, &c1.bytes) + { + let run = |bytes: &[u8], name: &str| { + let p = out.join(name); + let _ = std::fs::write(&p, bytes); + let _ = std::fs::set_permissions(&p, std::os::unix::fs::PermissionsExt::from_mode(0o755)); + let o = observe(&p, 20, &[]); + (o.exit, normalize::stdout(&o.stdout), normalize::stderr(&o.stderr)) + }; + let (oi, oc) = (run(a, "prog-incremental"), run(b, "prog-clean")); + what.push_str(if oi == oc { " (same run output)" } else { "; the programs behave differently" }); + } + found.push(what); + if let Some(b) = &inc_prog { + files.push((format!("{step}.incremental.out"), b.clone())); + } + if let Some(b) = &c1.bytes { + files.push((format!("{step}.clean.out"), b.clone())); + } + } + } + found.extend(checks.findings()); + found.sort(); + found.dedup(); + checks.notes.sort(); + checks.notes.dedup(); + rec.notes.extend(checks.notes.iter().map(|n| if n.starts_with("known reuse") { n.clone() } else { format!("untracked site: {n}") })); + if !found.is_empty() { + driver::write_finding(&args.sweep.work, test, &files, &serde_json::json!({ "found": found, "notes": rec.notes })); + } + rec.found = found; + rec +} + +pub fn run(args: Args) -> anyhow::Result { + let tests = uitest::tests(&args.sweep.tests, uitest::ALL, |t| uitest::flag_matches(t, &OWN)); + let tests = args.sweep.select(tests); + println!("{} tests", tests.len()); + Ok(driver::drive(&tests, &args.sweep, |t| check(&args, t))) +} diff --git a/rustc/big-run.sh b/rustc/big-run.sh index ae40a59..dfa84da 100755 --- a/rustc/big-run.sh +++ b/rustc/big-run.sh @@ -44,6 +44,8 @@ run() { # name args... sweep=(--tests "$tests" --jobs "$jobs") run diag-check diag-check --rustc "$rustc" "${sweep[@]}" --work "$dir/diag-check" +run diag-check-cc diag-check --rustc "$rustc" "${sweep[@]}" --compiler-checks --work "$dir/diag-check-cc" +run ui-incr ui-incr --rustc "$rustc" "${sweep[@]}" --work "$dir/ui-incr" run gate-check gate-check --rustc "$rustc" --rust "$rust" --jobs "$jobs" --work "$dir/gate-check" run solver-diff solver-diff --rustc "$rustc" "${sweep[@]}" --work "$dir/solver-diff" run crash-diff crash-diff --rustc "$rustc" --checked "$checked" --known "$here/crash-known.txt" "${sweep[@]}" --work "$dir/crash-diff" diff --git a/rustc/reuse-known.txt b/rustc/reuse-known.txt new file mode 100644 index 0000000..d23ce48 --- /dev/null +++ b/rustc/reuse-known.txt @@ -0,0 +1,7 @@ +# Reuse-check reports already triaged (docs/checks.md, fourth batch): exact `reuse_checks` names. +# Const-eval results that share an allocation in a clean session (the PostAnalysis and Codegen +# typing modes of one constant or promoted) get two after the round trip through the cache; +# finding 8's mechanism. No output bytes or behaviour differed in any test that showed it. +allocation sharing eval_to_const_value_raw / eval_to_const_value_raw / Codegen / PostAnalysis +allocation sharing eval_to_const_value_raw / eval_to_allocation_raw / Codegen / PostAnalysis +allocation sharing eval_to_const_value_raw / eval_to_allocation_raw / Codegen diff --git a/rustc/untracked-known.tsv b/rustc/untracked-known.tsv new file mode 100644 index 0000000..652870d --- /dev/null +++ b/rustc/untracked-known.tsv @@ -0,0 +1,95 @@ +# Untracked reads already reported and triaged (docs/untracked-reads.md): whatfile. +# From every untracked.txt of the fuzzer, replays and flag walks (~/mirth-work), 2026-10-10. +proc macro quoted spans compiler/rustc_metadata/src/rmeta/encoder.rs +source file contents compiler/rustc_codegen_llvm/src/debuginfo/metadata.rs +source text compiler/rustc_const_eval/src/const_eval/error.rs +source text compiler/rustc_hir_analysis/src/check/mod.rs +source text compiler/rustc_hir_typeck/src/cast.rs +source text compiler/rustc_hir_typeck/src/expr.rs +source text compiler/rustc_hir_typeck/src/fn_ctxt/suggestions.rs +source text compiler/rustc_hir_typeck/src/loops.rs +source text compiler/rustc_hir_typeck/src/op.rs +source text compiler/rustc_lint/src/builtin.rs +source text compiler/rustc_lint/src/types/literal.rs +source text compiler/rustc_lint/src/unused/must_use.rs +source text compiler/rustc_span/src/source_map.rs +source text compiler/rustc_trait_selection/src/error_reporting/traits/suggestions.rs +the untracked option build_sdylib_interface compiler/rustc_middle/src/ty/context.rs +the untracked option check_cfg_all_expected compiler/rustc_attr_parsing/src/check_cfg.rs +the untracked option cli_forced_codegen_units compiler/rustc_session/src/session.rs +the untracked option cli_forced_local_thinlto_off compiler/rustc_session/src/session.rs +the untracked option codegen_source_order compiler/rustc_middle/src/mono.rs +the untracked option diagnostic_width compiler/rustc_session/src/session.rs +the untracked option emit_stack_sizes compiler/rustc_codegen_llvm/src/back/write.rs +the untracked option externs compiler/rustc_lint/src/internal.rs +the untracked option extra_filename compiler/rustc_metadata/src/rmeta/encoder.rs +the untracked option future_incompat_test compiler/rustc_borrowck/src/lib.rs +the untracked option future_incompat_test compiler/rustc_hir_analysis/src/check_unused.rs +the untracked option future_incompat_test compiler/rustc_hir_analysis/src/hir_ty_lowering/dyn_trait.rs +the untracked option future_incompat_test compiler/rustc_hir_typeck/src/cast.rs +the untracked option future_incompat_test compiler/rustc_hir_typeck/src/fn_ctxt/_impl.rs +the untracked option future_incompat_test compiler/rustc_hir_typeck/src/method/probe.rs +the untracked option future_incompat_test compiler/rustc_lint/src/async_fn_in_trait.rs +the untracked option future_incompat_test compiler/rustc_lint/src/builtin.rs +the untracked option future_incompat_test compiler/rustc_lint/src/early.rs +the untracked option future_incompat_test compiler/rustc_lint/src/implicit_provenance_casts.rs +the untracked option future_incompat_test compiler/rustc_lint/src/impl_trait_overcaptures.rs +the untracked option future_incompat_test compiler/rustc_lint/src/internal.rs +the untracked option future_incompat_test compiler/rustc_lint/src/let_underscore.rs +the untracked option future_incompat_test compiler/rustc_lint/src/lifetime_syntax.rs +the untracked option future_incompat_test compiler/rustc_lint/src/macro_expr_fragment_specifier_2024_migration.rs +the untracked option future_incompat_test compiler/rustc_lint/src/multiple_supertrait_upcastable.rs +the untracked option future_incompat_test compiler/rustc_lint/src/nonstandard_style.rs +the untracked option future_incompat_test compiler/rustc_lint/src/redundant_semicolon.rs +the untracked option future_incompat_test compiler/rustc_lint/src/types/literal.rs +the untracked option future_incompat_test compiler/rustc_lint/src/types.rs +the untracked option future_incompat_test compiler/rustc_lint/src/unit_bindings.rs +the untracked option future_incompat_test compiler/rustc_lint/src/unqualified_local_imports.rs +the untracked option future_incompat_test compiler/rustc_lint/src/unused/must_use.rs +the untracked option future_incompat_test compiler/rustc_middle/src/middle/stability.rs +the untracked option future_incompat_test compiler/rustc_mir_build/src/thir/pattern/check_match.rs +the untracked option future_incompat_test compiler/rustc_mir_build/src/thir/pattern/migration.rs +the untracked option future_incompat_test compiler/rustc_mir_transform/src/known_panics_lint.rs +the untracked option future_incompat_test compiler/rustc_monomorphize/src/mono_checks/move_check.rs +the untracked option future_incompat_test compiler/rustc_passes/src/dead.rs +the untracked option future_incompat_test compiler/rustc_pattern_analysis/src/rustc.rs +the untracked option future_incompat_test compiler/rustc_privacy/src/lib.rs +the untracked option identify_regions compiler/rustc_middle/src/ty/print/pretty.rs +the untracked option incremental compiler/rustc_metadata/src/rmeta/encoder.rs +the untracked option incremental compiler/rustc_middle/src/mono.rs +the untracked option incremental compiler/rustc_middle/src/ty/mod.rs +the untracked option incremental compiler/rustc_mir_transform/src/cross_crate_inline.rs +the untracked option incremental compiler/rustc_mir_transform/src/inline.rs +the untracked option incremental compiler/rustc_session/src/session.rs +the untracked option incremental_verify_ich compiler/rustc_query_impl/src/incremental.rs +the untracked option jobs compiler/rustc_query_impl/src/execution.rs +the untracked option nll_facts_dir compiler/rustc_borrowck/src/nll.rs +the untracked option no_leak_check compiler/rustc_infer/src/infer/relate/higher_ranked.rs +the untracked option query_dep_graph compiler/rustc_query_impl/src/execution.rs +the untracked option query_dep_graph compiler/rustc_session/src/session.rs +the untracked option span_free_formats compiler/rustc_middle/src/ty/print/pretty.rs +the untracked option trim_diagnostic_paths compiler/rustc_middle/src/ty/print/pretty.rs +the untracked option ui_testing compiler/rustc_session/src/session.rs +the untracked option unpretty compiler/rustc_session/src/session.rs +the untracked option unstable_options compiler/rustc_session/src/session.rs +the untracked option write_long_types_to_disk compiler/rustc_middle/src/ty/error.rs +the untracked option write_long_types_to_disk compiler/rustc_trait_selection/src/error_reporting/infer/mod.rs +printing mode `with_no_trimmed_paths` - +# Known anywhere: options whose verdict in docs/untracked-reads.md does not depend on where they are read. +# Wording or marking of a replayed warning (minor): future_incompat_test, trim_diagnostic_paths, +# ui_testing, identify_regions, check_cfg_all_expected, write_long_types_to_disk, unstable_options. +the untracked option future_incompat_test * +the untracked option trim_diagnostic_paths * +the untracked option ui_testing * +the untracked option identify_regions * +the untracked option check_cfg_all_expected * +the untracked option write_long_types_to_disk * +the untracked option unstable_options * +# Benign: decide whether to check, record or partition, not what to compute. +the untracked option jobs * +the untracked option incremental * +the untracked option incremental_verify_ich * +the untracked option query_dep_graph * +the untracked option unpretty * +the untracked option cli_forced_codegen_units * +the untracked option cli_forced_local_thinlto_off * From 3441c48b47d617fe1c59255ecd63cce0f4ffd113 Mon Sep 17 00:00:00 2001 From: Zack Maril Date: Sat, 10 Oct 2026 13:16:35 +0000 Subject: [PATCH 2/2] ui-incr: -Cincremental before a test's flags, session suffixes of object names removed, reuse-check re-emissions told from duplicates; known reuse reports; finding 56 (-Zunleash: an incremental rebuild drops the warning and the feature-gate error); fourth batch in checks.md Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01QXiEXbESemwqMLYKaWLDbT --- crates/mirth-lab/src/artifacts.rs | 6 +++ crates/mirth-lab/src/rustc.rs | 9 +++- crates/mirth-lab/src/tools/ui_incr.rs | 9 ++-- docs/checks.md | 53 +++++++++++++++++++++ docs/hunt.md | 1 + docs/hunt/tests/unleash-gate-incremental.rs | 6 +++ docs/hunt/unleash-incremental.md | 50 +++++++++++++++++++ docs/shadow-mode.md | 5 ++ docs/untracked-reads.md | 4 ++ rustc/reuse-known.txt | 2 + 10 files changed, 141 insertions(+), 4 deletions(-) create mode 100644 docs/hunt/tests/unleash-gate-incremental.rs create mode 100644 docs/hunt/unleash-incremental.md diff --git a/crates/mirth-lab/src/artifacts.rs b/crates/mirth-lab/src/artifacts.rs index 431b0a0..aaf109e 100644 --- a/crates/mirth-lab/src/artifacts.rs +++ b/crates/mirth-lab/src/artifacts.rs @@ -11,6 +11,12 @@ use sha2::{Digest, Sha256}; /// `.<7 chars>.rcgu.o`: the per-session suffix of codegen-unit objects. static SESSION: LazyLock = 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 { + SESSION.replace_all(data, &b"$1"[..]).into_owned() +} + pub fn sha256(data: &[u8]) -> String { let mut h = Sha256::new(); h.update(data); diff --git a/crates/mirth-lab/src/rustc.rs b/crates/mirth-lab/src/rustc.rs index 3c8cfcf..44682e5 100644 --- a/crates/mirth-lab/src/rustc.rs +++ b/crates/mirth-lab/src/rustc.rs @@ -130,6 +130,9 @@ pub struct Compile<'a> { pub name_output: bool, /// More environment for rustc (the patched compiler's checks). pub env: Vec<(String, String)>, + /// `-Cincremental=`, passed before the test's flags (which may end with an option that + /// takes a value, such as `--cap-lints`). + pub incremental: Option, } impl<'a> Compile<'a> { @@ -147,6 +150,7 @@ impl<'a> Compile<'a> { bootstrap: true, name_output: true, env: Vec::new(), + incremental: None, } } @@ -158,7 +162,7 @@ impl<'a> Compile<'a> { /// 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.extra.push(format!("-Cincremental={}", incr.display())); + self.incremental = Some(incr.to_path_buf()); for (k, v) in crate::compiler_checks::env(all) { self = self.env(k, v); } @@ -208,6 +212,9 @@ 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) diff --git a/crates/mirth-lab/src/tools/ui_incr.rs b/crates/mirth-lab/src/tools/ui_incr.rs index 62f703b..a20c7c7 100644 --- a/crates/mirth-lab/src/tools/ui_incr.rs +++ b/crates/mirth-lab/src/tools/ui_incr.rs @@ -12,8 +12,9 @@ //! Each rebuild is compared with a clean build of the same source in a fresh incremental //! directory, at the same paths: the diagnostics (as a multiset; a different order is a note), //! the output bytes (metadata for check tests, the program otherwise), and for run tests the -//! program's output when the bytes differ. A byte difference between two clean builds makes the -//! test nondeterministic, and it is skipped. Stale reuse and new untracked reads the compiler +//! program's output when the bytes differ, with the per-session suffixes of object names removed +//! (`.<7 chars>.rcgu.o`). A byte difference between two clean builds makes the test +//! nondeterministic, and it is skipped. Stale reuse and new untracked reads the compiler //! reports in any session are findings too. //! //! The reuse check recomputes green values with their providers, and a provider that emits a @@ -26,6 +27,7 @@ use std::path::{Path, PathBuf}; use std::process::ExitCode; use std::sync::LazyLock; +use mirth_lab::artifacts; use mirth_lab::compiler_checks::{self, Known}; use mirth_lab::driver::{self, Record, Sweep}; use mirth_lab::normalize; @@ -141,7 +143,8 @@ fn build(args: &Args, test: &Test, src: &Path, out: &Path, incr: &Path, all: boo let stderr = compiler_checks::strip(&c.stderr); let mut diags: Vec = stderr.lines().filter(|l| !l.trim().is_empty() && !SUMMARY.is_match(l)).map(str::to_owned).collect(); diags.sort(); - Build { status: c.status, diags, bytes: std::fs::read(out.join("prog")).ok(), stderr: c.stderr } + let bytes = std::fs::read(out.join("prog")).ok().map(|b| artifacts::without_session_suffixes(&b)); + Build { status: c.status, diags, bytes, stderr: c.stderr } } fn check(args: &Args, test: &Test) -> Rec { diff --git a/docs/checks.md b/docs/checks.md index e324728..df8abb0 100644 --- a/docs/checks.md +++ b/docs/checks.md @@ -545,6 +545,57 @@ when the source test uses the harness) and searches rust-lang/rust's issues for panic's location and the first query on the stack, or a delayed bug's message, so one bug can show as several signatures (finding 50 as six). +### Fourth batch (2026-10-10): mirth's own checks over the corpus + +The patched compiler (`rustc-verify12`: [`hunt/verify-reuse.patch`](hunt/verify-reuse.patch) and +[`hunt/report-untracked.patch`](hunt/report-untracked.patch)) had only run its own checks in the +fuzzer, the replays and the flag walks. Both hook the dependency graph, so they act only in an +incremental session. + +- `--compiler-checks` (diag-check, gate-mutate): each compile is an incremental session with + `RUSTC_VERIFY_REUSE=all` (every cached query value recomputed at the end of the session and + compared) and `RUSTC_REPORT_UNTRACKED`. An untracked read is new unless its (what, file) pair + or its option is in `rustc/untracked-known.tsv` (every pair the fuzzer, replays and flag walks + reported, and the options whose verdict in [`untracked-reads.md`](untracked-reads.md) holds + anywhere); a new site of "source text" is a note. Triaged reuse reports are in + `rustc/reuse-known.txt`. +- `mirth-lab ui-incr`: P6 over the UI corpus. Per test, a clean incremental session, then three + rebuilds in it (unchanged; a blank line first, which moves every span; an unused fn appended), + each compared with a clean build of the same source: diagnostics, output bytes (metadata for + check tests, the program otherwise, with the per-session suffix of object names removed), and + the program's output when the bytes differ; with the reuse check and the untracked-read report + on in every session. + +| sweep | swept | result | +|---|---|---| +| `ui-incr` | 18,502 tests (7,249 compile, 11,236 fail as expected; 17 ICE or time out in the clean session) × 3 rebuilds, each against a clean build | finding 56; otherwise no incremental session differs from a clean one: no output byte, program output or diagnostic difference | +| `diag-check --compiler-checks` | 18,374 tests, each a clean incremental session recomputing every cached value | no recomputed value differs; no new untracked read | +| `gate-mutate --compiler-checks` | 3,000 mutants | nothing reported by the checks | + +What the checks report, all triaged: + +- **Allocation identity in const-eval results** (`rustc/reuse-known.txt`, 222 tests): the + `PostAnalysis` and `Codegen` evaluations of one constant or promoted share an allocation in a + clean session and get two after the round trip through the cache. Finding 8's mechanism + (decoding reserves a fresh `AllocId`; `alloc-dedup-on-decode.patch` covers only allocations + deduplicated when created). No output byte or program output differed in any of these tests; + not recorded as a finding. +- **The reuse check's own re-emissions** (4 tests): recomputing a green value runs its provider, + and a provider that emits a lint emits it again, printed without trimmed paths (the check runs + under `with_no_trimmed_paths`). `ui-incr` counts an extra diagnostic at a clean one's location + but worded differently as such a re-emission (a note) and an identical extra one as a + duplicate (a finding). The patch could silence diagnostics while it recomputes. +- **New sites of known untracked reads**: 46 places read source text (75 site and query pairs, + in 15 queries: `typeck_root`, `dyn_compatibility_violations`, lint passes, `mir_borrowck`, + `check_match`, `fn_sig`, `type_of`, `impl_trait_header` and others), each a position or a + wording computed from raw text, the class [`untracked-reads.md`](untracked-reads.md) describes; + and the options `future_incompat_test` and `ui_testing` at new lint sites. +- **Harness artifacts fixed on the way**: `-Cincremental` passed after a test's flags became the + value of a trailing `--cap-lints`; two clean incremental builds differ in the per-session + suffix of object names (78 tests skipped as nondeterministic until it was removed). + +release-diff is not included: it builds with official toolchains, which do not have the patches. + ## Running the checks The checks are subcommands of `mirth-lab` (`crates/mirth-lab`; `mirth-lab --help` lists them): @@ -557,6 +608,8 @@ target/release/mirth-lab solver-diff --rustc $R --tests $T --work target/release/mirth-lab rustdoc-diff --toolchain nightly-2026-10-06 --tests $T --work target/release/mirth-lab abi-diff --rustc $R --rust ~/mirth-work/rust --work --seed 3 target/release/mirth-lab lint-check --rustc $R --tests $T --work +target/release/mirth-lab ui-incr --rustc $R --tests $T --work # P6 over the UI corpus +target/release/mirth-lab diag-check --rustc $R --tests $T --compiler-checks --work target/release/mirth-lab gate-mutate --rustc $R --rust ~/mirth-work/rust --work --count 20000 --jobs 4 target/release/mirth-lab gate-mutate --rustc $R --rust ~/mirth-work/rust --work --triage target/release/mirth-lab release-diff --corpus ~/proofhouse-repos/rust --old nightly-2026-07-18 --new nightly-2026-10-06 --work diff --git a/docs/hunt.md b/docs/hunt.md index 6599e27..990bf54 100644 --- a/docs/hunt.md +++ b/docs/hunt.md @@ -79,6 +79,7 @@ with `-Zthreads=8`. `rustc/check.sh wide` runs the ordinary checks. | 53 | "AliasConst::type_of got InherentSelf - args should always be InherentImpl at this point" (`const_kind.rs:85`, `check_well_formed`) for `fn to_bytes() -> [u8; gca!(Self::SIZE)]` in an inherent impl, when `generic_const_exprs` is also enabled; without it the program is accepted as in #162147's regression test (`gca/wf-inherentimpl.rs`) | **looks new** (a route around closed #162147, fixed 2026-09-03), low; nightly-2026-10-06; found by `gate-mutate` (a splice whose `--cfg full` turns on `generic_const_exprs`); [repro](hunt/tests/gate-mutate/gce-inherent-self.rs) | | 54 | `rustc --test` panics "expected statement" (`rustc_expand/src/base.rs:172`) after E0736 for a `#[test] #[unsafe(naked)] extern "C" fn` nested inside another function's body; a `#[test]` inner fn without `naked` only warns "cannot test inner items" | **looks new** (closed issues with the message: #112360, #109816 (both `--test`), #83469, #149980; none open, none with `naked`), low (error recovery), **stable**: 1.82.0 through 1.88.0, 1.90.0, 1.98.0 and nightly-2026-10-06 (each tested) (1.81.0 rejects with E0658/E0787; before 1.88 the panic comes before the gate error); found by `gate-mutate` (an item moved into a generic fn); [repro](hunt/tests/gate-mutate/naked-test-inner-fn.rs) | | 55 | a hang under the new trait solver: a closure with a `for<'a, 'b>` binder returning a TAIT with two lifetimes, passed where a `for<'a> AsyncFn<&'a mut C, …>` bound (a trait with an `FnMut` supertrait and an associated future) is required, does not finish compiling (still running after 200 s); with `-Znext-solver=coherence` (the old solver) it reports E0046/E0308/E0277 in 0.05 s | **looks new** (no issue found), medium: the new solver is nightly's default, so the plain `rustc` hangs on nightly-2026-10-06; also hangs on nightly-2026-07-18 with `-Znext-solver=globally` (not a recent regression); found by `gate-mutate` (a module splice of two tests); [repro](hunt/tests/gate-mutate/next-solver-hang.rs) | +| 56 | `-Zunleash-the-miri-inside-of-you`: an incremental rebuild drops the "skipping const checks" warning and the error that the flag may not circumvent feature gates (session state filled as a side effect of const checking), so a rebuild accepts a crate a clean build rejects | **looks new**, low (a flag for testing the const evaluator); the warning since at least 1.80.0, the error on nightly-2026-10-06; found by `ui-incr`; [facts](hunt/unleash-incremental.md), [repro](hunt/tests/unleash-gate-incremental.rs) | Findings 1 and 2 are single-threaded: an ordinary `cargo build`, an edit, another `cargo build`, and the metadata differs from a clean build of the edited source. Both come diff --git a/docs/hunt/tests/unleash-gate-incremental.rs b/docs/hunt/tests/unleash-gate-incremental.rs new file mode 100644 index 0000000..76f1faa --- /dev/null +++ b/docs/hunt/tests/unleash-gate-incremental.rs @@ -0,0 +1,6 @@ +// Finding 56: a clean build rejects this crate with -Zunleash-the-miri-inside-of-you (the flag +// "may not be used to circumvent feature gates"); an incremental rebuild of the same source +// accepts it. RUSTC_BOOTSTRAP=1 rustc --crate-type lib -Zunleash-the-miri-inside-of-you +// -Cincremental=inc --emit=metadata unleash-gate-incremental.rs (twice; nightly-2026-10-06) +use std::sync::atomic::{AtomicUsize, Ordering}; +pub const fn f(a: &AtomicUsize) -> usize { a.fetch_add(1, Ordering::Relaxed) } diff --git a/docs/hunt/unleash-incremental.md b/docs/hunt/unleash-incremental.md new file mode 100644 index 0000000..c909a20 --- /dev/null +++ b/docs/hunt/unleash-incremental.md @@ -0,0 +1,50 @@ +# `-Zunleash-the-miri-inside-of-you`: an incremental rebuild drops its warning and its feature-gate error + +Facts for finding 56. Found by `mirth-lab ui-incr` (P6 over the UI corpus, +[`checks.md`](../checks.md), fourth batch) on `consts/const-eval/const_fn_ptr.rs` and +`const_fn_ptr_fail.rs`: after an edit, the incremental rebuild lacks the "skipping const checks" +warning a clean build of the same source prints. + +## What happens + +With `-Zunleash-the-miri-inside-of-you`, const checking does not reject operations it would +otherwise reject; it records each one in `Session::miri_unleashed_features` +(`rustc_session/src/session.rs`, from `rustc_const_eval/src/check_consts/check.rs:271`). At the +end of the session, `check_miri_unleashed_features` prints the warning "skipping const checks", +with one note per recorded operation, and when one of them skipped a feature gate and nothing +else failed, the error "`-Zunleash-the-miri-inside-of-you` may not be used to circumvent feature +gates, except when testing error paths in the CTFE engine". + +The list is session state filled as a side effect of const checking, not part of a query result +or a recorded side effect. When an incremental rebuild reuses const checking from the cache, it +does not run, nothing is recorded, and neither the warning nor the error is emitted. + +| `const_fn_ptr.rs` (run-pass, `-Zunleash-the-miri-inside-of-you`) | clean | rebuild, unchanged | rebuild, a blank line added | clean of the edited file | +|---|---|---|---|---| +| nightly-2026-10-06 | warning | none | none | warning | +| 1.98.0 | warning | none | none | warning | +| 1.80.0 | warning | none | none | warning | + +[`tests/unleash-gate-incremental.rs`](tests/unleash-gate-incremental.rs) (a `const fn` calling +`AtomicUsize::fetch_add`, a `const_atomic`-gated const fn), `--crate-type lib +-Zunleash-the-miri-inside-of-you -Cincremental=inc --emit=metadata`, `RUSTC_BOOTSTRAP=1`: + +| nightly-2026-10-06 | exit | +|---|---| +| clean build | 1 (the feature-gate error) | +| rebuild, unchanged | 0 | +| rebuild, a blank line added | 0 | +| clean build of the edited file | 1 | + +So the rebuild accepts a crate the clean build rejects. On 1.98.0 and 1.80.0 `fetch_add` is not a +gated const fn, so this file shows only the lost warning there. + +## Scope + +Only builds with `-Zunleash-the-miri-inside-of-you`, a flag for testing the const evaluator +(tests/ui/consts/miri_unleashed). Low severity; the same shape as the untracked options of +[`untracked-reads.md`](../untracked-reads.md): state that decides a diagnostic, outside the +dependency graph. + +Searches of rust-lang/rust issues for the warning and the flag with "incremental" found nothing +(2026-10-10). diff --git a/docs/shadow-mode.md b/docs/shadow-mode.md index 7fb93f4..fdd6b91 100644 --- a/docs/shadow-mode.md +++ b/docs/shadow-mode.md @@ -108,6 +108,11 @@ finding 6's checksum left out of incremental sessions by [`hunt/debuginfo-checksum-stopgap.patch`](hunt/debuginfo-checksum-stopgap.patch), a testing aid, not a fix). It costs more than the rest: about 40% on that rebuild. +Recomputing a green value runs its provider, and a provider that emits a lint emits it again, +after the session's own diagnostics and printed without trimmed paths (the check runs under +`with_no_trimmed_paths`); `ui-incr` tells these re-emissions from real duplicates by their +wording ([`checks.md`](checks.md), fourth batch). + Replayed diagnostics are not checked yet, and diagnostics LLVM emits while compiling a unit are not replayed at all ([finding 7](hunt.md)). diff --git a/docs/untracked-reads.md b/docs/untracked-reads.md index 7957c19..104422a 100644 --- a/docs/untracked-reads.md +++ b/docs/untracked-reads.md @@ -131,6 +131,10 @@ tracked changing only when the change keeps every token and position, such as a rewritten to the same length inside the span, so these can go stale but hardly will. Not reported upstream. +Over the UI suite (`diag-check --compiler-checks` and `ui-incr`, [`checks.md`](checks.md), fourth +batch), 46 more places read source text, in 15 queries, nearly all to word or place an error or +a lint; none is a new kind of read. + ## Printing modes inherited from the caller Printing has thread-local modes (`with_no_trimmed_paths!`, `with_reduced_queries!` and diff --git a/rustc/reuse-known.txt b/rustc/reuse-known.txt index d23ce48..7f208f3 100644 --- a/rustc/reuse-known.txt +++ b/rustc/reuse-known.txt @@ -5,3 +5,5 @@ allocation sharing eval_to_const_value_raw / eval_to_const_value_raw / Codegen / PostAnalysis allocation sharing eval_to_const_value_raw / eval_to_allocation_raw / Codegen / PostAnalysis allocation sharing eval_to_const_value_raw / eval_to_allocation_raw / Codegen +allocation sharing eval_to_allocation_raw / eval_to_allocation_raw / Codegen / PostAnalysis +allocation sharing eval_to_const_value_raw / eval_to_allocation_raw / PostAnalysis