Skip to content

[ownership] Add a new ReborrowVerifier #34438

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
Nov 3, 2020

Conversation

meg-gupta
Copy link
Contributor

This updates how we model reborrow's lifetimes for ownership verification.
Today we follow and combine a borrow's lifetime through phi args as well.
Owned values lifetimes end at a phi arg. This discrepency in modeling
lifetimes leads to the OwnershipVerifier raising errors incorrectly for
cases such as this, where the borrow and the base value do not dominate
the end_borrow:

bb0:
  cond_br undef, bb1, bb2
bb1:
  %copy0 = copy_value %0
  %borrow0 = begin_borrow %copy0
  br bb3(%borrow0, %copy0)
bb2:
  %copy1 = copy_value %1
  %borrow1 = begin_borrow %copy1
  br bb3(%borrow1, %copy1)
bb3(%borrow, %baseVal):
  end_borrow %borrow
  destroy_value %baseVal

This PR adds a new ReborrowVerifier. The ownership verifier collects borrow's
lifetime ending users and populates the worklist of the ReborrowVerifier
with reborrows and the corresponding base value.
ReborrowVerifier then verifies that the lifetime of the reborrow is
within the lifetime of the base value.

bool verify();

// Add a guaranteed phiArg and its base value to the worklist for verification
void add(SILPhiArgument *phiArg, SILValue baseVal) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Does this need to be in SIL? Can we hide it as a library detail?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'll update it.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done.

@meg-gupta meg-gupta requested review from gottesmm and atrick October 26, 2020 20:12
@meg-gupta meg-gupta force-pushed the reborrowverifier branch 2 times, most recently from f863ea3 to 48b505a Compare October 26, 2020 20:58
@meg-gupta
Copy link
Contributor Author

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

Some initial comments around perf/etc and some things for me that I need to think about. I want to look through this again before this goes in.

// this.
class ReborrowVerifier {
/// worklist of reborrows and their base value
llvm::SmallVector<std::pair<SILPhiArgument *, SILValue>, 8> worklist;
Copy link
Contributor

Choose a reason for hiding this comment

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

You don't need llvm:: on SmallVector.

// lies within the lifetime of its base value. It uses LinearLifetimeChecker for
// this.
class ReborrowVerifier {
/// worklist of reborrows and their base value
Copy link
Contributor

Choose a reason for hiding this comment

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

Caps plz. I would also put space in between ivars.

auto item = worklist.pop_back_val();
auto *phiArg = item.first;
auto baseVal = item.second;
SmallVector<Operand *, 4> phiArgUses(phiArg->getUses());
Copy link
Contributor

Choose a reason for hiding this comment

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

I would hoist these small things out. That way if we go big, we do not need to reallocate.

@@ -0,0 +1,63 @@
//===--- ReborrowVerifier.cpp ----------------------------------------===//
Copy link
Contributor

Choose a reason for hiding this comment

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

This header is messed up. I think it isn't 80 columns. Also can you change the copyright year to 2020?

@@ -50,6 +50,7 @@ class LinearLifetimeChecker {
class ErrorBuilder;

private:
friend class ReborrowVerifier;
Copy link
Contributor

Choose a reason for hiding this comment

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

Rather than doing this, can you expose a higher level API on LinearLifetimeChecker? Maybe an asserting variant of checkUses?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Am doing this to pass LinearLifetimeChecker::ErrorBuilder with continuous error number with a common errorMessageCounter. I would have used the public LinearLifetimeChecker ::validateLifetime but would loose the nice error numbering.
What do you think about exposing a version of LinearLifetimeChecker ::validateLifetime that has an errorBuilder parameter ?

@@ -285,6 +285,12 @@ class ValueBase : public SILNode, public SILAllocated<ValueBase> {
/// and it has a single consuming user. Returns .none otherwise.
inline Operand *getSingleConsumingUse() const;

inline SmallVector<Operand *, 4>
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 use an iterator here instead of returning a SmallVector?

Also why do you need the verifying argument?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

When verifying with OwnershipVerifierTextualErrorDumper - we don't want this assert to fire: https://github.com/apple/swift/blob/58d07a4efd0d9e5a38dc4b0d029d6f80d7152a62/include/swift/SIL/SILValue.h#L586. That reminds me that callers should pass true only when verifying with OwnershipVerifierTextualErrorDumper.


foundUses.push_back(op);
});
[&](Operand *op) { foundUses.push_back(op); });
}
Copy link
Contributor

Choose a reason for hiding this comment

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

I need to think about this. Just to make sure it works in my brain.

}
if (!isa<TermInst>(&i)) {
return true;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

I need to think about this. This is used for more than just the reborrow verifier.

@@ -215,6 +233,79 @@ bool SILValueOwnershipChecker::isCompatibleDefUse(
return false;
}

// Look for reborrows and find their base value and populate ReborrowVerifier's
// worklist.
void SILValueOwnershipChecker::populateReborrowVerifier(
Copy link
Contributor

Choose a reason for hiding this comment

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

I would stick this in ReborrowVerifier. SILValueOwnershipChecker doesn't need to know about this detail.

worklist.push_back(std::make_pair(phiArg, baseVal));
}

// check if the operand is visited
Copy link
Contributor

Choose a reason for hiding this comment

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

3 slashes for doxygen comment.

// check if the phi arg and base value are visited
bool isVisitedPhiArg(SILPhiArgument *phiArg, SILValue baseVal) {
auto itPhiArg = visitedPhiArgs.find(phiArg);
if (itPhiArg != visitedPhiArgs.end() && (*itPhiArg).second == baseVal)
Copy link
Contributor

Choose a reason for hiding this comment

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

You can just return this. That is:

return itPhiArg != visitedPhiArgs.end() && ...

/// ignore "leaks".
DeadEndBlocks &deadEndBlocks;
/// A cache map of borrow lifetime ending operands and their base value
llvm::DenseMap<Operand *, SILValue> visitedOps;
Copy link
Contributor

Choose a reason for hiding this comment

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

Do these need to be full on DenseMaps? Seems like they could be SmallDenseMaps.

@swift-ci
Copy link
Contributor

Build failed
Swift Test Linux Platform
Git Sha - 48b505a73f0467e0c794b9053e2f2a2d27304085

@swift-ci
Copy link
Contributor

Build failed
Swift Test OS X Platform
Git Sha - 48b505a73f0467e0c794b9053e2f2a2d27304085

<< ". Not in map!\n"
<< *this;
llvm_unreachable("standard error assertion");
if (!verifying) {
Copy link
Contributor

Choose a reason for hiding this comment

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

If we want to do this, why not pass back an optional?

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.

Some more comments. I want to understand this more before it goes in/have my questions answered.

#ifndef NDEBUG
if (!canAcceptKind(kind)) {
llvm::errs() << "Can not lookup lifetime constraint: " << kind
<< ". Not in map!\n"
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 this is needed here. Here is my suggestion, why don't we add two entry points, one that asserts and the other that returns an optional. That way the user can make the choice.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Right, I reverted to original

@@ -770,6 +781,71 @@ inline ValueBase::use_iterator ValueBase::use_end() const {
inline iterator_range<ValueBase::use_iterator> ValueBase::getUses() const {
return { use_begin(), use_end() };
}

class ConsumingUseIterator : public ValueBaseUseIterator {
public:
Copy link
Contributor

Choose a reason for hiding this comment

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

This needs to be git-clang-formated.

@@ -229,13 +229,6 @@ void BorrowingOperand::visitConsumingUsesOfBorrowIntroducingUserResults(
// This enables one to walk the def-use chain of guaranteed phis for a
// single guaranteed scope.
value.visitLocalScopeEndingUses([&](Operand *valueUser) {
if (auto subBorrowScopeOp = BorrowingOperand::get(valueUser)) {
if (subBorrowScopeOp->consumesGuaranteedValues()) {
subBorrowScopeOp->visitUserResultConsumingUses(func);
Copy link
Contributor

Choose a reason for hiding this comment

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

The point of this method was to be able to chain through a piece-wise guaranteed lifetime. Given that we are no longer doing that I don't think that this method makes sense as written anymore.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I am leaving BorrowingOperand::visitConsumingUsesOfBorrowIntroducingUserResults unchanged now. It gets called transitively from BorrowedValue::areUsesWithinScope which gets used in SemanticARCOpt

SmallVector<BorrowingOperand, 8> worklist;
SmallPtrSet<Operand *, 8> visitedValue;
worklist.push_back(*this);
visitedValue.insert(op);
bool foundError = false;

while (!worklist.empty()) {
auto scopedOperand = worklist.pop_back_val();
scopedOperand.visitConsumingUsesOfBorrowIntroducingUserResults(
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 this at this point should just be visitEndScopeInstructions, no?

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, changed it : )

}
// guaranteed phi args are verifier as part of
// ReborrowVerifier
if (nonConsumingUse->get().getOwnershipKind() ==
Copy link
Contributor

Choose a reason for hiding this comment

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

I don't understand why this is here. This is a detail of the ReborrowVerifier leaking into the LinearLifetimeChecker. Can you elaborate here?

foundError |=
initialScopedOperand->getImplicitUses(nonLifetimeEndingUsers, &error);
SmallVector<Operand *, 4> borrowLifetimeEndingUsers;
initialScopedOperand->getImplicitUses(borrowLifetimeEndingUsers, &error);
Copy link
Contributor

Choose a reason for hiding this comment

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

My thought: why not just make the reborrow verifier a struct that is created here that takes the initialScopedOperand and verifies based off of that? Would be simpler and details wouldn't leak out.

@meg-gupta meg-gupta force-pushed the reborrowverifier branch 2 times, most recently from a1f7fc7 to 58f716e Compare October 29, 2020 00:35
@meg-gupta
Copy link
Contributor Author

@swift-ci test

@swift-ci
Copy link
Contributor

Build failed
Swift Test Linux Platform
Git Sha - 58f716ed345fd2e1c058962393d2177b4caf21e7

return false;
// Reborrows are currently represented as a consuming
// use. Do not raise error for reborrows.
if (isReborrowInstruction(&i))
Copy link
Contributor

Choose a reason for hiding this comment

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

I thought about this in a deeper manner. The condition we are really looking for here is if the consuming use and the non consuming use are the same instruction. We can just hoist that out of the if into an earlier if statement and continue. I.e.:

for (auto *nonConsumingUse : nonConsumingUsesInBlock) {
  auto *consumingUser = consumingUse->getUser();
  if (nonConsumingUse->getUser() != consumingUser)) {
     if (std::find_if(... normal code ...)) {
        continue;
     }
  } else {
    if (isReborrowInstruction(consumingUser))
       continue;
  }

  ... throw error ...
}

Copy link
Contributor

Choose a reason for hiding this comment

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

I am also worried about baking in isReborrowInstruction into the verifier itself. It is meant to be a more general utility than this. Maybe add a flag that controls whether or not we allow for reborrows and have it disabled by default? Then at least we are only making this change where it is needed.

Another thing, on the reborrow failure path we could emit a better error as well (since it is not exactly an outside of lifetime use).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I refactored. But am not sure why would we need a flag ? Reborrows are never consuming

SmallVector<Operand *, 4> phiArgUses(phiArg->getUses().begin(),
phiArg->getUses().end());

// Verify whethre the guaranteed phi arg lies within the lifetime of the base
Copy link
Contributor

Choose a reason for hiding this comment

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

whethre -> whether

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.

Looking much better! Some more smaller things.

I looked (but maybe I needed to look harder = p, so if I missed it feel free to ignore). Can you add multiple diamond tests. Something that does:

checked_cast_br -> switch_enum?

SILValue baseVal) {
SmallPtrSet<SILBasicBlock *, 4> visitedBlocks;
bool result = false;
SmallVector<Operand *, 4> phiArgUses(phiArg->getUses().begin(),
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 you can just pass in phiArg->getUses(). I would try it.

if (isVisitedOp(borrowLifetimeEndOp, baseVal))
continue;

// Process reborrow
Copy link
Contributor

Choose a reason for hiding this comment

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

I would add a TODO here that says if we add more reborrow instructions this should be abstracted onto a ReborrowingOperand ADT type so we can abstract over terminator vs non-terminator. I am fine doing that latter.


// A guaranteed phi arg ends the borrow scope of its incoming value and begins a
// new borrow scope. ReborrowVerifier validates the lifetime of the reborrow
// lies within the lifetime of its base value. It uses LinearLifetimeChecker for
Copy link
Contributor

Choose a reason for hiding this comment

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

Three slashes here.

@@ -0,0 +1,267 @@

// RUN: %target-sil-opt -sil-ownership-verifier-enable-testing -ownership-verifier-textual-error-dumper -enable-sil-verify-all=0 %s -o /dev/null 2>&1 | %FileCheck %s
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit: Can you put the RUN line on the top line and put a space in between the REQUIRES: asserts and the imports.

This updates how we model reborrow's lifetimes for ownership verification.
Today we follow and combine a borrow's lifetime through phi args as well.
Owned values lifetimes end at a phi arg. This discrepency in modeling
lifetimes leads to the OwnershipVerifier raising errors incorrectly for
cases such as this, where the borrow and the base value do not dominate
the end_borrow:

bb0:
  cond_br undef, bb1, bb2
bb1:
  %copy0 = copy_value %0
  %borrow0 = begin_borrow %copy0
  br bb3(%borrow0, %copy0)
bb2:
  %copy1 = copy_value %1
  %borrow1 = begin_borrow %copy1
  br bb3(%borrow1, %copy1)
bb3(%borrow, %baseVal):
  end_borrow %borrow
  destroy_value %baseVal

This PR adds a new ReborrowVerifier. The ownership verifier collects borrow's
lifetime ending users and populates the worklist of the ReborrowVerifier
with reborrows and the corresponding base value.
ReborrowVerifier then verifies that the lifetime of the reborrow is
within the lifetime of the base value.
@meg-gupta
Copy link
Contributor Author

@swift-ci test

@swift-ci
Copy link
Contributor

Build failed
Swift Test Linux Platform
Git Sha - 601ea65

@swift-ci
Copy link
Contributor

Build failed
Swift Test OS X Platform
Git Sha - 601ea65

@gottesmm
Copy link
Contributor

gottesmm commented Nov 2, 2020

@swift-ci test

@swift-ci
Copy link
Contributor

swift-ci commented Nov 2, 2020

Build failed
Swift Test OS X Platform
Git Sha - 601ea65

@meg-gupta
Copy link
Contributor Author

@swift-ci test OS X Platform

@meg-gupta meg-gupta merged commit 6c46841 into swiftlang:main Nov 3, 2020
@meg-gupta meg-gupta deleted the reborrowverifier branch July 9, 2021 00:27
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.

3 participants