Skip to content

SwiftCompilerSources for ownership, borrowing, and liveness #70887

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 20 commits into from
Jan 18, 2024

Conversation

atrick
Copy link
Contributor

@atrick atrick commented Jan 12, 2024

Add BorrowUtils: OSSA infrastructure for borrow scopes.
Key APIs necessary for using OSSA.
- BorrowingInstruction
- BeginBorrowValue
- scopeEndingOperands
- BorrowIntroducers
- EnclosingValues
- innerAdjacentPhis

Add OwnershipLiveness.swift. Core OSSA APIs.
This is what you need to correctly analyze OSSA.
- computeLinearLiveness
- computeInteriorLiveness
- InteriorUseVisitor
- OwnershipUseVisitor
- LivenessBoundary

This incorporates review feedback from
#70460

@atrick
Copy link
Contributor Author

atrick commented Jan 12, 2024

@swift-ci test

@atrick
Copy link
Contributor Author

atrick commented Jan 13, 2024

@swift-ci test

@atrick
Copy link
Contributor Author

atrick commented Jan 13, 2024

This PR is blocked by a likely recent LoadableByAddress bug that happens to be exposed.

@atrick atrick force-pushed the ownership-liveness branch from 94d1b39 to 68527d6 Compare January 15, 2024 01:41
@atrick
Copy link
Contributor Author

atrick commented Jan 15, 2024

@swift-ci test

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.

The APIs and utilities in OwnershipLiveness still need quite a lot of polishing, clarification and better documentation.

Note that I also added a few comments in the old PR #70460

/// or introduces a borrow scope, then `value` is the single
/// introducer for itself.
///
/// Example: // introducers:
Copy link
Contributor

Choose a reason for hiding this comment

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

great comment with the examples!

/// If `value` is a reborrow, then this either returns a dominating
/// enclosing value or an outer adjacent phi.
///
/// Example: // enclosing value:
Copy link
Contributor

Choose a reason for hiding this comment

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

Again, great that you added these examples!

Which brings me back to the question that I asked in the other PR: It looks like enclosing values are only different than borrow introducers for certain kind of instructions, e.g. BorrowInstructions.

Wouldn't it be easier to gather enclosing values by e.g.

  var enclosingValues = Stack()
  gatherBorrowIntroducers(for: value, in: &borrowIntroducers)
  for bi in borrowIntroducers {
    if let bb = BeginBorrowedValue(bi) {
      gatherBorrowIntroducers(for: bb.baseOperand.value, in: &enclosingValues)
    } else {
      enclosingValues.append(bi.value)
    }
  }

... which maps 1-1 to the cases you listed in the comment?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That logic is stated in the simplest form in EnclosingValues.gather. It is called recursively with its own state for tracking reborrows.

Those top-level utility functions don't do anything but create a Stack state that's needed for analysis.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Note that EnclosingValues deals with reborrows, and BorrowIntroducers deals with guaranteed forwarding phis. It's a similar problem, so they both use mapToPhi. But it's also useful to know which you're handling, and splitting the utilities made it much easier for me to reason about the state of recursion.

Copy link
Contributor

Choose a reason for hiding this comment

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

That logic is stated in the simplest form in EnclosingValues.gather.

Right. But why is EnclosingValues.gather(forReborrow:) needed? Aren't enclosing values of a reborrow the same as its borrow introducers? So, shouldn't BorrowIntroducers.gather find the enclosing values?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

EnclosingValues deals with reborrows, and BorrowIntroducers deals with guaranteed forwarding phis. It's a similar problem, so they both use mapToPhi. But it's also useful to know which you're handling, and splitting the utilities made it much easier for me to reason about the state of recursion.

///
/// TODO: Verify that pointerEscape is only called for live ranges in which
/// `findPointerEscape()` returns true.
protocol AddressUseVisitor {
Copy link
Contributor

Choose a reason for hiding this comment

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

Also here: AddressUseVisitor is actually derived from AddressDefUseWalker. In addition to AddressDefUseWalker it refines the treatment of leaf-uses. This is how you can build on top of AddressDefUseWalker:

@@ -445,15 +445,12 @@ extension InteriorUseVisitor {
 ///
 /// TODO: Verify that pointerEscape is only called for live ranges in which
 /// `findPointerEscape()` returns true.
-protocol AddressUseVisitor {
+protocol AddressUseVisitor: AddressDefUseWalker where Path == UnusedWalkingPath {
   var _context: Context { get }
 
   /// Start a def-use walk from and address.
   mutating func walkDownUses(ofAddress: Value) -> WalkResult 
 
-  /// Start a def-use walk from an address operand.
-  mutating func walkDown(address: Operand) -> WalkResult
-
   /// An address projection produces a single address result and does not
   /// escape its address operand in any other way.
   ///
@@ -529,21 +526,13 @@ extension AddressUseVisitor {
     return walkDownUses(ofAddress: value)
   }
 
-  mutating func walkDown(address: Operand) -> WalkResult {
+  mutating func walkDown(address: Operand, path: UnusedWalkingPath = UnusedWalkingPath()) -> WalkResult {
     switch address.instruction {
     case let ba as BeginAccessInst:
       if handle(access: ba) == .abortWalk {
         return .abortWalk
       }
       return ba.endOperands.walk { leafUse(address: $0) }
-
-    case let load as LoadBorrowInst:
-      if handle(borrow: load) == .abortWalk {
-        return .abortWalk
-      }
-      return BeginBorrowValue(load)!.scopeEndingOperands.walk {
-        leafUse(address: $0)
-      }
     case let markDep as MarkDependenceInst:
       if markDep.valueOperand == address {
         return walkDown(projection: markDep, of: address)
@@ -561,21 +550,24 @@ extension AddressUseVisitor {
       */
       // A potentially escaping value depends on this address.
       return pointerEscape(address: address)
+    default:
+      break
+    }
+    return walkDownDefault(address: address, path: path)
+  }
 
+  mutating func leafUse(address: Operand, path: UnusedWalkingPath) -> WalkResult {
+    switch address.instruction {
+    case let load as LoadBorrowInst:
+      if handle(borrow: load) == .abortWalk {
+        return .abortWalk
+      }
+      return BeginBorrowValue(load)!.scopeEndingOperands.walk {
+        leafUse(address: $0)
+      }
     case let pai as PartialApplyInst where pai.isOnStack:
       return dependent(value: pai, dependsOn: address)
 
-    case is StructElementAddrInst, is TupleElementAddrInst,
-         is InitEnumDataAddrInst, is UncheckedTakeEnumDataAddrInst,
-         is InitExistentialAddrInst, is OpenExistentialAddrInst,
-         is IndexAddrInst, is MarkUnresolvedNonCopyableValueInst,
-         is ProjectBlockStorageInst, is TailAddrInst, is StoreBorrowInst,
-         is UncheckedAddrCastInst, is MarkUninitializedInst, is DropDeinitInst,
-         is TuplePackElementAddrInst, is CopyableToMoveOnlyWrapperAddrInst,
-         is MoveOnlyWrapperToCopyableAddrInst:
-      let svi = address.instruction as! SingleValueInstruction
-      return walkDown(projection: svi, of: address)
-
     case is ReturnInst, is ThrowInst, is YieldInst, is TryApplyInst,
          is SwitchEnumAddrInst, is CheckedCastAddrBranchInst,
          is SelectEnumAddrInst, is InjectEnumAddrInst,

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That Walker convention is good but I don't want to rename AddressUseVisitor yet. Its purpose of is to visit all of the address uses, not just the leaf uses, partitioning them by the kind of use. This is the most general possible address use visitor, so there's no other sensible name.

Note that I originally wrote AddressUseVisitor as an extension of AddressDefUseWalker, even though it's obviously backward, just to keep up the appearance of reuse. It was complex and bug-prone, and the results were invalid.

For now, I need to separate this new infrastructure from the code that affects the rest of the compiler. So redesigning AddressDefUseWalker should be a separate step. In fact, the separate list of opcodes is necessary until we want to change the behavior of existing passes.

Literally the only thing you want to reuse here is the list of address projection opcodes. The simple, obvious way to do that is with an AddressProjection abstraction. That would solve any short term problem. In general, forcing inheritance to reuse logic leads to incomprehensible, buggy code.

Longer term, the two utilities should be integrated. There are multiple problems with the current design and it's not possible to write a simple address use visitor on top of it. One thing I'll point out is that AddressDefUseWalker.leafUse is just outright wrong. It's called on just any unrecognizable use, and that's not suitable for utilities that are designed to be complete.

AddressDefUseWalker should be built on top of AddressUseVisitor. It should simply override projection to update the path and it should be responsible for the def-use walk itself, rather than just visiting the use. Then AddressUseVisitor won't have to do any walk and everything will make sense and work naturally.

Copy link
Contributor

Choose a reason for hiding this comment

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

The simple, obvious way to do that is with an AddressProjection abstraction.

AddressDefUseWalker is the address projection abstraction. Its implementation mainly consists of a switch over all address projection instructions and "translating" that to a projection path.

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 use AddressLifetimeDefUseWalker for this. Later, I think the other one should be renamedAddressPathDefUseWalker or AddressProjectionDefUseWalker. We should still add an AddressProjection protocol.

is ProjectBlockStorageInst, is TailAddrInst, is StoreBorrowInst,
is UncheckedAddrCastInst, is MarkUninitializedInst, is DropDeinitInst,
is TuplePackElementAddrInst, is CopyableToMoveOnlyWrapperAddrInst,
is MoveOnlyWrapperToCopyableAddrInst:
Copy link
Contributor

Choose a reason for hiding this comment

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

This is the case block which should be handled by AddressDefUseWalker. This implementation already contains unhealthy code duplication because it's easy to add instructions here and forget in AddressDefUseWalker or vice versa.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

As mentioned above, we should have one list of address projections, and it definitely should not be buried inside a walker! But for now, the two implementations are intentionally not in sync with each other. SIL verification should come first.

/// result and implement this as a fatalError.
mutating func visitPointerEscape(use: Operand) -> WalkResult

/// Handle begin_borrow, load_borrow, store_borrow, begin_apply.
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm completely confused by this comment.

}
}

/// Enumerate all special cases of ownership uses in a visitor
Copy link
Contributor

Choose a reason for hiding this comment

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

It's not clear to me what OwnershipUseVisitor does or should do. Also this comment is not really useful.
On the one hand it appears to wrap a visitor around the OperandOwnership enum. On the other hand it looks like it's also doing some kind of def-use walk.
The confusion already starts with that I'm not sure what the terms "outer" and "inner" scope/liferange/value actually refer to.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

OwnershipUseVisitor is the one place that define the effects of operand ownership. We need this so we don't end up with ad-hoc utilities in OptUtils like InstructionRange.insert(borrowScopeOf) which is out of place, incomplete, and likely misused.

Copy link
Contributor

@eeckstein eeckstein Jan 16, 2024

Choose a reason for hiding this comment

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

We really need to keep those new utilities understandable and easy to use. Also, it's important to make it clear how utilities relate to each other and when to use which utility.

@atrick atrick force-pushed the ownership-liveness branch from 68527d6 to 7d19418 Compare January 16, 2024 06:56
@atrick
Copy link
Contributor Author

atrick commented Jan 16, 2024

@swift-ci test

return
}
// avoid duplicates in the enclosingValues set.
var pushedEnclosingValues = ValueSet(context)
Copy link
Contributor

@eeckstein eeckstein Jan 16, 2024

Choose a reason for hiding this comment

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

You cannot create a ValueSet in a recursive function. It will run out of bit positions (for the set) pretty quickly.
Can you define pushedEnclosingValues as member property of EnclosingValues? IIUC, a single set for the whole gathering process would be good enough to avoid duplicate entries in the result.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks, i meant to convert that ValueSet into a cache. That's done now.

// of which dominates the value and introduces a borrow scope that
// encloses all forwarded uses of the guaranteed value.
//
// %1 = begin_borrow %0 // borrow introducer for %3
Copy link
Contributor

Choose a reason for hiding this comment

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

-> borrow introducer for %2

// %5 = owned value
// %6 = begin_borrow %5 %6 %5
// br bb3(%5, %6)
// bb3(%phi: @owned, invalid %0
Copy link
Contributor

Choose a reason for hiding this comment

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

%0 -> none (?)

switch self {
case .beginBorrow, .storeBorrow:
let svi = instruction as! SingleValueInstruction
return svi.uses.walk {
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 also write svi.uses.filterUsers(ofType: EndBorrowInst.self).walk ...

// cond_br ..., bb1, bb2
// bb1:
// %1 = owned value
// %2 = begin_borrow %0 %2 %1
Copy link
Contributor

Choose a reason for hiding this comment

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

%0 -> %1

// %6 = begin_borrow %0 %6 %0
// %7 = struct (%6) %6 %6
// br bb3(%6, %7)
// bb3(%reborrow: @guaranteed, %reborrow %0
Copy link
Contributor

Choose a reason for hiding this comment

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

Aren't we already printing reborrows with a @reborrow attribute (instead of @guaranteed)?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That would be great. For some reason we still print @guaranteed. I'll change the comments to @reborrow to anticipate fixing this.

let context: FunctionPassContext
var _context: Context { context }

let function: Function
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 do

   var function: Function { definingValue.parentFunction }

/// %0 = begin_borrow %outerValue
/// %1 = begin_borrow %0
/// end_borrow %1 // inner "use point" of %0
/// end_borrow %0 // outer use of %1
Copy link
Contributor

Choose a reason for hiding this comment

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

shouldn't this be an outer use of %0?

}
}

/// Visit only those uses of an value within an inner borrow scope
Copy link
Contributor

Choose a reason for hiding this comment

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

a value

@atrick atrick force-pushed the ownership-liveness branch from 7d19418 to 320d798 Compare January 17, 2024 07:43
@atrick
Copy link
Contributor Author

atrick commented Jan 17, 2024

@swift-ci test

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!

case let arg as Argument where arg.isReborrow:
self = .reborrow(Phi(arg)!)
default:
if value.definingInstruction is BeginApplyInst {
Copy link
Contributor

@eeckstein eeckstein Jan 17, 2024

Choose a reason for hiding this comment

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

nit, in case you are pushing a new version anyway: you can write

  case let mvir as MultipleValueInstructionResult where mvir.parentInstruction is BeginApplyInst:

Then you can also define the case payload as case beginApply(MultipleValueInstructionResult) which avoids the optional-nil-check when getting its parent instruction.

And rename `uses.lifetimeEndingUses` to `uses.endingLifetime`
Also fix the memory binding within stack access.
Format comments so they are readable on github and 80-col tools.
Key APIs necessary for using OSSA.

- BorrowingInstruction

- BeginBorrowValue

- scopeEndingOperands

- BorrowIntroducers

- EnclosingValues

- innerAdjacentPhis

These need to be complete to be correct.
This is what you need to correctly analyze OSSA.

- computeLinearLiveness

- computeInteriorLiveness

- InteriorUseVisitor

- OwnershipUseVisitor

- LivenessBoundary

Along with BorrowUtils.swift, all of our OSSA transformations are
built on top of these fundamentals. With these APIs, we can build
anything OSSA-related in SwiftCompilerSources. These utilities are
immediately needed for borrowed arguments and lifetime dependence. In
the near future, we can also use them to complete OSSA lifetimes and
*correctly* fixup OSSA after transformation without introducing lots
of copies and creating lots of incorrect corner cases.
Along with motivating unit tests.
To occur in the order that people will likely want to read.
@atrick atrick force-pushed the ownership-liveness branch 2 times, most recently from 58f670d to 8aa6833 Compare January 17, 2024 16:40
@atrick
Copy link
Contributor Author

atrick commented Jan 17, 2024

@swift-ci smoke test

@atrick atrick enabled auto-merge January 17, 2024 16:42
@atrick atrick force-pushed the ownership-liveness branch from 8aa6833 to 8341b73 Compare January 17, 2024 17:07
@atrick
Copy link
Contributor Author

atrick commented Jan 17, 2024

@swift-ci smoke test

@atrick
Copy link
Contributor Author

atrick commented Jan 17, 2024

@swift-ci smoke test linux

@atrick
Copy link
Contributor Author

atrick commented Jan 17, 2024

@swift-ci smoke test windows

@atrick
Copy link
Contributor Author

atrick commented Jan 18, 2024

@swift-ci smoke test linux

@atrick
Copy link
Contributor Author

atrick commented Jan 18, 2024

@swift-ci smoke test windows

We have language features that depends on swift_in_compiler. Any
platform that does not support this build feature does not really
support the language, so we must continue to disable tests.
@atrick
Copy link
Contributor Author

atrick commented Jan 18, 2024

@swift-ci smoke test

@atrick atrick merged commit dd3f6ef into swiftlang:main Jan 18, 2024
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.

2 participants