-
Notifications
You must be signed in to change notification settings - Fork 12.9k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
The unconditional_recursion
lint can trigger in divergent functions
#54444
Comments
Isn't the first one kind of correct, if misleading? The second one is indeed problematic. |
Technically correct, because it can't return at all ;) |
@estebank It's wrong because the function can diverge without recursively calling itself. |
@eddyb mmmm.... In my mind I read this as
while the latter, being You're correct, but I'd love it if we could somehow differentiate when evaluating divergence between "can't return" and "can't return but can panic". |
@estebank Both "stuck in an infinite loop" and "panics" are different from "infinitely self-recurses". What we want to detect is unguarded self-recursion, that is, when you forgot to check some condition that would give you a convergent or divergent "base case", that would stop the infinite recursion. The message of the lint doesn't help my point, it should be more clearly about "infinite recursion". While usually you're missing a condition, or you're calling the wrong function, it's also possible that you're using infinite recursion to infinitely repeat something - but we don't have guaranteed tail calls, so what you really want to do is rewrite the infinite recursion into an infinite loop. |
Triage: no change |
Since we currently lint this: fn inf() {
inf();
loop {}
} Wouldn't this need a backwards dataflow analysis to catch this case? It doesn't looks like the bit would ever be propagated to a return block like this. A backwards analysis would instead propagate it to the entry block, if I understand correctly. |
@jonas-schievink You're right, there's no merge for either the Presumably we want to tell these apart: fn inf() {
match foo() {
0 => inf(),
1 => inf(),
_ => inf(),
}
} vs fn inf() {
match foo() {
0 => inf(),
1 => loop {},
_ => inf(),
}
} So I guess the property you'd propagate backwards is "always reaches self-call", which means every block starts at The funny thing about this being a 1-bit-per-block analysis is that instead of a let mut results = BitSet::empty(body.basic_blocks());
let mut queue: VecDeque<_> = self_calls.iter().collect();
while let Some(bb) = queue.pop_front() {
if results.contains(bb) {
// Already propagated.
continue;
}
let always_reaches_self_call = match body[bb].terminator.kind {
TerminatorKind::Call(..) if self_calls.contains(bb) => true,
TerminatorKind::SwitchInt(branches)
=> branches.iter().all(|&target| results.contains(target)),
TerminatorKind::Goto { target }
// ... and all other single target variants, ignoring any cleanup edges
=> result.contains(target),
// We only ever go backwards through the CFG, these should be impossible to hit.
TerminatorKind::Return | TerminatorKind::Resume => unreachable!(),
};
if always_reaches_self_call {
// This is a newly confirmed-to-always-reach-self-call block.
assert!(results.insert(bb));
// Propagate backwards through the CFG.
queue.extend(body.predecessors(bb));
}
}
let fn_always_reaches_self_call = results[START_BLOCK]; |
…nas-schievink Use forward traversal for unconditional recursion lint While reviewing rust-lang#70822, I noted that rust-lang#54444 could be solved without requiring the predecessor graph and without allocating a `Vec<Span>` for every basic block. The unconditional recursion lint is not a performance bottleneck however, so I approved rust-lang#70822 as it was. Nevertheless, I wanted to try implementing my idea using `TriColorDepthFirstSearch`, which is a DFS that can differentiate between [forward/tree edges and backward ones](https://en.wikipedia.org/wiki/Depth-first_search#Output_of_a_depth-first_search). I found this approach more straightforward than the existing one, so I'm opening this PR to see if it is desirable. The pass is now just a DFS across the control-flow graph. We ignore false edges and false unwinds, as well as the successors of recursive calls, just like existing pass does. If we see a back-edge (loop) or a terminator that would cause us to yield control-flow back to the caller (`Return`, `Resume`, etc.), we know that the function does not unconditionally recurse. r? @jonas-schievink
@eddyb noticed these cases:
Try It
Try It
@eddyb thinks the way to resolve this is to reimplement the lint on top of the
dataflow
analysis stuff.https://discordapp.com/channels/442252698964721669/443151243398086667/492702651759329280
The text was updated successfully, but these errors were encountered: