-
Notifications
You must be signed in to change notification settings - Fork 10.5k
[SIL Diagnostics] Create a mandatory pass to check correct usage of yields in generalized accessors: _read and _modify #21067
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
Conversation
9bca614
to
772bb8d
Compare
@swift-ci Please smoke test |
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.
Just a quick drive by.
// correctness, but it could change the errors diagnosed when there are | ||
// multiple errors. Breadth-first search could diagnose errors along | ||
// shorter paths. | ||
llvm::DenseMap<SILBasicBlock*, BBState> visitedBBs; |
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.
As an aside. Please run this through git-clang-format. The patch is inconsistent about the location of the '*'.
@swift-ci Please clean smoke test |
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.
Generally looks good; just a few style quibbles.
include/swift/AST/DiagnosticsSIL.def
Outdated
NOTE(previous_yield, none, "previous 'yield' was here", ()) | ||
|
||
ERROR(possible_return_before_yield, none, "cannot guarantee that the accessor " | ||
"'yield's a value before returning", ()) |
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 probably reads better if you just don't quote yield
. It's a verb, like return
, and doesn't need to be treated as super-technical jargon.
static InFlightDiagnostic | ||
diagnose(ASTContext &Context, SourceLoc loc, Diag<T...> diag, U &&...args) { | ||
return Context.Diags.diagnose(loc, diag, std::forward<U>(args)...); | ||
} |
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 should consider pushing this up to SILTransform
, but it's not really harmful to declare it here if that's common practice for other diagnostic passes.
// When the successor follows a 'yield', update the successor state. | ||
if (isa<YieldInst>(term)) { | ||
insertResult.first->second.yieldState = YieldState::AfterYield; | ||
insertResult.first->second.yieldInst = term; |
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 should consider encapsulating BBState
: add functions to do the various state transitions (these can assert that (1) the current state is appropriate for the transition and (2) they've been provided the expected information (e.g. a yield instruction)) and functions to get the state-specific information (these can assert that the state is as expected). This sort of approach encourages you to write better, stronger assertions and can catch problems a lot faster if the code needs to evolve.
@gottesmm @rjmccall Thanks for the comments. I am trying to debug a test failure in the file: |
It's possible that the bots run |
772bb8d
to
e07362b
Compare
@swift-ci Please smoke test |
@swift-ci Please smoke test OS X platform |
@rjmccall I have encapsulated the state transitions of BBState in this commit based on your suggestions. Let me know if this is what you had in mind. |
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.
Looks great, thanks!
@swift-ci Please Test Source Compatibility |
@swift-ci please benchmark |
Build comment file:No performance and code size changes |
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 looks good to me. There are some suggestions for diagnostic improvements inline and a suggestion to use an RPO traversal.
include/swift/AST/DiagnosticsSIL.def
Outdated
@@ -358,6 +358,21 @@ WARNING(warning_int_to_fp_inexact, none, | |||
"'%1' is not exactly representable as %0; it becomes '%2'", | |||
(Type, StringRef, StringRef)) | |||
|
|||
|
|||
// Yield usage errors | |||
ERROR(return_before_yield, none, "accessor doesn't yield before returning",()) |
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.
Can this be more prescriptive? Perhaps "accessor must yield before returning"?
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.
Will do
include/swift/AST/DiagnosticsSIL.def
Outdated
// Yield usage errors | ||
ERROR(return_before_yield, none, "accessor doesn't yield before returning",()) | ||
|
||
ERROR(multiple_yields, none, "encountered multiple yields along " |
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.
Using "encountered" makes this diagnostic seem like it comes from the compiler's point of view. Can we rephrase this to be more from the user's point of view? Perhaps "accessor must not yield more than once"?
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.
Sure
include/swift/AST/DiagnosticsSIL.def
Outdated
|
||
NOTE(previous_yield, none, "previous yield was here", ()) | ||
|
||
ERROR(possible_return_before_yield, none, "cannot guarantee that the accessor " |
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.
"guarantee" is also from the compiler's point of view. Could we use similar diagnostic text to the definite initialization pass? Perhaps: "'accessor must yield on all paths before returning"?
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.
Okay.
include/swift/AST/DiagnosticsSIL.def
Outdated
"yields a value before returning", ()) | ||
|
||
NOTE(conflicting_join, none, | ||
"some paths reaching here have a yield and some don't", ()) |
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.
As a QoI improvement, I feel like this would be more helpful if this referred to control construct when we could figure it out. For example: "'then' branch yields but 'else' branch does not." You would still need a fallback case for when the compiler couldn't determine or understand the control construct.
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.
Okay. Let me see how much we can recover here.
public: | ||
void updateToAfterYield(SILInstruction *yldInst) { | ||
assert(yieldState == BeforeYield); | ||
assert(yldInst != nullptr); |
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 it would good to have call 'yldInst' 'yieldInst' so it is consistent with 'yieldState'.
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.
@devincoughlin yldInst
is a parameter, and yieldState
is the field of the struct: BBState. There is also a field calledyieldInst
in the BBState struct. So one assertion is checking a property on a field and another a property on the parameter. Perhaps I can make this parameter: yieldInstParam
var computed: Int { | ||
mutating _read { | ||
if stored > 0 { | ||
return |
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.
What happens when the only return is an explicit return and it hasn't yielded yet? Does the diagnostic go on the return statement itself?
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.
Yes. In the following example, the diagnostic will go on the return statement
var loc = 1
if flag {
loc = 2
}
return // expected-error {{accessor doesn't yield before returning}}
}
There can be only one return instruction in SIL. If the function can exit at multiple places the error will appear on the closing brace of the function definition. This is because at SIL level we only see one return instruction whose Loc
is the closing brace of the function definition.
llvm::Optional<SILInstruction *> returnInst = None; | ||
ASTContext &astCtx = fun.getModule().getASTContext(); | ||
|
||
while (!worklist.empty()) { |
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.
Since we're not iterating to a fixed point, instead of using a worklist can you just use PostOrderFunctionInfo::getReversePostOrder() to do the traversal?
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 are a couple of issues here: (a) the analysis does visit a node more than one once, if there is a conflict i.e, in the case of error. It pushes an already visited successor on to the worklist, if the successor is seen in a state that is different from the state it was seen with before. In the worst case, it processes each node twice, but that can happen only when there is an error. (b) The use of breadth-first search prioritizes shorter error paths over longer ones when there are multiple error paths. (However, an exception to this rule is that the analysis prioritizes "multiple-yield" error over a "return-without-yield" error by deferring the reporting of the latter error until the iteration completes.) I wonder whether RPO will preserve this behavior of shortest paths? E.g. if we have a tree of the form: (where D is a successor of C)
A
B ------ C
-------D
Then post-order is BDCA and I suppose reverse post-order is ACDB. So we see B
at the end, whereas BFS will see it as the first successor of A?
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.
Is the goal of visiting conflict blocks multiple times just to improve diagnostics in some way? It's not clear to me what it gets you at the moment.
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.
A block seen in the conflict state won't be visited twice, but a block seen in a non-conflict state could be processed again if it is seen in the conflict state. This is just to determine if we have a multiple yield error or a missing yield error. E.g.
if (flag) {
yield value
} <-- we see conflict here. And the error is that we don't yield on all paths.
return
OTOH,
if (flag) {
yield value
} <-- we see conflict here as before, but the error is that there are multiple yields along a path
yield value
return
It is just done to distinguish between the above two scenarios. Multiple yield errors seem easier to report by pointing to the two yields.
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.
That seems fair.
Is this ready for review? There's a lot of commented-out code. |
No not yet. I am working on recovering good diagnostics information for if-then-elses where one branch has a yield but other doesn't. I will ping you once it is ready for review. |
1dfb9be
to
fdc7656
Compare
@eeckstein @atrick This PR adds a mandatory pass for checking that generalized accessors (_read and _modify) yield exactly once. It would be great if you can provide your feedback on this. @rjmccall @devincoughlin I have planned to bring in the improvements to the diagnostics in two patches. In this commit, I have rewritten the original yield-once analysis in a data-flow analysis style by making the transfer functions and merge operations explicit. This refactoring makes adding improvements to the diagnostics easier. I have also added more tests in this commit. Let me know you feedback on this commit. Once this commit is merged, in a subsequent PR I will bring in the improvements for better diagnostics. The improvements will mainly target "conflict" cases where one branch yields but the other doesn't. |
@swift-ci Please smoke test |
eea5446
to
6501719
Compare
Fixing merge conflicts. |
@swift-ci please smoke test |
/// (BeforeYield) or after seeing a yield (AfterYield), or in both states | ||
/// (Conflict). This enum is a semi-lattice where Conflict is the top and | ||
/// the merge of BeforeYield and AfterYield states is Conflict. | ||
enum { BeforeYield, AfterYield, Conflict } yieldState = BeforeYield; |
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.
Please give BBState
a constructor with two arguments, remove the defaulting, and clean up the factories. It's fine to make it a private constructor, and you may need to add a getInitialState
factory.
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.
Sure. I like that but the only thing that makes default initialization necessary is the DenseMap bbToStateMap
used in the worklist algorithm. It has the type: llvm::DenseMap<SILBasicBlock *, BBState>
. I can make the BBState
optional and remove any default initialization/no-argument constructors. 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.
I don't think DenseMap
unconditionally requires its value type to be default-initializable; it's only required if you use methods that need default-initialization, like where you use try_emplace
, or if you use operator[]
anywhere instead of find
. I could be mistaken, of course.
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.
Yes your are right, without those operations, DenseMap doesn't need default constructors. I have pushed a commit to fix these.
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.
Looks good, thanks!
llvm::DenseMap<SILBasicBlock *, BBState> bbToStateMap; | ||
SmallVector<SILBasicBlock *, 16> worklist; | ||
|
||
auto *entryBB = &(*fun.begin()); |
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.
fun.getEntryBlock()
SmallVector<SILBasicBlock *, 16> worklist; | ||
|
||
auto *entryBB = &(*fun.begin()); | ||
bbToStateMap.try_emplace(entryBB); |
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.
Having a BBState::getInitialState()
will make this much clearer.
llvm::Optional<SILInstruction *> returnInst = None; | ||
ASTContext &astCtx = fun.getModule().getASTContext(); | ||
|
||
while (!worklist.empty()) { |
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.
Is the goal of visiting conflict blocks multiple times just to improve diagnostics in some way? It's not clear to me what it gets you at the moment.
6501719
to
5c01b4a
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.
A quick drive by. Also thanks for adding a .sil and a .swift test! = ).
private: | ||
// The following state is maintained for emitting diagnostics. | ||
|
||
// For AfterYield and Conflict states, this field records the yield |
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 should be a doxygen comment. (doxygen comment == '///'). We really need to get some sort of doxygen producing job up. It will make it easier to make sure this works well.
// instruction that was seen while propagating the state. | ||
SILInstruction *yieldInst; | ||
|
||
BBState(YieldState yState, SILInstruction *yieldI): |
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.
Can you make sure to run this through git-clang-format before committing? (I am guessing this needs to be run through b/c I don't think git-clang-format puts the ':' there.
/// A structure that records an error found during the analysis along with | ||
/// some context information that will be used by diagnostics. | ||
struct YieldError { | ||
// The kind of error. |
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 should be a doxygen comment.
/// an error that is detected. | ||
/// \return the state at the exit of the basic block if it can be computed | ||
/// and None otherwise. | ||
llvm::Optional<BBState> |
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.
We have imported Optional into the swift namespace, so I don't think you need the llvm:: any more for it. On another note, can you also make this helper function have static on it?
|
||
auto nextState = resultState.getValue(); | ||
|
||
for (auto &succ : bb->getSuccessors()) { |
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.
Do you ever actually need access to the successor? If not you can use:
for (auto *succBB : bb->getSuccessorBlocks()) {
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.
Sure. Will do.
5c01b4a
to
9ad1fdd
Compare
@swift-ci Please smoke test |
@swift-ci Please smoke test linux platform |
Github is having issues, which is causing CI to fail. |
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.
LGTM. Some minor comments inline.
include/swift/AST/DiagnosticsSIL.def
Outdated
ERROR(possible_return_before_yield, none, | ||
"accessor must yield on all paths before returning", ()) | ||
|
||
NOTE(one_yield, none, "found yield along one path", ()) |
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.
Perhaps "yield along one path is here"?
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors | ||
// | ||
//===----------------------------------------------------------------------===// | ||
|
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 put a brief description here of the high-level property that the pass is checking for here and a brief overview of high-level approach it takes. Not too much detail, but enough that someone new to the project could read it and know what the purpose of this pass is.
transferStateThroughBasicBlock(SILBasicBlock *bb, BBState inState, | ||
Optional<YieldError> &error) { | ||
auto *term = bb->getTerminator(); | ||
|
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.
Should this initialize 'error' to None? That way if a caller reuses the passed in error it will get correctly set to None when there is no error.
return oldState; | ||
} | ||
|
||
// Here, one state is AfterYield and the other one is BeforeYield. |
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.
Can you add an assertion for this? That way if the lattice changes but the merge function doesn't we'll get an assertion failure.
@swift-ci Please smoke test macOS platform |
9ad1fdd
to
7cc7809
Compare
@devincoughlin Thanks for the suggestions. I pushed a commit that fixes them. |
7cc7809
to
5cb9f68
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.
lgtm
@swift-ci Please test and merge |
1 similar comment
@swift-ci Please test and merge |
yields in generalized accessors: _read and _modify, which are yield-once corountines. This pass is based on the existing SIL verifier checks but diagnoses only those errors that can be introduced by programmers when using yields. <rdar://43578476>
5cb9f68
to
a9b0ebe
Compare
@swift-ci Please test and merge |
3 similar comments
@swift-ci Please test and merge |
@swift-ci Please test and merge |
@swift-ci Please test and merge |
This pass is based on the existing SIL verifier checks but diagnoses only those errors that can be introduced by programmers.
rdar://43578476