refactor and unify top-level error handling a bit

This commit is contained in:
Ralf Jung 2025-11-14 22:42:14 +01:00
parent ada5df7bdd
commit 4ed54a8389
9 changed files with 142 additions and 130 deletions

View file

@ -23,18 +23,18 @@ extern crate rustc_span;
mod log;
use std::env;
use std::num::NonZero;
use std::num::{NonZero, NonZeroI32};
use std::ops::Range;
use std::rc::Rc;
use std::str::FromStr;
use std::sync::atomic::{AtomicI32, AtomicU32, Ordering};
use std::sync::atomic::{AtomicU32, Ordering};
use miri::{
BacktraceStyle, BorrowTrackerMethod, GenmcConfig, GenmcCtx, MiriConfig, MiriEntryFnType,
ProvenanceMode, RetagFields, TreeBorrowsParams, ValidationMode, run_genmc_mode,
};
use rustc_abi::ExternAbi;
use rustc_data_structures::sync;
use rustc_data_structures::sync::{self, DynSync};
use rustc_driver::Compilation;
use rustc_hir::def_id::LOCAL_CRATE;
use rustc_hir::{self as hir, Node};
@ -120,15 +120,47 @@ fn entry_fn(tcx: TyCtxt<'_>) -> (DefId, MiriEntryFnType) {
}
}
fn run_many_seeds(
many_seeds: ManySeedsConfig,
eval_entry_once: impl Fn(u64) -> Result<(), NonZeroI32> + DynSync,
) -> Result<(), NonZeroI32> {
let exit_code =
sync::IntoDynSyncSend(AtomicU32::new(rustc_driver::EXIT_SUCCESS.cast_unsigned()));
let num_failed = sync::IntoDynSyncSend(AtomicU32::new(0));
sync::par_for_each_in(many_seeds.seeds.clone(), |&seed| {
if let Err(return_code) = eval_entry_once(seed.into()) {
eprintln!("FAILING SEED: {seed}");
if !many_seeds.keep_going {
// `abort_if_errors` would unwind but would not actually stop miri, since
// `par_for_each` waits for the rest of the threads to finish.
exit(return_code.get());
}
// Preserve the "maximum" return code (when interpreted as `u32`), to make
// the result order-independent and to make it 0 only if all executions were 0.
exit_code.fetch_max(return_code.get().cast_unsigned(), Ordering::Relaxed);
num_failed.fetch_add(1, Ordering::Relaxed);
}
});
let num_failed = num_failed.0.into_inner();
let exit_code = exit_code.0.into_inner().cast_signed();
if num_failed > 0 {
eprintln!("{num_failed}/{total} SEEDS FAILED", total = many_seeds.seeds.count());
Err(NonZeroI32::new(exit_code).unwrap())
} else {
assert!(exit_code == 0);
Ok(())
}
}
impl rustc_driver::Callbacks for MiriCompilerCalls {
fn after_analysis<'tcx>(
&mut self,
_: &rustc_interface::interface::Compiler,
tcx: TyCtxt<'tcx>,
) -> Compilation {
if tcx.sess.dcx().has_errors_or_delayed_bugs().is_some() {
tcx.dcx().fatal("miri cannot be run on programs that fail compilation");
}
tcx.dcx().abort_if_errors();
tcx.dcx().flush_delayed();
if !tcx.crate_types().contains(&CrateType::Executable) {
tcx.dcx().fatal("miri only makes sense on bin crates");
}
@ -161,64 +193,28 @@ impl rustc_driver::Callbacks for MiriCompilerCalls {
optimizations is usually marginal at best.");
}
// Run in GenMC mode if enabled.
if config.genmc_config.is_some() {
// Validate GenMC settings.
if let Err(err) = GenmcConfig::validate(&mut config, tcx) {
fatal_error!("Invalid settings: {err}");
}
// This is the entry point used in GenMC mode.
// This closure will be called multiple times to explore the concurrent execution space of the program.
let eval_entry_once = |genmc_ctx: Rc<GenmcCtx>| {
let res = if config.genmc_config.is_some() {
assert!(self.many_seeds.is_none());
run_genmc_mode(tcx, &config, |genmc_ctx: Rc<GenmcCtx>| {
miri::eval_entry(tcx, entry_def_id, entry_type, &config, Some(genmc_ctx))
};
let return_code = run_genmc_mode(&config, eval_entry_once, tcx).unwrap_or_else(|| {
tcx.dcx().abort_if_errors();
rustc_driver::EXIT_FAILURE
});
exit(return_code);
})
} else if let Some(many_seeds) = self.many_seeds.take() {
assert!(config.seed.is_none());
run_many_seeds(many_seeds, |seed| {
let mut config = config.clone();
config.seed = Some(seed);
eprintln!("Trying seed: {seed}");
miri::eval_entry(tcx, entry_def_id, entry_type, &config, /* genmc_ctx */ None)
})
} else {
miri::eval_entry(tcx, entry_def_id, entry_type, &config, None)
};
if let Some(many_seeds) = self.many_seeds.take() {
assert!(config.seed.is_none());
let exit_code = sync::IntoDynSyncSend(AtomicI32::new(rustc_driver::EXIT_SUCCESS));
let num_failed = sync::IntoDynSyncSend(AtomicU32::new(0));
sync::par_for_each_in(many_seeds.seeds.clone(), |seed| {
let mut config = config.clone();
config.seed = Some((*seed).into());
eprintln!("Trying seed: {seed}");
let return_code = miri::eval_entry(
tcx,
entry_def_id,
entry_type,
&config,
/* genmc_ctx */ None,
)
.unwrap_or(rustc_driver::EXIT_FAILURE);
if return_code != rustc_driver::EXIT_SUCCESS {
eprintln!("FAILING SEED: {seed}");
if !many_seeds.keep_going {
// `abort_if_errors` would actually not stop, since `par_for_each` waits for the
// rest of the to finish, so we just exit immediately.
exit(return_code);
}
exit_code.store(return_code, Ordering::Relaxed);
num_failed.fetch_add(1, Ordering::Relaxed);
}
});
let num_failed = num_failed.0.into_inner();
if num_failed > 0 {
eprintln!("{num_failed}/{total} SEEDS FAILED", total = many_seeds.seeds.count());
}
exit(exit_code.0.into_inner());
if let Err(return_code) = res {
tcx.dcx().abort_if_errors();
exit(return_code.get());
} else {
let return_code = miri::eval_entry(tcx, entry_def_id, entry_type, &config, None)
.unwrap_or_else(|| {
tcx.dcx().abort_if_errors();
rustc_driver::EXIT_FAILURE
});
exit(return_code);
exit(rustc_driver::EXIT_SUCCESS);
}
// Unreachable.
@ -747,6 +743,13 @@ fn main() {
);
};
// Validate GenMC settings.
if miri_config.genmc_config.is_some()
&& let Err(err) = GenmcConfig::validate(&mut miri_config)
{
fatal_error!("Invalid settings: {err}");
}
debug!("rustc arguments: {:?}", rustc_args);
debug!("crate arguments: {:?}", miri_config.args);
if !miri_config.native_lib.is_empty() && miri_config.native_lib_enable_tracing {

View file

@ -1,6 +1,4 @@
use genmc_sys::LogLevel;
use rustc_abi::Endian;
use rustc_middle::ty::TyCtxt;
use super::GenmcParams;
use crate::{IsolatedOp, MiriConfig, RejectOpWith};
@ -86,16 +84,11 @@ impl GenmcConfig {
///
/// Unsupported configurations return an error.
/// Adjusts Miri settings where required, printing a warnings if the change might be unexpected for the user.
pub fn validate(miri_config: &mut MiriConfig, tcx: TyCtxt<'_>) -> Result<(), &'static str> {
pub fn validate(miri_config: &mut MiriConfig) -> Result<(), &'static str> {
let Some(genmc_config) = miri_config.genmc_config.as_mut() else {
return Ok(());
};
// Check for supported target.
if tcx.data_layout.endian != Endian::Little || tcx.data_layout.pointer_size().bits() != 64 {
return Err("GenMC only supports 64bit little-endian targets");
}
// Check for disallowed configurations.
if !miri_config.data_race_detector {
return Err("Cannot disable data race detection in GenMC mode");

View file

@ -1,6 +1,5 @@
use rustc_abi::{Align, Size};
use rustc_const_eval::interpret::{AllocId, InterpCx, InterpResult};
use rustc_middle::ty::TyCtxt;
pub use self::intercept::EvalContextExt as GenmcEvalContextExt;
pub use self::run::run_genmc_mode;
@ -23,6 +22,7 @@ pub struct GenmcCtx {}
pub struct GenmcConfig {}
mod run {
use std::num::NonZeroI32;
use std::rc::Rc;
use rustc_middle::ty::TyCtxt;
@ -30,10 +30,10 @@ mod run {
use crate::{GenmcCtx, MiriConfig};
pub fn run_genmc_mode<'tcx>(
_config: &MiriConfig,
_eval_entry: impl Fn(Rc<GenmcCtx>) -> Option<i32>,
_tcx: TyCtxt<'tcx>,
) -> Option<i32> {
_config: &MiriConfig,
_eval_entry: impl Fn(Rc<GenmcCtx>) -> Result<(), NonZeroI32>,
) -> Result<(), NonZeroI32> {
unreachable!();
}
}
@ -240,10 +240,7 @@ impl GenmcConfig {
}
}
pub fn validate(
_miri_config: &mut crate::MiriConfig,
_tcx: TyCtxt<'_>,
) -> Result<(), &'static str> {
pub fn validate(_miri_config: &mut crate::MiriConfig) -> Result<(), &'static str> {
Ok(())
}
}

View file

@ -1,8 +1,10 @@
use std::num::NonZeroI32;
use std::rc::Rc;
use std::sync::Arc;
use std::time::Instant;
use genmc_sys::EstimationResult;
use rustc_abi::Endian;
use rustc_log::tracing;
use rustc_middle::ty::TyCtxt;
@ -24,10 +26,15 @@ pub(super) enum GenmcMode {
///
/// Returns `None` is an error is detected, or `Some(return_value)` with the return value of the last run of the program.
pub fn run_genmc_mode<'tcx>(
config: &MiriConfig,
eval_entry: impl Fn(Rc<GenmcCtx>) -> Option<i32>,
tcx: TyCtxt<'tcx>,
) -> Option<i32> {
config: &MiriConfig,
eval_entry: impl Fn(Rc<GenmcCtx>) -> Result<(), NonZeroI32>,
) -> Result<(), NonZeroI32> {
// Check for supported target.
if tcx.data_layout.endian != Endian::Little || tcx.data_layout.pointer_size().bits() != 64 {
tcx.dcx().fatal("GenMC only supports 64bit little-endian targets");
}
let genmc_config = config.genmc_config.as_ref().unwrap();
// Run in Estimation mode if requested.
if genmc_config.do_estimation {
@ -41,10 +48,10 @@ pub fn run_genmc_mode<'tcx>(
fn run_genmc_mode_impl<'tcx>(
config: &MiriConfig,
eval_entry: &impl Fn(Rc<GenmcCtx>) -> Option<i32>,
eval_entry: &impl Fn(Rc<GenmcCtx>) -> Result<(), NonZeroI32>,
tcx: TyCtxt<'tcx>,
mode: GenmcMode,
) -> Option<i32> {
) -> Result<(), NonZeroI32> {
let time_start = Instant::now();
let genmc_config = config.genmc_config.as_ref().unwrap();
@ -62,9 +69,9 @@ fn run_genmc_mode_impl<'tcx>(
genmc_ctx.prepare_next_execution();
// Execute the program until completion to get the return value, or return if an error happens:
let Some(return_code) = eval_entry(genmc_ctx.clone()) else {
if let Err(err) = eval_entry(genmc_ctx.clone()) {
genmc_ctx.print_genmc_output(genmc_config, tcx);
return None;
return Err(err);
};
// We inform GenMC that the execution is complete.
@ -80,18 +87,17 @@ fn run_genmc_mode_impl<'tcx>(
genmc_ctx.print_verification_output(genmc_config, elapsed_time_sec);
}
// Return the return code of the last execution.
return Some(return_code);
return Ok(());
}
ExecutionEndResult::Error(error) => {
// This can be reached for errors that affect the entire execution, not just a specific event.
// For instance, linearizability checking and liveness checking report their errors this way.
// Neither are supported by Miri-GenMC at the moment though. However, GenMC also
// treats races on deallocation as global errors, so this code path is still reachable.
// Neither are supported by Miri-GenMC at the moment though.
// Since we don't have any span information for the error at this point,
// we just print GenMC's error string, and the full GenMC output if requested.
eprintln!("(GenMC) Error detected: {error}");
genmc_ctx.print_genmc_output(genmc_config, tcx);
return None;
return Err(NonZeroI32::new(rustc_driver::EXIT_FAILURE).unwrap());
}
}
}

View file

@ -226,19 +226,20 @@ pub fn prune_stacktrace<'tcx>(
}
}
/// Emit a custom diagnostic without going through the miri-engine machinery.
/// Report the result of a Miri execution.
///
/// Returns `Some` if this was regular program termination with a given exit code and a `bool` indicating whether a leak check should happen; `None` otherwise.
pub fn report_error<'tcx>(
/// Returns `Some` if this was regular program termination with a given exit code and a `bool`
/// indicating whether a leak check should happen; `None` otherwise.
pub fn report_result<'tcx>(
ecx: &InterpCx<'tcx, MiriMachine<'tcx>>,
e: InterpErrorInfo<'tcx>,
res: InterpErrorInfo<'tcx>,
) -> Option<(i32, bool)> {
use InterpErrorKind::*;
use UndefinedBehaviorInfo::*;
let mut labels = vec![];
let (title, helps) = if let MachineStop(info) = e.kind() {
let (title, helps) = if let MachineStop(info) = res.kind() {
let info = info.downcast_ref::<TerminationInfo>().expect("invalid MachineStop payload");
use TerminationInfo::*;
let title = match info {
@ -334,7 +335,7 @@ pub fn report_error<'tcx>(
};
(title, helps)
} else {
let title = match e.kind() {
let title = match res.kind() {
UndefinedBehavior(ValidationError(validation_err))
if matches!(
validation_err.kind,
@ -344,7 +345,7 @@ pub fn report_error<'tcx>(
ecx.handle_ice(); // print interpreter backtrace (this is outside the eval `catch_unwind`)
bug!(
"This validation error should be impossible in Miri: {}",
format_interp_error(ecx.tcx.dcx(), e)
format_interp_error(ecx.tcx.dcx(), res)
);
}
UndefinedBehavior(_) => "Undefined Behavior",
@ -363,12 +364,12 @@ pub fn report_error<'tcx>(
ecx.handle_ice(); // print interpreter backtrace (this is outside the eval `catch_unwind`)
bug!(
"This error should be impossible in Miri: {}",
format_interp_error(ecx.tcx.dcx(), e)
format_interp_error(ecx.tcx.dcx(), res)
);
}
};
#[rustfmt::skip]
let helps = match e.kind() {
let helps = match res.kind() {
Unsupported(_) =>
vec![
note!("this is likely not a bug in the program; it indicates that the program performed an operation that Miri does not support"),
@ -422,7 +423,7 @@ pub fn report_error<'tcx>(
// We want to dump the allocation if this is `InvalidUninitBytes`.
// Since `format_interp_error` consumes `e`, we compute the outut early.
let mut extra = String::new();
match e.kind() {
match res.kind() {
UndefinedBehavior(InvalidUninitBytes(Some((alloc_id, access)))) => {
writeln!(
extra,
@ -448,7 +449,7 @@ pub fn report_error<'tcx>(
if let Some(title) = title {
write!(primary_msg, "{title}: ").unwrap();
}
write!(primary_msg, "{}", format_interp_error(ecx.tcx.dcx(), e)).unwrap();
write!(primary_msg, "{}", format_interp_error(ecx.tcx.dcx(), res)).unwrap();
if labels.is_empty() {
labels.push(format!("{} occurred here", title.unwrap_or("error")));

View file

@ -1,6 +1,7 @@
//! Main evaluator loop and setting up the initial stack frame.
use std::ffi::{OsStr, OsString};
use std::num::NonZeroI32;
use std::panic::{self, AssertUnwindSafe};
use std::path::PathBuf;
use std::rc::Rc;
@ -462,7 +463,7 @@ pub fn eval_entry<'tcx>(
entry_type: MiriEntryFnType,
config: &MiriConfig,
genmc_ctx: Option<Rc<GenmcCtx>>,
) -> Option<i32> {
) -> Result<(), NonZeroI32> {
// Copy setting before we move `config`.
let ignore_leaks = config.ignore_leaks;
@ -482,35 +483,50 @@ pub fn eval_entry<'tcx>(
ecx.handle_ice();
panic::resume_unwind(panic_payload)
});
// `Ok` can never happen; the interpreter loop always exits with an "error"
// (but that "error" might be just "regular program termination").
let Err(err) = res.report_err();
// Obtain the result of the execution. This is always an `Err`, but that doesn't necessarily
// indicate an error.
let Err(res) = res.report_err();
// Show diagnostic, if any.
let (return_code, leak_check) = report_error(&ecx, err)?;
// Error reporting: if we survive all checks, we return the exit code the program gave us.
'miri_error: {
// Show diagnostic, if any.
let Some((return_code, leak_check)) = report_result(&ecx, res) else {
break 'miri_error;
};
// If we get here there was no fatal error.
// Possibly check for memory leaks.
if leak_check && !ignore_leaks {
// Check for thread leaks.
if !ecx.have_all_terminated() {
tcx.dcx().err("the main thread terminated without waiting for all remaining threads");
tcx.dcx().note("set `MIRIFLAGS=-Zmiri-ignore-leaks` to disable this check");
return None;
}
// Check for memory leaks.
info!("Additional static roots: {:?}", ecx.machine.static_roots);
let leaks = ecx.take_leaked_allocations(|ecx| &ecx.machine.static_roots);
if !leaks.is_empty() {
report_leaks(&ecx, leaks);
tcx.dcx().note("set `MIRIFLAGS=-Zmiri-ignore-leaks` to disable this check");
// Ignore the provided return code - let the reported error
// determine the return code.
return None;
// If we get here there was no fatal error -- yet.
// Possibly check for memory leaks.
if leak_check && !ignore_leaks {
// Check for thread leaks.
if !ecx.have_all_terminated() {
tcx.dcx()
.err("the main thread terminated without waiting for all remaining threads");
tcx.dcx().note("set `MIRIFLAGS=-Zmiri-ignore-leaks` to disable this check");
break 'miri_error;
}
// Check for memory leaks.
info!("Additional static roots: {:?}", ecx.machine.static_roots);
let leaks = ecx.take_leaked_allocations(|ecx| &ecx.machine.static_roots);
if !leaks.is_empty() {
report_leaks(&ecx, leaks);
tcx.dcx().note("set `MIRIFLAGS=-Zmiri-ignore-leaks` to disable this check");
// Ignore the provided return code - let the reported error
// determine the return code.
break 'miri_error;
}
}
// The interpreter has not reported an error.
// (There could still be errors in the session if there are other interpreters.)
return match NonZeroI32::new(return_code) {
None => Ok(()),
Some(return_code) => Err(return_code),
};
}
Some(return_code)
// The interpreter reported an error.
assert!(tcx.dcx().has_errors().is_some());
Err(NonZeroI32::new(rustc_driver::EXIT_FAILURE).unwrap())
}
/// Turns an array of arguments into a Windows command line string.

View file

@ -136,7 +136,7 @@ pub use crate::concurrency::{GenmcConfig, GenmcCtx, run_genmc_mode};
pub use crate::data_structures::dedup_range_map::DedupRangeMap;
pub use crate::data_structures::mono_hash_map::MonoHashMap;
pub use crate::diagnostics::{
EvalContextExt as _, NonHaltingDiagnostic, TerminationInfo, report_error,
EvalContextExt as _, NonHaltingDiagnostic, TerminationInfo, report_result,
};
pub use crate::eval::{MiriConfig, MiriEntryFnType, create_ecx, eval_entry};
pub use crate::helpers::{EvalContextExt as _, ToU64 as _, ToUsize as _};

View file

@ -1,5 +1,3 @@
//@error-in-other-file: miri cannot be run on programs that fail compilation
#![deny(warnings, unused)]
struct Foo;

View file

@ -11,7 +11,5 @@ LL | #![deny(warnings, unused)]
| ^^^^^^
= note: `#[deny(dead_code)]` implied by `#[deny(unused)]`
error: miri cannot be run on programs that fail compilation
error: aborting due to 2 previous errors
error: aborting due to 1 previous error