Skip to content

[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

Merged
merged 1 commit into from
Feb 21, 2019

Conversation

ravikandhadai
Copy link
Contributor

This pass is based on the existing SIL verifier checks but diagnoses only those errors that can be introduced by programmers.

rdar://43578476

@ravikandhadai ravikandhadai force-pushed the yieldcheck branch 2 times, most recently from 9bca614 to 772bb8d Compare December 5, 2018 23:16
@ravikandhadai
Copy link
Contributor Author

@swift-ci Please smoke test

Copy link
Contributor

@gottesmm gottesmm left a 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;
Copy link
Contributor

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 '*'.

@ravikandhadai
Copy link
Contributor Author

@swift-ci Please clean smoke test

Copy link
Contributor

@rjmccall rjmccall left a 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.

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", ())
Copy link
Contributor

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)...);
}
Copy link
Contributor

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;
Copy link
Contributor

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.

@ravikandhadai
Copy link
Contributor Author

@gottesmm @rjmccall Thanks for the comments. I am trying to debug a test failure in the file: generalized_accessor.sil I created in this PR. The test fails with the error "expected value ownership kind in SIL code". But there is an ownership annotation: @trivial at the point where it shows the error. Also, I couldn't reproduce this error locally. Just wondering if this sounds familiar to you and if there is something I am missing.

@rjmccall
Copy link
Contributor

rjmccall commented Dec 6, 2018

It's possible that the bots run sil-opt tests with -enable-sil-ownership enabled but your local configuration doesn't. I don't know why that would be true.

@ravikandhadai
Copy link
Contributor Author

@rjmccall Thanks for the inputs. I think I know the problem. I guess @trivial is removed by @gottesmm recently and I wasn't on recent master.

@ravikandhadai
Copy link
Contributor Author

@swift-ci Please smoke test

2 similar comments
@ravikandhadai
Copy link
Contributor Author

@swift-ci Please smoke test

@ravikandhadai
Copy link
Contributor Author

@swift-ci Please smoke test

@ravikandhadai
Copy link
Contributor Author

@swift-ci Please smoke test OS X platform

@ravikandhadai
Copy link
Contributor Author

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

Copy link
Contributor

@rjmccall rjmccall left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks great, thanks!

@ravikandhadai
Copy link
Contributor Author

@swift-ci Please Test Source Compatibility

@ravikandhadai
Copy link
Contributor Author

@swift-ci please benchmark

@swift-ci
Copy link
Contributor

swift-ci commented Dec 7, 2018

Build comment file:

No performance and code size changes


Copy link
Contributor

@devincoughlin devincoughlin left a 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.

@@ -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",())
Copy link
Contributor

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

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will do

// Yield usage errors
ERROR(return_before_yield, none, "accessor doesn't yield before returning",())

ERROR(multiple_yields, none, "encountered multiple yields along "
Copy link
Contributor

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

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure


NOTE(previous_yield, none, "previous yield was here", ())

ERROR(possible_return_before_yield, none, "cannot guarantee that the accessor "
Copy link
Contributor

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

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay.

"yields a value before returning", ())

NOTE(conflicting_join, none,
"some paths reaching here have a yield and some don't", ())
Copy link
Contributor

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.

Copy link
Contributor Author

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);
Copy link
Contributor

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

Copy link
Contributor Author

@ravikandhadai ravikandhadai Dec 10, 2018

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
Copy link
Contributor

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?

Copy link
Contributor Author

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()) {
Copy link
Contributor

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?

Copy link
Contributor Author

@ravikandhadai ravikandhadai Dec 10, 2018

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?

Copy link
Contributor

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.

Copy link
Contributor Author

@ravikandhadai ravikandhadai Feb 15, 2019

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.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That seems fair.

@rjmccall
Copy link
Contributor

rjmccall commented Feb 6, 2019

Is this ready for review? There's a lot of commented-out code.

@ravikandhadai
Copy link
Contributor Author

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.

@ravikandhadai
Copy link
Contributor Author

ravikandhadai commented Feb 14, 2019

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

@ravikandhadai
Copy link
Contributor Author

@swift-ci Please smoke test

@ravikandhadai
Copy link
Contributor Author

Fixing merge conflicts.

@ravikandhadai
Copy link
Contributor Author

@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;
Copy link
Contributor

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.

Copy link
Contributor Author

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?

Copy link
Contributor

@rjmccall rjmccall Feb 15, 2019

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.

Copy link
Contributor Author

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.

Copy link
Contributor

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());
Copy link
Contributor

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);
Copy link
Contributor

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()) {
Copy link
Contributor

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.

Copy link
Contributor

@gottesmm gottesmm left a 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
Copy link
Contributor

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):
Copy link
Contributor

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.
Copy link
Contributor

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>
Copy link
Contributor

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()) {
Copy link
Contributor

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()) {

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure. Will do.

@ravikandhadai
Copy link
Contributor Author

@swift-ci Please smoke test

2 similar comments
@ravikandhadai
Copy link
Contributor Author

@swift-ci Please smoke test

@ravikandhadai
Copy link
Contributor Author

@swift-ci Please smoke test

@ravikandhadai
Copy link
Contributor Author

@swift-ci Please smoke test linux platform

@shahmishal
Copy link
Member

Github is having issues, which is causing CI to fail.

https://www.githubstatus.com

screen shot 2019-02-19 at 10 18 13 am

Copy link
Contributor

@devincoughlin devincoughlin left a 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.

ERROR(possible_return_before_yield, none,
"accessor must yield on all paths before returning", ())

NOTE(one_yield, none, "found yield along one path", ())
Copy link
Contributor

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
//
//===----------------------------------------------------------------------===//

Copy link
Contributor

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();

Copy link
Contributor

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.
Copy link
Contributor

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.

@shahmishal
Copy link
Member

@swift-ci Please smoke test macOS platform

@ravikandhadai
Copy link
Contributor Author

@devincoughlin Thanks for the suggestions. I pushed a commit that fixes them.

Copy link
Contributor

@eeckstein eeckstein left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm

@ravikandhadai
Copy link
Contributor Author

@swift-ci Please test and merge

1 similar comment
@ravikandhadai
Copy link
Contributor Author

@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>
@ravikandhadai
Copy link
Contributor Author

@swift-ci Please test and merge

3 similar comments
@ravikandhadai
Copy link
Contributor Author

@swift-ci Please test and merge

@ravikandhadai
Copy link
Contributor Author

@swift-ci Please test and merge

@ravikandhadai
Copy link
Contributor Author

@swift-ci Please test and merge

@swift-ci swift-ci merged commit 1a66c77 into swiftlang:master Feb 21, 2019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

8 participants