Skip to content

Commit 7dd0955

Browse files
Rollup merge of #119077 - tmiasko:lint, r=cjgillot
Separate MIR lints from validation Add a MIR lint pass, enabled with -Zlint-mir, which identifies undefined or likely erroneous behaviour. The initial implementation mostly migrates existing checks of this nature from MIR validator, where they did not belong (those checks have false positives and there is nothing inherently invalid about MIR with undefined behaviour). Fixes #104736 Fixes #104843 Fixes #116079 Fixes #116736 Fixes #118990
2 parents aaff415 + ba430a3 commit 7dd0955

File tree

13 files changed

+196
-56
lines changed

13 files changed

+196
-56
lines changed

compiler/rustc_const_eval/src/transform/validate.rs

+3-47
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,6 @@ use rustc_middle::mir::interpret::Scalar;
88
use rustc_middle::mir::visit::{NonUseContext, PlaceContext, Visitor};
99
use rustc_middle::mir::*;
1010
use rustc_middle::ty::{self, InstanceDef, ParamEnv, Ty, TyCtxt, TypeVisitableExt, Variance};
11-
use rustc_mir_dataflow::impls::MaybeStorageLive;
12-
use rustc_mir_dataflow::storage::always_storage_live_locals;
13-
use rustc_mir_dataflow::{Analysis, ResultsCursor};
1411
use rustc_target::abi::{Size, FIRST_VARIANT};
1512
use rustc_target::spec::abi::Abi;
1613

@@ -51,12 +48,6 @@ impl<'tcx> MirPass<'tcx> for Validator {
5148
Reveal::All => tcx.param_env_reveal_all_normalized(def_id),
5249
};
5350

54-
let always_live_locals = always_storage_live_locals(body);
55-
let storage_liveness = MaybeStorageLive::new(std::borrow::Cow::Owned(always_live_locals))
56-
.into_engine(tcx, body)
57-
.iterate_to_fixpoint()
58-
.into_results_cursor(body);
59-
6051
let can_unwind = if mir_phase <= MirPhase::Runtime(RuntimePhase::Initial) {
6152
// In this case `AbortUnwindingCalls` haven't yet been executed.
6253
true
@@ -83,7 +74,6 @@ impl<'tcx> MirPass<'tcx> for Validator {
8374
mir_phase,
8475
unwind_edge_count: 0,
8576
reachable_blocks: traversal::reachable_as_bitset(body),
86-
storage_liveness,
8777
place_cache: FxHashSet::default(),
8878
value_cache: FxHashSet::default(),
8979
can_unwind,
@@ -116,7 +106,6 @@ struct CfgChecker<'a, 'tcx> {
116106
mir_phase: MirPhase,
117107
unwind_edge_count: usize,
118108
reachable_blocks: BitSet<BasicBlock>,
119-
storage_liveness: ResultsCursor<'a, 'tcx, MaybeStorageLive<'static>>,
120109
place_cache: FxHashSet<PlaceRef<'tcx>>,
121110
value_cache: FxHashSet<u128>,
122111
// If `false`, then the MIR must not contain `UnwindAction::Continue` or
@@ -294,28 +283,13 @@ impl<'a, 'tcx> CfgChecker<'a, 'tcx> {
294283
}
295284

296285
impl<'a, 'tcx> Visitor<'tcx> for CfgChecker<'a, 'tcx> {
297-
fn visit_local(&mut self, local: Local, context: PlaceContext, location: Location) {
286+
fn visit_local(&mut self, local: Local, _context: PlaceContext, location: Location) {
298287
if self.body.local_decls.get(local).is_none() {
299288
self.fail(
300289
location,
301290
format!("local {local:?} has no corresponding declaration in `body.local_decls`"),
302291
);
303292
}
304-
305-
if self.reachable_blocks.contains(location.block) && context.is_use() {
306-
// We check that the local is live whenever it is used. Technically, violating this
307-
// restriction is only UB and not actually indicative of not well-formed MIR. This means
308-
// that an optimization which turns MIR that already has UB into MIR that fails this
309-
// check is not necessarily wrong. However, we have no such optimizations at the moment,
310-
// and so we include this check anyway to help us catch bugs. If you happen to write an
311-
// optimization that might cause this to incorrectly fire, feel free to remove this
312-
// check.
313-
self.storage_liveness.seek_after_primary_effect(location);
314-
let locals_with_storage = self.storage_liveness.get();
315-
if !locals_with_storage.contains(local) {
316-
self.fail(location, format!("use of local {local:?}, which has no storage here"));
317-
}
318-
}
319293
}
320294

321295
fn visit_statement(&mut self, statement: &Statement<'tcx>, location: Location) {
@@ -367,26 +341,8 @@ impl<'a, 'tcx> Visitor<'tcx> for CfgChecker<'a, 'tcx> {
367341
self.fail(location, format!("explicit `{kind:?}` is forbidden"));
368342
}
369343
}
370-
StatementKind::StorageLive(local) => {
371-
// We check that the local is not live when entering a `StorageLive` for it.
372-
// Technically, violating this restriction is only UB and not actually indicative
373-
// of not well-formed MIR. This means that an optimization which turns MIR that
374-
// already has UB into MIR that fails this check is not necessarily wrong. However,
375-
// we have no such optimizations at the moment, and so we include this check anyway
376-
// to help us catch bugs. If you happen to write an optimization that might cause
377-
// this to incorrectly fire, feel free to remove this check.
378-
if self.reachable_blocks.contains(location.block) {
379-
self.storage_liveness.seek_before_primary_effect(location);
380-
let locals_with_storage = self.storage_liveness.get();
381-
if locals_with_storage.contains(*local) {
382-
self.fail(
383-
location,
384-
format!("StorageLive({local:?}) which already has storage here"),
385-
);
386-
}
387-
}
388-
}
389-
StatementKind::StorageDead(_)
344+
StatementKind::StorageLive(_)
345+
| StatementKind::StorageDead(_)
390346
| StatementKind::Intrinsic(_)
391347
| StatementKind::Coverage(_)
392348
| StatementKind::ConstEvalCounter

compiler/rustc_mir_dataflow/src/impls/storage_liveness.rs

+6-6
Original file line numberDiff line numberDiff line change
@@ -81,17 +81,17 @@ impl<'tcx, 'a> crate::GenKillAnalysis<'tcx> for MaybeStorageLive<'a> {
8181
}
8282

8383
#[derive(Clone)]
84-
pub struct MaybeStorageDead {
85-
always_live_locals: BitSet<Local>,
84+
pub struct MaybeStorageDead<'a> {
85+
always_live_locals: Cow<'a, BitSet<Local>>,
8686
}
8787

88-
impl MaybeStorageDead {
89-
pub fn new(always_live_locals: BitSet<Local>) -> Self {
88+
impl<'a> MaybeStorageDead<'a> {
89+
pub fn new(always_live_locals: Cow<'a, BitSet<Local>>) -> Self {
9090
MaybeStorageDead { always_live_locals }
9191
}
9292
}
9393

94-
impl<'tcx> crate::AnalysisDomain<'tcx> for MaybeStorageDead {
94+
impl<'tcx, 'a> crate::AnalysisDomain<'tcx> for MaybeStorageDead<'a> {
9595
type Domain = BitSet<Local>;
9696

9797
const NAME: &'static str = "maybe_storage_dead";
@@ -112,7 +112,7 @@ impl<'tcx> crate::AnalysisDomain<'tcx> for MaybeStorageDead {
112112
}
113113
}
114114

115-
impl<'tcx> crate::GenKillAnalysis<'tcx> for MaybeStorageDead {
115+
impl<'tcx, 'a> crate::GenKillAnalysis<'tcx> for MaybeStorageDead<'a> {
116116
type Idx = Local;
117117

118118
fn domain_size(&self, body: &Body<'tcx>) -> usize {

compiler/rustc_mir_transform/src/lib.rs

+1
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ pub mod inline;
8686
mod instsimplify;
8787
mod jump_threading;
8888
mod large_enums;
89+
mod lint;
8990
mod lower_intrinsics;
9091
mod lower_slice_len;
9192
mod match_branches;
+119
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
//! This pass statically detects code which has undefined behaviour or is likely to be erroneous.
2+
//! It can be used to locate problems in MIR building or optimizations. It assumes that all code
3+
//! can be executed, so it has false positives.
4+
use rustc_index::bit_set::BitSet;
5+
use rustc_middle::mir::visit::{PlaceContext, Visitor};
6+
use rustc_middle::mir::*;
7+
use rustc_middle::ty::TyCtxt;
8+
use rustc_mir_dataflow::impls::{MaybeStorageDead, MaybeStorageLive};
9+
use rustc_mir_dataflow::storage::always_storage_live_locals;
10+
use rustc_mir_dataflow::{Analysis, ResultsCursor};
11+
use std::borrow::Cow;
12+
13+
pub fn lint_body<'tcx>(tcx: TyCtxt<'tcx>, body: &Body<'tcx>, when: String) {
14+
let reachable_blocks = traversal::reachable_as_bitset(body);
15+
let always_live_locals = &always_storage_live_locals(body);
16+
17+
let maybe_storage_live = MaybeStorageLive::new(Cow::Borrowed(always_live_locals))
18+
.into_engine(tcx, body)
19+
.iterate_to_fixpoint()
20+
.into_results_cursor(body);
21+
22+
let maybe_storage_dead = MaybeStorageDead::new(Cow::Borrowed(always_live_locals))
23+
.into_engine(tcx, body)
24+
.iterate_to_fixpoint()
25+
.into_results_cursor(body);
26+
27+
Lint {
28+
tcx,
29+
when,
30+
body,
31+
is_fn_like: tcx.def_kind(body.source.def_id()).is_fn_like(),
32+
always_live_locals,
33+
reachable_blocks,
34+
maybe_storage_live,
35+
maybe_storage_dead,
36+
}
37+
.visit_body(body);
38+
}
39+
40+
struct Lint<'a, 'tcx> {
41+
tcx: TyCtxt<'tcx>,
42+
when: String,
43+
body: &'a Body<'tcx>,
44+
is_fn_like: bool,
45+
always_live_locals: &'a BitSet<Local>,
46+
reachable_blocks: BitSet<BasicBlock>,
47+
maybe_storage_live: ResultsCursor<'a, 'tcx, MaybeStorageLive<'a>>,
48+
maybe_storage_dead: ResultsCursor<'a, 'tcx, MaybeStorageDead<'a>>,
49+
}
50+
51+
impl<'a, 'tcx> Lint<'a, 'tcx> {
52+
#[track_caller]
53+
fn fail(&self, location: Location, msg: impl AsRef<str>) {
54+
let span = self.body.source_info(location).span;
55+
self.tcx.sess.dcx().span_delayed_bug(
56+
span,
57+
format!(
58+
"broken MIR in {:?} ({}) at {:?}:\n{}",
59+
self.body.source.instance,
60+
self.when,
61+
location,
62+
msg.as_ref()
63+
),
64+
);
65+
}
66+
}
67+
68+
impl<'a, 'tcx> Visitor<'tcx> for Lint<'a, 'tcx> {
69+
fn visit_local(&mut self, local: Local, context: PlaceContext, location: Location) {
70+
if self.reachable_blocks.contains(location.block) && context.is_use() {
71+
self.maybe_storage_dead.seek_after_primary_effect(location);
72+
if self.maybe_storage_dead.get().contains(local) {
73+
self.fail(location, format!("use of local {local:?}, which has no storage here"));
74+
}
75+
}
76+
}
77+
78+
fn visit_statement(&mut self, statement: &Statement<'tcx>, location: Location) {
79+
match statement.kind {
80+
StatementKind::StorageLive(local) => {
81+
if self.reachable_blocks.contains(location.block) {
82+
self.maybe_storage_live.seek_before_primary_effect(location);
83+
if self.maybe_storage_live.get().contains(local) {
84+
self.fail(
85+
location,
86+
format!("StorageLive({local:?}) which already has storage here"),
87+
);
88+
}
89+
}
90+
}
91+
_ => {}
92+
}
93+
94+
self.super_statement(statement, location);
95+
}
96+
97+
fn visit_terminator(&mut self, terminator: &Terminator<'tcx>, location: Location) {
98+
match terminator.kind {
99+
TerminatorKind::Return => {
100+
if self.is_fn_like && self.reachable_blocks.contains(location.block) {
101+
self.maybe_storage_live.seek_after_primary_effect(location);
102+
for local in self.maybe_storage_live.get().iter() {
103+
if !self.always_live_locals.contains(local) {
104+
self.fail(
105+
location,
106+
format!(
107+
"local {local:?} still has storage when returning from function"
108+
),
109+
);
110+
}
111+
}
112+
}
113+
}
114+
_ => {}
115+
}
116+
117+
self.super_terminator(terminator, location);
118+
}
119+
}

compiler/rustc_mir_transform/src/pass_manager.rs

+11-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ use rustc_middle::mir::{self, Body, MirPhase, RuntimePhase};
22
use rustc_middle::ty::TyCtxt;
33
use rustc_session::Session;
44

5-
use crate::{validate, MirPass};
5+
use crate::{lint::lint_body, validate, MirPass};
66

77
/// Just like `MirPass`, except it cannot mutate `Body`.
88
pub trait MirLint<'tcx> {
@@ -109,6 +109,7 @@ fn run_passes_inner<'tcx>(
109109
phase_change: Option<MirPhase>,
110110
validate_each: bool,
111111
) {
112+
let lint = tcx.sess.opts.unstable_opts.lint_mir & !body.should_skip();
112113
let validate = validate_each & tcx.sess.opts.unstable_opts.validate_mir & !body.should_skip();
113114
let overridden_passes = &tcx.sess.opts.unstable_opts.mir_enable_passes;
114115
trace!(?overridden_passes);
@@ -131,6 +132,9 @@ fn run_passes_inner<'tcx>(
131132
if validate {
132133
validate_body(tcx, body, format!("before pass {name}"));
133134
}
135+
if lint {
136+
lint_body(tcx, body, format!("before pass {name}"));
137+
}
134138

135139
if let Some(prof_arg) = &prof_arg {
136140
tcx.sess
@@ -147,6 +151,9 @@ fn run_passes_inner<'tcx>(
147151
if validate {
148152
validate_body(tcx, body, format!("after pass {name}"));
149153
}
154+
if lint {
155+
lint_body(tcx, body, format!("after pass {name}"));
156+
}
150157

151158
body.pass_count += 1;
152159
}
@@ -164,6 +171,9 @@ fn run_passes_inner<'tcx>(
164171
if validate || new_phase == MirPhase::Runtime(RuntimePhase::Optimized) {
165172
validate_body(tcx, body, format!("after phase change to {}", new_phase.name()));
166173
}
174+
if lint {
175+
lint_body(tcx, body, format!("after phase change to {}", new_phase.name()));
176+
}
167177

168178
body.pass_count = 1;
169179
}

compiler/rustc_mir_transform/src/ref_prop.rs

+2-1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ use rustc_middle::ty::TyCtxt;
77
use rustc_mir_dataflow::impls::MaybeStorageDead;
88
use rustc_mir_dataflow::storage::always_storage_live_locals;
99
use rustc_mir_dataflow::Analysis;
10+
use std::borrow::Cow;
1011

1112
use crate::ssa::{SsaLocals, StorageLiveLocals};
1213

@@ -120,7 +121,7 @@ fn compute_replacement<'tcx>(
120121

121122
// Compute `MaybeStorageDead` dataflow to check that we only replace when the pointee is
122123
// definitely live.
123-
let mut maybe_dead = MaybeStorageDead::new(always_live_locals)
124+
let mut maybe_dead = MaybeStorageDead::new(Cow::Owned(always_live_locals))
124125
.into_engine(tcx, body)
125126
.iterate_to_fixpoint()
126127
.into_results_cursor(body);

compiler/rustc_session/src/options.rs

+2
Original file line numberDiff line numberDiff line change
@@ -1694,6 +1694,8 @@ options! {
16941694
"link native libraries in the linker invocation (default: yes)"),
16951695
link_only: bool = (false, parse_bool, [TRACKED],
16961696
"link the `.rlink` file generated by `-Z no-link` (default: no)"),
1697+
lint_mir: bool = (false, parse_bool, [UNTRACKED],
1698+
"lint MIR before and after each transformation"),
16971699
llvm_module_flag: Vec<(String, u32, String)> = (Vec::new(), parse_llvm_module_flag, [TRACKED],
16981700
"a list of module flags to pass to LLVM (space separated)"),
16991701
llvm_plugins: Vec<String> = (Vec::new(), parse_list, [TRACKED],

src/tools/compiletest/src/runtest.rs

+1
Original file line numberDiff line numberDiff line change
@@ -2445,6 +2445,7 @@ impl<'test> TestCx<'test> {
24452445
"-Copt-level=1",
24462446
&zdump_arg,
24472447
"-Zvalidate-mir",
2448+
"-Zlint-mir",
24482449
"-Zdump-mir-exclude-pass-number",
24492450
]);
24502451
if let Some(pass) = &self.props.mir_unit_test {

tests/mir-opt/reference_prop.rs

+1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
// compile-flags: -Zlint-mir=no
12
// unit-test: ReferencePropagation
23
// needs-unwind
34

tests/ui/mir/lint/no-storage.rs

+30
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
// compile-flags: -Zlint-mir --crate-type=lib -Ztreat-err-as-bug
2+
// failure-status: 101
3+
// dont-check-compiler-stderr
4+
// regex-error-pattern: use of local .*, which has no storage here
5+
#![feature(custom_mir, core_intrinsics)]
6+
extern crate core;
7+
use core::intrinsics::mir::*;
8+
9+
#[custom_mir(dialect = "built")]
10+
pub fn f(a: bool) {
11+
mir!(
12+
let b: ();
13+
{
14+
match a { true => bb1, _ => bb2 }
15+
}
16+
bb1 = {
17+
StorageLive(b);
18+
Goto(bb3)
19+
}
20+
bb2 = {
21+
Goto(bb3)
22+
}
23+
bb3 = {
24+
b = ();
25+
RET = b;
26+
StorageDead(b);
27+
Return()
28+
}
29+
)
30+
}

tests/ui/mir/validate/storage-live.rs tests/ui/mir/lint/storage-live.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// compile-flags: -Zvalidate-mir -Ztreat-err-as-bug
1+
// compile-flags: -Zlint-mir -Ztreat-err-as-bug
22
// failure-status: 101
33
// error-pattern: broken MIR in
44
// error-pattern: StorageLive(_1) which already has storage here

0 commit comments

Comments
 (0)