Skip to main content

charon_driver/
driver.rs

1//! Run the rustc compiler with our custom options and hooks.
2use crate::CharonFailure;
3use crate::toolchain::toolchain_version;
4use crate::translate::translate_crate;
5use charon_lib::errors::ErrorCtx;
6use charon_lib::options::{self, CliOpts};
7use charon_lib::transform::TransformCtx;
8use charon_lib::utils::arg_value;
9use itertools::Itertools;
10use rustc_driver::{Callbacks, Compilation};
11use rustc_interface::Config;
12use rustc_interface::interface::Compiler;
13use rustc_middle::ty::{InstanceKind, TyCtxt};
14use rustc_middle::util::Providers;
15use rustc_session::config::{OutputType, OutputTypes};
16use rustc_span::ErrorGuaranteed;
17use std::num::NonZero;
18use std::path::PathBuf;
19use std::process::Command;
20use std::sync::atomic::{AtomicBool, Ordering};
21use std::{env, fmt};
22
23/// Helper that runs the compiler and catches its fatal errors.
24fn run_compiler_with_callbacks(
25    args: Vec<String>,
26    callbacks: &mut (dyn Callbacks + Send),
27) -> Result<(), CharonFailure> {
28    rustc_driver::catch_fatal_errors(|| rustc_driver::run_compiler(&args, callbacks))
29        .map_err(|_| CharonFailure::RustcError)
30}
31
32/// Tweak options to get usable MIR even for foreign crates.
33fn set_mir_options(config: &mut Config) {
34    config.opts.unstable_opts.always_encode_mir = true;
35    config.opts.unstable_opts.mir_opt_level = Some(0);
36    config.opts.unstable_opts.mir_preserve_ub = true;
37    let disabled_mir_passes = ["CheckAlignment", "CheckNull"];
38    for pass in disabled_mir_passes {
39        config
40            .opts
41            .unstable_opts
42            .mir_enable_passes
43            .push((pass.to_owned(), false));
44    }
45}
46
47/// Enable rustc's parallel front-end.
48fn set_parallel_frontend(config: &mut Config) {
49    if config.opts.jobs.frontend.is_none() {
50        // Match rustc's `--jobs-frontend=0` behavior.
51        const RUSTC_MAX_THREADS_CAP: usize = u8::MAX as usize;
52        let threads = std::thread::available_parallelism()
53            .map_or(1, |n| n.get())
54            .min(RUSTC_MAX_THREADS_CAP);
55        config.opts.jobs.frontend = NonZero::new(threads).filter(|n| n.get() > 1);
56    }
57}
58
59// We use a static to be able to pass data to `override_queries`.
60static SKIP_BORROWCK: AtomicBool = AtomicBool::new(false);
61fn set_skip_borrowck() {
62    SKIP_BORROWCK.store(true, Ordering::SeqCst);
63}
64fn skip_borrowck_if_set(providers: &mut Providers) {
65    if SKIP_BORROWCK.load(Ordering::SeqCst) {
66        providers.queries.mir_borrowck = |tcx, _def_id| {
67            // Empty result, which is what is used for custom_mir bodies.
68            Ok(tcx.arena.alloc(Default::default()))
69        }
70    }
71}
72
73fn setup_compiler(
74    config: &mut Config,
75    options: &CliOpts,
76    do_translate: bool,
77    emit_artifacts: bool,
78    codegen: bool,
79) {
80    if do_translate {
81        if options.skip_borrowck {
82            // We use a static to be able to pass data to `override_queries`.
83            set_skip_borrowck();
84        }
85
86        config.override_queries = Some(|_sess, providers| {
87            skip_borrowck_if_set(providers);
88
89            // TODO: catch the MIR in-flight to avoid stealing issues?
90            // providers.mir_built = |tcx, def_id| {
91            //     let mir = (rustc_interface::DEFAULT_QUERY_PROVIDERS.mir_built)(tcx, def_id);
92            //     let mut mir = mir.steal();
93            //     // use the mir
94            //     tcx.alloc_steal_mir(mir)
95            // };
96        });
97
98        config.opts.unstable_opts.no_codegen = !codegen;
99        if !emit_artifacts {
100            config.opts.output_types = OutputTypes::new(&[(OutputType::Object, None)]);
101        }
102        set_parallel_frontend(config);
103    }
104    set_mir_options(config);
105}
106
107/// Run a couple of rustc queries that don't involve MIR (so that they don't steal it). Returns
108/// whether rustc reported errors. This lets us avoid running Charon translation on crates rustc
109/// already rejects.
110fn precheck_rustc_errors(tcx: TyCtxt<'_>) -> bool {
111    type QueryResult = Result<(), ErrorGuaranteed>;
112
113    tcx.par_hir_for_each_module(|module| {
114        tcx.ensure_ok().check_mod_attrs(module);
115        tcx.ensure_ok().check_mod_unstable_api_usage(module);
116    });
117
118    let _: QueryResult = tcx.ensure_result().check_type_wf(());
119    for &trait_def_id in tcx.all_local_trait_impls(()).keys() {
120        let _: QueryResult = tcx.ensure_result().coherent_trait(trait_def_id);
121    }
122    let _: QueryResult = tcx.ensure_result().crate_inherent_impls_validity_check(());
123    let _: QueryResult = tcx.ensure_result().crate_inherent_impls_overlap_check(());
124
125    tcx.par_hir_body_owners(|def_id| {
126        let def_kind = tcx.def_kind(def_id);
127        if !matches!(def_kind, rustc_hir::def::DefKind::AnonConst)
128            && !tcx.is_typeck_child(def_id.to_def_id())
129        {
130            tcx.ensure_ok().typeck(def_id);
131        }
132    });
133
134    tcx.dcx().has_errors().is_some()
135}
136
137/// Run rustc checks that normally happen close to codegen, so that we get all the post-mono errors
138/// etc.
139fn check_late_rustc_errors(tcx: TyCtxt<'_>) {
140    tcx.par_hir_body_owners(|def_id| {
141        let _ = tcx.instance_mir(InstanceKind::Item(def_id.to_def_id()));
142    });
143
144    if tcx.dcx().err_count() == 0 {
145        let _ = tcx.collect_and_partition_mono_items(());
146    }
147}
148
149/// Whether this sysroot provides libraries for the given target.
150fn sysroot_has_target(sysroot: &std::path::Path, target: &str) -> bool {
151    sysroot.join("lib").join("rustlib").join(target).is_dir()
152}
153
154/// Where we remember the sysroot that `cargo miri setup` computed.
155fn miri_sysroot_cache_file(target: &str) -> Option<PathBuf> {
156    let toolchain = toolchain_version();
157    let cache_dir = match env::var_os("CHARON_CACHE_DIR") {
158        Some(dir) => PathBuf::from(dir),
159        None => env::home_dir()?.join(".cache").join("charon"),
160    };
161    Some(
162        cache_dir
163            .join("full-mir-sysroot-cache")
164            .join(format!("{toolchain}-{target}")),
165    )
166}
167
168/// `cargo miri setup` sets up a sysroot containing a standard library built with
169/// `-Zalways-encode-mir`.
170fn setup_miri_sysroot(target: &str) -> Option<PathBuf> {
171    if let Some(root) = env::var_os("CHARON_MIRI_SYSROOTS")
172        && let sysroot = PathBuf::from(root)
173        && sysroot_has_target(&sysroot, target)
174    {
175        return Some(sysroot);
176    }
177
178    // Checked if we have this path in cache.
179    if let Some(cache_file) = miri_sysroot_cache_file(target)
180        && let Ok(contents) = std::fs::read_to_string(&cache_file)
181        && let sysroot = PathBuf::from(contents.trim())
182        && sysroot_has_target(&sysroot, target)
183    {
184        return Some(sysroot);
185    }
186
187    let mut cmd = Command::new("cargo");
188    cmd.arg("miri")
189        .arg("setup")
190        .arg(format!("--target={target}"))
191        .arg("--print-sysroot")
192        .env_remove("RUSTC_WORKSPACE_WRAPPER")
193        .env_remove("RUSTC_WRAPPER");
194
195    let output = match cmd.output() {
196        Ok(output) => output,
197        Err(err) => {
198            eprintln!(
199                "warning: failed to run `cargo miri setup` for target `{target}`; \
200                falling back to rustc's default sysroot: {err}"
201            );
202            return None;
203        }
204    };
205
206    if !output.status.success() {
207        let stderr = String::from_utf8_lossy(&output.stderr);
208        eprintln!(
209            "warning: `cargo miri setup` failed for target `{target}`; \
210            falling back to rustc's default sysroot: {}",
211            stderr.trim()
212        );
213        return None;
214    }
215
216    let stdout = String::from_utf8_lossy(&output.stdout);
217    let sysroot = stdout.lines().map(str::trim).find(|line| !line.is_empty());
218    match sysroot {
219        Some(sysroot) => {
220            // Memoise where the sysroot is, to avoid a subprocess call for all tests.
221            if let Some(cache_file) = miri_sysroot_cache_file(target)
222                && let Some(cache_dir) = cache_file.parent()
223                && std::fs::create_dir_all(cache_dir).is_ok()
224            {
225                let _ = std::fs::write(&cache_file, sysroot);
226            }
227            Some(PathBuf::from(sysroot))
228        }
229        None => {
230            eprintln!(
231                "warning: `cargo miri setup --print-sysroot` printed no sysroot for target \
232                `{target}`; falling back to rustc's default sysroot"
233            );
234            None
235        }
236    }
237}
238
239/// Run the rustc driver with our custom hooks. Returns `None` if the crate was not compiled with
240/// charon (e.g. because it was a dependency). Otherwise returns the translated crate, ready for
241/// post-processing transformations.
242pub fn run_rustc_driver() -> Result<Option<(TransformCtx, CliOpts)>, CharonFailure> {
243    // Retreive the command-line arguments pased to `charon_driver`. The first arg is the path to
244    // the current executable, we skip it.
245    let mut compiler_args: Vec<String> = env::args().skip(1).collect();
246    // We use `RUSTC_WORKSPACE_WRAPPER` to break cargo's caching; that value ends up as our first
247    // argument.
248    if compiler_args
249        .first()
250        .is_some_and(|arg| arg.starts_with("charon-dont-cache-this-"))
251    {
252        compiler_args.remove(0);
253    }
254    trace!(
255        "charon-driver called with args: {}",
256        compiler_args.iter().format(" ")
257    );
258
259    // When called using cargo, we tell cargo to use `charon-driver` by setting the `RUSTC_WRAPPER`
260    // env var. This uses `charon-driver` for all the crates being compiled.
261    // We may however not want to be calling charon on all crates; `CARGO_PRIMARY_PACKAGE` tells us
262    // whether the crate was specifically selected or is a dependency.
263    let is_workspace_dependency =
264        env::var("CHARON_USING_CARGO").is_ok() && env::var("CARGO_PRIMARY_PACKAGE").is_err();
265    // Let rustc emit artifacts (metadata, binaries) normally if invoked by `cargo` or when
266    // explicitly requested.
267    let emit_artifacts =
268        env::var("CHARON_USING_CARGO").is_ok() || env::var("CHARON_EMIT_ARTIFACTS").is_ok();
269    // Let rustc emit codegen artifacts, more specifically.
270    let mut codegen = emit_artifacts;
271    // Determines if we are being invoked to build a crate for the "target" architecture, in
272    // contrast to the "host" architecture. Host crates are for build scripts and proc macros and
273    // still need to be built like normal; target crates need to be processed by Charon.
274    //
275    // Currently, we detect this by checking for "--target=", which is never set for host crates.
276    // This matches what Miri does, which hopefully makes it reliable enough. This relies on us
277    // always invoking cargo itself with `--target`, which `charon` ensures.
278    let target = arg_value(&compiler_args, "--target");
279    // Whether this is the crate we want to translate.
280    let is_selected_crate = !is_workspace_dependency && target.is_some();
281
282    let mut error_ctx = ErrorCtx::new();
283
284    // Retrieve the Charon options by deserializing them from the environment variable
285    // (cargo-charon serialized the arguments and stored them in a specific environment
286    // variable before calling cargo with `RUSTC_WRAPPER=charon-driver`).
287    let mut options = match env::var(options::CHARON_ARGS)
288        .ok()
289        .map(|opts| serde_json::from_str::<options::CliOpts>(&opts).unwrap())
290    {
291        Some(options) => options,
292        None if !is_selected_crate => Default::default(),
293        None => {
294            register_error!(
295                error_ctx,
296                no_crate,
297                "environment variable `CHARON_ARGS` not set; \
298                don't call `charon-driver` directly, call `charon rustc` instead"
299            );
300            return Err(CharonFailure::CharonError(1));
301        }
302    };
303
304    if options.sysroot.as_deref() == Some("default") {
305        // Do nothing
306    } else if let Some(sysroot) = options.sysroot.as_ref()
307        && sysroot != "miri"
308    {
309        compiler_args.push(format!("--sysroot={sysroot}"));
310    } else if let Some(target) = target
311        && let Some(sysroot) = setup_miri_sysroot(target)
312    {
313        // In the default case, or `--sysroot=miri`, we ask Miri to build a full-mir syroot for us.
314        compiler_args.push(format!("--sysroot={}", sysroot.display()));
315        // The Miri sysroot doesn't support codegen.
316        codegen = false;
317    }
318
319    let output = if !is_selected_crate {
320        trace!("Skipping charon; running compiler normally instead.");
321        // Run the compiler normally.
322        run_compiler_with_callbacks(compiler_args, &mut RunCompilerNormallyCallbacks)?;
323        None
324    } else {
325        options.apply_preset();
326
327        error_ctx.continue_on_failure = !options.abort_on_error;
328        error_ctx.error_on_warnings = options.error_on_warnings;
329
330        for extra_flag in options.rustc_args.iter().cloned() {
331            compiler_args.push(extra_flag);
332        }
333
334        // Call the Rust compiler with our custom callback.
335        let mut callback = CharonCallbacks {
336            options: &options,
337            emit_artifacts,
338            codegen,
339            error_ctx: Some(error_ctx),
340            transform_ctx: None,
341        };
342        charon_lib::timing::time("rustc-driver", || {
343            run_compiler_with_callbacks(compiler_args, &mut callback)
344        })?;
345        // If `transform_ctx` is not set here, there was a fatal error.
346        let ctx = callback.transform_ctx.ok_or(CharonFailure::RustcError)?;
347        Some((ctx, options))
348    };
349    Ok(output)
350}
351
352/// The callbacks for Charon
353pub struct CharonCallbacks<'a> {
354    options: &'a CliOpts,
355    /// Whether rustc should emit the artifacts (metadata, binaries) it normally would. This is
356    /// needed under cargo so later crate invocations can consume earlier selected crates.
357    emit_artifacts: bool,
358    /// Whether to let rustc run codegen as it normally would.
359    codegen: bool,
360    /// Context for errors; `take()`n by translation.
361    error_ctx: Option<ErrorCtx>,
362    /// This is to be filled during the extraction; it contains the translated crate. `None` at the
363    /// start or if we couldn't translate anything.
364    transform_ctx: Option<TransformCtx>,
365}
366impl<'a> Callbacks for CharonCallbacks<'a> {
367    fn config(&mut self, config: &mut Config) {
368        setup_compiler(
369            config,
370            self.options,
371            true,
372            self.emit_artifacts,
373            self.codegen,
374        );
375    }
376
377    /// The MIR is modified in place: borrow-checking requires the "promoted" MIR, which causes the
378    /// "built" MIR (which results from the conversion to HIR to MIR) to become unaccessible.
379    /// Because we require built MIR at the moment, we hook ourselves before MIR-based analysis
380    /// passes.
381    fn after_expansion<'tcx>(&mut self, compiler: &Compiler, tcx: TyCtxt<'tcx>) -> Compilation {
382        // Set up our own `DefId` debug routine.
383        rustc_hir::def_id::DEF_ID_DEBUG
384            .swap(&(def_id_debug as fn(_, &mut fmt::Formatter<'_>) -> _));
385
386        if charon_lib::timing::time("rustc-precheck-errors", || precheck_rustc_errors(tcx)) {
387            return Compilation::Continue;
388        }
389
390        self.transform_ctx = charon_lib::timing::time("translate-crate", || {
391            translate_crate::translate(
392                tcx,
393                self.options,
394                self.error_ctx.take().unwrap(),
395                compiler.sess.opts.sysroot.path().to_owned(),
396            )
397        })
398        .ok();
399
400        Compilation::Continue
401    }
402    fn after_analysis<'tcx>(&mut self, _compiler: &Compiler, tcx: TyCtxt<'tcx>) -> Compilation {
403        if !self.emit_artifacts {
404            charon_lib::timing::time("rustc-late-checks", || check_late_rustc_errors(tcx));
405        }
406        Compilation::Continue
407    }
408}
409
410/// Dummy callbacks used to run the compiler normally when we shouldn't be analyzing the crate.
411pub struct RunCompilerNormallyCallbacks;
412
413impl Callbacks for RunCompilerNormallyCallbacks {
414    fn config(&mut self, config: &mut Config) {
415        setup_compiler(config, &Default::default(), false, true, true);
416    }
417}
418
419/// Custom `DefId` debug routine that doesn't print unstable values like ids and hashes.
420fn def_id_debug(def_id: rustc_hir::def_id::DefId, f: &mut fmt::Formatter<'_>) -> fmt::Result {
421    rustc_middle::ty::tls::with_opt(|opt_tcx| {
422        if let Some(tcx) = opt_tcx {
423            let crate_name = if def_id.is_local() {
424                tcx.crate_name(rustc_hir::def_id::LOCAL_CRATE)
425            } else {
426                tcx.cstore_untracked().crate_name(def_id.krate)
427            };
428            write!(
429                f,
430                "{}{}",
431                crate_name,
432                tcx.def_path(def_id).to_string_no_crate_verbose()
433            )?;
434        } else {
435            write!(f, "<can't access `tcx` to print `DefId` path>")?;
436        }
437        Ok(())
438    })
439}