Skip to content

Commit 87d859a

Browse files
Don't lint for self-recursion when the function can diverge
1 parent b543afc commit 87d859a

File tree

3 files changed

+132
-92
lines changed

3 files changed

+132
-92
lines changed

src/librustc_mir_build/build/mod.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -178,11 +178,11 @@ fn mir_build(tcx: TyCtxt<'_>, def_id: DefId) -> BodyAndCache<'_> {
178178
build::construct_const(cx, body_id, return_ty, return_ty_span)
179179
};
180180

181-
lints::check(tcx, &body, def_id);
182-
183181
let mut body = BodyAndCache::new(body);
184182
body.ensure_predecessors();
185183

184+
lints::check(tcx, &body.unwrap_read_only(), def_id);
185+
186186
// The borrow checker will replace all the regions here with its own
187187
// inference variables. There's no point having non-erased regions here.
188188
// The exception is `body.user_type_annotations`, which is used unmodified

src/librustc_mir_build/lints.rs

Lines changed: 112 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
use rustc_hir::def_id::DefId;
22
use rustc_hir::intravisit::FnKind;
33
use rustc_index::bit_set::BitSet;
4+
use rustc_index::vec::IndexVec;
45
use rustc_middle::hir::map::blocks::FnLikeNode;
5-
use rustc_middle::mir::{self, Body, TerminatorKind};
6+
use rustc_middle::mir::{BasicBlock, Body, ReadOnlyBodyAndCache, TerminatorKind, START_BLOCK};
67
use rustc_middle::ty::subst::InternalSubsts;
78
use rustc_middle::ty::{self, AssocItem, AssocItemContainer, Instance, TyCtxt};
89
use rustc_session::lint::builtin::UNCONDITIONAL_RECURSION;
10+
use std::collections::VecDeque;
911

10-
crate fn check<'tcx>(tcx: TyCtxt<'tcx>, body: &Body<'tcx>, def_id: DefId) {
12+
crate fn check<'tcx>(tcx: TyCtxt<'tcx>, body: &ReadOnlyBodyAndCache<'_, 'tcx>, def_id: DefId) {
1113
let hir_id = tcx.hir().as_local_hir_id(def_id).unwrap();
1214

1315
if let Some(fn_like_node) = FnLikeNode::from_node(tcx.hir().get(hir_id)) {
@@ -18,121 +20,141 @@ crate fn check<'tcx>(tcx: TyCtxt<'tcx>, body: &Body<'tcx>, def_id: DefId) {
1820
fn check_fn_for_unconditional_recursion<'tcx>(
1921
tcx: TyCtxt<'tcx>,
2022
fn_kind: FnKind<'_>,
21-
body: &Body<'tcx>,
23+
body: &ReadOnlyBodyAndCache<'_, 'tcx>,
2224
def_id: DefId,
2325
) {
2426
if let FnKind::Closure(_) = fn_kind {
2527
// closures can't recur, so they don't matter.
2628
return;
2729
}
2830

29-
//FIXME(#54444) rewrite this lint to use the dataflow framework
30-
31-
// Walk through this function (say `f`) looking to see if
32-
// every possible path references itself, i.e., the function is
33-
// called recursively unconditionally. This is done by trying
34-
// to find a path from the entry node to the exit node that
35-
// *doesn't* call `f` by traversing from the entry while
36-
// pretending that calls of `f` are sinks (i.e., ignoring any
37-
// exit edges from them).
38-
//
39-
// NB. this has an edge case with non-returning statements,
40-
// like `loop {}` or `panic!()`: control flow never reaches
41-
// the exit node through these, so one can have a function
42-
// that never actually calls itself but is still picked up by
43-
// this lint:
44-
//
45-
// fn f(cond: bool) {
46-
// if !cond { panic!() } // could come from `assert!(cond)`
47-
// f(false)
48-
// }
49-
//
50-
// In general, functions of that form may be able to call
51-
// itself a finite number of times and then diverge. The lint
52-
// considers this to be an error for two reasons, (a) it is
53-
// easier to implement, and (b) it seems rare to actually want
54-
// to have behaviour like the above, rather than
55-
// e.g., accidentally recursing after an assert.
56-
57-
let basic_blocks = body.basic_blocks();
58-
let mut reachable_without_self_call_queue = vec![mir::START_BLOCK];
59-
let mut reached_exit_without_self_call = false;
60-
let mut self_call_locations = vec![];
61-
let mut visited = BitSet::new_empty(basic_blocks.len());
31+
let self_calls = find_blocks_calling_self(tcx, &body, def_id);
32+
let mut results = IndexVec::from_elem_n(vec![], body.basic_blocks().len());
33+
let mut queue: VecDeque<_> = self_calls.iter().collect();
6234

63-
let param_env = tcx.param_env(def_id);
64-
let trait_substs_count = match tcx.opt_associated_item(def_id) {
65-
Some(AssocItem { container: AssocItemContainer::TraitContainer(trait_def_id), .. }) => {
66-
tcx.generics_of(trait_def_id).count()
67-
}
68-
_ => 0,
69-
};
70-
let caller_substs = &InternalSubsts::identity_for_item(tcx, def_id)[..trait_substs_count];
71-
72-
while let Some(bb) = reachable_without_self_call_queue.pop() {
73-
if !visited.insert(bb) {
74-
//already done
35+
while let Some(bb) = queue.pop_front() {
36+
if !results[bb].is_empty() {
37+
// Already propagated.
7538
continue;
7639
}
7740

78-
let block = &basic_blocks[bb];
79-
80-
if let Some(ref terminator) = block.terminator {
81-
match terminator.kind {
82-
TerminatorKind::Call { ref func, .. } => {
83-
let func_ty = func.ty(body, tcx);
84-
85-
if let ty::FnDef(fn_def_id, substs) = func_ty.kind {
86-
let (call_fn_id, call_substs) = if let Some(instance) =
87-
Instance::resolve(tcx, param_env, fn_def_id, substs)
88-
{
89-
(instance.def_id(), instance.substs)
90-
} else {
91-
(fn_def_id, substs)
92-
};
93-
94-
let is_self_call = call_fn_id == def_id
95-
&& &call_substs[..caller_substs.len()] == caller_substs;
96-
97-
if is_self_call {
98-
self_call_locations.push(terminator.source_info);
99-
100-
//this is a self call so we shouldn't explore
101-
//further down this path
102-
continue;
103-
}
104-
}
41+
let locations = if self_calls.contains(bb) {
42+
// `bb` *is* a self-call.
43+
vec![bb]
44+
} else {
45+
// If *all* successors of `bb` lead to a self-call, emit notes at all of their
46+
// locations.
47+
48+
// Converging successors without unwind paths.
49+
let terminator = body[bb].terminator();
50+
let relevant_successors = match &terminator.kind {
51+
TerminatorKind::Call { destination: Some((_, dest)), .. } => {
52+
Some(dest).into_iter().chain(&[])
10553
}
106-
TerminatorKind::Abort | TerminatorKind::Return => {
107-
//found a path!
108-
reached_exit_without_self_call = true;
109-
break;
54+
TerminatorKind::Call { destination: None, .. } => None.into_iter().chain(&[]),
55+
TerminatorKind::SwitchInt { targets, .. } => None.into_iter().chain(targets),
56+
TerminatorKind::Goto { target }
57+
| TerminatorKind::Drop { target, .. }
58+
| TerminatorKind::DropAndReplace { target, .. }
59+
| TerminatorKind::Assert { target, .. } => Some(target).into_iter().chain(&[]),
60+
TerminatorKind::Yield { .. } | TerminatorKind::GeneratorDrop => {
61+
None.into_iter().chain(&[])
11062
}
111-
_ => {}
112-
}
63+
TerminatorKind::FalseEdges { real_target, .. }
64+
| TerminatorKind::FalseUnwind { real_target, .. } => {
65+
Some(real_target).into_iter().chain(&[])
66+
}
67+
TerminatorKind::Resume
68+
| TerminatorKind::Abort
69+
| TerminatorKind::Return
70+
| TerminatorKind::Unreachable => {
71+
unreachable!("unexpected terminator {:?}", terminator.kind)
72+
}
73+
};
74+
75+
let all_are_self_calls =
76+
relevant_successors.clone().all(|&succ| !results[succ].is_empty());
11377

114-
for successor in terminator.successors() {
115-
reachable_without_self_call_queue.push(*successor);
78+
if all_are_self_calls {
79+
relevant_successors.flat_map(|&succ| results[succ].iter().copied()).collect()
80+
} else {
81+
vec![]
11682
}
83+
};
84+
85+
if !locations.is_empty() {
86+
// This is a newly confirmed-to-always-reach-self-call block.
87+
results[bb] = locations;
88+
89+
// Propagate backwards through the CFG.
90+
debug!("propagate loc={:?} in {:?} -> {:?}", results[bb], bb, body.predecessors()[bb]);
91+
queue.extend(body.predecessors()[bb].iter().copied());
11792
}
11893
}
11994

120-
// Check the number of self calls because a function that
121-
// doesn't return (e.g., calls a `-> !` function or `loop { /*
122-
// no break */ }`) shouldn't be linted unless it actually
123-
// recurs.
124-
if !reached_exit_without_self_call && !self_call_locations.is_empty() {
95+
debug!("unconditional recursion results: {:?}", results);
96+
97+
let self_call_locations = &mut results[START_BLOCK];
98+
self_call_locations.sort();
99+
self_call_locations.dedup();
100+
101+
if !self_call_locations.is_empty() {
125102
let hir_id = tcx.hir().as_local_hir_id(def_id).unwrap();
126103
let sp = tcx.sess.source_map().guess_head_span(tcx.hir().span(hir_id));
127104
tcx.struct_span_lint_hir(UNCONDITIONAL_RECURSION, hir_id, sp, |lint| {
128105
let mut db = lint.build("function cannot return without recursing");
129106
db.span_label(sp, "cannot return without recursing");
130107
// offer some help to the programmer.
131-
for location in &self_call_locations {
132-
db.span_label(location.span, "recursive call site");
108+
for bb in self_call_locations {
109+
let span = body.basic_blocks()[*bb].terminator().source_info.span;
110+
db.span_label(span, "recursive call site");
133111
}
134112
db.help("a `loop` may express intention better if this is on purpose");
135113
db.emit();
136114
});
137115
}
138116
}
117+
118+
/// Finds blocks with `Call` terminators that would end up calling back into the same method.
119+
fn find_blocks_calling_self<'tcx>(
120+
tcx: TyCtxt<'tcx>,
121+
body: &Body<'tcx>,
122+
def_id: DefId,
123+
) -> BitSet<BasicBlock> {
124+
let param_env = tcx.param_env(def_id);
125+
let trait_substs_count = match tcx.opt_associated_item(def_id) {
126+
Some(AssocItem { container: AssocItemContainer::TraitContainer(trait_def_id), .. }) => {
127+
tcx.generics_of(trait_def_id).count()
128+
}
129+
_ => 0,
130+
};
131+
let caller_substs = &InternalSubsts::identity_for_item(tcx, def_id)[..trait_substs_count];
132+
133+
let mut self_calls = BitSet::new_empty(body.basic_blocks().len());
134+
135+
for (bb, data) in body.basic_blocks().iter_enumerated() {
136+
if let TerminatorKind::Call { func, .. } = &data.terminator().kind {
137+
let func_ty = func.ty(body, tcx);
138+
139+
if let ty::FnDef(fn_def_id, substs) = func_ty.kind {
140+
let (call_fn_id, call_substs) =
141+
if let Some(instance) = Instance::resolve(tcx, param_env, fn_def_id, substs) {
142+
(instance.def_id(), instance.substs)
143+
} else {
144+
(fn_def_id, substs)
145+
};
146+
147+
// FIXME(#57965): Make this work across function boundaries
148+
149+
let is_self_call =
150+
call_fn_id == def_id && &call_substs[..caller_substs.len()] == caller_substs;
151+
152+
if is_self_call {
153+
self_calls.insert(bb);
154+
}
155+
}
156+
}
157+
}
158+
159+
self_calls
160+
}

src/test/ui/lint/lint-unconditional-recursion.rs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,4 +131,22 @@ trait Bar {
131131
}
132132
}
133133

134+
// Do not trigger on functions that may diverge instead of self-recursing (#54444)
135+
136+
pub fn loops(x: bool) {
137+
if x {
138+
loops(x);
139+
} else {
140+
loop {}
141+
}
142+
}
143+
144+
pub fn panics(x: bool) {
145+
if x {
146+
panics(!x);
147+
} else {
148+
panic!("panics");
149+
}
150+
}
151+
134152
fn main() {}

0 commit comments

Comments
 (0)