Migrating to this classification was made easy by the recent rewrite
of the OSSA constraint model. It's also consistent with
instruction-level abstractions for working with different kinds of
OperandOwnership that are being designed.
This classification vastly simplifies OSSA passes that rewrite OSSA
live ranges, making it straightforward to reason about completeness
and correctness. It will allow a simple utility to canonicalize OSSA
live ranges on-the-fly.
This avoids the need for OSSA-based utilities and passes to hard-code
SIL opcodes. This will allow several of those unmaintainable pieces of
code to be replaced with a trivial OperandOwnership check.
It's extremely important for SIL maintainers to see a list of all SIL
opcodes associated with a simple OSSA classification and set of
well-specified rules for each opcode class, without needing to guess
or reverse-engineer the meaning from the implementation. This
classification does that while eliminating a pile of unreadable
macros.
This classification system is the model that CopyPropagation was
initially designed to use. Now, rather than relying on a separate
pass, a simple, lightweight utility will canonicalize OSSA
live ranges.
The major problem with writing optimizations based on OperandOwnership
is that some operations don't follow structural OSSA requirements,
such as project_box and unchecked_ownership_conversion. Those are
classified as PointerEscape which prevents the compiler from reasoning
about, or rewriting the OSSA live range.
Functional Changes:
As a side effect, this corrects many operand constraints that should
in fact require trivial operand values.
I think what was happening here was that we were using one of the superclass
classofs and were getting lucky since in the place I was using this I was
guaranteed to have single value instructions and that is what I wrote as my
first case X ).
I also added more robust checks tieing the older isGuaranteed...* APIs to the
ForwardingOperand API. I also eliminated the notion of Branch being an owned
forwarding instruction. We only used this in one place in the compiler (when
finding owned value introducers), yet we treat a phi as an introducer, so we
would never hit a branch in our search since we would stop at the phi argument.
The bigger picture here is that this means that all "forwarding instructions"
either forward ownership for everything or for everything but owned/unowned.
And for those listening in, I did find one instruction that was from an
ownership forwarding subclass but was not marked as forwarding:
DifferentiableFunctionInst. With this change, we can no longer by mistake have
such errors enter the code base.
A reborrow occurs when a Borrowing Operand ends the lifetime of a borrowed value
and propagates forward a new guaranteed value that continues the guaranteed
lifetime of the value.
This makes it easier to understand conceptually why a ValueOwnershipKind with
Any ownership is invalid and also allowed me to explicitly document the lattice
that relates ownership constraints/value ownership kinds.
This makes it clearer that isConsumingUse() is not an owned oriented API and
returns also for instructions that end the lifetime of guaranteed values like
end_borrow.
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.
My upcoming SILGen change introduces more stores directly into
the result of a ref_element_addr or struct_element_addr in some
cases. Make sure we handle the failing combinations flagged by
the ownership verifier.
I think validating this was an oversight from the bringup of coroutines. I
discovered this while writing test cases for coroutine lifetime extension. I
realized it was possible to write a test case that should have triggered this
but was not.
I added some tests to make sure that we continue to flag this in the future.
rdar://69597888
When bringing up ownership I thought that it might be useful to distinguish
"normal users" that just require liveness and are real transitive users vs
implicit users that require liveness (I provide a constrasting example
below). Turns out this distinction did not provide any benefit, so I am ripping
it out so I can refactor this code to be used in other parts of the compiler
where we only want a single array of "normal users".
This is just a pure refactor. NFCI.
--------------------------------------------------------------------------------
An example of the former would be an apply that uses an owned value as a
guaranteed parameter:
```
bb0(%0 : @owned $Klass):
apply %func(%0) : $@convention(thin) (@guaranteed $Klass) -> ()
```
while an example of the latter is an end_apply of a coroutine that takes an
owned value as a guaranteed parameter:
```
bb0(%0 : @owned $Klass):
(%result, %token) = begin_apply %func(%0)
...
end_apply %token
destroy_value %0
```
In the latter, we can not move the destroy_value past the end_apply.
I am currently working on updating SimplifyCFG for ownership. One thing that I
am finding that I need is the ability to compute the lifetime of a guaranteed
argument from a checked_cast_br/switch_enum in order to convert said
instructions to a br. The issue comes from br acting as a consuming (lifetime
ending) use of a borrowed value, unlike checked_cast_br/switch_enum (which are
treated like forwarding instructions). This means that during the conversion we
need to insert a begin_borrow on the value before the new br instruction and
then create an end_borrow at the end of the successor block's argument's
lifetime.
By refactoring out this code from the ownership verifier, I can guarantee that
said lifetime computation will always match what the ownership verifier does
internally preventing them from getting out of sync.
This will just make them easier to update over time since adding a new function
doesn't require one to renumber the /entire/ file... *face-palm*.
Originally the reason why I added this is b/c I need to ensure that we handle
all errors exactly once so making sure we control the exact amount of emitted
errors is important. I can still do this with the new approach by just doing
per-function max error counts. Thus I also in this commit added FileCheck
patterns that implemented this scheme so now we have everything.
This will make it easier for me with a few further refactors to make the
ownership verifier testing mode emit per function error numbers instead of the
global error number that it is emitting now.
The reason why this is necessary is that today, the verification by
-sil-verify-all causes the errors to be emitted. That verification is done on a
per value level, rather than a per function level, so it is hard to get per
function error numbers without doing unprincipled things like propagating around
state saying what the current function being verified is.
This pass instead will let me make the error counter be per ErrorBuilder which
are created per function.
One thing to be aware of is that this /will/ cause SILValue::verifyOwnership to
not emit any output when the testing flag is enabled. This is to ensure I only
do not get duplicate textual error messages from the SILVerifier.
This is doing a few things with a simple change to use a builder:
1. It cleans up how we emit errors so we have a builder object that constructs
errors. The errors then just become dumb POD data that the builder vends to
callers that via the boolean values describe what errors were found.
2. Now that we have one place where we are actually emitting these errors, I
cleaned up how we emit the errors by normalizing the output so function names
are quoted the same.
3. I changed our error emission so that we emit a unique count of the errors as
we emit them. This makes it so that our pattern matching is much more robust
against weird pattern match errors that can be difficult to debug due to the
errors having unrelated test cases/file check patterns bleed
together. Before/end checks eliminate this problem. I updated all of the
relevant test cases.
The reason /why/ I am doing this though is that I am going to be adding support
to the LinearLifetimeChecker for flagging objects that are outside of the
lifetime that we are verifying (meaning either before or after). This is going
to cause me to need to track /all/ non consuming uses when performing linear
lifetime checks and thus most likely emit more errors. I was finding it to be
difficult to update the current tests given the state of the world before this
patch, so I was inspired to clean this up to satisfy practical as well as debt
concerns.
Specifically, I split it into 3 initial categories: IR, Utils, Verifier. I just
did this quickly, we can always split it more later if we want.
I followed the model that we use in SILOptimizer: ./lib/SIL/CMakeLists.txt vends
a macro (sil_register_sources) to the sub-folders that register the sources of
the subdirectory with a global state variable that ./lib/SIL/CMakeLists.txt
defines. Then after including those subdirs, the parent cmake declares the SIL
library. So the output is the same, but we have the flexibility of having
subdirectories to categorize source files.