Specifically, instead of what I call an IsolationRegionInfo + Type error, e.x.:
> task-isolated value of type 'NonSendableType' passed as a strongly transferred parameter; later accesses could race
we use instead the standard warning of:
```swift
func transfer(_ x: transferring Type) {}
func test(_ x: Type) {
transfer(x) // expected-warning {{transferring 'x' may cause a race}}
// expected-note @-1 {{task-isolated 'x' is passed as a transferring parameter; Uses in callee may race with later task-isolated uses}}
}
```
Implementation wise, I left in the old type diagnostic as a backup for now. With
time, I am going to want to eliminate it completely.
Now that we actually know the region that non transferrable things belong to, we
can use this information to give a better diagnostic here.
A really nice effect of this is that we now emit that actor isolated parameters
are actually actor isolated instead of task isolated.
In a subsequent commit, this is going to let me begin handling parameters with
actor regions in a nice way (and standardize all of the errors).
This is meant to be a refactoring commit that uses the current tests in tree to
make sure I did it correctly, so no tests need to be updated.
To keep this as an NFC commit, I only modeled initially actor isolated using
this. I am going to make it so that we properly treat global actor isolated
values as actor isolated/etc in a subsequent commit.
This makes it easier to debug/maintain/understand the code since the place that
emits the diagnostics is no longer split from the place that decides if a
diagnostic should be emitted. It also lets me eliminate the utility data
structure used to transfer data over.
Specifically, we previously would generate a list of errors to emit and then
have one centralized piece of code that went through the list and emitted them.
Instead, we now have a low level emitter struct that is just used to emit the
diagnostic at the immediate point where we would create previously an error
record. This places the diagnostic emission next to the piece of code that
triggered it being emitted making it easier to debug why an error was emitted
using -Xllvm -swift-diagnostics-assert-on-*.
I am going to do the same for transfer non transferrable in a little bit once
the async let patch has landed. It has some transfer non transferable error
additions that I want to get in so I am going to wait on that.
NFCI.
When we run RegionAnalysis, since it uses RPO order, we do not visit dead
blocks. This can create a problem when we emit diagnostics since we may merge in
a value into the region that was never actually defined. In this patch, if we
actually visit the block while performing dataflow, I mark a bit in its state
saying that it was live. Then when we emit diagnostics, I do not visit blocks
that were not marked live.
rdar://124042351
This just cleans up the code by not exposing the details from the
VariableNameInferrer outside of the utilities. It also makes it easier to write
conditional code that uses these helpers by returning an optional, so instead of
having a long setup + an if statement, we just have an if statement optional
check.
I just did a full pass through. There were some cases around nonisolated
closures defined in methods and global actor isolated things where we are now
emitting the wrong message. I am going to fix that in subsequent commits.
NFC.
Before this commit, the diagnostic emission was split into utilities that were
used by one large emitter class. This could get confusing since there was some
duplication of names in between the utilities. Now instead, each part of the
main diagnostic emitter is in the MARK: section of the utilities that it uses.
Just making the pass easier for me to reason about.
We purposely do not treat a PartitionOpKind::Transfer as a require since that
would cause us to error when we weakly transfer the same parameter multiple
times to the same function. This is safe since all of the function parameters
will be in the same region.
To ensure that this fixed the multiple strong transferring issue (which is
exposed by requiring before transferring), I also had to muck around a little
with how we emit errors to ensure that we emit errors if the transfer
instruction is also the require.
A name diagnostic looks as follows:
Some notes:
1. VariableNameUtils still doesn't pattern match all cases. In the cases where
we fail to pattern match, I fall back to the old diagnostics. I am going to be
adding tests/etc specific to VariableNameUtils in a later patch.
2. I took this as an opportunity to begin preparing to change the diagnostics
into diags of the following form:
a. The main diagnostic is changed to something short "transferring non-Sendable
%0 could yield races with later accesses".
b. I added a longer note at the same location that explains that our caller is
in a specific isolation domain and our callee is isolated differently.
c. I added a note on the definition location of the identified value.
I also while doing this I replaced a bunch of places where we used to crash due
to invariants failing to instead emit a "I don't know error" since it is more
actionable for the user.
This involves me changing where we emit the error. Instead of us emitting the
error early in the evaluator, we emit it later when we emit diagnostics for all
other transferring things.
This is NFCI.
This commit makes it so that we treat values captured by an actor isolated
closure as being transferred to that closure. I also introduced a new diagnostic
for these warnings that puts the main warning on the capture point of the value
so the user is able to see the actual capture that causes the transfer to occur:
```swift
nonisolated func testLocal2() async {
let l = NonSendableKlass()
// This is not safe since we use l later.
self.assumeIsolated { isolatedSelf in
isolatedSelf.ns = l
}
useValue(l) // expected-note {{access here could race}}
}
```
```
test.swift:74:14: warning: main actor-isolated closure captures value of non-Sendable type 'NonSendableKlass' from nonisolated context; later accesses to value could race
useValue(x) // expected-warning {{main actor-isolated closure captures value of non-Sendable type 'NonSendableKlass' from nonisolated context; later accesses to value could race}}
^
test.swift:76:12: note: access here could race
useValue(x) // expected-note {{access here could race}}
^
```
One thing to keep in mind is that if we have a function argument being captured
in this way, we still emit the "call site passes `self`" error. I am going to
begin cleaning that up in the next commit in this PR so that we emit a better
error here. But it makes sense to split these into two separate commits since
they are doing different things.
rdar://121345525
Before the previous patch, we were just getting lucky on macOS due to UB. Now
that the UB is fixed, we correctly crash without this commit since we were not
pattern matching the simple case of a local value that was transferred and used
later.
llvm::Optional<T> used to make it so that in asserts builds if one dereferenced the optional and nothing was there, one would get an assert. std::optional<T> does not have that property.
The specific semantics is if we assign into a transferring parameter's field,
then we "merge" src's value into the transferring parameter, so we
conservatively leave the region of the transferring parameter alone. If we
assign over the entire transferring parameter, we perform an assign fresh since
any value that used to be in the transferring parameter cannot reference
anything in its new value since they are all gone.
Before this commit, this was done at the beginning of TransferNonSendable. I
thought that those checks would be sufficient to ensure that
RegionAnalysisFunctionInfo was not created for functions that we do not
support. Turns out when we perform certain forms of verification, we force all
function analyses to be created for all functions meaning that we would create a
RegionAnalysisFunctionInfo for such an unsupported function causing us to hit
asserts.
In this commit, I move the check to whether or not we support a function into
RegionAnalysisFunctionInfo itself and use that to determine if we should run
TransferNonSendable. This additionally allows me to change
RegionAnalysisFunctionInfo so that one can construct one for an unsupported
function... as long as one doesn't actually touch any of its methods. If one
does, I put in an assert so we will know that operator error has occured.
NFCI. This is just a pure refactor of the analysis part of TransferNonSendable
into a separate SIL level analysis so it can be reused by other passes.
The reason that I am committing this earlier is that I am working concurrently
on other patches that change TransferNonSendable itself and I want to avoid
issues when rebasing those patches. Getting this patch into tree earlier avoids
that.
This is in preparation for adding a new flow sensitive initialization pass that
combines region based analysis with the current flow sensitive isolation's
diagnostic emitter. The idea is that we want to preserve the diagnostics from
that pass rather than try to make our own as an initial step.
NOTE: This required me to stop using swift::getUnderlyingObject from
getUnderlyingTrackedObject since when it stripsCasts it looks through
unchecked_trivial_bit_cast... but we only want to do that if both the operand
and result of the instruction are non-Sendable. To fix this I inlined
getUnderlyingObject's impl and removed that part of stripCasts.
NOTE: I am just adding coverage that we support these instructions. One can only
use this with Error today and Error is always Sendable. So this is just going
for completeness.
Semantically a mark_dependence returns a value that is equal to its first
parameter with the extra semantics that any destroys of the 2nd operand cannot
occur before any uses of the result of the instruction. From a region
perspective this suggests that the instruction should be an assign from the
first operand onto the result and act as a require on the result. Semantically
the requirement that the 2nd operand cannot be destroyed before any uses of the
result does not expose any memory or state from the first operand implying that
we don't need to merge it into the result region. The restriction is purely to
tell the optimizer what it can/cannot do rather.
We already only supported Ownership SSA since we run early in the pipeline
before OSSA is lowered. This just formalizes this behavior. I am marking these
instructions as Asserting (even though we will never see them) so I can
semantically be sure that all of the instructions are covered without using an
"unsupported" like moniker that I fear will lead to new instructions being added
as unsupported. Better to have a semantic thing for new instruction adders to
use.
This involved fixing a few different bugs.
1. We were just performing dataflow by setting that only the initial block needs
to be updated. This means that if there isn’t anything in the initial dataflow
block, we won’t visit any successor blocks. Instead the correct thing to do here
is to visit all blocks in the initial round.
2. I also needed to fix a separate issue where we were updating our union-find
data structure incorrectly as found by an assert on transfernonsendable.swift
that was triggered once I fixed 1. Put simply, we needed to set a new max label
+ 1 when our new max element is less than or equal to the old max label + 1…
before we just did less than so if we had a new max element that is the same as
our fresh label, we wouldn’t increment the fresh label.
rdar://119584497