Skip to content

Commit a209b4f

Browse files
authored
Rollup merge of #70822 - jonas-schievink:curse-of-the-recursion, r=ecstatic-morse
Don't lint for self-recursion when the function can diverge Fixes #54444
2 parents ba50bc5 + b8f416d commit a209b4f

File tree

3 files changed

+152
-97
lines changed

3 files changed

+152
-97
lines changed

src/librustc_mir_build/build/mod.rs

+2-2
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

+132-95
Original file line numberDiff line numberDiff line change
@@ -1,138 +1,175 @@
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)) {
14-
check_fn_for_unconditional_recursion(tcx, fn_like_node.kind(), body, def_id);
16+
if let FnKind::Closure(_) = fn_like_node.kind() {
17+
// closures can't recur, so they don't matter.
18+
return;
19+
}
20+
21+
check_fn_for_unconditional_recursion(tcx, body, def_id);
1522
}
1623
}
1724

1825
fn check_fn_for_unconditional_recursion<'tcx>(
1926
tcx: TyCtxt<'tcx>,
20-
fn_kind: FnKind<'_>,
21-
body: &Body<'tcx>,
27+
body: &ReadOnlyBodyAndCache<'_, 'tcx>,
2228
def_id: DefId,
2329
) {
24-
if let FnKind::Closure(_) = fn_kind {
25-
// closures can't recur, so they don't matter.
26-
return;
27-
}
30+
let self_calls = find_blocks_calling_self(tcx, &body, def_id);
2831

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());
32+
// Stores a list of `Span`s for every basic block. Those are the spans of self-calls where we
33+
// know that one of them will definitely be reached. If the list is empty, the block either
34+
// wasn't processed yet or will not always go to a self-call.
35+
let mut results = IndexVec::from_elem_n(vec![], body.basic_blocks().len());
6236

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];
37+
// We start the analysis at the self calls and work backwards.
38+
let mut queue: VecDeque<_> = self_calls.iter().collect();
7139

72-
while let Some(bb) = reachable_without_self_call_queue.pop() {
73-
if !visited.insert(bb) {
74-
//already done
40+
while let Some(bb) = queue.pop_front() {
41+
if !results[bb].is_empty() {
42+
// Already propagated.
7543
continue;
7644
}
7745

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-
}
46+
let locations = if self_calls.contains(bb) {
47+
// `bb` *is* a self-call.
48+
// We don't look at successors here because they are irrelevant here and we don't want
49+
// to lint them (eg. `f(); f()` should only lint the first call).
50+
vec![bb]
51+
} else {
52+
// If *all* successors of `bb` lead to a self-call, emit notes at all of their
53+
// locations.
54+
55+
// Determine all "relevant" successors. We ignore successors only reached via unwinding.
56+
let terminator = body[bb].terminator();
57+
let relevant_successors = match &terminator.kind {
58+
TerminatorKind::Call { destination: None, .. }
59+
| TerminatorKind::Yield { .. }
60+
| TerminatorKind::GeneratorDrop => None.into_iter().chain(&[]),
61+
TerminatorKind::SwitchInt { targets, .. } => None.into_iter().chain(targets),
62+
TerminatorKind::Goto { target }
63+
| TerminatorKind::Drop { target, .. }
64+
| TerminatorKind::DropAndReplace { target, .. }
65+
| TerminatorKind::Assert { target, .. }
66+
| TerminatorKind::FalseEdges { real_target: target, .. }
67+
| TerminatorKind::FalseUnwind { real_target: target, .. }
68+
| TerminatorKind::Call { destination: Some((_, target)), .. } => {
69+
Some(target).into_iter().chain(&[])
10570
}
106-
TerminatorKind::Abort | TerminatorKind::Return => {
107-
//found a path!
108-
reached_exit_without_self_call = true;
109-
break;
71+
TerminatorKind::Resume
72+
| TerminatorKind::Abort
73+
| TerminatorKind::Return
74+
| TerminatorKind::Unreachable => {
75+
// We propagate backwards, so these should never be encountered here.
76+
unreachable!("unexpected terminator {:?}", terminator.kind)
11077
}
111-
_ => {}
78+
};
79+
80+
// If all our successors are known to lead to self-calls, then we do too.
81+
let all_are_self_calls =
82+
relevant_successors.clone().all(|&succ| !results[succ].is_empty());
83+
84+
if all_are_self_calls {
85+
// We'll definitely lead to a self-call. Merge all call locations of the successors
86+
// for linting them later.
87+
relevant_successors.flat_map(|&succ| results[succ].iter().copied()).collect()
88+
} else {
89+
// At least 1 successor does not always lead to a self-call, so we also don't.
90+
vec![]
11291
}
92+
};
11393

114-
for successor in terminator.successors() {
115-
reachable_without_self_call_queue.push(*successor);
116-
}
94+
if !locations.is_empty() {
95+
// This is a newly confirmed-to-always-reach-self-call block.
96+
results[bb] = locations;
97+
98+
// Propagate backwards through the CFG.
99+
debug!("propagate loc={:?} in {:?} -> {:?}", results[bb], bb, body.predecessors()[bb]);
100+
queue.extend(body.predecessors()[bb].iter().copied());
117101
}
118102
}
119103

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() {
104+
debug!("unconditional recursion results: {:?}", results);
105+
106+
let self_call_locations = &mut results[START_BLOCK];
107+
self_call_locations.sort();
108+
self_call_locations.dedup();
109+
110+
if !self_call_locations.is_empty() {
125111
let hir_id = tcx.hir().as_local_hir_id(def_id).unwrap();
126112
let sp = tcx.sess.source_map().guess_head_span(tcx.hir().span(hir_id));
127113
tcx.struct_span_lint_hir(UNCONDITIONAL_RECURSION, hir_id, sp, |lint| {
128114
let mut db = lint.build("function cannot return without recursing");
129115
db.span_label(sp, "cannot return without recursing");
130116
// offer some help to the programmer.
131-
for location in &self_call_locations {
132-
db.span_label(location.span, "recursive call site");
117+
for bb in self_call_locations {
118+
let span = body.basic_blocks()[*bb].terminator().source_info.span;
119+
db.span_label(span, "recursive call site");
133120
}
134121
db.help("a `loop` may express intention better if this is on purpose");
135122
db.emit();
136123
});
137124
}
138125
}
126+
127+
/// Finds blocks with `Call` terminators that would end up calling back into the same method.
128+
fn find_blocks_calling_self<'tcx>(
129+
tcx: TyCtxt<'tcx>,
130+
body: &Body<'tcx>,
131+
def_id: DefId,
132+
) -> BitSet<BasicBlock> {
133+
let param_env = tcx.param_env(def_id);
134+
135+
// If this is trait/impl method, extract the trait's substs.
136+
let trait_substs_count = match tcx.opt_associated_item(def_id) {
137+
Some(AssocItem { container: AssocItemContainer::TraitContainer(trait_def_id), .. }) => {
138+
tcx.generics_of(trait_def_id).count()
139+
}
140+
_ => 0,
141+
};
142+
let trait_substs = &InternalSubsts::identity_for_item(tcx, def_id)[..trait_substs_count];
143+
144+
let mut self_calls = BitSet::new_empty(body.basic_blocks().len());
145+
146+
for (bb, data) in body.basic_blocks().iter_enumerated() {
147+
if let TerminatorKind::Call { func, .. } = &data.terminator().kind {
148+
let func_ty = func.ty(body, tcx);
149+
150+
if let ty::FnDef(fn_def_id, substs) = func_ty.kind {
151+
let (call_fn_id, call_substs) =
152+
if let Some(instance) = Instance::resolve(tcx, param_env, fn_def_id, substs) {
153+
(instance.def_id(), instance.substs)
154+
} else {
155+
(fn_def_id, substs)
156+
};
157+
158+
// FIXME(#57965): Make this work across function boundaries
159+
160+
// If this is a trait fn, the substs on the trait have to match, or we might be
161+
// calling into an entirely different method (for example, a call from the default
162+
// method in the trait to `<A as Trait<B>>::method`, where `A` and/or `B` are
163+
// specific types).
164+
let is_self_call =
165+
call_fn_id == def_id && &call_substs[..trait_substs.len()] == trait_substs;
166+
167+
if is_self_call {
168+
self_calls.insert(bb);
169+
}
170+
}
171+
}
172+
}
173+
174+
self_calls
175+
}

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

+18
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)