Skip to content
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

Closed
wesleywiser opened this issue Sep 21, 2018 · 8 comments · Fixed by #70822
Closed

The unconditional_recursion lint can trigger in divergent functions #54444

wesleywiser opened this issue Sep 21, 2018 · 8 comments · Fixed by #70822
Labels
A-lint Area: Lints (warnings about flaws in source code) such as unused_mut. C-bug Category: This is a bug. T-compiler Relevant to the compiler team, which will review and decide on the PR/issue.

Comments

@wesleywiser
Copy link
Member

wesleywiser commented Sep 21, 2018

@eddyb noticed these cases:

pub fn foo(x: bool) { // WARN function cannot return without recurring
    if x {
        foo(x);
    } else {
        loop {}
    }
}

Try It

pub fn foo(x: bool) { // WARN function cannot return without recursing
    if x {
        foo(!x);
    } else {
        panic!("foo");
    }
}

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

@wesleywiser wesleywiser added the A-lint Area: Lints (warnings about flaws in source code) such as unused_mut. label Sep 21, 2018
@estebank
Copy link
Contributor

Isn't the first one kind of correct, if misleading? The second one is indeed problematic.

@cramertj
Copy link
Member

// WARN function cannot return without recursing

Technically correct, because it can't return at all ;)

@eddyb
Copy link
Member

eddyb commented Sep 21, 2018

@estebank It's wrong because the function can diverge without recursively calling itself.
They're both literally the same example, I just replaced one form of divergence with another.

@estebank
Copy link
Contributor

@eddyb mmmm....

In my mind I read this as

pub fn foo(x: bool) { // Ret
    if x {
        foo(x);  // Ret
    } else {
        loop {}   // `!`, can't return
    }
}

while the latter, being panic! it is also !, but can terminate (while the loop has no side-effects).

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".

@eddyb
Copy link
Member

eddyb commented Sep 26, 2018

@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.

@jonas-schievink jonas-schievink added T-compiler Relevant to the compiler team, which will review and decide on the PR/issue. C-bug Category: This is a bug. labels Jan 28, 2019
@steveklabnik
Copy link
Member

Triage: no change

@jonas-schievink
Copy link
Contributor

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.

@eddyb
Copy link
Member

eddyb commented Apr 5, 2020

@jonas-schievink You're right, there's no merge for either the panic!(...) or the loop {} examples, that could collect the information.

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 false (except those that contain a self-call) and the merge operator is &.

The funny thing about this being a 1-bit-per-block analysis is that instead of a BitMatrix you can use a simple BitSet<BasicBlock> and fixpoint is also easier to achieve. Rough sketch:

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];

@bors bors closed this as completed in a209b4f Apr 9, 2020
Centril added a commit to Centril/rust that referenced this issue Apr 10, 2020
…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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-lint Area: Lints (warnings about flaws in source code) such as unused_mut. C-bug Category: This is a bug. T-compiler Relevant to the compiler team, which will review and decide on the PR/issue.
Projects
None yet
Development

Successfully merging a pull request may close this issue.

6 participants