-
Notifications
You must be signed in to change notification settings - Fork 10.5k
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
Conversation
@swift-ci test |
42a7cb9
to
b8df083
Compare
c46e94a
to
94d1b39
Compare
@swift-ci test |
This PR is blocked by a likely recent LoadableByAddress bug that happens to be exposed. |
94d1b39
to
68527d6
Compare
@swift-ci 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.
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: |
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 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: |
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.
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?
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 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.
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.
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.
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 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?
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.
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.
SwiftCompilerSources/Sources/Optimizer/Utilities/ForwardingUtils.swift
Outdated
Show resolved
Hide resolved
SwiftCompilerSources/Sources/Optimizer/Utilities/ForwardingUtils.swift
Outdated
Show resolved
Hide resolved
/// | ||
/// TODO: Verify that pointerEscape is only called for live ranges in which | ||
/// `findPointerEscape()` returns true. | ||
protocol AddressUseVisitor { |
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.
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,
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 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.
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.
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.
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 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: |
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 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.
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 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.
SwiftCompilerSources/Sources/Optimizer/Utilities/OwnershipLiveness.swift
Outdated
Show resolved
Hide resolved
/// result and implement this as a fatalError. | ||
mutating func visitPointerEscape(use: Operand) -> WalkResult | ||
|
||
/// Handle begin_borrow, load_borrow, store_borrow, begin_apply. |
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'm completely confused by this comment.
} | ||
} | ||
|
||
/// Enumerate all special cases of ownership uses in a visitor |
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'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.
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.
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.
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 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.
68527d6
to
7d19418
Compare
@swift-ci test |
return | ||
} | ||
// avoid duplicates in the enclosingValues set. | ||
var pushedEnclosingValues = ValueSet(context) |
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 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.
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.
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 |
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.
-> borrow introducer for %2
// %5 = owned value | ||
// %6 = begin_borrow %5 %6 %5 | ||
// br bb3(%5, %6) | ||
// bb3(%phi: @owned, invalid %0 |
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.
%0
-> none
(?)
switch self { | ||
case .beginBorrow, .storeBorrow: | ||
let svi = instruction as! SingleValueInstruction | ||
return svi.uses.walk { |
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 can also write svi.uses.filterUsers(ofType: EndBorrowInst.self).walk ...
// cond_br ..., bb1, bb2 | ||
// bb1: | ||
// %1 = owned value | ||
// %2 = begin_borrow %0 %2 %1 |
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.
%0
-> %1
// %6 = begin_borrow %0 %6 %0 | ||
// %7 = struct (%6) %6 %6 | ||
// br bb3(%6, %7) | ||
// bb3(%reborrow: @guaranteed, %reborrow %0 |
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.
Aren't we already printing reborrows with a @reborrow
attribute (instead of @guaranteed
)?
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 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 |
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 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 |
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.
shouldn't this be an outer use of %0
?
} | ||
} | ||
|
||
/// Visit only those uses of an value within an inner borrow scope |
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 value
7d19418
to
320d798
Compare
@swift-ci 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.
lgtm!
case let arg as Argument where arg.isReborrow: | ||
self = .reborrow(Phi(arg)!) | ||
default: | ||
if value.definingInstruction is BeginApplyInst { |
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.
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.
58f670d
to
8aa6833
Compare
@swift-ci smoke test |
8aa6833
to
8341b73
Compare
@swift-ci smoke test |
@swift-ci smoke test linux |
@swift-ci smoke test windows |
@swift-ci smoke test linux |
@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.
@swift-ci smoke test |
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