-
Notifications
You must be signed in to change notification settings - Fork 10.5k
Diagnose Infinite Recursion #11869
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
Diagnose Infinite Recursion #11869
Conversation
@swift-ci please smoke test |
ClassType = ClassType.getMetatypeInstanceType(M); | ||
|
||
auto *F | ||
= M.lookUpFunctionInVTable(ClassType.getClassOrBoundGenericClass(), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@slavapestov Is weeding out volatile methods/witnesses enough here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Update: Doesn’t matter now that dynamic witness methods are always going through ObjcMethodInst
The results of an unscientific comparison (stuck a master+Timer:
master+PR+Timer:
|
cc8dc7b
to
0918105
Compare
@swift-ci please smoke test |
@swiftix ping |
return true; | ||
|
||
if (FullApplySite FAI = FullApplySite::isa(&I)) { | ||
auto &M = FAI.getModule(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would suggest to move the logic that tries to determine the target function of the witness_method
and class_method
into a dedicated utility function, which would return the target function if it was able to find it. This function could be useful even outside of this code snippet.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It may also be interesting to do some checks to see if the exact dynamic type of the object, which is the operand of a class_method
/witness_method
instruction, is known statically. You may want to have a look at the Devirtualize.cpp
. I think it has a lot of things you could reuse here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
BasicCalleeAnalysis
is definitely already doing this. I'm going to look into refactoring this into a BottomUpIPAnalysis
pass.
Stack.push_back(Fn.getEntryBlock()); | ||
|
||
while (!Stack.empty()) { | ||
SILBasicBlock *CurBlock = Stack.pop_back_val(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wonder how this loop handles loops. Does its visit the BBs which belong to a loop multiple times?
BTW, is it necessary to start with the entry block? Does it matter at all, which block is used as a starting point? Would using RPOT order speed-up the convergence?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wonder how this loop handles loops. Does its visit the BBs which belong to a loop multiple times?
BasicBlocks can only migrate from the lower state (FoundRecursion) to higher states (FoundPathOutOfFunction) at which point they will be popped and ignored on the next turn. If the block doesn't migrate, then it will not be added to the work queue. Hence the analysis always converges.
For the loops I was able to come up with to convince myself of this, it wasn't visiting any of the basic blocks more than once and I wouldn't expect it to. Even if the loop header gets re-enqueued its successors will not be re-enqueued.
@@ -785,6 +890,18 @@ void swift::performSILDiagnoseUnreachable(SILModule *M, SILModuleTransform *T) { | |||
// Remove unreachable blocks. | |||
removeUnreachableBlocks(Fn, *M, &State); | |||
|
|||
// Gather exit blocks to check for naive self-recursion. | |||
llvm::SmallPtrSet<SILBasicBlock*, 4> ExitBlocks; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
IIRC, there was a utility function for something like this somewhere. But may be I'm wrong.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There's SILFunction::findExitingBlocks
, but it doesn't look like its other usage in Local.cpp
depends on it being ordered. I could switch them both to using a SmallPtrSet
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, I think it should be fine to switch them to use SmallPtrSet
|
||
// Diagnose infinitely recursive applies. | ||
if (checkInfinitelyRecursiveApplies(Fn, ExitBlocks)) | ||
diagnose(Fn.getModule().getASTContext(), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wonder if it would make sense to extend such an analysis to detect even a more general form of recursion, i.e. not only the self-recursion? It may turn out that it is just too expensive. But may be it is not so expensive.
You would essentially need to build a call-graph, I guess. We have a BasicCalleeAnalysis
that could help here.
WDYT?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It would be nice ™ to look into mutual recursion of two functions because of this SR.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
|
||
func tr(_ key: String) -> String { // expected-warning {{all paths through this function will call itself}} | ||
return tr(key) ?? key // expected-warning {{left side of nil coalescing operator '??' has non-optional type}} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could you add some negative test-cases as well? I.e. those ones which cannot be detected with a simple analysis yet, but could be eventually detected in the future?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There’s already a negative test for mutual recursion. Can you think of any others?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
May be something where class_method or witness_method cannot be devirtualized, i.e. you cannot determine the target?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added a new regression test for dynamic
methods. I can't really think of another way to have an in-module recursive function that can't be devirtualized.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Quick compile time comment.
|
||
// Diagnose infinitely recursive applies. | ||
if (checkInfinitelyRecursiveApplies(Fn, ExitBlocks)) | ||
diagnose(Fn.getModule().getASTContext(), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
0918105
to
fad43de
Compare
class C { | ||
lazy var a: String = { | ||
let a = "test" | ||
print(self.a) // Should warn - interprocedural cycle. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is SR-5224 :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think I know how to fix it too! Stand by.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
<3
fad43de
to
e76c0a2
Compare
@swift-ci please smoke test |
return; | ||
|
||
SILFunction *TargetFn = Fn; | ||
if (Fn->isBare() == IsNotBare) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@swiftix Is there a better way to detect this kind of thing? I'd rather not compute RPO info for more functions than I have to.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'll take a wild guess that large functions are almost always "not bare", so skipping this doesn't buy much. Although, it's probably helping for deserialized stdlib functions. @adrian-prantl, do you know what functions we expect to be "bare", and is that planning to change in the future?
I suppose you're looking for a more narrow way to filter on closures for lazy variable initialization. @jckarter may have some ideas.
@jrose-apple This crashes while trying to deserialize generic parameters for a vtable function. Any ideas? |
Nothing offhand. Those tests are about recovering from Clang declarations disappearing or changing names, though, and no work on the SIL deserializer has been done to support that. Doing inlining that the compiler normally wouldn't do could certainly trigger this…but I wouldn't really expect this to be doing more inlining than the compiler already does. |
It’s using the same primitives as the speculative devirtualizer. I can probably just bail on class decls with a Clang node in the meantime. |
It's not specifically Clang decls. It's decls that reference Clang decls. You can't tell if they're going to do it ahead of time. (I'm really saying "no, I don't know why this is happening, someone-probably-you will have to go find out, but here's the interesting bit about those tests".) |
Wonderful... There's a comment from @atrick that outlines what I'm seeing here
The generic parameters are being deserialized in an AbstractFunctionDecl context instead of the ambient ClassDecl context. |
That comment is the culmination of my attempt to understand deserialization well enough to work around some catastrophic bug. My understanding has only regressed since then, sorry. |
Even if I remove the assertion, there's a real lack of recovery in SIL deserialization. @jrose-apple perhaps it's enough to hack in a primitive that bails if it has to deserialize vtables at all. After all, really the only way that would be helpful is if the pass wanted to diagnose cross-module recursive calls and it can't even do in-module mutual recursion. |
Hm, well, the good news is it doesn't seem to depend on Clang decls after all? :-) Let's see. @CodaFi, are you actually using any of the cross-function analysis? I mean, you have a negative check for mutually recursive functions. Maybe it's worth just subsetting that out of the first implementation. I would say that the right answer is probably to make SIL function generic parameters just be independent from the AST all the time…unless there's a reason to have them continue pointing back to the AST? But that's not something that should get rolled into this PR; it needs its own work. |
Nope. This thing is only deserializing vtables in that test is because it sees a function call with a class-type receiver that happens to be in the damaged module. |
} | ||
|
||
// Ignore non-closure callees. | ||
if (!Callee->getLocation().isASTNode<AbstractClosureExpr>()) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You might want to at least check hasLocation
first. I would have to run an experiment to find out what SIL functions aren't getting a location.
I think the fact that this patch smashed two separate-but-related kinds of SR together is causing more friction than is worth it. I'm going to split the fix for SR-5224 off and just commit the bare-bones algorithm. |
d4c1574
to
563bb07
Compare
Beauty, eh? @swift-ci please smoke test |
Sorry for the delay. I've been off grid for a while. This looks very nice! Just one thing I have an issue with... If a successor is a block for which we've already found a recursive call (State==FoundRecursion), why do we put that block back on the Worklist, only to call You could solve this and simplify things considerably IMO, by doing away with
|
I agree that it simplifies things, but successors that we have already detected recursion-containing paths for are never re-enqueued. Suppose that we have detected recursion already in a block that is one of the successors of this block, and further, does not dominate it (as we would have ended the recursion analysis for that path at that dominating block). We would ask the state dictionary for information about that block and be told it has state I do agree that your algorithm is a nice simplification though. I'll take it! |
ad7f556
to
1deae40
Compare
@atrick One final review then? |
I think you need a bool flag to record that at least one recursive call is reachable and otherwise suppress diagnosis. Alternatively, you could comment that this always needs to run immediately after DiagnoseUnreachable (presumably that removed all unreachable exiting blocks), but a flag would be more defensive. This will still diagnose infinite recursion for a function with a reachable recursive call that does not dominate an infinite loop. I personally think that's good, especially since it's just a warning (using an infinite loop as the recursive base case can't be intentional). I don't think clang does significantly better here--it also diagnoses this case if the exit is reachable from the call, and clang gets it more wrong by failing to diagnose when a recursive call does dominate an infinite loop. (The way to get this "right" is to consider infinite loops "exits"). [In short, I don't think you should change this!] I hate to do this to you, but I just realized that calling |
I agree. I'll see about applying this to Clang as well. |
73d36a1
to
c6e74fa
Compare
@atrick Done. |
c6e74fa
to
6d6d974
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Great!
6d6d974
to
c9c4fe0
Compare
Add a new warning that detects when a function will call itself recursively on all code paths. Attempts to invoke functions like this may cause unbounded stack growth at least or undefined behavior in the worst cases. The detection code is implemented as DFS for a reachable exit path in a given SILFunction.
c9c4fe0
to
5c7b790
Compare
@swift-ci please smoke test |
⛵️ |
Add a new warning that detects when a function will call itself
recursively on all code paths. Attempts to invoke functions like this
may cause unbounded stack growth at least or undefined behavior in the
worst cases.
The detection code is implemented as DFS for a reachable exit path in
a given SILFunction.
Resolves SR-626, SR-677, SR-2559, and SR-4406
Has implications for SR-3016.