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
4 changes: 4 additions & 0 deletions crates/mirth-lab/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ mod tools {
pub mod gate_mutate;
pub mod grammar_coverage;
pub mod instr_check;
pub mod invariant_sweep;
pub mod lint_check;
pub mod miri_diff;
pub mod motivating;
Expand Down Expand Up @@ -75,6 +76,8 @@ enum Check {
ReproDiff(tools::repro_diff::Args),
/// Nothing unstable is usable from stable code (attributes, library items).
GateCheck(tools::gate_check::Args),
/// The compiler's own invariants (docs/hunt/check-invariants.patch) on every UI test.
InvariantSweep(tools::invariant_sweep::Args),
/// PGO and coverage instrumentation round trips.
InstrCheck(tools::instr_check::Args),
/// Real crates accepted by one toolchain are accepted by the next, in comparable time.
Expand Down Expand Up @@ -156,6 +159,7 @@ fn main() -> ExitCode {
Check::SuggestDiff(a) => tools::suggest_diff::run(a),
Check::ReproDiff(a) => tools::repro_diff::run(a),
Check::GateCheck(a) => tools::gate_check::run(a),
Check::InvariantSweep(a) => tools::invariant_sweep::run(a),
Check::GateMutate(a) => tools::gate_mutate::run(a),
Check::InstrCheck(a) => tools::instr_check::run(a),
Check::LintCheck(a) => tools::lint_check::run(a),
Expand Down
11 changes: 10 additions & 1 deletion crates/mirth-lab/src/rustc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,8 @@ 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,
/// Extra environment variables for rustc.
pub env: Vec<(String, String)>,
}

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

pub fn env(mut self, key: &str, value: &str) -> Self {
self.env.push((key.to_owned(), value.to_owned()));
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 @@ -195,7 +203,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, v)));
if self.bootstrap {
cmd.env("RUSTC_BOOTSTRAP", "1");
} else {
Expand Down
95 changes: 95 additions & 0 deletions crates/mirth-lab/src/tools/invariant_sweep.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
//! In-compiler invariants over the UI tests: each standalone test compiled by a compiler with
//! docs/hunt/check-invariants.patch, with `RUSTC_CHECK_INVARIANTS` set.
//!
//! The patch prints `rustc-invariant: <property>: <details>` for each violation and carries
//! on. Some checks it turns on are rustc's own debug-build assertions (layout sanity, argument
//! lists against generics in debug builds), which panic instead: an ICE that happens only with
//! the variable set is a finding too (`env-only-ice`). Tests that build (build-pass, run-pass,
//! *-fail past type checking) are compiled to a binary, so codegen's checks run; the rest to
//! metadata.

use std::collections::BTreeSet;
use std::path::PathBuf;
use std::process::ExitCode;

use mirth_lab::driver::{self, Record, Sweep};
use mirth_lab::rustc::{Compile, Status};
use mirth_lab::uitest::{self, Kind, Test};
use serde::Serialize;

#[derive(clap::Args, Debug)]
pub struct Args {
/// A rustc built with docs/hunt/check-invariants.patch.
#[arg(long)]
rustc: PathBuf,
#[command(flatten)]
sweep: Sweep,
}

const PREFIX: &str = "rustc-invariant: ";

#[derive(Serialize)]
struct Rec {
test: String,
status: String,
#[serde(skip_serializing_if = "Option::is_none")]
skip: Option<String>,
found: Vec<String>,
}

impl Record for Rec {
fn findings(&self) -> Vec<String> {
self.found.clone()
}
fn test(&self) -> &str {
&self.test
}
}

/// The first line of a panic message in rustc's stderr.
fn panic_line(stderr: &str) -> String {
let mut lines = stderr.lines();
while let Some(l) = lines.next() {
if l.contains("panicked at") {
let msg = lines.next().unwrap_or("");
return format!("{} {}", l.trim(), msg.trim()).chars().take(300).collect();
}
}
stderr.lines().find(|l| l.contains("internal compiler error")).unwrap_or("").chars().take(300).collect()
}

fn check(args: &Args, test: &Test) -> Rec {
let dir = driver::scratch_dir(&args.sweep);
let emit = if Kind::is_check(test.kind) { "metadata" } else { "link" };
let compile = |env: bool| {
let c = Compile::new(&args.rustc, &test.path, dir.path(), &test.flags, test.edition()).emit(emit).timeout(180);
if env { c.env("RUSTC_CHECK_INVARIANTS", "1") } else { c }.run()
};
let c = compile(true);
let mut rec = Rec { test: test.rel.clone(), status: format!("{:?}", c.status).to_lowercase(), skip: None, found: Vec::new() };
let mut found: BTreeSet<String> = c.stderr.lines().filter_map(|l| l.strip_prefix(PREFIX)).map(str::to_owned).collect();
match c.status {
Status::Timeout => rec.skip = Some("timeout".into()),
Status::Ice => {
let without = compile(false);
if without.status == Status::Ice {
rec.skip = Some("ice without the checks too".into());
} else {
found.insert(format!("env-only-ice: {}", panic_line(&c.stderr)));
}
}
_ => {}
}
rec.found = found.into_iter().collect();
if !rec.found.is_empty() {
driver::write_finding(&args.sweep.work, test, &[], &serde_json::json!({ "found": rec.found, "emit": emit }));
}
rec
}

pub fn run(args: Args) -> anyhow::Result<ExitCode> {
let tests = uitest::tests(&args.sweep.tests, uitest::ALL, |_| false);
let tests = args.sweep.select(tests);
println!("{} tests", tests.len());
Ok(driver::drive(&tests, &args.sweep, |t| check(&args, t)))
}
30 changes: 30 additions & 0 deletions docs/checks.md
Original file line number Diff line number Diff line change
Expand Up @@ -545,6 +545,35 @@ 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).

## In-compiler invariants (2026-10-10)

[`hunt/check-invariants.patch`](hunt/check-invariants.patch), applied last on the
verify-reuse stack (`rustc/regen-patches.sh` regenerates it), checks invariants from
[`properties.md`](properties.md) inside rustc on every compilation when `RUSTC_CHECK_INVARIANTS`
is set: a violation prints `rustc-invariant: <property>: <details>` and compilation goes on.
`RUSTC_CHECK_INVARIANTS=selftest` also prints which checks ran (and, for #18, which queries'
results are checked and which are not). Built into `~/mirth-work/rustc-verify13`.

| property | where | what upstream had |
|---|---|---|
| 18: query results contain no inference variables | `rustc_query_impl`: each provider's typed result, before it is erased | nothing. The probe dispatches on the value's type (autoref specialization): `TypeVisitable` values, `EarlyBinder`/`&`/`Option`/`Result` around one, canonical query responses, typeck results' node types, borrowck's hidden types, clauses, impl headers, layouts. 99 of the ~200 query kinds a small program runs are checked; the rest return types without types in them, or `Steal`ed bodies |
| 14: a compile that emitted no error has no error types | end of `analysis`, when no error or delayed bug was emitted: typeck results (tainted, node types), `type_of` and `fn_sig` of every local item | nothing |
| 24: symbol names are injective | `assert_symbols_are_distinct`: local mono items against upstream crates' exported symbols | within a session, in every build (fatal `SymbolAlreadyDefined`); across crates, nothing |
| 15: each metadata record is written once | `TableBuilder::set`: an entry set a second time | nothing |
| 9: layout views agree | type lowering: LLVM's ABI size of the lowered type against the layout's size; and rustc's own expensive layout sanity checks, on in release | the sanity checks in debug builds only |
| 1: interned values are well-formed | `debug_assert_args_compatible`, `debug_assert_alias_term_args_compatible`: argument lists against generics, reported instead of a bug | debug builds only |
| 7: spans are valid | metadata span encoding: `lo <= hi`, `lo` inside its file | `debug_assert!` only |

`mirth-lab invariant-sweep` compiles every standalone UI test with the variable set (to a
binary when the test builds, so codegen's checks run; to metadata otherwise), collects the
lines, and counts an ICE that happens only with the variable set as `env-only-ice` (the
enabled debug assertions panic instead of reporting).

| swept | result |
|---|---|
| 18,624 UI tests: 7,408 compile, 11,198 fail as expected, 17 ICE without the checks too, 1 timeout | 315 tests with findings, all property 15: findings 56 (the crate root's module children, encoded twice in every library with a public item) and 57 (coroutine layouts, encoded twice); proc-macro `def_keys` written twice on purpose (6 tests). Nothing for 18, 14, 24, 9, 1, 7 |
| 12,000 `gate-mutate` mutants with the variable set | no ICE signature the earlier run had not seen (the invariant lines themselves are not collected by gate-mutate) |

## Running the checks

The checks are subcommands of `mirth-lab` (`crates/mirth-lab`; `mirth-lab --help` lists them):
Expand All @@ -561,6 +590,7 @@ target/release/mirth-lab gate-mutate --rustc $R --rust ~/mirth-work/rust --work
target/release/mirth-lab gate-mutate --rustc $R --rust ~/mirth-work/rust --work <dir> --triage
target/release/mirth-lab release-diff --corpus ~/proofhouse-repos/rust --old nightly-2026-07-18 --new nightly-2026-10-06 --work <dir>
target/release/mirth-lab debug-check --toolchain nightly-2026-10-06 --work <dir> --seeds 0..4000 --jobs 4
target/release/mirth-lab invariant-sweep --rustc ~/mirth-work/rustc-verify13/bin/rustc --tests $T --work <dir>
```

Sweeps over UI tests share `--tests`, `--work`, `--only`, `--known`, `--jobs`, `--recheck` and
Expand Down
2 changes: 2 additions & 0 deletions docs/hunt.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,8 @@ 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 | the crate root's module children are encoded into metadata twice: `encode_def_ids` calls `encode_info_for_mod(CRATE_DEF_ID)` and then reaches the root again in its loop over all local `DefId`s (`DefKind::Mod`); in every library with a public item, `module_children_non_reexports`/`module_children_reexports`/`ambig_module_children` entry 0 point at a second copy and the first stays in the file unreferenced | low (bytes: 2,164 of `std`'s 8.0 MB `.rmeta`, 1,384 of `core`'s); since at least 1.80.0; found by the in-compiler invariant "each metadata record is written once" (`check-invariants.patch`, 310 UI tests); [facts](hunt/metadata-written-twice.md) |
| 57 | coroutine layouts are encoded into metadata twice: `encode_mir` records `mir_coroutine_witnesses` inside `if encode_opt` and again unconditionally at the end of the loop, so every coroutine with encoded optimized MIR has its `CoroutineLayout` written twice | low (bytes: 226 of a 16.5 KB async library's `.rmeta`); since at least 1.80.0; found by the same invariant (71 UI tests); [facts](hunt/metadata-written-twice.md) |

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
Expand Down
Loading
Loading