unlock unlimited code runs for $79, limited time →
NOSSLETTER.techLIVE
back to OSSpedia
rust-lang/rust
senior7/10
build-systemsrefactoring

Miri: give the incremental session a chance to finish

Project: rust-lang/rust | Language: Rust | Concepts: build-systems, refactoring | Difficulty: senior
Link: https://github.com/rust-lang/rust/pull/160760

The problem

Miri, the Rust interpreter, was previously terminating the compilation process early after interpretation, which prevented the Rust compiler's incremental compilation session from finalizing cleanly. This led to unnecessarily slow re-runs and incomplete incremental state, making repeated test runs much slower than necessary.

Why it's non-trivial

A naive fix might just "let the process continue" after Miri finishes, but the Rust compiler expects certain codegen and linking steps to occur, even if they are no-ops. Miri needs to provide a codegen backend that does nothing but still satisfies the compiler's expectations, and it must handle the distinction between dependency crates and the binary-under-test. If this isn't handled carefully, the build system could crash, hang, or produce incorrect artifacts.

The approach

1. Introduce a custom codegen backend for Miri

A new struct, MiriCodegenBackend, is introduced to wrap both the native codegen backend and a dummy backend, and to track whether it's being used for a dependency or the main binary:

struct MiriCodegenBackend {
    native: Box<dyn CodegenBackend>,
    dummy: DummyCodegenBackend,
    /// Whether we are in a dependency or in the to-be-interpreted binary crate
    dep: bool,
}

Annotation:
This struct allows Miri to selectively forward calls to either the native backend (for target config) or the dummy backend (for all other codegen operations), and to behave differently depending on whether it's compiling a dependency or the main crate.


2. Replace early process exit with Compilation::Continue

Previously, Miri would call exit(...) after interpretation, which aborted the compiler pipeline. Now, it returns Compilation::Continue so the compiler can finalize the incremental compilation session:

if let Some(return_code) = miri::eval_entry(tcx, entry_fn) {
    tcx.dcx().abort_if_errors();
    exit(return_code.get());
} else {
-    exit(rustc_driver::EXIT_SUCCESS);
+    // We want to continue here so rustc can do its usual shutdown and finalize the
+    // incremental session. Our custom codegen backend ensures nothing actually happens.
+    return Compilation::Continue;
}

Annotation:
This change ensures that, after Miri runs, the Rust compiler's shutdown sequence (including incremental state finalization) is allowed to complete. The custom backend ensures that no actual codegen or linking occurs.


3. Implement the custom backend's trait methods

MiriCodegenBackend implements CodegenBackend, forwarding most operations to the dummy backend, but customizing target_config (to match the native backend's target features) and link (to do nothing for the main binary):

impl CodegenBackend for MiriCodegenBackend {
    fn name(&self) -> &'static str {
        "miri"
    }

    fn target_config(&self, sess: &Session) -> TargetConfig {
        let native_target_config = self.native.target_config(sess);
        TargetConfig {
            internal_target_features: native_target_config.internal_target_features,
            has_reliable_f16: true,
            has_reliable_f128: true,
            has_reliable_f16_math: cfg!(target_has_reliable_f16_math),
            has_reliable_f128_math: false,
        }
    }

    // Everything complicated is forwarded to the dummy backend.
    fn supported_crate_types(&self, sess: &Session) -> Vec<CrateType> {
        self.dummy.supported_crate_types(sess)
    }
    fn codegen_crate<'tcx>(&self, tcx: TyCtxt<'tcx>) -> Box<dyn Any> {
        self.dummy.codegen_crate(tcx)
    }
    fn join_codegen(...) -> (CompiledModules, ...) {
        self.dummy.join_codegen(...)
    }
    fn link(&self, sess: &Session, ...) {
        // In the binary this should do nothing.
        if self.dep {
            self.dummy.link(sess, ...)
        }
    }
}

Annotation:
This ensures that the backend does just enough for the compiler to proceed, but doesn't actually produce code or artifacts for the main binary. For dependencies, it still produces rlibs as needed.


4. Adjust how the backend is constructed

The backend is now constructed with a dep flag, and the creation logic is updated accordingly:

fn make_miri_codegen_backend(sess: &Session, dep: bool) -> Box<dyn CodegenBackend> {
    let early_dcx = EarlyDiagCtxt::new(sess.opts.error_format);
    let native_codegen_backend = rustc_interface::util::get_codegen_backend(
        &early_dcx, &sess.opts.sysroot, None, &sess.target,
    );
    native_codegen_backend.init(sess);

    Box::new(MiriCodegenBackend { native: native_codegen_backend, dummy: DummyCodegenBackend, dep })
}

And the callbacks now pass the correct dep flag:

config.make_codegen_backend =
    Some(Box::new(|sess| make_miri_codegen_backend(sess, /* dep */ false)));
// ...
config.make_codegen_backend =
    Some(Box::new(|sess| make_miri_codegen_backend(sess, /* dep */ true)));

Annotation:
This ensures that the backend knows whether it should produce artifacts (for dependencies) or do nothing (for the main binary).


From the review thread

No notable review discussion on this PR.

Takeaways

  • When customizing build or execution flows, ensure that all expected pipeline stages are satisfied, even if they are no-ops.
    Skipping steps can break assumptions in the build system or leave state incomplete.

  • Use custom backend implementations to intercept and control toolchain behavior without violating interface contracts.
    This allows for advanced use-cases (like interpretation instead of codegen) while maintaining compatibility.

  • Distinguish between different compilation contexts (e.g., dependencies vs. main binary) to avoid unnecessary work or missing required outputs.
    Context-aware logic prevents both wasted effort and subtle build breakage.