From 48284f733df7040ab93a02e337af36045976045f Mon Sep 17 00:00:00 2001 From: Zack Maril Date: Sat, 10 Oct 2026 12:57:33 +0000 Subject: [PATCH] In-compiler invariants: check-invariants.patch (RUSTC_CHECK_INVARIANTS: query results without inference variables, no error types in an error-free compile, symbols distinct from upstream exports, metadata records written once, LLVM vs layout sizes and rustc's layout sanity checks in release, argument lists against generics, valid spans), built as rustc-verify13; mirth-lab invariant-sweep; findings 56-57 (metadata written twice); regen-patches.sh regenerates the new patch, without Python Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01QXiEXbESemwqMLYKaWLDbT --- crates/mirth-lab/src/main.rs | 4 + crates/mirth-lab/src/rustc.rs | 11 +- crates/mirth-lab/src/tools/invariant_sweep.rs | 95 +++ docs/checks.md | 30 + docs/hunt.md | 2 + docs/hunt/check-invariants.patch | 641 ++++++++++++++++++ docs/hunt/metadata-written-twice.md | 88 +++ rustc/big-run.sh | 1 + rustc/regen-patches.sh | 57 +- 9 files changed, 904 insertions(+), 25 deletions(-) create mode 100644 crates/mirth-lab/src/tools/invariant_sweep.rs create mode 100644 docs/hunt/check-invariants.patch create mode 100644 docs/hunt/metadata-written-twice.md diff --git a/crates/mirth-lab/src/main.rs b/crates/mirth-lab/src/main.rs index d15eceb..7733526 100644 --- a/crates/mirth-lab/src/main.rs +++ b/crates/mirth-lab/src/main.rs @@ -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; @@ -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. @@ -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), diff --git a/crates/mirth-lab/src/rustc.rs b/crates/mirth-lab/src/rustc.rs index 8952f3c..e9d0536 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, + /// Extra environment variables for rustc. + pub env: Vec<(String, String)>, } impl<'a> Compile<'a> { @@ -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, S: Into>(mut self, extra: I) -> Self { self.extra.extend(extra.into_iter().map(Into::into)); self @@ -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 { diff --git a/crates/mirth-lab/src/tools/invariant_sweep.rs b/crates/mirth-lab/src/tools/invariant_sweep.rs new file mode 100644 index 0000000..f576198 --- /dev/null +++ b/crates/mirth-lab/src/tools/invariant_sweep.rs @@ -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: :
` 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, + found: Vec, +} + +impl Record for Rec { + fn findings(&self) -> Vec { + 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 = 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 { + 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))) +} diff --git a/docs/checks.md b/docs/checks.md index e324728..893b7db 100644 --- a/docs/checks.md +++ b/docs/checks.md @@ -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: :
` 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): @@ -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 --triage target/release/mirth-lab release-diff --corpus ~/proofhouse-repos/rust --old nightly-2026-07-18 --new nightly-2026-10-06 --work target/release/mirth-lab debug-check --toolchain nightly-2026-10-06 --work --seeds 0..4000 --jobs 4 +target/release/mirth-lab invariant-sweep --rustc ~/mirth-work/rustc-verify13/bin/rustc --tests $T --work ``` Sweeps over UI tests share `--tests`, `--work`, `--only`, `--known`, `--jobs`, `--recheck` and diff --git a/docs/hunt.md b/docs/hunt.md index 6599e27..7336db9 100644 --- a/docs/hunt.md +++ b/docs/hunt.md @@ -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 diff --git a/docs/hunt/check-invariants.patch b/docs/hunt/check-invariants.patch new file mode 100644 index 0000000..40d07e1 --- /dev/null +++ b/docs/hunt/check-invariants.patch @@ -0,0 +1,641 @@ +diff --git a/compiler/rustc_codegen_llvm/src/llvm/ffi.rs b/compiler/rustc_codegen_llvm/src/llvm/ffi.rs +index bc9ce9580a9a..570f836f7376 100644 +--- a/compiler/rustc_codegen_llvm/src/llvm/ffi.rs ++++ b/compiler/rustc_codegen_llvm/src/llvm/ffi.rs +@@ -713,6 +713,12 @@ struct InvariantOpaque<'a> { + /// `&'ll DbgRecord` represents `LLVMDbgRecordRef`. + pub(crate) type DbgRecord; + } ++// `RUSTC_CHECK_INVARIANTS`: LLVM's view of a type's size, compared with the layout's. ++unsafe extern "C" { ++ pub(crate) type TargetData; ++ pub(crate) fn LLVMGetModuleDataLayout(M: &Module) -> &TargetData; ++ pub(crate) fn LLVMABISizeOfType(TD: &TargetData, Ty: &Type) -> c_ulonglong; ++} + #[repr(C)] + pub(crate) struct Builder<'a>(InvariantOpaque<'a>); + unsafe extern "C" { +diff --git a/compiler/rustc_codegen_llvm/src/type_of.rs b/compiler/rustc_codegen_llvm/src/type_of.rs +index 0624d8db460c..b6feebaa88e8 100644 +--- a/compiler/rustc_codegen_llvm/src/type_of.rs ++++ b/compiler/rustc_codegen_llvm/src/type_of.rs +@@ -281,6 +281,27 @@ fn llvm_type<'a>(&self, cx: &CodegenCx<'a, 'tcx>) -> &'a Type { + let (llfields, packed) = struct_llfields(cx, layout); + cx.set_struct_body(llty, &llfields, packed); + } ++ if rustc_data_structures::invariants::enabled() && self.is_sized() { ++ // `RUSTC_CHECK_INVARIANTS`: LLVM's size of the type equals the layout's. ++ if rustc_data_structures::invariants::selftest() { ++ rustc_data_structures::invariants::report("selftest", "layout-views-agree runs"); ++ } ++ let llsize = unsafe { ++ crate::llvm::LLVMABISizeOfType(crate::llvm::LLVMGetModuleDataLayout(cx.llmod), llty) ++ }; ++ if llsize != self.size.bytes() { ++ rustc_data_structures::invariants::report( ++ "layout-views-agree", ++ &rustc_middle::ty::print::with_no_trimmed_paths!(format!( ++ "`{}`{}: layout size {} but LLVM type {:?} has size {llsize}", ++ self.ty, ++ variant_index.map(|v| format!(" variant {v:?}")).unwrap_or_default(), ++ self.size.bytes(), ++ llty ++ )), ++ ); ++ } ++ } + llty + } + +diff --git a/compiler/rustc_data_structures/src/invariants.rs b/compiler/rustc_data_structures/src/invariants.rs +new file mode 100644 +index 000000000000..dd6d0ebcc288 +--- /dev/null ++++ b/compiler/rustc_data_structures/src/invariants.rs +@@ -0,0 +1,38 @@ ++//! For testing the compiler: invariants checked on every compilation when ++//! `RUSTC_CHECK_INVARIANTS` is set. A violation prints one line, ++//! `rustc-invariant: :
`, and compilation continues. ++ ++use std::collections::HashSet; ++use std::sync::{Mutex, OnceLock}; ++ ++/// Whether `RUSTC_CHECK_INVARIANTS` is set (read once). ++#[inline] ++pub fn enabled() -> bool { ++ static ENABLED: OnceLock = OnceLock::new(); ++ *ENABLED.get_or_init(|| std::env::var_os("RUSTC_CHECK_INVARIANTS").is_some()) ++} ++ ++/// `RUSTC_CHECK_INVARIANTS=selftest`: also report which checks ran (to see that they are wired). ++#[inline] ++pub fn selftest() -> bool { ++ static SELFTEST: OnceLock = OnceLock::new(); ++ *SELFTEST.get_or_init(|| { ++ std::env::var_os("RUSTC_CHECK_INVARIANTS").is_some_and(|v| v == "selftest") ++ }) ++} ++ ++/// Reports a violation of `property`, once per distinct line. ++#[cold] ++pub fn report(property: &str, details: &str) { ++ static SEEN: Mutex>> = Mutex::new(None); ++ let mut details: String = ++ details.chars().map(|c| if c == '\n' { ' ' } else { c }).take(600).collect(); ++ if details.len() >= 600 { ++ details.push_str(" …"); ++ } ++ let line = format!("rustc-invariant: {property}: {details}"); ++ let mut seen = SEEN.lock().unwrap_or_else(|e| e.into_inner()); ++ if seen.get_or_insert_with(HashSet::new).insert(line.clone()) { ++ eprintln!("{line}"); ++ } ++} +diff --git a/compiler/rustc_data_structures/src/lib.rs b/compiler/rustc_data_structures/src/lib.rs +index dc71d2ae3245..895993435de3 100644 +--- a/compiler/rustc_data_structures/src/lib.rs ++++ b/compiler/rustc_data_structures/src/lib.rs +@@ -60,6 +60,7 @@ + pub mod fx; + pub mod graph; + pub mod intern; ++pub mod invariants; + pub mod jobserver; + pub mod marker; + pub mod memmap; +diff --git a/compiler/rustc_interface/src/passes.rs b/compiler/rustc_interface/src/passes.rs +index eb0c39a279b5..2554e266ad6a 100644 +--- a/compiler/rustc_interface/src/passes.rs ++++ b/compiler/rustc_interface/src/passes.rs +@@ -1305,6 +1305,65 @@ fn analysis(tcx: TyCtxt<'_>, (): ()) { + }); + }); + } ++ ++ if rustc_data_structures::invariants::enabled() { ++ check_no_error_types(tcx); ++ } ++} ++ ++/// `RUSTC_CHECK_INVARIANTS`: a compilation that emitted no error contains no error types (and ++/// no inference variables) in its typeck results, item types and signatures, and no typeck ++/// result is tainted by errors: an `ErrorGuaranteed` exists only once an error was emitted. ++fn check_no_error_types(tcx: TyCtxt<'_>) { ++ use std::cell::Cell; ++ ++ use rustc_data_structures::invariants::report; ++ use rustc_hir::def::DefKind; ++ use rustc_middle::ty::TypeVisitableExt; ++ ++ if tcx.dcx().has_errors_or_delayed_bugs().is_some() { ++ return; ++ } ++ if rustc_data_structures::invariants::selftest() { ++ report("selftest", "no-error-types runs"); ++ } ++ for def_id in tcx.hir_body_owners() { ++ if tcx.typeck_root_def_id(def_id.to_def_id()) != def_id.to_def_id() { ++ continue; ++ } ++ let results = tcx.typeck(def_id); ++ if results.tainted_by_errors.is_some() { ++ report("no-error-types", &format!("typeck({def_id:?}) is tainted by errors")); ++ } ++ let bad = Cell::new(None); ++ results.node_types().items().any(|(_, &ty)| { ++ let found = ty.references_error() || ty.has_infer(); ++ if found { ++ bad.set(Some(ty)); ++ } ++ found ++ }); ++ if let Some(ty) = bad.get() { ++ report("no-error-types", &format!("typeck({def_id:?}) has a node of type `{ty:?}`")); ++ } ++ } ++ for def_id in tcx.hir_crate_items(()).definitions() { ++ let what = match tcx.def_kind(def_id) { ++ DefKind::Const | DefKind::Static { .. } | DefKind::TyAlias | DefKind::AssocConst ++ | DefKind::Field => { ++ let ty = tcx.type_of(def_id).instantiate_identity().skip_norm_wip(); ++ (ty.references_error() || ty.has_infer()).then(|| format!("type_of = `{ty:?}`")) ++ } ++ DefKind::Fn | DefKind::AssocFn => { ++ let sig = tcx.fn_sig(def_id).instantiate_identity().skip_norm_wip(); ++ (sig.references_error() || sig.has_infer()).then(|| format!("fn_sig = `{sig:?}`")) ++ } ++ _ => None, ++ }; ++ if let Some(what) = what { ++ report("no-error-types", &format!("{def_id:?}: {what}")); ++ } ++ } + } + + /// Runs the codegen backend, after which the AST and analysis can +diff --git a/compiler/rustc_metadata/src/rmeta/encoder.rs b/compiler/rustc_metadata/src/rmeta/encoder.rs +index 8125544d62ae..a6c63ed6cf8a 100644 +--- a/compiler/rustc_metadata/src/rmeta/encoder.rs ++++ b/compiler/rustc_metadata/src/rmeta/encoder.rs +@@ -284,6 +284,15 @@ fn encode(&self, s: &mut EncodeContext<'a, 'tcx>) { + + // The Span infrastructure should make sure that this invariant holds: + debug_assert!(self.lo <= self.hi); ++ if rustc_data_structures::invariants::selftest() { ++ rustc_data_structures::invariants::report("selftest", "spans-valid runs"); ++ } ++ if rustc_data_structures::invariants::enabled() && self.lo > self.hi { ++ rustc_data_structures::invariants::report( ++ "spans-valid", ++ &format!("encoding a span with lo {:?} > hi {:?}", self.lo, self.hi), ++ ); ++ } + + if !s.source_file_cache.0.contains(self.lo) { + let source_map = s.tcx.sess.source_map(); +@@ -293,6 +302,15 @@ fn encode(&self, s: &mut EncodeContext<'a, 'tcx>) { + } + let (ref source_file, source_file_index) = s.source_file_cache; + debug_assert!(source_file.contains(self.lo)); ++ if rustc_data_structures::invariants::enabled() && !source_file.contains(self.lo) { ++ rustc_data_structures::invariants::report( ++ "spans-valid", ++ &format!( ++ "encoding a span whose lo {:?} is outside the file found for it, {:?} ({:?}..{:?})", ++ self.lo, source_file.name, source_file.start_pos, source_file.end_position() ++ ), ++ ); ++ } + + if !source_file.contains(self.hi) { + // Unfortunately, macro expansion still sometimes generates Spans +diff --git a/compiler/rustc_metadata/src/rmeta/table.rs b/compiler/rustc_metadata/src/rmeta/table.rs +index c023b61d7357..ad63e244f9af 100644 +--- a/compiler/rustc_metadata/src/rmeta/table.rs ++++ b/compiler/rustc_metadata/src/rmeta/table.rs +@@ -473,7 +473,26 @@ pub(crate) fn set(&mut self, i: Ie, value: T) { + // > trick (i.e. divide things into buckets of 32 or 64 items and then + // > store bit-masks of which item in each bucket is actually serialized). + let block = self.blocks.ensure_contains_elem(i, || [0; N]); +- value.write_to_bytes(block); ++ if rustc_data_structures::invariants::selftest() { ++ rustc_data_structures::invariants::report("selftest", "metadata-written-once runs"); ++ } ++ if rustc_data_structures::invariants::enabled() && block.iter().any(|&b| b != 0) { ++ // `RUSTC_CHECK_INVARIANTS`: each entry is written once. ++ let old = *block; ++ *block = [0; N]; ++ value.write_to_bytes(block); ++ rustc_data_structures::invariants::report( ++ "metadata-written-once", ++ &format!( ++ "table of `{}` entry {} set twice ({})", ++ std::any::type_name::(), ++ i.index(), ++ if old == *block { "same value" } else { "different value" } ++ ), ++ ); ++ } else { ++ value.write_to_bytes(block); ++ } + if self.width != N { + let width = N - trailing_zeros(block); + self.width = self.width.max(width); +diff --git a/compiler/rustc_middle/src/ty/context.rs b/compiler/rustc_middle/src/ty/context.rs +index dd39a11051c9..d31bddab6ae2 100644 +--- a/compiler/rustc_middle/src/ty/context.rs ++++ b/compiler/rustc_middle/src/ty/context.rs +@@ -2233,6 +2233,21 @@ fn check_args_compatible_inner( + /// [`Self::debug_assert_alias_term_args_compatible`] instead. See note on + /// [`Self::check_args_compatible`]. + pub fn debug_assert_args_compatible(self, def_id: DefId, args: &'tcx [ty::GenericArg<'tcx>]) { ++ if !cfg!(debug_assertions) && rustc_data_structures::invariants::enabled() { ++ // `RUSTC_CHECK_INVARIANTS` in a release build: report instead of the bug below. ++ if rustc_data_structures::invariants::selftest() { ++ rustc_data_structures::invariants::report("selftest", "interned-args-well-formed runs"); ++ } ++ if !self.check_args_compatible(def_id, args) { ++ rustc_data_structures::invariants::report( ++ "interned-args-well-formed", ++ &crate::ty::print::with_no_queries!(format!( ++ "args {args:?} do not fit the generics of {def_id:?}" ++ )), ++ ); ++ } ++ return; ++ } + if cfg!(debug_assertions) && !self.check_args_compatible(def_id, args) { + let is_inherent_assoc_ty = matches!(self.def_kind(def_id), DefKind::AssocTy) + && matches!(self.def_kind(self.parent(def_id)), DefKind::Impl { of_trait: false }); +@@ -2245,6 +2260,18 @@ pub fn debug_assert_alias_term_args_compatible( + kind: ty::AliasTermKind<'tcx>, + args: ty::GenericArgsRef<'tcx>, + ) { ++ if !cfg!(debug_assertions) && rustc_data_structures::invariants::enabled() { ++ // `RUSTC_CHECK_INVARIANTS` in a release build: report instead of the bug below. ++ if !self.check_alias_term_args_compatible(kind, args) { ++ rustc_data_structures::invariants::report( ++ "interned-args-well-formed", ++ &crate::ty::print::with_no_queries!(format!( ++ "args {args:?} do not fit the generics of alias {kind:?}" ++ )), ++ ); ++ } ++ return; ++ } + if cfg!(debug_assertions) { + self.debug_assert_alias_term_kind_matches_def_kind(kind); + if !self.check_alias_term_args_compatible(kind, args) { +diff --git a/compiler/rustc_monomorphize/src/partitioning.rs b/compiler/rustc_monomorphize/src/partitioning.rs +index 7fc1f98a8367..6ce00a0d835a 100644 +--- a/compiler/rustc_monomorphize/src/partitioning.rs ++++ b/compiler/rustc_monomorphize/src/partitioning.rs +@@ -101,11 +101,11 @@ + use rustc_attr_ir::lang_items::LangItem; + use rustc_attr_ir::{InlineAttr, Linkage}; + use rustc_data_structures::either::Either; +-use rustc_data_structures::fx::{FxIndexMap, FxIndexSet}; ++use rustc_data_structures::fx::{FxHashMap, FxIndexMap, FxIndexSet}; + use rustc_data_structures::sync::par_join; + use rustc_data_structures::unord::{UnordMap, UnordSet}; + use rustc_hir::def::DefKind; +-use rustc_hir::def_id::{DefId, DefIdSet, LOCAL_CRATE}; ++use rustc_hir::def_id::{CrateNum, DefId, DefIdSet, LOCAL_CRATE}; + use rustc_hir::definitions::DefPathDataName; + use rustc_middle::middle::codegen_fn_attrs::CodegenFnAttrFlags; + use rustc_middle::middle::exported_symbols::{SymbolExportInfo, SymbolExportLevel}; +@@ -1164,6 +1164,62 @@ fn assert_symbols_are_distinct<'a, 'tcx, I>(tcx: TyCtxt<'tcx>, mono_items: I) + tcx.dcx().emit_fatal(SymbolAlreadyDefined { span, symbol: sym1.to_string() }); + } + } ++ ++ if rustc_data_structures::invariants::enabled() { ++ check_symbols_distinct_from_upstream(tcx, &symbols); ++ } ++} ++ ++/// `RUSTC_CHECK_INVARIANTS`: no local mono item has the symbol name of a different item that ++/// an upstream crate exports (the two would collide at link time, or one would bind to the ++/// other). ++fn check_symbols_distinct_from_upstream<'tcx>( ++ tcx: TyCtxt<'tcx>, ++ symbols: &[(&MonoItem<'tcx>, ty::SymbolName<'tcx>)], ++) { ++ use rustc_middle::middle::exported_symbols::ExportedSymbol; ++ ++ let mut upstream: FxHashMap<&str, (MonoItem<'tcx>, CrateNum)> = FxHashMap::default(); ++ for &cnum in tcx.crates(()) { ++ let exported = tcx ++ .exported_non_generic_symbols(cnum) ++ .iter() ++ .chain(tcx.exported_generic_symbols(cnum)); ++ for &(symbol, _) in exported { ++ let item = match symbol { ++ ExportedSymbol::NonGeneric(def_id) if tcx.is_static(def_id) => { ++ MonoItem::Static(def_id) ++ } ++ ExportedSymbol::NonGeneric(def_id) => MonoItem::Fn(ty::Instance::mono(tcx, def_id)), ++ ExportedSymbol::Generic(def_id, args) => { ++ MonoItem::Fn(ty::Instance::new_raw(def_id, args)) ++ } ++ ExportedSymbol::DropGlue(ty) => MonoItem::Fn(ty::Instance::resolve_drop_glue(tcx, ty)), ++ _ => continue, ++ }; ++ upstream.insert(symbol.symbol_name_for_local_instance(tcx).name, (item, cnum)); ++ } ++ } ++ if rustc_data_structures::invariants::selftest() { ++ rustc_data_structures::invariants::report( ++ "selftest", ++ &format!("symbol-names-injective runs ({} upstream symbols)", upstream.len()), ++ ); ++ } ++ for (item, name) in symbols { ++ if let Some((other, cnum)) = upstream.get(name.name) ++ && other != *item ++ { ++ rustc_data_structures::invariants::report( ++ "symbol-names-injective", ++ &format!( ++ "`{}` is the symbol of local {item:?} and of {other:?} exported by `{}`", ++ name.name, ++ tcx.crate_name(*cnum) ++ ), ++ ); ++ } ++ } + } + + fn collect_and_partition_mono_items(tcx: TyCtxt<'_>, (): ()) -> MonoItemPartitions<'_> { +diff --git a/compiler/rustc_query_impl/src/invariants.rs b/compiler/rustc_query_impl/src/invariants.rs +new file mode 100644 +index 000000000000..6e7ee8a8a968 +--- /dev/null ++++ b/compiler/rustc_query_impl/src/invariants.rs +@@ -0,0 +1,209 @@ ++//! `RUSTC_CHECK_INVARIANTS`: a query's result contains no inference variables. ++//! ++//! Called on each provider's typed result before it is erased. The probe dispatches by autoref ++//! specialization on the value's type, most specific first: a `TypeVisitable` value; then an ++//! `EarlyBinder` or a reference around one; anything else is not checked. ++ ++use std::fmt::Debug; ++use std::marker::PhantomData; ++ ++use rustc_data_structures::invariants::{report, selftest}; ++use rustc_middle::ty::print::with_no_queries; ++use rustc_data_structures::fx::FxIndexMap; ++use rustc_middle::ty::layout::TyAndLayout; ++use rustc_middle::infer::canonical::Canonical; ++use rustc_middle::ty::{ ++ DefinitionSiteHiddenType, EarlyBinder, GenericClauses, ImplTraitHeader, TyCtxt, TypeVisitable, ++ TypeVisitableExt, TypeckResults, ++}; ++ ++pub(crate) struct Probe<'a, 'tcx, T>(&'a T, PhantomData>); ++ ++impl<'a, 'tcx, T> Probe<'a, 'tcx, T> { ++ pub(crate) fn new(value: &'a T) -> Self { ++ Probe(value, PhantomData) ++ } ++} ++ ++fn check<'tcx, V: TypeVisitable> + Debug>(value: &V, query: &str, key: &dyn Debug) { ++ if selftest() { ++ report("selftest", &format!("query-result-infer checks `{query}`")); ++ } ++ if value.has_infer() { ++ let what = if value.has_non_region_infer() { "type or const" } else { "region" }; ++ let (key, value) = with_no_queries!((format!("{key:?}"), format!("{value:?}"))); ++ report("query-result-infer", &format!("`{query}({key})` returned a {what} inference variable: {value}")); ++ } ++} ++ ++/// A `TypeVisitable` value (receiver `&&&Probe`). ++pub(crate) trait CheckVisitable { ++ fn check_infer(&self, query: &str, key: &dyn Debug); ++} ++ ++impl<'b, 'c, 'a, 'tcx, T: TypeVisitable> + Debug> CheckVisitable for &'b &'c Probe<'a, 'tcx, T> { ++ fn check_infer(&self, query: &str, key: &dyn Debug) { ++ check(self.0, query, key) ++ } ++} ++ ++/// An `EarlyBinder` around a `TypeVisitable` value (receiver `&&Probe`). ++pub(crate) trait CheckEarlyBinder { ++ fn check_infer(&self, query: &str, key: &dyn Debug); ++} ++ ++impl<'b, 'a, 'tcx, U: TypeVisitable> + Debug> CheckEarlyBinder ++ for &'b Probe<'a, 'tcx, EarlyBinder<'tcx, U>> ++{ ++ fn check_infer(&self, query: &str, key: &dyn Debug) { ++ check(self.0.as_ref().skip_binder(), query, key) ++ } ++} ++ ++/// A reference to a `TypeVisitable` value (receiver `&&Probe`). ++pub(crate) trait CheckRef { ++ fn check_infer(&self, query: &str, key: &dyn Debug); ++} ++ ++impl<'b, 'd, 'a, 'tcx, U: TypeVisitable> + Debug> CheckRef for &'b Probe<'a, 'tcx, &'d U> { ++ fn check_infer(&self, query: &str, key: &dyn Debug) { ++ check(*self.0, query, key) ++ } ++} ++ ++/// The same level: query results that wrap `TypeVisitable` parts in other types. Each impl is a ++/// trait of its own; for a given value type at most one applies. ++pub(crate) trait CheckResultRef { ++ fn check_infer(&self, query: &str, key: &dyn Debug); ++} ++ ++/// `Result<&T, E>`: canonical query responses, codegen selection. ++impl<'b, 'd, 'a, 'tcx, U: TypeVisitable> + Debug, E> CheckResultRef ++ for &'b Probe<'a, 'tcx, Result<&'d U, E>> ++{ ++ fn check_infer(&self, query: &str, key: &dyn Debug) { ++ if let Ok(v) = self.0 { ++ check(*v, query, key) ++ } ++ } ++} ++ ++pub(crate) trait CheckOptionBinder { ++ fn check_infer(&self, query: &str, key: &dyn Debug); ++} ++ ++impl<'b, 'a, 'tcx, U: TypeVisitable> + Debug> CheckOptionBinder ++ for &'b Probe<'a, 'tcx, Option>> ++{ ++ fn check_infer(&self, query: &str, key: &dyn Debug) { ++ if let Some(v) = self.0 { ++ check(v.as_ref().skip_binder(), query, key) ++ } ++ } ++} ++ ++pub(crate) trait CheckResultOptionBinder { ++ fn check_infer(&self, query: &str, key: &dyn Debug); ++} ++ ++impl<'b, 'a, 'tcx, U: TypeVisitable> + Debug, E> CheckResultOptionBinder ++ for &'b Probe<'a, 'tcx, Result>, E>> ++{ ++ fn check_infer(&self, query: &str, key: &dyn Debug) { ++ if let Ok(Some(v)) = self.0 { ++ check(v.as_ref().skip_binder(), query, key) ++ } ++ } ++} ++ ++pub(crate) trait CheckTypeck { ++ fn check_infer(&self, query: &str, key: &dyn Debug); ++} ++ ++/// Typeck results: every node's type (after writeback). ++impl<'b, 'd, 'a, 'tcx> CheckTypeck for &'b Probe<'a, 'tcx, &'d TypeckResults<'tcx>> { ++ fn check_infer(&self, query: &str, key: &dyn Debug) { ++ for (_, ty) in self.0.node_types().items_in_stable_order() { ++ check(ty, query, key); ++ } ++ } ++} ++ ++pub(crate) trait CheckHiddenTypes { ++ fn check_infer(&self, query: &str, key: &dyn Debug); ++} ++ ++/// Borrowck's hidden types of opaques. ++impl<'b, 'd, 'a, 'tcx, K, E> CheckHiddenTypes ++ for &'b Probe<'a, 'tcx, Result<&'d FxIndexMap>, E>> ++{ ++ fn check_infer(&self, query: &str, key: &dyn Debug) { ++ if let Ok(map) = self.0 { ++ for hidden in map.values() { ++ check(hidden.ty.as_ref().skip_binder(), query, key); ++ } ++ } ++ } ++} ++ ++pub(crate) trait CheckClauses { ++ fn check_infer(&self, query: &str, key: &dyn Debug); ++} ++ ++impl<'b, 'a, 'tcx> CheckClauses for &'b Probe<'a, 'tcx, GenericClauses<'tcx>> { ++ fn check_infer(&self, query: &str, key: &dyn Debug) { ++ for (clause, _) in self.0.clauses { ++ check(clause, query, key); ++ } ++ } ++} ++ ++pub(crate) trait CheckImplHeader { ++ fn check_infer(&self, query: &str, key: &dyn Debug); ++} ++ ++impl<'b, 'a, 'tcx> CheckImplHeader for &'b Probe<'a, 'tcx, ImplTraitHeader<'tcx>> { ++ fn check_infer(&self, query: &str, key: &dyn Debug) { ++ check(self.0.trait_ref.as_ref().skip_binder(), query, key) ++ } ++} ++ ++pub(crate) trait CheckLayout { ++ fn check_infer(&self, query: &str, key: &dyn Debug); ++} ++ ++impl<'b, 'a, 'tcx, E> CheckLayout for &'b Probe<'a, 'tcx, Result, E>> { ++ fn check_infer(&self, query: &str, key: &dyn Debug) { ++ if let Ok(layout) = self.0 { ++ check(&layout.ty, query, key) ++ } ++ } ++} ++ ++pub(crate) trait CheckCanonical { ++ fn check_infer(&self, query: &str, key: &dyn Debug); ++} ++ ++/// Canonical query responses: canonicalization replaced every inference variable. ++impl<'b, 'd, 'a, 'tcx, U: TypeVisitable> + Debug, E> CheckCanonical ++ for &'b Probe<'a, 'tcx, Result<&'d Canonical<'tcx, U>, E>> ++{ ++ fn check_infer(&self, query: &str, key: &dyn Debug) { ++ if let Ok(c) = self.0 { ++ check(&c.value, query, key) ++ } ++ } ++} ++ ++/// Anything else (receiver `&Probe`): not checked. ++pub(crate) trait SkipCheck { ++ fn check_infer(&self, query: &str, _key: &dyn Debug); ++} ++ ++impl<'a, 'tcx, T> SkipCheck for Probe<'a, 'tcx, T> { ++ fn check_infer(&self, query: &str, _key: &dyn Debug) { ++ if selftest() { ++ report("selftest", &format!("`{query}` not checked: {}", std::any::type_name::())); ++ } ++ } ++} +diff --git a/compiler/rustc_query_impl/src/lib.rs b/compiler/rustc_query_impl/src/lib.rs +index 5eecf938d29b..e319ecdca9e7 100644 +--- a/compiler/rustc_query_impl/src/lib.rs ++++ b/compiler/rustc_query_impl/src/lib.rs +@@ -22,6 +22,7 @@ + mod execution; + mod handle_cycle_error; + mod incremental; ++mod invariants; + mod job; + mod query_vtables; + mod self_profile; +diff --git a/compiler/rustc_query_impl/src/query_vtables.rs b/compiler/rustc_query_impl/src/query_vtables.rs +index 0d05be18233a..b88a0c300b96 100644 +--- a/compiler/rustc_query_impl/src/query_vtables.rs ++++ b/compiler/rustc_query_impl/src/query_vtables.rs +@@ -133,6 +133,13 @@ pub(crate) fn __rust_begin_short_backtrace<'tcx>( + tracing::trace!(?provided_value); + }); + ++ if rustc_data_structures::invariants::enabled() { ++ #[allow(unused_imports)] ++ use crate::invariants::*; ++ (&&&Probe::<'_, 'tcx, _>::new(&provided_value)) ++ .check_infer(stringify!($name), &key); ++ } ++ + // Erase the returned value, because `QueryVTable` uses erased values. + // For queries with `arena_cache`, this also arena-allocates the value. + provided_to_erased(tcx, provided_value) +diff --git a/compiler/rustc_ty_utils/src/layout.rs b/compiler/rustc_ty_utils/src/layout.rs +index 8c1634417c21..f2e63a727166 100644 +--- a/compiler/rustc_ty_utils/src/layout.rs ++++ b/compiler/rustc_ty_utils/src/layout.rs +@@ -770,7 +770,7 @@ fn layout_of_uncached<'tcx>( + } + + // If the struct tail is sized and can be unsized, check that unsizing doesn't move the fields around. +- if cfg!(debug_assertions) ++ if (cfg!(debug_assertions) || rustc_data_structures::invariants::enabled()) + && maybe_unsized + && def + .non_enum_variant() +diff --git a/compiler/rustc_ty_utils/src/layout/invariant.rs b/compiler/rustc_ty_utils/src/layout/invariant.rs +index 562a3ef387d9..cbf3ea7483b1 100644 +--- a/compiler/rustc_ty_utils/src/layout/invariant.rs ++++ b/compiler/rustc_ty_utils/src/layout/invariant.rs +@@ -19,7 +19,8 @@ pub(super) fn layout_sanity_check<'tcx>(cx: &LayoutCx<'tcx>, layout: &TyAndLayou + // FIXME(#124403): Once `repr_c_enums_larger_than_int` is a hard error, we could assert + // here that a repr(c) enum discriminant is never larger than a c_int. + +- if !cfg!(debug_assertions) { ++ // `RUSTC_CHECK_INVARIANTS` runs the expensive checks in release builds too. ++ if !cfg!(debug_assertions) && !rustc_data_structures::invariants::enabled() { + // Stop here, the rest is kind of expensive. + return; + } diff --git a/docs/hunt/metadata-written-twice.md b/docs/hunt/metadata-written-twice.md new file mode 100644 index 0000000..14d23d8 --- /dev/null +++ b/docs/hunt/metadata-written-twice.md @@ -0,0 +1,88 @@ +# Metadata records written twice + +Facts for findings 56 and 57. Found by the in-compiler invariant "each metadata record is +written once" (property 15 of [`properties.md`](../properties.md)), checked by +[`check-invariants.patch`](check-invariants.patch) under `RUSTC_CHECK_INVARIANTS`: a table entry +set a second time is reported (`rustc-invariant: metadata-written-once: ...`). + +In both cases the second write encodes the same value again at a new position and points the +table entry at it. The first copy stays in the file, unreferenced. Nothing reads the wrong +value; the cost is bytes, and the invariant no longer holds, so a real double write (one that +changes the value) would not stand out. + +## 56. The crate root's module children + +`encode_def_ids` (`compiler/rustc_metadata/src/rmeta/encoder.rs`) starts with +`self.encode_info_for_mod(CRATE_DEF_ID)`, and its loop over `tcx.iter_local_def_id()` then +reaches the crate root again, whose `def_kind` is `DefKind::Mod`: + +```rust + fn encode_def_ids(&mut self) { + self.encode_info_for_mod(CRATE_DEF_ID); + ... + for local_id in tcx.iter_local_def_id() { + ... + if let DefKind::Mod = def_kind { + self.encode_info_for_mod(local_id); + } +``` + +So `module_children_non_reexports`, `module_children_reexports` and `ambig_module_children` +are encoded twice for entry 0. An empty array is the table's default and is not written, so +only crates whose root has children in those tables are affected: every library with a public +item. `pub fn f() {}` alone, as a lib, reports both tables. + +UI sweep: 310 of the 18,624 standalone tests (`module_children_non_reexports`), 306 +(`module_children_reexports`), 1 (`ambig_module_children`, `entry-point/imported_main_conflict_lib.rs`). + +## 57. Coroutine layouts + +`encode_mir` records `mir_coroutine_witnesses` inside `if encode_opt { ... }` and again, +unconditionally, at the end of the loop body: + +```rust + if encode_opt { + ... + if self.tcx.is_coroutine(def_id.to_def_id()) + && let Some(witnesses) = tcx.mir_coroutine_witnesses(def_id) + { + record_some_lazy!(self.tables.mir_coroutine_witnesses[def_id.to_def_id()] <- witnesses); + } + } + ... + if self.tcx.is_coroutine(def_id.to_def_id()) + && let Some(witnesses) = tcx.mir_coroutine_witnesses(def_id) + { + record_some_lazy!(self.tables.mir_coroutine_witnesses[def_id.to_def_id()] <- witnesses); + } +``` + +Every coroutine (async fn, async block, `gen` block) whose optimized MIR is encoded has its +`CoroutineLayout` written twice. UI sweep: 71 tests. + +## Size + +A compiler with both double writes removed (crate root skipped in the loop, the first +coroutine write deleted), against the same compiler with them, at the pin: + +| crate | `.rmeta` with | without | saved | +|---|---:|---:|---:| +| `std` | 8,002,280 | 8,000,116 | 2,164 | +| `core` | 68,750,338 | 68,748,954 | 1,384 | +| `alloc` | 8,768,651 | 8,768,552 | 99 | +| a 4-line async library (`-O`) | 16,501 | 16,275 | 226 | + +With the change, the invariant reports nothing for either case (6 reports to 0 for the async +library, 2 to 0 for `pub fn f() {}`). + +## Versions + +Both double writes are in `encoder.rs` at 1.80.0, 1.90.0 and 1.98.0 (source read through the +GitHub API; the local checkout is shallow) and at the pin. Searches of rust-lang/rust issues +and PRs on 2026-10-10 found nothing about either. + +## Also reported, and intended + +Proc-macro crates (6 UI tests): `encode_def_path_table` writes each proc macro's `def_keys` +entry, and `encode_proc_macros` then writes it again with `DefPathData::MacroNs(name)`. The +second write is deliberate (it changes the value); the first `DefKey` stays in the file. diff --git a/rustc/big-run.sh b/rustc/big-run.sh index ae40a59..f6e3588 100755 --- a/rustc/big-run.sh +++ b/rustc/big-run.sh @@ -43,6 +43,7 @@ run() { # name args... } sweep=(--tests "$tests" --jobs "$jobs") +run invariant-sweep invariant-sweep --rustc "$work/rustc-verify13/bin/rustc" "${sweep[@]}" --work "$dir/invariant-sweep" run diag-check diag-check --rustc "$rustc" "${sweep[@]}" --work "$dir/diag-check" 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" diff --git a/rustc/regen-patches.sh b/rustc/regen-patches.sh index cce662d..249f06e 100755 --- a/rustc/regen-patches.sh +++ b/rustc/regen-patches.sh @@ -1,47 +1,56 @@ #!/bin/bash -# Regenerate docs/hunt/{verify-reuse,report-untracked}.patch from ~/mirth-work/rust, which has -# applied: verify-reuse, the three fixes, report-untracked, and the stopgaps. +# Regenerate docs/hunt/{verify-reuse,report-untracked,check-invariants}.patch from +# ~/mirth-work/rust, which has applied, in order: verify-reuse, the three fixes, +# report-untracked, the stopgaps, and check-invariants. set -e # Stopgaps for findings 9-12, each made from its own files (applied after report-untracked). STOPGAPS="thinlto-order-stopgap print-type-sizes-trimmed-stopgap no-prepopulate-thinlto-stopgap rwpi-stopgap upstream-alloc-reference alloc-canonical-metadata-stopgap compiletest-solver-pin" H=$HOME/mirth-work/patches; RUST=$HOME/mirth-work/rust; T=$CLAUDE_JOB_DIR/tmp/regen +# Files the patches add (untracked in the tree). +NEW_REPORT="compiler/rustc_data_structures/src/untracked.rs" +NEW_INVARIANTS="compiler/rustc_data_structures/src/invariants.rs compiler/rustc_query_impl/src/invariants.rs" cd $RUST; git worktree remove --force $T 2>/dev/null || true; git worktree prune V="compiler/rustc_codegen_llvm/src/back/llvm_backend.rs compiler/rustc_codegen_llvm/src/base.rs compiler/rustc_codegen_llvm/src/llvm/ffi.rs compiler/rustc_codegen_ssa/src/base.rs compiler/rustc_codegen_ssa/src/traits/backend.rs compiler/rustc_incremental/src/persist/save.rs compiler/rustc_metadata/src/rmeta/encoder.rs compiler/rustc_middle/src/hooks.rs compiler/rustc_middle/src/query/on_disk_cache.rs compiler/rustc_query_impl/src/incremental.rs compiler/rustc_query_impl/src/lib.rs" # 1. the reuse check alone, relative to the pinned commit git worktree add -q --detach $T HEAD; cd $T git apply $H/generics-index-map.patch $H/alloc-dedup-on-decode.patch $H/metadata-source-files.patch -for f in $V; do cp $RUST/$f $T/$f; done -python3 - $T $V <<'PY' -import sys,re,pathlib -T=sys.argv[1] -for f in sys.argv[2:]: - p=pathlib.Path(T)/f; s=p.read_text() - s=re.sub(r'\(\*([\w\.\(\)]+?)\.(cg|unstable_opts)\.read_(\w+)\(\)\)', r'\1.\2.\3', s) - out=[] - for l in s.split('\n'): - if 'untracked::untracked_read(' in l or 'declare_untracked_input(' in l: - if out and out[-1].strip().startswith('// Its length, line table and content hash are read'): out.pop() - continue - out.append(l) - p.write_text('\n'.join(out)) -PY +for f in $V; do + cp $RUST/$f $T/$f + # Take check-invariants out of the copy (it is the last patch of the stack). + git apply -R --include=$f $H/check-invariants.patch + # Take report-untracked out: its option reads, and its declared and reported reads. + sed -E -i 's/\(\*([[:alnum:]_.()]+)\.(cg|unstable_opts)\.read_([[:alnum:]_]+)\(\)\)/\1.\2.\3/g' $f + sed -E -i '/\/\/ Its length, line table and content hash are read/{N;/untracked::untracked_read\(|declare_untracked_input\(/d}' $f + sed -E -i '/untracked::untracked_read\(|declare_untracked_input\(/d' $f +done git apply -R $H/generics-index-map.patch $H/alloc-dedup-on-decode.patch $H/metadata-source-files.patch git diff HEAD > $H/verify-reuse.patch # 2. the report patch, relative to the check and the fixes git checkout -q HEAD -- .; git apply $H/verify-reuse.patch git apply $H/generics-index-map.patch $H/alloc-dedup-on-decode.patch $H/metadata-source-files.patch git add -A -cd $RUST; for f in $(git diff --name-only) compiler/rustc_data_structures/src/untracked.rs; do cp $f $T/$f; done -cd $T; git apply -R $H/debuginfo-checksum-stopgap.patch; git apply -R $H/threads-def-order-stopgap.patch +cd $RUST; for f in $(git diff --name-only) $NEW_REPORT $NEW_INVARIANTS; do cp $f $T/$f; done +cd $T; git apply -R $H/check-invariants.patch +git apply -R $H/debuginfo-checksum-stopgap.patch; git apply -R $H/threads-def-order-stopgap.patch for p in $STOPGAPS; do git apply -R $H/$p.patch; done -git add -N compiler/rustc_data_structures/src/untracked.rs +git add -N $NEW_REPORT git diff > $H/report-untracked.patch -# 3. check the stack reproduces the tree -git checkout -q HEAD -- .; git reset -q; rm -f compiler/rustc_data_structures/src/untracked.rs +# 3. the invariants patch, relative to everything before it +git checkout -q HEAD -- .; git reset -q; rm -f $NEW_REPORT $NEW_INVARIANTS git apply $H/verify-reuse.patch git apply $H/generics-index-map.patch $H/alloc-dedup-on-decode.patch $H/metadata-source-files.patch git apply $H/report-untracked.patch; git apply $H/debuginfo-checksum-stopgap.patch $H/threads-def-order-stopgap.patch for p in $STOPGAPS; do git apply $H/$p.patch; done -n=0; cd $RUST; for f in $(git diff --name-only) compiler/rustc_data_structures/src/untracked.rs; do cmp -s $f $T/$f || { echo "differs: $f"; n=$((n+1)); }; done -echo "$n files differ"; wc -l $H/verify-reuse.patch $H/report-untracked.patch | head -2 +git add -A +cd $RUST; for f in $(git diff --name-only) $NEW_INVARIANTS; do cp $f $T/$f; done +cd $T; git add -N $NEW_INVARIANTS; git diff > $H/check-invariants.patch +# 4. check the stack reproduces the tree +git checkout -q HEAD -- .; git reset -q; rm -f $NEW_REPORT $NEW_INVARIANTS +git apply $H/verify-reuse.patch +git apply $H/generics-index-map.patch $H/alloc-dedup-on-decode.patch $H/metadata-source-files.patch +git apply $H/report-untracked.patch; git apply $H/debuginfo-checksum-stopgap.patch $H/threads-def-order-stopgap.patch +for p in $STOPGAPS; do git apply $H/$p.patch; done +git apply $H/check-invariants.patch +n=0; cd $RUST; for f in $(git diff --name-only) $NEW_REPORT $NEW_INVARIANTS; do cmp -s $f $T/$f || { echo "differs: $f"; n=$((n+1)); }; done +echo "$n files differ"; wc -l $H/verify-reuse.patch $H/report-untracked.patch $H/check-invariants.patch | head -3 git worktree remove --force $T