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