First restore the basic PrunedLiveness abstraction to its original
intention. Move code outside of the basic abstraction that polutes the
abstraction and is fundamentally wrong from the perspective of the
liveness abstraction.
Most clients need to reason about live ranges, including the def
points, not just liveness based on use points. Add a PrunedLiveRange
layer of types that understand where the live range is
defined. Knowing where the live range is defined (the kill set) helps
reliably check that arbitrary points are within the boundary. This
way, the client doesn't need to be manage this on its own. We can also
support holes in the live range for non-SSA liveness. This makes it
safe and correct for the way liveness is now being used. This layer
safety handles:
- multiple defs
- instructions that are both uses and defs
- dead values
- unreachable code
- self-loops
So it's no longer the client's responsibility to check these things!
Add SSAPrunedLiveness and MultiDefPrunedLiveness to safely handle each
situation.
Split code that I can't figure out into
DiagnosticPrunedLiveness. Hopefully it will be deleted soon.
Andy some time ago already created the new API but didn't go through and update
the old occurences. I did that in this PR and then deprecated the old API. The
tree is clean, so I could just remove it, but I decided to be nicer to
downstream people by deprecating it first.
The new utility, given an phi, visits all adjacent phis (i.e. arguments
to the same block) which are (potentially iterated) reborrows of a value
reaching the given phi.
Example: consume(x.field). This turned out to be a pretty simple extension of
the underlying model. The cases we are interested in are caused by a
non-reference nominal type having an extracted field being passed to a consuming
use. This always requires a copy.
The reason I missed this was I originally wrote the test cases around this for
classes which do not have this problem since the class is move only, not the
field due to class being a reference type. I then cargo culted this test case
for struct/other types and did not notice that we should have started to error
on these tests.
On an interesting note, I caught this on my branch where I am preparing the
optimizer to allow for values to have a move only bit. One of the constraints is
that once we are in guaranteed SIL, copy_value can not be used on any moveOnly
type (e.x.: $@moveOnly T). To ensure this doesn't happen, the move only checker:
1. Uses copy propagation to rewrite the copies of the base owned value.
2. Emit a diagnostic error upon any copies we found were needed due to the owned
value being consumed.
3. If a diagnostic was emitted, rewrite all copies of move only typed values to
be explicit_copy_value to ensure that in canonical SIL we do not violate the
invariant that copy_value can not be applied to move only type values. This
is ok to do since we are going to error and just want to avoid breaking the
invariant.
The end effect of this is that if we do not emit any diagnostic, any copy_value
on a move only typed value that is not eliminated by the checker hits an assert
in the verifier... allowing us to know that if a program successfully compiles
that all "move only" proofs successfully ran, guaranteeing safety!
Previously, visitTransitiveEndBorrows took BorrowedValues. However,
there is at least one kind of borrow--namely,
unchecked_ownership_conversion insts--that is not currently permitted by
the BorrowedValue API. The long term fix is to make BorrowedValue
handle such instructions. For now, change visitTransitiveEndBorrows to
take SILValues so that unchecked_ownership_conversion can be passed to
the API.
rdar://87985420
Replaced findInnerTransitiveGuaranteeedUsesOfBorrowedValue with
findExtendedUsesOfSimpleBorrowedValue. Starting from a borrowed value,
it finds all extended (i.e., seeing through copies) uses of the borrow
and its projections within the simple (i.e. without considering
reborrowing) borrow scope.
SemanticARCOptVisitor::performGuaranteedCopyValueOptimization was
converting this SIL
%borrow = begin_borrow %copiedValue
%copy = copy_value %borrow
%borrowCopy = begin_borrow %copy
end_borrow %borrow
end_borrow %borrowCopy
destroy_value %copy
// something something
unreachable
into
%borrow = begin_borrow %copiedValue
%innerBorrow = begin_borrow %borrow
end_borrow %borrow
end_borrow %innerBorrow
// something something
unreachable
Dead-end blocks are simply irrelevant for this
optimization. Unfortunately, there were multiple layers of attempted
workarounds that were hiding the real problem, except in rare cases.
Thanks Nate Chandler for reducing the test.
During copy propagation (for which -enable-copy-propagation must still
be passed), also try to shrink borrow scopes by hoisting end_borrows
using the newly added ShrinkBorrowScope utility.
Allow end_borrow instructions to be hoisted over instructions that are
not deinit barriers for the value which is borrowed. Deinit barriers
include uses of the value, loads of memory, loads of weak references
that may be zeroed during deinit, and "synchronization points".
rdar://79149830
findInnerTransitiveUsesForAddress was incorrectly returning true for
pointer escapes.
Introduce enum AddressUseKind { NonEscaping, PointerEscape, Unknown };
Clients need to handle each of these cases differently.
Recording uses is now optional. This utility is also used simply to
check for PointerEscape. When uses are recorded, they are used to
extend a live range. There could be a large number of transitive uses
that we don't want to pass back to the caller.
This replaces the recent BorrowedAddress utility (the address may not
be borrowed!)
APIs:
AddressOwnership::hasLocalOwnershipLifetime() - convenience on top of
AccessBase::hasLocalOwnershipLifetime().
AddressOwnership::getOwnershipReferenceRoot() - convenience on top of
AccessBase::getOwnershipReferenceRoot().
AddressOwnership::findTransitiveUses() - wrapper API over the internal
helper findTransitiveUsesForAddress()
AddressOwnership::areUsesWithinLifetime() - wrapper adds a quick check
on top of BorrowedValue::areUsesWithinLifetime()
To visitExtendedScopeEndingUses.
We need to talk about the "local" scope, which does not cross
reborrows, in contrast to the extended scope. So the name was a
contradiction that could cause confusion elsewhere.
Temporary check for malformed borrow scopes. Def-use traversals expect
at least one scope-ending use. Otherwise we can end up missing the
borrow entirely and miscompiling. SIL should guarantee scope-ending
uses on all paths, but that isn't fully enforced yet.
This abstraction is heavily used to ask about the current borrow scope
enclosing some uses. Give it the functionality it needs.
Add BorrowedValue::computeLiveness(PrunedLiveness). Get the borrow
scope's live range. A trivial 4-line implementation.
Cleanup BorrowedValue::areUsesWithinScope(). Quick check to see if a set
of uses known to be dominated by the borrow are within the borrow scope.
Add BorrowedValue::hasReborrow(). Is this local borrow scope
reborrowed? If not, then it's local scope-ending operations are the
end of its required lifetime.
Simplify BorrowedValue::gatherReborrows() to use OperandOwnership::Reborrow.
Determines whether an address might be inside a borrowed scope. If so,
then any address substitution needs to find that scope boundary to
avoid violating its basic guarantee that all uses are within scope.
Don't allow an owned call argument to be considered a valid BorrowingOperand.
More generally, make sure there is a perfect equivalence between valid
BorrowingOperand and the corresponding OperandOwnership kind.
For those who are unaware, a store_borrow is an instruction that is needed so
that we can without adding ARC traffic materialize a guaranteed object value
into memory so we can pass it as an in_guaranteed argument. It has not had its
model completely implemented due to time. I am going to add some information
about it in this commit message for others.
From a model semantic perspective, a store_borrow is meant to allow for a
guaranteed value to be materialized into memory safely for a scoped region of
time and be able to guarantee that:
1. The guaranteed value that was stored is always live.
2. The memory is never written to within that region.
The natural way to model this type of guaranteeing behavior from an object to an
address is via a safe interior pointer formulation and thus with that in mind I
made it so that store_borrow returned an address that was an interior pointer of
the guaranteed value. Sadly, we have not changed the compiler yet to use this
effectively since we in store_borrow code paths generally just use the input
address. That being said, in this PR I begin to move us toward this world by
making store_borrow's src operand an InteriorPointerOperand. This means that we
will require the borrowed value to be alive at all use points of the
store_borrow result. That will not have a large effect today but I am going to
loop back around and extend that.
There is additional work here that I wish to accomplish in the future is that I
would like to define an end_store_borrow instruction to explicitly end the scope
in which there is a condition on the borrow. Then we would require that the
memory we are using to for sure never be written to in that region and maybe
additionally try to give some sort of guarantee around exclusivity (consider if
we made it so that we could only store_borrow into an alloc_stack with certain
conditions). Not sure about the last one yet. But philosophically since we are
using this to just handle reabstraction suggests we can do something more
constrained.
This visits unique pairs of reborrowPhi and its base value.
The code for the utility is mostly refactored from the ReborrowVerifier.
This utility will be used later in DCE.
1. The reason to do this is that an InteriorPointerOperand is a value type.
2. Sometimes one wants to be able to use the IntPtrOperand that is not a const &
and the value type formulation always works.
Functionality wise, this is NFCI.
...for handling borrow scopes:
- find[Extended]TransitiveGuaranteedUses
- BorrowingOperand::visit[Extended]ScopeEndingUses
- BorrowedValue BorrowingOperand::getBorrowIntroducingUserResult()
And document the logic.
Mostly NFC in this commit, but more RAUW cases should be correctly
handled now.
Particularly, ensure that we can cleanly handle all manner of
reborrows. This is necessary to ensure both CanonicalizeOSSA and
replace-all-uses higher-level utilities handle all cases.
This generalizes some of the logic in CanonicalizeOSSA so it can be
shared by other high-level OSSA utilities.
These utilities extend the fundamental BorrowingOperand and
BorrowedValue functionality that has been designed recently. It builds
on and replaces a mix of piece-meal functionality that was needed
during bootstrapping. These APIs are now consistent with the more
recently designed code. It should be obvious what they mean and how to
use them, should be very hard to use them incorrectly, and should be
as efficient as possible, since they're fundamental to other
higher-level utilities.
Most importantly, there were several very subtle correctness issues
that were not handled cleanly in a centralized way. There are now a
mix of higher-level utilities trying to use this underlying
functionality, but it was hard to tell if all those higher-level
utilities were covering all the subtle cases:
- checking for PointerEscapes everywhere we need to
- the differences between uses of borrow introducers and other
guaranteed values
- handling the uses of nested borrow scopes
- transitively handling reborrows
- the difference between nested and top-level reborrows
- forwarding destructures (which can cause exponential explosion)
In short, I was fundamentally confused and getting things wrong before
designing these utilities.
A few notes:
1. We already have provide an operator* that does this and even better makes the
conversion explicit in the source.
2. The bool operator checks both that kind is not invalid and that the operand
is non-null. This ensures we can use an `if` to properly check for a valid
BorrowingOperand.
Instead of saving BorrowingOperand on the context save the SILBasicBlock
and index of the terminator operand.
This avoids the use-after-free in eliminateReborrowsOfRecursiveBorrows.
Previously, eliminateReborrowsOfRecursiveBorrows called helper
insertOwnedBaseValueAlongBranchEdge, which can delete a branch
instruction (reborrow) that could have been cached in
recursiveReborrows.
Add support for interleaved borrow scopes:
%b1 = begin_borrow %a
%c = copy
%b2 = begin_borrow %b1
end_borrow %b1
use %c
end_borrow %b2
Will be transformed to:
%c = copy %a
%b1 = begin_borrow %a
%b2 = begin_borrow %b1
end_borrow %b1
use %c
end_borrow %b2
This was the original intention but the implementation was incomplete.
This option can be enabled as soon as we need it for performance.
The intention is also to handle multi-block borrows, but that hasn't
been implemented.
In OSSA, we enforce that addresses from interior pointer instructions are scoped
within a borrow scope. This means that it is invalid to use such an address
outside of its parent borrow scope and as a result one can not just RAUW an
address value by a dominating address value since the latter may be invalid at
the former. I foresee that I am going to have to solve this problem and so I
decided to write this API to handle the vast majority of cases.
The way this API works is that it:
1. Computes an access path with base for the new value. If we do not have a base
value and a valid access path with root, we bail.
2. Then we check if our base value is the result of an interior pointer
instruction. If it isn't, we are immediately done and can RAUW without further
delay.
3. If we do have an interior pointer instruction, we see if the immediate
guaranteed value we projected from has a single borrow introducer value. If not,
we bail. I think this is reasonable since with time, all guaranteed values will
always only have a single borrow introducing value (once struct, tuple,
destructure_struct, destructure_tuple become reborrows).
4. Then we gather up all inner uses of our access path. If for some reason that
fails, we bail.
5. Then we see if all of those uses are within our borrow scope. If so, we can
RAUW without any further worry.
6. Otherwise, we perform a copy+borrow of our interior pointer's operand value
at the interior pointer, create a copy of the interior pointer instruction upon
this new borrow and then RAUW oldValue with that instead. By construction all
uses of oldValue will be within this new interior pointer scope.