Every rustc_private workaround mirth needs, and why. They are
version-specific: an API that exists on one nightly can be gone on the next.
rust-toolchain.toml pins the compiler these were learned against.
<sysroot>/lib/rustlib/rustc-src/rust/compiler/ ships with the rustc-dev
component (not rustlib/src/rust, which is rust-src, the standard library
only). rustc_middle/src/mir/syntax.rs defines every MIR enum. Documentation
and blog posts describe some other nightly.
The crates that use rustc_private say so in
[package.metadata.rust-analyzer]. rust-analyzer also needs the compiler's
source, which rustc-dev installs; point it there with
"rust-analyzer.rustc.source": "discover"in the editor's settings.
- Cargo puts the real compiler first. A
RUSTC_WRAPPERis invoked as<wrapper> <rustc> <args…>, and rustc's parser would read that path as a source file.mirth::rundrops argument 1 when its file stem isrustc. By shape, not position, so the same binary also works asRUSTC=. - Cargo probes the compiler first.
rustc -vVandrustc --print …compile nothing;rustc_driver::compiler_entrypointanswers them correctly.examples/count-calls/tests/cargo.rscompares the answer with the real compiler's. - What Cargo tells a wrapper.
CARGO_PRIMARY_PACKAGEmarks the packages the user asked to build,CARGO_MANIFEST_PATHnames the crate's manifest, and Cargo's probe has neither.mirth::configreads them. - sccache. A global
build.rustc-wrapper = sccachecomposes with a mirth wrapper. sccache's key does not describe what the plugin did to the MIR, so a hit would be an artifact that silently is not instrumented. SetRUSTC_WRAPPERexplicitly when running a plugin.
A rustc_private binary links librustc_driver and the toolchain's std
dynamically, and neither is on the default loader path.
mirth_build::link_to_the_toolchain emits an rpath into <sysroot>/lib on
Linux and macOS. Windows has no rpath; <sysroot>/bin must be on PATH
(Cargo arranges it for cargo run and cargo test).
rustc finds its own sysroot from where librustc_driver was loaded. That is
inference about the loader, so mirth::run also passes --sysroot with the
path baked in at build time.
- Through
optimized_mir.Config::override_queriesreplaces the provider. The override callsrustc_interface::DEFAULT_QUERY_PROVIDERS.queries.optimized_mirfor the compiler's body, then asks the plugin. Inspecting and rewriting both happen there: elsewhere,tcx.optimized_mir()is the override, and whichever body a query returns first is the one codegen gets. - The provider is a bare
fn.override_queriestakes a function pointer with nowhere for state, so the plugin lives in a global behind aMutex(not a thread-local: the query is asked from any thread). - Not for const bodies. rustc asserts that
optimized_miris not asked for a body evaluated at compile time, andDefKind::Fnis not enough to rule that out.mirth::plugin::has_optimized_miralso checkshir_body_const_context. Found compilingcorewith-Zbuild-std. - Closures too. After analysis, mirth asks for the optimized MIR of every
function and closure before calling the plugin's
finished. Code generation asks for most bodies only later, so a closure left out is rewritten after the plugin has reported, and anything it recorded about that body is lost. - The MIR inliner runs first.
optimized_mirhands over a body the inliner has already worked on, so a call to a small function such asstd::fs::renamemay be gone, replaced by that function's own calls.mirth-watchpasses-Zinline-mir=noto the crates it instruments; code generation still inlines afterwards. - Nothing checks what is put in.
optimized_mirruns after borrow checking and unsafety checking, so MIR emitted there is never verified. A wrong type surfaces as an ICE in codegen, or not at all.
--extern force:name=pathloads a crate nobody refers to; a plain--externis dropped as unused. It needs-Zunstable-options.-L dependency=for the crate's own directory and itsdepsdirectory. rustc finds a dependency of a dependency by searching, never through--extern, so every crate downstream of an instrumented one needs the runtime's directory on its search path. Without it the error names the crate being loaded, not the runtime: "can't find crate forrustc_middle".- Its functions have no path to resolve. Mark them
#[rustc_diagnostic_item = "…"]and look them up withtcx.get_diagnostic_item. Statics cannot carry that attribute: take the crate number from a function that can, and searchtcx.module_children(DefId { krate, index: CRATE_DEF_INDEX }). - Cargo runs
rustdocdirectly, not through the wrapper, so doctests of an instrumented crate need the same--extern force:and-LinRUSTDOCFLAGS.
mirth-watch writes down arguments by passing a reference to a generic hook,
argument::<T>(&T), with T: Capture. MIR built in optimized_mir is not
type-checked, so the plugin checks T itself and only calls the hook for a
type the runtime accepts.
- Plain data (structs and tuples of numbers, such as
DefId) is captured number by number, with field projections; nothing of the program's own code runs. - Pattern types. rustc's
newtype_indextypes store their value aspattern_type!(u32 is 0..=MAX). Such a field is transmuted to its base integer (same layout) before it is captured. Debugruns the program's code, so it is used only where the configuration asks, and only for a type that implements it, checked withtype_implements_trait.- What cannot be captured is reported while compiling, naming the site,
the argument and its type. Two kinds remain in
rustc/rmeta.toml: a generic parameter (TableBuilder::setcalled inside generic code, where the index type is stillIe), because capture is decided before monomorphization; and an enum (the(CrateNum, SimplifiedType)key ofcrate_incoherent_impls), because plain data is structs and tuples only.
Moving the pin breaks things in rustc_private. What has changed so far:
| older | now |
|---|---|
Rvalue::Use(Operand) |
Rvalue::Use(Operand, WithRetag) |
Rvalue::Len, NullaryOp, ShallowInitBox |
gone; a slice's length is UnOp::PtrMetadata |
StatementKind::Retag, Deinit |
gone |
Operand::{Copy, Move, Constant} |
plus Operand::RuntimeChecks |
Rvalue::CheckedBinaryOp |
BinOp::AddWithOverflow and friends |
providers.optimized_mir |
providers.queries.optimized_mir |
struct literals for Statement, BasicBlockData |
Statement::new, BasicBlockData::new_stmts |
Terminator { source_info, kind } |
plus attributes (2026-07-18), renamed loop_hint_attrs (2026-10-06) |
rustc_driver::run_compiler |
rustc_driver::compiler_entrypoint (2026-10-06) |
type_of(..).instantiate_identity() returns Ty |
returns Unnormalized<Ty>; call skip_normalization() |
Ty::new_fn_def takes a ty::Binder around the generic arguments.
Bootstrap's rustc shim runs RUSTC_WRAPPER_REAL as <wrapper> <rustc> <args…>, so a plugin can build rustc itself. Four things need care:
- Stage 0 must not have
rustc-dev. Bootstrap copies stage 0's libraries into the sysroot it compiles the compiler against. Prebuiltrustc_*crates there are found ahead of the ones being built, and the build fails with "found possibly newer version of crate". The plugin itself needsrustc-dev, sorustc/setup.shgives bootstrap a copy of the pinned nightly with that component's files removed. - Crates compiled without the plugin need the runtime too. A crate's
metadata records its dependencies transitively, and
rustc_data_structuresusestempfile, so instrumentingtempfilemakes most of the compiler depend on the runtime. Bootstrap compiles some crates withoutRUSTC_WRAPPER_REAL,rustdocamong them, and they fail with "can't find crate formirth_runtimewhichrustc_abidepends on".rustc/build.shandrustc/suites.shput the runtime's directory on every compile's search path throughRUSTFLAGS_BOOTSTRAPandRUSTFLAGS_NOT_BOOTSTRAP; at this commit the compiler andrustdocget the second. Copying the runtime into the stage 0 sysroot does not work: bootstrap re-syncs that directory. - Every
x.pyrun needs the same flags. Those variables are part of what Cargo fingerprints. Anx.pyrun without them recompiles the compiler, and without the wrapper, so the instrumentation is silently gone. - Cargo does not know about the wrapper. Changing the plugin or its
configuration does not rebuild anything.
rustc/build.sh --againdeletes the fingerprints of the crates in scope.
rustc uses jemalloc. A plugin binary uses the system allocator unless it
does the same, which measured about 16% slower on a large build. mirth does
not do this yet. #[global_allocator] aborts, because librustc_driver
already has an allocator compiled in. What works is rustc's own trick: link
tikv-jemalloc-sys with unprefixed_malloc_on_supported_platforms, and
keep its C symbols with #[used] statics so they interpose malloc for the
whole process.